UNPKG

@awsui/components-react

Version:

On July 19th, 2022, we launched [Cloudscape Design System](https://cloudscape.design). Cloudscape is an evolution of AWS-UI. It consists of user interface guidelines, front-end components, design resources, and development tools for building intuitive, en

99 lines 4.94 kB
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { createContext, useCallback, useEffect, useLayoutEffect, useState } from 'react'; import { findUpUntil } from '@awsui/component-toolkit/dom'; import * as tokens from '../internal/generated/styles/tokens'; import { useMobile } from '../internal/hooks/use-mobile'; import globalVars from '../internal/styles/global-vars'; import { getOverflowParents } from '../internal/utils/scrollable-containers'; export function computeOffset({ isMobile, __stickyOffset, __mobileStickyOffset, hasInnerOverflowParents, __additionalOffset, }) { const localOffset = isMobile ? (__stickyOffset !== null && __stickyOffset !== void 0 ? __stickyOffset : 0) - (__mobileStickyOffset !== null && __mobileStickyOffset !== void 0 ? __mobileStickyOffset : 0) : __stickyOffset !== null && __stickyOffset !== void 0 ? __stickyOffset : 0; if (hasInnerOverflowParents || __stickyOffset !== undefined) { return `${localOffset}px`; } const globalOffset = `var(${globalVars.stickyVerticalTopOffset}, 0px)`; return `calc(${globalOffset} + ${localOffset}px + ${__additionalOffset ? tokens.spaceScaledS : '0px'})`; } export const StickyHeaderContext = createContext({ isStuck: false, isStuckAtBottom: false, }); export const useStickyHeader = (rootRef, headerRef, __stickyHeader, __stickyOffset, __mobileStickyOffset, __disableMobile, __additionalOffset = false) => { const isMobile = useMobile(); const disableSticky = isMobile && __disableMobile; const isSticky = !disableSticky && !!__stickyHeader; // If it has overflow parents inside the app layout, we shouldn't apply a sticky offset. const [hasInnerOverflowParents, setHasInnerOverflowParents] = useState(false); const [isStuck, setIsStuck] = useState(false); const [isStuckAtBottom, setIsStuckAtBottom] = useState(false); useLayoutEffect(() => { if (rootRef.current) { const overflowParents = getOverflowParents(rootRef.current); const mainElement = findUpUntil(rootRef.current, elem => elem.tagName === 'MAIN'); // In both versions of the app layout, the scrolling element for disableBodyScroll // is the <main>. If the closest overflow parent is also the closest <main> and we have // offset values, it's safe to assume that it's the app layout scroll root and we // should stop there. setHasInnerOverflowParents(overflowParents.length > 0 && overflowParents[0] !== mainElement); } }, [rootRef]); const computedOffset = computeOffset({ isMobile, __stickyOffset, __mobileStickyOffset, hasInnerOverflowParents, __additionalOffset, }); const stickyStyles = isSticky ? { style: { top: computedOffset, }, } : {}; // "stuck" state, when the header has moved from its original posititon has a // box-shadow, applied here by a "header-stuck" className const checkIfStuck = useCallback(({ isTrusted, target, type }) => { if (type === 'resize' && target === window && !isTrusted) { // The window size didn't actually change, it was a synthetic event return; } if (rootRef.current && headerRef.current) { const rootTopBorderWidth = parseFloat(getComputedStyle(rootRef.current).borderTopWidth) || 0; // Using Math.round to adjust for rounding errors in floating-point arithmetic and timing issues const rootTop = Math.round(rootRef.current.getBoundingClientRect().top + rootTopBorderWidth); const headerTop = Math.round(headerRef.current.getBoundingClientRect().top); if (rootTop < headerTop) { setIsStuck(true); } else { setIsStuck(false); } const rootBottom = Math.round(rootRef.current.getBoundingClientRect().bottom - rootTopBorderWidth); const headerBottom = Math.round(headerRef.current.getBoundingClientRect().bottom); if (rootBottom <= headerBottom) { setIsStuckAtBottom(true); } else { setIsStuckAtBottom(false); } } }, [rootRef, headerRef]); useEffect(() => { if (isSticky) { const controller = new AbortController(); window.addEventListener('scroll', checkIfStuck, { capture: true, signal: controller.signal }); window.addEventListener('resize', checkIfStuck, { signal: controller.signal }); return () => { controller.abort(); }; } }, [isSticky, checkIfStuck]); return { isSticky, isStuck, isStuckAtBottom, stickyStyles, }; }; //# sourceMappingURL=use-sticky-header.js.map