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
3 changes: 3 additions & 0 deletions packages/eui/changelogs/upcoming/9592.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**Bug fixes**

- Fixed `EuiFlyout` to compare `pushMinBreakpoint` against the container's width, instead of the viewport width, when the `container` prop is provided. This ensures app-scoped flyouts switch between push and overlay modes based on the space actually available inside their container.
23 changes: 14 additions & 9 deletions packages/eui/src/components/flyout/flyout.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,20 @@ export const EuiFlyoutComponent = forwardRef(

// Ref for the main flyout element to pass to context
const internalParentFlyoutRef = useRef<HTMLDivElement>(null);
const isPushed = useIsPushed({ type, pushMinBreakpoint });

// Observe the container's dimensions so the resize hook and
// positioning styles stay aligned with its bounding rect.
// When no container is provided, these remain inert (null/undefined).
const containerDimensions = useResizeObserver(container ?? null, 'width');
const containerReferenceWidth = container
? containerDimensions.width || container.clientWidth
: undefined;

const isPushed = useIsPushed({
type,
pushMinBreakpoint,
containerWidth: containerReferenceWidth,
});
// When no explicit container is provided, push padding targets
// document.body and global push-offset CSS vars are set. When a
// container is provided, only that element receives padding.
Expand Down Expand Up @@ -477,14 +490,6 @@ export const EuiFlyoutComponent = forwardRef(
const siblingFlyoutWidth =
layoutMode === LAYOUT_MODE_STACKED ? 0 : _siblingFlyoutWidth;

// Observe the container's dimensions so the resize hook and
// positioning styles stay aligned with its bounding rect.
// When no container is provided, these remain inert (null/undefined).
const containerDimensions = useResizeObserver(container ?? null, 'width');
const containerReferenceWidth = container
? containerDimensions.width || container.clientWidth
: undefined;

// Track the container's bounding rect for positioning the flyout.
const [containerRect, setContainerRect] = useState<DOMRect | null>(null);

Expand Down
120 changes: 120 additions & 0 deletions packages/eui/src/components/flyout/hooks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { renderHook } from '../../test/rtl/render_hook';
import { useIsPushed } from './hooks';

// Mock the viewport-based breakpoint hook so tests can control it
// independently of the jsdom window size.
jest.mock('../../services', () => {
const actual = jest.requireActual('../../services');
return {
...actual,
useIsWithinMinBreakpoint: jest.fn(() => false),
};
});

const mockUseIsWithinMinBreakpoint = jest.requireMock('../../services')
.useIsWithinMinBreakpoint as jest.Mock;

describe('useIsPushed', () => {
beforeEach(() => {
mockUseIsWithinMinBreakpoint.mockReset().mockReturnValue(false);
});

describe('viewport fallback (no containerWidth)', () => {
it('returns true when type is push and viewport is large enough', () => {
mockUseIsWithinMinBreakpoint.mockReturnValue(true);
const { result } = renderHook(() =>
useIsPushed({ type: 'push', pushMinBreakpoint: 'm' })
);
expect(result.current).toBe(true);
});

it('returns false when viewport is too small', () => {
mockUseIsWithinMinBreakpoint.mockReturnValue(false);
const { result } = renderHook(() =>
useIsPushed({ type: 'push', pushMinBreakpoint: 'm' })
);
expect(result.current).toBe(false);
});
});

describe('container width', () => {
it('returns true when container width exceeds breakpoint', () => {
// Viewport mock returns false β€” should be ignored
mockUseIsWithinMinBreakpoint.mockReturnValue(false);
const { result } = renderHook(() =>
useIsPushed({
type: 'push',
pushMinBreakpoint: 'm',
containerWidth: 800, // > 768
})
);
expect(result.current).toBe(true);
});

it('returns false when container width is below breakpoint', () => {
// Viewport mock returns true β€” should be ignored
mockUseIsWithinMinBreakpoint.mockReturnValue(true);
const { result } = renderHook(() =>
useIsPushed({
type: 'push',
pushMinBreakpoint: 'm',
containerWidth: 700, // < 768
})
);
expect(result.current).toBe(false);
});

it('returns true when container width equals breakpoint exactly', () => {
const { result } = renderHook(() =>
useIsPushed({
type: 'push',
pushMinBreakpoint: 'm',
containerWidth: 768, // === 768
})
);
expect(result.current).toBe(true);
});

it('falls back to the viewport hook when the breakpoint key is not on the theme', () => {
mockUseIsWithinMinBreakpoint.mockReturnValue(true);
const { result } = renderHook(() =>
useIsPushed({
type: 'push',
pushMinBreakpoint: 'xxl', // not a default theme key
containerWidth: 2000,
})
);
expect(result.current).toBe(true);
expect(mockUseIsWithinMinBreakpoint).toHaveBeenCalledWith('xxl');
});
});

describe('non-push type', () => {
it('returns false for overlay type regardless of containerWidth', () => {
const { result } = renderHook(() =>
useIsPushed({
type: 'overlay',
pushMinBreakpoint: 'm',
containerWidth: 1200,
})
);
expect(result.current).toBe(false);
});

it('returns false for overlay type regardless of viewport', () => {
mockUseIsWithinMinBreakpoint.mockReturnValue(true);
const { result } = renderHook(() =>
useIsPushed({ type: 'overlay', pushMinBreakpoint: 'm' })
);
expect(result.current).toBe(false);
});
});
});
24 changes: 20 additions & 4 deletions packages/eui/src/components/flyout/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,39 @@
* Side Public License, v 1.
*/

import { useIsWithinMinBreakpoint } from '../../services';
import { useIsWithinMinBreakpoint, useEuiTheme } from '../../services';
import { EuiFlyoutProps } from './flyout';
import { usePropsWithComponentDefaults } from '../provider/component_defaults';
import { DEFAULT_PUSH_MIN_BREAKPOINT, DEFAULT_TYPE } from './const';

/**
* Determines if a flyout should be rendered in a "pushed" state based on its
* configuration and the current window size.
* configuration and the current window or container size.
*/
export const useIsPushed = (
props: Pick<EuiFlyoutProps, 'type' | 'pushMinBreakpoint'>
props: Pick<EuiFlyoutProps, 'type' | 'pushMinBreakpoint'> & {
containerWidth?: number;
}
) => {
const {
type = DEFAULT_TYPE,
pushMinBreakpoint = DEFAULT_PUSH_MIN_BREAKPOINT,
} = usePropsWithComponentDefaults('EuiFlyout', props);

const {
euiTheme: { breakpoint: breakpoints },
} = useEuiTheme();

// Always called to satisfy React hook rules; used as fallback
// when no container width is provided or the breakpoint key is
// not present on the theme.
const windowIsLargeEnoughToPush = useIsWithinMinBreakpoint(pushMinBreakpoint);
return type === 'push' && windowIsLargeEnoughToPush;

const minWidth = breakpoints[pushMinBreakpoint];
const isLargeEnoughToPush =
props.containerWidth != null && minWidth != null
? props.containerWidth >= minWidth
: windowIsLargeEnoughToPush;
Comment thread
acstll marked this conversation as resolved.

return type === 'push' && isLargeEnoughToPush;
Comment thread
tsullivan marked this conversation as resolved.
};
9 changes: 7 additions & 2 deletions packages/eui/src/components/flyout/manager/flyout_main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import React, { forwardRef } from 'react';

import { EuiManagedFlyout, type EuiManagedFlyoutProps } from './flyout_managed';
import { useHasChildFlyout, useFlyoutId } from './hooks';
import { useHasChildFlyout, useFlyoutId, useFlyoutManager } from './hooks';
import { euiMainFlyoutStyles } from './flyout_main.styles';
import { useEuiMemoizedStyles } from '../../../services';
import {
Expand Down Expand Up @@ -44,7 +44,12 @@ export const EuiFlyoutMain = forwardRef<HTMLElement, EuiFlyoutMainProps>(
const flyoutId = useFlyoutId(id);
const hasChildFlyout = useHasChildFlyout(flyoutId);
const styles = useEuiMemoizedStyles(euiMainFlyoutStyles);
const isPushed = useIsPushed({ type, pushMinBreakpoint });
const context = useFlyoutManager();
const isPushed = useIsPushed({
type,
pushMinBreakpoint,
containerWidth: context?.state.referenceWidth,
});
Comment thread
tsullivan marked this conversation as resolved.

const cssStyles = [
hasChildFlyout && !isPushed && styles.hasChildFlyout[side],
Expand Down