import { useState } from "react"; import { tss } from "./tss"; import { useStateRef } from "powerhooks/useStateRef"; import type { ReactNode } from "react"; import { useDomRect } from "powerhooks/useDomRect"; import { useEvt } from "evt/hooks/useEvt"; import { Evt } from "evt"; import { GlLinkToTop } from "./shared/GlLinkToTop"; import { disableEmotionWarnings } from "./tools/disableEmotionWarnings"; import { getScrollableParent } from "powerhooks/getScrollableParent"; disableEmotionWarnings(); export type HeaderOptions = HeaderOptions.TopOfPage | HeaderOptions.Sticky; export namespace HeaderOptions { export type TopOfPage = { position: "top of page"; isRetracted?: boolean; }; export type Sticky = { position: "sticky"; isRetracted?: boolean | "smart"; }; } export type GlTemplateProps = { header?: ReactNode; body?: ReactNode; footer?: ReactNode; headerOptions?: HeaderOptions; applyHeaderPadding?: boolean; className?: string; hasTopOfPageLinkButton?: boolean; classes?: Partial["classes"]>; }; //NOTE: Here we are sure that we are wrapped into a . //we can use useTheme, useStyles, ect... export function GlTemplate(props: GlTemplateProps) { const { header, body, footer, className, hasTopOfPageLinkButton, applyHeaderPadding, } = props; const rootRef = useStateRef(null); const headerOptions: Required = (() => { const { headerOptions } = props; if (headerOptions === undefined) { return { "position": "top of page", "isRetracted": false, } as const; } switch (headerOptions.position) { case "top of page": return { ...headerOptions, "isRetracted": headerOptions.isRetracted ?? false, }; case "sticky": return { ...headerOptions, "isRetracted": headerOptions.isRetracted ?? false, }; } })(); const { ref: headerWrapperRef, domRect: { height: headerHeight }, } = useDomRect(); const { ref: bodyAndFooterWrapperRef, domRect: { width: childrenWrapperWidth }, } = useDomRect(); const [isSmartHeaderVisible, setIsSmartHeaderVisible] = useState(true); useEvt( ctx => { const element = rootRef.current; if (!element) { return; } let previousScrollTop = 0; const scrollableParent = getScrollableParent({ element, "doReturnElementIfScrollable": true, }); Evt.from(ctx, scrollableParent, "scroll").attach(() => { const { scrollTop } = scrollableParent; setIsSmartHeaderVisible( scrollTop < previousScrollTop ? true : scrollTop <= headerHeight, ); previousScrollTop = scrollTop; }); }, [rootRef.current, headerHeight, headerOptions.isRetracted], ); const { classes, cx } = useStyles({ childrenWrapperWidth, headerHeight, "isHeaderRetracted": headerOptions.isRetracted === "smart" ? !isSmartHeaderVisible : headerOptions.isRetracted, "headerPosition": headerOptions.position, "applyHeaderPadding": applyHeaderPadding ?? false, "classesOverrides": props.classes, }); return (
{header}
{body} {hasTopOfPageLinkButton && }
{footer}
); } const useStyles = tss .withName({ GlTemplate }) .withParams<{ headerHeight: number; childrenWrapperWidth: number; isHeaderRetracted: boolean; headerPosition: Required["position"]; applyHeaderPadding: boolean; }>() .create( ({ theme, headerHeight, childrenWrapperWidth, isHeaderRetracted, headerPosition, applyHeaderPadding, }) => { return { "root": {}, "headerWrapper": { "padding": applyHeaderPadding ? theme.spacing({ "rightLeft": `${theme.paddingRightLeft}px`, "topBottom": `${theme.spacing(3)}px`, }) : undefined, "zIndex": 4000, ...(() => { switch (headerPosition) { case "sticky": return { "width": childrenWrapperWidth, "backgroundColor": "transparent", "top": !isHeaderRetracted ? 0 : -headerHeight, "transition": "top 350ms", "position": "sticky", "pointerEvents": isHeaderRetracted ? "none" : undefined, }; case "top of page": return {}; } })(), }, "footerWrapper": { "marginTop": "auto", }, "bodyAndFooterWrapper": { "display": "flex", "flexDirection": "column", "minHeight": window.innerHeight - headerHeight, }, }; }, );