Search code examples
reactjsscroll

How to scroll after render in React?


I am trying to make an accordion component that is similar to the <details><summary> HTML elements.

I want to make my own component because I want to use useState() to control whether my accordion is open or not.

First I tried this:

const WeirdAlAccordion: React.FC<MyProps> = ({
  children,
}: MyProps) => {
  const scrollingRef = useRef(null);
  const [isToggleOpen, setIsToggleOpen] = useState(false);

  const scrollMyRef = () => {
    // Only scroll into view if it is being opened.
    if (!isToggleOpen) {
      scrollingRef.current.scrollIntoView({ block: 'start', inline: 'nearest' });
    }
  };

  return (
    <>
      <div id="my-id" ref={scrollingRef}>
        <IonItem>
          <HandednessToggle
            checked={isToggleOpen}
            onClick={() => {
              setIsToggleOpen(!isToggleOpen);
              scrollMyRef();
            }}
            label={"My Label"}
            // Disable the toggle on click while in review mode.
            disabled={isAccordionKeyDisabled}
          />
        </IonItem>
        {isToggleOpen && children}
      </div>
    </>
  );
};

This scrolls the element into view, but if children contains a lot of text (for example, two screens' worth of text), the element that is scrolled into view won't be scrolled until it is at the top of the screen-- it could be halfway down the screen, or three-quarters; the placement seems a bit random.

What I want to happen: After clicking the toggle, I want the content to be expanded (children are shown with isToggleOpen) and then I want to scroll so that scrollingRef is at the top of the screen.


Solution

  • According to your request " I want to scroll so that scrollingRef is at the top of the screen", I modified your code to have the desired behaviour.

    import { useMemo, useRef, useState } from "react";
    import _ from "lodash";
    
    type MyProps = any;
    
    export const WeirdAlAccordion: React.FC<MyProps> = ({ children }: MyProps) => {
      const scrollingRef = useRef<HTMLDivElement | null>(null);
      const [isToggleOpen, setIsToggleOpen] = useState(false);
    
      const scrollMyRef = () => {
        // Only scroll into view if it is being opened.
        if (!isToggleOpen && scrollingRef?.current !== null) {
          scrollingRef.current.scrollIntoView({
            block: "start",
            inline: "nearest",
          });
        }
      };
      const mimicChildren = useMemo(() => {
        const ChildrenBasicArray = Array.from({ length: 10 }, (value, index) => ({
          value: index,
          id: _.uniqueId(),
        }));
        return ChildrenBasicArray.map((item, index) => (
          <div
            key={item.id}
            style={{
              height: "120px",
              backgroundColor: index % 2 === 0 ? "red" : "blue",
            }}
          >
            {item.value}
          </div>
        ));
      }, []);
      return (
        <>
          <div style={{ height: "300px" }}>fake element</div>
          <div style={{ height: "300px" }}>fake element</div>
          <div
            style={{
              height: isToggleOpen ? "600px" : "20px",
              width: "300px",
              overflow: isToggleOpen ? "scroll" : "visible",
              background: "#ccc",
            }}
            ref={scrollingRef}
            onClick={() => {
              setIsToggleOpen(!isToggleOpen);
              scrollMyRef();
            }}
          >
            {isToggleOpen && mimicChildren}
          </div>
          <div style={{ height: "300px" }}>fake element</div>
          <div style={{ height: "300px" }}>fake element</div>
          <div style={{ height: "300px" }}>fake element</div>
        </>
      );
    };