From 065fe5ea2fcae1822b7af986384f5e9c80dd47ea Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Wed, 29 Oct 2025 09:40:03 -0700 Subject: [PATCH 1/7] [Flyout System] Make session logic truly opt-in --- packages/eui/src/components/flyout/flyout.tsx | 6 +++--- .../eui/src/components/flyout/manager/const.ts | 3 --- .../containers/flyout/session-management.mdx | 17 +++++++++-------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/eui/src/components/flyout/flyout.tsx b/packages/eui/src/components/flyout/flyout.tsx index 23a44f1e213..52e48554311 100644 --- a/packages/eui/src/components/flyout/flyout.tsx +++ b/packages/eui/src/components/flyout/flyout.tsx @@ -38,8 +38,8 @@ export type EuiFlyoutProps = Omit< /** * Controls the way the session is managed for this flyout. * - `start`: Creates a new flyout session. Use this for the main flyout. - * - `inherit`: (default) Inherits an existing session if one is active, otherwise functions as a standard flyout. - * - `never`: Opts out of session management and always functions as a standard flyout. + * - `inherit`: Inherits an existing session if one is active, otherwise functions as a standard flyout. + * - `never`: (default) Disregards session management and always functions as a standard flyout. * * Check out [EuiFlyout session management](https://eui.elastic.co/docs/components/containers/flyout/session-management) * documentation to learn more. @@ -67,7 +67,7 @@ export const EuiFlyout = forwardRef< as, onClose, onActive, - session = SESSION_INHERIT, + session = SESSION_NEVER, ...rest } = usePropsWithComponentDefaults('EuiFlyout', props); const hasActiveSession = useRef(useHasActiveSession()); diff --git a/packages/eui/src/components/flyout/manager/const.ts b/packages/eui/src/components/flyout/manager/const.ts index af6341a2afe..b2044b611ec 100644 --- a/packages/eui/src/components/flyout/manager/const.ts +++ b/packages/eui/src/components/flyout/manager/const.ts @@ -8,9 +8,6 @@ /** * Allowed values for `session` prop to control the way the session is managed for a flyout. - * - `session="start"`: Creates a new flyout session. Use this for the main flyout. - * - `session="inherit"`: (default) Inherits an existing session if one is active, otherwise functions as a standard flyout. - * - `session="never"`: Opts out of session management and always functions as a standard flyout. */ export const SESSION_START = 'start'; export const SESSION_INHERIT = 'inherit'; diff --git a/packages/website/docs/components/containers/flyout/session-management.mdx b/packages/website/docs/components/containers/flyout/session-management.mdx index 622e33b3caf..ffe33e32636 100644 --- a/packages/website/docs/components/containers/flyout/session-management.mdx +++ b/packages/website/docs/components/containers/flyout/session-management.mdx @@ -113,8 +113,7 @@ about compatibility issues. Each time you set `session="start"`, you create a new flyout session and mark the rendered flyout as [main](#main-flyouts). To create a [child flyout](#child-flyouts) - a flyout that belongs to the same -session as the main flyout - you can set `session="inherit"` or omit the `session` prop entirely, -as "inherit" is the default behavior. +session as the main flyout - set `session="inherit"`. All bindings and configuration will be handled automatically. ### Session title @@ -134,8 +133,8 @@ field of the `flyoutMenuProps` to set the title of the flyout. I'm the main flyout - {/* Render a new child flyout - notice the lack of the `session` prop */} ``` -To prevent a flyout from being a part of the session management system, you -can set `session="never"` which will render it as a regular unmanaged flyout. +To prevent a flyout from being a part of the session management system, you can +set `session="never"`, or you can omit the `session` prop as `never` is the +default. This will will render it as a regular unmanaged flyout. ```tsx <> @@ -186,9 +186,10 @@ of an alert. ### Child flyouts -Child flyouts are directly related to a main flyout in their session. -They're created by rendering a `EuiFlyout` **without the session prop** inside -a [main flyout](#main-flyouts) - a flyout with `session="start"` prop set. +Child flyouts are directly related to a main flyout in their session. They're +created by rendering a `EuiFlyout` with the `session="inherit" prop set while +there is an active [main flyout](#main-flyouts) - a flyout with +`session="start"` prop set. They're meant to display secondary level information like the alert visualization. From 2dc39ef99174d758ef93b912ea0672f21e18ca24 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Wed, 29 Oct 2025 10:15:06 -0700 Subject: [PATCH 2/7] Fix child flyouts in storybooks, restructure child flyout outside of main --- .../manager/flyout_sessions.stories.tsx | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/eui/src/components/flyout/manager/flyout_sessions.stories.tsx b/packages/eui/src/components/flyout/manager/flyout_sessions.stories.tsx index 5ea91759a1d..f27b29d2537 100644 --- a/packages/eui/src/components/flyout/manager/flyout_sessions.stories.tsx +++ b/packages/eui/src/components/flyout/manager/flyout_sessions.stories.tsx @@ -173,7 +173,7 @@ const FlyoutSession: React.FC = React.memo((props) => { }, { title: 'session', - description: 'start', + description: start, }, ]} /> @@ -190,6 +190,7 @@ const FlyoutSession: React.FC = React.memo((props) => { {childSize && isChildFlyoutVisible && ( = React.memo((props) => { }, { title: 'session', - description: 'inherit', + description: inherit, }, ]} /> @@ -254,6 +255,7 @@ const NonSessionFlyout: React.FC<{ {isFlyoutVisible && ( @@ -272,9 +273,9 @@ const NonSessionFlyout: React.FC<{

- This is the content of a non-session flyout. We assure it will - never become a managed flyout by setting{' '} - {'session={never}'}. + This is the content of a non-session flyout. It is assured to + not be a managed flyout using the{' '} + {'session={never}'} behavior.

+ never (using default) + + ), + }, ]} />
@@ -467,6 +475,7 @@ const ExternalRootChildFlyout: React.FC<{ parentId: string }> = ({ {isOpen && ( Date: Wed, 29 Oct 2025 10:15:12 -0700 Subject: [PATCH 3/7] Fix child flyout in docs --- .../containers/flyout/session-management.mdx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/website/docs/components/containers/flyout/session-management.mdx b/packages/website/docs/components/containers/flyout/session-management.mdx index ffe33e32636..f0269ac6eac 100644 --- a/packages/website/docs/components/containers/flyout/session-management.mdx +++ b/packages/website/docs/components/containers/flyout/session-management.mdx @@ -39,14 +39,10 @@ export default () => { setIsOpenFlyoutA(false)} > - This flyout is rendered with the {'session="start"'} prop set which creates a new flyout session and marks this flyout as main. + This is Flyout A. This flyout is rendered with the {'session="start"'} prop set which created a new flyout session and marks this flyout as main. setName(e.target.value)} /> @@ -68,9 +64,9 @@ export default () => { > - This flyout is also rendered with the {'session="start"'} prop added which creates a second flyout session and marks this flyout as main in that session. + This is Flyout B. This flyout is also rendered with the {'session="start"'} prop added which creates a second flyout session and marks this flyout as main in that session.

- The first flyout still exists but is currently hidden since this one took precedence. You can jump back to it by clicking the Back button above if you'd like. + Flyout A exists but is currently hidden since this one took precedence. You can jump back to it by clicking the Back button above if you'd like.
setIsOpenFlyoutBChild((val) => !val)}> @@ -78,7 +74,12 @@ export default () => {
{isOpenFlyoutBChild && ( - setIsOpenFlyoutBChild(false)}> + setIsOpenFlyoutBChild(false)} + > This is a child flyout of Flyout B. It belongs to the same session as Flyout B because it's rendered inside of it and doesn't have the session prop set. @@ -187,7 +188,7 @@ of an alert. ### Child flyouts Child flyouts are directly related to a main flyout in their session. They're -created by rendering a `EuiFlyout` with the `session="inherit" prop set while +created by rendering a `EuiFlyout` with the `session="inherit"` prop set while there is an active [main flyout](#main-flyouts) - a flyout with `session="start"` prop set. From a33ad18876ad36f633c63b9f5fb28ef48481a8f1 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Wed, 29 Oct 2025 13:08:14 -0700 Subject: [PATCH 4/7] Update unit tests --- .../eui/src/components/flyout/flyout.test.tsx | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/packages/eui/src/components/flyout/flyout.test.tsx b/packages/eui/src/components/flyout/flyout.test.tsx index fac7862d66d..3d2e4de8bcf 100644 --- a/packages/eui/src/components/flyout/flyout.test.tsx +++ b/packages/eui/src/components/flyout/flyout.test.tsx @@ -438,7 +438,7 @@ describe('EuiFlyout', () => { }); describe('flyout routing logic', () => { - it('routes to child flyout when session is undefined and there is an active session', () => { + it('routes to child flyout when session is "inherit" and there is an active session', () => { // First render with just the main flyout to establish a session const { rerender, getByTestSubject } = render( @@ -463,7 +463,7 @@ describe('EuiFlyout', () => { {}} data-test-subj="child-flyout" - // session is undefined (not explicitly set) + session="inherit" /> ); @@ -514,7 +514,27 @@ describe('EuiFlyout', () => { expect(flyout).not.toHaveAttribute('data-managed-flyout-level'); }); - it('routes to child flyout when in managed context and there is an active session', () => { + it('routes to standard flyout when session undefined and there is an active session', () => { + const { getByTestSubject } = render( + + {/* Create an active session */} + {}} + session="start" + flyoutMenuProps={{ title: 'Main Flyout' }} + data-test-subj="main-flyout" + /> + {/* This flyout opted out of session management by default */} + {}} data-test-subj="standard-flyout" /> + + ); + + // Should render as standard flyout (EuiFlyoutComponent) + const flyout = getByTestSubject('standard-flyout'); + expect(flyout).not.toHaveAttribute('data-managed-flyout-level'); + }); + + it('routes to child flyout when session is "inherit" in a managed context and there is an active session', () => { // First render with just the main flyout to establish a session const { rerender, getByTestSubject } = render( @@ -539,7 +559,7 @@ describe('EuiFlyout', () => { {}} data-test-subj="child-flyout" - session={undefined} // Not explicitly set, should inherit + session="inherit" /> ); @@ -562,5 +582,35 @@ describe('EuiFlyout', () => { const flyout = getByTestSubject('flyout'); expect(flyout).not.toHaveAttribute('data-managed-flyout-level'); }); + + it('routes to standard flyout when session="inherit" but there is no active session', () => { + const { getByTestSubject } = render( + {}} + data-test-subj="flyout" + session="inherit" // Explicitly set to inherit, but no session to inherit from + /> + ); + + // Should gracefully degrade to standard flyout (EuiFlyoutComponent) when no session exists + const flyout = getByTestSubject('flyout'); + expect(flyout).not.toHaveAttribute('data-managed-flyout-level'); + }); + + it('routes to standard flyout when session="inherit" within Manager but no active session', () => { + const { getByTestSubject } = render( + + {}} + data-test-subj="flyout" + session="inherit" // Manager context exists but no main flyout has been created + /> + + ); + + // Should gracefully degrade to standard flyout when Manager exists but no session is active + const flyout = getByTestSubject('flyout'); + expect(flyout).not.toHaveAttribute('data-managed-flyout-level'); + }); }); }); From 122e5d6aa17df2211e58686a791bb5d84dfeece8 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 10 Nov 2025 11:08:02 -0700 Subject: [PATCH 5/7] =?UTF-8?q?Enable=20automatic=20session=20inheritance?= =?UTF-8?q?=20for=20flyout=20nested=20inside=20of=20a=20=E2=80=9Cmain?= =?UTF-8?q?=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/flyout/flyout.component.tsx | 3 +- .../eui/src/components/flyout/flyout.test.tsx | 85 ++++++++++++++++--- packages/eui/src/components/flyout/flyout.tsx | 31 ++++--- .../flyout/flyout_parent_context.tsx | 40 +++++++++ packages/eui/src/components/flyout/index.ts | 1 + .../manager/flyout_sessions.stories.tsx | 13 ++- 6 files changed, 149 insertions(+), 24 deletions(-) create mode 100644 packages/eui/src/components/flyout/flyout_parent_context.tsx diff --git a/packages/eui/src/components/flyout/flyout.component.tsx b/packages/eui/src/components/flyout/flyout.component.tsx index 39865e00e9d..410a6102895 100644 --- a/packages/eui/src/components/flyout/flyout.component.tsx +++ b/packages/eui/src/components/flyout/flyout.component.tsx @@ -72,6 +72,7 @@ import { EuiFlyoutResizeButton } from './_flyout_resize_button'; import { useEuiFlyoutResizable } from './use_flyout_resizable'; import type { EuiFlyoutCloseEvent } from './types'; import { useEuiFlyoutZIndex } from './use_flyout_z_index'; +import { EuiFlyoutParentProvider } from './flyout_parent_context'; interface _EuiFlyoutComponentProps { /** @@ -659,7 +660,7 @@ export const EuiFlyoutComponent = forwardRef( onKeyDown={onKeyDownResizableButton} /> )} - {children} + {children} diff --git a/packages/eui/src/components/flyout/flyout.test.tsx b/packages/eui/src/components/flyout/flyout.test.tsx index 3d2e4de8bcf..8410e80dc48 100644 --- a/packages/eui/src/components/flyout/flyout.test.tsx +++ b/packages/eui/src/components/flyout/flyout.test.tsx @@ -438,7 +438,31 @@ describe('EuiFlyout', () => { }); describe('flyout routing logic', () => { - it('routes to child flyout when session is "inherit" and there is an active session', () => { + it('routes to child flyout automatically when nested inside a parent flyout', () => { + const { getByTestSubject } = render( + + {}} + session="start" + flyoutMenuProps={{ title: 'Main Flyout' }} + data-test-subj="main-flyout" + > + {/* Child flyout nested inside parent - should auto-inherit */} + {}} data-test-subj="child-flyout" /> + + + ); + + // Main flyout should be rendered as managed + const mainFlyout = getByTestSubject('main-flyout'); + expect(mainFlyout).toHaveAttribute('data-managed-flyout-level', 'main'); + + // Child flyout should automatically become a managed child + const childFlyout = getByTestSubject('child-flyout'); + expect(childFlyout).toHaveAttribute('data-managed-flyout-level', 'child'); + }); + + it('routes to child flyout when session is explicitly "inherit" and there is an active session', () => { // First render with just the main flyout to establish a session const { rerender, getByTestSubject } = render( @@ -514,7 +538,7 @@ describe('EuiFlyout', () => { expect(flyout).not.toHaveAttribute('data-managed-flyout-level'); }); - it('routes to standard flyout when session undefined and there is an active session', () => { + it('routes to standard flyout when session="never" explicitly set and there is an active session', () => { const { getByTestSubject } = render( {/* Create an active session */} @@ -524,8 +548,12 @@ describe('EuiFlyout', () => { flyoutMenuProps={{ title: 'Main Flyout' }} data-test-subj="main-flyout" /> - {/* This flyout opted out of session management by default */} - {}} data-test-subj="standard-flyout" /> + {/* This flyout explicitly opts out of session management */} + {}} + session="never" + data-test-subj="standard-flyout" + /> ); @@ -534,21 +562,30 @@ describe('EuiFlyout', () => { expect(flyout).not.toHaveAttribute('data-managed-flyout-level'); }); - it('routes to child flyout when session is "inherit" in a managed context and there is an active session', () => { - // First render with just the main flyout to establish a session - const { rerender, getByTestSubject } = render( + it('routes to standard flyout when not nested inside a parent flyout', () => { + const { getByTestSubject } = render( + {/* Create an active session */} {}} session="start" flyoutMenuProps={{ title: 'Main Flyout' }} data-test-subj="main-flyout" /> + {/* This flyout is not nested inside the parent, so it doesn't auto-inherit */} + {}} data-test-subj="standard-flyout" /> ); - // Now render with the child flyout added - it should detect the active session - rerender( + // Should render as standard flyout (EuiFlyoutComponent) + const flyout = getByTestSubject('standard-flyout'); + expect(flyout).not.toHaveAttribute('data-managed-flyout-level'); + }); + + it('routes to child flyout when session is explicitly "inherit" across React roots', () => { + // This test demonstrates cross-root behavior: child is not nested in JSX tree + // but can still inherit via explicit session="inherit" + const { getByTestSubject } = render( {}} @@ -556,6 +593,7 @@ describe('EuiFlyout', () => { flyoutMenuProps={{ title: 'Main Flyout' }} data-test-subj="main-flyout" /> + {/* Not nested, but using explicit session="inherit" */} {}} data-test-subj="child-flyout" @@ -564,7 +602,11 @@ describe('EuiFlyout', () => { ); - // Should render as child flyout (EuiFlyoutChild) + // Main flyout should be managed + const mainFlyout = getByTestSubject('main-flyout'); + expect(mainFlyout).toHaveAttribute('data-managed-flyout-level', 'main'); + + // Child flyout should become managed via explicit inherit const flyout = getByTestSubject('child-flyout'); expect(flyout).toHaveAttribute('data-managed-flyout-level', 'child'); }); @@ -612,5 +654,28 @@ describe('EuiFlyout', () => { const flyout = getByTestSubject('flyout'); expect(flyout).not.toHaveAttribute('data-managed-flyout-level'); }); + + it('routes to standard flyout when nested but parent uses session="never"', () => { + const { getByTestSubject } = render( + + {}} + session="never" + data-test-subj="parent-flyout" + > + {/* Nested, but parent is not managed, so no auto-inheritance */} + {}} data-test-subj="child-flyout" /> + + + ); + + // Parent should be standard flyout + const parentFlyout = getByTestSubject('parent-flyout'); + expect(parentFlyout).not.toHaveAttribute('data-managed-flyout-level'); + + // Child should also be standard flyout (no session to inherit from) + const childFlyout = getByTestSubject('child-flyout'); + expect(childFlyout).not.toHaveAttribute('data-managed-flyout-level'); + }); }); }); diff --git a/packages/eui/src/components/flyout/flyout.tsx b/packages/eui/src/components/flyout/flyout.tsx index 52e48554311..a013c6d747a 100644 --- a/packages/eui/src/components/flyout/flyout.tsx +++ b/packages/eui/src/components/flyout/flyout.tsx @@ -16,6 +16,7 @@ import { import { EuiFlyoutChild, EuiFlyoutMain, useHasActiveSession } from './manager'; import { EuiFlyoutMenuContext } from './flyout_menu_context'; +import { useIsInsideParentFlyout } from './flyout_parent_context'; import { SESSION_INHERIT, SESSION_NEVER, SESSION_START } from './manager/const'; export type { @@ -39,11 +40,14 @@ export type EuiFlyoutProps = Omit< * Controls the way the session is managed for this flyout. * - `start`: Creates a new flyout session. Use this for the main flyout. * - `inherit`: Inherits an existing session if one is active, otherwise functions as a standard flyout. - * - `never`: (default) Disregards session management and always functions as a standard flyout. + * - `never`: Disregards session management and always functions as a standard flyout. + * + * When the `session` prop is undefined (not set), the flyout will automatically inherit from + * a parent flyout if it's nested inside one. Otherwise, it defaults to `never`. * * Check out [EuiFlyout session management](https://eui.elastic.co/docs/components/containers/flyout/session-management) * documentation to learn more. - * @default 'inherit' + * @default undefined (auto-inherit when nested, otherwise 'never') */ session?: | typeof SESSION_START @@ -67,10 +71,11 @@ export const EuiFlyout = forwardRef< as, onClose, onActive, - session = SESSION_NEVER, + session, ...rest } = usePropsWithComponentDefaults('EuiFlyout', props); - const hasActiveSession = useRef(useHasActiveSession()); + const hasActiveSession = useHasActiveSession(); + const isInsideParentFlyout = useIsInsideParentFlyout(); const isUnmanagedFlyout = useRef(false); /* @@ -79,9 +84,18 @@ export const EuiFlyout = forwardRef< * - session="inherit" + active session → Child flyout (auto-joins, works across React roots!) * - session="inherit" + no session → Standard flyout * - session="never" → Standard flyout (explicit opt-out) + * - session=undefined + inside parent + active session → Child flyout (auto-inherit) + * - session=undefined + not inside parent → Standard flyout (default behavior) */ - if (session !== SESSION_NEVER) { - if (session === SESSION_START) { + + // Determine effective session behavior when session is undefined + const effectiveSession = + session === undefined && isInsideParentFlyout && hasActiveSession + ? SESSION_INHERIT + : session ?? SESSION_NEVER; + + if (effectiveSession !== SESSION_NEVER) { + if (effectiveSession === SESSION_START) { // session=start: create new session if (isUnmanagedFlyout.current) { // TODO: @tkajtoch - We need to find a better way to handle the missing event. @@ -99,10 +113,7 @@ export const EuiFlyout = forwardRef< } // session=inherit: auto-join existing session as child - if ( - hasActiveSession.current && - (session === undefined || session === SESSION_INHERIT) - ) { + if (hasActiveSession && effectiveSession === SESSION_INHERIT) { return ( (false); + +/** + * Provider that wraps a flyout's children to indicate they're inside a parent flyout. + * Nested flyouts can use this to automatically default to session inheritance. + */ +export const EuiFlyoutParentProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + return ( + + {children} + + ); +}; + +/** + * Hook that returns `true` when called within a parent flyout's children. + * Used to automatically determine if a nested flyout should inherit the session. + */ +export const useIsInsideParentFlyout = () => + useContext(EuiFlyoutParentContext); + diff --git a/packages/eui/src/components/flyout/index.ts b/packages/eui/src/components/flyout/index.ts index d1ed2e57f60..b5c300815f0 100644 --- a/packages/eui/src/components/flyout/index.ts +++ b/packages/eui/src/components/flyout/index.ts @@ -32,3 +32,4 @@ export { EuiFlyoutMenu } from './flyout_menu'; // Hooks for using Manager-based flyouts export { useIsInManagedFlyout, useHasActiveSession } from './manager'; +export { useIsInsideParentFlyout } from './flyout_parent_context'; diff --git a/packages/eui/src/components/flyout/manager/flyout_sessions.stories.tsx b/packages/eui/src/components/flyout/manager/flyout_sessions.stories.tsx index f27b29d2537..c5310ec096b 100644 --- a/packages/eui/src/components/flyout/manager/flyout_sessions.stories.tsx +++ b/packages/eui/src/components/flyout/manager/flyout_sessions.stories.tsx @@ -190,7 +190,6 @@ const FlyoutSession: React.FC = React.memo((props) => { {childSize && isChildFlyoutVisible && ( = React.memo((props) => { > -

This is the content of the child flyout of {title}.

+

+ This is the content of the child flyout of {title}. It + automatically inherits the session because it's nested + inside the parent. +

= React.memo((props) => { }, { title: 'session', - description: inherit, + description: ( + <> + inherit (auto) + + ), }, ]} /> From 4e41cbb312853706ba2b0405045f16272f28b899 Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 10 Nov 2025 12:49:36 -0700 Subject: [PATCH 6/7] Update documentation --- packages/eui/src/components/flyout/README.md | 12 ++++++-- .../src/components/flyout/manager/README.md | 3 ++ .../containers/flyout/session-management.mdx | 28 +++++++++++++------ 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/eui/src/components/flyout/README.md b/packages/eui/src/components/flyout/README.md index d41234da10c..ddad71b70ba 100644 --- a/packages/eui/src/components/flyout/README.md +++ b/packages/eui/src/components/flyout/README.md @@ -13,6 +13,7 @@ flowchart EuiFlyout --> EuiFlyoutComponent EuiFlyout --> |"session = 'start'"|EuiFlyoutMain --> EuiManagedFlyout --> EuiFlyoutComponent EuiFlyout --> |"session = 'inherit'"|EuiFlyoutChild --> EuiManagedFlyout --> EuiFlyoutComponent + EuiFlyout --> |"nested + session undefined"|EuiFlyoutChild ``` The core implementation of EuiFlyout lives in the internal [EuiFlyoutComponent](./flyout.component.tsx) file. @@ -20,9 +21,14 @@ It contains the main logic and UI for rendering flyouts. However, it's not the c that EUI consumers interact with directly. The EuiFlyout export actually comes from [`flyout.tsx`](./flyout.tsx) which is a thin logical -wrapper that conditionally handles session management when `session="start"`, -or renders the plain [EuiFlyoutComponent](./flyout.component.tsx) otherwise. -That structure provides a better business logic separation. +wrapper that conditionally routes to different implementations: +- `session="start"` → [EuiFlyoutMain](./manager/flyout_main.tsx) (creates new session) +- `session="inherit"` → [EuiFlyoutChild](./manager/flyout_child.tsx) (joins existing session) +- `session="never"` → [EuiFlyoutComponent](./flyout.component.tsx) (standard flyout) +- `session` undefined + nested inside parent → [EuiFlyoutChild](./manager/flyout_child.tsx) (auto-inherits) +- `session` undefined + not nested → [EuiFlyoutComponent](./flyout.component.tsx) (standard flyout) + +This structure provides better business logic separation and enables intuitive nested flyout behavior. ## Resizable flyouts diff --git a/packages/eui/src/components/flyout/manager/README.md b/packages/eui/src/components/flyout/manager/README.md index a5b4fc45c42..3a4fed99c5d 100644 --- a/packages/eui/src/components/flyout/manager/README.md +++ b/packages/eui/src/components/flyout/manager/README.md @@ -22,6 +22,9 @@ alongside the main flyout. [EuiFlyoutChild](./flyout_child.tsx) renders [EuiManagedFlyout](./flyout_managed.tsx) and does state validation to ensure the child flyout is always rendered within a main flyout. +Child flyouts are created either by explicitly setting `session="inherit"` or automatically when a flyout +is nested inside a parent flyout's children (in the JSX tree) without an explicit `session` prop. + All child flyouts are of type `overlay`, and have `ownFocus` set to false, since that's handled separately. Child flyouts are positioned absolutely and moved to the side by the width of the main flyout, which is stored diff --git a/packages/website/docs/components/containers/flyout/session-management.mdx b/packages/website/docs/components/containers/flyout/session-management.mdx index f0269ac6eac..bd3bed96bbc 100644 --- a/packages/website/docs/components/containers/flyout/session-management.mdx +++ b/packages/website/docs/components/containers/flyout/session-management.mdx @@ -82,7 +82,7 @@ export default () => { > - This is a child flyout of Flyout B. It belongs to the same session as Flyout B because it's rendered inside of it and doesn't have the session prop set. + This is a child flyout of Flyout B. It belongs to the same session as Flyout B because it's rendered inside of it (nested in the JSX tree).

If you close Flyout B - main, this flyout will close, too.
@@ -113,9 +113,15 @@ about compatibility issues. Each time you set `session="start"`, you create a new flyout session and mark the rendered flyout as [main](#main-flyouts). + To create a [child flyout](#child-flyouts) - a flyout that belongs to the same -session as the main flyout - set `session="inherit"`. -All bindings and configuration will be handled automatically. +session as the main flyout - either: +- Render it inside a main flyout's children (nested in the JSX tree), OR +- Set `session="inherit"` explicitly (required for cross-React-root scenarios) + +When a flyout is nested inside a parent flyout and doesn't have an explicit +`session` prop, it will automatically inherit the session. All bindings and +configuration will be handled automatically. ### Session title @@ -145,9 +151,12 @@ field of the `flyoutMenuProps` to set the title of the flyout.
``` -To prevent a flyout from being a part of the session management system, you can -set `session="never"`, or you can omit the `session` prop as `never` is the -default. This will will render it as a regular unmanaged flyout. +To prevent a flyout from being a part of the session management system: +- Set `session="never"` explicitly, OR +- Don't nest it inside any parent flyout (non-nested flyouts default to standard behavior) + +To force a nested flyout to remain a standard flyout even when inside a parent, +explicitly set `session="never"`. This will render it as a regular unmanaged flyout. ```tsx <> @@ -188,9 +197,10 @@ of an alert. ### Child flyouts Child flyouts are directly related to a main flyout in their session. They're -created by rendering a `EuiFlyout` with the `session="inherit"` prop set while -there is an active [main flyout](#main-flyouts) - a flyout with -`session="start"` prop set. +created by rendering an `EuiFlyout` inside a main flyout's children (nested in +the JSX tree), which causes it to automatically inherit the session. Alternatively, +you can explicitly set `session="inherit"` when rendering outside the parent's +JSX tree while an active [main flyout](#main-flyouts) (with `session="start"`) exists. They're meant to display secondary level information like the alert visualization. From 2b2ea52dab4feb927738ab8d1d095daf531e0f6e Mon Sep 17 00:00:00 2001 From: Timothy Sullivan Date: Mon, 10 Nov 2025 13:02:32 -0700 Subject: [PATCH 7/7] fix lint --- packages/eui/src/components/flyout/flyout.tsx | 9 ++------- .../eui/src/components/flyout/flyout_parent_context.tsx | 4 +--- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/eui/src/components/flyout/flyout.tsx b/packages/eui/src/components/flyout/flyout.tsx index a013c6d747a..dd2c7408c43 100644 --- a/packages/eui/src/components/flyout/flyout.tsx +++ b/packages/eui/src/components/flyout/flyout.tsx @@ -67,13 +67,8 @@ export const EuiFlyout = forwardRef< HTMLDivElement | HTMLElement, EuiFlyoutProps<'div' | 'nav'> >((props, ref) => { - const { - as, - onClose, - onActive, - session, - ...rest - } = usePropsWithComponentDefaults('EuiFlyout', props); + const { as, onClose, onActive, session, ...rest } = + usePropsWithComponentDefaults('EuiFlyout', props); const hasActiveSession = useHasActiveSession(); const isInsideParentFlyout = useIsInsideParentFlyout(); const isUnmanagedFlyout = useRef(false); diff --git a/packages/eui/src/components/flyout/flyout_parent_context.tsx b/packages/eui/src/components/flyout/flyout_parent_context.tsx index 8d84366dc5e..cabda392eaf 100644 --- a/packages/eui/src/components/flyout/flyout_parent_context.tsx +++ b/packages/eui/src/components/flyout/flyout_parent_context.tsx @@ -35,6 +35,4 @@ export const EuiFlyoutParentProvider = ({ * Hook that returns `true` when called within a parent flyout's children. * Used to automatically determine if a nested flyout should inherit the session. */ -export const useIsInsideParentFlyout = () => - useContext(EuiFlyoutParentContext); - +export const useIsInsideParentFlyout = () => useContext(EuiFlyoutParentContext);