ui/ux on catalogue
This commit is contained in:
59
src/components/StickyRow.tsx
Normal file
59
src/components/StickyRow.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
||||
|
||||
/**
|
||||
* A row of page controls held under the nav bar while the page scrolls.
|
||||
*
|
||||
* Whatever sits above a page — its view tabs, its search, its action buttons —
|
||||
* used to leave with the scroll. Reaching the bottom of a long list put every
|
||||
* control over that list off the top of the window, and the only way back to
|
||||
* them was to scroll back through everything just read. The further in somebody
|
||||
* was, the further from the controls.
|
||||
*
|
||||
* The page still scrolls normally. Nothing here gets a scrollbar of its own —
|
||||
* the row simply stays where it is. Positioning and the canvas it paints live
|
||||
* in `.page-sticky`; this component exists for the one part CSS cannot answer,
|
||||
* which is whether the row is currently pinned.
|
||||
*
|
||||
* `PageHeader` wraps itself in this, so every page built from it gets the
|
||||
* behaviour without asking. Dispatch builds its own switcher rather than going
|
||||
* through `PageHeader`, so it wraps itself — which is the reason this is a
|
||||
* component and not four more lines inside the header.
|
||||
*/
|
||||
export function StickyRow({ children }: { children: ReactNode }) {
|
||||
/**
|
||||
* Pinned or not, so the hairline can be drawn only when it is.
|
||||
*
|
||||
* At rest the nav bar's own hairline sits just above this one, and two
|
||||
* parallel lines across the page with nothing between them read as a mistake.
|
||||
* The line is what separates the row from content passing UNDER it, so it
|
||||
* belongs to the pinned state rather than to the row.
|
||||
*
|
||||
* Watched with an observer rather than a scroll handler: a scroll listener
|
||||
* runs on every frame of every scroll to answer a question that changes
|
||||
* twice. The negative top margin puts the observer's boundary a pixel above
|
||||
* where the row comes to rest, so "no longer fully inside that box" means
|
||||
* exactly "stuck".
|
||||
*/
|
||||
const rowRef = useRef<HTMLDivElement>(null);
|
||||
const [isStuck, setIsStuck] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const element = rowRef.current;
|
||||
/* Guarded for any renderer without it — the row still sticks, it just
|
||||
never grows its line, which is a missing hairline rather than a crash. */
|
||||
if (!element || typeof IntersectionObserver === 'undefined') return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => setIsStuck(Boolean(entry && !entry.isIntersecting)),
|
||||
{ rootMargin: '-57px 0px 0px 0px', threshold: 1 },
|
||||
);
|
||||
observer.observe(element);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={rowRef} className="page-sticky" data-stuck={isStuck ? 'true' : 'false'}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user