diff --git a/packages/eui/changelogs/upcoming/8849.md b/packages/eui/changelogs/upcoming/8849.md
new file mode 100644
index 00000000000..522c928d862
--- /dev/null
+++ b/packages/eui/changelogs/upcoming/8849.md
@@ -0,0 +1,3 @@
+- Added `includeSelectorInFocusTrap` prop for `EuiFlyout`
+- Added component defaults for `EuiFlyout` that include `includeSelectorInFocusTrap` and `includeFixedHeadersInFocusTrap`
+
diff --git a/packages/eui/src/components/flyout/__snapshots__/flyout.test.tsx.snap b/packages/eui/src/components/flyout/__snapshots__/flyout.test.tsx.snap
index e0d99120e32..6dec7142e58 100644
--- a/packages/eui/src/components/flyout/__snapshots__/flyout.test.tsx.snap
+++ b/packages/eui/src/components/flyout/__snapshots__/flyout.test.tsx.snap
@@ -1125,7 +1125,7 @@ exports[`EuiFlyout renders extra screen reader instructions when fixed EuiHeader
>
You are in a modal dialog. Press Escape or tap/click outside the dialog on the shadowed overlay to close.
- You can still continue tabbing through the page headers in addition to the dialog.
+ You can still continue tabbing through other global page landmarks.
({
EuiOverlayMask: ({ headerZindexLocation, maskRef, ...props }: any) => (
@@ -51,10 +52,7 @@ describe('EuiFlyout', () => {
expect(baseElement).toMatchSnapshot();
expect(
- queryByText(
- 'You can still continue tabbing through the page headers in addition to the dialog.',
- { exact: false }
- )
+ queryByText('You can still continue tabbing through', { exact: false })
).toBeTruthy();
// Should not shard or render instructions when `includeFixedHeadersInFocusTrap={false}
@@ -65,12 +63,30 @@ describe('EuiFlyout', () => {
>
);
expect(
- queryByText('You can still continue tabbing through the page headers', {
+ queryByText('You can still continue tabbing through', {
exact: false,
})
).toBeFalsy();
});
+ it('renders extra screen reader instructions when specified selector exists on the page', () => {
+ const { queryByText } = render(
+ <>
+
+ {}}
+ includeSelectorInFocusTrap={'[data-custom-sidebar]'}
+ includeFixedHeadersInFocusTrap={false}
+ />
+ >
+ );
+
+ expect(
+ queryByText('You can still continue tabbing through', { exact: false })
+ ).toBeTruthy();
+ });
+
it('allows setting custom aria-describedby attributes', () => {
const { getByTestSubject } = render(
<>
@@ -308,4 +324,50 @@ describe('EuiFlyout', () => {
});
});
});
+
+ describe('component defaults', () => {
+ test('includeSelectorInFocusTrap', () => {
+ const { queryByText } = render(
+
+
+ {}} />
+ ,
+ {
+ wrapper: undefined,
+ }
+ );
+
+ expect(
+ queryByText('You can still continue tabbing through', { exact: false })
+ ).toBeTruthy();
+ });
+
+ test('includeFixedHeadersInFocusTrap', () => {
+ const { queryByText } = render(
+
+
+ {}} />
+ ,
+ {
+ wrapper: undefined,
+ }
+ );
+
+ expect(
+ queryByText('You can still continue tabbing through', { exact: false })
+ ).not.toBeTruthy();
+ });
+ });
});
diff --git a/packages/eui/src/components/flyout/flyout.tsx b/packages/eui/src/components/flyout/flyout.tsx
index c541c4a3757..7d6fbf917a3 100644
--- a/packages/eui/src/components/flyout/flyout.tsx
+++ b/packages/eui/src/components/flyout/flyout.tsx
@@ -48,6 +48,7 @@ import { EuiFlyoutCloseButton } from './_flyout_close_button';
import { euiFlyoutStyles } from './flyout.styles';
import { EuiFlyoutChild } from './flyout_child';
import { EuiFlyoutChildProvider } from './flyout_child_manager';
+import { usePropsWithComponentDefaults } from '../provider/component_defaults';
export const TYPES = ['push', 'overlay'] as const;
type _EuiFlyoutType = (typeof TYPES)[number];
@@ -162,6 +163,11 @@ interface _EuiFlyoutProps {
* Set this to `false` if you need to disable this behavior for a specific reason.
*/
includeFixedHeadersInFocusTrap?: boolean;
+
+ /**
+ * Specify additional css selectors to include in the focus trap.
+ */
+ includeSelectorInFocusTrap?: string[] | string;
}
const defaultElement = 'div';
@@ -179,7 +185,13 @@ export type EuiFlyoutProps =
export const EuiFlyout = forwardRef(
(
- {
+ props: EuiFlyoutProps,
+ ref:
+ | ((instance: ComponentPropsWithRef | null) => void)
+ | MutableRefObject | null>
+ | null
+ ) => {
+ const {
className,
children,
as,
@@ -200,14 +212,11 @@ export const EuiFlyout = forwardRef(
pushAnimation = false,
focusTrapProps: _focusTrapProps,
includeFixedHeadersInFocusTrap = true,
+ includeSelectorInFocusTrap,
'aria-describedby': _ariaDescribedBy,
...rest
- }: EuiFlyoutProps,
- ref:
- | ((instance: ComponentPropsWithRef | null) => void)
- | MutableRefObject | null>
- | null
- ) => {
+ } = usePropsWithComponentDefaults('EuiFlyout', props);
+
const Element = as || defaultElement;
const maskRef = useRef(null);
@@ -359,34 +368,51 @@ export const EuiFlyout = forwardRef(
* (both mousedown and mouseup) the overlay mask.
*/
const flyoutToggle = useRef(document.activeElement);
- const [fixedHeaders, setFixedHeaders] = useState([]);
+ const [focusTrapShards, setFocusTrapShards] = useState([]);
+
+ const focusTrapSelectors = useMemo(() => {
+ let selectors: string[] = [];
+
+ if (includeSelectorInFocusTrap) {
+ selectors = Array.isArray(includeSelectorInFocusTrap)
+ ? includeSelectorInFocusTrap
+ : [includeSelectorInFocusTrap];
+ }
- useEffect(() => {
if (includeFixedHeadersInFocusTrap) {
- const fixedHeaderEls = document.querySelectorAll(
- '.euiHeader[data-fixed-header]'
+ selectors.push('.euiHeader[data-fixed-header]');
+ }
+
+ return selectors;
+ }, [includeSelectorInFocusTrap, includeFixedHeadersInFocusTrap]);
+
+ useEffect(() => {
+ if (focusTrapSelectors.length > 0) {
+ const shardsEls = focusTrapSelectors.flatMap((selector) =>
+ Array.from(document.querySelectorAll(selector))
);
- setFixedHeaders(Array.from(fixedHeaderEls));
- // Flyouts that are toggled from fixed headers do not have working
+ setFocusTrapShards(Array.from(shardsEls));
+
+ // Flyouts that are toggled from shards do not have working
// focus trap autoFocus, so we need to focus the flyout wrapper ourselves
- fixedHeaderEls.forEach((header) => {
- if (header.contains(flyoutToggle.current)) {
+ shardsEls.forEach((shard) => {
+ if (shard.contains(flyoutToggle.current)) {
resizeRef?.focus();
}
});
} else {
- // Clear existing headers if necessary, e.g. switching to `false`
- setFixedHeaders((headers) => (headers.length ? [] : headers));
+ // Clear existing shards if necessary, e.g. switching to `false`
+ setFocusTrapShards((shards) => (shards.length ? [] : shards));
}
- }, [includeFixedHeadersInFocusTrap, resizeRef]);
+ }, [focusTrapSelectors, resizeRef]);
const focusTrapProps: EuiFlyoutProps['focusTrapProps'] = useMemo(
() => ({
..._focusTrapProps,
- shards: [...fixedHeaders, ...(_focusTrapProps?.shards || [])],
+ shards: [...focusTrapShards, ...(_focusTrapProps?.shards || [])],
}),
- [_focusTrapProps, fixedHeaders]
+ [_focusTrapProps, focusTrapShards]
);
/*
@@ -411,16 +437,16 @@ export const EuiFlyout = forwardRef(
default="You are in a non-modal dialog. To close the dialog, press Escape."
/>
)}{' '}
- {fixedHeaders.length > 0 && (
+ {focusTrapShards.length > 0 && (
)}
),
- [hasOverlayMask, descriptionId, fixedHeaders.length]
+ [hasOverlayMask, descriptionId, focusTrapShards.length]
);
/*
diff --git a/packages/eui/src/components/provider/component_defaults/component_defaults.tsx b/packages/eui/src/components/provider/component_defaults/component_defaults.tsx
index 8373aeb9b37..85e7a60e1c5 100644
--- a/packages/eui/src/components/provider/component_defaults/component_defaults.tsx
+++ b/packages/eui/src/components/provider/component_defaults/component_defaults.tsx
@@ -17,6 +17,7 @@ import React, {
import type { EuiPortalProps } from '../../portal';
import type { EuiFocusTrapProps } from '../../focus_trap';
import type { EuiTablePaginationProps, EuiTableProps } from '../../table';
+import type { EuiFlyoutProps } from '../../flyout';
export type EuiComponentDefaults = {
/**
@@ -43,6 +44,15 @@ export type EuiComponentDefaults = {
* Defaults will be inherited by all `EuiBasicTable`s and `EuiInMemoryTable`s.
*/
EuiTable?: Pick;
+
+ /**
+ * Provide a global configuration for `EuiFlyout`s.
+ * Defaults will be inherited by all `EuiFlyout`s.
+ */
+ EuiFlyout?: Pick<
+ EuiFlyoutProps,
+ 'includeSelectorInFocusTrap' | 'includeFixedHeadersInFocusTrap'
+ >;
};
// Declaring as a static const for reference integrity/reducing rerenders