mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-09 08:09:32 +08:00
feat(modal, popover): add ability to temporarily disable focus trapping (#29379)
Issue number: resolves #24646
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
import { isIonContent, findClosestIonContent } from '@utils/content';
|
||||
import { createGesture } from '@utils/gesture';
|
||||
import { clamp, raf, getElementRoot } from '@utils/helpers';
|
||||
import { FOCUS_TRAP_DISABLE_CLASS } from '@utils/overlays';
|
||||
|
||||
import type { Animation } from '../../../interface';
|
||||
import type { GestureDetail } from '../../../utils/gesture';
|
||||
@ -92,7 +93,7 @@ export const createSheetGesture = (
|
||||
* as inputs should not be focusable outside
|
||||
* the sheet.
|
||||
*/
|
||||
baseEl.classList.remove('ion-disable-focus-trap');
|
||||
baseEl.classList.remove(FOCUS_TRAP_DISABLE_CLASS);
|
||||
};
|
||||
|
||||
const disableBackdrop = () => {
|
||||
@ -106,7 +107,7 @@ export const createSheetGesture = (
|
||||
* Adding this class disables focus trapping
|
||||
* for the sheet temporarily.
|
||||
*/
|
||||
baseEl.classList.add('ion-disable-focus-trap');
|
||||
baseEl.classList.add(FOCUS_TRAP_DISABLE_CLASS);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -10,6 +10,7 @@ export interface ModalOptions<T extends ComponentRef = ComponentRef> {
|
||||
delegate?: FrameworkDelegate;
|
||||
animated?: boolean;
|
||||
canDismiss?: boolean | ((data?: any, role?: string) => Promise<boolean>);
|
||||
focusTrap?: boolean;
|
||||
|
||||
mode?: Mode;
|
||||
keyboardClose?: boolean;
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
present,
|
||||
createTriggerController,
|
||||
setOverlayId,
|
||||
FOCUS_TRAP_DISABLE_CLASS,
|
||||
} from '@utils/overlays';
|
||||
import { getClassMap } from '@utils/theme';
|
||||
import { deepReady, waitForMount } from '@utils/transition';
|
||||
@ -257,6 +258,25 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Prop() keepContentsMounted = 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;
|
||||
|
||||
/**
|
||||
* Determines whether or not a modal can dismiss
|
||||
* when calling the `dismiss` method.
|
||||
@ -905,7 +925,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes } = this;
|
||||
const { handle, isSheetModal, presentingElement, htmlAttributes, handleBehavior, inheritedAttributes, focusTrap } =
|
||||
this;
|
||||
|
||||
const showHandle = handle !== false && isSheetModal;
|
||||
const mode = getIonMode(this);
|
||||
@ -926,6 +947,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
[`modal-card`]: isCardModal,
|
||||
[`modal-sheet`]: isSheetModal,
|
||||
'overlay-hidden': true,
|
||||
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false,
|
||||
...getClassMap(this.cssClass),
|
||||
}}
|
||||
onIonBackdropTap={this.onBackdropTap}
|
||||
|
||||
@ -2,6 +2,7 @@ import { h } from '@stencil/core';
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Modal } from '../../modal';
|
||||
import { FOCUS_TRAP_DISABLE_CLASS } from '@utils/overlays';
|
||||
|
||||
describe('modal: htmlAttributes inheritance', () => {
|
||||
it('should correctly inherit attributes on host', async () => {
|
||||
@ -15,3 +16,26 @@ describe('modal: htmlAttributes inheritance', () => {
|
||||
await expect(modal.getAttribute('data-testid')).toBe('basic-modal');
|
||||
});
|
||||
});
|
||||
|
||||
describe('modal: focus trap', () => {
|
||||
it('should set the focus trap class when disabled', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Modal],
|
||||
template: () => <ion-modal focusTrap={false} overlayIndex={1}></ion-modal>,
|
||||
});
|
||||
|
||||
const modal = page.body.querySelector('ion-modal')!;
|
||||
|
||||
expect(modal.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(true);
|
||||
});
|
||||
it('should not set the focus trap class by default', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Modal],
|
||||
template: () => <ion-modal overlayIndex={1}></ion-modal>,
|
||||
});
|
||||
|
||||
const modal = page.body.querySelector('ion-modal')!;
|
||||
|
||||
expect(modal.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -21,6 +21,7 @@ export interface PopoverOptions<T extends ComponentRef = ComponentRef> {
|
||||
event?: Event;
|
||||
delegate?: FrameworkDelegate;
|
||||
animated?: boolean;
|
||||
focusTrap?: boolean;
|
||||
|
||||
mode?: Mode;
|
||||
keyboardClose?: boolean;
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user