Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
78efb04
plan for flyout menu action follow-up
tsullivan Jul 30, 2026
5f38af6
support for leading and trailing actions / control-group dividers
tsullivan Jul 30, 2026
d0e7309
fix for https://github.com/elastic/eui/issues/9304
tsullivan Jul 30, 2026
9c33a0f
improve tests
tsullivan Jul 30, 2026
0f7dd11
fix action icons height baseline
tsullivan Jul 31, 2026
35023e2
cleanup
tsullivan Jul 31, 2026
b1f1f06
pagination fixes
tsullivan Jul 31, 2026
bd1471e
high contrast fixes
tsullivan Jul 31, 2026
36ae2ed
jump-style pagination mode
tsullivan Jul 31, 2026
9d9b2f0
chore(eui): add VRT baseline screenshots
kibanamachine Jul 31, 2026
73bc634
cleanup
tsullivan Jul 31, 2026
d78678d
Merge branch 'flyouts/menu-actions-work' of github.com:tsullivan/eui …
tsullivan Jul 31, 2026
182ef8a
cleanup
tsullivan Jul 31, 2026
2632a1c
i18ntokens
tsullivan Jul 31, 2026
a38761d
cleanup
tsullivan Jul 31, 2026
458d892
chore(eui): update VRT baseline screenshots
kibanamachine Aug 3, 2026
4d293df
chore(eui): update VRT baseline screenshots
kibanamachine Aug 3, 2026
af137fb
Merge branch 'flyouts/menu-actions-work' of github.com:tsullivan/eui …
tsullivan Aug 3, 2026
ad581ad
Merge branch 'main' into flyouts/menu-actions-work
tsullivan Aug 3, 2026
75fef81
storybook description terminology
tsullivan Aug 3, 2026
520eb7e
remove hash router link from doc
tsullivan Aug 3, 2026
a8d45b0
disable hideTitle and hideCloseButton in story argTypes
tsullivan Aug 4, 2026
6914089
enforce comment etiquette
tsullivan Aug 4, 2026
3c0e2d7
plainer typing
tsullivan Aug 4, 2026
2c78cfe
updates from feedback
tsullivan Aug 4, 2026
97b4395
Merge branch 'flyouts/menu-actions-work' of github.com:tsullivan/eui …
tsullivan Aug 4, 2026
81989f1
fix tests
tsullivan Aug 4, 2026
19b992e
clean up unrelated VRT artifact
tsullivan Aug 4, 2026
324a71b
split up large flyout_menu component
tsullivan Aug 4, 2026
5b44212
cleanup hide title and hide close button
tsullivan Aug 4, 2026
235d974
post menu component split up: centralize translation strings
tsullivan Aug 4, 2026
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions packages/eui/changelogs/upcoming/9856.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
- Updated `EuiFlyoutMenu`:
- Added `leadingActions` and `trailingActions` props, which accept the new `EuiFlyoutMenuAction` type supporting `toolTipContent` and `toolTipProps`
- Changed the history trigger icon to `clockCounter`
- Added control-group dividers between built-in controls
- Added `previousIconType` and `nextIconType` to `pagination`, so consumers can override the default up/down chevrons with any icon type
- Added `onFirst`, `onLast`, `firstIconType`, and `lastIconType` to `pagination`, which render optional buttons for jumping to the beginning and end of the list.

**Bug fixes**

- The `EuiFlyoutMenu` history popover now only appears when `historyItems` has more than one entry. When there is a single entry in history, only the Back button is shown.
- Fixed `EuiFlyoutMenu` pagination controls disappearing when `pagination.total` is `1`; the controls now render (as "1 of 1" with both Prev/Next disabled) for any `total` of at least `1`

**Deprecations**

- Deprecated `customActions` and `EuiFlyoutMenuCustomAction` in `EuiFlyoutMenu`; use `trailingActions` and `EuiFlyoutMenuAction` instead

@weronikaolejniczak weronikaolejniczak Aug 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tsullivan we should add this to our deprecation schedule to be removed, maybe in 3 months?

Original file line number Diff line number Diff line change
Expand Up @@ -95,57 +95,3 @@ exports[`EuiContextMenuItem renders 1`] = `
</span>
</a>
`;

exports[`EuiContextMenuItem tooltip behavior 1`] = `
<body
class="euiBody-hasPortalContent"
>
<div>
<span
class="euiToolTipAnchor eui-displayBlock emotion-euiToolTipAnchor-inlineBlock"
id="generated-id-wrapper"
>
<button
aria-describedby="generated-id"
class="euiListItemLayout euiContextMenuItem emotion-euiListItemLayout-isInteractive-euiContextMenuItem-center"
data-test-subj="euiListItemLayout"
type="button"
>
<span
class="euiListItemLayout__content emotion-euiListItemLayout__content"
>
<span
class="euiListItemLayout__text euiContextMenuItem__text emotion-euiListItemLayout__text-wrap-euiContextMenuItem__text"
>
Hello
</span>
</span>
</button>
</span>
</div>
<div
data-euiportal="true"
>
<div
class="euiToolTipPopover euiToolTip emotion-euiToolTip-right"
data-position="right"
id="generated-id"
role="tooltip"
style="top: -10px; left: 16px;"
>
<div
class="euiToolTip__title emotion-euiToolTip__title"
>
Test
</div>
<div
class="euiToolTip__arrow emotion-euiToolTip__arrow-right"
style="top: 3px; right: 100%;"
/>
<div>
tooltip content
</div>
</div>
</div>
</body>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,16 @@ import { shouldRenderCustomStyles } from '../../test/internal';
import { requiredProps } from '../../test/required_props';

import { EuiContextMenuItem } from './context_menu_item';
import { toolTipManager } from '../tool_tip/tool_tip_manager';

describe('EuiContextMenuItem', () => {
// The manager is a module-level singleton that suppresses the entry animation
// for a tooltip opened shortly after another closed. Without a reset, one
// test's tooltip leaks an inline `animation: none` into the next one.
beforeEach(() => {
toolTipManager.reset();
});

shouldRenderCustomStyles(<EuiContextMenuItem />);

shouldRenderCustomStyles(
Expand Down Expand Up @@ -143,18 +151,56 @@ describe('EuiContextMenuItem', () => {
});
});

test('tooltip behavior', () => {
const { getByRole, baseElement } = render(
<EuiContextMenuItem
toolTipContent="tooltip content"
toolTipProps={{ title: 'Test', position: 'top' }}
>
Hello
</EuiContextMenuItem>
);
describe('tooltip behavior', () => {
const renderWithToolTip = () =>
render(
<EuiContextMenuItem
toolTipContent="tooltip content"
toolTipProps={{ title: 'Test', position: 'top' }}
>
Hello
</EuiContextMenuItem>
);

it('renders no tooltip until the item is hovered', () => {
const { queryByRole } = renderWithToolTip();

expect(queryByRole('tooltip')).not.toBeInTheDocument();
});

it('shows the tooltip title and content on hover', () => {
const { getByRole } = renderWithToolTip();

fireEvent.mouseOver(getByRole('button'));

fireEvent.mouseOver(getByRole('button'));
const tooltip = getByRole('tooltip');
expect(tooltip).toHaveTextContent('Test');
expect(tooltip).toHaveTextContent('tooltip content');
});

it('describes the item with the tooltip for assistive technology', () => {
const { getByRole } = renderWithToolTip();

fireEvent.mouseOver(getByRole('button'));

expect(getByRole('button')).toHaveAttribute(
'aria-describedby',
getByRole('tooltip').id
);
});

expect(baseElement).toMatchSnapshot();
it('forces the tooltip to the right, ignoring `toolTipProps.position`', () => {
const { getByRole } = renderWithToolTip();

fireEvent.mouseOver(getByRole('button'));

expect(getByRole('tooltip')).toHaveAttribute('data-position', 'right');
});

it('renders as a button so the tooltip is keyboard reachable', () => {
const { getByRole } = renderWithToolTip();

expect(getByRole('button')).toHaveAttribute('type', 'button');
});
});
});
7 changes: 6 additions & 1 deletion packages/eui/src/components/flyout/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export type EuiFlyoutSize = (typeof FLYOUT_SIZES)[number];

/** Menu display mode: always render menu when flyoutMenuProps is provided. */
export const MENU_DISPLAY_ALWAYS = 'always' as const;
/** Menu display mode: only render menu when it has content (back button, history, title, or custom actions). */
/** Menu display mode: only render menu when it has content (back button, history, title, or actions). */
export const MENU_DISPLAY_AUTO = 'auto' as const;
/** Allowed flyout menu display modes. */
export const FLYOUT_MENU_DISPLAY_MODES = [
Expand All @@ -36,6 +36,11 @@ export const FLYOUT_MENU_DISPLAY_MODES = [
export type EuiFlyoutMenuDisplayMode =
(typeof FLYOUT_MENU_DISPLAY_MODES)[number];

/**
* Minimum number of history items required to render the flyout menu's history popover.
*/
export const MIN_HISTORY_ITEMS = 2;

/** Allowed padding sizes for flyout content. */
export const FLYOUT_PADDING_SIZES = ['none', 's', 'm', 'l'] as const;
/** Type representing a supported flyout padding size. */
Expand Down
14 changes: 8 additions & 6 deletions packages/eui/src/components/flyout/flyout.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { DEFAULT_MENU_DISPLAY_MODE, FLYOUT_MENU_DISPLAY_MODES } from './const';

interface FlyoutStoryArgs extends EuiFlyoutProps {
onToggle?: (open: boolean) => void;
showCustomActions?: boolean;
showTrailingActions?: boolean;
}

const meta: Meta<FlyoutStoryArgs> = {
Expand All @@ -38,7 +38,7 @@ const meta: Meta<FlyoutStoryArgs> = {
control: { type: 'radio' },
description: 'The display mode of the flyout menu.',
},
showCustomActions: { control: 'boolean' },
showTrailingActions: { control: 'boolean' },
},
args: {
// Component defaults
Expand All @@ -53,7 +53,7 @@ const meta: Meta<FlyoutStoryArgs> = {
hideCloseButton: false,
ownFocus: true,
flyoutMenuDisplayMode: DEFAULT_MENU_DISPLAY_MODE,
showCustomActions: true,
showTrailingActions: true,
},
parameters: {
vrt: {
Expand All @@ -68,16 +68,18 @@ type Story = StoryObj<EuiFlyoutProps>;

const onClose = action('onClose');

const customActions = [
const trailingActions = [
{
iconType: 'gear',
onClick: () => action('Settings clicked')(),
'aria-label': 'Settings',
toolTipContent: 'Settings',
},
];

const StatefulFlyout = (props: Partial<FlyoutStoryArgs>) => {
const { onToggle, flyoutMenuDisplayMode, showCustomActions, ...rest } = props;
const { onToggle, flyoutMenuDisplayMode, showTrailingActions, ...rest } =
props;
const [_isOpen, setIsOpen] = useState(true);

const handleToggle = (open: boolean) => {
Expand All @@ -94,7 +96,7 @@ const StatefulFlyout = (props: Partial<FlyoutStoryArgs>) => {
<EuiFlyout
flyoutMenuDisplayMode={flyoutMenuDisplayMode}
flyoutMenuProps={{
customActions: showCustomActions ? customActions : undefined,
trailingActions: showTrailingActions ? trailingActions : undefined,
}}
{...rest}
onClose={() => {
Expand Down
41 changes: 40 additions & 1 deletion packages/eui/src/components/flyout/flyout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ describe('EuiFlyout', () => {
<EuiFlyout
onClose={() => {}}
flyoutMenuProps={{
customActions: [
trailingActions: [
{
iconType: 'gear',
onClick: () => {},
Expand Down Expand Up @@ -941,6 +941,45 @@ describe('EuiFlyout', () => {
expect(dialog).toHaveAttribute('aria-label', 'Test flyout');
expect(dialog).toHaveAttribute('data-managed-flyout-level', 'main');
});

it('only renders the history popover once more than two sessions are in history', () => {
const sharedKey = Symbol();
const Sessions = ({ count }: { count: number }) => (
<EuiFlyoutManager>
{Array.from({ length: count }, (_, index) => (
<EuiFlyout
key={index}
session="start"
historyKey={sharedKey}
onClose={() => {}}
flyoutMenuProps={{ title: `Session ${index + 1}` }}
aria-label={`Session ${index + 1}`}
>
Content
</EuiFlyout>
))}
</EuiFlyoutManager>
);

const { queryAllByTestSubject, rerender } = render(
<Sessions count={2} />
);

// Two sessions leaves a single history entry, which the back button
// already navigates to
expect(
queryAllByTestSubject('euiFlyoutMenuBackButton').length
).toBeGreaterThan(0);
expect(queryAllByTestSubject('euiFlyoutMenuHistoryButton')).toHaveLength(
0
);

rerender(<Sessions count={3} />);

expect(
queryAllByTestSubject('euiFlyoutMenuHistoryButton').length
).toBeGreaterThan(0);
});
});

describe('ref forwarding', () => {
Expand Down
Loading