Sticky header of scrolled content
BogdanGorelkin opened this issue · comments
Motivation
I am using smooth-scrollbar in react application.
I need to have an header of scrolled content at the top of SmoothScrollbar container, however position: 'sticky'
doesn't work.
Proposal
Here you can find link to codesandbox of the problem I am straggling.
App.tsx
import React from "react";
import SmoothScrollbar from "./SmoothScrollbar/SmoothScrollbar";
import { ScrollbarPlugin } from "./SmoothScrollbar/types";
import { OverscrollEffect } from "smooth-scrollbar/plugins/overscroll";
import "./styles.css";
const containerBg = (i: number) => `hsl(${i * 40}, 70%, 90%)`;
const headerBg = (i: number) => `hsl(${i * 40}, 70%, 50%)`;
export default function App() {
return (
<div className="App">
<SmoothScrollbar
plugins={
{ overscroll: { effect: OverscrollEffect.BOUNCE } } as ScrollbarPlugin
}
style={{ maxHeight: "100vh" }}
>
<div
style={{
height: "60vh",
overflowY: "auto",
position: "relative",
zIndex: 1,
}}
>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].map((i) => (
<div key={i} style={{ background: containerBg(i), height: "20em" }}>
<header
style={{ background: headerBg(i), position: "sticky", top: 0 }}
>
Header {i}
</header>
<h2 style={{ height: "20em" }}>{`some data inside container`}</h2>
</div>
))}
</div>
</SmoothScrollbar>
</div>
);
}
SmootScrollbar.tsx
import React, {
createElement,
cloneElement,
forwardRef,
isValidElement,
useEffect,
useCallback,
useRef,
} from "react";
import SmoothScrollbar from "smooth-scrollbar";
import type { Scrollbar } from "smooth-scrollbar/scrollbar";
import type { ScrollStatus } from "smooth-scrollbar/interfaces";
import { ScrollbarProps } from "./types";
const SmoothScrollbarReact = forwardRef<Scrollbar, ScrollbarProps>(
function SmoothScrollbarReact(
{ children, className, style, ...restProps },
ref,
) {
const mountedRef = useRef(false);
const scrollbar = useRef<Scrollbar>(null!);
const handleScroll = useCallback<(status: ScrollStatus) => void>(
(status) => {
if (typeof restProps.onScroll === "function") {
restProps.onScroll(status, scrollbar.current);
}
},
[restProps.onScroll],
);
const containerRef = useCallback((node: HTMLElement) => {
if (node instanceof HTMLElement) {
(async () => {
if (restProps.plugins?.overscroll) {
const { default: OverscrollPlugin } = await import(
"smooth-scrollbar/plugins/overscroll"
);
SmoothScrollbar.use(OverscrollPlugin);
}
scrollbar.current = SmoothScrollbar.init(node, restProps);
scrollbar.current.addListener(handleScroll);
})();
}
}, []);
useEffect(() => {
if (ref) {
(ref as React.MutableRefObject<Scrollbar>).current = scrollbar.current;
}
}, [scrollbar.current]);
useEffect(() => {
return () => {
if (scrollbar.current) {
scrollbar.current.removeListener(handleScroll);
scrollbar.current.destroy();
}
};
}, []);
useEffect(() => {
if (mountedRef.current === true) {
if (scrollbar.current) {
Object.keys(restProps).forEach((key) => {
if (!(key in scrollbar.current.options)) {
return;
}
if (key === "plugins") {
Object.keys(restProps.plugins).forEach((pluginName) => {
scrollbar.current.updatePluginOptions(
pluginName,
restProps.plugins[pluginName],
);
});
} else {
// @ts-expect-error
scrollbar.current.options[key] = restProps[key];
}
});
scrollbar.current.update();
}
} else {
mountedRef.current = true;
}
}, [restProps]);
if (isValidElement(children)) {
return cloneElement(children as React.ReactElement, {
ref: containerRef,
className:
(children.props.className ? `${children.props.className} ` : "") +
className,
style: {
...style,
...children.props.style,
},
});
}
return createElement(
"div",
{
ref: containerRef,
className,
style: {
...style,
WebkitBoxFlex: 1,
msFlex: 1,
MozFlex: 1,
flex: 1,
//overflow: 'auto', //if uncommented - sticky works, but smooth scrollbar breaks
},
},
createElement(
"div",
{
className,
},
children,
),
);
},
);
export default SmoothScrollbarReact;
types.ts
import type { Scrollbar } from "smooth-scrollbar/scrollbar";
import type {
ScrollbarOptions,
ScrollStatus,
} from "smooth-scrollbar/interfaces";
import type {
OverscrollOptions,
OverscrollEffect,
} from "smooth-scrollbar/plugins/overscroll";
export interface ScrollbarPlugin extends Record<string, unknown> {
overscroll?: Partial<Omit<OverscrollOptions, "effect">> & {
effect?: OverscrollEffect;
};
}
export type ScrollbarProps = Partial<ScrollbarOptions> &
React.PropsWithChildren<{
className?: string;
style?: React.CSSProperties;
plugins?: ScrollbarPlugin;
onScroll?: (status: ScrollStatus, scrollbar: Scrollbar | null) => void;
}>;