Thursday, November 10, 2022

sticky headers for sub-parts of page in react

 We had a request to have a sticky header for a sub-part of our page, so that as you scrolled down through a long job listing you would always see the "apply" button. (this is on a complex page with job listing cards on the left and the current job being viewed on the right.)

Frankly we overthought; looked at competitor sites, some of them were playing games with changing the heights of parent containers, or letting the big button header scroll up  but then pushing in a smaller mini version of the header. We thought the solution might need Intersection Observer, or maybe switch to "position:fixed" under conditions or some such, but I think we found something pretty cheap and cheerful that does the job. 

The trick is using "useRef" to get the dom element for the (position: relative) header, getting the parent of the "current" ref, using getBoundingClientRect() on the header and the parent to do the appropriate positioning math, and useEffect to set up and tear down the event listner.

End result for the Proof of Concept:

  const headerRef = useRef<HTMLDivElement>(null)

  const adjustHeaderPosition = (headerRef?: HTMLDivElement | null) => {
    if(! headerRef) return;
    const headerRect = headerRef?.getBoundingClientRect();
    const headerHeight = headerRect?.height || 0;
    const parentRect = headerRef?.parentElement?.getBoundingClientRect();
    const parentTop = parentRect?.top || 0;
    const parentHeight = parentRect?.height || 0;
    
    if(parentTop !== undefined && parentTop < 0) {
      let topLocation = parentTop * -1;
      if(topLocation + headerHeight > parentHeight) {
        topLocation = parentHeight - headerHeight;
      }
      headerRef.style.top =`${topLocation}px`;
    } else {
      headerRef.style.top =`0px`;
    }
  }

  useEffect(() => {
    function onScroll() {
      const header = headerRef?.current;
      adjustHeaderPosition(header);
    }
    window.addEventListener("scroll", onScroll);
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  return (
    <JobViewHeaderContainer>
    <JobViewHeaderContainer ref={headerRef}>

(the code is a little careful so as to avoid lint "this could be null!" warnings)

So it makes sure the bottom of the header doesn't go beyond the bottom of parent, and the overall effect is decent.

No comments:

Post a Comment