Skip to content

Commit 695e5b1

Browse files
feat(eui): use the focus trap pub sub in flyout and popover
1 parent de8d83d commit 695e5b1

3 files changed

Lines changed: 98 additions & 19 deletions

File tree

packages/eui/src/components/flyout/flyout.component.tsx

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
useEuiMemoizedStyles,
3333
useGeneratedHtmlId,
3434
useEuiThemeCSSVariables,
35+
focusTrapPubSub,
3536
} from '../../services';
3637
import {
3738
useCurrentSession,
@@ -462,26 +463,38 @@ export const EuiFlyoutComponent = forwardRef(
462463
return selectors;
463464
}, [includeSelectorInFocusTrap, includeFixedHeadersInFocusTrap]);
464465

465-
useEffect(() => {
466-
if (focusTrapSelectors.length > 0) {
467-
const shardsEls = focusTrapSelectors.flatMap((selector) =>
468-
Array.from(document.querySelectorAll<HTMLElement>(selector))
469-
);
470-
471-
setFocusTrapShards(Array.from(shardsEls));
472-
473-
// Flyouts that are toggled from shards do not have working
474-
// focus trap autoFocus, so we need to focus the flyout wrapper ourselves
475-
shardsEls.forEach((shard) => {
476-
if (shard.contains(flyoutToggle.current)) {
477-
resizeRef?.focus();
466+
const findShards = useCallback(
467+
(shouldAutoFocus: boolean = false) => {
468+
if (focusTrapSelectors.length > 0) {
469+
const shardsEls = focusTrapSelectors.flatMap((selector) =>
470+
Array.from(document.querySelectorAll<HTMLElement>(selector))
471+
);
472+
473+
setFocusTrapShards(Array.from(shardsEls));
474+
475+
// Flyouts that are toggled from shards do not have working
476+
// focus trap autoFocus, so we need to focus the flyout wrapper ourselves
477+
if (shouldAutoFocus) {
478+
shardsEls.forEach((shard) => {
479+
if (shard.contains(flyoutToggle.current)) {
480+
resizeRef?.focus();
481+
}
482+
});
478483
}
479-
});
480-
} else {
481-
// Clear existing shards if necessary, e.g. switching to `false`
482-
setFocusTrapShards((shards) => (shards.length ? [] : shards));
483-
}
484-
}, [focusTrapSelectors, resizeRef]);
484+
} else {
485+
// Clear existing shards if necessary, e.g. switching to `false`
486+
setFocusTrapShards((shards) => (shards.length ? [] : shards));
487+
}
488+
},
489+
[focusTrapSelectors, resizeRef]
490+
);
491+
492+
useEffect(() => {
493+
findShards(true);
494+
495+
const unsubscribe = focusTrapPubSub.subscribe(findShards);
496+
return unsubscribe;
497+
}, [findShards]);
485498

486499
const focusTrapProps: EuiFlyoutComponentProps['focusTrapProps'] = useMemo(
487500
() => ({

packages/eui/src/components/flyout/flyout.spec.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { EuiFlyout } from './flyout';
2323
import { EuiCollapsibleNav, EuiCollapsibleNavGroup } from '../collapsible_nav';
2424
import { EuiIcon } from '../icon';
2525
import { EuiButton } from '../button';
26+
import { EuiPopover } from '../popover';
2627

2728
const childrenDefault = (
2829
<>
@@ -457,4 +458,65 @@ describe('EuiFlyout', () => {
457458
cy.get(':root').cssVar(euiPushFlyoutOffsetInlineEnd).should('not.exist');
458459
});
459460
});
461+
462+
describe('Focus trap shards', () => {
463+
const FlyoutWithPopoverShard = () => {
464+
const [isFlyoutOpen, setIsFlyoutOpen] = useState(true);
465+
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
466+
467+
return (
468+
<>
469+
{isFlyoutOpen && (
470+
<EuiFlyout
471+
onClose={() => setIsFlyoutOpen(false)}
472+
data-test-subj="flyoutSpec"
473+
includeSelectorInFocusTrap="[data-test-subj='popover-panel']"
474+
>
475+
<EuiPopover
476+
isOpen={isPopoverOpen}
477+
closePopover={() => setIsPopoverOpen(false)}
478+
button={
479+
<EuiButton
480+
data-test-subj="popover-trigger"
481+
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
482+
>
483+
Toggle popover
484+
</EuiButton>
485+
}
486+
panelProps={{
487+
'data-test-subj': 'popover-panel',
488+
}}
489+
>
490+
<EuiButton data-test-subj="popover-button">
491+
Popover button
492+
</EuiButton>
493+
</EuiPopover>
494+
</EuiFlyout>
495+
)}
496+
</>
497+
);
498+
};
499+
500+
it('includes popover panels in the focus trap when opened', () => {
501+
cy.mount(<FlyoutWithPopoverShard />);
502+
cy.get('[data-test-subj="flyoutSpec"]').should('be.focused');
503+
504+
// 1. Open the popover.
505+
cy.get('[data-test-subj="popover-trigger"]').click();
506+
cy.get('[data-test-subj="popover-panel"]').should('exist');
507+
508+
// 2. Tab from the popover trigger into the popover content.
509+
cy.get('[data-test-subj="popover-trigger"]').should('be.focused');
510+
cy.realPress('Tab');
511+
cy.get('[data-test-subj="popover-button"]').should('be.focused');
512+
513+
// 3. Tab from the popover content to the flyout close button.
514+
cy.realPress('Tab');
515+
cy.get('[data-test-subj="euiFlyoutCloseButton"]').should('be.focused');
516+
517+
// 4. Tab from the close button back to the popover trigger.
518+
cy.realPress('Tab');
519+
cy.get('[data-test-subj="popover-trigger"]').should('be.focused');
520+
});
521+
});
460522
});

packages/eui/src/components/popover/popover.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
getWaitDuration,
2929
performOnFrame,
3030
htmlIdGenerator,
31+
focusTrapPubSub,
3132
} from '../../services';
3233
import { setMultipleRefs } from '../../services/hooks/useCombinedRefs';
3334

@@ -432,6 +433,7 @@ export class EuiPopover extends Component<Props, State> {
432433
this.respositionTimeout = window.setTimeout(() => {
433434
this.setState({ isOpenStable: true }, () => {
434435
this.positionPopoverFixed();
436+
focusTrapPubSub.publish();
435437
});
436438
}, durationMatch + delayMatch);
437439
};
@@ -484,6 +486,7 @@ export class EuiPopover extends Component<Props, State> {
484486
this.setState({
485487
isClosing: false,
486488
});
489+
focusTrapPubSub.publish();
487490
}, closingTransitionTime);
488491
}
489492
}
@@ -494,6 +497,7 @@ export class EuiPopover extends Component<Props, State> {
494497
clearTimeout(this.strandedFocusTimeout);
495498
clearTimeout(this.closingTransitionTimeout);
496499
cancelAnimationFrame(this.closingTransitionAnimationFrame!);
500+
focusTrapPubSub.publish();
497501
}
498502

499503
onMutation = (records: MutationRecord[]) => {

0 commit comments

Comments
 (0)