Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,55 @@ exports[`EuiFlyout props size accepts custom number 1`] = `
]
`;

exports[`EuiFlyout props size fill is rendered 1`] = `
[
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
data-focus-lock-disabled="false"
>
<div
aria-describedby="generated-id"
aria-modal="true"
class="euiFlyout emotion-euiFlyout-l-fill-noMaxWidth-overlay-right-right"
data-autofocus="true"
role="dialog"
tabindex="0"
>
<p
class="emotion-euiScreenReaderOnly"
id="generated-id"
>
You are in a modal dialog. Press Escape or tap/click outside the dialog on the shadowed overlay to close.
</p>
<button
aria-label="Close this dialog"
class="euiButtonIcon euiFlyout__closeButton emotion-euiButtonIcon-xs-empty-text-euiFlyout__closeButton-inside"
data-test-subj="euiFlyoutCloseButton"
type="button"
>
<span
aria-hidden="true"
class="euiButtonIcon__icon"
color="inherit"
data-euiicon-type="cross"
/>
</button>
</div>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>,
]
`;

exports[`EuiFlyout props size l is rendered 1`] = `
[
<div>
Expand Down
2 changes: 1 addition & 1 deletion packages/eui/src/components/flyout/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const FLYOUT_SIDES = ['left', 'right'] as const;
export type _EuiFlyoutSide = (typeof FLYOUT_SIDES)[number];

/** Allowed named flyout sizes used by the manager. */
export const FLYOUT_SIZES = ['s', 'm', 'l'] as const;
export const FLYOUT_SIZES = ['s', 'm', 'l', 'fill'] as const;
/** Type representing a supported named flyout size. */
export type EuiFlyoutSize = (typeof FLYOUT_SIZES)[number];

Expand Down
72 changes: 58 additions & 14 deletions packages/eui/src/components/flyout/flyout.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,13 @@ import {
useGeneratedHtmlId,
useEuiThemeCSSVariables,
} from '../../services';
import { useCurrentSession, useIsInManagedFlyout } from './manager';
import { logicalStyle } from '../../global_styling';
import {
useCurrentSession,
useIsInManagedFlyout,
useFlyoutLayoutMode,
useFlyoutId,
useFlyoutWidth,
} from './manager';

import { CommonProps, PropsOfElement } from '../common';
import { EuiFocusTrap, EuiFocusTrapProps } from '../focus_trap';
Expand All @@ -47,7 +52,7 @@ import { EuiPortal } from '../portal';
import { EuiScreenReaderOnly } from '../accessibility';

import { EuiFlyoutCloseButton } from './_flyout_close_button';
import { euiFlyoutStyles } from './flyout.styles';
import { euiFlyoutStyles, composeFlyoutInlineStyles } from './flyout.styles';
import { usePropsWithComponentDefaults } from '../provider/component_defaults';
import {
_EuiFlyoutPaddingSize,
Expand Down Expand Up @@ -273,6 +278,37 @@ 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) {
return {
isMainFlyout: false,
siblingFlyoutId: null,
hasValidSession: false,
sessionForWidth: null,
};
}

const siblingFlyoutId =
currentSession.main === flyoutId
? currentSession.child
: currentSession.main;

return {
siblingFlyoutId,
hasValidSession: true,
sessionForWidth: currentSession,
};
}, [flyoutId, currentSession]);

// Destructure for easier use
const { siblingFlyoutId } = flyoutIdentity;

const hasChildFlyout = currentSession?.child != null;
const isChildFlyout =
isInManagedContext && hasChildFlyout && currentSession?.child === id;
Expand Down Expand Up @@ -310,21 +346,29 @@ export const EuiFlyoutComponent = forwardRef(
[onClose, isPushed, shouldCloseOnEscape]
);

const siblingFlyoutWidth = useFlyoutWidth(siblingFlyoutId);

/**
* Set inline styles
*/
const inlineStyles = useMemo(() => {
const widthStyle =
!isEuiFlyoutSizeNamed(size) && logicalStyle('width', size);
const maxWidthStyle =
typeof maxWidth !== 'boolean' && logicalStyle('max-width', maxWidth);

return {
...style,
...widthStyle,
...maxWidthStyle,
};
}, [style, maxWidth, size]);
const composedStyles = composeFlyoutInlineStyles(
size,
layoutMode,
siblingFlyoutId,
siblingFlyoutWidth || null,
maxWidth
);

return { ...style, ...composedStyles };
}, [
style,
size,
layoutMode,
siblingFlyoutId,
siblingFlyoutWidth,
maxWidth,
]);

const styles = useEuiMemoizedStyles(euiFlyoutStyles);
const cssStyles = [
Expand Down
223 changes: 223 additions & 0 deletions packages/eui/src/components/flyout/flyout.styles.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/*
* 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.
*/

/*
* 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; 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 React from 'react';
import { render } from '../../test/rtl';
import { useEuiTheme } from '../../services';
import { euiFlyoutStyles, composeFlyoutInlineStyles } from './flyout.styles';

// Mock the flyout constants
jest.mock('./const', () => ({
isEuiFlyoutSizeNamed: jest.fn((size: string | number) => {
return ['s', 'm', 'l', 'fill'].includes(size as string);
}),
}));

describe('flyout.styles', () => {
describe('euiFlyoutStyles', () => {
const TestComponent = () => {
const { euiTheme } = useEuiTheme();
const styles = euiFlyoutStyles({
euiTheme,
colorMode: 'LIGHT',
modifications: {},
highContrastMode: false,
});
return <div data-testid="styles">{JSON.stringify(styles)}</div>;
};

it('should include fill size styles', () => {
const { getByTestId } = render(<TestComponent />);
const stylesText = getByTestId('styles').textContent;
expect(stylesText).toContain('fill');
});

it('should apply correct fill size CSS', () => {
const { getByTestId } = render(<TestComponent />);
const stylesText = getByTestId('styles').textContent;
expect(stylesText).toContain('90vw');
});

it('should include all named size styles', () => {
const { getByTestId } = render(<TestComponent />);
const stylesText = getByTestId('styles').textContent;
expect(stylesText).toContain('s');
expect(stylesText).toContain('m');
expect(stylesText).toContain('l');
expect(stylesText).toContain('fill');
});
});

describe('composeFlyoutInlineStyles - basic functionality', () => {
it('should handle custom width values (non-named sizes)', () => {
const result = composeFlyoutInlineStyles(
'400px',
'stacked',
null,
null,
undefined
);
expect(result).toEqual({ inlineSize: '400px' });
});

it('should handle fill size in stacked mode', () => {
const result = composeFlyoutInlineStyles(
'fill',
'stacked',
null,
null,
undefined
);
expect(result).toEqual({});
});

it('should calculate dynamic width for fill size in side-by-side mode', () => {
const result = composeFlyoutInlineStyles(
'fill',
'side-by-side',
'sibling-id',
300,
undefined
);
expect(result).toEqual({
inlineSize: 'calc(90vw - 300px)',
minInlineSize: '0',
});
});

it('should handle maxWidth for non-fill sizes', () => {
const result = composeFlyoutInlineStyles('m', 'stacked', null, null, 800);
expect(result).toEqual({
maxInlineSize: '800px',
});
});

it('should not apply dynamic styles when not fill size', () => {
const result = composeFlyoutInlineStyles(
'm',
'side-by-side',
'sibling-id',
300,
undefined
);
expect(result).toEqual({});
});

it('should not apply dynamic styles when not side-by-side mode', () => {
const result = composeFlyoutInlineStyles(
'fill',
'stacked',
'sibling-id',
300,
undefined
);
expect(result).toEqual({});
});
});

describe('composeFlyoutInlineStyles - maxWidth handling', () => {
it('should handle maxWidth for fill size without sibling', () => {
const result = composeFlyoutInlineStyles(
'fill',
'stacked',
null,
null,
600
);
expect(result).toEqual({
maxInlineSize: '600px',
minInlineSize: 'min(600px, 90vw)',
});
});

it('should handle maxWidth for fill size with sibling', () => {
const result = composeFlyoutInlineStyles(
'fill',
'side-by-side',
'sibling-id',
300,
600
);
expect(result).toEqual({
inlineSize: 'calc(90vw - 300px)',
maxInlineSize: 'min(600px, calc(90vw - 300px))',
minInlineSize: 'min(600px, calc(90vw - 300px))',
});
});

it('should handle string maxWidth values', () => {
const result = composeFlyoutInlineStyles(
'fill',
'stacked',
null,
null,
'50%'
);
expect(result).toEqual({
maxInlineSize: '50%',
minInlineSize: 'min(50%, 90vw)',
});
});

it('should handle boolean maxWidth (should be ignored)', () => {
const result = composeFlyoutInlineStyles(
'fill',
'stacked',
null,
null,
true
);
// Boolean maxWidth should be ignored, but the function still processes it
// because the condition `if (isFill && maxWidth)` evaluates to true for boolean true
expect(result).toEqual({
maxInlineSize: true,
minInlineSize: undefined,
});
});

it('should handle fill size with maxWidth but no sibling in side-by-side mode', () => {
// This tests the case where we're in side-by-side mode but there's no sibling
const result = composeFlyoutInlineStyles(
'fill',
'side-by-side',
null,
null,
600
);
expect(result).toEqual({
maxInlineSize: '600px',
minInlineSize: 'min(600px, 90vw)',
});
});

it('should handle maxWidth with sibling but no dynamic width calculation', () => {
// This tests the case where maxWidth is provided but dynamic width calculation
// is not applied (e.g., not fill size, not side-by-side, etc.)
const result = composeFlyoutInlineStyles(
'm',
'side-by-side',
'sibling-id',
300,
600
);
expect(result).toEqual({
maxInlineSize: '600px',
});
});
});
});
Loading