Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sticky header of scrolled content #557

Open
BogdanGorelkin opened this issue Feb 21, 2024 · 0 comments
Open

Sticky header of scrolled content #557

BogdanGorelkin opened this issue Feb 21, 2024 · 0 comments
Labels

Comments

@BogdanGorelkin
Copy link

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;
  }>;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant