11// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22// SPDX-License-Identifier: Apache-2.0
3- import { createContext , RefObject , useCallback , useEffect , useLayoutEffect , useState } from 'react' ;
3+ import { createContext , RefObject } from 'react' ;
44
5- import { findUpUntil } from '@cloudscape-design/component-toolkit/dom' ;
6-
7- import * as tokens from '../internal/generated/styles/tokens' ;
8- import { useMobile } from '../internal/hooks/use-mobile' ;
9- import globalVars from '../internal/styles/global-vars' ;
10- import { getOverflowParents } from '../internal/utils/scrollable-containers' ;
5+ import { useStickyPosition } from '../internal/components/sticky-header/use-sticky-position' ;
116
127interface StickyHeaderContextProps {
138 isStuck : boolean ;
149 isStuckAtBottom : boolean ;
1510}
1611
17- interface ComputeOffsetProps {
18- isMobile : boolean ;
19- __stickyOffset ?: number ;
20- __mobileStickyOffset ?: number ;
21- hasInnerOverflowParents : boolean ;
22- __additionalOffset ?: boolean ;
23- }
24-
25- export function computeOffset ( {
26- isMobile,
27- __stickyOffset,
28- __mobileStickyOffset,
29- hasInnerOverflowParents,
30- __additionalOffset,
31- } : ComputeOffsetProps ) : string {
32- const localOffset = isMobile ? ( __stickyOffset ?? 0 ) - ( __mobileStickyOffset ?? 0 ) : ( __stickyOffset ?? 0 ) ;
33- if ( hasInnerOverflowParents || __stickyOffset !== undefined ) {
34- return `${ localOffset } px` ;
35- }
36- const globalOffset = `var(${ globalVars . stickyVerticalTopOffset } , 0px)` ;
37-
38- return `calc(${ globalOffset } + ${ localOffset } px + ${ __additionalOffset ? tokens . spaceScaledS : '0px' } )` ;
39- }
40-
4112export const StickyHeaderContext = createContext < StickyHeaderContextProps > ( {
4213 isStuck : false ,
4314 isStuckAtBottom : false ,
@@ -51,91 +22,11 @@ export const useStickyHeader = (
5122 __mobileStickyOffset ?: number ,
5223 __disableMobile ?: boolean ,
5324 __additionalOffset = false
54- ) => {
55- const isMobile = useMobile ( ) ;
56- const disableSticky = isMobile && __disableMobile ;
57- const isSticky = ! disableSticky && ! ! __stickyHeader ;
58-
59- // If it has overflow parents inside the app layout, we shouldn't apply a sticky offset.
60- const [ hasInnerOverflowParents , setHasInnerOverflowParents ] = useState ( false ) ;
61- const [ isStuck , setIsStuck ] = useState ( false ) ;
62- const [ isStuckAtBottom , setIsStuckAtBottom ] = useState ( false ) ;
63-
64- useLayoutEffect ( ( ) => {
65- if ( rootRef . current ) {
66- const overflowParents = getOverflowParents ( rootRef . current ) ;
67- const mainElement = findUpUntil ( rootRef . current , elem => elem . tagName === 'MAIN' ) ;
68-
69- // In both versions of the app layout, the scrolling element for disableBodyScroll
70- // is the <main>. If the closest overflow parent is also the closest <main> and we have
71- // offset values, it's safe to assume that it's the app layout scroll root and we
72- // should stop there.
73- setHasInnerOverflowParents ( overflowParents . length > 0 && overflowParents [ 0 ] !== mainElement ) ;
74- }
75- } , [ rootRef ] ) ;
76-
77- const computedOffset = computeOffset ( {
78- isMobile,
79- __stickyOffset,
80- __mobileStickyOffset,
81- hasInnerOverflowParents,
82- __additionalOffset,
25+ ) =>
26+ useStickyPosition ( rootRef , headerRef , {
27+ sticky : __stickyHeader ,
28+ offset : __stickyOffset ,
29+ mobileOffset : __mobileStickyOffset ,
30+ disableMobile : __disableMobile ,
31+ additionalOffset : __additionalOffset ,
8332 } ) ;
84-
85- const stickyStyles = isSticky
86- ? {
87- style : {
88- top : computedOffset ,
89- } ,
90- }
91- : { } ;
92-
93- // "stuck" state, when the header has moved from its original posititon has a
94- // box-shadow, applied here by a "header-stuck" className
95- const checkIfStuck = useCallback (
96- ( { isTrusted, target, type } ) => {
97- if ( type === 'resize' && target === window && ! isTrusted ) {
98- // The window size didn't actually change, it was a synthetic event
99- return ;
100- }
101- if ( rootRef . current && headerRef . current ) {
102- const rootTopBorderWidth = parseFloat ( getComputedStyle ( rootRef . current ) . borderTopWidth ) || 0 ;
103-
104- // Using Math.round to adjust for rounding errors in floating-point arithmetic and timing issues
105- const rootTop = Math . round ( rootRef . current . getBoundingClientRect ( ) . top + rootTopBorderWidth ) ;
106- const headerTop = Math . round ( headerRef . current . getBoundingClientRect ( ) . top ) ;
107- if ( rootTop < headerTop ) {
108- setIsStuck ( true ) ;
109- } else {
110- setIsStuck ( false ) ;
111- }
112-
113- const rootBottom = Math . round ( rootRef . current . getBoundingClientRect ( ) . bottom - rootTopBorderWidth ) ;
114- const headerBottom = Math . round ( headerRef . current . getBoundingClientRect ( ) . bottom ) ;
115- if ( rootBottom <= headerBottom ) {
116- setIsStuckAtBottom ( true ) ;
117- } else {
118- setIsStuckAtBottom ( false ) ;
119- }
120- }
121- } ,
122- [ rootRef , headerRef ]
123- ) ;
124-
125- useEffect ( ( ) => {
126- if ( isSticky ) {
127- const controller = new AbortController ( ) ;
128- window . addEventListener ( 'scroll' , checkIfStuck , { capture : true , signal : controller . signal } ) ;
129- window . addEventListener ( 'resize' , checkIfStuck , { signal : controller . signal } ) ;
130- return ( ) => {
131- controller . abort ( ) ;
132- } ;
133- }
134- } , [ isSticky , checkIfStuck ] ) ;
135- return {
136- isSticky,
137- isStuck,
138- isStuckAtBottom,
139- stickyStyles,
140- } ;
141- } ;
0 commit comments