From e09d5c82b378edbe990d49e9abb1eebbd26cb992 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Thu, 11 Apr 2024 10:27:08 -0400 Subject: [PATCH] feat(modal, popver): allow focus trap disable --- core/api.txt | 2 ++ core/src/components.d.ts | 16 +++++++++++++ core/src/components/modal/modal.tsx | 22 +++++++++++++++++- core/src/components/popover/popover.tsx | 30 +++++++++++++++++++++++-- 4 files changed, 67 insertions(+), 3 deletions(-) diff --git a/core/api.txt b/core/api.txt index 77fb356712..7b9ac05232 100644 --- a/core/api.txt +++ b/core/api.txt @@ -831,6 +831,7 @@ ion-modal,prop,backdropDismiss,boolean,true,false,false ion-modal,prop,breakpoints,number[] | undefined,undefined,false,false ion-modal,prop,canDismiss,((data?: any, role?: string | undefined) => Promise) | boolean,true,false,false ion-modal,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false +ion-modal,prop,focusTrap,boolean,true,false,false ion-modal,prop,handle,boolean | undefined,undefined,false,false ion-modal,prop,handleBehavior,"cycle" | "none" | undefined,'none',false,false ion-modal,prop,htmlAttributes,undefined | { [key: string]: any; },undefined,false,false @@ -979,6 +980,7 @@ ion-popover,prop,componentProps,undefined | { [key: string]: any; },undefined,fa ion-popover,prop,dismissOnSelect,boolean,false,false,false ion-popover,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false ion-popover,prop,event,any,undefined,false,false +ion-popover,prop,focusTrap,boolean,true,false,false ion-popover,prop,htmlAttributes,undefined | { [key: string]: any; },undefined,false,false ion-popover,prop,isOpen,boolean,false,false,false ion-popover,prop,keepContentsMounted,boolean,false,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 73af9bcf99..1ac723a0bb 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -1723,6 +1723,10 @@ export namespace Components { * Animation to use when the modal is presented. */ "enterAnimation"?: AnimationBuilder; + /** + * 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. This would allow developers to manually move and manage focus within the 3rd party library's overlay. + */ + "focusTrap": boolean; /** * Returns the current breakpoint of a sheet style modal */ @@ -2139,6 +2143,10 @@ export namespace Components { * The event to pass to the popover animation. */ "event": any; + /** + * 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. This would allow developers to manually move and manage focus within the 3rd party library's overlay. + */ + "focusTrap": boolean; "getParentPopover": () => Promise; "hasController": boolean; /** @@ -6457,6 +6465,10 @@ declare namespace LocalJSX { * Animation to use when the modal is presented. */ "enterAnimation"?: AnimationBuilder; + /** + * 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. This would allow developers to manually move and manage focus within the 3rd party library's overlay. + */ + "focusTrap"?: boolean; /** * The horizontal line that displays at the top of a sheet modal. It is `true` by default when setting the `breakpoints` and `initialBreakpoint` properties. */ @@ -6803,6 +6815,10 @@ declare namespace LocalJSX { * The event to pass to the popover animation. */ "event"?: any; + /** + * 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. This would allow developers to manually move and manage focus within the 3rd party library's overlay. + */ + "focusTrap"?: boolean; "hasController"?: boolean; /** * Additional attributes to pass to the popover. diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx index 2d84e42c96..29ab00c493 100644 --- a/core/src/components/modal/modal.tsx +++ b/core/src/components/modal/modal.tsx @@ -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,23 @@ 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. + * This would allow developers to manually move and manage focus + * within the 3rd party library's overlay. + */ + @Prop() focusTrap = true; + /** * Determines whether or not a modal can dismiss * when calling the `dismiss` method. @@ -905,7 +923,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 +945,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} diff --git a/core/src/components/popover/popover.tsx b/core/src/components/popover/popover.tsx index d765bc3edf..22fb6cb1ab 100644 --- a/core/src/components/popover/popover.tsx +++ b/core/src/components/popover/popover.tsx @@ -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,23 @@ 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. + * This would allow developers to manually move and manage focus + * within the 3rd party library's overlay. + */ + @Prop() focusTrap = true; + @Watch('trigger') @Watch('triggerAction') onTriggerChange() { @@ -656,7 +681,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 +701,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}