Skip to content

Commit

Permalink
fix(modal, popover): fix focus-trap from preventing first click (#6769)
Browse files Browse the repository at this point in the history
**Related Issue:** #6581

## Summary

- Uses the component's host element for the focus trap. This seems to
fix the issue in the related PR. I guess we should always use the host
element in order to not run into any issues. Previously, it was using an
element inside the shadowRoot.
- Sets up a mutation observer to update focusable elements when new
elements are slotted
- Remove setting tabIndex on an element as a fallback
  • Loading branch information
driskull committed Apr 12, 2023
1 parent 198018c commit be4a63a
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 42 deletions.
18 changes: 10 additions & 8 deletions src/components/modal/modal.tsx
Expand Up @@ -190,6 +190,7 @@ export class Modal
connectConditionalSlotComponent(this);
connectLocalized(this);
connectMessages(this);
connectFocusTrap(this);
}

disconnectedCallback(): void {
Expand Down Expand Up @@ -351,9 +352,9 @@ export class Modal

modalContent: HTMLDivElement;

private mutationObserver: MutationObserver = createObserver("mutation", () => {
this.updateFooterVisibility();
});
private mutationObserver: MutationObserver = createObserver("mutation", () =>
this.handleMutationObserver()
);

private cssVarObserver: MutationObserver = createObserver("mutation", () => {
this.updateSizeCssVars();
Expand All @@ -367,8 +368,6 @@ export class Modal

focusTrap: FocusTrap;

focusTrapEl: HTMLDivElement;

closeButtonEl: HTMLButtonElement;

contentId: string;
Expand Down Expand Up @@ -443,7 +442,7 @@ export class Modal
@Method()
async setFocus(): Promise<void> {
await componentLoaded(this);
focusFirstTabbable(this.focusTrapEl);
focusFirstTabbable(this.el);
}

/**
Expand Down Expand Up @@ -480,8 +479,6 @@ export class Modal

private setTransitionEl = (el: HTMLDivElement): void => {
this.transitionEl = el;
this.focusTrapEl = el;
connectFocusTrap(this);
};

onBeforeOpen(): void {
Expand Down Expand Up @@ -560,6 +557,11 @@ export class Modal
document.documentElement.classList.remove(CSS.overflowHidden);
}

private handleMutationObserver = (): void => {
this.updateFooterVisibility();
this.updateFocusTrapElements();
};

private updateFooterVisibility = (): void => {
this.hasFooter = !!getSlotted(this.el, [SLOTS.back, SLOTS.primary, SLOTS.secondary]);
};
Expand Down
12 changes: 7 additions & 5 deletions src/components/popover/popover.tsx
Expand Up @@ -63,6 +63,7 @@ import {
setComponentLoaded,
setUpLoadableComponent
} from "../../utils/loadable";
import { createObserver } from "../../utils/observers";

const manager = new PopoverManager();

Expand Down Expand Up @@ -255,6 +256,10 @@ export class Popover
//
// --------------------------------------------------------------------------

mutationObserver: MutationObserver = createObserver("mutation", () =>
this.updateFocusTrapElements()
);

filteredFlipPlacements: EffectivePlacement[];

@Element() el: HTMLCalcitePopoverElement;
Expand Down Expand Up @@ -284,8 +289,6 @@ export class Popover

focusTrap: FocusTrap;

focusTrapEl: HTMLDivElement;

// --------------------------------------------------------------------------
//
// Lifecycle
Expand All @@ -298,6 +301,7 @@ export class Popover
connectMessages(this);
connectOpenCloseComponent(this);
this.setUpReferenceElement(this.hasLoaded);
connectFocusTrap(this);
}

async componentWillLoad(): Promise<void> {
Expand Down Expand Up @@ -391,7 +395,7 @@ export class Popover
async setFocus(): Promise<void> {
await componentLoaded(this);
forceUpdate(this.el);
focusFirstTabbable(this.focusTrapEl);
focusFirstTabbable(this.el);
}

/**
Expand All @@ -411,8 +415,6 @@ export class Popover
private setTransitionEl = (el: HTMLDivElement): void => {
this.transitionEl = el;
connectOpenCloseComponent(this);
this.focusTrapEl = el;
connectFocusTrap(this);
};

setFilteredPlacements = (): void => {
Expand Down
15 changes: 2 additions & 13 deletions src/utils/focusTrapComponent.spec.ts
Expand Up @@ -8,12 +8,11 @@ import {
describe("focusTrapComponent", () => {
it("focusTrapComponent lifecycle", () => {
const fakeComponent = {} as any;
fakeComponent.focusTrapEl = document.createElement("div");
fakeComponent.el = document.createElement("div");

connectFocusTrap(fakeComponent);

expect(fakeComponent.focusTrapEl.tabIndex).toBe(-1);
expect(fakeComponent.focusTrap).toBeDefined();
expect(fakeComponent.el).toBeDefined();
expect(fakeComponent.focusTrap.active).toBe(false);

const activateSpy = jest.fn();
Expand All @@ -34,14 +33,4 @@ describe("focusTrapComponent", () => {
deactivateFocusTrap(fakeComponent);
expect(deactivateSpy).toHaveBeenCalledTimes(1);
});

it("focusTrapEl with tabIndex`", () => {
const fakeComponent = {} as any;
fakeComponent.focusTrapEl = document.createElement("div");
fakeComponent.focusTrapEl.tabIndex = 0;

connectFocusTrap(fakeComponent);
expect(fakeComponent.focusTrapEl.tabIndex).toBe(0);
expect(fakeComponent.focusTrap).toBeDefined();
});
});
28 changes: 12 additions & 16 deletions src/utils/focusTrapComponent.ts
Expand Up @@ -4,9 +4,14 @@ import { FocusableElement, focusElement, tabbableOptions } from "./dom";
const trapStack: _FocusTrap[] = [];

/**
* Defines interface for components with a focus trap.
* Defines interface for components with a focus trap. Focusable content is required for components implementing focus trapping with this interface.
*/
export interface FocusTrapComponent {
/**
* The focus trap element.
*/
el: HTMLElement;

/**
* When `true`, prevents focus trapping.
*/
Expand All @@ -17,11 +22,6 @@ export interface FocusTrapComponent {
*/
focusTrap: FocusTrap;

/**
* The focus trap element.
*/
focusTrapEl: HTMLElement;

/**
* Method to update the element(s) that are used within the FocusTrap component.
*/
Expand All @@ -36,21 +36,17 @@ export type FocusTrap = _FocusTrap;
* @param {FocusTrapComponent} component The FocusTrap component.
*/
export function connectFocusTrap(component: FocusTrapComponent): void {
const { focusTrapEl } = component;
const { el } = component;

if (!focusTrapEl) {
if (!el) {
return;
}

if (focusTrapEl.tabIndex == null) {
focusTrapEl.tabIndex = -1;
}

const focusTrapOptions: FocusTrapOptions = {
clickOutsideDeactivates: true,
document: focusTrapEl.ownerDocument,
document: el.ownerDocument,
escapeDeactivates: false,
fallbackFocus: focusTrapEl,
fallbackFocus: el,
setReturnFocus: (el) => {
focusElement(el as FocusableElement);
return false;
Expand All @@ -59,7 +55,7 @@ export function connectFocusTrap(component: FocusTrapComponent): void {
trapStack
};

component.focusTrap = createFocusTrap(focusTrapEl, focusTrapOptions);
component.focusTrap = createFocusTrap(el, focusTrapOptions);
}

/**
Expand Down Expand Up @@ -95,5 +91,5 @@ export function deactivateFocusTrap(component: FocusTrapComponent): void {
* requestAnimationFrame(() => input.setFocus());
*/
export function updateFocusTrapElements(component: FocusTrapComponent): void {
component.focusTrap?.updateContainerElements(component.focusTrapEl);
component.focusTrap?.updateContainerElements(component.el);
}

0 comments on commit be4a63a

Please sign in to comment.