feat(modal, popover): add ability to temporarily disable focus trapping (#29379)

Issue number: resolves #24646
This commit is contained in:
Liam DeBeasi
2024-04-25 09:57:43 -04:00
committed by GitHub
parent e38e2e4d35
commit 7c00351680
11 changed files with 130 additions and 8 deletions

View File

@ -21,6 +21,7 @@ export interface PopoverOptions<T extends ComponentRef = ComponentRef> {
event?: Event;
delegate?: FrameworkDelegate;
animated?: boolean;
focusTrap?: boolean;
mode?: Mode;
keyboardClose?: boolean;

View File

@ -5,7 +5,15 @@ import { CoreDelegate, attachComponent, detachComponent } from '@utils/framework
import { addEventListener, raf, hasLazyBuild } from '@utils/helpers';
import { createLockController } from '@utils/lock-controller';
import { printIonWarning } from '@utils/logging';
import { BACKDROP, dismiss, eventMethod, prepareOverlay, present, setOverlayId } from '@utils/overlays';
import {
BACKDROP,
dismiss,
eventMethod,
prepareOverlay,
present,
setOverlayId,
FOCUS_TRAP_DISABLE_CLASS,
} from '@utils/overlays';
import { isPlatform } from '@utils/platform';
import { getClassMap } from '@utils/theme';
import { deepReady, waitForMount } from '@utils/transition';
@ -236,6 +244,25 @@ export class Popover implements ComponentInterface, PopoverInterface {
*/
@Prop() keyboardEvents = false;
/**
* If `true`, focus will not be allowed to move outside of this overlay.
* If `false`, focus will be allowed to move outside of the overlay.
*
* In most scenarios this property should remain set to `true`. Setting
* this property to `false` can cause severe accessibility issues as users
* relying on assistive technologies may be able to move focus into
* a confusing state. We recommend only setting this to `false` when
* absolutely necessary.
*
* Developers may want to consider disabling focus trapping if this
* overlay presents a non-Ionic overlay from a 3rd party library.
* Developers would disable focus trapping on the Ionic overlay
* when presenting the 3rd party overlay and then re-enable
* focus trapping when dismissing the 3rd party overlay and moving
* focus back to the Ionic overlay.
*/
@Prop() focusTrap = true;
@Watch('trigger')
@Watch('triggerAction')
onTriggerChange() {
@ -656,7 +683,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
render() {
const mode = getIonMode(this);
const { onLifecycle, parentPopover, dismissOnSelect, side, arrow, htmlAttributes } = this;
const { onLifecycle, parentPopover, dismissOnSelect, side, arrow, htmlAttributes, focusTrap } = this;
const desktop = isPlatform('desktop');
const enableArrow = arrow && !parentPopover;
@ -676,6 +703,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
'overlay-hidden': true,
'popover-desktop': desktop,
[`popover-side-${side}`]: true,
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false,
'popover-nested': !!parentPopover,
}}
onIonPopoverDidPresent={onLifecycle}

View File

@ -3,6 +3,8 @@ import { newSpecPage } from '@stencil/core/testing';
import { Popover } from '../../popover';
import { FOCUS_TRAP_DISABLE_CLASS } from '@utils/overlays';
describe('popover: htmlAttributes inheritance', () => {
it('should correctly inherit attributes on host', async () => {
const page = await newSpecPage({
@ -15,3 +17,26 @@ describe('popover: htmlAttributes inheritance', () => {
await expect(popover.getAttribute('data-testid')).toBe('basic-popover');
});
});
describe('popover: focus trap', () => {
it('should set the focus trap class when disabled', async () => {
const page = await newSpecPage({
components: [Popover],
template: () => <ion-popover focusTrap={false} overlayIndex={1}></ion-popover>,
});
const popover = page.body.querySelector('ion-popover')!;
expect(popover.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(true);
});
it('should not set the focus trap class by default', async () => {
const page = await newSpecPage({
components: [Popover],
template: () => <ion-popover overlayIndex={1}></ion-popover>,
});
const popover = page.body.querySelector('ion-popover')!;
expect(popover.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(false);
});
});