Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 83 additions & 25 deletions packages/eui/src/components/flyout/flyout.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import React, {
useEffect,
useLayoutEffect,
useRef,
useMemo,
useCallback,
Expand Down Expand Up @@ -40,6 +41,9 @@ import {
useFlyoutLayoutMode,
useFlyoutId,
useFlyoutWidth,
useIsFlyoutActive,
useFlyoutManager,
useHasPushPadding,
} from './manager';

import { CommonProps, PropsOfElement } from '../common';
Expand Down Expand Up @@ -267,6 +271,20 @@ export const EuiFlyoutComponent = forwardRef(
const internalParentFlyoutRef = useRef<HTMLDivElement>(null);
const isPushed = useIsPushed({ type, pushMinBreakpoint });

// Get managed flyout context early so it's available for effects
const currentSession = useCurrentSession();
const isInManagedContext = useIsInManagedFlyout();
const flyoutId = useFlyoutId(id);
const layoutMode = useFlyoutLayoutMode();
const isActiveManagedFlyout = useIsFlyoutActive(flyoutId);
const flyoutManager = useFlyoutManager();

// Use a ref to access the latest flyoutManager without triggering effect re-runs
const flyoutManagerRef = useRef(flyoutManager);
useEffect(() => {
flyoutManagerRef.current = flyoutManager;
}, [flyoutManager]);

const {
onMouseDown: onMouseDownResizableButton,
onKeyDown: onKeyDownResizableButton,
Expand Down Expand Up @@ -294,31 +312,69 @@ export const EuiFlyoutComponent = forwardRef(
]);
const { width } = useResizeObserver(isPushed ? resizeRef : null, 'width');

useEffect(() => {
/**
* Use useLayoutEffect (not useEffect) to ensure padding changes happen synchronously
* before child components render. This prevents RemoveScrollBar from measuring the body
* in an inconsistent state during flyout transitions.
*/
useLayoutEffect(() => {
/**
* Accomodate for the `isPushed` state by adding padding to the body equal to the width of the element
* Accomodate for the `isPushed` state by adding padding to the body equal to the width of the element.
* For managed flyouts, only apply padding if this flyout is active.
*/
if (isPushed) {
const paddingSide =
side === 'left' ? 'paddingInlineStart' : 'paddingInlineEnd';
const cssVarName = `--euiPushFlyoutOffset${
side === 'left' ? 'InlineStart' : 'InlineEnd'
}`;
if (!isPushed) {
return; // Only push-type flyouts manage body padding
}

document.body.style[paddingSide] = `${width}px`;
const shouldApplyPadding = !isInManagedContext || isActiveManagedFlyout;

const paddingSide =
side === 'left' ? 'paddingInlineStart' : 'paddingInlineEnd';
const cssVarName = `--euiPushFlyoutOffset${
side === 'left' ? 'InlineStart' : 'InlineEnd'
}`;
const managerSide = side === 'left' ? 'left' : 'right';

// EUI doesn't use this css variable, but it is useful for consumers
if (shouldApplyPadding) {
document.body.style[paddingSide] = `${width}px`;
setGlobalCSSVariables({
[cssVarName]: `${width}px`,
});
return () => {
document.body.style[paddingSide] = '';
setGlobalCSSVariables({
[cssVarName]: null,
});
};
// Update manager state if in managed context
if (isInManagedContext && flyoutManagerRef.current) {
flyoutManagerRef.current.setPushPadding(managerSide, width);
}
} else {
// Explicitly remove padding when this push flyout becomes inactive
document.body.style[paddingSide] = '';
setGlobalCSSVariables({
[cssVarName]: null,
});
// Clear manager state if in managed context
if (isInManagedContext && flyoutManagerRef.current) {
flyoutManagerRef.current.setPushPadding(managerSide, 0);
}
}
}, [isPushed, setGlobalCSSVariables, side, width]);

// Cleanup on unmount
return () => {
document.body.style[paddingSide] = '';
setGlobalCSSVariables({
[cssVarName]: null,
});
// Clear manager state on unmount if in managed context
if (isInManagedContext && flyoutManagerRef.current) {
flyoutManagerRef.current.setPushPadding(managerSide, 0);
}
};
}, [
isPushed,
isInManagedContext,
isActiveManagedFlyout,
setGlobalCSSVariables,
side,
width,
]);

/**
* This class doesn't actually do anything by EUI, but is nice to add for consumers (JIC)
Expand All @@ -331,13 +387,6 @@ export const EuiFlyoutComponent = forwardRef(
};
}, []);

const currentSession = useCurrentSession();
const isInManagedContext = useIsInManagedFlyout();

// Get flyout manager context for dynamic width calculation
const flyoutId = useFlyoutId(id);
const layoutMode = useFlyoutLayoutMode();

// Memoize flyout identification and relationships to prevent race conditions
const flyoutIdentity = useMemo(() => {
if (!flyoutId || !currentSession) {
Expand Down Expand Up @@ -603,6 +652,15 @@ export const EuiFlyoutComponent = forwardRef(

const maskCombinedRefs = useCombinedRefs([maskProps?.maskRef, maskRef]);

/**
* For overlay flyouts in managed contexts, coordinate scroll locking with push flyout state.
* Only enable scroll lock when there's no active push padding in the manager.
*/
const hasPushPaddingInManager = useHasPushPadding();
const shouldDeferScrollLock =
!isPushed && isInManagedContext && hasPushPaddingInManager;
const shouldUseScrollLock = hasOverlayMask && !shouldDeferScrollLock;

return (
<EuiFlyoutOverlay
hasOverlayMask={hasOverlayMask}
Expand All @@ -616,7 +674,7 @@ export const EuiFlyoutComponent = forwardRef(
<EuiWindowEvent event="keydown" handler={onKeyDown} />
<EuiFocusTrap
disabled={isPushed}
scrollLock={hasOverlayMask}
scrollLock={shouldUseScrollLock}
clickOutsideDisables={!ownFocus}
onClickOutside={onClickOutside}
{...focusTrapProps}
Expand Down
22 changes: 21 additions & 1 deletion packages/eui/src/components/flyout/manager/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const ACTION_SET_ACTIVITY_STAGE = `${PREFIX}/setActivityStage` as const;
export const ACTION_GO_BACK = `${PREFIX}/goBack` as const;
/** Dispatched to navigate to a specific flyout (remove all sessions after it). */
export const ACTION_GO_TO_FLYOUT = `${PREFIX}/goToFlyout` as const;
/** Dispatched to set push padding offset for a side. */
export const ACTION_SET_PUSH_PADDING = `${PREFIX}/setPushPadding` as const;

/**
* Add a flyout to manager state. The manager will create or update
Expand Down Expand Up @@ -91,6 +93,13 @@ export interface GoToFlyoutAction extends BaseAction {
flyoutId: string;
}

/** Set push padding offset for a specific side. */
export interface SetPushPaddingAction extends BaseAction {
type: typeof ACTION_SET_PUSH_PADDING;
side: 'left' | 'right';
width: number;
}

/** Union of all flyout manager actions. */
export type Action =
| AddFlyoutAction
Expand All @@ -100,7 +109,8 @@ export type Action =
| SetLayoutModeAction
| SetActivityStageAction
| GoBackAction
| GoToFlyoutAction;
| GoToFlyoutAction
| SetPushPaddingAction;

/**
* Register a flyout with the manager.
Expand Down Expand Up @@ -173,3 +183,13 @@ export const goToFlyout = (flyoutId: string): GoToFlyoutAction => ({
type: ACTION_GO_TO_FLYOUT,
flyoutId,
});

/** Set push padding offset for a specific side. */
export const setPushPadding = (
side: 'left' | 'right',
width: number
): SetPushPaddingAction => ({
type: ACTION_SET_PUSH_PADDING,
side,
width,
});
Loading