From b820d4cf2f6053d90e4b24652534f965c160fb8f Mon Sep 17 00:00:00 2001 From: Ryan Keairns Date: Mon, 3 Nov 2025 12:02:11 -0800 Subject: [PATCH 1/4] Add EuiFlyout pattern stories to Storybook - Add BasicBodyOnly: minimal flyout with just body content - Add WithHeader: flyout with header for read-only reference content - Add WithHeaderAndFooter: canonical pattern with footer actions - Add WithBanner: flyout with banner notification example - Add Resizable: resizable flyout with JSON content example - Add ResizableWithPush: resizable flyout with push type behavior - Add MenuAndContent: settings flyout with navigation menu - Add ParentChildWithHistory: nested flyout with breadcrumb navigation - Update PushFlyouts story to start closed instead of open - Update Playground story to use render children pattern with close handler --- .../src/components/flyout/flyout.stories.tsx | 1497 ++++++++++++++++- 1 file changed, 1457 insertions(+), 40 deletions(-) diff --git a/packages/eui/src/components/flyout/flyout.stories.tsx b/packages/eui/src/components/flyout/flyout.stories.tsx index 723cfc3ae48..edf6521b354 100644 --- a/packages/eui/src/components/flyout/flyout.stories.tsx +++ b/packages/eui/src/components/flyout/flyout.stories.tsx @@ -10,7 +10,28 @@ import React, { useRef, useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { EuiButton, EuiCallOut, EuiSpacer, EuiText, EuiTitle } from '../index'; +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiCodeBlock, + EuiSpacer, + EuiText, + EuiTitle, + EuiForm, + EuiFormRow, + EuiFieldText, + EuiSelect, + EuiSwitch, + EuiComboBox, + EuiFlexGroup, + EuiFlexItem, + EuiListGroup, + EuiListGroupItem, + EuiBreadcrumbs, + EuiBasicTable, + EuiPanel, +} from '../index'; import { EuiFlyout, @@ -18,7 +39,13 @@ import { EuiFlyoutBody, EuiFlyoutHeader, EuiFlyoutFooter, + EuiFlyoutResizable, } from './index'; +import { + EuiFlyoutManager, + EuiFlyoutMain, + EuiFlyoutChild, +} from './manager'; import { LOKI_SELECTORS } from '../../../.storybook/loki'; const meta: Meta = { @@ -55,9 +82,11 @@ type Story = StoryObj; const onClose = action('onClose'); const StatefulFlyout = ( - props: Partial void }> + props: Partial & { onToggle: (open: boolean) => void }> & { + children?: React.ReactNode | ((close: () => void) => React.ReactNode); + } ) => { - const { onToggle } = props; + const { onToggle, children, ...rest } = props; const [_isOpen, setIsOpen] = useState(true); const handleToggle = (open: boolean) => { @@ -65,41 +94,52 @@ const StatefulFlyout = ( onToggle?.(open); }; + const handleClose = () => { + handleToggle(false); + onClose(); + }; + return ( <> handleToggle(!_isOpen)}> Toggle flyout {_isOpen && ( - { - handleToggle(false); - onClose(); - }} - /> + + {typeof children === 'function' ? children(handleClose) : children} + )} ); }; export const Playground: Story = { - args: { - children: ( - <> - - -

Flyout header

-
-
- Flyout body - - Flyout footer - - - ), - }, - render: ({ ...args }) => , + render: ({ ...args }) => ( + !open && onClose()}> + {(close) => ( + <> + + +

Flyout header

+
+
+ Flyout body + + + + + Cancel + + + + Save changes + + + + + )} +
+ ), }; export const PushFlyouts: Story = { @@ -114,33 +154,342 @@ export const PushFlyouts: Story = { args: { type: 'push', pushAnimation: false, - pushMinBreakpoint: 'xs', + pushMinBreakpoint: 'm', }, render: ({ ...args }) => { + const [isOpen, setIsOpen] = useState(false); const fillerText = (

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eu condimentum ipsum, nec ornare metus. Sed egestas elit nec placerat suscipit. Cras pulvinar nisi eget enim sodales fringilla. Aliquam - lobortis lorem at ornare aliquet. Mauris laoreet laoreet mollis. - Pellentesque aliquet tortor dui, non luctus turpis pulvinar vitae. - Nunc ultrices scelerisque erat eu rutrum. Nam at ligula enim. Ut nec - nisl faucibus, euismod neque ut, aliquam nisl. Donec eu ante ut arcu - rutrum blandit nec ac nisl. In elementum id enim vitae aliquam. In - sagittis, neque vitae ultricies interdum, sapien justo efficitur - ligula, sit amet fermentum nisl magna sit amet turpis. Nulla facilisi. - Proin nec viverra mi. Morbi dolor arcu, ornare non consequat et, - viverra dapibus tellus. + lobortis lorem at ornare aliquet.

); + return ( +
+ setIsOpen(!isOpen)}> + Toggle flyout + + {isOpen && ( + { + setIsOpen(false); + onClose(); + }} + > + {fillerText} + + )} + + +

The content will resized to fit the flyout.

+
+
+ ); + }, +}; + +export const Resizable: Story = { + parameters: { + codeSnippet: { + snippet: `const [isOpen, setIsOpen] = useState(false); +const titleId = 'resizableFlyoutTitle'; + +<> + setIsOpen(true)}> + Open resizable flyout + + + {isOpen && ( + setIsOpen(false)} + minWidth={420} + maxWidth={800} + > + + +

Document details

+
+ + +

+ Drag the left edge of the flyout to resize when you need + more space for wide content. +

+
+
+ + + +

+ Resizable flyouts are useful when content width is + unpredictable, like long field values or JSON payloads. +

+
+ + + {\`{ + "id": "a2f9c4d1-39c2-4f89-a81e-93a3f42c1b7e", + "user": { + "name": "elastic-observability", + "email": "alerts@example.com" + }, + "message": "The flyout width may need to grow to keep this readable.", + "tags": ["alert", "observability", "resizable"] +}\`} + +
+ + + + + setIsOpen(false)}> + Close + + + + +
+ )} +`, + }, + }, + render: () => { + const [isOpen, setIsOpen] = useState(false); + const titleId = 'resizableFlyoutTitle'; + + return ( + <> + setIsOpen(true)}> + Open resizable flyout + + + {isOpen && ( + { + setIsOpen(false); + onClose(event); + }} + > + + +

Document details

+
+ + +

+ Drag the left edge of the flyout to resize when you + need more space for wide content. +

+
+
+ + + +

+ Resizable flyouts are useful when content width is + unpredictable, like long field values or JSON payloads. +

+
+ + + {`{ + "id": "a2f9c4d1-39c2-4f89-a81e-93a3f42c1b7e", + "user": { + "name": "elastic-observability", + "email": "alerts@example.com" + }, + "message": "The flyout width may need to grow to keep this readable.", + "tags": ["alert", "observability", "resizable"] +}`} + +
+ + + + + setIsOpen(false)}> + Close + + + + +
+ )} + + ); + }, +}; + +export const ResizableWithPush: Story = { + parameters: { + codeSnippet: { + snippet: `const [isOpen, setIsOpen] = useState(false); +const titleId = 'resizablePushFlyoutTitle'; + +<> + setIsOpen(true)}> + Open resizable push flyout + + + {isOpen && ( + setIsOpen(false)} + minWidth={420} + maxWidth={800} + > + + +

Document details

+
+ + +

+ This resizable flyout pushes page content instead of + overlaying it. Drag the left edge to resize. +

+
+
+ + + +

+ Combine resizable with push type when you want users to + reference page content while viewing flyout details. +

+
+ + +{\`{ + "id": "a2f9c4d1-39c2-4f89-a81e-93a3f42c1b7e", + "user": { + "name": "elastic-observability", + "email": "alerts@example.com" + }, + "message": "The flyout width may need to grow to keep this readable.", + "tags": ["alert", "observability", "resizable"] +}\`} + +
+ + + + + setIsOpen(false)}> + Close + + + + +
+ )} +`, + }, + }, + render: () => { + const [isOpen, setIsOpen] = useState(false); + const titleId = 'resizablePushFlyoutTitle'; + return ( <> - - {fillerText} - - {fillerText} + setIsOpen(true)}> + Open resizable push flyout + + + {isOpen && ( + { + setIsOpen(false); + onClose(event); + }} + > + + +

Document details

+
+ + +

+ This resizable flyout pushes page content instead of + overlaying it. Drag the left edge to resize. +

+
+
+ + + +

+ Combine resizable with push type when you want users to + reference page content while viewing flyout details. +

+
+ + + {`{ + "id": "a2f9c4d1-39c2-4f89-a81e-93a3f42c1b7e", + "user": { + "name": "elastic-observability", + "email": "alerts@example.com" + }, + "message": "The flyout width may need to grow to keep this readable.", + "tags": ["alert", "observability", "resizable"] +}`} + +
+ + + + + setIsOpen(false)}> + Close + + + + +
+ )} ); }, @@ -220,3 +569,1071 @@ export const HighContrast: Story = { ), }, }; + +// Pattern stories + +export const BasicBodyOnly: Story = { + parameters: { + codeSnippet: { + snippet: `const [isOpen, setIsOpen] = useState(false); + +<> + setIsOpen(true)}>Open flyout + {isOpen && ( + setIsOpen(false)} + > + + +

+ Use this minimal flyout for quick supporting content that doesn't + require a header or footer. +

+
    +
  • Show brief contextual help or tips.
  • +
  • Display read-only details or metadata.
  • +
+
+ +
+
+ )} +`, + }, + }, + render: () => { + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + setIsOpen(true)}>Open flyout + {isOpen && ( + { + setIsOpen(false); + onClose(); + }} + > + + +

+ Use this minimal flyout for quick supporting content that + doesn't require a header or footer. +

+
    +
  • Show brief contextual help or tips.
  • +
  • Display read-only details or metadata.
  • +
+
+ +
+
+ )} + + ); + }, +}; + + +export const WithHeader: Story = { + parameters: { + codeSnippet: { + snippet: `const [isOpen, setIsOpen] = useState(false); +const titleId = 'flyoutWithHeaderTitle'; + +<> + setIsOpen(true)}>Open flyout + {isOpen && ( + setIsOpen(false)}> + + +

Keyboard shortcuts

+
+ + +

+ A recommended minimal accessible flyout pattern for + read-only reference content. +

+
+
+ + +

+ Use a header when you need a clear title and context, even if there + are no footer actions. +

+
    +
  • Ctrl + / – Toggle help
  • +
  • Ctrl + K – Open command palette
  • +
  • Ctrl + F – Search within the page
  • +
+

+ Users can close the flyout with the default close button when + they're done. +

+
+
+
+ )} +`, + }, + }, + render: () => { + const [isOpen, setIsOpen] = useState(false); + const titleId = 'flyoutWithHeaderTitle'; + + return ( + <> + setIsOpen(true)}>Open flyout + {isOpen && ( + { + setIsOpen(false); + onClose(); + }} + > + + +

Keyboard shortcuts

+
+ + +

+ A recommended minimal accessible flyout pattern for + read-only reference content. +

+
+
+ + +

+ Use a header when you need a clear title and context, even if + there are no footer actions. +

+
    +
  • + Ctrl + / – Toggle help +
  • +
  • + Ctrl + K – Open command palette +
  • +
  • + Ctrl + F – Search within the page +
  • +
+

+ Users can close the flyout with the default close button when + they're done. +

+
+
+
+ )} + + ); + }, +}; + +export const WithHeaderAndFooter: Story = { + parameters: { + codeSnippet: { + snippet: `const [isOpen, setIsOpen] = useState(false); +const titleId = 'flyoutWithFooterTitle'; + +<> + setIsOpen(true)}>Open flyout + {isOpen && ( + setIsOpen(false)}> + + +

Edit dashboard

+
+
+ + + + + + + + + + + + + + + + + + {}} /> + + + + + + setIsOpen(false)}>Cancel + + + setIsOpen(false)}> + Save changes + + + + +
+ )} +`, + }, + }, + render: () => { + const [isOpen, setIsOpen] = useState(false); + const titleId = 'flyoutWithFooterTitle'; + + return ( + <> + setIsOpen(true)}>Open flyout + {isOpen && ( + { + setIsOpen(false); + onClose(); + }} + > + + +

Edit dashboard

+
+
+ + + + + + + + + + + + + + + + + + {}} + /> + + + + + + { + setIsOpen(false); + onClose(); + }} + > + Cancel + + + + { + console.log('Dashboard saved'); + setIsOpen(false); + onClose(); + }} + > + Save changes + + + + +
+ )} + + ); + }, +}; + +export const WithBanner: Story = { + parameters: { + codeSnippet: { + snippet: `const [isOpen, setIsOpen] = useState(false); +const titleId = 'flyoutWithBannerTitle'; + +<> + setIsOpen(true)}>Edit notification settings + {isOpen && ( + setIsOpen(false)}> + + +

Edit notification settings

+
+ + +

Changes affect how alerts are delivered to this user.

+
+
+ + +

Double-check the email and severity before saving.

+ + } + > + + + + + + + + + +
+ + + + + +

Use the banner for important, contextual notices.

+
+
+ + + + setIsOpen(false)}> + Cancel + + + + setIsOpen(false)} fill> + Save changes + + + + +
+
+
+ )} +`, + }, + }, + render: () => { + const [isOpen, setIsOpen] = useState(false); + const titleId = 'flyoutWithBannerTitle'; + + return ( + <> + setIsOpen(true)}> + Edit notification settings + + + {isOpen && ( + { + setIsOpen(false); + onClose(); + }} + > + + +

Edit notification settings

+
+ + +

Changes affect how alerts are delivered to this user.

+
+
+ + +

Double-check the email and severity before saving.

+ + } + > + + + + + + + + + +
+ + + + + { + setIsOpen(false); + onClose(); + }} + > + Cancel + + + + { + console.log('Dashboard saved'); + setIsOpen(false); + onClose(); + }} + > + Save changes + + + + +
+ )} + + ); + }, +}; + +export const MenuAndContent: Story = { + parameters: { + codeSnippet: { + snippet: `const [isOpen, setIsOpen] = useState(false); +const [selectedSection, setSelectedSection] = useState('general'); +const titleId = 'menuFlyoutTitle'; + +const sections = [ + { + id: 'general', + label: 'General', + title: 'General settings', + description: 'Configure basic application settings', + fields: ( + <> + + + + + + + + ), + }, + // Add more sections as needed +]; + +const currentSection = sections.find((s) => s.id === selectedSection) || sections[0]; + +<> + setIsOpen(true)}>Open settings menu + {isOpen && ( + setIsOpen(false)}> + + +

Application settings

+
+
+ + + + + {sections.map((section) => ( + setSelectedSection(section.id)} + /> + ))} + + + + +

{currentSection.title}

+
+ + +

{currentSection.description}

+
+ + {currentSection.fields} +
+
+
+ + + + setIsOpen(false)}>Cancel + + + setIsOpen(false)}> + Save changes + + + + +
+ )} +`, + }, + }, + render: () => { + const [isOpen, setIsOpen] = useState(false); + const [selectedSection, setSelectedSection] = useState('general'); + const titleId = 'menuFlyoutTitle'; + + const sections = [ + { + id: 'general', + label: 'General', + title: 'General settings', + description: 'Configure basic application settings', + fields: ( + <> + + + + + + + + ), + }, + { + id: 'notifications', + label: 'Notifications', + title: 'Notification preferences', + description: 'Manage how you receive notifications', + fields: ( + <> + + {}} + /> + + + {}} + /> + + + + + + ), + }, + { + id: 'advanced', + label: 'Advanced', + title: 'Advanced settings', + description: 'Configure advanced options', + fields: ( + <> + + {}} + /> + + + {}} + /> + + + + + + ), + }, + ]; + + const currentSection = + sections.find((s) => s.id === selectedSection) || sections[0]; + + return ( + <> + setIsOpen(true)}> + Open settings menu + + {isOpen && ( + { + setIsOpen(false); + onClose(); + }} + > + + +

Application settings

+
+
+ + + + + {sections.map((section) => ( + setSelectedSection(section.id)} + /> + ))} + + + + +

{currentSection.title}

+
+ + +

{currentSection.description}

+
+ + {currentSection.fields} +
+
+
+ + + + { + setIsOpen(false); + onClose(); + }} + > + Cancel + + + + { + console.log('Settings saved'); + setIsOpen(false); + onClose(); + }} + > + Save changes + + + + +
+ )} + + ); + }, +}; + +export const ParentChildWithHistory: Story = { + parameters: { + codeSnippet: { + snippet: `const [isOpen, setIsOpen] = useState(false); +const [selectedRule, setSelectedRule] = useState(null); +const mainTitleId = 'parentFlyoutTitle'; +const childTitleId = 'childFlyoutTitle'; + +const rules = [ + { id: '1', name: 'High CPU usage alert', status: 'Active' }, + { id: '2', name: 'Memory threshold warning', status: 'Active' }, +]; + +const selectedRuleData = rules.find((r) => r.id === selectedRule); + +const columns = [ + { field: 'name', name: 'Rule name' }, + { field: 'status', name: 'Status' }, + { + name: 'Actions', + render: (rule) => ( + setSelectedRule(rule.id)}> + Edit + + ), + }, +]; + +<> + setIsOpen(true)}>Manage rules + {isOpen && ( + + { + setIsOpen(false); + setSelectedRule(null); + }} + > + + +

Manage rules

+
+
+ + +

Select a rule to view and edit its configuration.

+
+ + +
+ + { + setIsOpen(false); + setSelectedRule(null); + }} + > + Done + + +
+ + {selectedRule && selectedRuleData && ( + setSelectedRule(null)} + > + + setSelectedRule(null), + }, + { text: selectedRuleData.name }, + ]} + /> + + +

{selectedRuleData.name}

+
+
+ + + + + + + + + + + + + + + + {}} + /> + + + + + + + setSelectedRule(null)}> + Back to rules + + + + { + setSelectedRule(null); + }} + > + Save + + + + +
+ )} +
+ )} +`, + }, + }, + render: () => { + const [isOpen, setIsOpen] = useState(false); + const [selectedRule, setSelectedRule] = useState(null); + const mainTitleId = 'parentFlyoutTitle'; + const childTitleId = 'childFlyoutTitle'; + + const rules = [ + { id: '1', name: 'High CPU usage alert', status: 'Active' }, + { id: '2', name: 'Memory threshold warning', status: 'Active' }, + { id: '3', name: 'Disk space monitor', status: 'Inactive' }, + { id: '4', name: 'Network traffic alert', status: 'Active' }, + ]; + + const selectedRuleData = rules.find((r) => r.id === selectedRule); + + const columns = [ + { + field: 'name', + name: 'Rule name', + }, + { + field: 'status', + name: 'Status', + }, + { + name: 'Actions', + render: (rule: (typeof rules)[0]) => ( + setSelectedRule(rule.id)}> + Edit + + ), + }, + ]; + + return ( + <> + setIsOpen(true)}>Manage rules + {isOpen && ( + + { + setIsOpen(false); + setSelectedRule(null); + onClose(); + }} + > + + +

Manage rules

+
+
+ + +

Select a rule to view and edit its configuration.

+
+ + +
+ + { + setIsOpen(false); + setSelectedRule(null); + onClose(); + }} + > + Done + + +
+ + {selectedRule && selectedRuleData && ( + setSelectedRule(null)} + > + + setSelectedRule(null), + }, + { + text: selectedRuleData.name, + }, + ]} + /> + + +

{selectedRuleData.name}

+
+
+ + + + + + + + + + + + + + + + {}} + /> + + + + + + + setSelectedRule(null)}> + Back to rules + + + + { + console.log('Rule saved'); + setSelectedRule(null); + }} + > + Save + + + + +
+ )} +
+ )} + + ); + }, +}; From 8cc428caf0f27a2866b39fdef8c7e18aca3932cc Mon Sep 17 00:00:00 2001 From: Ryan Keairns Date: Mon, 3 Nov 2025 14:52:04 -0800 Subject: [PATCH 2/4] Add Share custom action example to flyoutMenuProps --- .../src/components/flyout/flyout.stories.tsx | 1907 +++++++---------- 1 file changed, 727 insertions(+), 1180 deletions(-) diff --git a/packages/eui/src/components/flyout/flyout.stories.tsx b/packages/eui/src/components/flyout/flyout.stories.tsx index edf6521b354..1cc393aeee7 100644 --- a/packages/eui/src/components/flyout/flyout.stories.tsx +++ b/packages/eui/src/components/flyout/flyout.stories.tsx @@ -28,8 +28,6 @@ import { EuiFlexItem, EuiListGroup, EuiListGroupItem, - EuiBreadcrumbs, - EuiBasicTable, EuiPanel, } from '../index'; @@ -41,11 +39,6 @@ import { EuiFlyoutFooter, EuiFlyoutResizable, } from './index'; -import { - EuiFlyoutManager, - EuiFlyoutMain, - EuiFlyoutChild, -} from './manager'; import { LOKI_SELECTORS } from '../../../.storybook/loki'; const meta: Meta = { @@ -101,7 +94,7 @@ const StatefulFlyout = ( return ( <> - handleToggle(!_isOpen)}> + handleToggle(!_isOpen)}> Toggle flyout {_isOpen && ( @@ -142,460 +135,323 @@ export const Playground: Story = { ), }; -export const PushFlyouts: Story = { - parameters: { - controls: { - include: ['pushAnimation', 'pushMinBreakpoint', 'side', 'size', 'type'], - }, - loki: { - chromeSelector: LOKI_SELECTORS.default, - }, - }, - args: { - type: 'push', - pushAnimation: false, - pushMinBreakpoint: 'm', - }, - render: ({ ...args }) => { - const [isOpen, setIsOpen] = useState(false); - const fillerText = ( - -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eu - condimentum ipsum, nec ornare metus. Sed egestas elit nec placerat - suscipit. Cras pulvinar nisi eget enim sodales fringilla. Aliquam - lobortis lorem at ornare aliquet. -

-
- ); - return ( -
- setIsOpen(!isOpen)}> - Toggle flyout - - {isOpen && ( - { - setIsOpen(false); - onClose(); - }} - > - {fillerText} - - )} - - -

The content will resized to fit the flyout.

-
-
- ); - }, -}; - -export const Resizable: Story = { +export const WithHeader: Story = { parameters: { codeSnippet: { snippet: `const [isOpen, setIsOpen] = useState(false); -const titleId = 'resizableFlyoutTitle'; +const titleId = 'flyoutWithHeaderTitle'; <> - setIsOpen(true)}> - Open resizable flyout - - + setIsOpen(true)}>Open flyout with header {isOpen && ( - setIsOpen(false)} - minWidth={420} - maxWidth={800} - > + setIsOpen(false)}> -

Document details

+

Keyboard shortcuts

- Drag the left edge of the flyout to resize when you need - more space for wide content. + A recommended minimal accessible flyout pattern for + read-only reference content.

- - +

- Resizable flyouts are useful when content width is - unpredictable, like long field values or JSON payloads. + Use a header when you need a clear title and context, even if there + are no footer actions. +

+
    +
  • Ctrl + / – Toggle help
  • +
  • Ctrl + K – Open command palette
  • +
  • Ctrl + F – Search within the page
  • +
+

+ Users can close the flyout with the default close button when + they're done.

- - - {\`{ - "id": "a2f9c4d1-39c2-4f89-a81e-93a3f42c1b7e", - "user": { - "name": "elastic-observability", - "email": "alerts@example.com" - }, - "message": "The flyout width may need to grow to keep this readable.", - "tags": ["alert", "observability", "resizable"] -}\`} -
- - - - - setIsOpen(false)}> - Close - - - - -
+
)} `, }, }, render: () => { const [isOpen, setIsOpen] = useState(false); - const titleId = 'resizableFlyoutTitle'; + const titleId = 'flyoutWithHeaderTitle'; return ( <> - setIsOpen(true)}> - Open resizable flyout - - + setIsOpen(true)}>Open flyout with header {isOpen && ( - { + onClose={() => { setIsOpen(false); - onClose(event); + onClose(); }} > -

Document details

+

Keyboard shortcuts

- Drag the left edge of the flyout to resize when you - need more space for wide content. + A recommended minimal accessible flyout pattern for + read-only reference content.

- - +

- Resizable flyouts are useful when content width is - unpredictable, like long field values or JSON payloads. + Use a header when you need a clear title and context, even if + there are no footer actions. +

+
    +
  • + Ctrl + / – Toggle help +
  • +
  • + Ctrl + K – Open command palette +
  • +
  • + Ctrl + F – Search within the page +
  • +
+

+ Users can close the flyout with the default close button when + they're done.

- - - {`{ - "id": "a2f9c4d1-39c2-4f89-a81e-93a3f42c1b7e", - "user": { - "name": "elastic-observability", - "email": "alerts@example.com" - }, - "message": "The flyout width may need to grow to keep this readable.", - "tags": ["alert", "observability", "resizable"] -}`} -
- - - - - setIsOpen(false)}> - Close - - - - -
+ )} ); }, }; -export const ResizableWithPush: Story = { +export const WithHeaderAndFooter: Story = { parameters: { codeSnippet: { snippet: `const [isOpen, setIsOpen] = useState(false); -const titleId = 'resizablePushFlyoutTitle'; +const titleId = 'flyoutWithFooterTitle'; <> - setIsOpen(true)}> - Open resizable push flyout - - + setIsOpen(true)}>Open flyout with header and footer {isOpen && ( - setIsOpen(false)} - minWidth={420} - maxWidth={800} - > + setIsOpen(false)}> -

Document details

+

Edit dashboard

- - -

- This resizable flyout pushes page content instead of - overlaying it. Drag the left edge to resize. -

-
- - -

- Combine resizable with push type when you want users to - reference page content while viewing flyout details. -

-
- - -{\`{ - "id": "a2f9c4d1-39c2-4f89-a81e-93a3f42c1b7e", - "user": { - "name": "elastic-observability", - "email": "alerts@example.com" - }, - "message": "The flyout width may need to grow to keep this readable.", - "tags": ["alert", "observability", "resizable"] -}\`} - -
- - - - - setIsOpen(false)}> - Close - - - - -
- )} -`, - }, - }, + + + + + + + + + + + + + + + + + {}} /> + + + + + + setIsOpen(false)}>Cancel + + + setIsOpen(false)}> + Save changes + + + + + + )} +`, + }, + }, render: () => { const [isOpen, setIsOpen] = useState(false); - const titleId = 'resizablePushFlyoutTitle'; + const titleId = 'flyoutWithFooterTitle'; return ( <> - setIsOpen(true)}> - Open resizable push flyout - - + setIsOpen(true)}>Open flyout with header and footer {isOpen && ( - { + onClose={() => { setIsOpen(false); - onClose(event); + onClose(); }} > -

Document details

+

Edit dashboard

- - -

- This resizable flyout pushes page content instead of - overlaying it. Drag the left edge to resize. -

-
- - -

- Combine resizable with push type when you want users to - reference page content while viewing flyout details. -

-
- - - {`{ - "id": "a2f9c4d1-39c2-4f89-a81e-93a3f42c1b7e", - "user": { - "name": "elastic-observability", - "email": "alerts@example.com" - }, - "message": "The flyout width may need to grow to keep this readable.", - "tags": ["alert", "observability", "resizable"] -}`} - + + + + + + + + + + + + + + + + + {}} + /> +
- - + - setIsOpen(false)}> - Close + { + setIsOpen(false); + onClose(); + }} + > + Cancel + + { + console.log('Dashboard saved'); + setIsOpen(false); + onClose(); + }} + > + Save changes + + -
+ )} ); }, }; -export const ManualReturnFocus: Story = { - parameters: { - controls: { - include: ['focusTrapProps'], - }, - }, - args: { - children: ( - <> - - -

Flyout header

-
-
- Flyout body - - Flyout footer - - - ), - }, - render: function Render({ ...args }) { - const manualTriggerRef = useRef(null); - - return ( - <> - - Manual trigger - - - { - if (manualTriggerRef.current) { - manualTriggerRef.current?.focus(); - return false; - } - - if (returnTo && returnTo !== document.body) { - return true; - } - - return false; - }, - }} - /> - - ); - }, -}; - -export const HighContrast: Story = { - tags: ['vrt-only'], - globals: { highContrastMode: true }, - args: { - children: ( - <> - - -

Flyout header

-
-
- Flyout banner}> - Flyout body - - - Flyout footer - - - ), - }, -}; - -// Pattern stories - -export const BasicBodyOnly: Story = { +export const WithHeaderAndMenu: Story = { parameters: { codeSnippet: { snippet: `const [isOpen, setIsOpen] = useState(false); +const titleId = 'flyoutWithHeaderAndMenuTitle'; <> - setIsOpen(true)}>Open flyout + setIsOpen(true)}>Open flyout with header and menu {isOpen && ( setIsOpen(false)} + flyoutMenuProps={{ + title: 'Flyout menu', + showBackButton: false, + }} > + + +

Document details

+
+ + +

+ Combine a menu bar with a header for navigable reference content. +

+
+

- Use this minimal flyout for quick supporting content that doesn't - require a header or footer. + Use a menu bar for shared controls (like a share button) + along with structured content in a header.

    -
  • Show brief contextual help or tips.
  • -
  • Display read-only details or metadata.
  • +
  • The menu bar provides navigation context
  • +
  • The header provides content structure
  • +
  • Perfect for drill-down or multi-step workflows
  • +
  • Back button is automatically displayed in managed session flyouts
-
)} @@ -604,30 +460,52 @@ export const BasicBodyOnly: Story = { }, render: () => { const [isOpen, setIsOpen] = useState(false); + const titleId = 'flyoutWithHeaderAndMenuTitle'; return ( <> - setIsOpen(true)}>Open flyout + setIsOpen(true)}>Open flyout with header and menu {isOpen && ( { setIsOpen(false); onClose(); }} + flyoutMenuProps={{ + customActions: [ + { + iconType: 'share', + onClick: () => alert('Share clicked'), + 'aria-label': 'Share this document', + }, + ], + }} > + + +

Document details

+
+ + +

+ Combine a menu bar with a header for navigable reference content. +

+
+

- Use this minimal flyout for quick supporting content that - doesn't require a header or footer. + Use a menu bar when you need navigation controls (like a back button) + along with structured content in a header.

    -
  • Show brief contextual help or tips.
  • -
  • Display read-only details or metadata.
  • +
  • The menu bar provides navigation context
  • +
  • The header provides content structure
  • +
  • Perfect for drill-down or multi-step workflows
  • +
  • Back button is automatically displayed in managed session flyouts
-
)} @@ -636,535 +514,563 @@ export const BasicBodyOnly: Story = { }, }; - -export const WithHeader: Story = { +export const WithBanner: Story = { parameters: { codeSnippet: { snippet: `const [isOpen, setIsOpen] = useState(false); -const titleId = 'flyoutWithHeaderTitle'; +const titleId = 'flyoutWithBannerTitle'; <> - setIsOpen(true)}>Open flyout + setIsOpen(true)}>Open flyout with banner {isOpen && ( setIsOpen(false)}> -

Keyboard shortcuts

+

Edit notification settings

-

- A recommended minimal accessible flyout pattern for - read-only reference content. -

+

Changes affect how alerts are delivered to this user.

- - -

- Use a header when you need a clear title and context, even if there - are no footer actions. -

-
    -
  • Ctrl + / – Toggle help
  • -
  • Ctrl + K – Open command palette
  • -
  • Ctrl + F – Search within the page
  • -
-

- Users can close the flyout with the default close button when - they're done. -

-
+ + +

Double-check the email and severity before saving.

+ + } + > + + + + + + + + +
+ + + + + +

Use the banner for important, contextual notices.

+
+
+ + + + setIsOpen(false)}> + Cancel + + + + setIsOpen(false)} fill> + Save changes + + + + +
+
)} `, }, }, - render: () => { + render: () => { + const [isOpen, setIsOpen] = useState(false); + const titleId = 'flyoutWithBannerTitle'; + + return ( + <> + setIsOpen(true)}> + Open flyout with banner + + + {isOpen && ( + { + setIsOpen(false); + onClose(); + }} + > + + +

Edit notification settings

+
+ + +

Changes affect how alerts are delivered to this user.

+
+
+ + +

Double-check the email and severity before saving.

+ + } + > + + + + + + + + + +
+ + + + + { + setIsOpen(false); + onClose(); + }} + > + Cancel + + + + { + console.log('Dashboard saved'); + setIsOpen(false); + onClose(); + }} + > + Save changes + + + + +
+ )} + + ); + }, +}; + +export const PushFlyouts: Story = { + parameters: { + controls: { + include: ['pushAnimation', 'pushMinBreakpoint', 'side', 'size', 'type'], + }, + loki: { + chromeSelector: LOKI_SELECTORS.default, + }, + }, + args: { + type: 'push', + pushAnimation: false, + pushMinBreakpoint: 'm', + }, + render: ({ ...args }) => { const [isOpen, setIsOpen] = useState(false); - const titleId = 'flyoutWithHeaderTitle'; - + const fillerText = ( + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eu + condimentum ipsum, nec ornare metus. Sed egestas elit nec placerat + suscipit. Cras pulvinar nisi eget enim sodales fringilla. Aliquam + lobortis lorem at ornare aliquet. +

+
+ ); return ( - <> - setIsOpen(true)}>Open flyout +
+ setIsOpen(!isOpen)}> + Toggle push flyout + + + +

The content will resized to fit the flyout.

+
{isOpen && ( { setIsOpen(false); onClose(); }} > - - -

Keyboard shortcuts

-
- - -

- A recommended minimal accessible flyout pattern for - read-only reference content. -

-
-
- - -

- Use a header when you need a clear title and context, even if - there are no footer actions. -

-
    -
  • - Ctrl + / – Toggle help -
  • -
  • - Ctrl + K – Open command palette -
  • -
  • - Ctrl + F – Search within the page -
  • -
-

- Users can close the flyout with the default close button when - they're done. -

-
-
+ {fillerText}
)} - +
); }, }; -export const WithHeaderAndFooter: Story = { +export const Resizable: Story = { parameters: { codeSnippet: { snippet: `const [isOpen, setIsOpen] = useState(false); -const titleId = 'flyoutWithFooterTitle'; +const titleId = 'resizableFlyoutTitle'; <> - setIsOpen(true)}>Open flyout + setIsOpen(true)}> + Open resizable flyout + + + +

The content will resized to fit the flyout.

+
{isOpen && ( - setIsOpen(false)}> + setIsOpen(false)} + minWidth={420} + maxWidth={800} + > -

Edit dashboard

+

Document details

+ + +

+ Drag the left edge of the flyout to resize when you need + more space for wide content. +

+
+ - - - - - - - - - - - - - - - - - {}} /> - + +

+ Resizable flyouts are useful when content width is + unpredictable, like long field values or JSON payloads. +

+
+ + + {\`{ + "id": "a2f9c4d1-39c2-4f89-a81e-93a3f42c1b7e", + "user": { + "name": "elastic-observability", + "email": "alerts@example.com" + }, + "message": "The flyout width may need to grow to keep this readable.", + "tags": ["alert", "observability", "resizable"] +}\`} +
+ - - - setIsOpen(false)}>Cancel - + - setIsOpen(false)}> - Save changes - + setIsOpen(false)}> + Close + -
+ )} `, }, }, render: () => { const [isOpen, setIsOpen] = useState(false); - const titleId = 'flyoutWithFooterTitle'; + const titleId = 'resizableFlyoutTitle'; return ( <> - setIsOpen(true)}>Open flyout - {isOpen && ( - { - setIsOpen(false); - onClose(); - }} - > - - -

Edit dashboard

-
-
- - - - - - - - - - - - - - - - - - {}} - /> - + setIsOpen(true)}> + Open resizable flyout + + + {isOpen && ( + { + setIsOpen(false); + onClose(event); + }} + > + + +

Document details

+
+ + +

+ Drag the left edge of the flyout to resize when you + need more space for wide content. +

+
+
+ + + +

+ Resizable flyouts are useful when content width is + unpredictable, like long field values or JSON payloads. +

+
+ + + {`{ + "id": "a2f9c4d1-39c2-4f89-a81e-93a3f42c1b7e", + "user": { + "name": "elastic-observability", + "email": "alerts@example.com" + }, + "message": "The flyout width may need to grow to keep this readable.", + "tags": ["alert", "observability", "resizable"] +}`} +
+ - + - { - setIsOpen(false); - onClose(); - }} - > - Cancel + setIsOpen(false)}> + Close - - { - console.log('Dashboard saved'); - setIsOpen(false); - onClose(); - }} - > - Save changes - - -
+ )} ); }, }; -export const WithBanner: Story = { +export const ResizableWithPush: Story = { parameters: { codeSnippet: { snippet: `const [isOpen, setIsOpen] = useState(false); -const titleId = 'flyoutWithBannerTitle'; +const titleId = 'resizablePushFlyoutTitle'; -<> - setIsOpen(true)}>Edit notification settings +
+ setIsOpen(true)}> + Open resizable push flyout + + + +

The content will resized to fit the flyout.

+
{isOpen && ( - setIsOpen(false)}> + setIsOpen(false)} + minWidth={420} + maxWidth={800} + > -

Edit notification settings

+

Document details

-

Changes affect how alerts are delivered to this user.

+

+ This resizable flyout pushes page content instead of + overlaying it. Drag the left edge to resize. +

- -

Double-check the email and severity before saving.

- - } - > - - - - - - - - - + + +

+ Combine resizable with push type when you want users to + reference page content while viewing flyout details. +

+
+ + +{\`{ + "id": "a2f9c4d1-39c2-4f89-a81e-93a3f42c1b7e", + "user": { + "name": "elastic-observability", + "email": "alerts@example.com" + }, + "message": "The flyout width may need to grow to keep this readable.", + "tags": ["alert", "observability", "resizable"] +}\`} +
- - - -

Use the banner for important, contextual notices.

-
-
+ - - - setIsOpen(false)}> - Cancel - - - - setIsOpen(false)} fill> - Save changes - - - + setIsOpen(false)}> + Close +
-
+ )} -`, +
`, }, }, render: () => { const [isOpen, setIsOpen] = useState(false); - const titleId = 'flyoutWithBannerTitle'; + const titleId = 'resizablePushFlyoutTitle'; return ( - <> +
setIsOpen(true)}> - Edit notification settings + Open resizable push flyout + + +

The content will resized to fit the flyout.

+
{isOpen && ( - { + minWidth={420} + maxWidth={800} + onClose={(event) => { setIsOpen(false); - onClose(); + onClose(event); }} > -

Edit notification settings

+

Document details

-

Changes affect how alerts are delivered to this user.

+

+ This resizable flyout pushes page content instead of + overlaying it. Drag the left edge to resize. +

- -

Double-check the email and severity before saving.

- - } - > - - - - - - - - - + + +

+ Combine resizable with push type when you want users to + reference page content while viewing flyout details. +

+
+ + + {`{ + "id": "a2f9c4d1-39c2-4f89-a81e-93a3f42c1b7e", + "user": { + "name": "elastic-observability", + "email": "alerts@example.com" + }, + "message": "The flyout width may need to grow to keep this readable.", + "tags": ["alert", "observability", "resizable"] +}`} +
- + - { - setIsOpen(false); - onClose(); - }} - > - Cancel + setIsOpen(false)}> + Close - - { - console.log('Dashboard saved'); - setIsOpen(false); - onClose(); - }} - > - Save changes - - -
+ )} - +
); }, }; -export const MenuAndContent: Story = { +export const BasicBodyOnly: Story = { parameters: { codeSnippet: { snippet: `const [isOpen, setIsOpen] = useState(false); -const [selectedSection, setSelectedSection] = useState('general'); -const titleId = 'menuFlyoutTitle'; - -const sections = [ - { - id: 'general', - label: 'General', - title: 'General settings', - description: 'Configure basic application settings', - fields: ( - <> - - - - - - - - ), - }, - // Add more sections as needed -]; - -const currentSection = sections.find((s) => s.id === selectedSection) || sections[0]; <> - setIsOpen(true)}>Open settings menu + setIsOpen(true)}>Open flyout with body only {isOpen && ( - setIsOpen(false)}> - - -

Application settings

-
-
+ setIsOpen(false)} + > - - - - {sections.map((section) => ( - setSelectedSection(section.id)} - /> - ))} - - - - -

{currentSection.title}

-
- - -

{currentSection.description}

-
- - {currentSection.fields} -
-
+ +

+ Use this minimal flyout for quick supporting content that doesn't + require a header or footer. +

+
    +
  • Show brief contextual help or tips.
  • +
  • Display read-only details or metadata.
  • +
+
+
- - - - setIsOpen(false)}>Cancel - - - setIsOpen(false)}> - Save changes - - - -
)} `, @@ -1172,171 +1078,31 @@ const currentSection = sections.find((s) => s.id === selectedSection) || section }, render: () => { const [isOpen, setIsOpen] = useState(false); - const [selectedSection, setSelectedSection] = useState('general'); - const titleId = 'menuFlyoutTitle'; - - const sections = [ - { - id: 'general', - label: 'General', - title: 'General settings', - description: 'Configure basic application settings', - fields: ( - <> - - - - - - - - ), - }, - { - id: 'notifications', - label: 'Notifications', - title: 'Notification preferences', - description: 'Manage how you receive notifications', - fields: ( - <> - - {}} - /> - - - {}} - /> - - - - - - ), - }, - { - id: 'advanced', - label: 'Advanced', - title: 'Advanced settings', - description: 'Configure advanced options', - fields: ( - <> - - {}} - /> - - - {}} - /> - - - - - - ), - }, - ]; - - const currentSection = - sections.find((s) => s.id === selectedSection) || sections[0]; return ( <> - setIsOpen(true)}> - Open settings menu - + setIsOpen(true)}>Open flyout with body only {isOpen && ( { setIsOpen(false); onClose(); }} > - - -

Application settings

-
-
- - - - {sections.map((section) => ( - setSelectedSection(section.id)} - /> - ))} - - - - -

{currentSection.title}

-
- - -

{currentSection.description}

-
- - {currentSection.fields} -
-
+ +

+ Use this minimal flyout for quick supporting content that + doesn't require a header or footer. +

+
    +
  • Show brief contextual help or tips.
  • +
  • Display read-only details or metadata.
  • +
+
+
- - - - { - setIsOpen(false); - onClose(); - }} - > - Cancel - - - - { - console.log('Settings saved'); - setIsOpen(false); - onClose(); - }} - > - Save changes - - - -
)} @@ -1344,296 +1110,77 @@ const currentSection = sections.find((s) => s.id === selectedSection) || section }, }; -export const ParentChildWithHistory: Story = { +export const ManualReturnFocus: Story = { parameters: { - codeSnippet: { - snippet: `const [isOpen, setIsOpen] = useState(false); -const [selectedRule, setSelectedRule] = useState(null); -const mainTitleId = 'parentFlyoutTitle'; -const childTitleId = 'childFlyoutTitle'; - -const rules = [ - { id: '1', name: 'High CPU usage alert', status: 'Active' }, - { id: '2', name: 'Memory threshold warning', status: 'Active' }, -]; - -const selectedRuleData = rules.find((r) => r.id === selectedRule); - -const columns = [ - { field: 'name', name: 'Rule name' }, - { field: 'status', name: 'Status' }, - { - name: 'Actions', - render: (rule) => ( - setSelectedRule(rule.id)}> - Edit - - ), + controls: { + include: ['focusTrapProps'], + }, }, -]; - -<> - setIsOpen(true)}>Manage rules - {isOpen && ( - - { - setIsOpen(false); - setSelectedRule(null); - }} - > + args: { + children: ( + <> -

Manage rules

+

Flyout header

- - -

Select a rule to view and edit its configuration.

-
- - -
+ Flyout body - { - setIsOpen(false); - setSelectedRule(null); - }} - > - Done - + Flyout footer -
- - {selectedRule && selectedRuleData && ( - setSelectedRule(null)} - > - - setSelectedRule(null), - }, - { text: selectedRuleData.name }, - ]} - /> - - -

{selectedRuleData.name}

-
-
- - - - - - - - - - - - - - - - {}} - /> - - - - - - - setSelectedRule(null)}> - Back to rules - - - - { - setSelectedRule(null); - }} - > - Save - - - - -
- )} -
- )} -`, - }, + + ), }, - render: () => { - const [isOpen, setIsOpen] = useState(false); - const [selectedRule, setSelectedRule] = useState(null); - const mainTitleId = 'parentFlyoutTitle'; - const childTitleId = 'childFlyoutTitle'; - - const rules = [ - { id: '1', name: 'High CPU usage alert', status: 'Active' }, - { id: '2', name: 'Memory threshold warning', status: 'Active' }, - { id: '3', name: 'Disk space monitor', status: 'Inactive' }, - { id: '4', name: 'Network traffic alert', status: 'Active' }, - ]; - - const selectedRuleData = rules.find((r) => r.id === selectedRule); - - const columns = [ - { - field: 'name', - name: 'Rule name', - }, - { - field: 'status', - name: 'Status', - }, - { - name: 'Actions', - render: (rule: (typeof rules)[0]) => ( - setSelectedRule(rule.id)}> - Edit - - ), - }, - ]; + render: function Render({ ...args }) { + const manualTriggerRef = useRef(null); return ( <> - setIsOpen(true)}>Manage rules - {isOpen && ( - - { - setIsOpen(false); - setSelectedRule(null); - onClose(); - }} - > - - -

Manage rules

-
-
- - -

Select a rule to view and edit its configuration.

-
- - -
- - { - setIsOpen(false); - setSelectedRule(null); - onClose(); - }} - > - Done - - -
+ + Manual trigger + + + { + if (manualTriggerRef.current) { + manualTriggerRef.current?.focus(); + return false; + } - {selectedRule && selectedRuleData && ( - setSelectedRule(null)} - > - - setSelectedRule(null), - }, - { - text: selectedRuleData.name, - }, - ]} - /> - - -

{selectedRuleData.name}

-
-
- - - - - - - - - - - - - - - - {}} - /> - - - - - - - setSelectedRule(null)}> - Back to rules - - - - { - console.log('Rule saved'); - setSelectedRule(null); - }} - > - Save - - - - -
- )} -
- )} + if (returnTo && returnTo !== document.body) { + return true; + } + + return false; + }, + }} + /> ); }, }; + +export const HighContrast: Story = { + tags: ['vrt-only'], + globals: { highContrastMode: true }, + args: { + children: ( + <> + + +

Flyout header

+
+
+ Flyout banner}> + Flyout body + + + Flyout footer + + + ), + }, +}; From 3fe4498a5e8e73ba5e87ed35146022140c7011f0 Mon Sep 17 00:00:00 2001 From: Ryan Keairns Date: Mon, 3 Nov 2025 16:32:36 -0800 Subject: [PATCH 3/4] Enhance Flyout Manager Playground story - Add second child flyout button to demonstrate multiple child flyouts - Add button to open second managed session (main flyout) - Improve layout with better organized content sections - Clarify terminology: rename 'grandchild' to 'second child' - Fix onClose handler prop ordering to ensure proper state updates - Update content to better explain managed session behavior - Set initial state to closed for better UX on load --- .../flyout/manager/flyout_manager.stories.tsx | 202 ++++++++++++++---- 1 file changed, 162 insertions(+), 40 deletions(-) diff --git a/packages/eui/src/components/flyout/manager/flyout_manager.stories.tsx b/packages/eui/src/components/flyout/manager/flyout_manager.stories.tsx index 7cf1641986f..0d96b48a299 100644 --- a/packages/eui/src/components/flyout/manager/flyout_manager.stories.tsx +++ b/packages/eui/src/components/flyout/manager/flyout_manager.stories.tsx @@ -11,8 +11,11 @@ import type { Meta, StoryObj } from '@storybook/react'; import React, { useState } from 'react'; import { EuiBreakpointSize } from '../../../services'; -import { EuiButton } from '../../button'; +import { EuiButton, EuiButtonEmpty } from '../../button'; +import { EuiFlexGroup, EuiFlexItem } from '../../flex'; +import { EuiHorizontalRule } from '../../horizontal_rule'; import { EuiSpacer } from '../../spacer'; +import { EuiTitle } from '../../title'; import { EuiText } from '../../text'; import { FLYOUT_TYPES, EuiFlyout } from '../flyout'; import { EuiFlyoutBody } from '../flyout_body'; @@ -171,8 +174,10 @@ const StatefulFlyout: React.FC = ({ childFlyoutResizable, ...args }) => { - const [isMainOpen, setIsMainOpen] = useState(true); + const [isMainOpen, setIsMainOpen] = useState(false); const [isChildOpen, setIsChildOpen] = useState(false); + const [isSecondChildOpen, setIsSecondChildOpen] = useState(false); + const [isSecondSessionOpen, setIsSecondSessionOpen] = useState(false); const openMain = () => { setIsMainOpen(true); @@ -181,6 +186,7 @@ const StatefulFlyout: React.FC = ({ const closeMain = () => { setIsMainOpen(false); setIsChildOpen(false); + setIsSecondChildOpen(false); playgroundActions.log('Parent flyout closed'); }; const openChild = () => { @@ -189,31 +195,76 @@ const StatefulFlyout: React.FC = ({ }; const closeChild = () => { setIsChildOpen(false); + setIsSecondChildOpen(false); playgroundActions.log('Child flyout closed'); }; + const openSecondChild = () => { + setIsSecondChildOpen(true); + playgroundActions.log('Second child flyout opened'); + }; + const closeSecondChild = () => { + setIsSecondChildOpen(false); + setIsChildOpen(false); + playgroundActions.log('Second child flyout closed'); + }; + const openSecondSession = () => { + setIsSecondSessionOpen(true); + playgroundActions.log('Second session opened'); + }; + const closeSecondSession = () => { + setIsSecondSessionOpen(false); + playgroundActions.log('Second session closed'); + }; const layoutMode = useFlyoutLayoutMode(); return ( <> - -

- This is the main page content. Watch how it behaves when the flyout - type changes. -

-

- Current layout mode: {layoutMode} -

-
- - {isMainOpen ? ( - Close Main Flyout - ) : ( - Open Main Flyout - )} - + + + +

+ Managed flyout session +

+
+
+ + +

+ This is a managed flyout session. Navigate from one main flyout to another. +
+ Note that child flyouts are not stored in the session history, so they will not be accessible via the history popover. +

+
+
+ + + {isMainOpen ? 'Close' : 'Open'} main flyout + + + + + +

+ Managed flyouts appear side-by-side or stacked based on the viewport width. +

+
+
+ + +

Resize the browser window to see how the layout mode changes. +
+ Current layout mode: {layoutMode}. +

+
+
+
{isMainOpen && ( = ({ ownFocus={false} resizable={mainFlyoutResizable} aria-label={`Main Flyout Menu (${mainSize})`} - {...args} onClose={closeMain} > - +

This is the main flyout content.

-

- Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum - neque sequi illo, cum rerum quia ab animi velit sit incidunt - inventore temporibus eaque nam veritatis amet maxime maiores - optio quam? -

- {!isChildOpen ? ( - Open child panel - ) : ( - Close child panel - )} + + {isChildOpen ? 'Close' : 'Open'} child panel + {isChildOpen && ( - +

This is the child flyout content.

Size restrictions apply:

    @@ -267,14 +312,54 @@ const StatefulFlyout: React.FC = ({ is "fill"
- -

- Lorem ipsum dolor sit amet consectetur adipisicing elit. - Dolorum neque sequi illo, cum rerum quia ab animi velit - sit incidunt inventore temporibus eaque nam veritatis amet - maxime maiores optio quam? -

+ + + + + {isSecondChildOpen ? 'Close' : 'Open'} another child + + + + + Open another main flyout + + + + {isSecondChildOpen && ( + + + +

This is the second child flyout.

+
    +
  • + Child flyouts are not stored in the session history, so they will not be accessible via the history popover. +
  • +
  • + Closing any child flyout will close all child flyouts + in the session. +
  • +
+
+
+
+ )}
{showFooter && ( @@ -296,6 +381,43 @@ const StatefulFlyout: React.FC = ({ )}
)} + {isSecondSessionOpen && ( + + + +

+ This is a completely separate flyout session, independent from + the main session. In other words, a second main flyout. +

+
    +
  • + It was opened from a child flyout but starts its own session with{' '} + session="start", so it's a sibling + session rather than a nested child. +
  • +
  • + Notice how the flyout menu shows this as a separate session that + you can navigate to independently. +
  • +
  • + Upon closing this flyout, the previous main flyout will be restored. +
  • +
+
+
+
+ )} ); }; From faf851d0e03c61737469c99062e2ef9ef79a9abb Mon Sep 17 00:00:00 2001 From: Ryan Keairns Date: Tue, 4 Nov 2025 12:47:29 -0800 Subject: [PATCH 4/4] Reorganize flyout stories structure - Move subcomponent stories to dedicated subcomponents directory - Update flyout.stories.tsx and flyout_sessions.stories.tsx - Clean up story organization for better maintainability --- .../src/components/flyout/flyout.stories.tsx | 2 - .../manager/flyout_sessions.stories.tsx | 70 ++++++++++--------- .../flyout_body.stories.tsx | 12 ++-- .../flyout_footer.stories.tsx | 14 ++-- .../flyout_header.stories.tsx | 12 ++-- .../flyout_menu.stories.tsx | 18 +++-- .../flyout_resizable.stories.tsx | 16 +++-- 7 files changed, 80 insertions(+), 64 deletions(-) rename packages/eui/src/components/flyout/{ => subcomponents}/flyout_body.stories.tsx (78%) rename packages/eui/src/components/flyout/{ => subcomponents}/flyout_footer.stories.tsx (74%) rename packages/eui/src/components/flyout/{ => subcomponents}/flyout_header.stories.tsx (78%) rename packages/eui/src/components/flyout/{ => subcomponents}/flyout_menu.stories.tsx (88%) rename packages/eui/src/components/flyout/{ => subcomponents}/flyout_resizable.stories.tsx (84%) diff --git a/packages/eui/src/components/flyout/flyout.stories.tsx b/packages/eui/src/components/flyout/flyout.stories.tsx index 1cc393aeee7..ef735cc4cf8 100644 --- a/packages/eui/src/components/flyout/flyout.stories.tsx +++ b/packages/eui/src/components/flyout/flyout.stories.tsx @@ -26,8 +26,6 @@ import { EuiComboBox, EuiFlexGroup, EuiFlexItem, - EuiListGroup, - EuiListGroupItem, EuiPanel, } from '../index'; 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..c8294c0dc92 100644 --- a/packages/eui/src/components/flyout/manager/flyout_sessions.stories.tsx +++ b/packages/eui/src/components/flyout/manager/flyout_sessions.stories.tsx @@ -30,6 +30,7 @@ import { EuiFlyoutHeader, EuiPanel, EuiProvider, + EuiHorizontalRule, EuiSpacer, EuiSwitch, EuiSwitchEvent, @@ -306,8 +307,7 @@ const MultiSessionFlyoutDemo: React.FC = () => { const listItems = useMemo( () => [ { - title: 'Session A: main size = s, child size = s', - description: ( + title: ( { hasChildBackground={hasChildBackground} /> ), + description: 'Session A: main size = s, child size = s', }, { - title: 'Session B: main size = m, child size = s', - description: ( + title: ( { hasChildBackground={hasChildBackground} /> ), + description: 'Session B: main size = m, child size = s', }, { - title: 'Session C: main size = s, child size = fill', - description: ( + title: ( { hasChildBackground={hasChildBackground} /> ), + description: 'Session C: main size = s, child size = fill', }, { - title: 'Session D: main size = fill, child size = s', - description: ( + title: ( { hasChildBackground={hasChildBackground} /> ), + description: 'Session D: main size = fill, child size = s', }, { - title: 'Session E: main size = fill', - description: ( + title: ( { hasChildBackground={hasChildBackground} /> ), + description: 'Session E: main size = fill', }, { - title: - 'Session F: main size = undefined, child size = fill (maxWidth 1000px)', - description: ( + title: ( { hasChildBackground={hasChildBackground} /> ), + description: + 'Session F: main size = undefined, child size = fill (maxWidth 1000px)', }, { - title: 'Session G: main size = fill (maxWidth 1000px), child size = s', - description: ( + title: ( { hasChildBackground={hasChildBackground} /> ), + description: 'Session G: main size = fill (maxWidth 1000px), child size = s', }, { - title: 'Session H: main size = s, child size = s, ownFocus = true', - description: ( + title: ( { hasChildBackground={hasChildBackground} /> ), + description: 'Session H: main size = s, child size = s, ownFocus = true', }, { - title: 'Non-session flyout', - description: , + title: , + description: 'Non-session flyout', }, ], [flyoutType, hasChildBackground] @@ -414,21 +415,26 @@ const MultiSessionFlyoutDemo: React.FC = () => { return ( <> - - - setChildBackgroundShaded((prev) => !prev)} - /> - + + + + + + setChildBackgroundShaded((prev) => !prev)} + /> + + + diff --git a/packages/eui/src/components/flyout/flyout_body.stories.tsx b/packages/eui/src/components/flyout/subcomponents/flyout_body.stories.tsx similarity index 78% rename from packages/eui/src/components/flyout/flyout_body.stories.tsx rename to packages/eui/src/components/flyout/subcomponents/flyout_body.stories.tsx index 571553fbf8e..4a15f88d5a9 100644 --- a/packages/eui/src/components/flyout/flyout_body.stories.tsx +++ b/packages/eui/src/components/flyout/subcomponents/flyout_body.stories.tsx @@ -9,13 +9,13 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { EuiCallOut } from '../call_out'; -import { EuiFlyout } from './flyout'; -import { EuiFlyoutBody, EuiFlyoutBodyProps } from './flyout_body'; -import { LOKI_SELECTORS } from '../../../.storybook/loki'; +import { EuiCallOut } from '../../call_out'; +import { EuiFlyout } from '../flyout'; +import { EuiFlyoutBody, EuiFlyoutBodyProps } from '../flyout_body'; +import { LOKI_SELECTORS } from '../../../../.storybook/loki'; const meta: Meta = { - title: 'Layout/EuiFlyout/EuiFlyoutBody', + title: 'Layout/EuiFlyout/Subcomponents/EuiFlyoutBody', component: EuiFlyoutBody, argTypes: { // TODO: editable banner JSX @@ -29,6 +29,7 @@ const meta: Meta = { // Flyout content is rendered in a portal chromeSelector: LOKI_SELECTORS.portal, }, + docsOnly: true, }, }; @@ -46,3 +47,4 @@ export const Playground: Story = {
), }; + diff --git a/packages/eui/src/components/flyout/flyout_footer.stories.tsx b/packages/eui/src/components/flyout/subcomponents/flyout_footer.stories.tsx similarity index 74% rename from packages/eui/src/components/flyout/flyout_footer.stories.tsx rename to packages/eui/src/components/flyout/subcomponents/flyout_footer.stories.tsx index 78b37763cf9..9d9720440e3 100644 --- a/packages/eui/src/components/flyout/flyout_footer.stories.tsx +++ b/packages/eui/src/components/flyout/subcomponents/flyout_footer.stories.tsx @@ -9,14 +9,14 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { EuiButton } from '../button'; -import { EuiFlyout } from './flyout'; -import { EuiFlyoutBody } from './flyout_body'; -import { EuiFlyoutFooter, EuiFlyoutFooterProps } from './flyout_footer'; -import { LOKI_SELECTORS } from '../../../.storybook/loki'; +import { EuiButton } from '../../button'; +import { EuiFlyout } from '../flyout'; +import { EuiFlyoutBody } from '../flyout_body'; +import { EuiFlyoutFooter, EuiFlyoutFooterProps } from '../flyout_footer'; +import { LOKI_SELECTORS } from '../../../../.storybook/loki'; const meta: Meta = { - title: 'Layout/EuiFlyout/EuiFlyoutFooter', + title: 'Layout/EuiFlyout/Subcomponents/EuiFlyoutFooter', component: EuiFlyoutFooter, argTypes: { // TODO: editable children @@ -26,6 +26,7 @@ const meta: Meta = { // Flyout content is rendered in a portal chromeSelector: LOKI_SELECTORS.portal, }, + docsOnly: true, }, }; @@ -43,3 +44,4 @@ export const Playground: Story = {
), }; + diff --git a/packages/eui/src/components/flyout/flyout_header.stories.tsx b/packages/eui/src/components/flyout/subcomponents/flyout_header.stories.tsx similarity index 78% rename from packages/eui/src/components/flyout/flyout_header.stories.tsx rename to packages/eui/src/components/flyout/subcomponents/flyout_header.stories.tsx index a0766e0cff3..a9af098cf74 100644 --- a/packages/eui/src/components/flyout/flyout_header.stories.tsx +++ b/packages/eui/src/components/flyout/subcomponents/flyout_header.stories.tsx @@ -9,13 +9,13 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { EuiTitle } from '../title'; -import { EuiFlyout } from './flyout'; -import { EuiFlyoutHeader, EuiFlyoutHeaderProps } from './flyout_header'; -import { LOKI_SELECTORS } from '../../../.storybook/loki'; +import { EuiTitle } from '../../title'; +import { EuiFlyout } from '../flyout'; +import { EuiFlyoutHeader, EuiFlyoutHeaderProps } from '../flyout_header'; +import { LOKI_SELECTORS } from '../../../../.storybook/loki'; const meta: Meta = { - title: 'Layout/EuiFlyout/EuiFlyoutHeader', + title: 'Layout/EuiFlyout/Subcomponents/EuiFlyoutHeader', component: EuiFlyoutHeader, argTypes: { // TODO: editable children JSX @@ -29,6 +29,7 @@ const meta: Meta = { // Flyout content is rendered in a portal chromeSelector: LOKI_SELECTORS.portal, }, + docsOnly: true, }, }; @@ -49,3 +50,4 @@ export const Playground: Story = { ), }; + diff --git a/packages/eui/src/components/flyout/flyout_menu.stories.tsx b/packages/eui/src/components/flyout/subcomponents/flyout_menu.stories.tsx similarity index 88% rename from packages/eui/src/components/flyout/flyout_menu.stories.tsx rename to packages/eui/src/components/flyout/subcomponents/flyout_menu.stories.tsx index 44e571933d2..8eb911aab2e 100644 --- a/packages/eui/src/components/flyout/flyout_menu.stories.tsx +++ b/packages/eui/src/components/flyout/subcomponents/flyout_menu.stories.tsx @@ -10,12 +10,12 @@ import React, { useState } from 'react'; import { action } from '@storybook/addon-actions'; import { Meta, StoryObj } from '@storybook/react'; -import { EuiButton } from '../button'; -import { EuiSpacer } from '../spacer'; -import { EuiText } from '../text'; -import { EuiFlyout } from './flyout'; -import { EuiFlyoutBody } from './flyout_body'; -import { EuiFlyoutMenu, EuiFlyoutMenuProps } from './flyout_menu'; +import { EuiButton } from '../../button'; +import { EuiSpacer } from '../../spacer'; +import { EuiText } from '../../text'; +import { EuiFlyout } from '../flyout'; +import { EuiFlyoutBody } from '../flyout_body'; +import { EuiFlyoutMenu, EuiFlyoutMenuProps } from '../flyout_menu'; interface Args extends EuiFlyoutMenuProps { showCustomActions: boolean; @@ -23,7 +23,7 @@ interface Args extends EuiFlyoutMenuProps { } const meta: Meta = { - title: 'Layout/EuiFlyout/EuiFlyoutMenu', + title: 'Layout/EuiFlyout/Subcomponents/EuiFlyoutMenu', component: EuiFlyoutMenu, argTypes: { showBackButton: { control: 'boolean' }, @@ -39,6 +39,9 @@ const meta: Meta = { showCustomActions: true, showHistoryItems: true, }, + parameters: { + docsOnly: true, + }, }; export default meta; @@ -119,3 +122,4 @@ export const MenuBarExample: StoryObj = { name: 'Playground', render: (args) => , }; + diff --git a/packages/eui/src/components/flyout/flyout_resizable.stories.tsx b/packages/eui/src/components/flyout/subcomponents/flyout_resizable.stories.tsx similarity index 84% rename from packages/eui/src/components/flyout/flyout_resizable.stories.tsx rename to packages/eui/src/components/flyout/subcomponents/flyout_resizable.stories.tsx index 5e1afbf47a4..8d1925fe2bc 100644 --- a/packages/eui/src/components/flyout/flyout_resizable.stories.tsx +++ b/packages/eui/src/components/flyout/subcomponents/flyout_resizable.stories.tsx @@ -8,20 +8,20 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { LOKI_SELECTORS } from '../../../.storybook/loki'; -import { moveStorybookControlsToCategory } from '../../../.storybook/utils'; +import { LOKI_SELECTORS } from '../../../../.storybook/loki'; +import { moveStorybookControlsToCategory } from '../../../../.storybook/utils'; -import { EuiText } from '../text'; -import { EuiFlyoutBody } from './flyout_body'; +import { EuiText } from '../../text'; +import { EuiFlyoutBody } from '../flyout_body'; -import defaultFlyoutMeta from './flyout.stories'; +import defaultFlyoutMeta from '../flyout.stories'; import { EuiFlyoutResizable, EuiFlyoutResizableProps, -} from './flyout_resizable'; +} from '../flyout_resizable'; const meta: Meta = { - title: 'Layout/EuiFlyout/EuiFlyoutResizable', + title: 'Layout/EuiFlyout/Subcomponents/EuiFlyoutResizable', component: EuiFlyoutResizable as any, argTypes: { // TODO: `size` control isn't working correctly for whatever reason (appears to be a Storybook bug @@ -40,6 +40,7 @@ const meta: Meta = { // Flyout content is rendered in a portal chromeSelector: LOKI_SELECTORS.portal, }, + docsOnly: true, }, }; // Stateful flyouts are already tested via default EuiFlyout stories, hide non-relevant props @@ -86,3 +87,4 @@ export const Playground: Story = { hideCloseButton: true, }, }; +