fix(ui): smooth desktop sidebar sticky transitions (#2528)
* fix(ui): blend vibrant sidebar transitions * fix(ui): fade sidebar content below header * fix(ui): smooth vibrant sidebar sticky headers * fix(ui): scope sticky fade to desktop * fix(ui): respect sticky header preference
This commit is contained in:
@@ -7,78 +7,43 @@ type Args = {
|
||||
projectHeaderSentinelRefs: React.MutableRefObject<Map<string, HTMLDivElement | null>>;
|
||||
};
|
||||
|
||||
type StickyHeaderArgs = {
|
||||
enabled: boolean;
|
||||
isDesktopShellRuntime: boolean;
|
||||
};
|
||||
|
||||
export const useStickyHeader = (args: StickyHeaderArgs) => {
|
||||
const { enabled, isDesktopShellRuntime } = args;
|
||||
const [sentinel, setSentinel] = React.useState<HTMLDivElement | null>(null);
|
||||
const [isStuck, setIsStuck] = React.useState(false);
|
||||
|
||||
const sentinelRef = React.useCallback((node: HTMLDivElement | null) => {
|
||||
setSentinel(node);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!enabled || !isDesktopShellRuntime || !sentinel) {
|
||||
setIsStuck(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
setIsStuck(entry.intersectionRatio < 1);
|
||||
}, { threshold: 1 });
|
||||
|
||||
observer.observe(sentinel);
|
||||
return () => observer.disconnect();
|
||||
}, [enabled, isDesktopShellRuntime, sentinel]);
|
||||
|
||||
return { isStuck, sentinelRef };
|
||||
};
|
||||
|
||||
export const useStickyProjectHeaders = (args: Args): Set<string> => {
|
||||
const { enabled = true, isDesktopShellRuntime, projectSections, projectHeaderSentinelRefs } = args;
|
||||
const [stuckProjectHeaders, setStuckProjectHeaders] = React.useState<Set<string>>(new Set());
|
||||
|
||||
React.useEffect(() => {
|
||||
if (enabled && isDesktopShellRuntime) {
|
||||
if (!enabled || !isDesktopShellRuntime) {
|
||||
setStuckProjectHeaders((prev) => (prev.size === 0 ? prev : new Set()));
|
||||
return;
|
||||
}
|
||||
|
||||
setStuckProjectHeaders((prev) => (prev.size === 0 ? prev : new Set()));
|
||||
}, [enabled, isDesktopShellRuntime]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!enabled || !isDesktopShellRuntime) {
|
||||
const firstSentinel = Array.from(projectHeaderSentinelRefs.current.values()).find((el) => el !== null);
|
||||
const root = firstSentinel?.closest<HTMLElement>('.oc-sidebar-scroller') ?? null;
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const projectId = (entry.target as HTMLElement).dataset.projectId;
|
||||
if (!projectId) {
|
||||
return;
|
||||
setStuckProjectHeaders((prev) => {
|
||||
const next = new Set(prev);
|
||||
let changed = false;
|
||||
for (const entry of entries) {
|
||||
const projectId = (entry.target as HTMLElement).dataset.projectId;
|
||||
if (!projectId) continue;
|
||||
|
||||
const rootTop = entry.rootBounds?.top ?? root.getBoundingClientRect().top;
|
||||
const isAboveScroller = !entry.isIntersecting && entry.boundingClientRect.top < rootTop;
|
||||
if (next.has(projectId) === isAboveScroller) continue;
|
||||
|
||||
changed = true;
|
||||
if (isAboveScroller) next.add(projectId);
|
||||
else next.delete(projectId);
|
||||
}
|
||||
|
||||
setStuckProjectHeaders((prev) => {
|
||||
if (!entry.isIntersecting) {
|
||||
if (prev.has(projectId)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.add(projectId);
|
||||
return next;
|
||||
}
|
||||
|
||||
if (!prev.has(projectId)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.delete(projectId);
|
||||
return next;
|
||||
});
|
||||
return changed ? next : prev;
|
||||
});
|
||||
},
|
||||
{ threshold: 0 },
|
||||
{ root, threshold: 0 },
|
||||
);
|
||||
|
||||
projectHeaderSentinelRefs.current.forEach((el) => {
|
||||
|
||||
Reference in New Issue
Block a user