mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-04 03:48:13 +08:00
fix(sheet): disable focus trap with string-based logic as well
This commit is contained in:
@ -98,7 +98,11 @@ export const createSheetGesture = (
|
|||||||
// Respect explicit opt-out of focus trapping/backdrop interactions
|
// Respect explicit opt-out of focus trapping/backdrop interactions
|
||||||
// If focusTrap is false or showBackdrop is false, do not enable the backdrop or re-enable focus trap
|
// If focusTrap is false or showBackdrop is false, do not enable the backdrop or re-enable focus trap
|
||||||
const el = baseEl as HTMLIonModalElement & { focusTrap?: boolean; showBackdrop?: boolean };
|
const el = baseEl as HTMLIonModalElement & { focusTrap?: boolean; showBackdrop?: boolean };
|
||||||
if (el.focusTrap === false || el.showBackdrop === false) {
|
const focusTrapAttr = el.getAttribute?.('focus-trap');
|
||||||
|
const showBackdropAttr = el.getAttribute?.('show-backdrop');
|
||||||
|
const focusTrapDisabled = el.focusTrap === false || focusTrapAttr === 'false';
|
||||||
|
const backdropDisabled = el.showBackdrop === false || showBackdropAttr === 'false';
|
||||||
|
if (focusTrapDisabled || backdropDisabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
baseEl.style.setProperty('pointer-events', 'auto');
|
baseEl.style.setProperty('pointer-events', 'auto');
|
||||||
@ -241,10 +245,12 @@ export const createSheetGesture = (
|
|||||||
* ion-backdrop and .modal-wrapper always have pointer-events: auto
|
* ion-backdrop and .modal-wrapper always have pointer-events: auto
|
||||||
* applied, so the modal content can still be interacted with.
|
* applied, so the modal content can still be interacted with.
|
||||||
*/
|
*/
|
||||||
const shouldEnableBackdrop =
|
const modalEl = baseEl as HTMLIonModalElement & { focusTrap?: boolean; showBackdrop?: boolean };
|
||||||
currentBreakpoint > backdropBreakpoint &&
|
const focusTrapAttr = modalEl.getAttribute?.('focus-trap');
|
||||||
(baseEl as HTMLIonModalElement & { focusTrap?: boolean }).focusTrap !== false &&
|
const showBackdropAttr = modalEl.getAttribute?.('show-backdrop');
|
||||||
(baseEl as HTMLIonModalElement & { showBackdrop?: boolean }).showBackdrop !== false;
|
const focusTrapDisabled = modalEl.focusTrap === false || focusTrapAttr === 'false';
|
||||||
|
const backdropDisabled = modalEl.showBackdrop === false || showBackdropAttr === 'false';
|
||||||
|
const shouldEnableBackdrop = currentBreakpoint > backdropBreakpoint && !focusTrapDisabled && !backdropDisabled;
|
||||||
if (shouldEnableBackdrop) {
|
if (shouldEnableBackdrop) {
|
||||||
enableBackdrop();
|
enableBackdrop();
|
||||||
} else {
|
} else {
|
||||||
@ -591,10 +597,16 @@ export const createSheetGesture = (
|
|||||||
* Backdrop should become enabled
|
* Backdrop should become enabled
|
||||||
* after the backdropBreakpoint value
|
* after the backdropBreakpoint value
|
||||||
*/
|
*/
|
||||||
|
const modalEl = baseEl as HTMLIonModalElement & {
|
||||||
|
focusTrap?: boolean;
|
||||||
|
showBackdrop?: boolean;
|
||||||
|
};
|
||||||
|
const focusTrapAttr = modalEl.getAttribute?.('focus-trap');
|
||||||
|
const showBackdropAttr = modalEl.getAttribute?.('show-backdrop');
|
||||||
|
const focusTrapDisabled = modalEl.focusTrap === false || focusTrapAttr === 'false';
|
||||||
|
const backdropDisabled = modalEl.showBackdrop === false || showBackdropAttr === 'false';
|
||||||
const shouldEnableBackdrop =
|
const shouldEnableBackdrop =
|
||||||
currentBreakpoint > backdropBreakpoint &&
|
currentBreakpoint > backdropBreakpoint && !focusTrapDisabled && !backdropDisabled;
|
||||||
(baseEl as HTMLIonModalElement & { focusTrap?: boolean }).focusTrap !== false &&
|
|
||||||
(baseEl as HTMLIonModalElement & { showBackdrop?: boolean }).showBackdrop !== false;
|
|
||||||
if (shouldEnableBackdrop) {
|
if (shouldEnableBackdrop) {
|
||||||
enableBackdrop();
|
enableBackdrop();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1237,6 +1237,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
const isHandleCycle = handleBehavior === 'cycle';
|
const isHandleCycle = handleBehavior === 'cycle';
|
||||||
const isSheetModalWithHandle = isSheetModal && showHandle;
|
const isSheetModalWithHandle = isSheetModal && showHandle;
|
||||||
|
|
||||||
|
const focusTrapAttr = this.el.getAttribute('focus-trap');
|
||||||
return (
|
return (
|
||||||
<Host
|
<Host
|
||||||
no-router
|
no-router
|
||||||
@ -1253,7 +1254,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
|||||||
[`modal-sheet`]: isSheetModal,
|
[`modal-sheet`]: isSheetModal,
|
||||||
[`modal-no-expand-scroll`]: isSheetModal && !expandToScroll,
|
[`modal-no-expand-scroll`]: isSheetModal && !expandToScroll,
|
||||||
'overlay-hidden': true,
|
'overlay-hidden': true,
|
||||||
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false,
|
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false || focusTrapAttr === 'false',
|
||||||
...getClassMap(this.cssClass),
|
...getClassMap(this.cssClass),
|
||||||
}}
|
}}
|
||||||
onIonBackdropTap={this.onBackdropTap}
|
onIonBackdropTap={this.onBackdropTap}
|
||||||
|
|||||||
@ -28,6 +28,18 @@ describe('modal: focus trap', () => {
|
|||||||
|
|
||||||
expect(modal.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(true);
|
expect(modal.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(true);
|
||||||
});
|
});
|
||||||
|
it('should set the focus trap class when disabled via attribute string', async () => {
|
||||||
|
const page = await newSpecPage({
|
||||||
|
components: [Modal],
|
||||||
|
html: `
|
||||||
|
<ion-modal focus-trap="false"></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 () => {
|
it('should not set the focus trap class by default', async () => {
|
||||||
const page = await newSpecPage({
|
const page = await newSpecPage({
|
||||||
components: [Modal],
|
components: [Modal],
|
||||||
|
|||||||
@ -687,6 +687,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
|||||||
const { onLifecycle, parentPopover, dismissOnSelect, side, arrow, htmlAttributes, focusTrap } = this;
|
const { onLifecycle, parentPopover, dismissOnSelect, side, arrow, htmlAttributes, focusTrap } = this;
|
||||||
const desktop = isPlatform('desktop');
|
const desktop = isPlatform('desktop');
|
||||||
const enableArrow = arrow && !parentPopover;
|
const enableArrow = arrow && !parentPopover;
|
||||||
|
const focusTrapAttr = this.el.getAttribute('focus-trap');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Host
|
<Host
|
||||||
@ -704,7 +705,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
|||||||
'overlay-hidden': true,
|
'overlay-hidden': true,
|
||||||
'popover-desktop': desktop,
|
'popover-desktop': desktop,
|
||||||
[`popover-side-${side}`]: true,
|
[`popover-side-${side}`]: true,
|
||||||
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false,
|
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false || focusTrapAttr === 'false',
|
||||||
'popover-nested': !!parentPopover,
|
'popover-nested': !!parentPopover,
|
||||||
}}
|
}}
|
||||||
onIonPopoverDidPresent={onLifecycle}
|
onIonPopoverDidPresent={onLifecycle}
|
||||||
|
|||||||
@ -29,6 +29,18 @@ describe('popover: focus trap', () => {
|
|||||||
|
|
||||||
expect(popover.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(true);
|
expect(popover.classList.contains(FOCUS_TRAP_DISABLE_CLASS)).toBe(true);
|
||||||
});
|
});
|
||||||
|
it('should set the focus trap class when disabled via attribute string', async () => {
|
||||||
|
const page = await newSpecPage({
|
||||||
|
components: [Popover],
|
||||||
|
html: `
|
||||||
|
<ion-popover focus-trap="false"></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 () => {
|
it('should not set the focus trap class by default', async () => {
|
||||||
const page = await newSpecPage({
|
const page = await newSpecPage({
|
||||||
components: [Popover],
|
components: [Popover],
|
||||||
|
|||||||
@ -539,11 +539,18 @@ export const present = async <OverlayPresentOptions>(
|
|||||||
* view container subtree, skip adding aria-hidden/inert there
|
* view container subtree, skip adding aria-hidden/inert there
|
||||||
* to avoid disabling the overlay.
|
* to avoid disabling the overlay.
|
||||||
*/
|
*/
|
||||||
const overlayEl = overlay.el as HTMLIonOverlayElement & { focusTrap?: boolean; showBackdrop?: boolean };
|
const overlayEl = overlay.el as HTMLIonOverlayElement & {
|
||||||
const shouldTrapFocus = overlayEl.tagName !== 'ION-TOAST' && overlayEl.focusTrap !== false;
|
focusTrap?: boolean;
|
||||||
|
showBackdrop?: boolean;
|
||||||
|
};
|
||||||
|
const focusTrapAttr = overlayEl.getAttribute?.('focus-trap');
|
||||||
|
const showBackdropAttr = overlayEl.getAttribute?.('show-backdrop');
|
||||||
|
const focusTrapDisabled = overlayEl.focusTrap === false || focusTrapAttr === 'false';
|
||||||
|
const backdropDisabled = overlayEl.showBackdrop === false || showBackdropAttr === 'false';
|
||||||
|
const shouldTrapFocus = overlayEl.tagName !== 'ION-TOAST' && !focusTrapDisabled;
|
||||||
// Only lock out root content when backdrop is active. Developers relying on showBackdrop=false
|
// Only lock out root content when backdrop is active. Developers relying on showBackdrop=false
|
||||||
// expect background interaction to remain enabled.
|
// expect background interaction to remain enabled.
|
||||||
const shouldLockRoot = shouldTrapFocus && overlayEl.showBackdrop !== false;
|
const shouldLockRoot = shouldTrapFocus && !backdropDisabled;
|
||||||
|
|
||||||
overlay.presented = true;
|
overlay.presented = true;
|
||||||
overlay.willPresent.emit();
|
overlay.willPresent.emit();
|
||||||
@ -681,11 +688,21 @@ export const dismiss = async <OverlayDismissOptions>(
|
|||||||
*/
|
*/
|
||||||
const overlaysLockingRoot = presentedOverlays.filter((o) => {
|
const overlaysLockingRoot = presentedOverlays.filter((o) => {
|
||||||
const el = o as HTMLIonOverlayElement & { focusTrap?: boolean; showBackdrop?: boolean };
|
const el = o as HTMLIonOverlayElement & { focusTrap?: boolean; showBackdrop?: boolean };
|
||||||
return el.tagName !== 'ION-TOAST' && el.focusTrap !== false && el.showBackdrop !== false;
|
const focusTrapAttr = el.getAttribute?.('focus-trap');
|
||||||
|
const showBackdropAttr = el.getAttribute?.('show-backdrop');
|
||||||
|
const focusTrapDisabled = el.focusTrap === false || focusTrapAttr === 'false';
|
||||||
|
const backdropDisabled = el.showBackdrop === false || showBackdropAttr === 'false';
|
||||||
|
return el.tagName !== 'ION-TOAST' && !focusTrapDisabled && !backdropDisabled;
|
||||||
});
|
});
|
||||||
const overlayEl = overlay.el as HTMLIonOverlayElement & { focusTrap?: boolean; showBackdrop?: boolean };
|
const overlayEl = overlay.el as HTMLIonOverlayElement & {
|
||||||
const locksRoot =
|
focusTrap?: boolean;
|
||||||
overlayEl.tagName !== 'ION-TOAST' && overlayEl.focusTrap !== false && overlayEl.showBackdrop !== false;
|
showBackdrop?: boolean;
|
||||||
|
};
|
||||||
|
const focusTrapAttr = overlayEl.getAttribute?.('focus-trap');
|
||||||
|
const showBackdropAttr = overlayEl.getAttribute?.('show-backdrop');
|
||||||
|
const focusTrapDisabled = overlayEl.focusTrap === false || focusTrapAttr === 'false';
|
||||||
|
const backdropDisabled = overlayEl.showBackdrop === false || showBackdropAttr === 'false';
|
||||||
|
const locksRoot = overlayEl.tagName !== 'ION-TOAST' && !focusTrapDisabled && !backdropDisabled;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this is the last visible overlay that is trapping focus
|
* If this is the last visible overlay that is trapping focus
|
||||||
|
|||||||
@ -37,6 +37,26 @@ describe('overlays: scroll blocking', () => {
|
|||||||
expect(body).not.toHaveClass('backdrop-no-scroll');
|
expect(body).not.toHaveClass('backdrop-no-scroll');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not block scroll when focus-trap attribute is set to "false"', async () => {
|
||||||
|
const page = await newSpecPage({
|
||||||
|
components: [Modal],
|
||||||
|
html: `
|
||||||
|
<ion-modal focus-trap="false"></ion-modal>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const modal = page.body.querySelector('ion-modal')!;
|
||||||
|
const body = page.doc.querySelector('body')!;
|
||||||
|
|
||||||
|
await modal.present();
|
||||||
|
|
||||||
|
expect(body).not.toHaveClass('backdrop-no-scroll');
|
||||||
|
|
||||||
|
await modal.dismiss();
|
||||||
|
|
||||||
|
expect(body).not.toHaveClass('backdrop-no-scroll');
|
||||||
|
});
|
||||||
|
|
||||||
it('should not block scroll when the overlay is dismissed', async () => {
|
it('should not block scroll when the overlay is dismissed', async () => {
|
||||||
const page = await newSpecPage({
|
const page = await newSpecPage({
|
||||||
components: [Modal],
|
components: [Modal],
|
||||||
|
|||||||
Reference in New Issue
Block a user