diff --git a/packages/eui/src/components/flyout/flyout.component.tsx b/packages/eui/src/components/flyout/flyout.component.tsx index b2331e3c2a4..c4300b8878c 100644 --- a/packages/eui/src/components/flyout/flyout.component.tsx +++ b/packages/eui/src/components/flyout/flyout.component.tsx @@ -611,6 +611,15 @@ export const EuiFlyoutComponent = forwardRef( return _titleId || generatedMenuId; }, [hasMenu, _titleId, generatedMenuId]); + // If the flyout level is LEVEL_MAIN, the title should be hidden by default + const flyoutMenuHideTitle = useMemo(() => { + if (!hasMenu) return undefined; + if (_flyoutMenuProps?.hideTitle !== undefined) { + return _flyoutMenuProps.hideTitle; + } + return currentSession?.mainFlyoutId === flyoutId; + }, [hasMenu, _flyoutMenuProps, currentSession, flyoutId]); + const ariaLabelledBy = useMemo(() => { if (flyoutMenuId) { return classnames(flyoutMenuId, _ariaLabelledBy); @@ -700,7 +709,11 @@ export const EuiFlyoutComponent = forwardRef( /> )} {_flyoutMenuProps && ( - + )} {resizable && ( = { title: 'Layout/EuiFlyout/EuiFlyoutMenu', component: EuiFlyoutMenu, argTypes: { + hideTitle: { control: 'boolean' }, showBackButton: { control: 'boolean' }, showCustomActions: { control: 'boolean' }, 'aria-label': { table: { disable: true } }, @@ -38,6 +40,7 @@ const meta: Meta = { showBackButton: true, showCustomActions: true, showHistoryItems: true, + hideTitle: true, }, }; @@ -45,6 +48,7 @@ export default meta; const MenuBarFlyout = (args: Args) => { const { + hideTitle, hideCloseButton, showBackButton, showCustomActions, @@ -80,6 +84,8 @@ const MenuBarFlyout = (args: Args) => { 'aria-label': `${iconType} action`, })); + const titleId = 'menu-bar-example-main-title'; + return ( <> @@ -94,8 +100,11 @@ const MenuBarFlyout = (args: Args) => { type="overlay" outsideClickCloses={false} ownFocus + aria-labelledby={titleId} flyoutMenuProps={{ title: 'Flyout title', + titleId, + hideTitle, hideCloseButton, showBackButton, backButtonProps, @@ -103,6 +112,13 @@ const MenuBarFlyout = (args: Args) => { customActions: showCustomActions ? customActions : undefined, }} > + {hideTitle && ( + + +

Simple flyout header

+
+
+ )}

Simple flyout content.

diff --git a/packages/eui/src/components/flyout/flyout_menu.styles.ts b/packages/eui/src/components/flyout/flyout_menu.styles.ts index d3ae142bf39..af73cfd659f 100644 --- a/packages/eui/src/components/flyout/flyout_menu.styles.ts +++ b/packages/eui/src/components/flyout/flyout_menu.styles.ts @@ -8,6 +8,7 @@ import { css } from '@emotion/react'; import { UseEuiTheme } from '../../services'; +import { euiScreenReaderOnly } from '../accessibility'; export const euiFlyoutMenuStyles = (euiThemeContext: UseEuiTheme) => { const { euiTheme } = euiThemeContext; @@ -31,5 +32,8 @@ export const euiFlyoutMenuStyles = (euiThemeContext: UseEuiTheme) => { euiFlyoutMenu__actions: css` block-size: calc(${euiTheme.size.m} * 1.8); `, + euiFlyoutMenu__hiddenTitle: css` + ${euiScreenReaderOnly()} + `, }; }; diff --git a/packages/eui/src/components/flyout/flyout_menu.test.tsx b/packages/eui/src/components/flyout/flyout_menu.test.tsx new file mode 100644 index 00000000000..f63652f2146 --- /dev/null +++ b/packages/eui/src/components/flyout/flyout_menu.test.tsx @@ -0,0 +1,289 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { render, screen } from '../../test/rtl'; +import { requiredProps } from '../../test'; + +import { EuiFlyoutMenu } from './flyout_menu'; +import { EuiFlyoutMenuContext } from './flyout_menu_context'; + +describe('EuiFlyoutMenu', () => { + const onClose = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const renderWithContext = (ui: React.ReactElement) => { + return render( + + {ui} + + ); + }; + + describe('basic rendering', () => { + it('renders with title', () => { + const { container } = renderWithContext( + + ); + + expect(container.querySelector('.euiFlyoutMenu')).toBeInTheDocument(); + expect(screen.getByText('Test Title')).toBeInTheDocument(); + }); + + it('renders without title', () => { + const { container } = renderWithContext( + + ); + + expect(container.querySelector('.euiFlyoutMenu')).toBeInTheDocument(); + }); + + it('renders with custom titleId', () => { + const { container } = renderWithContext( + + ); + + const titleElement = container.querySelector('#my-custom-id'); + expect(titleElement).toBeInTheDocument(); + expect(titleElement?.textContent).toBe('Custom Title'); + }); + }); + + describe('hideTitle prop', () => { + it('shows title by default when hideTitle is not specified', () => { + const { getByText } = renderWithContext( + + ); + + const title = getByText('Visible Title'); + expect(title).toBeInTheDocument(); + expect(title).toBeVisible(); + }); + + it('applies screen reader only styles when hideTitle is true', () => { + const { container, getByText } = renderWithContext( + + ); + + const titleContainer = container.querySelector('#test-title-id'); + expect(titleContainer).toBeInTheDocument(); + + const titleText = getByText('Hidden Title'); + expect(titleText).toBeInTheDocument(); + // The title should have the hiddenTitle CSS class applied + // We can't test visibility directly in JSDOM as CSS-in-JS isn't fully evaluated + expect(titleText.className).toContain('euiFlyoutMenu__hiddenTitle'); + }); + + it('shows title when hideTitle is false', () => { + const { getByText } = renderWithContext( + + ); + + const title = getByText('Visible Title'); + expect(title).toBeVisible(); + }); + }); + + describe('close button', () => { + it('renders close button by default', () => { + const { container } = renderWithContext( + + ); + + expect( + container.querySelector('[data-test-subj="euiFlyoutCloseButton"]') + ).toBeInTheDocument(); + }); + + it('hides close button when hideCloseButton is true', () => { + const { container } = renderWithContext( + + ); + + expect( + container.querySelector('[data-test-subj="euiFlyoutCloseButton"]') + ).not.toBeInTheDocument(); + }); + + it('calls onClose when close button is clicked', () => { + const { container } = renderWithContext( + + ); + + const closeButton = container.querySelector( + '[data-test-subj="euiFlyoutCloseButton"]' + ); + closeButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + + expect(onClose).toHaveBeenCalledTimes(1); + }); + }); + + describe('back button', () => { + it('does not render back button by default', () => { + const { queryByText } = renderWithContext( + + ); + + expect(queryByText('Back')).not.toBeInTheDocument(); + }); + + it('renders back button when showBackButton is true', () => { + const { getByText } = renderWithContext( + + ); + + expect(getByText('Back')).toBeInTheDocument(); + }); + + it('calls backButtonProps.onClick when back button is clicked', () => { + const handleBack = jest.fn(); + const { getByText } = renderWithContext( + + ); + + getByText('Back').click(); + expect(handleBack).toHaveBeenCalledTimes(1); + }); + }); + + describe('history items', () => { + const historyItems = [ + { title: 'History 1', onClick: jest.fn() }, + { title: 'History 2', onClick: jest.fn() }, + ]; + + it('does not render history popover when historyItems is empty', () => { + const { container } = renderWithContext( + + ); + + expect( + container.querySelector('[aria-label="History"]') + ).not.toBeInTheDocument(); + }); + + it('renders history popover when historyItems are provided', () => { + const { container } = renderWithContext( + + ); + + expect( + container.querySelector('[aria-label="History"]') + ).toBeInTheDocument(); + }); + }); + + describe('custom actions', () => { + const customActions = [ + { + iconType: 'gear', + onClick: jest.fn(), + 'aria-label': 'Settings', + }, + { + iconType: 'share', + onClick: jest.fn(), + 'aria-label': 'Share', + }, + ]; + + it('does not render custom actions when not provided', () => { + const { container } = renderWithContext( + + ); + + expect(container.querySelectorAll('.euiButtonIcon').length).toBe(1); // Only close button + }); + + it('renders custom action buttons', () => { + const { container } = renderWithContext( + + ); + + const buttons = container.querySelectorAll('.euiButtonIcon'); + // Should have 2 custom actions + 1 close button = 3 total + expect(buttons.length).toBeGreaterThanOrEqual(2); + }); + + it('calls onClick when custom action is clicked', () => { + const { container } = renderWithContext( + + ); + + const settingsButton = container.querySelector('[aria-label="Settings"]'); + settingsButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + + expect(customActions[0].onClick).toHaveBeenCalledTimes(1); + }); + }); + + describe('accessibility', () => { + it('title is accessible even when hidden', () => { + const { container } = renderWithContext( + + ); + + const title = container.querySelector('#sr-title'); + expect(title).toBeInTheDocument(); + expect(title?.textContent).toBe('Screen Reader Title'); + // The title should still be accessible to screen readers + }); + + it('provides aria-label for back button', () => { + const { container } = renderWithContext( + + ); + + const backButton = container.querySelector( + 'button[aria-label="Go back"]' + ); + expect(backButton).toBeInTheDocument(); + }); + + it('provides aria-labels for custom actions', () => { + const customActions = [ + { iconType: 'gear', onClick: jest.fn(), 'aria-label': 'Settings' }, + ]; + + const { container } = renderWithContext( + + ); + + const settingsButton = container.querySelector('[aria-label="Settings"]'); + expect(settingsButton).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/eui/src/components/flyout/flyout_menu.tsx b/packages/eui/src/components/flyout/flyout_menu.tsx index ae735158385..1330d789724 100644 --- a/packages/eui/src/components/flyout/flyout_menu.tsx +++ b/packages/eui/src/components/flyout/flyout_menu.tsx @@ -47,7 +47,7 @@ export interface EuiFlyoutHistoryItem { } /** - * Custom action item for the flyout menu header + * Custom action item for the flyout menu component */ export interface EuiFlyoutMenuCustomAction { /** @@ -75,26 +75,29 @@ export type EuiFlyoutMenuProps = CommonProps & * ```jsx * - * + * ... * * ``` */ titleId?: string; /** - * Title for the menu header + * Title for the menu component. In a managed flyout context, the title is used to indicate the flyout session for history navigation. */ title?: React.ReactNode; /** - * Hides the close button in the menu header + * Hides the title in the `EuiFlyoutMenu`. This is useful when the title is already shown in an `EuiFlyoutHeader`. + * @default true for main flyout in a managed flyout session; false otherwise + */ + hideTitle?: boolean; + /** + * Hides the close button in the menu component * @default false */ hideCloseButton?: boolean; /** - * Shows a back button in the menu header + * Shows a back button in the menu component * @default false */ showBackButton?: boolean; @@ -107,7 +110,7 @@ export type EuiFlyoutMenuProps = CommonProps & */ historyItems?: EuiFlyoutHistoryItem[]; /** - * List of custom action items for the menu header + * List of custom action items for the menu component */ customActions?: EuiFlyoutMenuCustomAction[]; }; @@ -172,9 +175,10 @@ const HistoryPopover: React.FC<{ * @private */ export const EuiFlyoutMenu: FunctionComponent = ({ - titleId, className, title, + titleId, + hideTitle, hideCloseButton, historyItems = [], showBackButton, @@ -191,7 +195,7 @@ export const EuiFlyoutMenu: FunctionComponent = ({ if (title) { titleNode = ( -

{title}

+

{title}

); } diff --git a/packages/eui/src/components/flyout/manager/flyout_managed.test.tsx b/packages/eui/src/components/flyout/manager/flyout_managed.test.tsx index be678d38556..85d7648bab2 100644 --- a/packages/eui/src/components/flyout/manager/flyout_managed.test.tsx +++ b/packages/eui/src/components/flyout/manager/flyout_managed.test.tsx @@ -277,6 +277,96 @@ describe('EuiManagedFlyout', () => { }); }); + describe('hideTitle prop handling', () => { + it('passes hideTitle prop through flyoutMenuProps when explicitly set to true', () => { + const { getByTestSubject } = renderInProvider( + {}} + flyoutMenuProps={{ + title: 'Test Title', + hideTitle: true, + }} + /> + ); + + const flyout = getByTestSubject('managed-flyout'); + expect(flyout).toBeInTheDocument(); + // The hideTitle prop should be passed through to the base component + }); + + it('passes hideTitle prop through flyoutMenuProps when explicitly set to false', () => { + const { getByTestSubject } = renderInProvider( + {}} + flyoutMenuProps={{ + title: 'Test Title', + hideTitle: false, + }} + /> + ); + + const flyout = getByTestSubject('managed-flyout'); + expect(flyout).toBeInTheDocument(); + // The hideTitle prop should be passed through to the base component + }); + + it('merges hideTitle with other flyoutMenuProps correctly', () => { + const { getByTestSubject } = renderInProvider( + {}} + flyoutMenuProps={{ + title: 'Test Title', + hideTitle: true, + hideCloseButton: false, + }} + /> + ); + + const flyout = getByTestSubject('managed-flyout'); + expect(flyout).toBeInTheDocument(); + }); + + it('does not include hideTitle when not specified for main flyout', () => { + const { getByTestSubject } = renderInProvider( + {}} + flyoutMenuProps={{ + title: 'Test Title', + // hideTitle not specified - will be auto-determined by base component + }} + /> + ); + + const flyout = getByTestSubject('managed-flyout'); + expect(flyout).toBeInTheDocument(); + }); + + it('does not include hideTitle when not specified for child flyout', () => { + const { getByTestSubject } = renderInProvider( + {}} + flyoutMenuProps={{ + title: 'Child Title', + // hideTitle not specified - will be auto-determined by base component + }} + /> + ); + + const flyout = getByTestSubject('managed-flyout'); + expect(flyout).toBeInTheDocument(); + }); + }); + describe('size handling', () => { it('defaults main flyout size to "m" when no size is provided', () => { // Import the real validation function to test the actual behavior diff --git a/packages/eui/src/components/flyout/manager/flyout_managed.tsx b/packages/eui/src/components/flyout/manager/flyout_managed.tsx index 7b5d4580d8c..090fe751e4c 100644 --- a/packages/eui/src/components/flyout/manager/flyout_managed.tsx +++ b/packages/eui/src/components/flyout/manager/flyout_managed.tsx @@ -5,8 +5,10 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + import React, { useEffect, useMemo, useRef } from 'react'; import { useEuiMemoizedStyles } from '../../../services'; +import { useEuiI18n } from '../../i18n'; import { useResizeObserver } from '../../observer/resize_observer'; import { EuiFlyoutComponent, @@ -38,7 +40,6 @@ import type { EuiFlyoutLevel } from './types'; import { createValidationErrorMessage, isNamedSize, - validateFlyoutTitle, validateManagedFlyoutSize, validateSizeCombination, } from './validation'; @@ -119,11 +120,23 @@ export const EuiManagedFlyout = ({ } } - // Validate title - const title = _flyoutMenuProps?.title || props['aria-label']; - const titleError = validateFlyoutTitle(title, flyoutId, level); - if (titleError) { - throw new Error(createValidationErrorMessage(titleError)); + const defaultTitle = useEuiI18n( + 'euiFlyoutManaged.defaultTitle', + 'Unknown Flyout' + ); + + // Set title from flyoutMenuProps or aria-label + // TODO: allow aria-labelledby references to be used + let title = _flyoutMenuProps?.title || props['aria-label']; + if ( + process.env.NODE_ENV === 'development' && + level === LEVEL_MAIN && + !title + ) { + console.warn( + `Managed flyout "${flyoutId}" requires a title, which can be provided through 'flyoutMenuProps.title' or 'aria-label'. Using default title: "${defaultTitle}"` + ); + title = defaultTitle; } const isActive = useIsFlyoutActive(flyoutId); 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..1e722779576 100644 --- a/packages/eui/src/components/flyout/manager/flyout_manager.stories.tsx +++ b/packages/eui/src/components/flyout/manager/flyout_manager.stories.tsx @@ -19,6 +19,7 @@ import { EuiFlyoutBody } from '../flyout_body'; import { EuiFlyoutFooter } from '../flyout_footer'; import { EuiFlyoutChild, EuiFlyoutChildProps } from './flyout_child'; import { useFlyoutLayoutMode } from './hooks'; +import { EuiFlyoutHeader } from '../flyout_header'; type EuiFlyoutChildActualProps = Pick< EuiFlyoutChildProps, @@ -226,6 +227,13 @@ const StatefulFlyout: React.FC = ({ {...args} onClose={closeMain} > + + +

+ Main Flyout Menu ({mainSize}) +

+
+

This is the main flyout content.

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 163473f96d0..dafa9ee6856 100644 --- a/packages/eui/src/components/flyout/manager/flyout_sessions.stories.tsx +++ b/packages/eui/src/components/flyout/manager/flyout_sessions.stories.tsx @@ -170,6 +170,11 @@ const FlyoutSession: React.FC = (props) => { onActive={mainFlyoutOnActive} onClose={mainFlyoutOnClose} > + + +

{title} - Main Flyout

+
+

This is the content of {title}.

@@ -206,7 +211,6 @@ const FlyoutSession: React.FC = (props) => { = ({ id }) => { ownFocus={false} flyoutMenuProps={{ title: `${id} flyout` }} > + + +

+ {id} - External Root Flyout +

+
+

diff --git a/packages/eui/src/components/flyout/manager/validation.test.ts b/packages/eui/src/components/flyout/manager/validation.test.ts index 55cb531c56c..4835b348112 100644 --- a/packages/eui/src/components/flyout/manager/validation.test.ts +++ b/packages/eui/src/components/flyout/manager/validation.test.ts @@ -12,7 +12,6 @@ import { validateSizeCombination, createValidationErrorMessage, FlyoutValidationError, - validateFlyoutTitle, } from './validation'; describe('Flyout Size Validation', () => { @@ -49,7 +48,7 @@ describe('Flyout Size Validation', () => { expect(error).toEqual({ type: 'INVALID_SIZE_TYPE', message: - 'Child flyouts must use named sizes (s, m, l, fill). Received: 100px', + 'Child flyout test-id must use a named size (s, m, l, fill). Received: 100px', flyoutId: 'test-id', level: 'child', size: '100px', @@ -57,26 +56,6 @@ describe('Flyout Size Validation', () => { }); }); - describe('validateFlyoutTitle', () => { - it('should return null for child flyouts without title', () => { - expect(validateFlyoutTitle(undefined, 'child-id', 'child')).toBeNull(); - }); - - it('should return null for main flyouts with valid title', () => { - expect(validateFlyoutTitle('Main Title', 'main-id', 'main')).toBeNull(); - }); - - it('should return error for empty string title', () => { - const error = validateFlyoutTitle('', 'test-id', 'main'); - expect(error).toEqual({ - type: 'INVALID_FLYOUT_MENU_TITLE', - message: `Managed flyouts require either a 'flyoutMenuProps.title' or an 'aria-label' to provide the flyout menu title.`, - flyoutId: 'test-id', - level: 'main', - }); - }); - }); - describe('validateSizeCombination', () => { it('should return null for valid combinations', () => { expect(validateSizeCombination('s', 'm')).toBeNull(); @@ -141,7 +120,7 @@ describe('Flyout Size Validation', () => { const error: FlyoutValidationError = { type: 'INVALID_SIZE_TYPE', message: - 'Child flyouts must use named sizes (s, m, l, fill). Received: 100px', + 'Child flyout test-id must use a named size (s, m, l, fill). Received: 100px', flyoutId: 'test-id', level: 'child', size: '100px', @@ -149,7 +128,7 @@ describe('Flyout Size Validation', () => { const message = createValidationErrorMessage(error); expect(message).toBe( - 'EuiFlyout validation error: Child flyouts must use named sizes (s, m, l, fill). Received: 100px' + 'EuiFlyout validation error: Child flyout test-id must use a named size (s, m, l, fill). Received: 100px' ); expect(consoleSpy).toHaveBeenCalledWith(error); }); @@ -184,14 +163,14 @@ describe('Flyout Size Validation', () => { const error: FlyoutValidationError = { type: 'INVALID_FLYOUT_MENU_TITLE', message: - "Managed flyouts require either a 'flyoutMenuProps.title' or an 'aria-label' to provide the flyout menu title.", + 'Managed flyout "test-id" requires a title, which can be provided through \'aria-label\' or \'flyoutMenuProps.title\'. Using default title: "Unknown Flyout"', flyoutId: 'test-id', level: 'main', }; const message = createValidationErrorMessage(error); expect(message).toBe( - "EuiFlyout validation error: Managed flyouts require either a 'flyoutMenuProps.title' or an 'aria-label' to provide the flyout menu title." + 'EuiFlyout validation error: Managed flyout "test-id" requires a title, which can be provided through \'aria-label\' or \'flyoutMenuProps.title\'. Using default title: "Unknown Flyout"' ); expect(consoleSpy).toHaveBeenCalledWith(error); }); diff --git a/packages/eui/src/components/flyout/manager/validation.ts b/packages/eui/src/components/flyout/manager/validation.ts index caf784da2cf..0c50ef81871 100644 --- a/packages/eui/src/components/flyout/manager/validation.ts +++ b/packages/eui/src/components/flyout/manager/validation.ts @@ -8,8 +8,7 @@ import { EuiFlyoutSize, FLYOUT_SIZES } from '../const'; import { EuiFlyoutComponentProps } from '../flyout.component'; -import { EuiFlyoutMenuProps } from '../flyout_menu'; -import { LEVEL_CHILD, LEVEL_MAIN } from './const'; +import { LEVEL_CHILD } from './const'; import { EuiFlyoutLevel } from './types'; type FlyoutValidationErrorType = @@ -46,7 +45,7 @@ export function validateManagedFlyoutSize( const namedSizes = FLYOUT_SIZES.join(', '); return { type: 'INVALID_SIZE_TYPE', - message: `Child flyouts must use named sizes (${namedSizes}). Received: ${size}`, + message: `Child flyout ${flyoutId} must use a named size (${namedSizes}). Received: ${size}`, flyoutId, level, size, @@ -55,25 +54,6 @@ export function validateManagedFlyoutSize( return null; } -/** - * Validates that a title is provided - */ -export function validateFlyoutTitle( - flyoutMenuTitle: EuiFlyoutMenuProps['title'] | undefined, - flyoutId: string, - level: EuiFlyoutLevel -): FlyoutValidationError | null { - if (level === LEVEL_MAIN && !flyoutMenuTitle) { - return { - type: 'INVALID_FLYOUT_MENU_TITLE', - message: `Managed flyouts require either a 'flyoutMenuProps.title' or an 'aria-label' to provide the flyout menu title.`, - flyoutId, - level, - }; - } - return null; -} - /** * Validates size combinations for parent-child flyouts */ diff --git a/packages/website/docs/components/containers/flyout/session-management.mdx b/packages/website/docs/components/containers/flyout/session-management.mdx index bd3bed96bbc..1949d8166fb 100644 --- a/packages/website/docs/components/containers/flyout/session-management.mdx +++ b/packages/website/docs/components/containers/flyout/session-management.mdx @@ -39,8 +39,17 @@ export default () => { setIsOpenFlyoutA(false)} > + + +

+ Welcome to the flyout session management journey! +

+ + 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. @@ -56,12 +65,17 @@ export default () => { Glad to meet you, {name || 'stranger'}!, - }} + aria-labelledby="flyoutBHeading" + flyoutMenuProps={{ title: 'Flyout B' }} onClose={() => setIsOpenFlyoutB(false)} > + + +

+ Glad to meet you, {name || 'stranger'}! +

+
+
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. @@ -75,9 +89,8 @@ export default () => { {isOpenFlyoutBChild && ( setIsOpenFlyoutBChild(false)} > @@ -125,9 +138,18 @@ configuration will be handled automatically. ### Session title -Flyout sessions within the managed system require a title that can be -referenced in the history navigation features. Use the `title` -field of the `flyoutMenuProps` to set the title of the flyout. +Managed flyout sessions require a title that can be referenced in the history +navigation features. Use the `title` field of the `flyoutMenuProps` to set the +title of the flyout session. If `flyoutMenuProps` is omitted or a `title` field +is not given in `flyoutMenuProps`, the manager will use the `aria-label` +attribute of `EuiFlyout` as the source for the session's title. If no source +for a title is given, a default title will be provided. + +:::info Note +The `title` from `flyoutMenuProps` is hidden from the menu on main flyouts (which should use +`EuiFlyoutHeader` for the visible heading) but is displayed in the menu of child flyouts. +For main flyouts, the title is primarily used for history navigation. +::: ```tsx {/* Render a new main flyout and create a flyout session */} @@ -136,9 +158,17 @@ field of the `flyoutMenuProps` to set the title of the flyout. flyoutMenuProps={{ title: 'My main flyout' }} + aria-labelledby="myFlyoutHeading" > + + +

+ I'm the main flyout heading +

+
+
- I'm the main flyout + I'm the main flyout body - I'm the child flyout that belongs to the same session as the main flyout + {/* Render a child flyout contents. Notice the lack of the EuiFlyoutHeader component - the child flyout menu shows the title from flyoutMenuProps */} + + I'm the child flyout that belongs to the same session as the main flyout +
```