diff --git a/.github/workflows/actions/build-angular-server/action.yml b/.github/workflows/actions/build-angular-server/action.yml index b530d300788..c48d1dcb3b6 100644 --- a/.github/workflows/actions/build-angular-server/action.yml +++ b/.github/workflows/actions/build-angular-server/action.yml @@ -3,7 +3,7 @@ description: 'Build Ionic Angular Server' runs: using: 'composite' steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/build-angular/action.yml b/.github/workflows/actions/build-angular/action.yml index 80da1c353d7..349c6734e43 100644 --- a/.github/workflows/actions/build-angular/action.yml +++ b/.github/workflows/actions/build-angular/action.yml @@ -3,7 +3,7 @@ description: 'Build Ionic Angular' runs: using: 'composite' steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/build-core-stencil-prerelease/action.yml b/.github/workflows/actions/build-core-stencil-prerelease/action.yml index 7611061b503..070f84c4c3e 100644 --- a/.github/workflows/actions/build-core-stencil-prerelease/action.yml +++ b/.github/workflows/actions/build-core-stencil-prerelease/action.yml @@ -9,7 +9,7 @@ runs: using: 'composite' steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x diff --git a/.github/workflows/actions/build-core/action.yml b/.github/workflows/actions/build-core/action.yml index 08d27a8d8b2..b0ec39decf8 100644 --- a/.github/workflows/actions/build-core/action.yml +++ b/.github/workflows/actions/build-core/action.yml @@ -9,7 +9,7 @@ runs: using: 'composite' steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x - name: Install Dependencies diff --git a/.github/workflows/actions/build-react-router/action.yml b/.github/workflows/actions/build-react-router/action.yml index 390378cb12d..61d5f6b2d45 100644 --- a/.github/workflows/actions/build-react-router/action.yml +++ b/.github/workflows/actions/build-react-router/action.yml @@ -3,7 +3,7 @@ description: 'Build Ionic React Router' runs: using: 'composite' steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/build-react/action.yml b/.github/workflows/actions/build-react/action.yml index 3ea565d62e3..6b8b9f74178 100644 --- a/.github/workflows/actions/build-react/action.yml +++ b/.github/workflows/actions/build-react/action.yml @@ -3,7 +3,7 @@ description: 'Build Ionic React' runs: using: 'composite' steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/build-vue-router/action.yml b/.github/workflows/actions/build-vue-router/action.yml index 623bdc4c7a1..e1c7716f5ea 100644 --- a/.github/workflows/actions/build-vue-router/action.yml +++ b/.github/workflows/actions/build-vue-router/action.yml @@ -3,7 +3,7 @@ description: 'Builds Ionic Vue Router' runs: using: 'composite' steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/build-vue/action.yml b/.github/workflows/actions/build-vue/action.yml index f2be91e1090..bc8a47facc2 100644 --- a/.github/workflows/actions/build-vue/action.yml +++ b/.github/workflows/actions/build-vue/action.yml @@ -3,7 +3,7 @@ description: 'Build Ionic Vue' runs: using: 'composite' steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/publish-npm/action.yml b/.github/workflows/actions/publish-npm/action.yml index 6bd557db3b9..5c5b49d56c6 100644 --- a/.github/workflows/actions/publish-npm/action.yml +++ b/.github/workflows/actions/publish-npm/action.yml @@ -19,7 +19,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x # Provenance requires npm 9.5.0+ diff --git a/.github/workflows/actions/test-angular-e2e/action.yml b/.github/workflows/actions/test-angular-e2e/action.yml index 705d6e27cf4..cd7ebfe0aec 100644 --- a/.github/workflows/actions/test-angular-e2e/action.yml +++ b/.github/workflows/actions/test-angular-e2e/action.yml @@ -6,7 +6,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/test-core-clean-build/action.yml b/.github/workflows/actions/test-core-clean-build/action.yml index d822e69468d..ea6da763fd9 100644 --- a/.github/workflows/actions/test-core-clean-build/action.yml +++ b/.github/workflows/actions/test-core-clean-build/action.yml @@ -3,7 +3,7 @@ description: 'Test Core Clean Build' runs: using: 'composite' steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x diff --git a/.github/workflows/actions/test-core-lint/action.yml b/.github/workflows/actions/test-core-lint/action.yml index a4298c2c0a1..b0e45abdaea 100644 --- a/.github/workflows/actions/test-core-lint/action.yml +++ b/.github/workflows/actions/test-core-lint/action.yml @@ -3,7 +3,7 @@ description: 'Test Core Lint' runs: using: 'composite' steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x - name: Install Dependencies diff --git a/.github/workflows/actions/test-core-screenshot/action.yml b/.github/workflows/actions/test-core-screenshot/action.yml index 588c310b462..f3d599f02ca 100644 --- a/.github/workflows/actions/test-core-screenshot/action.yml +++ b/.github/workflows/actions/test-core-screenshot/action.yml @@ -13,7 +13,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/test-core-spec/action.yml b/.github/workflows/actions/test-core-spec/action.yml index f6246664066..cdec48fabff 100644 --- a/.github/workflows/actions/test-core-spec/action.yml +++ b/.github/workflows/actions/test-core-spec/action.yml @@ -6,7 +6,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x - name: Install Dependencies diff --git a/.github/workflows/actions/test-react-e2e/action.yml b/.github/workflows/actions/test-react-e2e/action.yml index ab056ac667b..3cf40c29b86 100644 --- a/.github/workflows/actions/test-react-e2e/action.yml +++ b/.github/workflows/actions/test-react-e2e/action.yml @@ -6,7 +6,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/test-react-router-e2e/action.yml b/.github/workflows/actions/test-react-router-e2e/action.yml index cf71e4da5aa..f1f0150de11 100644 --- a/.github/workflows/actions/test-react-router-e2e/action.yml +++ b/.github/workflows/actions/test-react-router-e2e/action.yml @@ -6,7 +6,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/test-vue-e2e/action.yml b/.github/workflows/actions/test-vue-e2e/action.yml index 93a21db7855..905cb319a7f 100644 --- a/.github/workflows/actions/test-vue-e2e/action.yml +++ b/.github/workflows/actions/test-vue-e2e/action.yml @@ -6,7 +6,7 @@ inputs: runs: using: 'composite' steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x - uses: ./.github/workflows/actions/download-archive diff --git a/.github/workflows/actions/update-reference-screenshots/action.yml b/.github/workflows/actions/update-reference-screenshots/action.yml index d454b2e2a32..95d0c7b726b 100644 --- a/.github/workflows/actions/update-reference-screenshots/action.yml +++ b/.github/workflows/actions/update-reference-screenshots/action.yml @@ -7,7 +7,7 @@ on: runs: using: 'composite' steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 22.x - uses: actions/download-artifact@v5 diff --git a/.github/workflows/assign-issues.yml b/.github/workflows/assign-issues.yml index 4608d2323dd..d06c1f52e10 100644 --- a/.github/workflows/assign-issues.yml +++ b/.github/workflows/assign-issues.yml @@ -13,6 +13,6 @@ jobs: - name: 'Auto-assign issue' uses: pozil/auto-assign-issue@39c06395cbac76e79afc4ad4e5c5c6db6ecfdd2e # v2.2.0 with: - assignees: brandyscarney, ShaneK + assignees: brandyscarney, thetaPC, ShaneK numOfAssignee: 1 allowSelfAssign: false diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index a4e35060dfb..7f5d8de9787 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -13,7 +13,7 @@ jobs: triage: runs-on: ubuntu-latest steps: - - uses: actions/labeler@v5 + - uses: actions/labeler@v6 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" sync-labels: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bf71d444ba..47210c4bf27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) + + +### Bug Fixes + +* **modal:** allow sheet modals to skip focus trap ([#30689](https://github.com/ionic-team/ionic-framework/issues/30689)) ([a40d957](https://github.com/ionic-team/ionic-framework/commit/a40d957ad9c1897af365a91b45b00228a00d614c)), closes [#30684](https://github.com/ionic-team/ionic-framework/issues/30684) +* **vue:** emit component-specific overlay events ([#30688](https://github.com/ionic-team/ionic-framework/issues/30688)) ([024d090](https://github.com/ionic-team/ionic-framework/commit/024d090122548e26ec2cdcfae4637dde8f288278)), closes [#30641](https://github.com/ionic-team/ionic-framework/issues/30641) + + + + + ## [8.7.4](https://github.com/ionic-team/ionic-framework/compare/v8.7.3...v8.7.4) (2025-09-17) diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 8e9e8ba9851..5be7265795b 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) + + +### Bug Fixes + +* **modal:** allow sheet modals to skip focus trap ([#30689](https://github.com/ionic-team/ionic-framework/issues/30689)) ([a40d957](https://github.com/ionic-team/ionic-framework/commit/a40d957ad9c1897af365a91b45b00228a00d614c)), closes [#30684](https://github.com/ionic-team/ionic-framework/issues/30684) + + + + + ## [8.7.4](https://github.com/ionic-team/ionic-framework/compare/v8.7.3...v8.7.4) (2025-09-17) diff --git a/core/Dockerfile b/core/Dockerfile index 6ccd28e99ba..095cde63a4a 100644 --- a/core/Dockerfile +++ b/core/Dockerfile @@ -1,5 +1,5 @@ # Get Playwright -FROM mcr.microsoft.com/playwright:v1.55.0 +FROM mcr.microsoft.com/playwright:v1.55.1 # Set the working directory WORKDIR /ionic diff --git a/core/package-lock.json b/core/package-lock.json index fcdd42d8483..7ba1b6e38dc 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ionic/core", - "version": "8.7.4", + "version": "8.7.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/core", - "version": "8.7.4", + "version": "8.7.5", "license": "MIT", "dependencies": { "@phosphor-icons/core": "^2.1.1", @@ -23,7 +23,7 @@ "@clack/prompts": "^0.11.0", "@ionic/eslint-config": "^0.3.0", "@ionic/prettier-config": "^2.0.0", - "@playwright/test": "^1.55.0", + "@playwright/test": "^1.55.1", "@rollup/plugin-node-resolve": "^8.4.0", "@rollup/plugin-virtual": "^2.0.3", "@stencil/angular-output-target": "^0.10.0", @@ -2352,13 +2352,13 @@ "integrity": "sha512-v4ARvrip4qBCImOE5rmPUylOEK4iiED9ZyKjcvzuezqMaiRASCHKcRIuvvxL/twvLpkfnEODCOJp5dM4eZilxQ==" }, "node_modules/@playwright/test": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz", - "integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==", + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.1.tgz", + "integrity": "sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.55.0" + "playwright": "1.55.1" }, "bin": { "playwright": "cli.js" @@ -9904,13 +9904,13 @@ } }, "node_modules/playwright": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", - "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz", + "integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.55.0" + "playwright-core": "1.55.1" }, "bin": { "playwright": "cli.js" @@ -9923,9 +9923,9 @@ } }, "node_modules/playwright-core": { - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", - "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz", + "integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/core/package.json b/core/package.json index 8d636f002ec..7869748e203 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/core", - "version": "8.7.4", + "version": "8.7.5", "description": "Base components for Ionic", "keywords": [ "ionic", @@ -45,7 +45,7 @@ "@clack/prompts": "^0.11.0", "@ionic/eslint-config": "^0.3.0", "@ionic/prettier-config": "^2.0.0", - "@playwright/test": "^1.55.0", + "@playwright/test": "^1.55.1", "@rollup/plugin-node-resolve": "^8.4.0", "@rollup/plugin-virtual": "^2.0.3", "@stencil/angular-output-target": "^0.10.0", diff --git a/core/src/components/modal/gestures/sheet.ts b/core/src/components/modal/gestures/sheet.ts index d3dd93d133e..8840e3123be 100644 --- a/core/src/components/modal/gestures/sheet.ts +++ b/core/src/components/modal/gestures/sheet.ts @@ -96,6 +96,12 @@ export const createSheetGesture = ( const contentAnimation = animation.childAnimations.find((ani) => ani.id === 'contentAnimation'); const enableBackdrop = () => { + // 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) { + return; + } baseEl.style.setProperty('pointer-events', 'auto'); backdropEl.style.setProperty('pointer-events', 'auto'); @@ -236,7 +242,10 @@ 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; + const shouldEnableBackdrop = + currentBreakpoint > backdropBreakpoint && + (baseEl as HTMLIonModalElement & { focusTrap?: boolean }).focusTrap !== false && + (baseEl as HTMLIonModalElement & { showBackdrop?: boolean }).showBackdrop !== false; if (shouldEnableBackdrop) { enableBackdrop(); } else { @@ -584,7 +593,10 @@ export const createSheetGesture = ( * Backdrop should become enabled * after the backdropBreakpoint value */ - const shouldEnableBackdrop = currentBreakpoint > backdropBreakpoint; + const shouldEnableBackdrop = + currentBreakpoint > backdropBreakpoint && + (baseEl as HTMLIonModalElement & { focusTrap?: boolean }).focusTrap !== false && + (baseEl as HTMLIonModalElement & { showBackdrop?: boolean }).showBackdrop !== false; if (shouldEnableBackdrop) { enableBackdrop(); } else { diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index 7b8d35e7951..fce56657584 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -494,10 +494,8 @@ export const setRootAriaHidden = (hidden = false) => { if (hidden) { viewContainer.setAttribute('aria-hidden', 'true'); - viewContainer.setAttribute('inert', ''); } else { viewContainer.removeAttribute('aria-hidden'); - viewContainer.removeAttribute('inert'); } }; @@ -529,15 +527,37 @@ export const present = async ( * focus traps. * * All other overlays should have focus traps to prevent - * the keyboard focus from leaving the overlay. + * the keyboard focus from leaving the overlay unless + * developers explicitly opt out (for example, sheet + * modals that should permit background interaction). + * + * Note: Some apps move inline overlays to a specific container + * during the willPresent lifecycle (e.g., React portals via + * onWillPresent). Defer applying aria-hidden/inert to the app + * root until after willPresent so we can detect where the + * overlay is finally inserted. If the overlay is inside the + * view container subtree, skip adding aria-hidden/inert there + * to avoid disabling the overlay. */ - if (overlay.el.tagName !== 'ION-TOAST') { - setRootAriaHidden(true); - document.body.classList.add(BACKDROP_NO_SCROLL); - } + const overlayEl = overlay.el as HTMLIonOverlayElement & { focusTrap?: boolean; showBackdrop?: boolean }; + const shouldTrapFocus = overlayEl.tagName !== 'ION-TOAST' && overlayEl.focusTrap !== false; + // 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; overlay.presented = true; overlay.willPresent.emit(); + + if (shouldLockRoot) { + const root = getAppRoot(document); + const viewContainer = root.querySelector('ion-router-outlet, #ion-view-container-root'); + const overlayInsideViewContainer = viewContainer ? viewContainer.contains(overlayEl) : false; + + if (!overlayInsideViewContainer) { + setRootAriaHidden(true); + } + document.body.classList.add(BACKDROP_NO_SCROLL); + } overlay.willPresentShorthand?.emit(); const mode = getIonMode(overlay); @@ -653,22 +673,28 @@ export const dismiss = async ( * For accessibility, toasts lack focus traps and don't receive * `aria-hidden` on the root element when presented. * - * All other overlays use focus traps to keep keyboard focus - * within the overlay, setting `aria-hidden` on the root element - * to enhance accessibility. - * - * Therefore, we must remove `aria-hidden` from the root element - * when the last non-toast overlay is dismissed. + * Overlays that opt into focus trapping set `aria-hidden` + * on the root element to keep keyboard focus and pointer + * events inside the overlay. We must remove `aria-hidden` + * from the root element when the last focus-trapping overlay + * is dismissed. */ - const overlaysNotToast = presentedOverlays.filter((o) => o.tagName !== 'ION-TOAST'); - - const lastOverlayNotToast = overlaysNotToast.length === 1 && overlaysNotToast[0].id === overlay.el.id; + 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 overlayEl = overlay.el as HTMLIonOverlayElement & { focusTrap?: boolean; showBackdrop?: boolean }; + const locksRoot = + overlayEl.tagName !== 'ION-TOAST' && overlayEl.focusTrap !== false && overlayEl.showBackdrop !== false; /** - * If this is the last visible overlay that is not a toast + * If this is the last visible overlay that is trapping focus * then we want to re-add the root to the accessibility tree. */ - if (lastOverlayNotToast) { + const lastOverlayTrappingFocus = + locksRoot && overlaysLockingRoot.length === 1 && overlaysLockingRoot[0].id === overlayEl.id; + + if (lastOverlayTrappingFocus) { setRootAriaHidden(false); document.body.classList.remove(BACKDROP_NO_SCROLL); } diff --git a/lerna.json b/lerna.json index 29cdc617c16..283bc5a42ef 100644 --- a/lerna.json +++ b/lerna.json @@ -3,5 +3,5 @@ "core", "packages/*" ], - "version": "8.7.4" + "version": "8.7.5" } \ No newline at end of file diff --git a/packages/angular-server/CHANGELOG.md b/packages/angular-server/CHANGELOG.md index a8b9fd6f838..2286ed5513c 100644 --- a/packages/angular-server/CHANGELOG.md +++ b/packages/angular-server/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) + +**Note:** Version bump only for package @ionic/angular-server + + + + + ## [8.7.4](https://github.com/ionic-team/ionic-framework/compare/v8.7.3...v8.7.4) (2025-09-17) **Note:** Version bump only for package @ionic/angular-server diff --git a/packages/angular-server/package-lock.json b/packages/angular-server/package-lock.json index 05662502038..58d98508f35 100644 --- a/packages/angular-server/package-lock.json +++ b/packages/angular-server/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/angular-server", - "version": "8.7.4", + "version": "8.7.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/angular-server", - "version": "8.7.4", + "version": "8.7.5", "license": "MIT", "dependencies": { - "@ionic/core": "^8.7.4" + "@ionic/core": "^8.7.5" }, "devDependencies": { "@angular-eslint/eslint-plugin": "^16.0.0", @@ -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", diff --git a/packages/angular-server/package.json b/packages/angular-server/package.json index 5fd65a465ff..f17037d02ba 100644 --- a/packages/angular-server/package.json +++ b/packages/angular-server/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/angular-server", - "version": "8.7.4", + "version": "8.7.5", "description": "Angular SSR Module for Ionic", "keywords": [ "ionic", @@ -62,6 +62,6 @@ }, "prettier": "@ionic/prettier-config", "dependencies": { - "@ionic/core": "^8.7.4" + "@ionic/core": "^8.7.5" } } diff --git a/packages/angular/CHANGELOG.md b/packages/angular/CHANGELOG.md index b5df3e3df87..79e9b64e6d3 100644 --- a/packages/angular/CHANGELOG.md +++ b/packages/angular/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) + + +### Bug Fixes + +* **modal:** allow sheet modals to skip focus trap ([#30689](https://github.com/ionic-team/ionic-framework/issues/30689)) ([a40d957](https://github.com/ionic-team/ionic-framework/commit/a40d957ad9c1897af365a91b45b00228a00d614c)), closes [#30684](https://github.com/ionic-team/ionic-framework/issues/30684) + + + + + ## [8.7.4](https://github.com/ionic-team/ionic-framework/compare/v8.7.3...v8.7.4) (2025-09-17) diff --git a/packages/angular/package-lock.json b/packages/angular/package-lock.json index 31ceaa1a298..e840282dae0 100644 --- a/packages/angular/package-lock.json +++ b/packages/angular/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/angular", - "version": "8.7.4", + "version": "8.7.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/angular", - "version": "8.7.4", + "version": "8.7.5", "license": "MIT", "dependencies": { - "@ionic/core": "^8.7.4", + "@ionic/core": "^8.7.5", "ionicons": "^8.0.13", "jsonc-parser": "^3.0.0", "tslib": "^2.3.0" @@ -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", diff --git a/packages/angular/package.json b/packages/angular/package.json index fc9a71b4e31..bfdef31fd6e 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/angular", - "version": "8.7.4", + "version": "8.7.5", "description": "Angular specific wrappers for @ionic/core", "keywords": [ "ionic", @@ -48,7 +48,7 @@ } }, "dependencies": { - "@ionic/core": "^8.7.4", + "@ionic/core": "^8.7.5", "ionicons": "^8.0.13", "jsonc-parser": "^3.0.0", "tslib": "^2.3.0" diff --git a/packages/angular/test/base/e2e/src/lazy/modal-dynamic-wrapper.spec.ts b/packages/angular/test/base/e2e/src/lazy/modal-dynamic-wrapper.spec.ts new file mode 100644 index 00000000000..b76f16015bf --- /dev/null +++ b/packages/angular/test/base/e2e/src/lazy/modal-dynamic-wrapper.spec.ts @@ -0,0 +1,38 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Modals: Dynamic Wrapper', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/lazy/modal-dynamic-wrapper'); + }); + + test('should render dynamic component inside modal', async ({ page }) => { + await page.locator('#open-dynamic-modal').click(); + + await expect(page.locator('ion-modal')).toBeVisible(); + await expect(page.locator('#dynamic-component-loaded')).toBeVisible(); + }); + + test('should allow interacting with background content while sheet is open', async ({ page }) => { + await page.locator('#open-dynamic-modal').click(); + + await expect(page.locator('ion-modal')).toBeVisible(); + + await page.locator('#background-action').click(); + + await expect(page.locator('#background-action-count')).toHaveText('1'); + }); + + test('should prevent interacting with background content when focus is trapped', async ({ page }) => { + await page.locator('#open-focused-modal').click(); + + await expect(page.locator('ion-modal')).toBeVisible(); + + // Attempt to click the background button via coordinates; click should be intercepted by backdrop + const box = await page.locator('#background-action').boundingBox(); + if (box) { + await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2); + } + + await expect(page.locator('#background-action-count')).toHaveText('0'); + }); +}); diff --git a/packages/angular/test/base/e2e/src/lazy/modal-sheet-inline.spec.ts b/packages/angular/test/base/e2e/src/lazy/modal-sheet-inline.spec.ts new file mode 100644 index 00000000000..0e23c057d0a --- /dev/null +++ b/packages/angular/test/base/e2e/src/lazy/modal-sheet-inline.spec.ts @@ -0,0 +1,34 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Modals: Inline Sheet', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/lazy/modal-sheet-inline'); + }); + + test('should open inline sheet modal', async ({ page }) => { + await page.locator('#present-inline-sheet-modal').click(); + + await expect(page.locator('ion-modal')).toBeVisible(); + await expect(page.locator('#current-breakpoint')).toHaveText('0.2'); + await expect(page.locator('ion-modal ion-item')).toHaveCount(4); + }); + + test('should expand to 0.75 breakpoint when searchbar is clicked', async ({ page }) => { + await page.locator('#present-inline-sheet-modal').click(); + await expect(page.locator('#current-breakpoint')).toHaveText('0.2'); + + await page.locator('ion-modal ion-searchbar').click(); + + await expect(page.locator('#current-breakpoint')).toHaveText('0.75'); + }); + + test('should allow interacting with background content while sheet is open', async ({ page }) => { + await page.locator('#present-inline-sheet-modal').click(); + + await expect(page.locator('ion-modal')).toBeVisible(); + + await page.locator('#background-action').click(); + + await expect(page.locator('#background-action-count')).toHaveText('1'); + }); +}); diff --git a/packages/angular/test/base/e2e/src/standalone/modal-dynamic-wrapper.spec.ts b/packages/angular/test/base/e2e/src/standalone/modal-dynamic-wrapper.spec.ts new file mode 100644 index 00000000000..6f3e0c6a03f --- /dev/null +++ b/packages/angular/test/base/e2e/src/standalone/modal-dynamic-wrapper.spec.ts @@ -0,0 +1,38 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Modals: Dynamic Wrapper (standalone)', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/standalone/modal-dynamic-wrapper'); + }); + + test('should render dynamic component inside modal', async ({ page }) => { + await page.locator('#open-dynamic-modal').click(); + + await expect(page.locator('ion-modal')).toBeVisible(); + await expect(page.locator('#dynamic-component-loaded')).toBeVisible(); + }); + + test('should allow interacting with background content while sheet is open', async ({ page }) => { + await page.locator('#open-dynamic-modal').click(); + + await expect(page.locator('ion-modal')).toBeVisible(); + + await page.locator('#background-action').click(); + + await expect(page.locator('#background-action-count')).toHaveText('1'); + }); + + test('should prevent interacting with background content when focus is trapped', async ({ page }) => { + await page.locator('#open-focused-modal').click(); + + await expect(page.locator('ion-modal')).toBeVisible(); + + // Attempt to click the background button via coordinates; click should be intercepted by backdrop + const box = await page.locator('#background-action').boundingBox(); + if (box) { + await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2); + } + + await expect(page.locator('#background-action-count')).toHaveText('0'); + }); +}); diff --git a/packages/angular/test/base/e2e/src/standalone/modal-sheet-inline.spec.ts b/packages/angular/test/base/e2e/src/standalone/modal-sheet-inline.spec.ts new file mode 100644 index 00000000000..3bebd5173cd --- /dev/null +++ b/packages/angular/test/base/e2e/src/standalone/modal-sheet-inline.spec.ts @@ -0,0 +1,34 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Modals: Inline Sheet (standalone)', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/standalone/modal-sheet-inline'); + }); + + test('should open inline sheet modal', async ({ page }) => { + await page.locator('#present-inline-sheet-modal').click(); + + await expect(page.locator('ion-modal')).toBeVisible(); + await expect(page.locator('#current-breakpoint')).toHaveText('0.2'); + await expect(page.locator('ion-modal ion-item')).toHaveCount(4); + }); + + test('should expand to 0.75 breakpoint when searchbar is clicked', async ({ page }) => { + await page.locator('#present-inline-sheet-modal').click(); + await expect(page.locator('#current-breakpoint')).toHaveText('0.2'); + + await page.locator('ion-modal ion-searchbar').click(); + + await expect(page.locator('#current-breakpoint')).toHaveText('0.75'); + }); + + test('should allow interacting with background content while sheet is open', async ({ page }) => { + await page.locator('#present-inline-sheet-modal').click(); + + await expect(page.locator('ion-modal')).toBeVisible(); + + await page.locator('#background-action').click(); + + await expect(page.locator('#background-action-count')).toHaveText('1'); + }); +}); diff --git a/packages/angular/test/base/src/app/lazy/app-lazy/app.routes.ts b/packages/angular/test/base/src/app/lazy/app-lazy/app.routes.ts index 1a46992f92c..35a77b19cf1 100644 --- a/packages/angular/test/base/src/app/lazy/app-lazy/app.routes.ts +++ b/packages/angular/test/base/src/app/lazy/app-lazy/app.routes.ts @@ -37,6 +37,8 @@ export const routes: Routes = [ { path: 'template-form', component: TemplateFormComponent }, { path: 'modals', component: ModalComponent }, { path: 'modal-inline', loadChildren: () => import('../modal-inline').then(m => m.ModalInlineModule) }, + { path: 'modal-sheet-inline', loadChildren: () => import('../modal-sheet-inline').then(m => m.ModalSheetInlineModule) }, + { path: 'modal-dynamic-wrapper', loadChildren: () => import('../modal-dynamic-wrapper').then(m => m.ModalDynamicWrapperModule) }, { path: 'view-child', component: ViewChildComponent }, { path: 'keep-contents-mounted', loadChildren: () => import('../keep-contents-mounted').then(m => m.OverlayAutoMountModule) }, { path: 'overlays-inline', loadChildren: () => import('../overlays-inline').then(m => m.OverlaysInlineModule) }, @@ -90,4 +92,3 @@ export const routes: Routes = [ ] }, ]; - diff --git a/packages/angular/test/base/src/app/lazy/home-page/home-page.component.html b/packages/angular/test/base/src/app/lazy/home-page/home-page.component.html index 136a0119d34..c3ac2659151 100644 --- a/packages/angular/test/base/src/app/lazy/home-page/home-page.component.html +++ b/packages/angular/test/base/src/app/lazy/home-page/home-page.component.html @@ -35,6 +35,16 @@ Modals Test + + + Modal Sheet Inline Test + + + + + Modal Dynamic Wrapper Test + + Router link Test diff --git a/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/dynamic-component-wrapper.component.ts b/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/dynamic-component-wrapper.component.ts new file mode 100644 index 00000000000..f4304c03a50 --- /dev/null +++ b/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/dynamic-component-wrapper.component.ts @@ -0,0 +1,26 @@ +import { Component, ComponentRef, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; + +@Component({ + selector: 'app-dynamic-component-wrapper', + template: ` + + + + `, + standalone: false +}) +export class DynamicComponentWrapperComponent implements OnInit, OnDestroy { + + @Input() componentRef?: ComponentRef; + @ViewChild('container', { read: ViewContainerRef, static: true }) container!: ViewContainerRef; + + ngOnInit(): void { + if (this.componentRef) { + this.container.insert(this.componentRef.hostView); + } + } + + ngOnDestroy(): void { + this.componentRef?.destroy(); + } +} diff --git a/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/dynamic-modal-content.component.ts b/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/dynamic-modal-content.component.ts new file mode 100644 index 00000000000..ad1b76c05a7 --- /dev/null +++ b/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/dynamic-modal-content.component.ts @@ -0,0 +1,20 @@ +import { Component, EventEmitter, Output } from "@angular/core"; + +@Component({ + selector: 'app-dynamic-modal-content', + template: ` + + + Dynamic Sheet Content + + + +

Dynamic component rendered inside wrapper.

+ Close +
+ `, + standalone: false +}) +export class DynamicModalContentComponent { + @Output() dismiss = new EventEmitter(); +} diff --git a/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/index.ts b/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/index.ts new file mode 100644 index 00000000000..ca0da1dfbe7 --- /dev/null +++ b/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/index.ts @@ -0,0 +1,2 @@ +export * from './modal-dynamic-wrapper.component'; +export * from './modal-dynamic-wrapper.module'; diff --git a/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/modal-dynamic-wrapper-routing.module.ts b/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/modal-dynamic-wrapper-routing.module.ts new file mode 100644 index 00000000000..a8ef812c715 --- /dev/null +++ b/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/modal-dynamic-wrapper-routing.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from "@angular/core"; +import { RouterModule } from "@angular/router"; +import { ModalDynamicWrapperComponent } from "."; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: '', + component: ModalDynamicWrapperComponent + } + ]) + ], + exports: [RouterModule] +}) +export class ModalDynamicWrapperRoutingModule { } diff --git a/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/modal-dynamic-wrapper.component.html b/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/modal-dynamic-wrapper.component.html new file mode 100644 index 00000000000..c3655c56252 --- /dev/null +++ b/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/modal-dynamic-wrapper.component.html @@ -0,0 +1,8 @@ +Open Dynamic Sheet Modal +Open Focus-Trapped Sheet Modal +Background Action +

+ Background action count: {{ backgroundActionCount }} +

+ + diff --git a/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/modal-dynamic-wrapper.component.ts b/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/modal-dynamic-wrapper.component.ts new file mode 100644 index 00000000000..dd05d3c6f10 --- /dev/null +++ b/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/modal-dynamic-wrapper.component.ts @@ -0,0 +1,104 @@ +import { Component, ComponentRef, OnDestroy, ViewChild, ViewContainerRef } from "@angular/core"; +import { ModalController } from "@ionic/angular"; +import { DynamicComponentWrapperComponent } from "./dynamic-component-wrapper.component"; +import { DynamicModalContentComponent } from "./dynamic-modal-content.component"; + +@Component({ + selector: 'app-modal-dynamic-wrapper', + templateUrl: './modal-dynamic-wrapper.component.html', + standalone: false +}) +export class ModalDynamicWrapperComponent implements OnDestroy { + + @ViewChild('modalHost', { read: ViewContainerRef, static: true }) modalHost!: ViewContainerRef; + + backgroundActionCount = 0; + + private currentModal?: HTMLIonModalElement; + private currentComponentRef?: ComponentRef; + + constructor(private modalCtrl: ModalController) {} + + async openModal() { + await this.closeModal(); + + const componentRef = this.modalHost.createComponent(DynamicModalContentComponent); + this.modalHost.detach(); + componentRef.instance.dismiss.subscribe(() => this.closeModal()); + + this.currentComponentRef = componentRef; + + const modal = await this.modalCtrl.create({ + component: DynamicComponentWrapperComponent, + componentProps: { + componentRef + }, + breakpoints: [0, 0.2, 0.75, 1], + initialBreakpoint: 0.2, + backdropDismiss: false, + focusTrap: false, + handleBehavior: 'cycle' + }); + + this.currentModal = modal; + + modal.onWillDismiss().then(() => this.destroyComponent()); + + await modal.present(); + } + + async openFocusedModal() { + await this.closeModal(); + + const componentRef = this.modalHost.createComponent(DynamicModalContentComponent); + this.modalHost.detach(); + componentRef.instance.dismiss.subscribe(() => this.closeModal()); + + this.currentComponentRef = componentRef; + + const modal = await this.modalCtrl.create({ + component: DynamicComponentWrapperComponent, + componentProps: { + componentRef, + }, + // Choose a higher initial breakpoint to ensure backdrop is active immediately + breakpoints: [0, 0.25, 0.5, 0.75, 1], + initialBreakpoint: 0.5, + // Keep backdrop active but do not dismiss on tap to avoid interfering with assertions + backdropDismiss: false, + // Explicitly enable focus trapping to block background interaction + focusTrap: true, + handleBehavior: 'cycle', + }); + + this.currentModal = modal; + + modal.onWillDismiss().then(() => this.destroyComponent()); + + await modal.present(); + } + + async closeModal() { + if (this.currentModal) { + await this.currentModal.dismiss(); + this.currentModal = undefined; + } + + this.destroyComponent(); + } + + private destroyComponent() { + if (this.currentComponentRef) { + this.currentComponentRef.destroy(); + this.currentComponentRef = undefined; + } + } + + onBackgroundActionClick() { + this.backgroundActionCount++; + } + + ngOnDestroy(): void { + this.destroyComponent(); + } +} diff --git a/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/modal-dynamic-wrapper.module.ts b/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/modal-dynamic-wrapper.module.ts new file mode 100644 index 00000000000..2ab22c5aca3 --- /dev/null +++ b/packages/angular/test/base/src/app/lazy/modal-dynamic-wrapper/modal-dynamic-wrapper.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { IonicModule } from "@ionic/angular"; +import { DynamicComponentWrapperComponent } from "./dynamic-component-wrapper.component"; +import { DynamicModalContentComponent } from "./dynamic-modal-content.component"; +import { ModalDynamicWrapperRoutingModule } from "./modal-dynamic-wrapper-routing.module"; +import { ModalDynamicWrapperComponent } from "./modal-dynamic-wrapper.component"; + +@NgModule({ + imports: [CommonModule, IonicModule, ModalDynamicWrapperRoutingModule], + declarations: [ModalDynamicWrapperComponent, DynamicComponentWrapperComponent, DynamicModalContentComponent], + exports: [ModalDynamicWrapperComponent] +}) +export class ModalDynamicWrapperModule { } diff --git a/packages/angular/test/base/src/app/lazy/modal-sheet-inline/index.ts b/packages/angular/test/base/src/app/lazy/modal-sheet-inline/index.ts new file mode 100644 index 00000000000..0283c2cd952 --- /dev/null +++ b/packages/angular/test/base/src/app/lazy/modal-sheet-inline/index.ts @@ -0,0 +1,2 @@ +export * from './modal-sheet-inline.component'; +export * from './modal-sheet-inline.module'; diff --git a/packages/angular/test/base/src/app/lazy/modal-sheet-inline/modal-sheet-inline-routing.module.ts b/packages/angular/test/base/src/app/lazy/modal-sheet-inline/modal-sheet-inline-routing.module.ts new file mode 100644 index 00000000000..54c208d7022 --- /dev/null +++ b/packages/angular/test/base/src/app/lazy/modal-sheet-inline/modal-sheet-inline-routing.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from "@angular/core"; +import { RouterModule } from "@angular/router"; +import { ModalSheetInlineComponent } from "."; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: '', + component: ModalSheetInlineComponent + } + ]) + ], + exports: [RouterModule] +}) +export class ModalSheetInlineRoutingModule { } diff --git a/packages/angular/test/base/src/app/lazy/modal-sheet-inline/modal-sheet-inline.component.html b/packages/angular/test/base/src/app/lazy/modal-sheet-inline/modal-sheet-inline.component.html new file mode 100644 index 00000000000..6224ed10919 --- /dev/null +++ b/packages/angular/test/base/src/app/lazy/modal-sheet-inline/modal-sheet-inline.component.html @@ -0,0 +1,46 @@ + + Present Inline Sheet Modal + + +

+ Current breakpoint: {{ currentBreakpoint }} +

+ + + Background Action + + +

+ Background action count: {{ backgroundActionCount }} +

+ + + + + + + + + + + +

{{ contact.name }}

+

{{ contact.title }}

+
+
+
+
+
+
diff --git a/packages/angular/test/base/src/app/lazy/modal-sheet-inline/modal-sheet-inline.component.ts b/packages/angular/test/base/src/app/lazy/modal-sheet-inline/modal-sheet-inline.component.ts new file mode 100644 index 00000000000..2e0dc32e089 --- /dev/null +++ b/packages/angular/test/base/src/app/lazy/modal-sheet-inline/modal-sheet-inline.component.ts @@ -0,0 +1,79 @@ +import { Component, ViewChild } from "@angular/core"; +import { IonModal } from "@ionic/angular"; + +interface Contact { + name: string; + title: string; + avatar: string; +} + +@Component({ + selector: 'app-modal-sheet-inline', + templateUrl: './modal-sheet-inline.component.html', + standalone: false +}) +export class ModalSheetInlineComponent { + + @ViewChild('inlineSheetModal', { read: IonModal }) inlineSheetModal?: IonModal; + + readonly breakpoints: number[] = [0, 0.2, 0.75, 1]; + + readonly contacts: Contact[] = [ + { + name: 'Connor Smith', + title: 'Sales Rep', + avatar: 'https://i.pravatar.cc/300?u=b' + }, + { + name: 'Daniel Smith', + title: 'Product Designer', + avatar: 'https://i.pravatar.cc/300?u=a' + }, + { + name: 'Greg Smith', + title: 'Director of Operations', + avatar: 'https://i.pravatar.cc/300?u=d' + }, + { + name: 'Zoey Smith', + title: 'CEO', + avatar: 'https://i.pravatar.cc/300?u=e' + } + ]; + + isSheetOpen = false; + + currentBreakpoint = 'closed'; + + backgroundActionCount = 0; + + presentInlineSheetModal() { + this.isSheetOpen = true; + this.currentBreakpoint = '0.2'; + } + + async expandInlineSheet() { + const modal = this.inlineSheetModal; + + if (!modal) { + return; + } + + await modal.setCurrentBreakpoint(0.75); + this.currentBreakpoint = '0.75'; + } + + onSheetDidDismiss() { + this.isSheetOpen = false; + this.currentBreakpoint = 'closed'; + } + + onSheetBreakpointDidChange(event: CustomEvent<{ breakpoint: number }>) { + this.currentBreakpoint = event.detail.breakpoint.toString(); + } + + onBackgroundActionClick() { + this.backgroundActionCount++; + } + +} diff --git a/packages/angular/test/base/src/app/lazy/modal-sheet-inline/modal-sheet-inline.module.ts b/packages/angular/test/base/src/app/lazy/modal-sheet-inline/modal-sheet-inline.module.ts new file mode 100644 index 00000000000..9dd75b0a53b --- /dev/null +++ b/packages/angular/test/base/src/app/lazy/modal-sheet-inline/modal-sheet-inline.module.ts @@ -0,0 +1,12 @@ +import { CommonModule } from "@angular/common"; +import { NgModule } from "@angular/core"; +import { IonicModule } from "@ionic/angular"; +import { ModalSheetInlineRoutingModule } from "./modal-sheet-inline-routing.module"; +import { ModalSheetInlineComponent } from "./modal-sheet-inline.component"; + +@NgModule({ + imports: [CommonModule, IonicModule, ModalSheetInlineRoutingModule], + declarations: [ModalSheetInlineComponent], + exports: [ModalSheetInlineComponent] +}) +export class ModalSheetInlineModule { } diff --git a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts index fafb69c62ad..ed9628ae7c9 100644 --- a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts +++ b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts @@ -11,6 +11,8 @@ export const routes: Routes = [ { path: 'action-sheet-controller', loadComponent: () => import('../action-sheet-controller/action-sheet-controller.component').then(c => c.ActionSheetControllerComponent) }, { path: 'popover', loadComponent: () => import('../popover/popover.component').then(c => c.PopoverComponent) }, { path: 'modal', loadComponent: () => import('../modal/modal.component').then(c => c.ModalComponent) }, + { path: 'modal-sheet-inline', loadComponent: () => import('../modal-sheet-inline/modal-sheet-inline.component').then(c => c.ModalSheetInlineComponent) }, + { path: 'modal-dynamic-wrapper', loadComponent: () => import('../modal-dynamic-wrapper/modal-dynamic-wrapper.component').then(c => c.ModalDynamicWrapperComponent) }, { path: 'programmatic-modal', loadComponent: () => import('../programmatic-modal/programmatic-modal.component').then(c => c.ProgrammaticModalComponent) }, { path: 'router-outlet', loadComponent: () => import('../router-outlet/router-outlet.component').then(c => c.RouterOutletComponent) }, { path: 'back-button', loadComponent: () => import('../back-button/back-button.component').then(c => c.BackButtonComponent) }, diff --git a/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html b/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html index 7900bdfb64e..fd6ae409a3b 100644 --- a/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html +++ b/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html @@ -90,6 +90,16 @@ Modal Test
+ + + Modal Sheet Inline Test + + + + + Modal Dynamic Wrapper Test + + Programmatic Modal Test diff --git a/packages/angular/test/base/src/app/standalone/modal-dynamic-wrapper/dynamic-component-wrapper.component.ts b/packages/angular/test/base/src/app/standalone/modal-dynamic-wrapper/dynamic-component-wrapper.component.ts new file mode 100644 index 00000000000..d79ec4abff8 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/modal-dynamic-wrapper/dynamic-component-wrapper.component.ts @@ -0,0 +1,27 @@ +import { Component, ComponentRef, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { IonContent } from '@ionic/angular/standalone'; + +@Component({ + selector: 'app-dynamic-component-wrapper', + template: ` + + + + `, + standalone: true, + imports: [IonContent], +}) +export class DynamicComponentWrapperComponent implements OnInit, OnDestroy { + @Input() componentRef?: ComponentRef; + @ViewChild('container', { read: ViewContainerRef, static: true }) container!: ViewContainerRef; + + ngOnInit(): void { + if (this.componentRef) { + this.container.insert(this.componentRef.hostView); + } + } + + ngOnDestroy(): void { + this.componentRef?.destroy(); + } +} diff --git a/packages/angular/test/base/src/app/standalone/modal-dynamic-wrapper/dynamic-modal-content.component.ts b/packages/angular/test/base/src/app/standalone/modal-dynamic-wrapper/dynamic-modal-content.component.ts new file mode 100644 index 00000000000..4022f65b5ee --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/modal-dynamic-wrapper/dynamic-modal-content.component.ts @@ -0,0 +1,28 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { + IonButton, + IonContent, + IonHeader, + IonTitle, + IonToolbar, +} from '@ionic/angular/standalone'; + +@Component({ + selector: 'app-dynamic-modal-content', + template: ` + + + Dynamic Sheet Content + + + +

Dynamic component rendered inside wrapper.

+ Close +
+ `, + standalone: true, + imports: [IonButton, IonContent, IonHeader, IonTitle, IonToolbar], +}) +export class DynamicModalContentComponent { + @Output() dismiss = new EventEmitter(); +} diff --git a/packages/angular/test/base/src/app/standalone/modal-dynamic-wrapper/modal-dynamic-wrapper.component.html b/packages/angular/test/base/src/app/standalone/modal-dynamic-wrapper/modal-dynamic-wrapper.component.html new file mode 100644 index 00000000000..c3655c56252 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/modal-dynamic-wrapper/modal-dynamic-wrapper.component.html @@ -0,0 +1,8 @@ +Open Dynamic Sheet Modal +Open Focus-Trapped Sheet Modal +Background Action +

+ Background action count: {{ backgroundActionCount }} +

+ + diff --git a/packages/angular/test/base/src/app/standalone/modal-dynamic-wrapper/modal-dynamic-wrapper.component.ts b/packages/angular/test/base/src/app/standalone/modal-dynamic-wrapper/modal-dynamic-wrapper.component.ts new file mode 100644 index 00000000000..a88f18ef3e2 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/modal-dynamic-wrapper/modal-dynamic-wrapper.component.ts @@ -0,0 +1,103 @@ +import { CommonModule } from '@angular/common'; +import { Component, ComponentRef, OnDestroy, ViewChild, ViewContainerRef } from '@angular/core'; +import { IonButton, ModalController } from '@ionic/angular/standalone'; + +import { DynamicComponentWrapperComponent } from './dynamic-component-wrapper.component'; +import { DynamicModalContentComponent } from './dynamic-modal-content.component'; + +@Component({ + selector: 'app-modal-dynamic-wrapper', + templateUrl: './modal-dynamic-wrapper.component.html', + standalone: true, + imports: [CommonModule, IonButton], +}) +export class ModalDynamicWrapperComponent implements OnDestroy { + @ViewChild('modalHost', { read: ViewContainerRef, static: true }) modalHost!: ViewContainerRef; + + backgroundActionCount = 0; + + private currentModal?: HTMLIonModalElement; + private currentComponentRef?: ComponentRef; + + constructor(private modalCtrl: ModalController) {} + + async openModal() { + await this.closeModal(); + + const componentRef = this.modalHost.createComponent(DynamicModalContentComponent); + this.modalHost.detach(); + componentRef.instance.dismiss.subscribe(() => this.closeModal()); + + this.currentComponentRef = componentRef; + + const modal = await this.modalCtrl.create({ + component: DynamicComponentWrapperComponent, + componentProps: { + componentRef, + }, + breakpoints: [0, 0.2, 0.75, 1], + initialBreakpoint: 0.2, + backdropDismiss: false, + focusTrap: false, + handleBehavior: 'cycle', + }); + + this.currentModal = modal; + + modal.onWillDismiss().then(() => this.destroyComponent()); + + await modal.present(); + } + + async openFocusedModal() { + await this.closeModal(); + + const componentRef = this.modalHost.createComponent(DynamicModalContentComponent); + this.modalHost.detach(); + componentRef.instance.dismiss.subscribe(() => this.closeModal()); + + this.currentComponentRef = componentRef; + + const modal = await this.modalCtrl.create({ + component: DynamicComponentWrapperComponent, + componentProps: { + componentRef, + }, + breakpoints: [0, 0.25, 0.5, 0.75, 1], + initialBreakpoint: 0.5, + backdropDismiss: false, + focusTrap: true, + handleBehavior: 'cycle', + }); + + this.currentModal = modal; + + modal.onWillDismiss().then(() => this.destroyComponent()); + + await modal.present(); + } + + async closeModal() { + if (this.currentModal) { + await this.currentModal.dismiss(); + this.currentModal = undefined; + } + + this.destroyComponent(); + } + + private destroyComponent() { + if (this.currentComponentRef) { + this.currentComponentRef.destroy(); + this.currentComponentRef = undefined; + } + } + + onBackgroundActionClick() { + this.backgroundActionCount++; + } + + ngOnDestroy(): void { + this.destroyComponent(); + } +} diff --git a/packages/angular/test/base/src/app/standalone/modal-sheet-inline/modal-sheet-inline.component.html b/packages/angular/test/base/src/app/standalone/modal-sheet-inline/modal-sheet-inline.component.html new file mode 100644 index 00000000000..6224ed10919 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/modal-sheet-inline/modal-sheet-inline.component.html @@ -0,0 +1,46 @@ + + Present Inline Sheet Modal + + +

+ Current breakpoint: {{ currentBreakpoint }} +

+ + + Background Action + + +

+ Background action count: {{ backgroundActionCount }} +

+ + + + + + + + + + + +

{{ contact.name }}

+

{{ contact.title }}

+
+
+
+
+
+
diff --git a/packages/angular/test/base/src/app/standalone/modal-sheet-inline/modal-sheet-inline.component.ts b/packages/angular/test/base/src/app/standalone/modal-sheet-inline/modal-sheet-inline.component.ts new file mode 100644 index 00000000000..2cdee8df097 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/modal-sheet-inline/modal-sheet-inline.component.ts @@ -0,0 +1,100 @@ +import { CommonModule } from '@angular/common'; +import { Component, ViewChild } from '@angular/core'; +import { + IonAvatar, + IonButton, + IonContent, + IonImg, + IonItem, + IonLabel, + IonList, + IonModal, + IonSearchbar, +} from '@ionic/angular/standalone'; + +interface Contact { + name: string; + title: string; + avatar: string; +} + +@Component({ + selector: 'app-modal-sheet-inline', + templateUrl: './modal-sheet-inline.component.html', + standalone: true, + imports: [ + CommonModule, + IonAvatar, + IonButton, + IonContent, + IonImg, + IonItem, + IonLabel, + IonList, + IonModal, + IonSearchbar, + ], +}) +export class ModalSheetInlineComponent { + @ViewChild('inlineSheetModal', { read: IonModal }) inlineSheetModal?: IonModal; + + readonly breakpoints: number[] = [0, 0.2, 0.75, 1]; + + readonly contacts: Contact[] = [ + { + name: 'Connor Smith', + title: 'Sales Rep', + avatar: 'https://i.pravatar.cc/300?u=b', + }, + { + name: 'Daniel Smith', + title: 'Product Designer', + avatar: 'https://i.pravatar.cc/300?u=a', + }, + { + name: 'Greg Smith', + title: 'Director of Operations', + avatar: 'https://i.pravatar.cc/300?u=d', + }, + { + name: 'Zoey Smith', + title: 'CEO', + avatar: 'https://i.pravatar.cc/300?u=e', + }, + ]; + + isSheetOpen = false; + + currentBreakpoint = 'closed'; + + backgroundActionCount = 0; + + presentInlineSheetModal() { + this.isSheetOpen = true; + this.currentBreakpoint = '0.2'; + } + + async expandInlineSheet() { + const modal = this.inlineSheetModal; + + if (!modal) { + return; + } + + await modal.setCurrentBreakpoint(0.75); + this.currentBreakpoint = '0.75'; + } + + onSheetDidDismiss() { + this.isSheetOpen = false; + this.currentBreakpoint = 'closed'; + } + + onSheetBreakpointDidChange(event: CustomEvent<{ breakpoint: number }>) { + this.currentBreakpoint = event.detail.breakpoint.toString(); + } + + onBackgroundActionClick() { + this.backgroundActionCount++; + } +} diff --git a/packages/docs/CHANGELOG.md b/packages/docs/CHANGELOG.md index d732ce1da44..942e8829315 100644 --- a/packages/docs/CHANGELOG.md +++ b/packages/docs/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) + +**Note:** Version bump only for package @ionic/docs + + + + + ## [8.7.4](https://github.com/ionic-team/ionic-framework/compare/v8.7.3...v8.7.4) (2025-09-17) **Note:** Version bump only for package @ionic/docs diff --git a/packages/docs/package-lock.json b/packages/docs/package-lock.json index 84681be496d..0ea9a4f7398 100644 --- a/packages/docs/package-lock.json +++ b/packages/docs/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ionic/docs", - "version": "8.7.4", + "version": "8.7.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/docs", - "version": "8.7.4", + "version": "8.7.5", "license": "MIT" } } diff --git a/packages/docs/package.json b/packages/docs/package.json index 0ef9ba6e49b..3b1eec2020f 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/docs", - "version": "8.7.4", + "version": "8.7.5", "description": "Pre-packaged API documentation for the Ionic docs.", "main": "core.json", "types": "core.d.ts", diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md index 96ce86fdad3..dcff3c6e956 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) + +**Note:** Version bump only for package @ionic/react-router + + + + + ## [8.7.4](https://github.com/ionic-team/ionic-framework/compare/v8.7.3...v8.7.4) (2025-09-17) **Note:** Version bump only for package @ionic/react-router diff --git a/packages/react-router/package-lock.json b/packages/react-router/package-lock.json index 9a00aa0b8f0..b0abc70ecd2 100644 --- a/packages/react-router/package-lock.json +++ b/packages/react-router/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/react-router", - "version": "8.7.4", + "version": "8.7.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/react-router", - "version": "8.7.4", + "version": "8.7.5", "license": "MIT", "dependencies": { - "@ionic/react": "^8.7.4", + "@ionic/react": "^8.7.5", "tslib": "*" }, "devDependencies": { @@ -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": "*" } diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 3442afabbc7..840cfe017a6 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/react-router", - "version": "8.7.4", + "version": "8.7.5", "description": "React Router wrapper for @ionic/react", "keywords": [ "ionic", @@ -36,7 +36,7 @@ "dist/" ], "dependencies": { - "@ionic/react": "^8.7.4", + "@ionic/react": "^8.7.5", "tslib": "*" }, "peerDependencies": { diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index e88df89c730..6c7b05c0a33 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) + + +### Bug Fixes + +* **modal:** allow sheet modals to skip focus trap ([#30689](https://github.com/ionic-team/ionic-framework/issues/30689)) ([a40d957](https://github.com/ionic-team/ionic-framework/commit/a40d957ad9c1897af365a91b45b00228a00d614c)), closes [#30684](https://github.com/ionic-team/ionic-framework/issues/30684) + + + + + ## [8.7.4](https://github.com/ionic-team/ionic-framework/compare/v8.7.3...v8.7.4) (2025-09-17) diff --git a/packages/react/package-lock.json b/packages/react/package-lock.json index 576713eff59..68c8d93aab3 100644 --- a/packages/react/package-lock.json +++ b/packages/react/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/react", - "version": "8.7.4", + "version": "8.7.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/react", - "version": "8.7.4", + "version": "8.7.5", "license": "MIT", "dependencies": { - "@ionic/core": "^8.7.4", + "@ionic/core": "^8.7.5", "ionicons": "^8.0.13", "tslib": "*" }, @@ -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", diff --git a/packages/react/package.json b/packages/react/package.json index cd0b987ca12..bfb5516c651 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/react", - "version": "8.7.4", + "version": "8.7.5", "description": "React specific wrapper for @ionic/core", "keywords": [ "ionic", @@ -40,7 +40,7 @@ "css/" ], "dependencies": { - "@ionic/core": "^8.7.4", + "@ionic/core": "^8.7.5", "ionicons": "^8.0.13", "tslib": "*" }, diff --git a/packages/react/test/base/src/pages/overlay-components/ModalFocusTrap.tsx b/packages/react/test/base/src/pages/overlay-components/ModalFocusTrap.tsx new file mode 100644 index 00000000000..7227946b772 --- /dev/null +++ b/packages/react/test/base/src/pages/overlay-components/ModalFocusTrap.tsx @@ -0,0 +1,61 @@ +import React, { useState } from 'react'; +import { IonButton, IonContent, IonModal, IonPage } from '@ionic/react'; + +const ModalFocusTrap: React.FC = () => { + const [showNonTrapped, setShowNonTrapped] = useState(false); + const [showTrapped, setShowTrapped] = useState(false); + const [count, setCount] = useState(0); + + return ( + + + setShowNonTrapped(true)}> + Open Non-Trapped Sheet Modal + + setShowTrapped(true)}> + Open Focus-Trapped Sheet Modal + + + setCount((c) => c + 1)}> + Background Action + +
+ Background action count: {count} +
+ + setShowNonTrapped(false)} + breakpoints={[0, 0.25, 0.5, 0.75, 1]} + initialBreakpoint={0.25} + backdropDismiss={false} + focusTrap={false} + handleBehavior="cycle" + > + +

Non-trapped modal content

+ setShowNonTrapped(false)}>Close +
+
+ + setShowTrapped(false)} + breakpoints={[0, 0.25, 0.5, 0.75, 1]} + initialBreakpoint={0.5} + backdropDismiss={false} + focusTrap={true} + handleBehavior="cycle" + > + +

Focus-trapped modal content

+ setShowTrapped(false)}>Close +
+
+
+
+ ); +}; + +export default ModalFocusTrap; + diff --git a/packages/react/test/base/src/pages/overlay-components/ModalTeleport.tsx b/packages/react/test/base/src/pages/overlay-components/ModalTeleport.tsx new file mode 100644 index 00000000000..bd9028f842a --- /dev/null +++ b/packages/react/test/base/src/pages/overlay-components/ModalTeleport.tsx @@ -0,0 +1,71 @@ +import React, { useState } from 'react'; +import { + IonButton, + IonButtons, + IonContent, + IonHeader, + IonModal, + IonPage, + IonTitle, + IonToolbar, +} from '@ionic/react'; + +const ModalTeleport: React.FC = () => { + const [isOpen, setIsOpen] = useState(false); + const [count, setCount] = useState(0); + + return ( + + +
+ + setCount((c) => c + 1)}> + Background Action + +
+ Background action count: {count} +
+ + setIsOpen(true)}> + Open Teleported Modal + + + {isOpen && ( + setIsOpen(false)} + onWillPresent={(event) => { + const container = document.getElementById('example'); + if (container) { + container.appendChild(event.target as HTMLElement); + } + }} + breakpoints={[0.2, 0.5, 0.7]} + initialBreakpoint={0.5} + showBackdrop={false} + > + + + Modal + + setIsOpen(false)}> + Close + + + + + +

+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Magni illum quidem recusandae ducimus quos + reprehenderit. Veniam, molestias quos, dolorum consequuntur nisi deserunt omnis id illo sit cum qui. + Eaque, dicta. +

+
+
+ )} +
+
+ ); +}; + +export default ModalTeleport; diff --git a/packages/react/test/base/src/pages/overlay-components/OverlayComponents.tsx b/packages/react/test/base/src/pages/overlay-components/OverlayComponents.tsx index cd9a4ff1045..19aebc9081c 100644 --- a/packages/react/test/base/src/pages/overlay-components/OverlayComponents.tsx +++ b/packages/react/test/base/src/pages/overlay-components/OverlayComponents.tsx @@ -14,6 +14,8 @@ import ActionSheetComponent from './ActionSheetComponent'; import AlertComponent from './AlertComponent'; import LoadingComponent from './LoadingComponent'; import ModalComponent from './ModalComponent'; +import ModalFocusTrap from './ModalFocusTrap'; +import ModalTeleport from './ModalTeleport'; import PickerComponent from './PickerComponent'; import PopoverComponent from './PopoverComponent'; import ToastComponent from './ToastComponent'; @@ -28,7 +30,9 @@ const OverlayHooks: React.FC = () => { - + + + @@ -46,10 +50,18 @@ const OverlayHooks: React.FC = () => { Loading - + Modal + + + Modal Focus + + + + Modal Teleport + Picker diff --git a/packages/react/test/base/tests/e2e/specs/overlay-components/IonModal.cy.ts b/packages/react/test/base/tests/e2e/specs/overlay-components/IonModal.cy.ts index b3dc847074d..bf6ce5aca3c 100644 --- a/packages/react/test/base/tests/e2e/specs/overlay-components/IonModal.cy.ts +++ b/packages/react/test/base/tests/e2e/specs/overlay-components/IonModal.cy.ts @@ -1,6 +1,6 @@ describe('IonModal', () => { beforeEach(() => { - cy.visit('/overlay-components/modal'); + cy.visit('/overlay-components/modal-basic'); }); it('display modal', () => { diff --git a/packages/react/test/base/tests/e2e/specs/overlay-components/IonModalFocusTrap.cy.ts b/packages/react/test/base/tests/e2e/specs/overlay-components/IonModalFocusTrap.cy.ts new file mode 100644 index 00000000000..395c642dcc4 --- /dev/null +++ b/packages/react/test/base/tests/e2e/specs/overlay-components/IonModalFocusTrap.cy.ts @@ -0,0 +1,36 @@ +describe('IonModal: focusTrap regression', () => { + beforeEach(() => { + cy.visit('/overlay-components/modal-focus-trap'); + }); + + it('should allow interacting with background when focusTrap=false', () => { + cy.get('#open-non-trapped-modal').click(); + cy.get('ion-modal').should('be.visible'); + + cy.get('#background-action').click(); + cy.get('#background-action-count').should('have.text', '1'); + }); + + it('should prevent interacting with background when focusTrap=true', () => { + cy.get('#open-trapped-modal').click(); + cy.get('ion-modal').should('be.visible'); + + // Ensure backdrop is active and capturing pointer events + cy.get('ion-backdrop').should('exist'); + cy.get('ion-backdrop').should('have.css', 'pointer-events', 'auto'); + + // Baseline: counter is 0 + cy.get('#background-action-count').should('have.text', '0'); + + // Click the center of the background button via body coordinates (topmost element will receive it) + cy.get('#background-action').then(($btn) => { + const rect = $btn[0].getBoundingClientRect(); + const x = rect.left + rect.width / 2; + const y = rect.top + rect.height / 2; + cy.get('body').click(x, y); + }); + + // Counter should remain unchanged + cy.get('#background-action-count').should('have.text', '0'); + }); +}); diff --git a/packages/react/test/base/tests/e2e/specs/overlay-components/IonModalTeleport.cy.ts b/packages/react/test/base/tests/e2e/specs/overlay-components/IonModalTeleport.cy.ts new file mode 100644 index 00000000000..738f2cd9859 --- /dev/null +++ b/packages/react/test/base/tests/e2e/specs/overlay-components/IonModalTeleport.cy.ts @@ -0,0 +1,27 @@ +describe('IonModal: inline teleport with showBackdrop=false', () => { + beforeEach(() => { + cy.visit('/overlay-components/modal-teleport'); + }); + + it('should render and remain interactive when appended into a page container', () => { + cy.get('#open-teleport-modal').click(); + cy.get('ion-modal').should('be.visible'); + + // Verify modal content is interactable: close button should dismiss the modal + cy.get('#close-teleport-modal').click(); + cy.get('ion-modal').should('not.exist'); + }); + + it('should allow background interaction when showBackdrop=false', () => { + cy.get('#open-teleport-modal').click(); + cy.get('ion-modal').should('be.visible'); + + // Ensure the background button is clickable while modal is open + cy.get('#teleport-background-action').click(); + cy.get('#teleport-background-action-count').should('have.text', '1'); + + // Cleanup + cy.get('#close-teleport-modal').click(); + cy.get('ion-modal').should('not.exist'); + }); +}); diff --git a/packages/react/test/base/tests/e2e/specs/overlay-components/KeepContentsMounted.cy.ts b/packages/react/test/base/tests/e2e/specs/overlay-components/KeepContentsMounted.cy.ts index 8e4b0bdf833..ee5086d2f1a 100644 --- a/packages/react/test/base/tests/e2e/specs/overlay-components/KeepContentsMounted.cy.ts +++ b/packages/react/test/base/tests/e2e/specs/overlay-components/KeepContentsMounted.cy.ts @@ -1,7 +1,7 @@ describe('keepContentsMounted', () => { describe('modal', () => { it('should not mount component if false', () => { - cy.visit('/overlay-components/modal'); + cy.visit('/overlay-components/modal-basic'); cy.get('ion-modal ion-content').should('not.exist'); }); diff --git a/packages/vue-router/CHANGELOG.md b/packages/vue-router/CHANGELOG.md index 9845def7cb2..cf6cdfedb4a 100644 --- a/packages/vue-router/CHANGELOG.md +++ b/packages/vue-router/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) + +**Note:** Version bump only for package @ionic/vue-router + + + + + ## [8.7.4](https://github.com/ionic-team/ionic-framework/compare/v8.7.3...v8.7.4) (2025-09-17) **Note:** Version bump only for package @ionic/vue-router diff --git a/packages/vue-router/package-lock.json b/packages/vue-router/package-lock.json index 6e25076d3df..5c9e6036d55 100644 --- a/packages/vue-router/package-lock.json +++ b/packages/vue-router/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/vue-router", - "version": "8.7.4", + "version": "8.7.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@ionic/vue-router", - "version": "8.7.4", + "version": "8.7.5", "license": "MIT", "dependencies": { - "@ionic/vue": "^8.7.4" + "@ionic/vue": "^8.7.5" }, "devDependencies": { "@ionic/eslint-config": "^0.3.0", @@ -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" } diff --git a/packages/vue-router/package.json b/packages/vue-router/package.json index 199ff019eab..ba2858d5e3d 100644 --- a/packages/vue-router/package.json +++ b/packages/vue-router/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/vue-router", - "version": "8.7.4", + "version": "8.7.5", "description": "Vue Router integration for @ionic/vue", "scripts": { "test.spec": "jest", @@ -44,7 +44,7 @@ }, "homepage": "https://github.com/ionic-team/ionic-framework#readme", "dependencies": { - "@ionic/vue": "^8.7.4" + "@ionic/vue": "^8.7.5" }, "devDependencies": { "@ionic/eslint-config": "^0.3.0", diff --git a/packages/vue/CHANGELOG.md b/packages/vue/CHANGELOG.md index 67a9704e54b..d0850bccee8 100644 --- a/packages/vue/CHANGELOG.md +++ b/packages/vue/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.7.5](https://github.com/ionic-team/ionic-framework/compare/v8.7.4...v8.7.5) (2025-09-24) + + +### Bug Fixes + +* **vue:** emit component-specific overlay events ([#30688](https://github.com/ionic-team/ionic-framework/issues/30688)) ([024d090](https://github.com/ionic-team/ionic-framework/commit/024d090122548e26ec2cdcfae4637dde8f288278)), closes [#30641](https://github.com/ionic-team/ionic-framework/issues/30641) + + + + + ## [8.7.4](https://github.com/ionic-team/ionic-framework/compare/v8.7.3...v8.7.4) (2025-09-17) diff --git a/packages/vue/package-lock.json b/packages/vue/package-lock.json index c4ee7d7891c..29f867b7894 100644 --- a/packages/vue/package-lock.json +++ b/packages/vue/package-lock.json @@ -1,15 +1,15 @@ { "name": "@ionic/vue", - "version": "8.7.4", + "version": "8.7.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ionic/vue", - "version": "8.7.4", + "version": "8.7.5", "license": "MIT", "dependencies": { - "@ionic/core": "^8.7.4", + "@ionic/core": "^8.7.5", "@stencil/vue-output-target": "0.10.7", "ionicons": "^8.0.13" }, @@ -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", diff --git a/packages/vue/package.json b/packages/vue/package.json index 2aeb7b572de..d3469d121f5 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/vue", - "version": "8.7.4", + "version": "8.7.5", "description": "Vue specific wrapper for @ionic/core", "scripts": { "eslint": "eslint src", @@ -68,7 +68,7 @@ "vue-router": "^4.0.16" }, "dependencies": { - "@ionic/core": "^8.7.4", + "@ionic/core": "^8.7.5", "@stencil/vue-output-target": "0.10.7", "ionicons": "^8.0.13" }, diff --git a/packages/vue/src/utils/overlays.ts b/packages/vue/src/utils/overlays.ts index 91133e3490a..b917e750541 100644 --- a/packages/vue/src/utils/overlays.ts +++ b/packages/vue/src/utils/overlays.ts @@ -1,5 +1,5 @@ -import type { VNode, ComponentOptions } from "vue"; -import { defineComponent, h, ref, onMounted } from "vue"; +import type { ComponentOptions, VNode } from "vue"; +import { defineComponent, h, onMounted, ref } from "vue"; // TODO(FW-2969): types @@ -147,23 +147,32 @@ export const defineOverlayContainer = ( const elementRef = ref(); onMounted(() => { + // Convert name from kebab-case to camelCase + const componentName = name.replace(/-([a-z])/g, (_, letter) => + letter.toUpperCase() + ); elementRef.value.addEventListener("ionMount", (ev: Event) => { emit("ionMount", ev); + emit(componentName + "IonMount", ev); isOpen.value = true; }); elementRef.value.addEventListener("willPresent", (ev: Event) => { emit("willPresent", ev); + emit(componentName + "WillPresent", ev); isOpen.value = true; }); elementRef.value.addEventListener("didDismiss", (ev: Event) => { emit("didDismiss", ev); + emit(componentName + "DidDismiss", ev); isOpen.value = false; }); elementRef.value.addEventListener("willDismiss", (ev: Event) => { emit("willDismiss", ev); + emit(componentName + "WillDismiss", ev); }); elementRef.value.addEventListener("didPresent", (ev: Event) => { emit("didPresent", ev); + emit(componentName + "DidPresent", ev); }); }); diff --git a/packages/vue/test/base/src/views/Overlays.vue b/packages/vue/test/base/src/views/Overlays.vue index 49b956e0ddd..0b382b94b47 100644 --- a/packages/vue/test/base/src/views/Overlays.vue +++ b/packages/vue/test/base/src/views/Overlays.vue @@ -64,6 +64,10 @@ Modal onDidPresent:
{{ didPresent }}

Modal onWillDismiss:
{{ willDismiss }}

Modal onDidDismiss:
{{ didDismiss }}

+ Modal ionModalWillPresent:
{{ ionModalWillPresent }}

+ Modal ionModalDidPresent:
{{ ionModalDidPresent }}

+ Modal ionModalWillDismiss:
{{ ionModalWillDismiss }}

+ Modal ionModalDidDismiss:
{{ ionModalDidDismiss }}

@@ -266,6 +274,19 @@ export default defineComponent({ const openModal = async () => { const modal = await modalController.create({ cssClass: "ion-modal-controller", component: ModalContent, componentProps: overlayProps }); + + // Attach lifecycle listeners for controller-created modal + modal.addEventListener('willPresent', () => { willPresent.value += 1; }); + modal.addEventListener('didPresent', () => { didPresent.value += 1; }); + modal.addEventListener('willDismiss', () => { willDismiss.value += 1; }); + modal.addEventListener('didDismiss', () => { didDismiss.value += 1; }); + + // Long-form event names + modal.addEventListener('ionModalWillPresent', () => { ionModalWillPresent.value += 1; }); + modal.addEventListener('ionModalDidPresent', () => { ionModalDidPresent.value += 1; }); + modal.addEventListener('ionModalWillDismiss', () => { ionModalWillDismiss.value += 1; }); + modal.addEventListener('ionModalDidDismiss', () => { ionModalDidDismiss.value += 1; }); + await modal.present(); } @@ -335,21 +356,37 @@ export default defineComponent({ const didPresent = ref(0); const willDismiss = ref(0); const didDismiss = ref(0); + const ionModalWillPresent = ref(0); + const ionModalDidPresent = ref(0); + const ionModalWillDismiss = ref(0); + const ionModalDidDismiss = ref(0); const onModalWillPresent = () => willPresent.value += 1; const onModalDidPresent = () => { didPresent.value += 1; setModalRef(true); } const onModalWillDismiss = () => willDismiss.value += 1; const onModalDidDismiss = () => { didDismiss.value += 1; setModalRef(false); } + const onIonModalWillPresent = () => ionModalWillPresent.value += 1; + const onIonModalDidPresent = () => ionModalDidPresent.value += 1; + const onIonModalWillDismiss = () => ionModalWillDismiss.value += 1; + const onIonModalDidDismiss = () => ionModalDidDismiss.value += 1; return { onModalWillPresent, onModalDidPresent, onModalWillDismiss, onModalDidDismiss, + onIonModalWillPresent, + onIonModalDidPresent, + onIonModalWillDismiss, + onIonModalDidDismiss, willPresent, didPresent, willDismiss, didDismiss, + ionModalWillPresent, + ionModalDidPresent, + ionModalWillDismiss, + ionModalDidDismiss, changeLoadingProps, overlayProps, present, diff --git a/packages/vue/test/base/tests/e2e/specs/overlays.cy.js b/packages/vue/test/base/tests/e2e/specs/overlays.cy.js index 79f4ff4efb1..8d34036799b 100644 --- a/packages/vue/test/base/tests/e2e/specs/overlays.cy.js +++ b/packages/vue/test/base/tests/e2e/specs/overlays.cy.js @@ -1,7 +1,11 @@ const testController = (overlay, shadow = false) => { const selector = `.${overlay}-controller`; - cy.get(`ion-radio#${overlay}`).click(); - cy.get('ion-radio#controller').click(); + cy.get(`ion-radio#${overlay}`) + .scrollIntoView({ offset: { top: -100, left: 0 } }) + .click({ force: true }); + cy.get('ion-radio#controller') + .scrollIntoView({ offset: { top: -100, left: 0 } }) + .click({ force: true }); cy.get('ion-button#present-overlay').click(); cy.get(selector).should('exist').should('be.visible'); @@ -16,8 +20,12 @@ const testController = (overlay, shadow = false) => { } const testComponent = (overlay, shadow = false) => { - cy.get(`ion-radio#${overlay}`).click(); - cy.get('ion-radio#component').click(); + cy.get(`ion-radio#${overlay}`) + .scrollIntoView({ offset: { top: -100, left: 0 } }) + .click({ force: true }); + cy.get('ion-radio#component') + .scrollIntoView({ offset: { top: -100, left: 0 } }) + .click({ force: true }); cy.get('ion-button#present-overlay').click(); cy.get(overlay).should('exist').should('be.visible'); @@ -40,8 +48,12 @@ const testComponent = (overlay, shadow = false) => { } const testInlineOverlay = (overlay, shadow = false) => { - cy.get(`ion-radio#${overlay}`).click(); - cy.get('ion-radio#component').click(); + cy.get(`ion-radio#${overlay}`) + .scrollIntoView({ offset: { top: -100, left: 0 } }) + .click({ force: true }); + cy.get('ion-radio#component') + .scrollIntoView({ offset: { top: -100, left: 0 } }) + .click({ force: true }); cy.get('ion-button#present-overlay').click(); cy.get(overlay).should('exist').should('be.visible'); @@ -214,6 +226,135 @@ describe('Overlays', () => { }); }); + it('should fire long-form lifecycle events on overlays', () => { + cy.get('ion-radio#ion-modal').click(); + cy.get('ion-radio#component').click(); + + cy.get('ion-button#present-overlay').click(); + cy.get('ion-modal').should('exist'); + + testLongLifecycle('overlays', { + willPresent: 1, + didPresent: 1, + willDismiss: 0, + didDismiss: 0 + }); + + cy.get('ion-modal #dismiss').click(); + + testLongLifecycle('overlays', { + willPresent: 1, + didPresent: 1, + willDismiss: 1, + didDismiss: 1 + }); + + cy.get('ion-button#present-overlay').click(); + cy.get('ion-modal').should('exist'); + + testLongLifecycle('overlays', { + willPresent: 2, + didPresent: 2, + willDismiss: 1, + didDismiss: 1 + }); + + cy.get('ion-modal #dismiss').click(); + + testLongLifecycle('overlays', { + willPresent: 2, + didPresent: 2, + willDismiss: 2, + didDismiss: 2 + }); + }); + + it('should fire lifecycle events on controller overlays', () => { + cy.get('ion-radio#ion-modal').click(); + cy.get('ion-radio#controller').click(); + + cy.get('ion-button#present-overlay').click(); + cy.get('ion-modal').should('exist'); + + testLifecycle('overlays', { + willPresent: 1, + didPresent: 1, + willDismiss: 0, + didDismiss: 0 + }); + + cy.get('ion-modal #dismiss').click(); + + testLifecycle('overlays', { + willPresent: 1, + didPresent: 1, + willDismiss: 1, + didDismiss: 1 + }); + + cy.get('ion-button#present-overlay').click(); + cy.get('ion-modal').should('exist'); + + testLifecycle('overlays', { + willPresent: 2, + didPresent: 2, + willDismiss: 1, + didDismiss: 1 + }); + + cy.get('ion-modal #dismiss').click(); + + testLifecycle('overlays', { + willPresent: 2, + didPresent: 2, + willDismiss: 2, + didDismiss: 2 + }); + }); + + it('should fire long-form lifecycle events on controller overlays', () => { + cy.get('ion-radio#ion-modal').click(); + cy.get('ion-radio#controller').click(); + + cy.get('ion-button#present-overlay').click(); + cy.get('ion-modal').should('exist'); + + testLongLifecycle('overlays', { + willPresent: 1, + didPresent: 1, + willDismiss: 0, + didDismiss: 0 + }); + + cy.get('ion-modal #dismiss').click(); + + testLongLifecycle('overlays', { + willPresent: 1, + didPresent: 1, + willDismiss: 1, + didDismiss: 1 + }); + + cy.get('ion-button#present-overlay').click(); + cy.get('ion-modal').should('exist'); + + testLongLifecycle('overlays', { + willPresent: 2, + didPresent: 2, + willDismiss: 1, + didDismiss: 1 + }); + + cy.get('ion-modal #dismiss').click(); + + testLongLifecycle('overlays', { + willPresent: 2, + didPresent: 2, + willDismiss: 2, + didDismiss: 2 + }); + }); + it('should unmount modal via component', () => { cy.get('ion-radio#ion-modal').click(); cy.get('ion-radio#component').click(); @@ -260,3 +401,9 @@ const testLifecycle = (selector, expected = {}) => { cy.get(`[data-pageid=${selector}] #didDismiss`).should('have.text', expected.didDismiss); } +const testLongLifecycle = (selector, expected = {}) => { + cy.get(`[data-pageid=${selector}] #ionModalWillPresent`).should('have.text', expected.willPresent); + cy.get(`[data-pageid=${selector}] #ionModalDidPresent`).should('have.text', expected.didPresent); + cy.get(`[data-pageid=${selector}] #ionModalWillDismiss`).should('have.text', expected.willDismiss); + cy.get(`[data-pageid=${selector}] #ionModalDidDismiss`).should('have.text', expected.didDismiss); +}