@@ -10,7 +10,6 @@ import React, {
1010 useState ,
1111} from 'react' ;
1212import ResizeObserver from 'resize-observer-polyfill' ;
13- import { useDebouncedCallback } from 'use-debounce' ;
1413import { OverlayProps } from './overlay-model' ;
1514import './overlay.scss' ;
1615import { OverlayContext , OverlayContextModel } from './withOverlay' ;
@@ -45,6 +44,8 @@ const Overlay: React.FunctionComponent<OverlayProps> = ({
4544 const overlayRef = useRef < HTMLDivElement | null > ( null ) ;
4645 const overlayContentRef = useRef < HTMLDivElement | null > ( null ) ;
4746 const observer = useRef < ResizeObserver | null > ( null ) ;
47+ const rafIdRef = useRef < number | null > ( null ) ;
48+ const scrollListenersRef = useRef < Set < HTMLElement | Document > > ( new Set ( ) ) ;
4849 const [ retriggerStyleCal , setRetriggerStyleCal ] = useState < number > ( 0 ) ;
4950
5051 /**
@@ -55,9 +56,15 @@ const Overlay: React.FunctionComponent<OverlayProps> = ({
5556 width : number ;
5657 } | null > ( null ) ;
5758
58- const retrigger = useDebouncedCallback ( ( ) => {
59- setRetriggerStyleCal ( new Date ( ) . getTime ( ) ) ;
60- } , 5 ) ;
59+ const retrigger = useCallback ( ( ) => {
60+ if ( rafIdRef . current !== null ) {
61+ cancelAnimationFrame ( rafIdRef . current ) ;
62+ }
63+ rafIdRef . current = requestAnimationFrame ( ( ) => {
64+ setRetriggerStyleCal ( prev => prev + 1 ) ;
65+ rafIdRef . current = null ;
66+ } ) ;
67+ } , [ ] ) ;
6168
6269 const overlayWrapperClass = useMemo ( ( ) => {
6370 return classNames ( [ 'rc-overlay-wrapper' ] , {
@@ -136,18 +143,65 @@ const Overlay: React.FunctionComponent<OverlayProps> = ({
136143 return { } as CSSProperties ;
137144 } , [ placementReference , retriggerStyleCal , overlayDimensions , placement , context , leftOffset , placementOffset ] ) ;
138145
139- // event handlers
146+ /**
147+ * Finds all scrollable parent elements of the placement reference
148+ */
149+ const getScrollableParents = useCallback ( ( element : HTMLElement | null ) : ( HTMLElement | Document ) [ ] => {
150+ const scrollableParents : ( HTMLElement | Document ) [ ] = [ document ] ;
151+
152+ if ( ! element ) return scrollableParents ;
153+
154+ let current : HTMLElement | null = element ;
155+ while ( current && current !== document . body && current !== document . documentElement ) {
156+ const style = window . getComputedStyle ( current ) ;
157+ const overflowY = style . overflowY ;
158+ const overflowX = style . overflowX ;
159+ const isScrollable =
160+ ( overflowY === 'auto' || overflowY === 'scroll' ) ||
161+ ( overflowX === 'auto' || overflowX === 'scroll' ) ;
162+
163+ if ( isScrollable && current . scrollHeight > current . clientHeight ) {
164+ scrollableParents . push ( current ) ;
165+ }
166+
167+ current = current . parentElement ;
168+ }
169+
170+ return scrollableParents ;
171+ } , [ ] ) ;
172+
173+ /**
174+ * Synchronizes the position of the overlay content with the scroll position
175+ * Uses requestAnimationFrame for smooth, performant updates
176+ */
177+ const handleScroll = useCallback ( ( ) => {
178+ retrigger ( ) ;
179+ } , [ retrigger ] ) ;
140180
141181 const closeProcess = useCallback ( ( ) => {
142- document . removeEventListener ( 'scroll' , handleWindowScroll ) ;
182+ if ( rafIdRef . current !== null ) {
183+ cancelAnimationFrame ( rafIdRef . current ) ;
184+ rafIdRef . current = null ;
185+ }
186+
187+ const eventOptions = { capture : true , passive : true } as AddEventListenerOptions ;
188+ scrollListenersRef . current . forEach ( listener => {
189+ if ( listener === document ) {
190+ document . removeEventListener ( 'scroll' , handleScroll , eventOptions ) ;
191+ } else {
192+ ( listener as HTMLElement ) . removeEventListener ( 'scroll' , handleScroll , eventOptions ) ;
193+ }
194+ } ) ;
195+ scrollListenersRef . current . clear ( ) ;
196+
143197 observer . current ?. disconnect ( ) ;
144198 onClose ?.( ) ;
145199 setHideOverlay ( true ) ;
146200
147201 if ( hideDocumentOverflow ) {
148202 // document.body.style.overflow = 'auto';
149203 }
150- } , [ ] ) ;
204+ } , [ handleScroll , onClose , hideDocumentOverflow ] ) ;
151205
152206 /**
153207 *
@@ -166,7 +220,7 @@ const Overlay: React.FunctionComponent<OverlayProps> = ({
166220 if ( context ?. childClosing ) {
167221 closeProcess ( ) ;
168222 }
169- } , [ context ?. childClosing ] ) ;
223+ } , [ context ?. childClosing , closeProcess ] ) ;
170224
171225 /**
172226 * Closes the overlay when click outside of the overlay content
@@ -181,16 +235,7 @@ const Overlay: React.FunctionComponent<OverlayProps> = ({
181235 closeProcess ( ) ;
182236 }
183237 } ,
184- [ overlayContentRef ]
185- ) ;
186-
187- /**
188- * Synchronizes the position of the overlay content with the scroll position
189- * (do not auto-close overlays on scroll).
190- */
191- const handleWindowScroll = useDebouncedCallback (
192- ( ) => setRetriggerStyleCal ( new Date ( ) . getTime ( ) ) ,
193- 10
238+ [ closeProcess ]
194239 ) ;
195240
196241 // onMount process
@@ -201,36 +246,58 @@ const Overlay: React.FunctionComponent<OverlayProps> = ({
201246 document . body . style . overflow = 'hidden' ;
202247 }
203248
204- document . addEventListener ( 'scroll' , handleWindowScroll ) ;
249+ const scrollableParents = getScrollableParents ( placementReference ?. current || null ) ;
250+ const eventOptions = { capture : true , passive : true } as AddEventListenerOptions ;
251+
252+ scrollableParents . forEach ( parent => {
253+ if ( parent === document ) {
254+ document . addEventListener ( 'scroll' , handleScroll , eventOptions ) ;
255+ } else {
256+ ( parent as HTMLElement ) . addEventListener ( 'scroll' , handleScroll , eventOptions ) ;
257+ }
258+ scrollListenersRef . current . add ( parent ) ;
259+ } ) ;
205260
206261 if ( overlayAnimation ) {
207262 setHideOverlay ( false ) ;
208263 }
209264
210- // cleanup
211265 return ( ) => {
212- document . removeEventListener ( 'scroll' , handleWindowScroll ) ;
266+ if ( rafIdRef . current !== null ) {
267+ cancelAnimationFrame ( rafIdRef . current ) ;
268+ rafIdRef . current = null ;
269+ }
270+
271+ scrollListenersRef . current . forEach ( listener => {
272+ if ( listener === document ) {
273+ document . removeEventListener ( 'scroll' , handleScroll , eventOptions ) ;
274+ } else {
275+ ( listener as HTMLElement ) . removeEventListener ( 'scroll' , handleScroll , eventOptions ) ;
276+ }
277+ } ) ;
278+ scrollListenersRef . current . clear ( ) ;
279+
213280 if ( hideDocumentOverflow ) {
214281 document . body . style . overflow = originalOverflow ;
215282 }
216- // observer.current?.disconnect();
217283 } ;
218- } , [ hideDocumentOverflow , handleWindowScroll , overlayAnimation ] ) ;
284+ } , [ hideDocumentOverflow , handleScroll , overlayAnimation , placementReference , getScrollableParents ] ) ;
219285
220286 const onRef = useCallback ( ( node : HTMLDivElement ) => {
221287 const ele = node as HTMLDivElement ;
222288 if ( ele ) {
223289 overlayRef . current = ele ;
224290
291+ if ( observer . current ) {
292+ observer . current . disconnect ( ) ;
293+ }
294+
225295 observer . current = new ResizeObserver ( retrigger ) ;
226-
227296 observer . current . observe ( ele ) ;
228297
229298 onOpen ?.( ) ;
230- // setTimeout(() => {
231- // }, 50);
232299 }
233- } , [ ] ) ;
300+ } , [ retrigger , onOpen ] ) ;
234301
235302 const onOverlayRef = useCallback ( ( node : HTMLDivElement ) => {
236303 const ele = node as HTMLDivElement ;
@@ -249,18 +316,18 @@ const Overlay: React.FunctionComponent<OverlayProps> = ({
249316 * we would want to hide the overlay content until the overlay is positioned correctly.
250317 */
251318 const customPlacementStyle = useMemo < CSSProperties > ( ( ) => {
252- if ( placement && placementStyle ) {
319+ if ( placement && placementStyle && Object . keys ( placementStyle ) . length > 0 ) {
253320 return placementStyle ;
254321 }
255322
256- if ( placement && ! placementStyle ) {
323+ if ( placement && ( ! placementStyle || Object . keys ( placementStyle ) . length === 0 ) ) {
257324 return {
258325 visibility : 'hidden' ,
259326 } ;
260327 }
261328
262329 return { } ;
263- } , [ JSON . stringify ( placementStyle ) , placement ] ) ;
330+ } , [ placementStyle , placement ] ) ;
264331
265332 return ! disableBackdrop ? (
266333 < div
0 commit comments