diff --git a/packages/eui/changelogs/upcoming/8846.md b/packages/eui/changelogs/upcoming/8846.md new file mode 100644 index 00000000000..66154da50c7 --- /dev/null +++ b/packages/eui/changelogs/upcoming/8846.md @@ -0,0 +1,5 @@ +- Updates to `EuiFlyoutSessionProvider` + * Remove the onUnmount callbacks from various flyout configurations + * Consolidate unmount behavior with a single onUnmount prop at the provider level. + * Removed onCloseFlyout and onCloseChildFlyout from the flyout render context. + * Fixed the canGoBack logic in packages/eui/src/components/flyout/sessions/use_eui_flyout.ts. diff --git a/packages/eui/src/components/flyout/index.ts b/packages/eui/src/components/flyout/index.ts index a4084092d4c..f0e2af53cec 100644 --- a/packages/eui/src/components/flyout/index.ts +++ b/packages/eui/src/components/flyout/index.ts @@ -32,5 +32,6 @@ export type { EuiFlyoutSessionOpenMainOptions, EuiFlyoutSessionProviderComponentProps, EuiFlyoutSessionRenderContext, + EuiFlyoutSessionApi, } from './sessions'; export { EuiFlyoutSessionProvider, useEuiFlyoutSession } from './sessions'; diff --git a/packages/eui/src/components/flyout/sessions/README.md b/packages/eui/src/components/flyout/sessions/README.md index dc8cc0692c7..f87412f9b40 100644 --- a/packages/eui/src/components/flyout/sessions/README.md +++ b/packages/eui/src/components/flyout/sessions/README.md @@ -18,10 +18,7 @@ The `EuiFlyoutSessionProvider` is the core stateful component. You must wrap it The `flyoutContext` object passed to your render prop functions is of type `EuiFlyoutSessionRenderContext` and contains the following useful properties: * **`meta`**: `MetaType` - The arbitrary data object you passed into the `meta` property when calling `openFlyout` or `openChildFlyout`. This is a generic, allowing you to have type safety for your custom data. -* **`onCloseFlyout`**: `() => void` - A callback function that closes the current main flyout. -* **`onCloseChildFlyout`**: `() => void` - A callback function that closes the current child flyout. -* **`flyoutSize`**: The size of the currently active flyout. -* **`flyoutType`**: `'main' | 'child'` - Indicates which flyout the render prop is being called for. +* **`activeFlyoutGroup`**: `EuiFlyoutSessionGroup` - The currently active flyout group. ## `useEuiFlyoutSession` Hook API @@ -29,11 +26,11 @@ The `useEuiFlyoutSession` hook is generic and can be typed to match the `meta` o ### Methods -* `openFlyout(options)`: Opens a new main flyout. If a flyout is already open, it adds the new one to a history stack. -* `openChildFlyout(options)`: Opens a new child flyout to the left of the main flyout. -* `openFlyoutGroup(options)`: Opens a group containing a main flyout and a child flyout. -* `closeFlyout()`: Closes the currently open main flyout. If there's a previous flyout in the history stack, it will be shown. +* `openFlyout(options: EuiFlyoutSessionOpenMainOptions)`: Opens a new main flyout. If a flyout is already open, it adds the new one to a history stack. +* `openChildFlyout(options: EuiFlyoutSessionOpenChildOptions)`: Opens a new child flyout to the left of the main flyout. +* `openFlyoutGroup(options: EuiFlyoutSessionOpenGroupOptions)`: Opens a group containing a main flyout and a child flyout. * `closeChildFlyout()`: Closes the currently open child flyout. +* `goBack()`: If there's a previous flyout in the history stack, it will be shown. * `clearHistory()`: Closes all flyouts by clearing the history stack of all flyouts in the session. ### State Values @@ -55,6 +52,8 @@ import { EuiButton, EuiFlyoutBody, EuiText, + EuiFlyoutHeader, + EuiTitle, EuiFlyoutSessionProvider, useEuiFlyoutSession, } from '@elastic/eui'; @@ -67,6 +66,7 @@ const FlyoutAppControls: React.FC = () => { // will add the new flyout to the history stack. openFlyout({ size: 'm', + meta: { title: 'My Flyout' }, }); }; @@ -81,12 +81,19 @@ const FlyoutApp: React.FC = () => { // The EuiFlyoutSessionRenderContext is passed to your render prop functions. // This can contain a custom `meta` object (set in the `openFlyout` function call) // which allows you to customize the content shown in the flyout. - const renderMainFlyoutContent = (context: EuiFlyoutSessionRenderContext) => ( - - -

Simple flyout content

-
-
+ const renderMainFlyoutContent = (flyoutContext: EuiFlyoutSessionRenderContext<{ title: string }>) => ( + <> + + +

{flyoutContext.meta.title}

+
+
+ + +

Simple flyout content

+
+
+ ); return ( diff --git a/packages/eui/src/components/flyout/sessions/flyout_provider.stories.tsx b/packages/eui/src/components/flyout/sessions/flyout_provider.stories.tsx index 33f86be2924..953f18b6b66 100644 --- a/packages/eui/src/components/flyout/sessions/flyout_provider.stories.tsx +++ b/packages/eui/src/components/flyout/sessions/flyout_provider.stories.tsx @@ -84,7 +84,6 @@ const ShoppingCartContent: React.FC = ({ closeChildFlyout(); // If we add an onClose handler to the child flyout, we have to call closeChildFlyout within it for the flyout to actually close }, }, - onUnmount: () => console.log('Unmounted item details child flyout'), }; openChildFlyout(options); }; @@ -98,8 +97,6 @@ const ShoppingCartContent: React.FC = ({ ...config?.mainFlyoutProps, 'aria-label': 'Review order', }, - onUnmount: () => - console.log(`Unmounted review order flyout (${reviewFlyoutSize})`), }; openFlyout(options); }; @@ -258,6 +255,13 @@ const WithHistoryAppControls: React.FC = () => { clearHistory, } = useEuiFlyoutSession(); + const handleCloseOrGoBack = () => { + if (canGoBack) { + goBack(); + } else { + clearHistory(); + } + }; const handleOpenShoppingCart = () => { const options: EuiFlyoutSessionOpenMainOptions = { size: 'm', @@ -272,7 +276,6 @@ const WithHistoryAppControls: React.FC = () => { clearHistory(); // If we add an onClose handler to the main flyout, we have to call clearHistory within it for the flyout to actually close }, }, - onUnmount: () => console.log('Unmounted shopping cart flyout'), }; openFlyout(options); }; @@ -297,7 +300,11 @@ const WithHistoryAppControls: React.FC = () => { Close child flyout - + Close/Go back @@ -351,6 +358,7 @@ const WithHistoryApp: React.FC = () => { console.log('All flyouts have been unmounted')} > @@ -386,7 +394,6 @@ const GroupOpenerControls: React.FC = () => { className: 'groupOpenerMainFlyout', 'aria-label': 'Main flyout', }, - onUnmount: () => console.log('Unmounted main flyout'), }, child: { size: 's', @@ -394,7 +401,6 @@ const GroupOpenerControls: React.FC = () => { className: 'groupOpenerChildFlyout', 'aria-label': 'Child flyout', }, - onUnmount: () => console.log('Unmounted child flyout'), }, }; openFlyoutGroup(options); diff --git a/packages/eui/src/components/flyout/sessions/flyout_provider.tsx b/packages/eui/src/components/flyout/sessions/flyout_provider.tsx index 380b5af380c..d82151ae310 100644 --- a/packages/eui/src/components/flyout/sessions/flyout_provider.tsx +++ b/packages/eui/src/components/flyout/sessions/flyout_provider.tsx @@ -21,6 +21,7 @@ import { interface FlyoutSessionContextProps { state: EuiFlyoutSessionHistoryState; dispatch: React.Dispatch; + onUnmount?: EuiFlyoutSessionProviderComponentProps['onUnmount']; } const EuiFlyoutSessionContext = createContext( @@ -53,7 +54,12 @@ export const useEuiFlyoutSessionContext = () => { */ export const EuiFlyoutSessionProvider: React.FC< EuiFlyoutSessionProviderComponentProps -> = ({ children, renderMainFlyoutContent, renderChildFlyoutContent }) => { +> = ({ + children, + renderMainFlyoutContent, + renderChildFlyoutContent, + onUnmount, +}) => { const [state, dispatch] = useReducer(flyoutReducer, initialFlyoutState); const { activeFlyoutGroup } = state; @@ -69,30 +75,14 @@ export const EuiFlyoutSessionProvider: React.FC< let childFlyoutContentNode: React.ReactNode = null; if (activeFlyoutGroup) { - const mainRenderContext: EuiFlyoutSessionRenderContext = { - flyoutProps: activeFlyoutGroup.config.mainFlyoutProps || {}, - flyoutSize: activeFlyoutGroup.config.mainSize, - flyoutType: 'main', - dispatch, + const renderContext: EuiFlyoutSessionRenderContext = { activeFlyoutGroup, - onCloseFlyout: handleClose, - onCloseChildFlyout: handleCloseChild, meta: activeFlyoutGroup.meta, }; - mainFlyoutContentNode = renderMainFlyoutContent(mainRenderContext); + mainFlyoutContentNode = renderMainFlyoutContent(renderContext); if (activeFlyoutGroup.isChildOpen && renderChildFlyoutContent) { - const childRenderContext: EuiFlyoutSessionRenderContext = { - flyoutProps: activeFlyoutGroup.config.childFlyoutProps || {}, - flyoutSize: activeFlyoutGroup.config.childSize, - flyoutType: 'child', - dispatch, - activeFlyoutGroup, - onCloseFlyout: handleClose, - onCloseChildFlyout: handleCloseChild, - meta: activeFlyoutGroup.meta, - }; - childFlyoutContentNode = renderChildFlyoutContent(childRenderContext); + childFlyoutContentNode = renderChildFlyoutContent(renderContext); } else if (activeFlyoutGroup.isChildOpen && !renderChildFlyoutContent) { console.warn( 'EuiFlyoutSessionProvider: A child flyout is open, but renderChildFlyoutContent was not provided.' @@ -105,7 +95,7 @@ export const EuiFlyoutSessionProvider: React.FC< const flyoutPropsChild = config?.childFlyoutProps || {}; return ( - + {children} {activeFlyoutGroup?.isMainOpen && ( ( ): EuiFlyoutSessionHistoryState { switch (action.type) { case 'OPEN_MAIN_FLYOUT': { - const { size, flyoutProps, onUnmount } = action.payload; + const { size, flyoutProps } = action.payload; const newHistory = [...state.history]; if (state.activeFlyoutGroup) { @@ -80,8 +80,6 @@ export function flyoutReducer( mainFlyoutProps: flyoutProps, childFlyoutProps: {}, }, - mainOnUnmount: onUnmount, - childOnUnmount: undefined, meta: action.payload.meta !== undefined ? state.activeFlyoutGroup?.meta !== undefined @@ -104,7 +102,7 @@ export function flyoutReducer( return state; } - const { size, flyoutProps, onUnmount } = action.payload; + const { size, flyoutProps } = action.payload; const updatedActiveGroup: EuiFlyoutSessionGroup = { ...state.activeFlyoutGroup, isChildOpen: true, @@ -113,7 +111,6 @@ export function flyoutReducer( childSize: size, childFlyoutProps: flyoutProps, }, - childOnUnmount: onUnmount, meta: action.payload.meta !== undefined ? state.activeFlyoutGroup.meta !== undefined @@ -146,8 +143,6 @@ export function flyoutReducer( mainFlyoutProps: main.flyoutProps, childFlyoutProps: child.flyoutProps, }, - mainOnUnmount: main.onUnmount, - childOnUnmount: child.onUnmount, meta, }; @@ -165,8 +160,6 @@ export function flyoutReducer( return state; } - state.activeFlyoutGroup.childOnUnmount?.(); - const updatedActiveGroup: EuiFlyoutSessionGroup = { ...state.activeFlyoutGroup, isChildOpen: false, @@ -174,7 +167,6 @@ export function flyoutReducer( ...state.activeFlyoutGroup.config, childFlyoutProps: {}, }, - childOnUnmount: undefined, }; return { @@ -187,12 +179,6 @@ export function flyoutReducer( if (!state.activeFlyoutGroup) return initialFlyoutState as EuiFlyoutSessionHistoryState; - if (state.activeFlyoutGroup.isChildOpen) { - state.activeFlyoutGroup.childOnUnmount?.(); - } - - state.activeFlyoutGroup.mainOnUnmount?.(); - // Restore from history or return to initial state if (state.history.length > 0) { const newHistory = [...state.history]; @@ -214,8 +200,7 @@ export function flyoutReducer( return state; } - const { configChanges, newMainOnUnmount, newChildOnUnmount } = - action.payload; + const { configChanges } = action.payload; const updatedActiveGroup: EuiFlyoutSessionGroup = { ...state.activeFlyoutGroup, @@ -223,14 +208,6 @@ export function flyoutReducer( ...state.activeFlyoutGroup.config, ...configChanges, }, - mainOnUnmount: - newMainOnUnmount !== undefined - ? newMainOnUnmount - : state.activeFlyoutGroup.mainOnUnmount, - childOnUnmount: - newChildOnUnmount !== undefined - ? newChildOnUnmount - : state.activeFlyoutGroup.childOnUnmount, }; const finalUpdatedActiveGroup = applySizeConstraints(updatedActiveGroup); diff --git a/packages/eui/src/components/flyout/sessions/index.ts b/packages/eui/src/components/flyout/sessions/index.ts index e21963c5e15..92fac89377b 100644 --- a/packages/eui/src/components/flyout/sessions/index.ts +++ b/packages/eui/src/components/flyout/sessions/index.ts @@ -21,4 +21,5 @@ export { useEuiFlyoutSession, type EuiFlyoutSessionOpenChildOptions, type EuiFlyoutSessionOpenMainOptions, + type EuiFlyoutSessionApi, } from './use_eui_flyout'; diff --git a/packages/eui/src/components/flyout/sessions/types.ts b/packages/eui/src/components/flyout/sessions/types.ts index 9eaabeabdea..e0993f3187c 100644 --- a/packages/eui/src/components/flyout/sessions/types.ts +++ b/packages/eui/src/components/flyout/sessions/types.ts @@ -27,8 +27,6 @@ export interface EuiFlyoutSessionGroup { isMainOpen: boolean; isChildOpen: boolean; config: EuiFlyoutSessionConfig; - mainOnUnmount?: () => void; - childOnUnmount?: () => void; meta?: FlyoutMeta; } @@ -46,8 +44,6 @@ export type EuiFlyoutSessionAction = type: 'UPDATE_ACTIVE_FLYOUT_CONFIG'; payload: { configChanges: Partial; - newMainOnUnmount?: () => void; - newChildOnUnmount?: () => void; }; } | { @@ -55,7 +51,6 @@ export type EuiFlyoutSessionAction = payload: { size: EuiFlyoutSize; flyoutProps?: Partial>; - onUnmount?: () => void; meta?: FlyoutMeta; }; } @@ -64,7 +59,6 @@ export type EuiFlyoutSessionAction = payload: { size: 's' | 'm'; flyoutProps?: Partial>; - onUnmount?: () => void; meta?: FlyoutMeta; }; } @@ -74,12 +68,10 @@ export type EuiFlyoutSessionAction = main: { size: EuiFlyoutSize; flyoutProps?: Partial>; - onUnmount?: () => void; }; child: { size: 's' | 'm'; flyoutProps?: Partial>; - onUnmount?: () => void; }; meta?: FlyoutMeta; }; @@ -92,13 +84,7 @@ export type EuiFlyoutSessionAction = * Flyout session context managed by `EuiFlyoutSessionProvider`, and passed to the `renderMainFlyoutContent` and `renderChildFlyoutContent` functions. */ export interface EuiFlyoutSessionRenderContext { - flyoutProps: Partial; - flyoutSize: EuiFlyoutProps['size'] | EuiFlyoutChildProps['size']; - flyoutType: 'main' | 'child'; - dispatch: React.Dispatch>; activeFlyoutGroup: EuiFlyoutSessionGroup | null; - onCloseFlyout: () => void; - onCloseChildFlyout: () => void; meta?: FlyoutMeta; } @@ -107,6 +93,7 @@ export interface EuiFlyoutSessionRenderContext { */ export interface EuiFlyoutSessionProviderComponentProps { children: React.ReactNode; + onUnmount?: () => void; renderMainFlyoutContent: ( context: EuiFlyoutSessionRenderContext ) => React.ReactNode; 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 ef39cddca84..76c4f52e87e 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 @@ -36,7 +36,6 @@ interface TestComponentProps { onOpenFlyout?: () => void; onOpenChildFlyout?: () => void; onOpenFlyoutGroup?: () => void; - onCloseChildFlyout?: () => void; onGoBack?: () => void; onClearHistory?: () => void; } @@ -45,7 +44,6 @@ const TestComponent: React.FC = ({ onOpenFlyout, onOpenChildFlyout, onOpenFlyoutGroup, - onCloseChildFlyout, onGoBack, onClearHistory, }) => { @@ -99,12 +97,10 @@ const TestComponent: React.FC = ({ main: { size: 'm', flyoutProps: { className: 'main-flyout' }, - onUnmount: () => {}, }, child: { size: 's', flyoutProps: { className: 'child-flyout' }, - onUnmount: () => {}, }, meta: { type: 'testGroup' }, }; @@ -119,7 +115,6 @@ const TestComponent: React.FC = ({ data-testid="closeChildFlyoutButton" onClick={() => { closeChildFlyout(); - if (onCloseChildFlyout) onCloseChildFlyout(); }} disabled={!isChildFlyoutOpen} > @@ -164,8 +159,11 @@ const TestComponent: React.FC = ({ // Create a wrapper component that provides the context const TestWrapper: React.FC< - React.PropsWithChildren & { children?: React.ReactNode } -> = ({ children }) => { + React.PropsWithChildren & { + children?: React.ReactNode; + onUnmount?: () => void; + } +> = ({ children, onUnmount }) => { const renderMainFlyoutContent = (context: any) => (
Main flyout content: {JSON.stringify(context.meta)} @@ -182,6 +180,7 @@ const TestWrapper: React.FC< {children} @@ -211,7 +210,7 @@ describe('useEuiFlyoutSession', () => { 'Child flyout is closed' ); expect(screen.getByTestId('canGoBackStatus').textContent).toBe( - 'Can go back' + 'Cannot go back' ); }); @@ -255,15 +254,14 @@ describe('useEuiFlyoutSession', () => { 'Child flyout is open' ); expect(screen.getByTestId('canGoBackStatus').textContent).toBe( - 'Can go back' + 'Cannot go back' ); }); test('closeChildFlyout closes only the child flyout', () => { - const onCloseChildFlyout = jest.fn(); render( - + ); @@ -273,7 +271,6 @@ describe('useEuiFlyoutSession', () => { // Then close the child flyout fireEvent.click(screen.getByTestId('closeChildFlyoutButton')); - expect(onCloseChildFlyout).toHaveBeenCalledTimes(1); expect(screen.getByTestId('flyoutStatus').textContent).toBe( 'Flyout is open' ); @@ -309,129 +306,18 @@ describe('useEuiFlyoutSession', () => { fireEvent.click(screen.getByTestId('goBackButton')); expect(onGoBack).toHaveBeenCalledTimes(1); - // Verify we can still go back as there's history - expect(screen.getByTestId('canGoBackStatus').textContent).toBe( - 'Can go back' - ); - - // Go back again should close the main flyout - fireEvent.click(screen.getByTestId('goBackButton')); - - expect(onGoBack).toHaveBeenCalledTimes(2); - expect(screen.getByTestId('flyoutStatus').textContent).toBe( - 'Flyout is closed' - ); + // Verify we cannot go back as there's no more history expect(screen.getByTestId('canGoBackStatus').textContent).toBe( 'Cannot go back' ); - }); - - test('goBack navigates through the history stack and restores previous flyouts', () => { - render( - - - - ); - - // Open first flyout (A) - fireEvent.click(screen.getByTestId('openFlyoutButton')); - // Verify the content is rendered by checking the element exists and has content - expect(screen.getByTestId('mainFlyoutContent')).toBeTruthy(); - expect(screen.getByTestId('mainFlyoutContent').textContent).toContain( - '"type":"test"' - ); - - // Open second flyout (B), which should push A to history - fireEvent.click(screen.getByTestId('openFlyoutButton')); - // Verify the main flyout is still open - expect(screen.getByTestId('mainFlyoutContent')).toBeTruthy(); - expect(screen.getByTestId('canGoBackStatus').textContent).toBe( - 'Can go back' - ); - - // Go back to first flyout (A) - fireEvent.click(screen.getByTestId('goBackButton')); - - // Should still have a flyout open (first one) - expect(screen.getByTestId('flyoutStatus').textContent).toBe( - 'Flyout is open' - ); - expect(screen.getByTestId('canGoBackStatus').textContent).toBe( - 'Can go back' - ); - // Go back again, which should close everything as there's no more history + // The go back button is now disabled, so a click won't do anything. + // The flyout should still be open. fireEvent.click(screen.getByTestId('goBackButton')); - expect(screen.getByTestId('flyoutStatus').textContent).toBe( - 'Flyout is closed' - ); - expect(screen.getByTestId('canGoBackStatus').textContent).toBe( - 'Cannot go back' - ); - }); - - // We can skip this test since our implementation no longer depends on onClose handlers - // and we've fixed the issue in the reducer already - test('goBack works correctly regardless of onClose handlers', () => { - // Create a test component with onClose handlers - const CustomTestComponent = () => { - const { openFlyout, goBack, clearHistory, isFlyoutOpen, canGoBack } = - useEuiFlyoutSession(); - - return ( -
- - - - - - -
- {isFlyoutOpen ? 'Flyout is open' : 'Flyout is closed'} -
- -
- {canGoBack ? 'Can go back' : 'Cannot go back'} -
-
- ); - }; - - render( - - - - ); - - // Open flyout - fireEvent.click(screen.getByTestId('openFlyoutButton')); + expect(onGoBack).toHaveBeenCalledTimes(1); expect(screen.getByTestId('flyoutStatus').textContent).toBe( 'Flyout is open' ); - - // Go back should close the flyout - fireEvent.click(screen.getByTestId('goBackButton')); - expect(screen.getByTestId('flyoutStatus').textContent).toBe( - 'Flyout is closed' - ); }); test('clearHistory closes all flyouts', () => { @@ -493,4 +379,65 @@ describe('useEuiFlyoutSession', () => { 'Child flyout is open' ); }); + + describe('onUnmount callback', () => { + test('is called when all flyouts are closed via clearHistory', () => { + const onUnmount = jest.fn(); + render( + + + + ); + + // Open a flyout group + fireEvent.click(screen.getByTestId('openFlyoutGroupButton')); + expect(screen.getByTestId('flyoutStatus').textContent).toBe( + 'Flyout is open' + ); + expect(onUnmount).not.toHaveBeenCalled(); + + // Clear history, which should close all flyouts and trigger onUnmount + fireEvent.click(screen.getByTestId('clearHistoryButton')); + expect(screen.getByTestId('flyoutStatus').textContent).toBe( + 'Flyout is closed' + ); + expect(onUnmount).toHaveBeenCalledTimes(1); + }); + + test('is not called when a flyout is opened', () => { + const onUnmount = jest.fn(); + render( + + + + ); + + fireEvent.click(screen.getByTestId('openFlyoutButton')); + expect(onUnmount).not.toHaveBeenCalled(); + }); + + test('is not called while there are still flyouts in the history stack', () => { + const onUnmount = jest.fn(); + render( + + + + ); + + // Open two flyouts to create a history stack + fireEvent.click(screen.getByTestId('openFlyoutButton')); + fireEvent.click(screen.getByTestId('openFlyoutButton')); + expect(screen.getByTestId('canGoBackStatus').textContent).toBe( + 'Can go back' + ); + expect(onUnmount).not.toHaveBeenCalled(); + + // Go back once, which leaves one flyout open + fireEvent.click(screen.getByTestId('goBackButton')); + expect(screen.getByTestId('flyoutStatus').textContent).toBe( + 'Flyout is open' + ); + expect(onUnmount).not.toHaveBeenCalled(); + }); + }); }); 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 59476dc7119..586b52e5442 100644 --- a/packages/eui/src/components/flyout/sessions/use_eui_flyout.ts +++ b/packages/eui/src/components/flyout/sessions/use_eui_flyout.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { useEffect, useRef } from 'react'; import { EuiFlyoutSize } from '../flyout'; import { useEuiFlyoutSessionContext } from './flyout_provider'; import { EuiFlyoutSessionConfig } from './types'; @@ -16,7 +17,6 @@ import { EuiFlyoutSessionConfig } from './types'; export interface EuiFlyoutSessionOpenMainOptions { size: EuiFlyoutSize; flyoutProps?: EuiFlyoutSessionConfig['mainFlyoutProps']; - onUnmount?: () => void; meta?: Meta; } @@ -26,7 +26,6 @@ export interface EuiFlyoutSessionOpenMainOptions { export interface EuiFlyoutSessionOpenChildOptions { size: 's' | 'm'; flyoutProps?: EuiFlyoutSessionConfig['childFlyoutProps']; - onUnmount?: () => void; meta?: Meta; } @@ -37,21 +36,43 @@ export interface EuiFlyoutSessionOpenGroupOptions { main: { size: EuiFlyoutSize; flyoutProps?: EuiFlyoutSessionConfig['mainFlyoutProps']; - onUnmount?: () => void; }; child: { size: 's' | 'm'; flyoutProps?: EuiFlyoutSessionConfig['childFlyoutProps']; - onUnmount?: () => void; }; meta?: Meta; // Shared meta for both flyouts } +export interface EuiFlyoutSessionApi { + openFlyout: (options: EuiFlyoutSessionOpenMainOptions) => void; + openChildFlyout: (options: EuiFlyoutSessionOpenChildOptions) => void; + openFlyoutGroup: (options: EuiFlyoutSessionOpenGroupOptions) => void; + closeChildFlyout: () => void; + goBack: () => void; + clearHistory: () => void; + isFlyoutOpen: boolean; + isChildFlyoutOpen: boolean; + canGoBack: boolean; +} + /** * Hook for accessing the flyout API + * @public */ -export function useEuiFlyoutSession() { - const { state, dispatch } = useEuiFlyoutSessionContext(); +export function useEuiFlyoutSession(): EuiFlyoutSessionApi { + const { state, dispatch, onUnmount } = useEuiFlyoutSessionContext(); + const isInitialMount = useRef(true); + + useEffect(() => { + // When there is no active flyout, we should call the onUnmount callback. + // Ensure this is not called on the initial render, only on subsequent state changes. + if (isInitialMount.current) { + isInitialMount.current = false; + } else if (state.activeFlyoutGroup === null) { + onUnmount?.(); + } + }, [state.activeFlyoutGroup, onUnmount]); const openFlyout = (options: EuiFlyoutSessionOpenMainOptions) => { dispatch({ @@ -80,12 +101,10 @@ export function useEuiFlyoutSession() { main: { size: options.main.size, flyoutProps: options.main.flyoutProps, - onUnmount: options.main.onUnmount, }, child: { size: options.child.size, flyoutProps: options.child.flyoutProps, - onUnmount: options.child.onUnmount, }, meta: options.meta, }, @@ -100,15 +119,15 @@ export function useEuiFlyoutSession() { dispatch({ type: 'GO_BACK' }); }; - const canGoBack = !!state.activeFlyoutGroup; + const clearHistory = () => { + dispatch({ type: 'CLEAR_HISTORY' }); + }; const isFlyoutOpen = !!state.activeFlyoutGroup?.isMainOpen; const isChildFlyoutOpen = !!state.activeFlyoutGroup?.isChildOpen; - const clearHistory = () => { - dispatch({ type: 'CLEAR_HISTORY' }); - }; + const canGoBack = !!state.history.length; return { openFlyout, @@ -116,9 +135,9 @@ export function useEuiFlyoutSession() { openFlyoutGroup, closeChildFlyout, goBack, - canGoBack, + clearHistory, isFlyoutOpen, isChildFlyoutOpen, - clearHistory, + canGoBack, }; }