@@ -273,10 +275,34 @@ const ItemDetailsContent: React.FC = ({
);
};
+const OrderConfirmedContent: React.FC = ({
+ itemQuantity,
+}) => {
+ const { closeSession } = useEuiFlyoutSession();
+ return (
+ <>
+
+
+ Order confirmed
+ Item: Flux Capacitor
+ Quantity: {itemQuantity}
+
+ Your order has been confirmed. Check your email for details.
+
+
+
+
+ Close
+
+
+ >
+ );
+};
+
// Component for the main control buttons and state display
const ECommerceAppControls: React.FC = () => {
const {
- openFlyout,
+ openManagedFlyout,
goBack,
isFlyoutOpen,
canGoBack,
@@ -294,7 +320,9 @@ const ECommerceAppControls: React.FC = () => {
}
};
const handleOpenShoppingCart = () => {
- const options: EuiFlyoutSessionOpenMainOptions = {
+ const options: EuiFlyoutSessionOpenManagedOptions = {
+ title: 'Shopping cart',
+ hideTitle: true, // title will only show in the history popover
size: 'm',
meta: { ecommerceMainFlyoutKey: 'shoppingCart' },
flyoutProps: {
@@ -308,7 +336,7 @@ const ECommerceAppControls: React.FC = () => {
},
},
};
- openFlyout(options);
+ openManagedFlyout(options);
};
return (
@@ -356,19 +384,24 @@ const ECommerceApp: React.FC = () => {
const { meta } = context;
const { ecommerceMainFlyoutKey } = meta || {};
- if (ecommerceMainFlyoutKey === 'shoppingCart') {
- return (
-
- );
- }
- if (ecommerceMainFlyoutKey === 'reviewOrder') {
- return ;
+ switch (ecommerceMainFlyoutKey) {
+ case 'orderConfirmed':
+ return ;
+ case 'reviewOrder':
+ return ;
+ case 'shoppingCart':
+ return (
+
+ );
}
- loggerAction('renderMainFlyoutContent: Unknown flyout key', meta);
+ loggerAction(
+ 'renderMainFlyoutContent: Unknown flyout key',
+ meta?.ecommerceMainFlyoutKey
+ );
return null;
};
@@ -377,10 +410,30 @@ const ECommerceApp: React.FC = () => {
return ;
};
+ const ecommerceHistoryFilter = useCallback(
+ (
+ history: EuiFlyoutSessionHistoryState['history'],
+ activeFlyoutGroup?: EuiFlyoutSessionGroup | null
+ ) => {
+ const isOrderConfirmationActive =
+ activeFlyoutGroup?.meta?.ecommerceMainFlyoutKey === 'orderConfirmed';
+
+ // If on order confirmation page, clear history to remove "Back" button
+ if (isOrderConfirmationActive) {
+ loggerAction('Clearing history');
+ return [];
+ }
+
+ return history;
+ },
+ []
+ );
+
return (
{
loggerAction('All flyouts have been unmounted');
}}
@@ -402,6 +455,150 @@ export const ECommerceWithHistory: StoryObj = {
},
};
+/**
+ * --------------------------------------
+ * Deep History Example (advanced use case)
+ * --------------------------------------
+ */
+
+interface DeepHistoryAppMeta {
+ page: 'page01' | 'page02' | 'page03' | 'page04' | 'page05' | '';
+}
+
+const getHistoryManagedFlyoutOptions = (
+ page: DeepHistoryAppMeta['page']
+): EuiFlyoutSessionOpenManagedOptions => {
+ return {
+ title: page,
+ size: 'm',
+ meta: { page },
+ flyoutProps: {
+ type: 'push',
+ pushMinBreakpoint: 'xs',
+ 'aria-label': page,
+ },
+ };
+};
+
+const DeepHistoryPage: React.FC = ({ page }) => {
+ const { openManagedFlyout, closeSession } = useEuiFlyoutSession();
+ const [nextPage, setNextPage] = useState('');
+
+ useEffect(() => {
+ switch (page) {
+ case 'page01':
+ setNextPage('page02');
+ break;
+ case 'page02':
+ setNextPage('page03');
+ break;
+ case 'page03':
+ setNextPage('page04');
+ break;
+ case 'page04':
+ setNextPage('page05');
+ break;
+ case 'page05':
+ setNextPage('');
+ break;
+ }
+ }, [page]);
+
+ const handleOpenNextFlyout = () => {
+ const options = getHistoryManagedFlyoutOptions(nextPage);
+ openManagedFlyout(options);
+ };
+
+ return (
+ <>
+
+
+ Page {page}
+
+
+
+ {nextPage === '' ? (
+ <>
+
+
+ This is the content for {page}.
+ You have reached the end of the history.
+
+
+ >
+ ) : (
+ <>
+
+ This is the content for {page}.
+
+
+
+ Navigate to {nextPage}
+
+ >
+ )}
+
+
+
+ Close
+
+
+ >
+ );
+};
+
+// Component for the main control buttons and state display
+const DeepHistoryAppControls: React.FC = () => {
+ const { openManagedFlyout, isFlyoutOpen } = useEuiFlyoutSession();
+ const { state } = useEuiFlyoutSessionContext(); // Use internal hook for displaying raw state
+
+ const handleOpenManagedFlyout = () => {
+ const options = getHistoryManagedFlyoutOptions('page01');
+ openManagedFlyout(options);
+ };
+
+ return (
+ <>
+
+ Begin flyout navigation
+
+
+
+ >
+ );
+};
+
+const DeepHistoryApp: React.FC = () => {
+ // Render function for MAIN flyout content
+ const renderMainFlyoutContent = (
+ context: EuiFlyoutSessionRenderContext
+ ) => {
+ const { meta } = context;
+ const { page } = meta || { page: 'page01' };
+ return ;
+ };
+
+ return (
+ loggerAction('All flyouts have been unmounted')}
+ >
+
+
+ );
+};
+
+export const DeepHistory: StoryObj = {
+ name: 'Deep History Navigation',
+ render: () => {
+ return ;
+ },
+};
+
/**
* --------------------------------------
* Group opener example (simple use case)
@@ -429,6 +626,7 @@ const GroupOpenerControls: React.FC<{
}
const options: EuiFlyoutSessionOpenGroupOptions = {
main: {
+ title: 'Group opener, main flyout',
size: mainFlyoutSize,
flyoutProps: {
type: mainFlyoutType,
@@ -444,6 +642,7 @@ const GroupOpenerControls: React.FC<{
},
},
child: {
+ title: 'Group opener, child flyout',
size: childFlyoutSize,
flyoutProps: {
className: 'groupOpenerChildFlyout',
@@ -514,11 +713,6 @@ const GroupOpenerApp: React.FC = () => {
const { closeSession } = useEuiFlyoutSession();
return (
<>
-
-
- Main Flyout
-
-
@@ -540,11 +734,6 @@ const GroupOpenerApp: React.FC = () => {
const { closeChildFlyout } = useEuiFlyoutSession();
return (
<>
-
-
- Child Flyout
-
-
diff --git a/packages/eui/src/components/flyout/sessions/flyout_provider.tsx b/packages/eui/src/components/flyout/sessions/flyout_provider.tsx
index b1ba6391400..c2a06224d7f 100644
--- a/packages/eui/src/components/flyout/sessions/flyout_provider.tsx
+++ b/packages/eui/src/components/flyout/sessions/flyout_provider.tsx
@@ -6,11 +6,17 @@
* Side Public License, v 1.
*/
-import React, { createContext, useContext, useReducer } from 'react';
+import React, {
+ createContext,
+ useContext,
+ useReducer,
+ useCallback,
+} from 'react';
+import { EuiFlyoutMenu } from '../flyout_menu';
import { EuiFlyout, EuiFlyoutChild } from '../index';
-
import { flyoutReducer, initialFlyoutState } from './flyout_reducer';
+import { ManagedFlyoutMenu } from './managed_flyout_menu';
import {
EuiFlyoutSessionAction,
EuiFlyoutSessionHistoryState,
@@ -22,6 +28,7 @@ interface FlyoutSessionContextProps {
state: EuiFlyoutSessionHistoryState;
dispatch: React.Dispatch;
onUnmount?: EuiFlyoutSessionProviderComponentProps['onUnmount'];
+ historyFilter: EuiFlyoutSessionProviderComponentProps['historyFilter'];
}
const EuiFlyoutSessionContext = createContext(
@@ -58,9 +65,32 @@ export const EuiFlyoutSessionProvider: React.FC<
children,
renderMainFlyoutContent,
renderChildFlyoutContent,
+ historyFilter,
onUnmount,
}) => {
- const [state, dispatch] = useReducer(flyoutReducer, initialFlyoutState);
+ const wrappedReducer = useCallback(
+ (
+ state: EuiFlyoutSessionHistoryState,
+ action: EuiFlyoutSessionAction
+ ) => {
+ const nextState = flyoutReducer(state, action);
+
+ if (!historyFilter) return nextState;
+
+ const filteredHistory = historyFilter(
+ nextState.history || [],
+ nextState.activeFlyoutGroup
+ );
+
+ return {
+ ...nextState,
+ history: filteredHistory,
+ };
+ },
+ [historyFilter]
+ );
+
+ const [state, dispatch] = useReducer(wrappedReducer, initialFlyoutState);
const { activeFlyoutGroup } = state;
const handleClose = () => {
@@ -71,6 +101,14 @@ export const EuiFlyoutSessionProvider: React.FC<
dispatch({ type: 'CLOSE_CHILD_FLYOUT' });
};
+ const handleGoBack = () => {
+ dispatch({ type: 'GO_BACK' });
+ };
+
+ const handleGoToHistoryItem = (index: number) => {
+ dispatch({ type: 'GO_TO_HISTORY_ITEM', index });
+ };
+
let mainFlyoutContentNode: React.ReactNode = null;
let childFlyoutContentNode: React.ReactNode = null;
@@ -95,7 +133,9 @@ export const EuiFlyoutSessionProvider: React.FC<
const flyoutPropsChild = config?.childFlyoutProps || {};
return (
-
+
{children}
{activeFlyoutGroup?.isMainOpen && (
+ {config?.isManaged && (
+
+ )}
{mainFlyoutContentNode}
{activeFlyoutGroup.isChildOpen && childFlyoutContentNode && (
+
{childFlyoutContentNode}
)}
diff --git a/packages/eui/src/components/flyout/sessions/flyout_reducer.ts b/packages/eui/src/components/flyout/sessions/flyout_reducer.ts
index 2e7ae9f21a1..ee89b36a4a7 100644
--- a/packages/eui/src/components/flyout/sessions/flyout_reducer.ts
+++ b/packages/eui/src/components/flyout/sessions/flyout_reducer.ts
@@ -54,6 +54,19 @@ const applySizeConstraints = (
};
};
+/**
+ * Helper to merge meta objects from current state and incoming action
+ * @internal
+ */
+const mergeMeta = (
+ currentMeta: FlyoutMeta | undefined,
+ newMeta: FlyoutMeta | undefined
+): FlyoutMeta | undefined => {
+ if (newMeta === undefined) return currentMeta;
+ if (currentMeta === undefined) return newMeta;
+ return { ...currentMeta, ...newMeta };
+};
+
/**
* Flyout reducer
* Controls state changes for flyout groups
@@ -68,7 +81,7 @@ export function flyoutReducer(
const newHistory = [...state.history];
if (state.activeFlyoutGroup) {
- newHistory.push(state.activeFlyoutGroup);
+ newHistory.unshift(state.activeFlyoutGroup);
}
const newActiveGroup: EuiFlyoutSessionGroup = {
@@ -78,7 +91,34 @@ export function flyoutReducer(
mainSize: size,
mainFlyoutProps: flyoutProps,
},
- meta,
+ meta: mergeMeta(state.activeFlyoutGroup?.meta, meta),
+ };
+
+ return {
+ activeFlyoutGroup: applySizeConstraints(newActiveGroup),
+ history: newHistory,
+ };
+ }
+
+ case 'OPEN_MANAGED_FLYOUT': {
+ const { size, title, hideTitle, flyoutProps, meta } = action.payload; // EuiFlyoutSessionOpenManagedOptions
+ const newHistory = [...state.history];
+
+ if (state.activeFlyoutGroup) {
+ newHistory.unshift(state.activeFlyoutGroup);
+ }
+
+ const newActiveGroup: EuiFlyoutSessionGroup = {
+ isMainOpen: true,
+ isChildOpen: false,
+ config: {
+ isManaged: true,
+ mainSize: size,
+ mainTitle: title,
+ hideMainTitle: hideTitle,
+ mainFlyoutProps: flyoutProps,
+ },
+ meta: mergeMeta(state.activeFlyoutGroup?.meta, meta),
};
return {
@@ -95,16 +135,17 @@ export function flyoutReducer(
return state;
}
- const { size, flyoutProps, meta } = action.payload;
+ const { size, flyoutProps, title, meta } = action.payload;
const updatedActiveGroup: EuiFlyoutSessionGroup = {
...state.activeFlyoutGroup,
isChildOpen: true,
config: {
- ...state.activeFlyoutGroup.config,
+ ...state.activeFlyoutGroup.config, // retain main flyout config
+ childTitle: title,
childSize: size,
childFlyoutProps: flyoutProps,
},
- meta,
+ meta: mergeMeta(state.activeFlyoutGroup?.meta, meta),
};
return {
@@ -118,7 +159,7 @@ export function flyoutReducer(
const newHistory = [...state.history];
if (state.activeFlyoutGroup) {
- newHistory.push(state.activeFlyoutGroup);
+ newHistory.unshift(state.activeFlyoutGroup);
}
// Create the new active group with both main and child flyouts open
@@ -126,12 +167,16 @@ export function flyoutReducer(
isMainOpen: true,
isChildOpen: true,
config: {
+ isManaged: true,
mainSize: main.size,
+ mainTitle: main.title,
+ hideMainTitle: main.hideTitle,
+ childTitle: child.title,
childSize: child.size,
mainFlyoutProps: main.flyoutProps,
childFlyoutProps: child.flyoutProps,
},
- meta,
+ meta: mergeMeta(state.activeFlyoutGroup?.meta, meta),
};
return {
@@ -163,6 +208,19 @@ export function flyoutReducer(
};
}
+ case 'GO_TO_HISTORY_ITEM': {
+ const { index } = action;
+ const targetGroup = state.history[index];
+ const newHistory = state.history.slice(index + 1);
+
+ return {
+ activeFlyoutGroup: targetGroup
+ ? applySizeConstraints(targetGroup)
+ : state.activeFlyoutGroup,
+ history: newHistory,
+ };
+ }
+
case 'GO_BACK': {
if (!state.activeFlyoutGroup)
return initialFlyoutState as EuiFlyoutSessionHistoryState;
@@ -170,7 +228,7 @@ export function flyoutReducer(
// Restore from history or return to initial state
if (state.history.length > 0) {
const newHistory = [...state.history];
- const previousGroup = newHistory.pop();
+ const previousGroup = newHistory.shift();
return {
activeFlyoutGroup: previousGroup
? applySizeConstraints(previousGroup)
diff --git a/packages/eui/src/components/flyout/sessions/index.ts b/packages/eui/src/components/flyout/sessions/index.ts
index 899c423ea4f..444a3e4f37a 100644
--- a/packages/eui/src/components/flyout/sessions/index.ts
+++ b/packages/eui/src/components/flyout/sessions/index.ts
@@ -17,6 +17,7 @@ export type {
EuiFlyoutSessionOpenChildOptions,
EuiFlyoutSessionOpenGroupOptions,
EuiFlyoutSessionOpenMainOptions,
+ EuiFlyoutSessionOpenManagedOptions,
EuiFlyoutSessionProviderComponentProps,
EuiFlyoutSessionRenderContext,
} from './types';
diff --git a/packages/eui/src/components/flyout/sessions/managed_flyout_menu.test.tsx b/packages/eui/src/components/flyout/sessions/managed_flyout_menu.test.tsx
new file mode 100644
index 00000000000..ba3d51e9234
--- /dev/null
+++ b/packages/eui/src/components/flyout/sessions/managed_flyout_menu.test.tsx
@@ -0,0 +1,92 @@
+/*
+ * 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 React from 'react';
+import { fireEvent } from '@testing-library/react';
+import { render } from '../../../test/rtl';
+import { ManagedFlyoutMenu } from './managed_flyout_menu';
+import { EuiFlyoutSessionGroup } from './types';
+
+describe('FlyoutSystemMenu', () => {
+ const mockHistoryItems: Array> = [
+ {
+ isMainOpen: true,
+ isChildOpen: false,
+ config: { mainSize: 's', mainTitle: 'History Item 1' },
+ },
+ {
+ isMainOpen: true,
+ isChildOpen: false,
+ config: { mainSize: 'm', mainTitle: 'History Item 2' },
+ },
+ ];
+
+ it('renders with a title', () => {
+ const { getByText } = render(
+ {}}
+ handleGoToHistoryItem={() => {}}
+ />
+ );
+ expect(getByText('Test Title')).toBeInTheDocument();
+ });
+
+ it('renders without a title', () => {
+ const { queryByText } = render(
+ {}}
+ handleGoToHistoryItem={() => {}}
+ />
+ );
+ expect(queryByText('Test Title')).not.toBeInTheDocument();
+ });
+
+ it('renders with back button and history popover when history items are present', () => {
+ const { getByText, getByLabelText } = render(
+ {}}
+ handleGoToHistoryItem={() => {}}
+ />
+ );
+ expect(getByText('Back')).toBeInTheDocument();
+ expect(getByLabelText('History')).toBeInTheDocument();
+ });
+
+ it('calls handleGoBack when back button is clicked', () => {
+ const handleGoBack = jest.fn();
+ const { getByText } = render(
+ {}}
+ />
+ );
+ fireEvent.click(getByText('Back'));
+ expect(handleGoBack).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls handleGoToHistoryItem when a history item is clicked', () => {
+ const handleGoToHistoryItem = jest.fn();
+ const { getByLabelText, getByText } = render(
+ {}}
+ handleGoToHistoryItem={handleGoToHistoryItem}
+ />
+ );
+
+ fireEvent.click(getByLabelText('History'));
+ fireEvent.click(getByText('History Item 1'));
+
+ expect(handleGoToHistoryItem).toHaveBeenCalledWith(0);
+ });
+});
diff --git a/packages/eui/src/components/flyout/sessions/managed_flyout_menu.tsx b/packages/eui/src/components/flyout/sessions/managed_flyout_menu.tsx
new file mode 100644
index 00000000000..ebeea27647f
--- /dev/null
+++ b/packages/eui/src/components/flyout/sessions/managed_flyout_menu.tsx
@@ -0,0 +1,89 @@
+/*
+ * 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 React, { useState } from 'react';
+
+import { EuiButtonEmpty, EuiButtonIcon } from '../../button';
+import { EuiIcon } from '../../icon';
+import { EuiListGroup } from '../../list_group';
+import { EuiListGroupItem } from '../../list_group/list_group_item';
+import { EuiPopover } from '../../popover';
+import { EuiFlyoutMenu, EuiFlyoutMenuProps } from '../flyout_menu';
+import { EuiFlyoutSessionGroup } from './types';
+
+/**
+ * Top flyout menu bar
+ * This automatically appears for "managed flyouts" (those that were opened with `openManagedFlyout`),
+ * @internal
+ */
+export const ManagedFlyoutMenu = (
+ props: Pick & {
+ handleGoBack: () => void;
+ handleGoToHistoryItem: (index: number) => void;
+ historyItems: Array>;
+ }
+) => {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+ const { title, historyItems, handleGoBack, handleGoToHistoryItem } = props;
+
+ let backButton: React.ReactNode | undefined;
+ let historyPopover: React.ReactNode | undefined;
+
+ if (!!historyItems.length) {
+ const handlePopoverButtonClick = () => {
+ setIsPopoverOpen(!isPopoverOpen);
+ };
+
+ backButton = (
+
+ Back
+
+ );
+
+ historyPopover = (
+
+ }
+ isOpen={isPopoverOpen}
+ closePopover={() => setIsPopoverOpen(false)}
+ panelPaddingSize="xs"
+ anchorPosition="downLeft"
+ >
+
+ {historyItems.map((item, index) => (
+ {
+ handleGoToHistoryItem(index);
+ setIsPopoverOpen(false);
+ }}
+ >
+ {item.config.mainTitle}
+
+ ))}
+
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/packages/eui/src/components/flyout/sessions/types.ts b/packages/eui/src/components/flyout/sessions/types.ts
index 97c611ee9ac..a351396e232 100644
--- a/packages/eui/src/components/flyout/sessions/types.ts
+++ b/packages/eui/src/components/flyout/sessions/types.ts
@@ -6,17 +6,24 @@
* Side Public License, v 1.
*/
-import { EuiFlyoutProps, EuiFlyoutSize } from '../flyout';
-import { EuiFlyoutChildProps } from '../flyout_child';
+import type { EuiFlyoutProps, EuiFlyoutSize } from '../flyout';
+import type { EuiFlyoutChildProps } from '../flyout_child';
/**
* Configuration used for setting display options for main and child flyouts in a session.
*/
export interface EuiFlyoutSessionConfig {
mainSize: EuiFlyoutSize;
- childSize?: 's' | 'm';
+ mainTitle?: string;
+ hideMainTitle?: boolean;
+ childSize?: 's' | 'm' | 'fill';
+ childTitle?: string;
mainFlyoutProps?: Partial>;
childFlyoutProps?: Partial>;
+ /**
+ * Indicates if the flyout was opened with openManagedFlyout or openFlyout
+ */
+ isManaged?: boolean;
}
/**
@@ -31,12 +38,31 @@ export interface EuiFlyoutSessionOpenMainOptions {
meta?: Meta;
}
+export interface EuiFlyoutSessionOpenManagedOptions {
+ size: EuiFlyoutSize;
+ flyoutProps?: EuiFlyoutSessionConfig['mainFlyoutProps'];
+ /**
+ * Title to display in top menu bar and in the options of the history popover
+ */
+ title: string;
+ /**
+ * Allows title to be hidden from top menu bar. If this is true,
+ * the title will only be used for the history popover
+ */
+ hideTitle?: boolean;
+ /**
+ * Caller-defined data
+ */
+ meta?: Meta;
+}
+
/**
* Options that control a child flyout in a session
*/
export interface EuiFlyoutSessionOpenChildOptions {
- size: 's' | 'm';
+ size: 's' | 'm' | 'fill';
flyoutProps?: EuiFlyoutSessionConfig['childFlyoutProps'];
+ title: string;
/**
* Caller-defined data
*/
@@ -47,7 +73,7 @@ export interface EuiFlyoutSessionOpenChildOptions {
* Options for opening both a main flyout and child flyout simultaneously
*/
export interface EuiFlyoutSessionOpenGroupOptions {
- main: EuiFlyoutSessionOpenMainOptions;
+ main: EuiFlyoutSessionOpenManagedOptions;
child: EuiFlyoutSessionOpenChildOptions;
/**
* Caller-defined data
@@ -89,6 +115,10 @@ export type EuiFlyoutSessionAction =
type: 'OPEN_MAIN_FLYOUT';
payload: EuiFlyoutSessionOpenMainOptions;
}
+ | {
+ type: 'OPEN_MANAGED_FLYOUT';
+ payload: EuiFlyoutSessionOpenManagedOptions;
+ }
| {
type: 'OPEN_CHILD_FLYOUT';
payload: EuiFlyoutSessionOpenChildOptions;
@@ -98,6 +128,7 @@ export type EuiFlyoutSessionAction =
payload: EuiFlyoutSessionOpenGroupOptions;
}
| { type: 'GO_BACK' }
+ | { type: 'GO_TO_HISTORY_ITEM'; index: number }
| { type: 'CLOSE_CHILD_FLYOUT' }
| { type: 'CLOSE_SESSION' };
@@ -117,16 +148,21 @@ export interface EuiFlyoutSessionRenderContext {
*/
export interface EuiFlyoutSessionProviderComponentProps {
children: React.ReactNode;
- onUnmount?: () => void;
renderMainFlyoutContent: (
context: EuiFlyoutSessionRenderContext
) => React.ReactNode;
renderChildFlyoutContent?: (
context: EuiFlyoutSessionRenderContext
) => React.ReactNode;
+ historyFilter?: (
+ history: EuiFlyoutSessionHistoryState['history'],
+ activeFlyoutGroup?: EuiFlyoutSessionGroup | null
+ ) => EuiFlyoutSessionHistoryState['history'];
+ onUnmount?: () => void;
}
export interface EuiFlyoutSessionApi {
+ openManagedFlyout: (options: EuiFlyoutSessionOpenManagedOptions) => void;
openFlyout: (options: EuiFlyoutSessionOpenMainOptions) => void;
openChildFlyout: (options: EuiFlyoutSessionOpenChildOptions) => void;
openFlyoutGroup: (options: EuiFlyoutSessionOpenGroupOptions) => void;
diff --git a/packages/eui/src/components/flyout/sessions/use_eui_flyout.test.tsx b/packages/eui/src/components/flyout/sessions/use_eui_flyout.test.tsx
index e7ef635a8b0..f2b6c145050 100644
--- a/packages/eui/src/components/flyout/sessions/use_eui_flyout.test.tsx
+++ b/packages/eui/src/components/flyout/sessions/use_eui_flyout.test.tsx
@@ -6,14 +6,14 @@
* Side Public License, v 1.
*/
+import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
-import { render, fireEvent, screen } from '@testing-library/react';
import { EuiFlyoutSessionProvider } from './flyout_provider';
import type {
- EuiFlyoutSessionOpenMainOptions,
EuiFlyoutSessionOpenChildOptions,
EuiFlyoutSessionOpenGroupOptions,
+ EuiFlyoutSessionOpenMainOptions,
} from './types';
import { useEuiFlyoutSession } from './use_eui_flyout';
@@ -79,6 +79,7 @@ const TestComponent: React.FC = ({
data-testid="openChildFlyoutButton"
onClick={() => {
const options: EuiFlyoutSessionOpenChildOptions = {
+ title: 'Child flyout',
size: 's',
meta: { type: 'testChild' },
};
@@ -95,10 +96,12 @@ const TestComponent: React.FC = ({
onClick={() => {
const options: EuiFlyoutSessionOpenGroupOptions = {
main: {
+ title: 'Main flyout',
size: 'm',
flyoutProps: { className: 'main-flyout' },
},
child: {
+ title: 'Child flyout',
size: 's',
flyoutProps: { className: 'child-flyout' },
},
diff --git a/packages/eui/src/components/flyout/sessions/use_eui_flyout.ts b/packages/eui/src/components/flyout/sessions/use_eui_flyout.ts
index b2192b5049a..67a15a531f0 100644
--- a/packages/eui/src/components/flyout/sessions/use_eui_flyout.ts
+++ b/packages/eui/src/components/flyout/sessions/use_eui_flyout.ts
@@ -13,6 +13,7 @@ import type {
EuiFlyoutSessionOpenChildOptions,
EuiFlyoutSessionOpenGroupOptions,
EuiFlyoutSessionOpenMainOptions,
+ EuiFlyoutSessionOpenManagedOptions,
} from './types';
/**
@@ -33,6 +34,9 @@ export function useEuiFlyoutSession(): EuiFlyoutSessionApi {
}
}, [state.activeFlyoutGroup, onUnmount]);
+ /**
+ * Open a "plain" main flyout without an automatic top menu bar
+ */
const openFlyout = (options: EuiFlyoutSessionOpenMainOptions) => {
dispatch({
type: 'OPEN_MAIN_FLYOUT',
@@ -40,6 +44,19 @@ export function useEuiFlyoutSession(): EuiFlyoutSessionApi {
});
};
+ /**
+ * Open a "managed" main flyout, with an automatic top menu bar
+ */
+ const openManagedFlyout = (options: EuiFlyoutSessionOpenManagedOptions) => {
+ dispatch({
+ type: 'OPEN_MANAGED_FLYOUT',
+ payload: options,
+ });
+ };
+
+ /**
+ * Open a "managed" child flyout, with an automatic top menu bar
+ */
const openChildFlyout = (options: EuiFlyoutSessionOpenChildOptions) => {
if (!state.activeFlyoutGroup || !state.activeFlyoutGroup.isMainOpen) {
console.warn(
@@ -53,6 +70,9 @@ export function useEuiFlyoutSession(): EuiFlyoutSessionApi {
});
};
+ /**
+ * Open a pair of managed main and child flyouts
+ */
const openFlyoutGroup = (options: EuiFlyoutSessionOpenGroupOptions) => {
dispatch({
type: 'OPEN_FLYOUT_GROUP',
@@ -80,6 +100,7 @@ export function useEuiFlyoutSession(): EuiFlyoutSessionApi {
return {
openFlyout,
+ openManagedFlyout,
openChildFlyout,
openFlyoutGroup,
closeChildFlyout,
diff --git a/packages/website/docs/components/containers/flyout/index.mdx b/packages/website/docs/components/containers/flyout/index.mdx
index 07ada387f79..2d9f89ac4b7 100644
--- a/packages/website/docs/components/containers/flyout/index.mdx
+++ b/packages/website/docs/components/containers/flyout/index.mdx
@@ -1001,6 +1001,25 @@ The `EuiFlyoutChild` must include an `EuiFlyoutBody` child and can only be used
Both parent and child flyouts use `role="dialog"` and `aria-modal="true"` for accessibility. Focus is managed automatically between them, with the child flyout taking focus when open and returning focus to the parent when closed.
+### Flyout menu (Beta)
+
+:::info Note
+This component is still in beta and may change in the future.
+:::
+
+Use `EuiFlyoutChild` to create a nested flyout that aligns to the left edge of a parent `EuiFlyout`. On smaller screens, the child flyout stacks above the parent.
+
+```tsx
+
+
+ Hi mom
+
+ Parent header
+ Parent body
+ Parent footer
+
+```
+
## Props
import docgen from '@elastic/eui-docgen/dist/components/flyout';