update page header render to use css instead of state

This commit is contained in:
jeffvli
2025-11-22 04:08:38 -08:00
parent b159dd4452
commit 1c5212d756
3 changed files with 89 additions and 28 deletions
@@ -1,6 +1,5 @@
import { useInView } from 'motion/react';
import { useOverlayScrollbars } from 'overlayscrollbars-react'; import { useOverlayScrollbars } from 'overlayscrollbars-react';
import { CSSProperties, forwardRef, ReactNode, Ref, useEffect, useRef, useState } from 'react'; import { CSSProperties, forwardRef, ReactNode, Ref, useEffect, useRef } from 'react';
import styles from './native-scroll-area.module.css'; import styles from './native-scroll-area.module.css';
@@ -25,12 +24,7 @@ export const NativeScrollArea = forwardRef(
ref: Ref<HTMLDivElement>, ref: Ref<HTMLDivElement>,
) => { ) => {
const { windowBarStyle } = useWindowSettings(); const { windowBarStyle } = useWindowSettings();
const containerRef = useRef(null); const containerRef = useRef<HTMLDivElement | null>(null);
const [isPastOffset, setIsPastOffset] = useState(false);
const isInView = useInView({
current: pageHeaderProps?.target?.current,
});
const scrollHandlerRef = useRef<null | number>(null); const scrollHandlerRef = useRef<null | number>(null);
@@ -43,26 +37,23 @@ export const NativeScrollArea = forwardRef(
} }
scrollHandlerRef.current = requestAnimationFrame(() => { scrollHandlerRef.current = requestAnimationFrame(() => {
if (noHeader) { if (noHeader || !pageHeaderProps) {
return setIsPastOffset(true); return;
} }
if (pageHeaderProps?.target || !pageHeaderProps?.offset) { const scrollElement = e?.target as HTMLDivElement;
return setIsPastOffset(true); if (!scrollElement || !containerRef.current) {
return;
} }
const offset = pageHeaderProps?.offset; const offset = pageHeaderProps.offset || 0;
const scrollTop = (e?.target as HTMLDivElement)?.scrollTop; const scrollTop = scrollElement.scrollTop;
if (scrollTop > offset && isPastOffset === false) { if (scrollTop > offset) {
return setIsPastOffset(true); containerRef.current.setAttribute('data-scrolled', 'true');
} else {
containerRef.current.setAttribute('data-scrolled', 'false');
} }
if (scrollTop <= offset && isPastOffset === true) {
return setIsPastOffset(false);
}
return null;
}); });
}, },
}, },
@@ -81,21 +72,22 @@ export const NativeScrollArea = forwardRef(
useEffect(() => { useEffect(() => {
if (containerRef.current) { if (containerRef.current) {
initialize(containerRef.current as HTMLDivElement); initialize(containerRef.current as HTMLDivElement);
if (!noHeader && pageHeaderProps) {
containerRef.current.setAttribute('data-scrolled', 'false');
}
} }
}, [initialize]); }, [initialize, noHeader, pageHeaderProps]);
const mergedRef = useMergedRef(ref, containerRef); const mergedRef = useMergedRef(ref, containerRef);
const shouldShowHeader = !noHeader && isPastOffset && !isInView;
return ( return (
<> <>
{windowBarStyle === Platform.WEB && <div className={styles.dragContainer} />} {windowBarStyle === Platform.WEB && <div className={styles.dragContainer} />}
{shouldShowHeader && ( {!noHeader && pageHeaderProps && (
<PageHeader <PageHeader
animated animated
isHidden={false}
position="absolute" position="absolute"
scrollContainerRef={containerRef}
{...pageHeaderProps} {...pageHeaderProps}
/> />
)} )}
@@ -1,8 +1,26 @@
.container { .container {
position: relative; position: relative;
z-index: 190; z-index: 190;
visibility: hidden;
width: 100%; width: 100%;
height: 65px; height: 65px;
pointer-events: none;
opacity: 0;
transition:
opacity 0.2s ease-in-out,
visibility 0.2s ease-in-out;
}
.container[data-visible='true'] {
visibility: visible;
pointer-events: auto;
opacity: 1;
}
.container.visible {
visibility: visible;
pointer-events: auto;
opacity: 1;
} }
.header { .header {
@@ -1,6 +1,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useInView } from 'motion/react';
import { AnimatePresence, motion, Variants } from 'motion/react'; import { AnimatePresence, motion, Variants } from 'motion/react';
import { CSSProperties, ReactNode, useRef } from 'react'; import { CSSProperties, ReactNode, RefObject, useEffect, useRef } from 'react';
import styles from './page-header.module.css'; import styles from './page-header.module.css';
@@ -18,6 +19,8 @@ export interface PageHeaderProps
height?: string; height?: string;
isHidden?: boolean; isHidden?: boolean;
position?: string; position?: string;
scrollContainerRef?: RefObject<HTMLDivElement | null>;
target?: RefObject<HTMLElement | null>;
} }
const variants: Variants = { const variants: Variants = {
@@ -39,6 +42,8 @@ export const PageHeader = ({
height, height,
isHidden, isHidden,
position, position,
scrollContainerRef,
target,
...props ...props
}: PageHeaderProps) => { }: PageHeaderProps) => {
const ref = useRef(null); const ref = useRef(null);
@@ -46,10 +51,56 @@ export const PageHeader = ({
const { windowBarStyle } = useWindowSettings(); const { windowBarStyle } = useWindowSettings();
const { mode } = useAppTheme(); const { mode } = useAppTheme();
const isInView = useInView({
current: target?.current || null,
});
useEffect(() => {
const headerElement = ref.current as HTMLElement | null;
const scrollContainer = scrollContainerRef?.current;
if (!scrollContainerRef) {
if (headerElement) {
headerElement.setAttribute('data-visible', isHidden ? 'false' : 'true');
}
return undefined;
}
if (!scrollContainer || !headerElement) {
if (headerElement) {
headerElement.setAttribute('data-visible', 'false');
}
return undefined;
}
const updateVisibility = () => {
const dataScrolled = scrollContainer.getAttribute('data-scrolled');
const isScrolled = dataScrolled === 'true';
const shouldShow = isScrolled && !isInView;
if (shouldShow) {
headerElement.setAttribute('data-visible', 'true');
} else {
headerElement.setAttribute('data-visible', 'false');
}
};
updateVisibility();
const observer = new MutationObserver(updateVisibility);
observer.observe(scrollContainer, {
attributeFilter: ['data-scrolled'],
attributes: true,
});
return () => observer.disconnect();
}, [isInView, scrollContainerRef, isHidden]);
return ( return (
<> <>
<Flex <Flex
className={styles.container} className={styles.container}
data-visible="false"
ref={ref} ref={ref}
style={{ height, position: position as CSSProperties['position'] }} style={{ height, position: position as CSSProperties['position'] }}
{...props} {...props}