Compare commits

..

3 Commits

Author SHA1 Message Date
ShaneK
f332f62cbd fix(sheet): disable focus trap with string-based logic as well 2025-10-02 06:38:43 -07:00
Shane
3b80473f2f merge release-8.7.5 (#30695)
v8.7.5

---------

Co-authored-by: ionitron <hi@ionicframework.com>
2025-09-24 13:08:06 -07:00
ionitron
99d2b731f5 chore(): update package lock files 2025-09-24 19:52:26 +00:00
15 changed files with 143 additions and 68 deletions

View File

@@ -18338,4 +18338,4 @@
"dev": true
}
}
}
}

View File

@@ -98,7 +98,11 @@ export const createSheetGesture = (
// 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
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;
}
baseEl.style.setProperty('pointer-events', 'auto');
@@ -241,10 +245,12 @@ export const createSheetGesture = (
* ion-backdrop and .modal-wrapper always have pointer-events: auto
* applied, so the modal content can still be interacted with.
*/
const shouldEnableBackdrop =
currentBreakpoint > backdropBreakpoint &&
(baseEl as HTMLIonModalElement & { focusTrap?: boolean }).focusTrap !== false &&
(baseEl as HTMLIonModalElement & { showBackdrop?: boolean }).showBackdrop !== false;
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 = currentBreakpoint > backdropBreakpoint && !focusTrapDisabled && !backdropDisabled;
if (shouldEnableBackdrop) {
enableBackdrop();
} else {
@@ -591,10 +597,16 @@ export const createSheetGesture = (
* Backdrop should become enabled
* 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 =
currentBreakpoint > backdropBreakpoint &&
(baseEl as HTMLIonModalElement & { focusTrap?: boolean }).focusTrap !== false &&
(baseEl as HTMLIonModalElement & { showBackdrop?: boolean }).showBackdrop !== false;
currentBreakpoint > backdropBreakpoint && !focusTrapDisabled && !backdropDisabled;
if (shouldEnableBackdrop) {
enableBackdrop();
} else {

View File

@@ -1237,6 +1237,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
const isHandleCycle = handleBehavior === 'cycle';
const isSheetModalWithHandle = isSheetModal && showHandle;
const focusTrapAttr = this.el.getAttribute('focus-trap');
return (
<Host
no-router
@@ -1253,7 +1254,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
[`modal-sheet`]: isSheetModal,
[`modal-no-expand-scroll`]: isSheetModal && !expandToScroll,
'overlay-hidden': true,
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false,
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false || focusTrapAttr === 'false',
...getClassMap(this.cssClass),
}}
onIonBackdropTap={this.onBackdropTap}

View File

@@ -28,6 +28,18 @@ describe('modal: focus trap', () => {
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 () => {
const page = await newSpecPage({
components: [Modal],

View File

@@ -687,6 +687,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
const { onLifecycle, parentPopover, dismissOnSelect, side, arrow, htmlAttributes, focusTrap } = this;
const desktop = isPlatform('desktop');
const enableArrow = arrow && !parentPopover;
const focusTrapAttr = this.el.getAttribute('focus-trap');
return (
<Host
@@ -704,7 +705,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
'overlay-hidden': true,
'popover-desktop': desktop,
[`popover-side-${side}`]: true,
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false,
[FOCUS_TRAP_DISABLE_CLASS]: focusTrap === false || focusTrapAttr === 'false',
'popover-nested': !!parentPopover,
}}
onIonPopoverDidPresent={onLifecycle}

View File

@@ -29,6 +29,18 @@ describe('popover: focus trap', () => {
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 () => {
const page = await newSpecPage({
components: [Popover],

View File

@@ -539,11 +539,18 @@ export const present = async <OverlayPresentOptions>(
* view container subtree, skip adding aria-hidden/inert there
* to avoid disabling the overlay.
*/
const overlayEl = overlay.el as HTMLIonOverlayElement & { focusTrap?: boolean; showBackdrop?: boolean };
const shouldTrapFocus = overlayEl.tagName !== 'ION-TOAST' && overlayEl.focusTrap !== false;
const overlayEl = overlay.el as HTMLIonOverlayElement & {
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
// expect background interaction to remain enabled.
const shouldLockRoot = shouldTrapFocus && overlayEl.showBackdrop !== false;
const shouldLockRoot = shouldTrapFocus && !backdropDisabled;
overlay.presented = true;
overlay.willPresent.emit();
@@ -681,11 +688,21 @@ export const dismiss = async <OverlayDismissOptions>(
*/
const overlaysLockingRoot = presentedOverlays.filter((o) => {
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 locksRoot =
overlayEl.tagName !== 'ION-TOAST' && overlayEl.focusTrap !== false && overlayEl.showBackdrop !== false;
const overlayEl = overlay.el as HTMLIonOverlayElement & {
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 locksRoot = overlayEl.tagName !== 'ION-TOAST' && !focusTrapDisabled && !backdropDisabled;
/**
* If this is the last visible overlay that is trapping focus

View File

@@ -37,6 +37,26 @@ describe('overlays: scroll blocking', () => {
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 () => {
const page = await newSpecPage({
components: [Modal],

View File

@@ -1031,9 +1031,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.4.tgz",
"integrity": "sha512-ZCJYKLWdxq+x4OmEDvodqR+y/FSDJYkkFHozWe1+b/p0l9lNN13lLuSZVs0AEOgPtO89Atl67rTbpGE2ad/SCw==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz",
"integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.36.2",
@@ -7306,9 +7306,9 @@
"dev": true
},
"@ionic/core": {
"version": "8.7.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.4.tgz",
"integrity": "sha512-ZCJYKLWdxq+x4OmEDvodqR+y/FSDJYkkFHozWe1+b/p0l9lNN13lLuSZVs0AEOgPtO89Atl67rTbpGE2ad/SCw==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz",
"integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==",
"requires": {
"@stencil/core": "4.36.2",
"ionicons": "^8.0.13",
@@ -11286,4 +11286,4 @@
}
}
}
}
}

View File

@@ -1398,9 +1398,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.4.tgz",
"integrity": "sha512-ZCJYKLWdxq+x4OmEDvodqR+y/FSDJYkkFHozWe1+b/p0l9lNN13lLuSZVs0AEOgPtO89Atl67rTbpGE2ad/SCw==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz",
"integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.36.2",
@@ -9079,4 +9079,4 @@
}
}
}
}
}

View File

@@ -10,4 +10,4 @@
"license": "MIT"
}
}
}
}

View File

@@ -238,9 +238,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.4.tgz",
"integrity": "sha512-ZCJYKLWdxq+x4OmEDvodqR+y/FSDJYkkFHozWe1+b/p0l9lNN13lLuSZVs0AEOgPtO89Atl67rTbpGE2ad/SCw==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz",
"integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.36.2",
@@ -415,12 +415,12 @@
}
},
"node_modules/@ionic/react": {
"version": "8.7.4",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.4.tgz",
"integrity": "sha512-ImJo4VLT687nGS72zGo87b+aaaD4tWWFGtpbmM21rnh2xkWQJKjvZQRAgA6AK9tdMMYHqy0bJvAkykFpzV68XA==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.5.tgz",
"integrity": "sha512-ID1in1YhmjlpLUF1aMv9zSEVc+ZiXs1fNWKJLK4U02LRQoNxmKagwYLxItAuls0KqduCErcqfC5pOcBJDtMl4Q==",
"license": "MIT",
"dependencies": {
"@ionic/core": "8.7.4",
"@ionic/core": "8.7.5",
"ionicons": "^8.0.13",
"tslib": "*"
},
@@ -4175,9 +4175,9 @@
"dev": true
},
"@ionic/core": {
"version": "8.7.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.4.tgz",
"integrity": "sha512-ZCJYKLWdxq+x4OmEDvodqR+y/FSDJYkkFHozWe1+b/p0l9lNN13lLuSZVs0AEOgPtO89Atl67rTbpGE2ad/SCw==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz",
"integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==",
"requires": {
"@stencil/core": "4.36.2",
"ionicons": "^8.0.13",
@@ -4281,11 +4281,11 @@
"requires": {}
},
"@ionic/react": {
"version": "8.7.4",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.4.tgz",
"integrity": "sha512-ImJo4VLT687nGS72zGo87b+aaaD4tWWFGtpbmM21rnh2xkWQJKjvZQRAgA6AK9tdMMYHqy0bJvAkykFpzV68XA==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/react/-/react-8.7.5.tgz",
"integrity": "sha512-ID1in1YhmjlpLUF1aMv9zSEVc+ZiXs1fNWKJLK4U02LRQoNxmKagwYLxItAuls0KqduCErcqfC5pOcBJDtMl4Q==",
"requires": {
"@ionic/core": "8.7.4",
"@ionic/core": "8.7.5",
"ionicons": "^8.0.13",
"tslib": "*"
}
@@ -6844,4 +6844,4 @@
"dev": true
}
}
}
}

View File

@@ -736,9 +736,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.4.tgz",
"integrity": "sha512-ZCJYKLWdxq+x4OmEDvodqR+y/FSDJYkkFHozWe1+b/p0l9lNN13lLuSZVs0AEOgPtO89Atl67rTbpGE2ad/SCw==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz",
"integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.36.2",
@@ -11913,4 +11913,4 @@
}
}
}
}
}

View File

@@ -673,9 +673,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.4.tgz",
"integrity": "sha512-ZCJYKLWdxq+x4OmEDvodqR+y/FSDJYkkFHozWe1+b/p0l9lNN13lLuSZVs0AEOgPtO89Atl67rTbpGE2ad/SCw==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz",
"integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.36.2",
@@ -865,12 +865,12 @@
}
},
"node_modules/@ionic/vue": {
"version": "8.7.4",
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.4.tgz",
"integrity": "sha512-Gof5oHUfyCMBA5VvvtHaLmP4OX+on4nSxCrrDXCFFbgE3b9CXUJGSpBqPwzvrVxkpbPHfkbkgJXhoIWlls4zXA==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.5.tgz",
"integrity": "sha512-wx7o+ABDDTWLM47CIjxueoZtKbvMQ9AolqGY4/2JvAJds/JlSs4kOEes/AzQ/1dREEp+4sOapmTtJnyauErY3A==",
"license": "MIT",
"dependencies": {
"@ionic/core": "8.7.4",
"@ionic/core": "8.7.5",
"@stencil/vue-output-target": "0.10.7",
"ionicons": "^8.0.13"
}
@@ -8041,9 +8041,9 @@
"dev": true
},
"@ionic/core": {
"version": "8.7.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.4.tgz",
"integrity": "sha512-ZCJYKLWdxq+x4OmEDvodqR+y/FSDJYkkFHozWe1+b/p0l9lNN13lLuSZVs0AEOgPtO89Atl67rTbpGE2ad/SCw==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz",
"integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==",
"requires": {
"@stencil/core": "4.36.2",
"ionicons": "^8.0.13",
@@ -8156,11 +8156,11 @@
"requires": {}
},
"@ionic/vue": {
"version": "8.7.4",
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.4.tgz",
"integrity": "sha512-Gof5oHUfyCMBA5VvvtHaLmP4OX+on4nSxCrrDXCFFbgE3b9CXUJGSpBqPwzvrVxkpbPHfkbkgJXhoIWlls4zXA==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/vue/-/vue-8.7.5.tgz",
"integrity": "sha512-wx7o+ABDDTWLM47CIjxueoZtKbvMQ9AolqGY4/2JvAJds/JlSs4kOEes/AzQ/1dREEp+4sOapmTtJnyauErY3A==",
"requires": {
"@ionic/core": "8.7.4",
"@ionic/core": "8.7.5",
"@stencil/vue-output-target": "0.10.7",
"ionicons": "^8.0.13"
}
@@ -12991,4 +12991,4 @@
"dev": true
}
}
}
}

View File

@@ -222,9 +222,9 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.4.tgz",
"integrity": "sha512-ZCJYKLWdxq+x4OmEDvodqR+y/FSDJYkkFHozWe1+b/p0l9lNN13lLuSZVs0AEOgPtO89Atl67rTbpGE2ad/SCw==",
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.5.tgz",
"integrity": "sha512-Uk1qdGPoLHaVhd2FnYSAvRehd3VwwcPIfXaR51qiC7C2L5VhD27VyLSgDetc15G4U+VAIFjgUSR/pKdLFEuMPA==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.36.2",
@@ -4019,4 +4019,4 @@
"dev": true
}
}
}
}