Skip to content
Merged
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
107 changes: 80 additions & 27 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,19 @@ export const EuiFlyoutComponent = forwardRef(
const internalParentFlyoutRef = useRef<HTMLDivElement>(null);
const isPushed = useIsPushed({ type, pushMinBreakpoint });

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 +311,66 @@ export const EuiFlyoutComponent = forwardRef(
]);
const { width } = useResizeObserver(isPushed ? resizeRef : null, 'width');

useEffect(() => {
/**
* Accomodate for the `isPushed` state by adding padding to the body equal to the width of the element
*/
if (isPushed) {
const paddingSide =
side === 'left' ? 'paddingInlineStart' : 'paddingInlineEnd';
const cssVarName = `--euiPushFlyoutOffset${
side === 'left' ? 'InlineStart' : 'InlineEnd'
}`;
/**
* Effect for adding push padding to body. Using useLayoutEffect to ensure
* padding changes happen synchronously before child components render -
* this is needed to prevent RemoveScrollBar from measuring the body in an
* inconsistent state during flyout transitions.
*/
useLayoutEffect(() => {
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 +383,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 +648,14 @@ export const EuiFlyoutComponent = forwardRef(

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

/**
* For overlay flyouts in managed contexts, coordinate scroll locking with push flyout state.
*/
const hasPushPaddingInManager = useHasPushPadding();
const shouldDeferScrollLock =
!isPushed && isInManagedContext && hasPushPaddingInManager;
const shouldUseScrollLock = hasOverlayMask && !shouldDeferScrollLock;

return (
<EuiFlyoutOverlay
hasOverlayMask={hasOverlayMask}
Expand All @@ -616,7 +669,7 @@ export const EuiFlyoutComponent = forwardRef(
<EuiWindowEvent event="keydown" handler={onKeyDown} />
<EuiFocusTrap
disabled={isPushed}
scrollLock={hasOverlayMask}
scrollLock={shouldUseScrollLock}
clickOutsideDisables={!ownFocus}
onClickOutside={onClickOutside}
{...focusTrapProps}
Expand Down
45 changes: 45 additions & 0 deletions packages/eui/src/components/flyout/flyout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,51 @@ describe('EuiFlyout', () => {
});
});

describe('push padding manager state coordination', () => {
it('applies body padding for push flyouts', () => {
const { container } = render(
<EuiFlyout
onClose={() => {}}
type="push"
pushMinBreakpoint="xs"
data-test-subj="push-flyout"
/>
);

const flyout = container.querySelector('[data-test-subj="push-flyout"]');
expect(flyout).toBeInTheDocument();

// Body should have padding applied
const bodyPaddingEnd = document.body.style.paddingInlineEnd;
expect(bodyPaddingEnd).toBeTruthy();
});

it('removes body padding on unmount', () => {
const { unmount } = render(
<EuiFlyout
onClose={() => {}}
type="push"
pushMinBreakpoint="xs"
data-test-subj="push-flyout"
/>
);

// Verify padding was applied
expect(document.body.style.paddingInlineEnd).toBeTruthy();

unmount();

// Verify padding was cleared
expect(document.body.style.paddingInlineEnd).toBe('');
});

afterEach(() => {
// Clean up body styles after each test
document.body.style.paddingInlineStart = '';
document.body.style.paddingInlineEnd = '';
});
});

describe('flyout routing logic', () => {
it('routes to child flyout when session is undefined and there is an active session', () => {
// First render with just the main flyout to establish a session
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