1010
1111import React , {
1212 useEffect ,
13+ useLayoutEffect ,
1314 useRef ,
1415 useMemo ,
1516 useCallback ,
@@ -40,6 +41,9 @@ import {
4041 useFlyoutLayoutMode ,
4142 useFlyoutId ,
4243 useFlyoutWidth ,
44+ useIsFlyoutActive ,
45+ useFlyoutManager ,
46+ useHasPushPadding ,
4347} from './manager' ;
4448
4549import { CommonProps , PropsOfElement } from '../common' ;
@@ -268,6 +272,19 @@ export const EuiFlyoutComponent = forwardRef(
268272 const internalParentFlyoutRef = useRef < HTMLDivElement > ( null ) ;
269273 const isPushed = useIsPushed ( { type, pushMinBreakpoint } ) ;
270274
275+ const currentSession = useCurrentSession ( ) ;
276+ const isInManagedContext = useIsInManagedFlyout ( ) ;
277+ const flyoutId = useFlyoutId ( id ) ;
278+ const layoutMode = useFlyoutLayoutMode ( ) ;
279+ const isActiveManagedFlyout = useIsFlyoutActive ( flyoutId ) ;
280+ const flyoutManager = useFlyoutManager ( ) ;
281+
282+ // Use a ref to access the latest flyoutManager without triggering effect re-runs
283+ const flyoutManagerRef = useRef ( flyoutManager ) ;
284+ useEffect ( ( ) => {
285+ flyoutManagerRef . current = flyoutManager ;
286+ } , [ flyoutManager ] ) ;
287+
271288 const {
272289 onMouseDown : onMouseDownResizableButton ,
273290 onKeyDown : onKeyDownResizableButton ,
@@ -295,31 +312,66 @@ export const EuiFlyoutComponent = forwardRef(
295312 ] ) ;
296313 const { width } = useResizeObserver ( isPushed ? resizeRef : null , 'width' ) ;
297314
298- useEffect ( ( ) => {
299- /**
300- * Accomodate for the `isPushed` state by adding padding to the body equal to the width of the element
301- */
302- if ( isPushed ) {
303- const paddingSide =
304- side === 'left' ? 'paddingInlineStart' : 'paddingInlineEnd' ;
305- const cssVarName = `--euiPushFlyoutOffset $ {
306- side === 'left' ? 'InlineStart' : 'InlineEnd'
307- } ` ;
315+ /**
316+ * Effect for adding push padding to body. Using useLayoutEffect to ensure
317+ * padding changes happen synchronously before child components render -
318+ * this is needed to prevent RemoveScrollBar from measuring the body in an
319+ * inconsistent state during flyout transitions.
320+ */
321+ useLayoutEffect ( ( ) => {
322+ if ( ! isPushed ) {
323+ return ; // Only push-type flyouts manage body padding
324+ }
308325
309- document . body . style [ paddingSide ] = `${ width } px` ;
326+ const shouldApplyPadding = ! isInManagedContext || isActiveManagedFlyout ;
327+
328+ const paddingSide =
329+ side === 'left' ? 'paddingInlineStart' : 'paddingInlineEnd' ;
330+ const cssVarName = `--euiPushFlyoutOffset${
331+ side === 'left' ? 'InlineStart' : 'InlineEnd'
332+ } `;
333+ const managerSide = side === 'left' ? 'left' : 'right' ;
310334
311- // EUI doesn't use this css variable, but it is useful for consumers
335+ if ( shouldApplyPadding ) {
336+ document . body . style [ paddingSide ] = `${ width } px` ;
312337 setGlobalCSSVariables ( {
313338 [ cssVarName ] : `${ width } px` ,
314339 } ) ;
315- return ( ) => {
316- document . body . style [ paddingSide ] = '' ;
317- setGlobalCSSVariables ( {
318- [ cssVarName ] : null ,
319- } ) ;
320- } ;
340+ // Update manager state if in managed context
341+ if ( isInManagedContext && flyoutManagerRef . current ) {
342+ flyoutManagerRef . current . setPushPadding ( managerSide , width ) ;
343+ }
344+ } else {
345+ // Explicitly remove padding when this push flyout becomes inactive
346+ document . body . style [ paddingSide ] = '' ;
347+ setGlobalCSSVariables ( {
348+ [ cssVarName ] : null ,
349+ } ) ;
350+ // Clear manager state if in managed context
351+ if ( isInManagedContext && flyoutManagerRef . current ) {
352+ flyoutManagerRef . current . setPushPadding ( managerSide , 0 ) ;
353+ }
321354 }
322- } , [ isPushed , setGlobalCSSVariables , side , width ] ) ;
355+
356+ // Cleanup on unmount
357+ return ( ) => {
358+ document . body . style [ paddingSide ] = '' ;
359+ setGlobalCSSVariables ( {
360+ [ cssVarName ] : null ,
361+ } ) ;
362+ // Clear manager state on unmount if in managed context
363+ if ( isInManagedContext && flyoutManagerRef . current ) {
364+ flyoutManagerRef . current . setPushPadding ( managerSide , 0 ) ;
365+ }
366+ } ;
367+ } , [
368+ isPushed ,
369+ isInManagedContext ,
370+ isActiveManagedFlyout ,
371+ setGlobalCSSVariables ,
372+ side ,
373+ width ,
374+ ] ) ;
323375
324376 /**
325377 * This class doesn't actually do anything by EUI, but is nice to add for consumers (JIC)
@@ -332,13 +384,6 @@ export const EuiFlyoutComponent = forwardRef(
332384 } ;
333385 } , [ ] ) ;
334386
335- const currentSession = useCurrentSession ( ) ;
336- const isInManagedContext = useIsInManagedFlyout ( ) ;
337-
338- // Get flyout manager context for dynamic width calculation
339- const flyoutId = useFlyoutId ( id ) ;
340- const layoutMode = useFlyoutLayoutMode ( ) ;
341-
342387 // Memoize flyout identification and relationships to prevent race conditions
343388 const flyoutIdentity = useMemo ( ( ) => {
344389 if ( ! flyoutId || ! currentSession ) {
@@ -604,6 +649,14 @@ export const EuiFlyoutComponent = forwardRef(
604649
605650 const maskCombinedRefs = useCombinedRefs ( [ maskProps ?. maskRef , maskRef ] ) ;
606651
652+ /**
653+ * For overlay flyouts in managed contexts, coordinate scroll locking with push flyout state.
654+ */
655+ const hasPushPaddingInManager = useHasPushPadding ( ) ;
656+ const shouldDeferScrollLock =
657+ ! isPushed && isInManagedContext && hasPushPaddingInManager ;
658+ const shouldUseScrollLock = hasOverlayMask && ! shouldDeferScrollLock ;
659+
607660 return (
608661 < EuiFlyoutOverlay
609662 hasOverlayMask = { hasOverlayMask }
@@ -617,7 +670,7 @@ export const EuiFlyoutComponent = forwardRef(
617670 < EuiWindowEvent event = "keydown" handler = { onKeyDown } />
618671 < EuiFocusTrap
619672 disabled = { isPushed }
620- scrollLock = { hasOverlayMask }
673+ scrollLock = { shouldUseScrollLock }
621674 clickOutsideDisables = { ! ownFocus }
622675 onClickOutside = { onClickOutside }
623676 { ...focusTrapProps }
0 commit comments