idiotWu / smooth-scrollbar

Customizable, Extendable, and High-Performance JavaScript-Based Scrollbar Solution.

Home Page:https://idiotwu.github.io/smooth-scrollbar/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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;
  }>;