From a8ef353bc031630b373f2bdd1bdc1cafd7e35be9 Mon Sep 17 00:00:00 2001 From: Matt Driscoll Date: Thu, 17 Nov 2022 16:34:28 -0800 Subject: [PATCH] feat(popover): Add focus-trap to popover and disableFocusTrap property. (#5725) * refactor(modal): Update modal to use focus-trap module. * cleanup * cleanup * fix test * cleanup * cleanup * remove `previousActiveElement` internal prop. focus utility handles this already. * set fallbackFocus element. * feat(popover): Add focus-trap to popover. #2133 * cleanup * cleanup * cleanup * add disableFocusTrap prop to popover * review feedback * add spec test * revert changes * add popover tests for setFocus --- src/components/action-menu/action-menu.tsx | 3 +- .../input-time-picker/input-time-picker.tsx | 1 + src/components/popover/popover.e2e.ts | 23 ++++++++++++++- src/components/popover/popover.tsx | 29 +++++++++++++++++-- 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/components/action-menu/action-menu.tsx b/src/components/action-menu/action-menu.tsx index c7d2b7a4ca8..06006e91c70 100755 --- a/src/components/action-menu/action-menu.tsx +++ b/src/components/action-menu/action-menu.tsx @@ -286,7 +286,8 @@ export class ActionMenu { return ( { @@ -765,4 +765,25 @@ describe("calcite-popover", () => { expect(await popover.getProperty("open")).toBe(false); }); + + describe("setFocus", () => { + const createPopoverHTML = (contentHTML?: string, attrs?: string) => + `${contentHTML}`; + + const closeButtonFocusId = "close-button"; + + const contentButtonClass = "my-button"; + const contentHTML = ``; + + it("should focus content by default", async () => + focusable(createPopoverHTML(contentHTML), { + focusTargetSelector: `.${contentButtonClass}` + })); + + it("should focus close button", async () => + focusable(createPopoverHTML(contentHTML, "closable"), { + focusId: closeButtonFocusId, + shadowFocusTargetSelector: `.${CSS.closeButton}` + })); + }); }); diff --git a/src/components/popover/popover.tsx b/src/components/popover/popover.tsx index c39ecde6036..deb9be2388a 100644 --- a/src/components/popover/popover.tsx +++ b/src/components/popover/popover.tsx @@ -27,6 +27,13 @@ import { reposition, updateAfterClose } from "../../utils/floating-ui"; +import { + FocusTrapComponent, + FocusTrap, + connectFocusTrap, + activateFocusTrap, + deactivateFocusTrap +} from "../../utils/focusTrapComponent"; import { guid } from "../../utils/guid"; import { queryElementRoots, toAriaBoolean } from "../../utils/dom"; @@ -50,7 +57,7 @@ const manager = new PopoverManager(); styleUrl: "popover.scss", shadow: true }) -export class Popover implements FloatingUIComponent, OpenCloseComponent { +export class Popover implements FloatingUIComponent, OpenCloseComponent, FocusTrapComponent { // -------------------------------------------------------------------------- // // Properties @@ -94,6 +101,11 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { */ @Prop({ reflect: true }) disableFlip = false; + /** + * When `true`, prevents focus trapping. + */ + @Prop({ reflect: true }) disableFocusTrap = false; + /** * When `true`, removes the caret pointer. */ @@ -240,6 +252,10 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { hasLoaded = false; + focusTrap: FocusTrap; + + focusTrapEl: HTMLDivElement; + // -------------------------------------------------------------------------- // // Lifecycle @@ -271,6 +287,7 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { this.removeReferences(); disconnectFloatingUI(this, this.effectiveReferenceElement, this.el); disconnectOpenCloseComponent(this); + deactivateFocusTrap(this); } //-------------------------------------------------------------------------- @@ -350,7 +367,7 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { return; } - this.el?.focus(); + activateFocusTrap(this); } /** @@ -369,9 +386,11 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { // // -------------------------------------------------------------------------- - private setTransitionEl = (el): void => { + private setTransitionEl = (el: HTMLDivElement): void => { this.transitionEl = el; connectOpenCloseComponent(this); + this.focusTrapEl = el; + connectFocusTrap(this); }; setFilteredPlacements = (): void => { @@ -465,6 +484,9 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { onOpen(): void { this.calcitePopoverOpen.emit(); + if (!this.disableFocusTrap) { + activateFocusTrap(this); + } } onBeforeClose(): void { @@ -473,6 +495,7 @@ export class Popover implements FloatingUIComponent, OpenCloseComponent { onClose(): void { this.calcitePopoverClose.emit(); + deactivateFocusTrap(this); } storeArrowEl = (el: HTMLDivElement): void => {