Skip to content
Open
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
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