Compare commits

..

5 Commits

Author SHA1 Message Date
Brandy Smith
cb7436f051 chore: build & lint 2025-07-02 13:32:26 -04:00
Brandy Smith
f1dc0b2b06 fix(utils): update to track pointer instead of keyboard 2025-07-02 13:30:38 -04:00
Brandy Smith
0ded8ba19f fix(utils): update focus visible to work with keydown again 2025-07-02 13:30:38 -04:00
Brandy Carney
b09caeb053 fix(utils): update focus visible to remove keydown approach 2025-07-02 13:30:37 -04:00
Brandy Carney
42c9db52a7 test(angular): add new pages to navigate between lazy and standalone tests 2025-07-02 13:29:42 -04:00
231 changed files with 29142 additions and 12570 deletions

View File

@@ -10,7 +10,7 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v4
with:
name: ${{ inputs.name }}
path: ${{ inputs.path }}

View File

@@ -32,10 +32,6 @@ runs:
run: npm install
shell: bash
working-directory: ./packages/angular/test/build/${{ inputs.app }}
- name: Install Playwright Browsers
run: npx playwright install
shell: bash
working-directory: ./packages/angular/test/build/${{ inputs.app }}
- name: Sync Built Changes
run: npm run sync
shell: bash

View File

@@ -10,7 +10,7 @@ runs:
- uses: actions/setup-node@v4
with:
node-version: 22.x
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v4
with:
path: ./artifacts
- name: Extract Archives

View File

@@ -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

View File

@@ -9,35 +9,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Validate PR title
if: |
!contains(github.event.pull_request.title, 'release') &&
!contains(github.event.pull_request.title, 'chore')
uses: amannn/action-semantic-pull-request@v5
uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# Configure that a scope must always be provided.
requireScope: true
# Configure allowed commit types
types: |
feat
fix
docs
style
refactor
perf
test
build
ci
revert
release
chore
# Configure additional validation for the subject based on a regex.
# This example ensures the subject doesn't start with an uppercase character.
subjectPattern: ^(?![A-Z]).+$
# If `subjectPattern` is configured, you can use this property to
# override the default error message that is shown when the pattern
# doesn't match. The variables `subject` and `title` can be used
# If `subjectPattern` is configured, you can use this property to
# override the default error message that is shown when the pattern
# doesn't match. The variables `subject` and `title` can be used
# within the message.
subjectPatternError: |
The subject "{subject}" found in the pull request title "{title}" didn't match the configured pattern. Please ensure that the subject doesn't start with an uppercase character.
# If the PR contains one of these newline-delimited labels, the
# validation is skipped. If you want to rerun the validation when
# labels change, you might want to use the `labeled` and `unlabeled`
# event triggers in your workflow.
ignoreLabels: |
release

View File

@@ -3,106 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.2](https://github.com/ionic-team/ionic-framework/compare/v8.7.1...v8.7.2) (2025-08-06)
### Bug Fixes
* **reorder-group:** add children fallback for framework compatibility ([#30593](https://github.com/ionic-team/ionic-framework/issues/30593)) ([1cd81b9](https://github.com/ionic-team/ionic-framework/commit/1cd81b92301378d55bce63a01dfcf95a91c92652)), closes [#30592](https://github.com/ionic-team/ionic-framework/issues/30592)
* **tabs:** add fallback to select tab if router integration fails ([#30599](https://github.com/ionic-team/ionic-framework/issues/30599)) ([a2e803a](https://github.com/ionic-team/ionic-framework/commit/a2e803a553dc58fc0e1599e515a56180a7ab263a)), closes [#30552](https://github.com/ionic-team/ionic-framework/issues/30552)
## [8.7.1](https://github.com/ionic-team/ionic-framework/compare/v8.7.0...v8.7.1) (2025-07-31)
### Dependencies
* **stencil:** upgrade `@stencil/core` to version 4.36.2
# [8.7.0](https://github.com/ionic-team/ionic-framework/compare/v8.6.7...v8.7.0) (2025-07-30)
### Features
* **css:** add new css utility classes for display and flex utils ([#30567](https://github.com/ionic-team/ionic-framework/issues/30567)) ([75f6c05](https://github.com/ionic-team/ionic-framework/commit/75f6c05fb96313ef890cc80a229a3a3ed3d57460)), closes [#22469](https://github.com/ionic-team/ionic-framework/issues/22469)
* **datetime:** add border property to highlightedDates ([#30534](https://github.com/ionic-team/ionic-framework/issues/30534)) ([d5627c7](https://github.com/ionic-team/ionic-framework/commit/d5627c73681faf658ea3b869f3fb04d708391eb9)), closes [#29833](https://github.com/ionic-team/ionic-framework/issues/29833)
* **deps:** update ionicons to v8 ([#30390](https://github.com/ionic-team/ionic-framework/issues/30390)) ([74cd71a](https://github.com/ionic-team/ionic-framework/commit/74cd71af243183aa738d11b280e155bdfd652126)), closes [#30445](https://github.com/ionic-team/ionic-framework/issues/30445)
* **modal:** add IonModalToken for injecting modal elements in Angular components ([#30474](https://github.com/ionic-team/ionic-framework/issues/30474)) ([30d1910](https://github.com/ionic-team/ionic-framework/commit/30d1910d6ea5428b414d0e127e7681f59426c538))
* **reorder-group:** add ionReorderStart, ionReorderMove, ionReorderEnd events ([#30471](https://github.com/ionic-team/ionic-framework/issues/30471)) ([b154f4e](https://github.com/ionic-team/ionic-framework/commit/b154f4ed095890f57ccab539fd9217976a5466e5)), closes [#23148](https://github.com/ionic-team/ionic-framework/issues/23148) [#27614](https://github.com/ionic-team/ionic-framework/issues/27614)
## [8.6.7](https://github.com/ionic-team/ionic-framework/compare/v8.6.6...v8.6.7) (2025-07-30)
### Dependencies
* **stencil:** downgrade `@stencil/core` to version 4.33.1
_Stencil has been downgraded due to an uncaught regression in Reorder._
## [8.6.6](https://github.com/ionic-team/ionic-framework/compare/v8.6.5...v8.6.6) (2025-07-30)
### Dependencies
* **stencil:** upgrade `@stencil/core` to version 4.36.2
## [8.6.5](https://github.com/ionic-team/ionic-framework/compare/v8.6.4...v8.6.5) (2025-07-16)
### Bug Fixes
* **input-otp:** improve autofill detection and invalid character handling ([#30541](https://github.com/ionic-team/ionic-framework/issues/30541)) ([8b4023d](https://github.com/ionic-team/ionic-framework/commit/8b4023d520212c254395a5be6d3a76dcbee6f2da)), closes [#30459](https://github.com/ionic-team/ionic-framework/issues/30459)
* **input:** prevent layout shift when hiding password toggle ([#30533](https://github.com/ionic-team/ionic-framework/issues/30533)) ([f1defba](https://github.com/ionic-team/ionic-framework/commit/f1defba2acb417c6f243b2902923d85efbb6f879)), closes [#29562](https://github.com/ionic-team/ionic-framework/issues/29562)
* **item:** allow nested content to be conditionally interactive ([#30519](https://github.com/ionic-team/ionic-framework/issues/30519)) ([3f730ab](https://github.com/ionic-team/ionic-framework/commit/3f730ab1d77be54d1faf14168eee9e9dc41002d6)), closes [#29763](https://github.com/ionic-team/ionic-framework/issues/29763)
* **modal:** dismiss child modals when parent is dismissed ([#30540](https://github.com/ionic-team/ionic-framework/issues/30540)) ([9b0099f](https://github.com/ionic-team/ionic-framework/commit/9b0099f462fda6d40b49dde1a1c97afbbbee2287)), closes [#30389](https://github.com/ionic-team/ionic-framework/issues/30389)
* **modal:** dismiss modal when parent element is removed from DOM ([#30544](https://github.com/ionic-team/ionic-framework/issues/30544)) ([850338c](https://github.com/ionic-team/ionic-framework/commit/850338cbd5c76addbc2cc3068b93071dea14c0af)), closes [#30389](https://github.com/ionic-team/ionic-framework/issues/30389)
* **modal:** improve card modal background transition from portrait to landscape ([#30551](https://github.com/ionic-team/ionic-framework/issues/30551)) ([d37b9b8](https://github.com/ionic-team/ionic-framework/commit/d37b9b8e468b7b2c9cda8b27fe7019bb905ad2bf))
* **segment-view:** scroll to correct content when height is not set ([#30547](https://github.com/ionic-team/ionic-framework/issues/30547)) ([d14311f](https://github.com/ionic-team/ionic-framework/commit/d14311fb65ae3de7ba7578791ce1ea44f186c413)), closes [#30543](https://github.com/ionic-team/ionic-framework/issues/30543)
## [8.6.4](https://github.com/ionic-team/ionic-framework/compare/v8.6.3...v8.6.4) (2025-07-09)
### Bug Fixes
* **modal:** support iOS card view transitions for viewport changes ([#30520](https://github.com/ionic-team/ionic-framework/issues/30520)) ([0fd9e82](https://github.com/ionic-team/ionic-framework/commit/0fd9e824508333a53175d7da5f681fc3126a2394)), closes [#30296](https://github.com/ionic-team/ionic-framework/issues/30296)
## [8.6.3](https://github.com/ionic-team/ionic-framework/compare/v8.6.2...v8.6.3) (2025-07-02)
### Bug Fixes
* **angular:** update schematics to support Angular's latest build system ([#30525](https://github.com/ionic-team/ionic-framework/issues/30525)) ([08e3e7a](https://github.com/ionic-team/ionic-framework/commit/08e3e7ab5165baea668571af9845933b5befeb46)), closes [ionic-team/ionic-docs#2091](https://github.com/ionic-team/ionic-docs/issues/2091)
* **modal:** add conditional tabIndex for handle cycling ([#30510](https://github.com/ionic-team/ionic-framework/issues/30510)) ([ee47660](https://github.com/ionic-team/ionic-framework/commit/ee47660745428e04c78cfef0555f3c5788959a8c))
* **select:** focus the correct selected item in an action sheet interface with a header ([#30481](https://github.com/ionic-team/ionic-framework/issues/30481)) ([80a111c](https://github.com/ionic-team/ionic-framework/commit/80a111cffac70e831eb57e827301370163ef4e2a)), closes [#30480](https://github.com/ionic-team/ionic-framework/issues/30480)
## [8.6.2](https://github.com/ionic-team/ionic-framework/compare/v8.6.1...v8.6.2) (2025-06-18)

View File

@@ -3,104 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.2](https://github.com/ionic-team/ionic-framework/compare/v8.7.1...v8.7.2) (2025-08-06)
### Bug Fixes
* **reorder-group:** add children fallback for framework compatibility ([#30593](https://github.com/ionic-team/ionic-framework/issues/30593)) ([1cd81b9](https://github.com/ionic-team/ionic-framework/commit/1cd81b92301378d55bce63a01dfcf95a91c92652)), closes [#30592](https://github.com/ionic-team/ionic-framework/issues/30592)
* **tabs:** add fallback to select tab if router integration fails ([#30599](https://github.com/ionic-team/ionic-framework/issues/30599)) ([a2e803a](https://github.com/ionic-team/ionic-framework/commit/a2e803a553dc58fc0e1599e515a56180a7ab263a)), closes [#30552](https://github.com/ionic-team/ionic-framework/issues/30552)
## [8.7.1](https://github.com/ionic-team/ionic-framework/compare/v8.7.0...v8.7.1) (2025-07-31)
### Dependencies
* **stencil:** upgrade `@stencil/core` to version 4.36.2
# [8.7.0](https://github.com/ionic-team/ionic-framework/compare/v8.6.7...v8.7.0) (2025-07-30)
### Features
* **css:** add new css utility classes for display and flex utils ([#30567](https://github.com/ionic-team/ionic-framework/issues/30567)) ([75f6c05](https://github.com/ionic-team/ionic-framework/commit/75f6c05fb96313ef890cc80a229a3a3ed3d57460)), closes [#22469](https://github.com/ionic-team/ionic-framework/issues/22469)
* **datetime:** add border property to highlightedDates ([#30534](https://github.com/ionic-team/ionic-framework/issues/30534)) ([d5627c7](https://github.com/ionic-team/ionic-framework/commit/d5627c73681faf658ea3b869f3fb04d708391eb9)), closes [#29833](https://github.com/ionic-team/ionic-framework/issues/29833)
* **deps:** update ionicons to v8 ([#30390](https://github.com/ionic-team/ionic-framework/issues/30390)) ([74cd71a](https://github.com/ionic-team/ionic-framework/commit/74cd71af243183aa738d11b280e155bdfd652126)), closes [#30445](https://github.com/ionic-team/ionic-framework/issues/30445)
* **reorder-group:** add ionReorderStart, ionReorderMove, ionReorderEnd events ([#30471](https://github.com/ionic-team/ionic-framework/issues/30471)) ([b154f4e](https://github.com/ionic-team/ionic-framework/commit/b154f4ed095890f57ccab539fd9217976a5466e5)), closes [#23148](https://github.com/ionic-team/ionic-framework/issues/23148) [#27614](https://github.com/ionic-team/ionic-framework/issues/27614)
## [8.6.7](https://github.com/ionic-team/ionic-framework/compare/v8.6.6...v8.6.7) (2025-07-30)
### Dependencies
* **stencil:** downgrade `@stencil/core` to version 4.33.1
_Stencil has been downgraded due to an uncaught regression in Reorder._
## [8.6.6](https://github.com/ionic-team/ionic-framework/compare/v8.6.5...v8.6.6) (2025-07-30)
### Dependencies
* **stencil:** upgrade `@stencil/core` to version 4.36.2
## [8.6.5](https://github.com/ionic-team/ionic-framework/compare/v8.6.4...v8.6.5) (2025-07-16)
### Bug Fixes
* **input-otp:** improve autofill detection and invalid character handling ([#30541](https://github.com/ionic-team/ionic-framework/issues/30541)) ([8b4023d](https://github.com/ionic-team/ionic-framework/commit/8b4023d520212c254395a5be6d3a76dcbee6f2da)), closes [#30459](https://github.com/ionic-team/ionic-framework/issues/30459)
* **input:** prevent layout shift when hiding password toggle ([#30533](https://github.com/ionic-team/ionic-framework/issues/30533)) ([f1defba](https://github.com/ionic-team/ionic-framework/commit/f1defba2acb417c6f243b2902923d85efbb6f879)), closes [#29562](https://github.com/ionic-team/ionic-framework/issues/29562)
* **item:** allow nested content to be conditionally interactive ([#30519](https://github.com/ionic-team/ionic-framework/issues/30519)) ([3f730ab](https://github.com/ionic-team/ionic-framework/commit/3f730ab1d77be54d1faf14168eee9e9dc41002d6)), closes [#29763](https://github.com/ionic-team/ionic-framework/issues/29763)
* **modal:** dismiss child modals when parent is dismissed ([#30540](https://github.com/ionic-team/ionic-framework/issues/30540)) ([9b0099f](https://github.com/ionic-team/ionic-framework/commit/9b0099f462fda6d40b49dde1a1c97afbbbee2287)), closes [#30389](https://github.com/ionic-team/ionic-framework/issues/30389)
* **modal:** dismiss modal when parent element is removed from DOM ([#30544](https://github.com/ionic-team/ionic-framework/issues/30544)) ([850338c](https://github.com/ionic-team/ionic-framework/commit/850338cbd5c76addbc2cc3068b93071dea14c0af)), closes [#30389](https://github.com/ionic-team/ionic-framework/issues/30389)
* **modal:** improve card modal background transition from portrait to landscape ([#30551](https://github.com/ionic-team/ionic-framework/issues/30551)) ([d37b9b8](https://github.com/ionic-team/ionic-framework/commit/d37b9b8e468b7b2c9cda8b27fe7019bb905ad2bf))
* **segment-view:** scroll to correct content when height is not set ([#30547](https://github.com/ionic-team/ionic-framework/issues/30547)) ([d14311f](https://github.com/ionic-team/ionic-framework/commit/d14311fb65ae3de7ba7578791ce1ea44f186c413)), closes [#30543](https://github.com/ionic-team/ionic-framework/issues/30543)
## [8.6.4](https://github.com/ionic-team/ionic-framework/compare/v8.6.3...v8.6.4) (2025-07-09)
### Bug Fixes
* **modal:** support iOS card view transitions for viewport changes ([#30520](https://github.com/ionic-team/ionic-framework/issues/30520)) ([0fd9e82](https://github.com/ionic-team/ionic-framework/commit/0fd9e824508333a53175d7da5f681fc3126a2394)), closes [#30296](https://github.com/ionic-team/ionic-framework/issues/30296)
## [8.6.3](https://github.com/ionic-team/ionic-framework/compare/v8.6.2...v8.6.3) (2025-07-02)
### Bug Fixes
* **modal:** add conditional tabIndex for handle cycling ([#30510](https://github.com/ionic-team/ionic-framework/issues/30510)) ([ee47660](https://github.com/ionic-team/ionic-framework/commit/ee47660745428e04c78cfef0555f3c5788959a8c))
* **select:** focus the correct selected item in an action sheet interface with a header ([#30481](https://github.com/ionic-team/ionic-framework/issues/30481)) ([80a111c](https://github.com/ionic-team/ionic-framework/commit/80a111cffac70e831eb57e827301370163ef4e2a)), closes [#30480](https://github.com/ionic-team/ionic-framework/issues/30480)
## [8.6.2](https://github.com/ionic-team/ionic-framework/compare/v8.6.1...v8.6.2) (2025-06-18)

View File

@@ -1,5 +1,5 @@
# Get Playwright
FROM mcr.microsoft.com/playwright:v1.54.2
FROM mcr.microsoft.com/playwright:v1.53.1
# Set the working directory
WORKDIR /ionic

View File

@@ -1508,9 +1508,6 @@ ion-reorder-group,none
ion-reorder-group,prop,disabled,boolean,true,false,false
ion-reorder-group,method,complete,complete(listOrReorder?: boolean | any[]) => Promise<any>
ion-reorder-group,event,ionItemReorder,ItemReorderEventDetail,true
ion-reorder-group,event,ionReorderEnd,ReorderEndEventDetail,true
ion-reorder-group,event,ionReorderMove,ReorderMoveEventDetail,true
ion-reorder-group,event,ionReorderStart,void,true
ion-ripple-effect,shadow
ion-ripple-effect,prop,type,"bounded" | "unbounded",'bounded',false,false

173
core/package-lock.json generated
View File

@@ -1,16 +1,16 @@
{
"name": "@ionic/core",
"version": "8.7.2",
"version": "8.6.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/core",
"version": "8.7.2",
"version": "8.6.2",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.36.2",
"ionicons": "^8.0.13",
"@stencil/core": "4.33.1",
"ionicons": "^7.2.2",
"tslib": "^2.1.0"
},
"devDependencies": {
@@ -22,13 +22,13 @@
"@clack/prompts": "^0.11.0",
"@ionic/eslint-config": "^0.3.0",
"@ionic/prettier-config": "^2.0.0",
"@playwright/test": "^1.54.2",
"@playwright/test": "^1.53.2",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/angular-output-target": "^1.0.0",
"@stencil/angular-output-target": "^0.10.0",
"@stencil/react-output-target": "0.5.3",
"@stencil/sass": "^3.0.9",
"@stencil/vue-output-target": "0.11.8",
"@stencil/vue-output-target": "0.10.8",
"@types/jest": "^29.5.6",
"@types/node": "^14.6.0",
"@typescript-eslint/eslint-plugin": "^6.7.2",
@@ -663,36 +663,36 @@
"dev": true
},
"node_modules/@capacitor/core": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.2.tgz",
"integrity": "sha512-akCf9A1FUR8AWTtmgGjHEq6LmGsjA2U7igaJ9PxiCBfyxKqlDbuGHrlNdpvHEjV5tUPH3KYtkze6gtFcNKPU9A==",
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.0.tgz",
"integrity": "sha512-P6NnjoHyobZgTjynlZSn27d0SUj6j38inlNxFnKZr9qwU7/r6+0Sg2nWkGkIH/pMmXHsvGD8zVe6KUq1UncIjA==",
"dev": true,
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/@capacitor/haptics": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-7.0.2.tgz",
"integrity": "sha512-vqfeEM6s2zMgLjpITCTUIy7P/hadq/Gr5E/RClFgMJPB41Y5FsqOKD+j85/uwh8N2cf/aWaPeXUmjnTzJbEB2g==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-7.0.1.tgz",
"integrity": "sha512-ewZmspE5krgDUj5ZvUDcfNZvgerAIr+3bDSk6DLzyvBZ/dYmr/tMLu5H6WtYaaKYZJ32aZAudGpIal5epDyBYA==",
"dev": true,
"peerDependencies": {
"@capacitor/core": ">=7.0.0"
}
},
"node_modules/@capacitor/keyboard": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.2.tgz",
"integrity": "sha512-9We5BY1mu+QWOReDukr+6HxA4Bh0mKBU0txFtwXJdjBohttMYWJzB+dQf4oHrX8odiU2Cm/BfDdAU2wV06Cyig==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.1.tgz",
"integrity": "sha512-Gi064vOARMac+x9/DmEFeywN9oAETMf3OYsMuYm9gA8SvdsDJ3QJqMoFnSEIORYXe21Jzt2SIEdLlpT65P/b2g==",
"dev": true,
"peerDependencies": {
"@capacitor/core": ">=7.0.0"
}
},
"node_modules/@capacitor/status-bar": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.2.tgz",
"integrity": "sha512-fYYkkdzCbQV+MjZVnaQTFl5I4bddnFW8ZrPVxDjNoGVPTUG7H58Ij1+NcuNxHLXjJvZOoZeYJ3w3I16Wb2zssw==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.1.tgz",
"integrity": "sha512-iDv3mXYo9CdxYRVwt3/pRyuk25p7Sn4GfaS/zMZyVIqTzsvKLCIIH3GdKK+ta+nsNcAVpCw/t5jFEBt1D18ctA==",
"dev": true,
"peerDependencies": {
"@capacitor/core": ">=7.0.0"
@@ -1715,12 +1715,12 @@
}
},
"node_modules/@playwright/test": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz",
"integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==",
"version": "1.53.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.2.tgz",
"integrity": "sha512-tEB2U5z74ebBeyfGNZ3Jfg29AnW+5HlWhvHtb/Mqco9pFdZU1ZLNdVb2UtB5CvmiilNr2ZfVH/qMmAROG/XTzw==",
"dev": true,
"dependencies": {
"playwright": "1.54.2"
"playwright": "1.53.2"
},
"bin": {
"playwright": "cli.js"
@@ -1905,18 +1905,18 @@
}
},
"node_modules/@stencil/angular-output-target": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-1.0.0.tgz",
"integrity": "sha512-6/XtjEWlJS+40b3x2geCV43rjQKlHS/a1/62QjYGXBtHhMo92KyGwKns3ntmX6B4fdk/xAKehxD2WKtHIM3oxQ==",
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-0.10.2.tgz",
"integrity": "sha512-jPRa2NMAPtm/iMY+mUaWATbIhgY5zPJfUNQyF8nwC0rMrfXifPoRCf6BbH2S4Gy7SX0X4hlP+jAbVUjQNg/P+Q==",
"dev": true,
"peerDependencies": {
"@stencil/core": ">=2.0.0 || >=3 || >= 4.0.0-beta.0 || >= 4.0.0"
}
},
"node_modules/@stencil/core": {
"version": "4.36.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz",
"integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==",
"version": "4.33.1",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.33.1.tgz",
"integrity": "sha512-12k9xhAJBkpg598it+NRmaYIdEe6TSnsL/v6/KRXDcUyTK11VYwZQej2eHnMWtqot+znJ+GNTqb5YbiXi+5Low==",
"license": "MIT",
"bin": {
"stencil": "bin/stencil"
@@ -1960,9 +1960,9 @@
}
},
"node_modules/@stencil/vue-output-target": {
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.11.8.tgz",
"integrity": "sha512-R/kQoN15irgL7NJxWaUNSmwDLfoDBZjlYaXNnW3LHlF30TYfyez6pRgD7ZglSSTVktMtCXz6ZPhg0uq59VkhOw==",
"version": "0.10.8",
"resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.10.8.tgz",
"integrity": "sha512-/a20LG29xqy/lxBqo6zc1LbyS20GW9xghypZ7vYdo5fQB8jHClAQDkn+c8aykQlre5TtHiODgvr/rRDUrQKwyg==",
"dev": true,
"peerDependencies": {
"@stencil/core": ">=2.0.0 || >=3 || >= 4.0.0-beta.0 || >= 4.0.0",
@@ -3474,9 +3474,9 @@
]
},
"node_modules/chalk": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz",
"integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
"dev": true,
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
@@ -5711,12 +5711,11 @@
}
},
"node_modules/ionicons": {
"version": "8.0.13",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-8.0.13.tgz",
"integrity": "sha512-2QQVyG2P4wszne79jemMjWYLp0DBbDhr4/yFroPCxvPP1wtMxgdIV3l5n+XZ5E9mgoXU79w7yTWpm2XzJsISxQ==",
"license": "MIT",
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.2.2.tgz",
"integrity": "sha512-I3iYIfc9Q9FRifWyFSwTAvbEABWlWY32i0sAVDDPGYnaIZVugkLCZFbEcrphW6ixVPg8tt1oLwalo/JJwbEqnA==",
"dependencies": {
"@stencil/core": "^4.35.3"
"@stencil/core": "^4.0.3"
}
},
"node_modules/is-alphabetical": {
@@ -8593,12 +8592,12 @@
}
},
"node_modules/playwright": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz",
"integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==",
"version": "1.53.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.2.tgz",
"integrity": "sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==",
"dev": true,
"dependencies": {
"playwright-core": "1.54.2"
"playwright-core": "1.53.2"
},
"bin": {
"playwright": "cli.js"
@@ -8611,9 +8610,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz",
"integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==",
"version": "1.53.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.2.tgz",
"integrity": "sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
@@ -11102,32 +11101,32 @@
"dev": true
},
"@capacitor/core": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.2.tgz",
"integrity": "sha512-akCf9A1FUR8AWTtmgGjHEq6LmGsjA2U7igaJ9PxiCBfyxKqlDbuGHrlNdpvHEjV5tUPH3KYtkze6gtFcNKPU9A==",
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.4.0.tgz",
"integrity": "sha512-P6NnjoHyobZgTjynlZSn27d0SUj6j38inlNxFnKZr9qwU7/r6+0Sg2nWkGkIH/pMmXHsvGD8zVe6KUq1UncIjA==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
}
},
"@capacitor/haptics": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-7.0.2.tgz",
"integrity": "sha512-vqfeEM6s2zMgLjpITCTUIy7P/hadq/Gr5E/RClFgMJPB41Y5FsqOKD+j85/uwh8N2cf/aWaPeXUmjnTzJbEB2g==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-7.0.1.tgz",
"integrity": "sha512-ewZmspE5krgDUj5ZvUDcfNZvgerAIr+3bDSk6DLzyvBZ/dYmr/tMLu5H6WtYaaKYZJ32aZAudGpIal5epDyBYA==",
"dev": true,
"requires": {}
},
"@capacitor/keyboard": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.2.tgz",
"integrity": "sha512-9We5BY1mu+QWOReDukr+6HxA4Bh0mKBU0txFtwXJdjBohttMYWJzB+dQf4oHrX8odiU2Cm/BfDdAU2wV06Cyig==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.1.tgz",
"integrity": "sha512-Gi064vOARMac+x9/DmEFeywN9oAETMf3OYsMuYm9gA8SvdsDJ3QJqMoFnSEIORYXe21Jzt2SIEdLlpT65P/b2g==",
"dev": true,
"requires": {}
},
"@capacitor/status-bar": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.2.tgz",
"integrity": "sha512-fYYkkdzCbQV+MjZVnaQTFl5I4bddnFW8ZrPVxDjNoGVPTUG7H58Ij1+NcuNxHLXjJvZOoZeYJ3w3I16Wb2zssw==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.1.tgz",
"integrity": "sha512-iDv3mXYo9CdxYRVwt3/pRyuk25p7Sn4GfaS/zMZyVIqTzsvKLCIIH3GdKK+ta+nsNcAVpCw/t5jFEBt1D18ctA==",
"dev": true,
"requires": {}
},
@@ -11863,12 +11862,12 @@
}
},
"@playwright/test": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz",
"integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==",
"version": "1.53.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.2.tgz",
"integrity": "sha512-tEB2U5z74ebBeyfGNZ3Jfg29AnW+5HlWhvHtb/Mqco9pFdZU1ZLNdVb2UtB5CvmiilNr2ZfVH/qMmAROG/XTzw==",
"dev": true,
"requires": {
"playwright": "1.54.2"
"playwright": "1.53.2"
}
},
"@rollup/plugin-node-resolve": {
@@ -11977,16 +11976,16 @@
}
},
"@stencil/angular-output-target": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-1.0.0.tgz",
"integrity": "sha512-6/XtjEWlJS+40b3x2geCV43rjQKlHS/a1/62QjYGXBtHhMo92KyGwKns3ntmX6B4fdk/xAKehxD2WKtHIM3oxQ==",
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-0.10.2.tgz",
"integrity": "sha512-jPRa2NMAPtm/iMY+mUaWATbIhgY5zPJfUNQyF8nwC0rMrfXifPoRCf6BbH2S4Gy7SX0X4hlP+jAbVUjQNg/P+Q==",
"dev": true,
"requires": {}
},
"@stencil/core": {
"version": "4.36.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz",
"integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==",
"version": "4.33.1",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.33.1.tgz",
"integrity": "sha512-12k9xhAJBkpg598it+NRmaYIdEe6TSnsL/v6/KRXDcUyTK11VYwZQej2eHnMWtqot+znJ+GNTqb5YbiXi+5Low==",
"requires": {
"@rollup/rollup-darwin-arm64": "4.34.9",
"@rollup/rollup-darwin-x64": "4.34.9",
@@ -12013,9 +12012,9 @@
"requires": {}
},
"@stencil/vue-output-target": {
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.11.8.tgz",
"integrity": "sha512-R/kQoN15irgL7NJxWaUNSmwDLfoDBZjlYaXNnW3LHlF30TYfyez6pRgD7ZglSSTVktMtCXz6ZPhg0uq59VkhOw==",
"version": "0.10.8",
"resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.10.8.tgz",
"integrity": "sha512-/a20LG29xqy/lxBqo6zc1LbyS20GW9xghypZ7vYdo5fQB8jHClAQDkn+c8aykQlre5TtHiODgvr/rRDUrQKwyg==",
"dev": true,
"requires": {}
},
@@ -13076,9 +13075,9 @@
"dev": true
},
"chalk": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz",
"integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
"dev": true
},
"chalk-template": {
@@ -14745,11 +14744,11 @@
}
},
"ionicons": {
"version": "8.0.13",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-8.0.13.tgz",
"integrity": "sha512-2QQVyG2P4wszne79jemMjWYLp0DBbDhr4/yFroPCxvPP1wtMxgdIV3l5n+XZ5E9mgoXU79w7yTWpm2XzJsISxQ==",
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.2.2.tgz",
"integrity": "sha512-I3iYIfc9Q9FRifWyFSwTAvbEABWlWY32i0sAVDDPGYnaIZVugkLCZFbEcrphW6ixVPg8tt1oLwalo/JJwbEqnA==",
"requires": {
"@stencil/core": "^4.35.3"
"@stencil/core": "^4.0.3"
}
},
"is-alphabetical": {
@@ -16812,19 +16811,19 @@
}
},
"playwright": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz",
"integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==",
"version": "1.53.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.2.tgz",
"integrity": "sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==",
"dev": true,
"requires": {
"fsevents": "2.3.2",
"playwright-core": "1.54.2"
"playwright-core": "1.53.2"
}
},
"playwright-core": {
"version": "1.54.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz",
"integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==",
"version": "1.53.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.2.tgz",
"integrity": "sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==",
"dev": true
},
"postcss": {
@@ -18338,4 +18337,4 @@
"dev": true
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "8.7.2",
"version": "8.6.2",
"description": "Base components for Ionic",
"keywords": [
"ionic",
@@ -31,8 +31,8 @@
"loader/"
],
"dependencies": {
"@stencil/core": "4.36.2",
"ionicons": "^8.0.13",
"@stencil/core": "4.33.1",
"ionicons": "^7.2.2",
"tslib": "^2.1.0"
},
"devDependencies": {
@@ -44,13 +44,13 @@
"@clack/prompts": "^0.11.0",
"@ionic/eslint-config": "^0.3.0",
"@ionic/prettier-config": "^2.0.0",
"@playwright/test": "^1.54.2",
"@playwright/test": "^1.53.2",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/angular-output-target": "^1.0.0",
"@stencil/angular-output-target": "^0.10.0",
"@stencil/react-output-target": "0.5.3",
"@stencil/sass": "^3.0.9",
"@stencil/vue-output-target": "0.11.8",
"@stencil/vue-output-target": "0.10.8",
"@types/jest": "^29.5.6",
"@types/node": "^14.6.0",
"@typescript-eslint/eslint-plugin": "^6.7.2",

View File

@@ -30,7 +30,7 @@ import { PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAct
import { RadioGroupChangeEventDetail, RadioGroupCompareFn } from "./components/radio-group/radio-group-interface";
import { PinFormatter, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue } from "./components/range/range-interface";
import { RefresherEventDetail } from "./components/refresher/refresher-interface";
import { ItemReorderEventDetail, ReorderEndEventDetail, ReorderMoveEventDetail } from "./components/reorder-group/reorder-group-interface";
import { ItemReorderEventDetail } from "./components/reorder-group/reorder-group-interface";
import { NavigationHookCallback } from "./components/route/route-interface";
import { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
import { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
@@ -68,7 +68,7 @@ export { PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAct
export { RadioGroupChangeEventDetail, RadioGroupCompareFn } from "./components/radio-group/radio-group-interface";
export { PinFormatter, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue } from "./components/range/range-interface";
export { RefresherEventDetail } from "./components/refresher/refresher-interface";
export { ItemReorderEventDetail, ReorderEndEventDetail, ReorderMoveEventDetail } from "./components/reorder-group/reorder-group-interface";
export { ItemReorderEventDetail } from "./components/reorder-group/reorder-group-interface";
export { NavigationHookCallback } from "./components/route/route-interface";
export { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
export { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
@@ -2783,7 +2783,7 @@ export namespace Components {
}
interface IonReorderGroup {
/**
* Completes the reorder operation. Must be called by the `ionReorderEnd` event. If a list of items is passed, the list will be reordered and returned in the proper order. If no parameters are passed or if `true` is passed in, the reorder will complete and the item will remain in the position it was dragged to. If `false` is passed, the reorder will complete and the item will bounce back to its original position.
* Completes the reorder operation. Must be called by the `ionItemReorder` event. If a list of items is passed, the list will be reordered and returned in the proper order. If no parameters are passed or if `true` is passed in, the reorder will complete and the item will remain in the position it was dragged to. If `false` is passed, the reorder will complete and the item will bounce back to its original position.
* @param listOrReorder A list of items to be sorted and returned in the new order or a boolean of whether or not the reorder should reposition the item.
*/
"complete": (listOrReorder?: boolean | any[]) => Promise<any>;
@@ -4769,9 +4769,6 @@ declare global {
};
interface HTMLIonReorderGroupElementEventMap {
"ionItemReorder": ItemReorderEventDetail;
"ionReorderStart": void;
"ionReorderMove": ReorderMoveEventDetail;
"ionReorderEnd": ReorderEndEventDetail;
}
interface HTMLIonReorderGroupElement extends Components.IonReorderGroup, HTMLStencilElement {
addEventListener<K extends keyof HTMLIonReorderGroupElementEventMap>(type: K, listener: (this: HTMLIonReorderGroupElement, ev: IonReorderGroupCustomEvent<HTMLIonReorderGroupElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
@@ -8056,22 +8053,9 @@ declare namespace LocalJSX {
*/
"disabled"?: boolean;
/**
* Event that needs to be listened to in order to complete the reorder action.
* @deprecated Use `ionReorderEnd` instead. If you are accessing `event.detail.from` or `event.detail.to` and relying on them being different you should now add checks as they are always emitted in `ionReorderEnd`, even when they are the same.
* Event that needs to be listened to in order to complete the reorder action. Once the event has been emitted, the `complete()` method then needs to be called in order to finalize the reorder action.
*/
"onIonItemReorder"?: (event: IonReorderGroupCustomEvent<ItemReorderEventDetail>) => void;
/**
* Event that is emitted when the reorder gesture ends. The from and to properties are always available, regardless of if the reorder gesture moved the item. If the item did not change from its start position, the from and to properties will be the same. Once the event has been emitted, the `complete()` method then needs to be called in order to finalize the reorder action.
*/
"onIonReorderEnd"?: (event: IonReorderGroupCustomEvent<ReorderEndEventDetail>) => void;
/**
* Event that is emitted as the reorder gesture moves.
*/
"onIonReorderMove"?: (event: IonReorderGroupCustomEvent<ReorderMoveEventDetail>) => void;
/**
* Event that is emitted when the reorder gesture starts.
*/
"onIonReorderStart"?: (event: IonReorderGroupCustomEvent<void>) => void;
}
interface IonRippleEffect {
/**

View File

@@ -125,7 +125,7 @@
<ion-toolbar color="dark">
<ion-buttons slot="start">
<ion-back-button class="ion-display-none"></ion-back-button>
<ion-back-button class="ion-hide"></ion-back-button>
</ion-buttons>
<ion-title>Hidden</ion-title>
</ion-toolbar>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -22,15 +22,15 @@ export type DatetimePresentation = 'date-time' | 'time-date' | 'date' | 'time' |
export type TitleSelectedDatesFormatter = (selectedDates: string[]) => string;
/**
* DatetimeHighlightStyle must include textColor, backgroundColor, or border.
* It cannot be an empty object.
*/
export type DatetimeHighlightStyle = {
textColor?: string;
backgroundColor?: string;
border?: string;
} & ({ textColor: string } | { backgroundColor: string } | { border: string });
export type DatetimeHighlightStyle =
| {
textColor: string;
backgroundColor?: string;
}
| {
textColor?: string;
backgroundColor: string;
};
export type DatetimeHighlight = { date: string } & DatetimeHighlightStyle;

View File

@@ -2335,7 +2335,6 @@ export class Datetime implements ComponentInterface {
`${dateStyle ? dateStyle.backgroundColor : ''}`,
'important'
);
el.style.setProperty('border', `${dateStyle ? dateStyle.border : ''}`, 'important');
}
}}
tabindex="-1"

View File

@@ -5,8 +5,6 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('datetime: custom'), () => {
test.beforeEach(async ({ page }) => {
await page.goto(`/src/components/datetime/test/custom`, config);
await page.locator('.datetime-ready').last().waitFor();
});
test('should allow styling wheel style datetimes', async ({ page }) => {
@@ -32,13 +30,6 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test('should allow styling calendar days in grid style datetimes', async ({ page }) => {
const datetime = page.locator('#custom-calendar-days');
// Wait for calendar days to be rendered
await page.waitForFunction(() => {
const datetime = document.querySelector('#custom-calendar-days');
const calendarDays = datetime?.shadowRoot?.querySelectorAll('.calendar-day');
return calendarDays && calendarDays.length > 0;
});
await expect(datetime).toHaveScreenshot(screenshot(`datetime-custom-calendar-days`));
});
});

View File

@@ -164,7 +164,7 @@
const customDatetime = document.querySelector('#custom-calendar-days');
// Mock the current day to always have the same screenshots
const mockToday = '2023-06-10T16:22:00.000Z';
const mockToday = '2023-06-10T16:22';
Date = class extends Date {
constructor(...args) {
if (args.length === 0) {

View File

@@ -22,23 +22,11 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
await expect(monthYearToggle).toContainText('January 2022');
// Click to open the picker
await monthYearToggle.click();
await page.waitForChanges();
// Wait for the picker to be open
await page.locator('.month-year-picker-open').waitFor();
// Wait a bit for the picker to fully load
await page.waitForTimeout(200);
const ionChange = await page.spyOnEvent('ionChange');
// Click on February
await monthColumnItems.filter({ hasText: 'February' }).click();
// Wait for changes
await ionChange.next();
// February
await monthColumnItems.nth(1).click();
await page.waitForChanges();
await expect(monthYearToggle).toContainText('February 2022');
@@ -50,23 +38,13 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
const datetime = page.locator('ion-datetime');
const ionChange = await page.spyOnEvent('ionChange');
// Click to open the picker
await monthYearToggle.click();
await page.waitForChanges();
// Wait for the picker to be open
await page.locator('.month-year-picker-open').waitFor();
// February
await monthColumnItems.nth(1).click();
// Wait a bit for the picker to fully load
await page.waitForTimeout(200);
// Click on February
await monthColumnItems.filter({ hasText: 'February' }).click();
// Wait for changes
await ionChange.next();
await page.waitForChanges();
await expect(ionChange).toHaveReceivedEventTimes(1);
await expect(datetime).toHaveJSProperty('value', '2022-02-28');
});

View File

@@ -21,19 +21,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
date: '2023-01-01', // ensure selected date style overrides highlight
textColor: '#800080',
backgroundColor: '#ffc0cb',
border: '2px solid purple',
},
{
date: '2023-01-02',
textColor: '#b22222',
backgroundColor: '#fa8072',
border: '2px solid purple',
},
{
date: '2023-01-03',
textColor: '#0000ff',
backgroundColor: '#add8e6',
border: '2px solid purple',
},
];
});
@@ -55,7 +52,6 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
return {
textColor: '#b22222',
backgroundColor: '#fa8072',
border: '2px solid purple',
};
}
@@ -63,7 +59,6 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
return {
textColor: '#800080',
backgroundColor: '#ffc0cb',
border: '2px solid purple',
};
}
@@ -71,7 +66,6 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
return {
textColor: '#0000ff',
backgroundColor: '#add8e6',
border: '2px solid purple',
};
}
@@ -83,7 +77,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
await expect(datetime).toHaveScreenshot(screenshot(`datetime-highlightedDates-callback`));
});
test('should render highlights correctly when only using only one color property', async ({ page }) => {
test('should render highlights correctly when only using one color or the other', async ({ page }) => {
const datetime = page.locator('ion-datetime');
await datetime.evaluate((el: HTMLIonDatetimeElement) => {
@@ -96,10 +90,6 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
date: '2023-01-03',
textColor: '#0000ff',
},
{
date: '2023-01-04',
border: '2px solid purple',
},
];
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -78,10 +78,6 @@
textColor: 'blue',
backgroundColor: 'lightblue',
},
{
date: '2023-01-07',
border: '2px dotted red',
},
];
document.querySelector('#withCallback').highlightedDates = (isoString) => {
@@ -107,7 +103,6 @@
date: new Date().toISOString().split('T')[0],
textColor: 'purple',
backgroundColor: 'pink',
border: '2px solid purple',
},
];
</script>

View File

@@ -207,7 +207,6 @@ export const getHighlightStyles = (
return {
textColor: matchingHighlight.textColor,
backgroundColor: matchingHighlight.backgroundColor,
border: matchingHighlight.border,
} as DatetimeHighlightStyle;
}
} else {

View File

@@ -2,7 +2,7 @@
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Infinite Scroll - Top</title>
<title>Infinite Scroll - Basic</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
@@ -18,7 +18,7 @@
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Infinite Scroll - Top</ion-title>
<ion-title>Infinite Scroll - Basic</ion-title>
</ion-toolbar>
</ion-header>
@@ -28,9 +28,9 @@
</ion-infinite-scroll-content>
</ion-infinite-scroll>
<div id="list"></div>
<button onclick="toggleInfiniteScroll()" class="expand">Toggle InfiniteScroll</button>
<ion-list id="list"></ion-list>
</ion-content>
</ion-app>
@@ -46,26 +46,17 @@
console.log('Loading data...');
await wait(500);
infiniteScroll.complete();
appendItems(true);
appendItems();
// Custom event consumed in the e2e tests
window.dispatchEvent(new CustomEvent('ionInfiniteComplete'));
console.log('Done');
});
function appendItems(newItems = false) {
const randomColor =
'#' +
Math.floor(Math.random() * 16777215)
.toString(16)
.padStart(6, '0');
function appendItems() {
for (var i = 0; i < 30; i++) {
const el = document.createElement('ion-item');
el.textContent = `Item ${1 + i}`;
if (newItems) {
el.style.borderLeft = `4px solid ${randomColor}`;
}
el.textContent = `${1 + i}`;
list.prepend(el);
}
}

View File

@@ -48,7 +48,6 @@ export class InputOTP implements ComponentInterface {
@State() private inputValues: string[] = [];
@State() hasFocus = false;
@State() private previousInputValues: string[] = [];
/**
* Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user.
@@ -337,7 +336,6 @@ export class InputOTP implements ComponentInterface {
});
// Update the value without emitting events
this.value = this.inputValues.join('');
this.previousInputValues = [...this.inputValues];
}
/**
@@ -527,12 +525,19 @@ export class InputOTP implements ComponentInterface {
}
/**
* Handles keyboard navigation for the OTP component.
* Handles keyboard navigation and input for the OTP component.
*
* Navigation:
* - Backspace: Clears current input and moves to previous box if empty
* - Arrow Left/Right: Moves focus between input boxes
* - Tab: Allows normal tab navigation between components
*
* Input Behavior:
* - Validates input against the allowed pattern
* - When entering a key in a filled box:
* - Shifts existing values right if there is room
* - Updates the value of the input group
* - Prevents default behavior to avoid automatic focus shift
*/
private onKeyDown = (index: number) => (event: KeyboardEvent) => {
const { length } = this;
@@ -590,32 +595,34 @@ export class InputOTP implements ComponentInterface {
// Let all tab events proceed normally
return;
}
// If the input box contains a value and the key being
// entered is a valid key for the input box update the value
// and shift the values to the right if there is room.
if (this.inputValues[index] && this.validKeyPattern.test(event.key)) {
if (!this.inputValues[length - 1]) {
for (let i = length - 1; i > index; i--) {
this.inputValues[i] = this.inputValues[i - 1];
this.inputRefs[i].value = this.inputValues[i] || '';
}
}
this.inputValues[index] = event.key;
this.inputRefs[index].value = event.key;
this.updateValue(event);
// Prevent default to avoid the browser from
// automatically moving the focus to the next input
event.preventDefault();
}
};
/**
* Processes all input scenarios for each input box.
*
* This function manages:
* 1. Autofill handling
* 2. Input validation
* 3. Full selection replacement or typing in an empty box
* 4. Inserting in the middle with available space (shifting)
* 5. Single character replacement
*/
private onInput = (index: number) => (event: InputEvent) => {
const { length, validKeyPattern } = this;
const input = event.target as HTMLInputElement;
const value = input.value;
const previousValue = this.previousInputValues[index] || '';
const value = (event.target as HTMLInputElement).value;
// 1. Autofill handling
// If the length of the value increases by more than 1 from the previous
// value, treat this as autofill. This is to prevent the case where the
// user is typing a single character into an input box containing a value
// as that will trigger this function with a value length of 2 characters.
const isAutofill = value.length - previousValue.length > 1;
if (isAutofill) {
// Distribute valid characters across input boxes
// If the value is longer than 1 character (autofill), split it into
// characters and filter out invalid ones
if (value.length > 1) {
const validChars = value
.split('')
.filter((char) => validKeyPattern.test(char))
@@ -632,10 +639,8 @@ export class InputOTP implements ComponentInterface {
});
}
for (let i = 0; i < length; i++) {
this.inputValues[i] = validChars[i] || '';
this.inputRefs[i].value = validChars[i] || '';
}
// Update the value of the input group and emit the input change event
this.value = validChars.join('');
this.updateValue(event);
// Focus the first empty input box or the last input box if all boxes
@@ -646,85 +651,23 @@ export class InputOTP implements ComponentInterface {
this.inputRefs[nextIndex]?.focus();
}, 20);
this.previousInputValues = [...this.inputValues];
return;
}
// 2. Input validation
// If the character entered is invalid (does not match the pattern),
// restore the previous value and exit
if (value.length > 0 && !validKeyPattern.test(value[value.length - 1])) {
input.value = this.inputValues[index] || '';
this.previousInputValues = [...this.inputValues];
// Only allow input if it matches the pattern
if (value.length > 0 && !validKeyPattern.test(value)) {
this.inputRefs[index].value = '';
this.inputValues[index] = '';
return;
}
// 3. Full selection replacement or typing in an empty box
// If the user selects all text in the input box and types, or if the
// input box is empty, replace only this input box. If the box is empty,
// move to the next box, otherwise stay focused on this box.
const isAllSelected = input.selectionStart === 0 && input.selectionEnd === value.length;
const isEmpty = !this.inputValues[index];
if (isAllSelected || isEmpty) {
this.inputValues[index] = value;
input.value = value;
this.updateValue(event);
this.focusNext(index);
this.previousInputValues = [...this.inputValues];
return;
}
// 4. Inserting in the middle with available space (shifting)
// If typing in a filled input box and there are empty boxes at the end,
// shift all values starting at the current box to the right, and insert
// the new character at the current box.
const hasAvailableBoxAtEnd = this.inputValues[this.inputValues.length - 1] === '';
if (this.inputValues[index] && hasAvailableBoxAtEnd && value.length === 2) {
// Get the inserted character (from event or by diffing value/previousValue)
let newChar = (event as InputEvent).data;
if (!newChar) {
newChar = value.split('').find((c, i) => c !== previousValue[i]) || value[value.length - 1];
}
// Validate the new character before shifting
if (!validKeyPattern.test(newChar)) {
input.value = this.inputValues[index] || '';
this.previousInputValues = [...this.inputValues];
return;
}
// Shift values right from the end to the insertion point
for (let i = this.inputValues.length - 1; i > index; i--) {
this.inputValues[i] = this.inputValues[i - 1];
this.inputRefs[i].value = this.inputValues[i] || '';
}
this.inputValues[index] = newChar;
this.inputRefs[index].value = newChar;
this.updateValue(event);
this.previousInputValues = [...this.inputValues];
return;
}
// 5. Single character replacement
// Handles replacing a single character in a box containing a value based
// on the cursor position. We need the cursor position to determine which
// character was the last character typed. For example, if the user types "2"
// in an input box with the cursor at the beginning of the value of "6",
// the value will be "26", but we want to grab the "2" as the last character
// typed.
const cursorPos = input.selectionStart ?? value.length;
const newCharIndex = cursorPos - 1;
const newChar = value[newCharIndex] ?? value[0];
// Check if the new character is valid before updating the value
if (!validKeyPattern.test(newChar)) {
input.value = this.inputValues[index] || '';
this.previousInputValues = [...this.inputValues];
return;
}
this.inputValues[index] = newChar;
input.value = newChar;
// For single character input, fill the current box
this.inputValues[index] = value;
this.updateValue(event);
this.previousInputValues = [...this.inputValues];
if (value.length > 0) {
this.focusNext(index);
}
};
/**
@@ -768,8 +711,12 @@ export class InputOTP implements ComponentInterface {
// Focus the next empty input after pasting
// If all boxes are filled, focus the last input
const nextEmptyIndex = validChars.length < length ? validChars.length : length - 1;
inputRefs[nextEmptyIndex]?.focus();
const nextEmptyIndex = validChars.length;
if (nextEmptyIndex < length) {
inputRefs[nextEmptyIndex]?.focus();
} else {
inputRefs[length - 1]?.focus();
}
};
/**

View File

@@ -442,67 +442,6 @@ configs({ modes: ['ios'] }).forEach(({ title, config }) => {
await verifyInputValues(inputOtp, ['1', '9', '3', '']);
});
test('should replace the last value when typing one more than the length', async ({ page }) => {
await page.setContent(`<ion-input-otp>Description</ion-input-otp>`, config);
const inputOtp = page.locator('ion-input-otp');
const firstInput = inputOtp.locator('input').first();
await firstInput.focus();
await page.keyboard.type('12345');
await verifyInputValues(inputOtp, ['1', '2', '3', '5']);
});
test('should replace the last value when typing one more than the length and the type is text', async ({
page,
}, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/30459',
});
await page.setContent(`<ion-input-otp type="text">Description</ion-input-otp>`, config);
const inputOtp = page.locator('ion-input-otp');
const firstInput = inputOtp.locator('input').first();
await firstInput.focus();
await page.keyboard.type('abcde');
await verifyInputValues(inputOtp, ['a', 'b', 'c', 'e']);
});
test('should not insert or shift when typing an invalid character before a number', async ({ page }) => {
await page.setContent(`<ion-input-otp value="12">Description</ion-input-otp>`, config);
const inputOtp = page.locator('ion-input-otp');
const firstInput = inputOtp.locator('input').first();
await firstInput.focus();
// Move cursor to the start of the first input
await firstInput.evaluate((el: HTMLInputElement) => el.setSelectionRange(0, 0));
await page.keyboard.type('w');
await verifyInputValues(inputOtp, ['1', '2', '', '']);
});
test('should not insert or shift when typing an invalid character after a number', async ({ page }) => {
await page.setContent(`<ion-input-otp value="12">Description</ion-input-otp>`, config);
const inputOtp = page.locator('ion-input-otp');
const firstInput = inputOtp.locator('input').first();
await firstInput.focus();
// Move cursor to the end of the first input
await firstInput.evaluate((el: HTMLInputElement) => el.setSelectionRange(1, 1));
await page.keyboard.type('w');
await verifyInputValues(inputOtp, ['1', '2', '', '']);
});
});
test.describe(title('input-otp: autofill functionality'), () => {
@@ -521,53 +460,6 @@ configs({ modes: ['ios'] }).forEach(({ title, config }) => {
await expect(lastInput).toBeFocused();
});
test('should handle autofill correctly when all characters are the same', async ({ page }) => {
await page.setContent(`<ion-input-otp>Description</ion-input-otp>`, config);
const firstInput = page.locator('ion-input-otp input').first();
await firstInput.focus();
await simulateAutofill(firstInput, '1111');
const inputOtp = page.locator('ion-input-otp');
await verifyInputValues(inputOtp, ['1', '1', '1', '1']);
const lastInput = page.locator('ion-input-otp input').last();
await expect(lastInput).toBeFocused();
});
test('should handle autofill correctly when length is 2', async ({ page }) => {
await page.setContent(`<ion-input-otp length="2">Description</ion-input-otp>`, config);
const firstInput = page.locator('ion-input-otp input').first();
await firstInput.focus();
await simulateAutofill(firstInput, '12');
const inputOtp = page.locator('ion-input-otp');
await verifyInputValues(inputOtp, ['1', '2']);
const lastInput = page.locator('ion-input-otp input').last();
await expect(lastInput).toBeFocused();
});
test('should handle autofill correctly when length is 2 after typing 1 character', async ({ page }) => {
await page.setContent(`<ion-input-otp length="2">Description</ion-input-otp>`, config);
await page.keyboard.type('1');
const secondInput = page.locator('ion-input-otp input').nth(1);
await secondInput.focus();
await simulateAutofill(secondInput, '22');
const inputOtp = page.locator('ion-input-otp');
await verifyInputValues(inputOtp, ['2', '2']);
const lastInput = page.locator('ion-input-otp input').last();
await expect(lastInput).toBeFocused();
});
test('should handle autofill correctly when it exceeds the length', async ({ page }) => {
await page.setContent(`<ion-input-otp>Description</ion-input-otp>`, config);

View File

@@ -95,8 +95,6 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
el.separators = [2, 3];
});
await page.waitForChanges();
await expect(await hasSeparatorAfter(page, 0)).toBe(false);
await expect(await hasSeparatorAfter(page, 1)).toBe(true);
await expect(await hasSeparatorAfter(page, 2)).toBe(true);

View File

@@ -618,5 +618,5 @@
*/
:host([disabled]) ::slotted(ion-input-password-toggle),
:host([readonly]) ::slotted(ion-input-password-toggle) {
visibility: hidden;
display: none;
}

View File

@@ -26,69 +26,5 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
const input = page.locator('ion-input');
await expect(input).toHaveScreenshot(screenshot(`input-disabled`));
});
test('should maintain consistent height when password toggle is hidden on disabled input', async ({
page,
}, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/29562',
});
await page.setContent(
`
<ion-input label="Password" type="password" value="password123">
<ion-input-password-toggle slot="end"></ion-input-password-toggle>
</ion-input>
`,
config
);
const input = page.locator('ion-input');
// Get the height when input is enabled
const enabledHeight = await input.boundingBox().then((box) => box?.height);
// Disable the input
await input.evaluate((el) => el.setAttribute('disabled', 'true'));
await page.waitForChanges();
// Get the height when input is disabled
const disabledHeight = await input.boundingBox().then((box) => box?.height);
// Verify heights are the same
expect(enabledHeight).toBe(disabledHeight);
});
test('should maintain consistent height when password toggle is hidden on readonly input', async ({
page,
}, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/29562',
});
await page.setContent(
`
<ion-input label="Password" type="password" value="password123">
<ion-input-password-toggle slot="end"></ion-input-password-toggle>
</ion-input>
`,
config
);
const input = page.locator('ion-input');
// Get the height when input is enabled
const enabledHeight = await input.boundingBox().then((box) => box?.height);
// Make the input readonly
await input.evaluate((el) => el.setAttribute('readonly', 'true'));
await page.waitForChanges();
// Get the height when input is readonly
const readonlyHeight = await input.boundingBox().then((box) => box?.height);
// Verify heights are the same
expect(enabledHeight).toBe(readonlyHeight);
});
});
});

View File

@@ -1,9 +1,6 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
/**
* This behavior does not vary across modes/directions
*/
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('item-sliding: async'), () => {
test.beforeEach(async ({ page }) => {
@@ -38,85 +35,5 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await expect(itemSlidingEl).toHaveClass(/item-sliding-active-slide/);
});
// NOTE: This test uses the CDN version of Ionic.
// If this test fails, it is likely due to a regression in the published package.
test('should not throw errors when adding multiple items with side="end" using the Ionic CDN', async ({
page,
}, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/29499',
});
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
page.on('pageerror', (error) => {
errors.push(error.message);
});
// This issue only happens when using a CDN version of Ionic
// so we need to use the CDN by passing the `importIonicFromCDN` option
// to setContent.
await page.setContent(
`
<ion-header>
<ion-toolbar>
<ion-title>Item Sliding</ion-title>
<ion-buttons slot="end">
<ion-button id="addItem" onclick="addItem()">ADD ITEM</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list id="list"></ion-list>
</ion-content>
<script>
let itemList = [];
function generateItem() {
const currentItem = itemList.length + 1;
const item = \`
<ion-item-sliding>
<ion-item>
<ion-label>Sliding Item \${currentItem}</ion-label>
</ion-item>
<ion-item-options side="end">
<ion-item-option>Delete</ion-item-option>
</ion-item-options>
</ion-item-sliding>
\`;
itemList.push(item);
return item;
}
function addItem() {
const list = document.getElementById('list');
list.innerHTML += generateItem();
const currentItem = itemList.length;
}
</script>
`,
{ ...config, importIonicFromCDN: true }
);
// Click the button enough times to reproduce the issue
const addButton = page.locator('#addItem');
await addButton.click();
await addButton.click();
await addButton.click();
await page.waitForChanges();
// Check that the items have been added
const items = page.locator('ion-item-sliding');
expect(await items.count()).toBe(3);
// Check that no errors have been logged
expect(errors.length).toBe(0);
});
});
});

View File

@@ -37,7 +37,6 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
@State() multipleInputs = false;
@State() focusable = true;
@State() isInteractive = false;
/**
* The color to use from your application's color palette.
@@ -173,12 +172,14 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
componentDidLoad() {
raf(() => {
this.setMultipleInputs();
this.setIsInteractive();
this.focusable = this.isFocusable();
});
}
private totalNestedInputs() {
// If the item contains multiple clickable elements and/or inputs, then the item
// should not have a clickable input cover over the entire item to prevent
// interfering with their individual click events
private setMultipleInputs() {
// The following elements have a clickable cover that is relative to the entire item
const covers = this.el.querySelectorAll('ion-checkbox, ion-datetime, ion-select, ion-radio');
@@ -192,19 +193,6 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
// The following elements should also stay clickable when an input with cover is present
const clickables = this.el.querySelectorAll('ion-router-link, ion-button, a, button');
return {
covers,
inputs,
clickables,
};
}
// If the item contains multiple clickable elements and/or inputs, then the item
// should not have a clickable input cover over the entire item to prevent
// interfering with their individual click events
private setMultipleInputs() {
const { covers, inputs, clickables } = this.totalNestedInputs();
// Check for multiple inputs to change the position of the input cover to relative
// for all of the covered inputs above
this.multipleInputs =
@@ -213,19 +201,6 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
(covers.length > 0 && this.isClickable());
}
private setIsInteractive() {
// If item contains any interactive children, set isInteractive to `true`
const { covers, inputs, clickables } = this.totalNestedInputs();
this.isInteractive = covers.length > 0 || inputs.length > 0 || clickables.length > 0;
}
// slot change listener updates state to reflect how/if item should be interactive
private updateInteractivityOnSlotChange = () => {
this.setIsInteractive();
this.setMultipleInputs();
};
// If the item contains an input including a checkbox, datetime, select, or radio
// then the item will have a clickable input cover that covers the item
// that should get the hover, focused and activated states UNLESS it has multiple
@@ -389,12 +364,12 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
disabled={disabled}
{...clickFn}
>
<slot name="start" onSlotchange={this.updateInteractivityOnSlotChange}></slot>
<slot name="start"></slot>
<div class="item-inner">
<div class="input-wrapper">
<slot onSlotchange={this.updateInteractivityOnSlotChange}></slot>
<slot></slot>
</div>
<slot name="end" onSlotchange={this.updateInteractivityOnSlotChange}></slot>
<slot name="end"></slot>
{showDetail && (
<ion-icon
icon={detailIcon}

View File

@@ -252,46 +252,5 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
await expect(list).toHaveScreenshot(screenshot(`item-inputs-div-with-inputs`));
});
test('should update interactivity state when elements are conditionally rendered', async ({ page }, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/29763',
});
await page.setContent(
`
<ion-list>
<ion-item>
<ion-label>Conditional Checkbox</ion-label>
</ion-item>
</ion-list>
`,
config
);
const item = page.locator('ion-item');
await page.evaluate(() => {
const item = document.querySelector('ion-item');
const checkbox = document.createElement('ion-checkbox');
item?.appendChild(checkbox);
});
await page.waitForChanges();
const checkbox = page.locator('ion-checkbox');
await expect(checkbox).not.toBeChecked();
// Test that clicking on the left edge of the item toggles the checkbox
await item.click({
position: {
x: 5,
y: 5,
},
});
await expect(checkbox).toBeChecked();
});
});
});

View File

@@ -84,7 +84,7 @@
}
function initGroup(group) {
var groupEl = document.getElementById(group.id);
groupEl.addEventListener('ionReorderEnd', function (ev) {
groupEl.addEventListener('ionItemReorder', function (ev) {
ev.detail.complete();
});
var groupItems = [];

View File

@@ -18,13 +18,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
const heading = page.locator('ion-menu h1');
await expect(heading).toHaveText('Open Menu');
/**
* Disable the 'scrollable-region-focusable' rule because this test
* is missing the required `ion-app` wrapper component. The `ion-app`
* wrapper provides the necessary focus management that allows the
* menu content to be focusable.
*/
const results = await new AxeBuilder({ page }).disableRules('scrollable-region-focusable').analyze();
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
});

View File

@@ -48,7 +48,7 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio
}
if (presentingEl) {
const isPortrait = window.innerWidth < 768;
const isMobile = window.innerWidth < 768;
const hasCardModal =
presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined;
const presentingElRoot = getElementRoot(presentingEl);
@@ -61,7 +61,7 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio
const bodyEl = document.body;
if (isPortrait) {
if (isMobile) {
/**
* Fallback for browsers that does not support `max()` (ex: Firefox)
* No need to worry about statusbar padding since engines like Gecko

View File

@@ -35,7 +35,7 @@ export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio
.addAnimation(wrapperAnimation);
if (presentingEl) {
const isPortrait = window.innerWidth < 768;
const isMobile = window.innerWidth < 768;
const hasCardModal =
presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined;
const presentingElRoot = getElementRoot(presentingEl);
@@ -61,7 +61,7 @@ export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio
const bodyEl = document.body;
if (isPortrait) {
if (isMobile) {
const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
const modalTransform = hasCardModal ? '-10px' : transformOffset;
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;

View File

@@ -1,188 +0,0 @@
import { createAnimation } from '@utils/animation/animation';
import { getElementRoot } from '@utils/helpers';
import type { Animation } from '../../../interface';
import { SwipeToCloseDefaults } from '../gestures/swipe-to-close';
import type { ModalAnimationOptions } from '../modal-interface';
/**
* Transition animation from portrait view to landscape view
* This handles the case where a card modal is open in portrait view
* and the user switches to landscape view
*/
export const portraitToLandscapeTransition = (
baseEl: HTMLElement,
opts: ModalAnimationOptions,
duration = 300
): Animation => {
const { presentingEl } = opts;
if (!presentingEl) {
// No transition needed for non-card modals
return createAnimation('portrait-to-landscape-transition');
}
const presentingElIsCardModal =
presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined;
const presentingElRoot = getElementRoot(presentingEl);
const bodyEl = document.body;
const baseAnimation = createAnimation('portrait-to-landscape-transition')
.addElement(baseEl)
.easing('cubic-bezier(0.32,0.72,0,1)')
.duration(duration);
const presentingAnimation = createAnimation().beforeStyles({
transform: 'translateY(0)',
'transform-origin': 'top center',
overflow: 'hidden',
});
if (!presentingElIsCardModal) {
// The presenting element is not a card modal, so we do not
// need to care about layering and modal-specific styles.
const root = getElementRoot(baseEl);
const wrapperAnimation = createAnimation()
.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')!)
.fromTo('opacity', '1', '1'); // Keep wrapper visible in landscape
const backdropAnimation = createAnimation()
.addElement(root.querySelector('ion-backdrop')!)
.fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
// Animate presentingEl from portrait state back to normal
const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
const fromTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
presentingAnimation
.addElement(presentingEl)
.afterStyles({
transform: 'translateY(0px) scale(1)',
'border-radius': '0px',
})
.beforeAddWrite(() => bodyEl.style.setProperty('background-color', ''))
.fromTo('transform', fromTransform, 'translateY(0px) scale(1)')
.fromTo('filter', 'contrast(0.85)', 'contrast(1)')
.fromTo('border-radius', '10px 10px 0 0', '0px');
baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
} else {
// The presenting element is a card modal, so we do
// need to care about layering and modal-specific styles.
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
const fromTransform = `translateY(-10px) scale(${toPresentingScale})`;
const toTransform = `translateY(0px) scale(1)`;
presentingAnimation
.addElement(presentingEl)
.afterStyles({
transform: toTransform,
})
.fromTo('transform', fromTransform, toTransform)
.fromTo('filter', 'contrast(0.85)', 'contrast(1)');
const shadowAnimation = createAnimation()
.addElement(presentingElRoot.querySelector('.modal-shadow')!)
.afterStyles({
transform: toTransform,
opacity: '0',
})
.fromTo('transform', fromTransform, toTransform);
baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
}
return baseAnimation;
};
/**
* Transition animation from landscape view to portrait view
* This handles the case where a card modal is open in landscape view
* and the user switches to portrait view
*/
export const landscapeToPortraitTransition = (
baseEl: HTMLElement,
opts: ModalAnimationOptions,
duration = 300
): Animation => {
const { presentingEl } = opts;
if (!presentingEl) {
// No transition needed for non-card modals
return createAnimation('landscape-to-portrait-transition');
}
const presentingElIsCardModal =
presentingEl.tagName === 'ION-MODAL' && (presentingEl as HTMLIonModalElement).presentingElement !== undefined;
const presentingElRoot = getElementRoot(presentingEl);
const bodyEl = document.body;
const baseAnimation = createAnimation('landscape-to-portrait-transition')
.addElement(baseEl)
.easing('cubic-bezier(0.32,0.72,0,1)')
.duration(duration);
const presentingAnimation = createAnimation().beforeStyles({
transform: 'translateY(0)',
'transform-origin': 'top center',
overflow: 'hidden',
});
if (!presentingElIsCardModal) {
// The presenting element is not a card modal, so we do not
// need to care about layering and modal-specific styles.
const root = getElementRoot(baseEl);
const wrapperAnimation = createAnimation()
.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')!)
.fromTo('opacity', '1', '1'); // Keep wrapper visible
const backdropAnimation = createAnimation()
.addElement(root.querySelector('ion-backdrop')!)
.fromTo('opacity', 'var(--backdrop-opacity)', 'var(--backdrop-opacity)'); // Keep backdrop visible
// Animate presentingEl from normal state to portrait state
const transformOffset = !CSS.supports('width', 'max(0px, 1px)') ? '30px' : 'max(30px, var(--ion-safe-area-top))';
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
const toTransform = `translateY(${transformOffset}) scale(${toPresentingScale})`;
presentingAnimation
.addElement(presentingEl)
.afterStyles({
transform: toTransform,
})
.beforeAddWrite(() => bodyEl.style.setProperty('background-color', 'black'))
.keyframes([
{ offset: 0, transform: 'translateY(0px) scale(1)', filter: 'contrast(1)', borderRadius: '0px' },
{ offset: 0.2, transform: 'translateY(0px) scale(1)', filter: 'contrast(1)', borderRadius: '10px 10px 0 0' },
{ offset: 1, transform: toTransform, filter: 'contrast(0.85)', borderRadius: '10px 10px 0 0' },
]);
baseAnimation.addAnimation([presentingAnimation, wrapperAnimation, backdropAnimation]);
} else {
// The presenting element is also a card modal, so we need
// to handle layering and modal-specific styles.
const toPresentingScale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
const fromTransform = `translateY(-10px) scale(${toPresentingScale})`;
const toTransform = `translateY(0) scale(1)`;
presentingAnimation
.addElement(presentingEl)
.afterStyles({
transform: toTransform,
})
.fromTo('transform', fromTransform, toTransform);
const shadowAnimation = createAnimation()
.addElement(presentingElRoot.querySelector('.modal-shadow')!)
.afterStyles({
transform: toTransform,
opacity: '0',
})
.fromTo('transform', fromTransform, toTransform);
baseAnimation.addAnimation([presentingAnimation, shadowAnimation]);
}
return baseAnimation;
};

View File

@@ -1,8 +1,8 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Listen, Method, Prop, State, Watch, h, writeTask } from '@stencil/core';
import { Component, Element, Event, Host, Method, Prop, State, Watch, h, writeTask } from '@stencil/core';
import { findIonContent, printIonContentErrorMsg } from '@utils/content';
import { CoreDelegate, attachComponent, detachComponent } from '@utils/framework-delegate';
import { raf, inheritAttributes, hasLazyBuild, getElementRoot } from '@utils/helpers';
import { raf, inheritAttributes, hasLazyBuild } from '@utils/helpers';
import type { Attributes } from '@utils/helpers';
import { createLockController } from '@utils/lock-controller';
import { printIonWarning } from '@utils/logging';
@@ -37,12 +37,11 @@ import type { OverlayEventDetail } from '../../utils/overlays-interface';
import { iosEnterAnimation } from './animations/ios.enter';
import { iosLeaveAnimation } from './animations/ios.leave';
import { portraitToLandscapeTransition, landscapeToPortraitTransition } from './animations/ios.transition';
import { mdEnterAnimation } from './animations/md.enter';
import { mdLeaveAnimation } from './animations/md.leave';
import type { MoveSheetToBreakpointOptions } from './gestures/sheet';
import { createSheetGesture } from './gestures/sheet';
import { createSwipeToCloseGesture, SwipeToCloseDefaults } from './gestures/swipe-to-close';
import { createSwipeToCloseGesture } from './gestures/swipe-to-close';
import type { ModalBreakpointChangeEventDetail, ModalHandleBehavior } from './modal-interface';
import { setCardStatusBarDark, setCardStatusBarDefault } from './utils';
@@ -91,16 +90,6 @@ export class Modal implements ComponentInterface, OverlayInterface {
// Whether or not modal is being dismissed via gesture
private gestureAnimationDismissing = false;
// View transition properties for handling portrait/landscape switches
private currentViewIsPortrait?: boolean;
private viewTransitionAnimation?: Animation;
private resizeTimeout?: any;
// Mutation observer to watch for parent removal
private parentRemovalObserver?: MutationObserver;
// Cached original parent from before modal is moved to body during presentation
private cachedOriginalParent?: HTMLElement;
lastFocus?: HTMLElement;
animation?: Animation;
@@ -272,19 +261,6 @@ export class Modal implements ComponentInterface, OverlayInterface {
}
}
@Listen('resize', { target: 'window' })
onWindowResize() {
// Only handle resize for iOS card modals when no custom animations are provided
if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
return;
}
clearTimeout(this.resizeTimeout);
this.resizeTimeout = setTimeout(() => {
this.handleViewTransition();
}, 50); // Debounce to avoid excessive calls during active resizing
}
/**
* If `true`, the component passed into `ion-modal` will
* automatically be mounted when the modal is created. The
@@ -402,8 +378,6 @@ export class Modal implements ComponentInterface, OverlayInterface {
disconnectedCallback() {
this.triggerController.removeClickListener();
this.cleanupViewTransitionListener();
this.cleanupParentRemovalObserver();
}
componentWillLoad() {
@@ -413,11 +387,6 @@ export class Modal implements ComponentInterface, OverlayInterface {
const attributesToInherit = ['aria-label', 'role'];
this.inheritedAttributes = inheritAttributes(el, attributesToInherit);
// Cache original parent before modal gets moved to body during presentation
if (el.parentNode) {
this.cachedOriginalParent = el.parentNode as HTMLElement;
}
/**
* When using a controller modal you can set attributes
* using the htmlAttributes property. Since the above attributes
@@ -650,12 +619,6 @@ export class Modal implements ComponentInterface, OverlayInterface {
this.initSwipeToClose();
}
// Initialize view transition listener for iOS card modals
this.initViewTransitionListener();
// Initialize parent removal observer
this.initParentRemovalObserver();
unlock();
}
@@ -798,13 +761,6 @@ export class Modal implements ComponentInterface, OverlayInterface {
*/
const unlock = await this.lockController.lock();
/**
* Dismiss all child modals. This is especially important in
* Angular and React because it's possible to lose control of a child
* modal when the parent modal is dismissed.
*/
await this.dismissNestedModals();
/**
* If a canDismiss handler is responsible
* for calling the dismiss method, we should
@@ -860,8 +816,6 @@ export class Modal implements ComponentInterface, OverlayInterface {
if (this.gesture) {
this.gesture.destroy();
}
this.cleanupViewTransitionListener();
this.cleanupParentRemovalObserver();
}
this.currentBreakpoint = undefined;
this.animation = undefined;
@@ -1009,217 +963,6 @@ export class Modal implements ComponentInterface, OverlayInterface {
}
};
private initViewTransitionListener() {
// Only enable for iOS card modals when no custom animations are provided
if (getIonMode(this) !== 'ios' || !this.presentingElement || this.enterAnimation || this.leaveAnimation) {
return;
}
// Set initial view state
this.currentViewIsPortrait = window.innerWidth < 768;
}
private handleViewTransition() {
const isPortrait = window.innerWidth < 768;
// Only transition if view state actually changed
if (this.currentViewIsPortrait === isPortrait) {
return;
}
// Cancel any ongoing transition animation
if (this.viewTransitionAnimation) {
this.viewTransitionAnimation.destroy();
this.viewTransitionAnimation = undefined;
}
const { presentingElement } = this;
if (!presentingElement) {
return;
}
// Create transition animation
let transitionAnimation: Animation;
if (this.currentViewIsPortrait && !isPortrait) {
// Portrait to landscape transition
transitionAnimation = portraitToLandscapeTransition(this.el, {
presentingEl: presentingElement,
currentBreakpoint: this.currentBreakpoint,
backdropBreakpoint: this.backdropBreakpoint,
expandToScroll: this.expandToScroll,
});
} else {
// Landscape to portrait transition
transitionAnimation = landscapeToPortraitTransition(this.el, {
presentingEl: presentingElement,
currentBreakpoint: this.currentBreakpoint,
backdropBreakpoint: this.backdropBreakpoint,
expandToScroll: this.expandToScroll,
});
}
// Update state and play animation
this.currentViewIsPortrait = isPortrait;
this.viewTransitionAnimation = transitionAnimation;
transitionAnimation.play().then(() => {
this.viewTransitionAnimation = undefined;
// After orientation transition, recreate the swipe-to-close gesture
// with updated animation that reflects the new presenting element state
this.reinitSwipeToClose();
});
}
private cleanupViewTransitionListener() {
// Clear any pending resize timeout
if (this.resizeTimeout) {
clearTimeout(this.resizeTimeout);
this.resizeTimeout = undefined;
}
if (this.viewTransitionAnimation) {
this.viewTransitionAnimation.destroy();
this.viewTransitionAnimation = undefined;
}
}
private reinitSwipeToClose() {
// Only reinitialize if we have a presenting element and are on iOS
if (getIonMode(this) !== 'ios' || !this.presentingElement) {
return;
}
// Clean up existing gesture and animation
if (this.gesture) {
this.gesture.destroy();
this.gesture = undefined;
}
if (this.animation) {
// Properly end the progress-based animation at initial state before destroying
// to avoid leaving modal in intermediate swipe position
this.animation.progressEnd(0, 0, 0);
this.animation.destroy();
this.animation = undefined;
}
// Force the modal back to the correct position or it could end up
// in a weird state after destroying the animation
raf(() => {
this.ensureCorrectModalPosition();
this.initSwipeToClose();
});
}
private ensureCorrectModalPosition() {
const { el, presentingElement } = this;
const root = getElementRoot(el);
const wrapperEl = root.querySelector('.modal-wrapper') as HTMLElement | null;
if (wrapperEl) {
wrapperEl.style.transform = 'translateY(0vh)';
wrapperEl.style.opacity = '1';
}
if (presentingElement?.tagName === 'ION-MODAL') {
const isPortrait = window.innerWidth < 768;
if (isPortrait) {
const transformOffset = !CSS.supports('width', 'max(0px, 1px)')
? '30px'
: 'max(30px, var(--ion-safe-area-top))';
const scale = SwipeToCloseDefaults.MIN_PRESENTING_SCALE;
presentingElement.style.transform = `translateY(${transformOffset}) scale(${scale})`;
} else {
presentingElement.style.transform = 'translateY(0px) scale(1)';
}
}
}
/**
* When the slot changes, we need to find all the modals in the slot
* and set the data-parent-ion-modal attribute on them so we can find them
* and dismiss them when we get dismissed.
* We need to do it this way because when a modal is opened, it's moved to
* the end of the body and is no longer an actual child of the modal.
*/
private onSlotChange = ({ target }: Event) => {
const slot = target as HTMLSlotElement;
slot.assignedElements().forEach((el) => {
el.querySelectorAll('ion-modal').forEach((childModal) => {
// We don't need to write to the DOM if the modal is already tagged
// If this is a deeply nested modal, this effect should cascade so we don't
// need to worry about another modal claiming the same child.
if (childModal.getAttribute('data-parent-ion-modal') === null) {
childModal.setAttribute('data-parent-ion-modal', this.el.id);
}
});
});
};
private async dismissNestedModals(): Promise<void> {
const nestedModals = document.querySelectorAll(`ion-modal[data-parent-ion-modal="${this.el.id}"]`);
nestedModals?.forEach(async (modal) => {
await (modal as HTMLIonModalElement).dismiss(undefined, 'parent-dismissed');
});
}
private initParentRemovalObserver() {
if (typeof MutationObserver === 'undefined') {
return;
}
// Only observe if we have a cached parent and are in browser environment
if (typeof window === 'undefined' || !this.cachedOriginalParent) {
return;
}
// Don't observe document or fragment nodes as they can't be "removed"
if (
this.cachedOriginalParent.nodeType === Node.DOCUMENT_NODE ||
this.cachedOriginalParent.nodeType === Node.DOCUMENT_FRAGMENT_NODE
) {
return;
}
this.parentRemovalObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.removedNodes.length > 0) {
// Check if our cached original parent was removed
const cachedParentWasRemoved = Array.from(mutation.removedNodes).some((node) => {
const isDirectMatch = node === this.cachedOriginalParent;
const isContainedMatch = this.cachedOriginalParent
? (node as HTMLElement).contains?.(this.cachedOriginalParent)
: false;
return isDirectMatch || isContainedMatch;
});
// Also check if parent is no longer connected to DOM
const cachedParentDisconnected = this.cachedOriginalParent && !this.cachedOriginalParent.isConnected;
if (cachedParentWasRemoved || cachedParentDisconnected) {
this.dismiss(undefined, 'parent-removed');
// Release the reference to the cached original parent
// so we don't have a memory leak
this.cachedOriginalParent = undefined;
}
}
});
});
// Observe document body with subtree to catch removals at any level
this.parentRemovalObserver.observe(document.body, {
childList: true,
subtree: true,
});
}
private cleanupParentRemovalObserver() {
this.parentRemovalObserver?.disconnect();
this.parentRemovalObserver = undefined;
}
render() {
const {
handle,
@@ -1297,7 +1040,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
ref={(el) => (this.dragHandleEl = el)}
></button>
)}
<slot onSlotchange={this.onSlotChange}></slot>
<slot></slot>
</div>
</Host>
);

View File

@@ -22,91 +22,31 @@
</ion-header>
<ion-content class="ion-padding">
<div id="modal-container">
<button id="open-inline-modal" onclick="openModal(event)">Open Modal</button>
<ion-modal swipe-to-close="true">
<ion-header>
<ion-toolbar>
<ion-title> Modal </ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<p>This is my inline modal content!</p>
<button id="open-child-modal" onclick="openChildModal(event)">Open Child Modal</button>
<button id="remove-modal-container" onclick="removeModalContainer(event)">
Remove Modal Container
</button>
<button id="open-inline-modal" onclick="openModal(event)">Open Modal</button>
<ion-modal id="child-modal" swipe-to-close="true">
<ion-header>
<ion-toolbar>
<ion-title>Child Modal</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<p>This is the child modal content!</p>
<p>When the parent modal is dismissed, this child modal should also be dismissed automatically.</p>
<button id="dismiss-parent" onclick="dismissParent(event)">Dismiss Parent Modal</button>
<button id="dismiss-child" onclick="dismissChild(event)">Dismiss Child Modal</button>
<button id="child-remove-modal-container" onclick="removeModalContainer(event)">
Remove Modal Container
</button>
</ion-content>
</ion-modal>
</ion-content>
</ion-modal>
</div>
<ion-modal swipe-to-close="true">
<ion-header>
<ion-toolbar>
<ion-title> Modal </ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding"> This is my inline modal content! </ion-content>
</ion-modal>
</ion-content>
</div>
</ion-app>
<script>
const modal = document.querySelector('ion-modal');
const childModal = document.querySelector('#child-modal');
modal.presentingElement = document.querySelector('.ion-page');
childModal.presentingElement = modal;
const openModal = () => {
modal.isOpen = true;
};
const openChildModal = () => {
childModal.isOpen = true;
};
const dismissParent = () => {
modal.isOpen = false;
};
const dismissChild = () => {
childModal.isOpen = false;
};
const removeModalContainer = () => {
const container = document.querySelector('#modal-container');
if (container) {
container.remove();
console.log('Modal container removed from DOM');
}
};
modal.addEventListener('didDismiss', () => {
modal.isOpen = false;
});
childModal.addEventListener('didDismiss', () => {
childModal.isOpen = false;
});
// Add event listeners to demonstrate the new functionality
modal.addEventListener('ionModalDidDismiss', (event) => {
console.log('Parent modal dismissed with role:', event.detail.role);
});
childModal.addEventListener('ionModalDidDismiss', (event) => {
console.log('Child modal dismissed with role:', event.detail.role);
});
</script>
</body>
</html>

View File

@@ -7,7 +7,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await page.goto('/src/components/modal/test/inline', config);
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
const modal = page.locator('ion-modal').first();
const modal = page.locator('ion-modal');
await page.click('#open-inline-modal');
@@ -22,67 +22,6 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await expect(modal).toBeHidden();
});
test('it should dismiss child modals when parent modal is dismissed', async ({ page }) => {
await page.goto('/src/components/modal/test/inline', config);
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
const parentModal = page.locator('ion-modal').first();
const childModal = page.locator('#child-modal');
// Open the parent modal
await page.click('#open-inline-modal');
await ionModalDidPresent.next();
await expect(parentModal).toBeVisible();
// Open the child modal
await page.click('#open-child-modal');
await ionModalDidPresent.next();
await expect(childModal).toBeVisible();
// Both modals should be visible
await expect(parentModal).toBeVisible();
await expect(childModal).toBeVisible();
// Dismiss the parent modal
await page.click('#dismiss-parent');
// Wait for both modals to be dismissed
await ionModalDidDismiss.next(); // child modal dismissed
await ionModalDidDismiss.next(); // parent modal dismissed
// Both modals should be hidden
await expect(parentModal).toBeHidden();
await expect(childModal).toBeHidden();
});
test('it should only dismiss child modal when child dismiss button is clicked', async ({ page }) => {
await page.goto('/src/components/modal/test/inline', config);
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
const parentModal = page.locator('ion-modal').first();
const childModal = page.locator('#child-modal');
// Open the parent modal
await page.click('#open-inline-modal');
await ionModalDidPresent.next();
await expect(parentModal).toBeVisible();
// Open the child modal
await page.click('#open-child-modal');
await ionModalDidPresent.next();
await expect(childModal).toBeVisible();
// Dismiss only the child modal
await page.click('#dismiss-child');
await ionModalDidDismiss.next();
// Parent modal should still be visible, child modal should be hidden
await expect(parentModal).toBeVisible();
await expect(childModal).toBeHidden();
});
test('presenting should create a single root element with the ion-page class', async ({ page }, testInfo) => {
testInfo.annotations.push({
type: 'issue',
@@ -122,152 +61,5 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await modal.evaluate((el: HTMLIonModalElement) => el.firstElementChild!.firstElementChild!.className)
).not.toContain('ion-page');
});
test('it should dismiss modal when parent container is removed from DOM', async ({ page }) => {
await page.goto('/src/components/modal/test/inline', config);
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
const modal = page.locator('ion-modal').first();
const modalContainer = page.locator('#modal-container');
// Open the modal
await page.click('#open-inline-modal');
await ionModalDidPresent.next();
await expect(modal).toBeVisible();
// Remove the modal container from DOM
await page.click('#remove-modal-container');
// Wait for modal to be dismissed
const dismissEvent = await ionModalDidDismiss.next();
// Verify the modal was dismissed with the correct role
expect(dismissEvent.detail.role).toBe('parent-removed');
// Verify the modal is no longer visible
await expect(modal).toBeHidden();
// Verify the container was actually removed
await expect(modalContainer).not.toBeAttached();
});
test('it should dismiss both parent and child modals when parent container is removed from DOM', async ({
page,
}) => {
await page.goto('/src/components/modal/test/inline', config);
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
const parentModal = page.locator('ion-modal').first();
const childModal = page.locator('#child-modal');
const modalContainer = page.locator('#modal-container');
// Open the parent modal
await page.click('#open-inline-modal');
await ionModalDidPresent.next();
await expect(parentModal).toBeVisible();
// Open the child modal
await page.click('#open-child-modal');
await ionModalDidPresent.next();
await expect(childModal).toBeVisible();
// Remove the modal container from DOM
await page.click('#child-remove-modal-container');
// Wait for both modals to be dismissed
const firstDismissEvent = await ionModalDidDismiss.next();
const secondDismissEvent = await ionModalDidDismiss.next();
// Verify at least one modal was dismissed with 'parent-removed' role
const dismissRoles = [firstDismissEvent.detail.role, secondDismissEvent.detail.role];
expect(dismissRoles).toContain('parent-removed');
// Verify both modals are no longer visible
await expect(parentModal).toBeHidden();
await expect(childModal).toBeHidden();
// Verify the container was actually removed
await expect(modalContainer).not.toBeAttached();
});
test('it should dismiss modals when top-level ancestor is removed', async ({ page }) => {
// We need to make sure we can close a modal when a much higher
// element is removed from the DOM. This will be a common
// use case in frameworks like Angular and React, where an entire
// page container for much more than the modal might be swapped out.
await page.setContent(
`
<ion-app>
<div class="ion-page">
<ion-header>
<ion-toolbar>
<ion-title>Top Level Removal Test</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div id="top-level-container">
<div id="nested-container">
<button id="open-nested-modal">Open Nested Modal</button>
<ion-modal id="nested-modal">
<ion-header>
<ion-toolbar>
<ion-title>Nested Modal</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<p>This modal's original parent is deeply nested</p>
<button id="remove-top-level">Remove Top Level Container</button>
</ion-content>
</ion-modal>
</div>
</div>
</ion-content>
</div>
</ion-app>
<script>
const nestedModal = document.querySelector('#nested-modal');
nestedModal.presentingElement = document.querySelector('.ion-page');
document.getElementById('open-nested-modal').addEventListener('click', () => {
nestedModal.isOpen = true;
});
document.getElementById('remove-top-level').addEventListener('click', () => {
document.querySelector('#top-level-container').remove();
});
</script>
`,
config
);
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
const nestedModal = page.locator('#nested-modal');
const topLevelContainer = page.locator('#top-level-container');
// Open the nested modal
await page.click('#open-nested-modal');
await ionModalDidPresent.next();
await expect(nestedModal).toBeVisible();
// Remove the top-level container
await page.click('#remove-top-level');
// Wait for modal to be dismissed
const dismissEvent = await ionModalDidDismiss.next();
// Verify the modal was dismissed with the correct role
expect(dismissEvent.detail.role).toBe('parent-removed');
// Verify the modal is no longer visible
await expect(nestedModal).toBeHidden();
// Verify the container was actually removed
await expect(topLevelContainer).not.toBeAttached();
});
});
});

View File

@@ -107,7 +107,6 @@ const renderProgress = (value: number, buffer: number) => {
* When finalBuffer === 1, we use display: none
* instead of removing the element to avoid flickering.
*/
// TODO(FW-6697): change `ion-hide` class to `ion-display-none` or another class
<div
class={{ 'buffer-circles-container': true, 'ion-hide': finalBuffer === 1 }}
style={{ transform: `translateX(${finalBuffer * 100}%)` }}

View File

@@ -1,33 +1,10 @@
// TODO(FW-6590): Remove this once the deprecated event is removed
export interface ItemReorderEventDetail {
from: number;
to: number;
complete: (data?: boolean | any[]) => any;
}
// TODO(FW-6590): Remove this once the deprecated event is removed
export interface ItemReorderCustomEvent extends CustomEvent {
detail: ItemReorderEventDetail;
target: HTMLIonReorderGroupElement;
}
export interface ReorderMoveEventDetail {
from: number;
to: number;
}
export interface ReorderEndEventDetail {
from: number;
to: number;
complete: (data?: boolean | any[]) => any;
}
export interface ReorderMoveCustomEvent extends CustomEvent {
detail: ReorderMoveEventDetail;
target: HTMLIonReorderGroupElement;
}
export interface ReorderEndCustomEvent extends CustomEvent {
detail: ReorderEndEventDetail;
target: HTMLIonReorderGroupElement;
}

View File

@@ -6,9 +6,8 @@ import { hapticSelectionChanged, hapticSelectionEnd, hapticSelectionStart } from
import { getIonMode } from '../../global/ionic-global';
import type { Gesture, GestureDetail } from '../../interface';
import type { HTMLStencilElement } from '../../utils/element-interface';
import type { ItemReorderEventDetail, ReorderMoveEventDetail, ReorderEndEventDetail } from './reorder-group-interface';
import type { ItemReorderEventDetail } from './reorder-group-interface';
// TODO(FW-2832): types
@@ -39,7 +38,7 @@ export class ReorderGroup implements ComponentInterface {
@State() state = ReorderGroupState.Idle;
@Element() el!: HTMLStencilElement;
@Element() el!: HTMLElement;
/**
* If `true`, the reorder will be hidden.
@@ -52,35 +51,12 @@ export class ReorderGroup implements ComponentInterface {
}
}
// TODO(FW-6590): Remove this in a major release.
/**
* Event that needs to be listened to in order to complete the reorder action.
* @deprecated Use `ionReorderEnd` instead. If you are accessing
* `event.detail.from` or `event.detail.to` and relying on them
* being different you should now add checks as they are always emitted
* in `ionReorderEnd`, even when they are the same.
*/
@Event() ionItemReorder!: EventEmitter<ItemReorderEventDetail>;
/**
* Event that is emitted when the reorder gesture starts.
*/
@Event() ionReorderStart!: EventEmitter<void>;
/**
* Event that is emitted as the reorder gesture moves.
*/
@Event() ionReorderMove!: EventEmitter<ReorderMoveEventDetail>;
/**
* Event that is emitted when the reorder gesture ends.
* The from and to properties are always available, regardless of
* if the reorder gesture moved the item. If the item did not change
* from its start position, the from and to properties will be the same.
* Once the event has been emitted, the `complete()` method then needs
* to be called in order to finalize the reorder action.
*/
@Event() ionReorderEnd!: EventEmitter<ReorderEndEventDetail>;
@Event() ionItemReorder!: EventEmitter<ItemReorderEventDetail>;
async connectedCallback() {
const contentEl = findClosestIonContent(this.el);
@@ -112,8 +88,7 @@ export class ReorderGroup implements ComponentInterface {
}
/**
* Completes the reorder operation. Must be called by the `ionReorderEnd` event.
*
* Completes the reorder operation. Must be called by the `ionItemReorder` event.
* If a list of items is passed, the list will be reordered and returned in the
* proper order.
*
@@ -153,7 +128,7 @@ export class ReorderGroup implements ComponentInterface {
const heights = this.cachedHeights;
heights.length = 0;
const el = this.el;
const children: any = el.__children || el.children;
const children: any = el.children;
if (!children || children.length === 0) {
return;
}
@@ -188,8 +163,6 @@ export class ReorderGroup implements ComponentInterface {
item.classList.add(ITEM_REORDER_SELECTED);
hapticSelectionStart();
this.ionReorderStart.emit();
}
private onMove(ev: GestureDetail) {
@@ -206,7 +179,6 @@ export class ReorderGroup implements ComponentInterface {
const currentY = Math.max(top, Math.min(ev.currentY, bottom));
const deltaY = scroll + currentY - ev.startY;
const normalizedY = currentY - top;
const fromIndex = this.lastToIndex;
const toIndex = this.itemIndexForTop(normalizedY);
if (toIndex !== this.lastToIndex) {
const fromIndex = indexForItem(selectedItem);
@@ -218,11 +190,6 @@ export class ReorderGroup implements ComponentInterface {
// Update selected item position
selectedItem.style.transform = `translateY(${deltaY}px)`;
this.ionReorderMove.emit({
from: fromIndex,
to: toIndex,
});
}
private onEnd() {
@@ -239,7 +206,6 @@ export class ReorderGroup implements ComponentInterface {
if (toIndex === fromIndex) {
this.completeReorder();
} else {
// TODO(FW-6590): Remove this once the deprecated event is removed
this.ionItemReorder.emit({
from: fromIndex,
to: toIndex,
@@ -248,18 +214,12 @@ export class ReorderGroup implements ComponentInterface {
}
hapticSelectionEnd();
this.ionReorderEnd.emit({
from: fromIndex,
to: toIndex,
complete: this.completeReorder.bind(this),
});
}
private completeReorder(listOrReorder?: boolean | any[]): any {
const selectedItemEl = this.selectedItemEl;
if (selectedItemEl && this.state === ReorderGroupState.Complete) {
const children: any = this.el.__children || this.el.children;
const children = this.el.children as any;
const len = children.length;
const toIndex = this.lastToIndex;
const fromIndex = indexForItem(selectedItemEl);
@@ -309,7 +269,7 @@ export class ReorderGroup implements ComponentInterface {
/********* DOM WRITE ********* */
private reorderMove(fromIndex: number, toIndex: number) {
const itemHeight = this.selectedItemHeight;
const children: any = this.el.__children || this.el.children;
const children = this.el.children;
for (let i = 0; i < children.length; i++) {
const style = (children[i] as any).style;
let value = '';

View File

@@ -122,25 +122,8 @@
const reorderGroup = document.getElementById('reorder');
reorderGroup.disabled = !reorderGroup.disabled;
// TODO(FW-6590): Remove this once the deprecated event is removed
reorderGroup.addEventListener('ionItemReorder', ({ detail }) => {
console.log('ionItemReorder: Dragged from index', detail.from, 'to', detail.to);
});
reorderGroup.addEventListener('ionReorderStart', () => {
console.log('ionReorderStart');
});
reorderGroup.addEventListener('ionReorderMove', ({ detail }) => {
console.log('ionReorderMove: Dragged from index', detail.from, 'to', detail.to);
});
reorderGroup.addEventListener('ionReorderEnd', ({ detail }) => {
if (detail.from !== detail.to) {
console.log('ionReorderEnd: Dragged from index', detail.from, 'to', detail.to);
} else {
console.log('ionReorderEnd: No position change occurred');
}
console.log('Dragged from index', detail.from, 'to', detail.to);
detail.complete();
});

View File

@@ -14,7 +14,7 @@
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<body onLoad="render()">
<ion-app>
<ion-header>
<ion-toolbar>
@@ -24,7 +24,7 @@
<ion-content id="content">
<ion-list>
<ion-reorder-group disabled="false">
<ion-reorder-group id="reorderGroup" disabled="false">
<!-- items will be inserted here -->
</ion-reorder-group>
</ion-list>
@@ -36,44 +36,27 @@
for (var i = 0; i < 30; i++) {
items.push(i + 1);
}
const reorderGroup = document.querySelector('ion-reorder-group');
reorderItems(items);
const reorderGroup = document.getElementById('reorderGroup');
function render() {
let html = '';
for (let item of items) {
html += `
<ion-item>
<ion-label>${item}</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>`;
}
reorderGroup.innerHTML = html;
}
reorderGroup.addEventListener('ionItemReorder', ({ detail }) => {
console.log('Dragged from index', detail.from, 'to', detail.to);
reorderGroup.addEventListener('ionReorderEnd', ({ detail }) => {
// Before complete is called with the items they will remain in the
// order before the drag
console.log('Before complete', items);
// Finish the reorder and position the item in the DOM based on
// where the gesture ended. Update the items variable to the
// new order of items
items = detail.complete(items);
// Reorder the items in the DOM
reorderItems(items);
// After complete is called the items will be in the new order
console.log('After complete', items);
});
function reorderItems(items) {
reorderGroup.replaceChildren();
let reordered = '';
for (let i = 0; i < items.length; i++) {
reordered += `
<ion-item>
<ion-label>
Item ${items[i]}
</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
`;
}
reorderGroup.innerHTML = reordered;
}
</script>
</body>
</html>

View File

@@ -37,9 +37,9 @@
</ion-reorder-group>
<script>
const group = document.querySelector('ion-reorder-group');
group.addEventListener('ionReorderEnd', (ev) => {
group.addEventListener('ionItemReorder', (ev) => {
ev.detail.complete();
window.dispatchEvent(new CustomEvent('ionReorderComplete'));
window.dispatchEvent(new CustomEvent('ionItemReorderComplete'));
});
</script>
</body>

View File

@@ -11,24 +11,24 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
});
test('should drag and drop when ion-reorder wraps ion-item', async ({ page }) => {
const items = page.locator('ion-item');
const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete');
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
await dragElementBy(items.nth(1), page, 0, 300);
await ionReorderComplete.next();
await ionItemReorderComplete.next();
await expect(items).toContainText(['Item 1', 'Item 3', 'Item 4', 'Item 2']);
});
test('should drag and drop when ion-item wraps ion-reorder', async ({ page }) => {
const reorderHandle = page.locator('ion-reorder');
const items = page.locator('ion-item');
const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete');
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
await dragElementBy(reorderHandle.nth(0), page, 0, 300);
await ionReorderComplete.next();
await ionItemReorderComplete.next();
await expect(items).toContainText(['Item 2', 'Item 3', 'Item 4', 'Item 1']);
});

View File

@@ -68,9 +68,9 @@
customElements.define('app-reorder', AppReorder);
const group = document.querySelector('ion-reorder-group');
group.addEventListener('ionReorderEnd', (ev) => {
group.addEventListener('ionItemReorder', (ev) => {
ev.detail.complete();
window.dispatchEvent(new CustomEvent('ionReorderComplete'));
window.dispatchEvent(new CustomEvent('ionItemReorderComplete'));
});
</script>
</ion-app>

View File

@@ -11,24 +11,24 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
});
test('should drag and drop when ion-reorder wraps ion-item', async ({ page }) => {
const items = page.locator('app-reorder');
const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete');
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
await dragElementBy(items.nth(1), page, 0, 300);
await ionReorderComplete.next();
await ionItemReorderComplete.next();
await expect(items).toContainText(['Item 1', 'Item 3', 'Item 4', 'Item 2']);
});
test('should drag and drop when ion-item wraps ion-reorder', async ({ page }) => {
const reorderHandle = page.locator('app-reorder ion-reorder');
const items = page.locator('app-reorder');
const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete');
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
await dragElementBy(reorderHandle.nth(0), page, 0, 300);
await ionReorderComplete.next();
await ionItemReorderComplete.next();
await expect(items).toContainText(['Item 2', 'Item 3', 'Item 4', 'Item 1']);
});

View File

@@ -1,289 +0,0 @@
import { expect } from '@playwright/test';
import { configs, dragElementBy, test } from '@utils/test/playwright';
/**
* This behavior does not vary across modes/directions.
*/
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('reorder-group: events:'), () => {
test.describe('ionReorderStart', () => {
test('should emit when the reorder operation starts', async ({ page }) => {
await page.setContent(
`
<ion-reorder-group disabled="false">
<ion-item>
<ion-label>Item 1</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 2</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 3</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
</ion-reorder-group>
`,
config
);
const reorderGroup = page.locator('ion-reorder-group');
const ionReorderStart = await page.spyOnEvent('ionReorderStart');
await expect(ionReorderStart).toHaveReceivedEventTimes(0);
// Start the drag to verify it emits the event without having to
// actually move the item. Do not release the drag here.
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 0, undefined, undefined, false);
await page.waitForChanges();
await expect(ionReorderStart).toHaveReceivedEventTimes(1);
// Drag the reorder item further to verify it does
// not emit the event again
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 300);
await page.waitForChanges();
await expect(ionReorderStart).toHaveReceivedEventTimes(1);
});
});
test.describe('ionReorderMove', () => {
test('should emit when the reorder operation does not move the item position', async ({ page }) => {
await page.setContent(
`
<ion-reorder-group disabled="false">
<ion-item>
<ion-label>Item 1</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 2</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 3</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
</ion-reorder-group>
`,
config
);
const reorderGroup = page.locator('ion-reorder-group');
const ionReorderMove = await page.spyOnEvent('ionReorderMove');
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 0);
await page.waitForChanges();
expect(ionReorderMove.events.length).toBeGreaterThan(0);
// Grab the last event to verify that it is emitting
// the correct from and to positions
const lastEvent = ionReorderMove.events[ionReorderMove.events.length - 1];
expect(lastEvent?.detail.from).toBe(0);
expect(lastEvent?.detail.to).toBe(0);
});
test('should emit when the reorder operation moves the item by multiple positions', async ({ page }) => {
await page.setContent(
`
<ion-reorder-group disabled="false">
<ion-item>
<ion-label>Item 1</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 2</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 3</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
</ion-reorder-group>
`,
config
);
const reorderGroup = page.locator('ion-reorder-group');
const ionReorderMove = await page.spyOnEvent('ionReorderMove');
// Drag the reorder item by a lot to verify it emits the event
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 300);
await page.waitForChanges();
expect(ionReorderMove.events.length).toBeGreaterThan(0);
// Grab the last event where the from and to are different to
// verify that it is not using the gesture start position as the from
const lastDifferentEvent = ionReorderMove.events
.reverse()
.find((event) => event.detail.from !== event.detail.to);
expect(lastDifferentEvent?.detail.from).toBe(1);
expect(lastDifferentEvent?.detail.to).toBe(2);
});
});
test.describe('ionReorderEnd', () => {
test('should emit without details when the reorder operation ends without moving the item position', async ({
page,
}) => {
await page.setContent(
`
<ion-reorder-group disabled="false">
<ion-item>
<ion-label>Item 1</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 2</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 3</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
</ion-reorder-group>
`,
config
);
const reorderGroup = page.locator('ion-reorder-group');
const ionReorderEnd = await page.spyOnEvent('ionReorderEnd');
// Drag the reorder item a little bit but not enough to
// make it switch to a different position
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 20);
await page.waitForChanges();
await expect(ionReorderEnd).toHaveReceivedEventTimes(1);
await expect(ionReorderEnd).toHaveReceivedEventDetail({ from: 0, to: 0, complete: undefined });
});
test('should emit with details when the reorder operation ends and the item has moved', async ({ page }) => {
await page.setContent(
`
<ion-reorder-group disabled="false">
<ion-item>
<ion-label>Item 1</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 2</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 3</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
</ion-reorder-group>
`,
config
);
const reorderGroup = page.locator('ion-reorder-group');
const ionReorderEnd = await page.spyOnEvent('ionReorderEnd');
// Start the drag to verify it does not emit the event at the start
// of the drag or during the drag. Do not release the drag here.
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 100, undefined, undefined, false);
await page.waitForChanges();
await expect(ionReorderEnd).toHaveReceivedEventTimes(0);
// Drag the reorder item further and release the drag to verify it emits the event
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 300);
await page.waitForChanges();
await expect(ionReorderEnd).toHaveReceivedEventTimes(1);
await expect(ionReorderEnd).toHaveReceivedEventDetail({ from: 0, to: 2, complete: undefined });
});
});
// TODO(FW-6590): Remove this once the deprecated event is removed
test.describe('ionItemReorder', () => {
test('should not emit when the reorder operation ends without moving the item position', async ({ page }) => {
await page.setContent(
`
<ion-reorder-group disabled="false">
<ion-item>
<ion-label>Item 1</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 2</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 3</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
</ion-reorder-group>
`,
config
);
const reorderGroup = page.locator('ion-reorder-group');
const ionItemReorder = await page.spyOnEvent('ionItemReorder');
// Drag the reorder item a little bit but not enough to
// make it switch to a different position
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 20);
await page.waitForChanges();
await expect(ionItemReorder).toHaveReceivedEventTimes(0);
});
test('should emit when the reorder operation ends and the item has moved', async ({ page }) => {
await page.setContent(
`
<ion-reorder-group disabled="false">
<ion-item>
<ion-label>Item 1</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 2</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
<ion-item>
<ion-label>Item 3</ion-label>
<ion-reorder slot="end"></ion-reorder>
</ion-item>
</ion-reorder-group>
`,
config
);
const reorderGroup = page.locator('ion-reorder-group');
const ionItemReorder = await page.spyOnEvent('ionItemReorder');
// Start the drag to verify it does not emit the event at the start
// of the drag or during the drag. Do not release the drag here.
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 100, undefined, undefined, false);
await page.waitForChanges();
await expect(ionItemReorder).toHaveReceivedEventTimes(0);
// Drag the reorder item further and release the drag to verify it emits the event
await dragElementBy(reorderGroup.locator('ion-reorder').first(), page, 0, 300);
await page.waitForChanges();
await expect(ionItemReorder).toHaveReceivedEventTimes(1);
await expect(ionItemReorder).toHaveReceivedEventDetail({ from: 0, to: 2, complete: undefined });
});
});
});
});

View File

@@ -57,9 +57,9 @@
<script>
const group = document.querySelector('ion-reorder-group');
group.addEventListener('ionReorderEnd', (ev) => {
group.addEventListener('ionItemReorder', (ev) => {
ev.detail.complete();
window.dispatchEvent(new CustomEvent('ionReorderComplete'));
window.dispatchEvent(new CustomEvent('ionItemReorderComplete'));
});
</script>
</body>

View File

@@ -11,24 +11,24 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
});
test('should drag and drop when ion-reorder wraps ion-item', async ({ page }) => {
const items = page.locator('ion-item');
const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete');
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
await dragElementBy(items.nth(1), page, 0, 300);
await ionReorderComplete.next();
await ionItemReorderComplete.next();
await expect(items).toContainText(['Item 1', 'Item 3', 'Item 4', 'Item 2']);
});
test('should drag and drop when ion-item wraps ion-reorder', async ({ page }) => {
const reorderHandle = page.locator('ion-reorder');
const items = page.locator('ion-item');
const ionReorderComplete = await page.spyOnEvent('ionReorderComplete');
const ionItemReorderComplete = await page.spyOnEvent('ionItemReorderComplete');
await expect(items).toContainText(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
await dragElementBy(reorderHandle.nth(0), page, 0, 300);
await ionReorderComplete.next();
await ionItemReorderComplete.next();
await expect(items).toContainText(['Item 2', 'Item 3', 'Item 4', 'Item 1']);
});

View File

@@ -1,6 +1,10 @@
import type { AnimationBuilder, ComponentProps, HTMLStencilElement } from '../../../interface';
import type { AnimationBuilder, ComponentProps } from '../../../interface';
import type { NavigationHookCallback } from '../../route/route-interface';
export interface HTMLStencilElement extends HTMLElement {
componentOnReady(): Promise<this>;
}
export interface NavOutlet {
setRouteId(
id: string,

View File

@@ -9,12 +9,6 @@
width: 100%;
// Workaround for a Safari/WebKit bug where flexbox children with dynamic
// height (e.g., height: fit-content) are not included in the scrollable area
// when using horizontal scrolling. This is needed to make the segment view
// scroll to the correct content.
min-height: 1px;
overflow-y: scroll;
/* Hide scrollbar in Firefox */

View File

@@ -1,75 +0,0 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Segment View - Dynamic Height</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
ion-segment-content {
display: flex;
align-items: center;
justify-content: center;
height: fit-content;
}
ion-segment-content:nth-of-type(1) {
background: lightpink;
}
ion-segment-content:nth-of-type(2) {
background: lightblue;
}
ion-segment-content:nth-of-type(3) {
background: lightgreen;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Segment View - Dynamic Height</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-segment>
<ion-segment-button value="first" content-id="first">
<ion-label>First</ion-label>
</ion-segment-button>
<ion-segment-button value="second" content-id="second">
<ion-label>Second</ion-label>
</ion-segment-button>
<ion-segment-button value="third" content-id="third">
<ion-label>Third</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="first">
Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro. De carne lumbering animata corpora
quaeritis. Summus brains sit, morbo vel maleficia? De apocalypsi gorger omero undead survivor dictum mauris.
Hi mindless mortuis soulless creaturas, imo evil stalking monstra adventus resi dentevil vultus comedat
cerebella viventium. Qui animated corpse, cricket bat max brucks terribilem incessu zomby. The voodoo
sacerdos flesh eater, suscitat mortuos comedere carnem virus. Zonbi tattered for solum oculi eorum defunctis
go lum cerebro. Nescio brains an Undead zombies. Sicut malus putrid voodoo horror. Nigh tofth eliv
</ion-segment-content>
<ion-segment-content id="second">
<ion-input value="" label="Email"></ion-input>
</ion-segment-content>
<ion-segment-content id="third">
<ion-img class="img-part" src="https://picsum.photos/200/300"></ion-img>
</ion-segment-content>
</ion-segment-view>
</ion-content>
</ion-app>
</body>
</html>

View File

@@ -1,85 +0,0 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
/**
* This behavior does not vary across modes/directions
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('segment-view: dynamic height'), () => {
test('should show the third content when clicking the third button', async ({ page, skip }) => {
// Skip this test on Chrome and Firefox
skip.browser('firefox', 'Original issue only happens on Safari.');
skip.browser('chromium', 'Original issue only happens on Safari.');
await page.setContent(
`
<style>
ion-segment-content {
display: flex;
align-items: center;
justify-content: center;
height: fit-content;
}
</style>
<ion-segment>
<ion-segment-button value="first" content-id="first">
<ion-label>First</ion-label>
</ion-segment-button>
<ion-segment-button value="second" content-id="second">
<ion-label>Second</ion-label>
</ion-segment-button>
<ion-segment-button value="third" content-id="third">
<ion-label>Third</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="first">
Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro. De carne lumbering animata corpora
quaeritis. Summus brains sit, morbo vel maleficia? De apocalypsi gorger omero undead survivor dictum
mauris. Hi mindless mortuis soulless creaturas, imo evil stalking monstra adventus resi dentevil vultus
comedat cerebella viventium. Qui animated corpse, cricket bat max brucks terribilem incessu zomby. The
voodoo sacerdos flesh eater, suscitat mortuos comedere carnem virus. Zonbi tattered for solum oculi eorum
defunctis go lum cerebro. Nescio brains an Undead zombies. Sicut malus putrid voodoo horror. Nigh tofth eliv
</ion-segment-content>
<ion-segment-content id="second">
<ion-input value="" label="Email"></ion-input>
</ion-segment-content>
<ion-segment-content id="third">
<ion-img class="img-part" src="https://picsum.photos/200/300"></ion-img>
</ion-segment-content>
</ion-segment-view>
`,
config
);
// Click the third button
await page.locator('ion-segment-button[value="third"]').click();
// Wait for the content to be scrolled
await page.waitForChanges();
// Wait for the image to load and be visible
const imgLocator = page.locator('ion-segment-content#third ion-img');
await imgLocator.waitFor({ state: 'visible', timeout: 10000 });
// Wait for any layout adjustments
await page.waitForChanges();
// Check that the third content is visible
const segmentView = page.locator('ion-segment-view');
const thirdContent = page.locator('ion-segment-content#third');
const viewBox = await segmentView.boundingBox();
const contentBox = await thirdContent.boundingBox();
if (!viewBox || !contentBox) throw new Error('Bounding box not found');
// Allow a small tolerance to account for subpixel rendering,
// scrollbars, or layout rounding differences
const tolerance = 10;
expect(contentBox.x).toBeGreaterThanOrEqual(viewBox.x);
expect(contentBox.x + contentBox.width).toBeLessThanOrEqual(viewBox.x + viewBox.width + tolerance);
});
});
});

View File

@@ -68,27 +68,7 @@ export class Tabs implements NavOutlet {
componentWillRender() {
const tabBar = this.el.querySelector('ion-tab-bar');
if (tabBar) {
let tab = this.selectedTab ? this.selectedTab.tab : undefined;
// Fallback: if no selectedTab is set but we're using router mode,
// determine the active tab from the current URL. This works around
// timing issues in React Router integration where setRouteId may not
// be called in time for the initial render.
// TODO(FW-6724): Remove this with React Router upgrade
if (!tab && this.useRouter && typeof window !== 'undefined') {
const currentPath = window.location.pathname;
const tabButtons = this.el.querySelectorAll('ion-tab-button');
// Look for a tab button that matches the current path pattern
for (const tabButton of tabButtons) {
const tabId = tabButton.getAttribute('tab');
if (tabId && currentPath.includes(tabId)) {
tab = tabId;
break;
}
}
}
const tab = this.selectedTab ? this.selectedTab.tab : undefined;
tabBar.selectedTab = tab;
}
}

View File

@@ -2,14 +2,9 @@
@import "../themes/ionic.mixins";
// Display
// ------------------------------------------------------------------
// Provides utility classes to control the CSS display property
// of elements. Includes responsive variants for toggling between
// block, inline, flex, grid, and other display values at different
// breakpoints.
// --------------------------------------------------
// Modifies display of a particular element based on the given classes
// TODO(FW-6697): remove ion-hide-* classes in favor of the new
// ion-display-* classes
.ion-hide {
display: none !important;
}
@@ -34,29 +29,3 @@
}
}
}
$display-values: (
none,
inline,
inline-block,
block,
flex,
inline-flex,
grid,
inline-grid,
table,
table-cell,
table-row
);
@each $display in $display-values {
@each $breakpoint in map-keys($screen-breakpoints) {
$infix: breakpoint-infix($breakpoint, $screen-breakpoints);
@include media-breakpoint-up($breakpoint, $screen-breakpoints) {
.ion-display#{$infix}-#{$display} {
display: #{$display} !important;
}
}
}
}

View File

@@ -1,211 +1,99 @@
@import "../themes/ionic.globals";
@import "../themes/ionic.mixins";
// Flex Utilities
// ------------------------------------------------------------------
// Provides utility classes to control flexbox layout, alignment,
// and sizing of elements. Includes responsive variants for managing
// flex direction, alignment, justification, wrapping, growth,
// shrinking, and ordering at different breakpoints.
// Align Content
// ------------------------------------------------------------------
$align-content-values: (
start: flex-start,
end: flex-end,
center: center,
between: space-between,
around: space-around,
stretch: stretch
);
@each $breakpoint in map-keys($screen-breakpoints) {
$infix: breakpoint-infix($breakpoint, $screen-breakpoints);
@include media-breakpoint-up($breakpoint, $screen-breakpoints) {
@each $key, $value in $align-content-values {
.ion-align-content#{$infix}-#{$key} {
align-content: #{$value} !important;
}
}
}
}
// Align Items
// ------------------------------------------------------------------
$align-items-values: (
start,
end,
center,
stretch,
baseline
);
@each $breakpoint in map-keys($screen-breakpoints) {
$infix: breakpoint-infix($breakpoint, $screen-breakpoints);
@include media-breakpoint-up($breakpoint, $screen-breakpoints) {
@each $value in $align-items-values {
.ion-align-items#{$infix}-#{$value} {
align-items: #{$value} !important;
}
}
}
}
// --------------------------------------------------
// Creates flex classes to align flex containers
// and items
// Align Self
// ------------------------------------------------------------------
// --------------------------------------------------
$align-self-values: (
start,
end,
center,
stretch,
baseline,
auto
);
@each $breakpoint in map-keys($screen-breakpoints) {
$infix: breakpoint-infix($breakpoint, $screen-breakpoints);
@include media-breakpoint-up($breakpoint, $screen-breakpoints) {
@each $value in $align-self-values {
.ion-align-self#{$infix}-#{$value} {
align-self: #{$value} !important;
}
}
}
.ion-align-self-start {
align-self: flex-start !important;
}
// Justify Content
// ------------------------------------------------------------------
$justify-content-values: (
start: flex-start,
end: flex-end,
center: center,
between: space-between,
around: space-around,
evenly: space-evenly
);
@each $breakpoint in map-keys($screen-breakpoints) {
$infix: breakpoint-infix($breakpoint, $screen-breakpoints);
@include media-breakpoint-up($breakpoint, $screen-breakpoints) {
@each $key, $value in $justify-content-values {
.ion-justify-content#{$infix}-#{$key} {
justify-content: #{$value} !important;
}
}
}
.ion-align-self-end {
align-self: flex-end !important;
}
// Flex Direction
// ------------------------------------------------------------------
$flex-direction-values: (
row,
row-reverse,
column,
column-reverse
);
@each $breakpoint in map-keys($screen-breakpoints) {
$infix: breakpoint-infix($breakpoint, $screen-breakpoints);
@include media-breakpoint-up($breakpoint, $screen-breakpoints) {
@each $value in $flex-direction-values {
.ion-flex#{$infix}-#{$value} {
flex-direction: #{$value} !important;
}
}
}
.ion-align-self-center {
align-self: center !important;
}
.ion-align-self-stretch {
align-self: stretch !important;
}
.ion-align-self-baseline {
align-self: baseline !important;
}
.ion-align-self-auto {
align-self: auto !important;
}
// Flex Wrap
// ------------------------------------------------------------------
// --------------------------------------------------
$flex-wrap-values: (
wrap,
nowrap,
wrap-reverse
);
@each $value in $flex-wrap-values {
// TODO(FW-6697): remove ion-wrap, ion-nowrap, ion-wrap-reverse
// in favor of the new ion-flex-wrap, ion-flex-nowrap, and
// ion-flex-wrap-reverse classes
.ion-#{$value} {
flex-wrap: #{$value} !important;
}
.ion-wrap {
flex-wrap: wrap !important;
}
@each $breakpoint in map-keys($screen-breakpoints) {
$infix: breakpoint-infix($breakpoint, $screen-breakpoints);
@include media-breakpoint-up($breakpoint, $screen-breakpoints) {
@each $value in $flex-wrap-values {
.ion-flex#{$infix}-#{$value} {
flex-wrap: #{$value} !important;
}
}
}
.ion-nowrap {
flex-wrap: nowrap !important;
}
// Flex Fill
// ------------------------------------------------------------------
$flex-fill-values: (
1,
auto,
initial,
none
);
@each $breakpoint in map-keys($screen-breakpoints) {
$infix: breakpoint-infix($breakpoint, $screen-breakpoints);
@include media-breakpoint-up($breakpoint, $screen-breakpoints) {
@each $value in $flex-fill-values {
.ion-flex#{$infix}-#{$value} {
flex: #{$value} !important;
}
}
}
.ion-wrap-reverse {
flex-wrap: wrap-reverse !important;
}
// Flex Grow and Shrink
// ------------------------------------------------------------------
@each $breakpoint in map-keys($screen-breakpoints) {
$infix: breakpoint-infix($breakpoint, $screen-breakpoints);
@include media-breakpoint-up($breakpoint, $screen-breakpoints) {
.ion-flex#{$infix}-grow-0 {
flex-grow: 0 !important;
}
// Justify Content
// --------------------------------------------------
.ion-flex#{$infix}-grow-1 {
flex-grow: 1 !important;
}
.ion-flex#{$infix}-shrink-0 {
flex-shrink: 0 !important;
}
.ion-flex#{$infix}-shrink-1 {
flex-shrink: 1 !important;
}
}
.ion-justify-content-start {
justify-content: flex-start !important;
}
// Flex Order
// ------------------------------------------------------------------
@each $breakpoint in map-keys($screen-breakpoints) {
$infix: breakpoint-infix($breakpoint, $screen-breakpoints);
@include media-breakpoint-up($breakpoint, $screen-breakpoints) {
.ion-order#{$infix}-first { order: -1 !important; }
@for $i from 0 through 12 {
.ion-order#{$infix}-#{$i} { order: #{$i} !important; }
}
.ion-order#{$infix}-last { order: 13 !important; }
}
.ion-justify-content-center {
justify-content: center !important;
}
.ion-justify-content-end {
justify-content: flex-end !important;
}
.ion-justify-content-around {
justify-content: space-around !important;
}
.ion-justify-content-between {
justify-content: space-between !important;
}
.ion-justify-content-evenly {
justify-content: space-evenly !important;
}
// Align Items
// --------------------------------------------------
.ion-align-items-start {
align-items: flex-start !important;
}
.ion-align-items-center {
align-items: center !important;
}
.ion-align-items-end {
align-items: flex-end !important;
}
.ion-align-items-stretch {
align-items: stretch !important;
}
.ion-align-items-baseline {
align-items: baseline !important;
}

View File

@@ -1,48 +0,0 @@
import { test, expect } from '@playwright/test';
import fs from 'fs';
import path from 'path';
test.describe('display css utility classes', () => {
let css: string;
test.beforeAll(() => {
css = fs.readFileSync(path.resolve(__dirname, '../../../css/display.css'), 'utf8');
});
const INFIXES = ['', '-sm', '-md', '-lg', '-xl'];
// TODO(FW-6697): remove `ion-hide classes` test
test('ion-hide classes', () => {
expect(css).toContain('.ion-hide');
const values = ['up', 'down'];
for (const value of values) {
for (const infix of INFIXES) {
expect(css).toContain(`.ion-hide${infix}-${value}`);
}
}
});
test('ion-display classes', () => {
const values = [
'none',
'inline',
'inline-block',
'block',
'flex',
'inline-flex',
'grid',
'inline-grid',
'table',
'table-cell',
'table-row',
];
for (const value of values) {
for (const infix of INFIXES) {
expect(css).toContain(`.ion-display${infix}-${value}`);
}
}
});
});

View File

@@ -1,100 +0,0 @@
import { test, expect } from '@playwright/test';
import fs from 'fs';
import path from 'path';
test.describe('flex-utils css utility classes', () => {
let css: string;
test.beforeAll(() => {
css = fs.readFileSync(path.resolve(__dirname, '../../../css/flex-utils.css'), 'utf8');
});
const INFIXES = ['', '-sm', '-md', '-lg', '-xl'];
test('align-content classes', () => {
const values = ['start', 'end', 'center', 'between', 'around', 'stretch'];
for (const value of values) {
for (const infix of INFIXES) {
expect(css).toContain(`.ion-align-content${infix}-${value}`);
}
}
});
test('align-items classes', () => {
const values = ['start', 'center', 'end', 'stretch', 'baseline'];
for (const value of values) {
for (const infix of INFIXES) {
expect(css).toContain(`.ion-align-items${infix}-${value}`);
}
}
});
test('align-self classes', () => {
const values = ['start', 'end', 'center', 'stretch', 'baseline', 'auto'];
for (const value of values) {
for (const infix of INFIXES) {
expect(css).toContain(`.ion-align-self${infix}-${value}`);
}
}
});
test('justify-content classes', () => {
const values = ['start', 'center', 'end', 'around', 'between', 'evenly'];
for (const value of values) {
for (const infix of INFIXES) {
expect(css).toContain(`.ion-justify-content${infix}-${value}`);
}
}
});
test('flex-direction classes', () => {
const values = ['row', 'row-reverse', 'column', 'column-reverse'];
for (const value of values) {
for (const infix of INFIXES) {
expect(css).toContain(`.ion-flex${infix}-${value}`);
}
}
});
test('flex-wrap classes', () => {
const values = ['wrap', 'nowrap', 'wrap-reverse'];
// TODO(FW-6697): remove all `ion-wrap-*` expects
for (const value of values) {
expect(css).toContain(`.ion-${value}`);
}
for (const value of values) {
for (const infix of INFIXES) {
expect(css).toContain(`.ion-flex${infix}-${value}`);
}
}
});
test('flex-fill classes', () => {
const values = ['1', 'auto', 'initial', 'none'];
for (const value of values) {
for (const infix of INFIXES) {
expect(css).toContain(`.ion-flex${infix}-${value}`);
}
}
});
test('flex-grow and flex-shrink classes', () => {
const values = ['grow', 'shrink'];
for (const value of values) {
for (const infix of INFIXES) {
expect(css).toContain(`.ion-flex${infix}-${value}-0`);
expect(css).toContain(`.ion-flex${infix}-${value}-1`);
}
}
});
test('flex-order classes', () => {
for (const infix of INFIXES) {
expect(css).toContain(`.ion-order${infix}-first`);
expect(css).toContain(`.ion-order${infix}-last`);
for (let i = 0; i <= 12; i++) {
expect(css).toContain(`.ion-order${infix}-${i}`);
}
}
});
});

View File

@@ -23,13 +23,9 @@ export { PickerOptions, PickerColumnOption } from './components/picker-legacy/pi
export { PopoverOptions } from './components/popover/popover-interface';
export { RadioGroupCustomEvent } from './components/radio-group/radio-group-interface';
export { RangeCustomEvent, PinFormatter } from './components/range/range-interface';
export { RouterCustomEvent } from './components/router/utils/interface';
export { HTMLStencilElement, RouterCustomEvent } from './components/router/utils/interface';
export { RefresherCustomEvent } from './components/refresher/refresher-interface';
export {
ItemReorderCustomEvent,
ReorderEndCustomEvent,
ReorderMoveCustomEvent,
} from './components/reorder-group/reorder-group-interface';
export { ItemReorderCustomEvent } from './components/reorder-group/reorder-group-interface';
export { SearchbarCustomEvent } from './components/searchbar/searchbar-interface';
export { SegmentCustomEvent } from './components/segment/segment-interface';
export { SelectCustomEvent, SelectCompareFn } from './components/select/select-interface';
@@ -49,7 +45,6 @@ export {
AnimationKeyFrames,
AnimationLifecycle,
} from './utils/animation/animation-interface';
export { HTMLStencilElement } from './utils/element-interface';
export { TransitionOptions } from './utils/transition';
export { HTMLIonOverlayElement, OverlayController, OverlayInterface } from './utils/overlays-interface';
export { Config, config } from './global/config';

View File

@@ -1,19 +1,6 @@
// The interfaces in this file are used to make sure our components
// have the correct properties defined that are needed to pass to
// the native HTML elements they render. The HTMLStencilElement interface
// extends HTMLElement to provide Stencil-specific functionality like
// componentOnReady() and proper children handling.
export interface HTMLStencilElement extends HTMLElement {
componentOnReady(): Promise<this>;
/**
* Stencil patches `el.children` to behave like calling `el.children` on an
* element with shadow DOM even though the component is not a shadow DOM element.
* To allow components to work properly we need to access the original accessor
* for this property which is `__children`.
*/
__children?: HTMLCollection;
}
// the native HTML elements they render
export interface AnchorInterface {
href: string | undefined;

View File

@@ -22,39 +22,54 @@ export interface FocusVisibleUtility {
export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility => {
let currentFocus: Element[] = [];
let keyboardMode = true;
// Tracks if the last interaction was a pointer event (mouse, touch, pen)
// Used to distinguish between pointer and keyboard navigation for focus styling
let hadPointerEvent = false;
const ref = rootEl ? rootEl.shadowRoot! : document;
const root = rootEl ? rootEl : document.body;
// Adds or removes the focused class for styling
const setFocus = (elements: Element[]) => {
currentFocus.forEach((el) => el.classList.remove(ION_FOCUSED));
elements.forEach((el) => el.classList.add(ION_FOCUSED));
currentFocus = elements;
};
const pointerDown = () => {
keyboardMode = false;
setFocus([]);
// Do not set focus on pointer interactions
const pointerDown = (ev: Event) => {
if (ev instanceof PointerEvent && ev.pointerType !== '') {
hadPointerEvent = true;
// Reset after the event loop so only the immediate focusin is suppressed
setTimeout(() => {
hadPointerEvent = false;
}, 0);
}
};
// Clear hadPointerEvent so keyboard navigation shows focus
// Also, clear focus if the key is not a navigation key
const onKeydown = (ev: Event) => {
keyboardMode = FOCUS_KEYS.includes((ev as KeyboardEvent).key);
if (!keyboardMode) {
hadPointerEvent = false;
const keyboardEvent = ev as KeyboardEvent;
if (!FOCUS_KEYS.includes(keyboardEvent.key)) {
setFocus([]);
}
};
// Set focus if the last interaction was NOT a pointer event
// This works around iOS/Safari bugs where keydown is not fired for Tab
const onFocusin = (ev: Event) => {
if (keyboardMode && ev.composedPath !== undefined) {
const toFocus = ev.composedPath().filter((el: any) => {
// TODO(FW-2832): type
if (el.classList) {
return el.classList.contains(ION_FOCUSABLE);
}
return false;
}) as Element[];
const target = ev.target as HTMLElement;
if (target.classList.contains(ION_FOCUSABLE) && !hadPointerEvent) {
const toFocus = ev
.composedPath()
.filter((el): el is HTMLElement => el instanceof HTMLElement && el.classList.contains(ION_FOCUSABLE));
setFocus(toFocus);
}
};
const onFocusout = () => {
if (ref.activeElement === root) {
setFocus([]);
@@ -64,15 +79,13 @@ export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility =>
ref.addEventListener('keydown', onKeydown);
ref.addEventListener('focusin', onFocusin);
ref.addEventListener('focusout', onFocusout);
ref.addEventListener('touchstart', pointerDown, { passive: true });
ref.addEventListener('mousedown', pointerDown);
ref.addEventListener('pointerdown', pointerDown, { passive: true });
const destroy = () => {
ref.removeEventListener('keydown', onKeydown);
ref.removeEventListener('focusin', onFocusin);
ref.removeEventListener('focusout', onFocusout);
ref.removeEventListener('touchstart', pointerDown);
ref.removeEventListener('mousedown', pointerDown);
ref.removeEventListener('pointerdown', pointerDown);
};
return {

View File

@@ -33,26 +33,6 @@ export const setContent = async (page: Page, html: string, testInfo: TestInfo, o
const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL;
// The Ionic bundle is included locally by default unless the test
// config passes in the importIonicFromCDN option. This is useful
// when testing with the CDN version of Ionic.
let ionicCSSImports = `
<link href="${baseUrl}/css/ionic.bundle.css" rel="stylesheet" />
`;
let ionicJSImports = `
<script type="module" src="${baseUrl}/dist/ionic/ionic.esm.js"></script>
`;
if (options?.importIonicFromCDN) {
ionicCSSImports = `
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/core/css/ionic.bundle.css" />
`;
ionicJSImports = `
<script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.esm.js"></script>
<script nomodule src="https://cdn.jsdelivr.net/npm/@ionic/core/dist/ionic/ionic.js"></script>
`;
}
const output = `
<!DOCTYPE html>
<html dir="${direction}" lang="en">
@@ -60,11 +40,11 @@ export const setContent = async (page: Page, html: string, testInfo: TestInfo, o
<title>Ionic Playwright Test</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
${ionicCSSImports}
<link href="${baseUrl}/css/ionic.bundle.css" rel="stylesheet" />
<link href="${baseUrl}/scripts/testing/styles.css" rel="stylesheet" />
${palette !== 'light' ? `<link href="${baseUrl}/css/palettes/${palette}.always.css" rel="stylesheet" />` : ''}
<script src="${baseUrl}/scripts/testing/scripts.js"></script>
${ionicJSImports}
<script type="module" src="${baseUrl}/dist/ionic/ionic.esm.js"></script>
<script>
window.Ionic = {
config: {

View File

@@ -31,12 +31,6 @@ interface PageOptions {
* - `'commit'` - consider operation to be finished when network response is received and the document started loading.
*/
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit';
/**
* If true, the default Ionic imports will be included
* via the CDN instead of the local bundle.
*/
importIonicFromCDN?: boolean;
}
export interface E2EPage extends Page {

View File

@@ -216,7 +216,7 @@ export const config: Config = {
},
{
type: 'dist',
esmLoaderPath: '../loader',
esmLoaderPath: '../loader'
},
{
type: 'dist-custom-elements',

View File

@@ -3,5 +3,5 @@
"core",
"packages/*"
],
"version": "8.7.2"
"version": "8.6.2"
}

View File

@@ -3,70 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.2](https://github.com/ionic-team/ionic-framework/compare/v8.7.1...v8.7.2) (2025-08-06)
**Note:** Version bump only for package @ionic/angular-server
## [8.7.1](https://github.com/ionic-team/ionic-framework/compare/v8.7.0...v8.7.1) (2025-07-31)
**Note:** Version bump only for package @ionic/angular-server
# [8.7.0](https://github.com/ionic-team/ionic-framework/compare/v8.6.7...v8.7.0) (2025-07-30)
**Note:** Version bump only for package @ionic/angular-server
## [8.6.7](https://github.com/ionic-team/ionic-framework/compare/v8.6.6...v8.6.7) (2025-07-30)
**Note:** Version bump only for package @ionic/angular-server
## [8.6.6](https://github.com/ionic-team/ionic-framework/compare/v8.6.5...v8.6.6) (2025-07-30)
**Note:** Version bump only for package @ionic/angular-server
## [8.6.5](https://github.com/ionic-team/ionic-framework/compare/v8.6.4...v8.6.5) (2025-07-16)
**Note:** Version bump only for package @ionic/angular-server
## [8.6.4](https://github.com/ionic-team/ionic-framework/compare/v8.6.3...v8.6.4) (2025-07-09)
**Note:** Version bump only for package @ionic/angular-server
## [8.6.3](https://github.com/ionic-team/ionic-framework/compare/v8.6.2...v8.6.3) (2025-07-02)
**Note:** Version bump only for package @ionic/angular-server
## [8.6.2](https://github.com/ionic-team/ionic-framework/compare/v8.6.1...v8.6.2) (2025-06-18)
**Note:** Version bump only for package @ionic/angular-server

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/angular-server",
"version": "8.7.2",
"version": "8.6.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/angular-server",
"version": "8.7.2",
"version": "8.6.2",
"license": "MIT",
"dependencies": {
"@ionic/core": "^8.7.2"
"@ionic/core": "^8.6.2"
},
"devDependencies": {
"@angular-eslint/eslint-plugin": "^16.0.0",
@@ -1031,13 +1031,13 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "8.7.1",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.1.tgz",
"integrity": "sha512-TSJDPWayn23Dw0gjwvbumo6piDrpZvyVccgMUGyKDrqduvBogzIsPrjPBYfTF4z4Sc/W0HMad17nBskC2+ybqw==",
"version": "8.6.2",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.6.2.tgz",
"integrity": "sha512-CGZ9CDp/XHtm9WrK3wt0ZtR2f2B76qEvJIaF/juCqmpza9Al6u2L9R/NTEwInDRCWfbkAIF22nHNH54/VvN78Q==",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.36.2",
"ionicons": "^8.0.13",
"@stencil/core": "4.33.1",
"ionicons": "^7.2.2",
"tslib": "^2.1.0"
}
},
@@ -1386,9 +1386,9 @@
]
},
"node_modules/@stencil/core": {
"version": "4.36.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz",
"integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==",
"version": "4.33.1",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.33.1.tgz",
"integrity": "sha512-12k9xhAJBkpg598it+NRmaYIdEe6TSnsL/v6/KRXDcUyTK11VYwZQej2eHnMWtqot+znJ+GNTqb5YbiXi+5Low==",
"license": "MIT",
"bin": {
"stencil": "bin/stencil"
@@ -3818,12 +3818,11 @@
}
},
"node_modules/ionicons": {
"version": "8.0.13",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-8.0.13.tgz",
"integrity": "sha512-2QQVyG2P4wszne79jemMjWYLp0DBbDhr4/yFroPCxvPP1wtMxgdIV3l5n+XZ5E9mgoXU79w7yTWpm2XzJsISxQ==",
"license": "MIT",
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
"integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
"dependencies": {
"@stencil/core": "^4.35.3"
"@stencil/core": "^4.0.3"
}
},
"node_modules/is-array-buffer": {
@@ -7306,12 +7305,12 @@
"dev": true
},
"@ionic/core": {
"version": "8.7.1",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.7.1.tgz",
"integrity": "sha512-TSJDPWayn23Dw0gjwvbumo6piDrpZvyVccgMUGyKDrqduvBogzIsPrjPBYfTF4z4Sc/W0HMad17nBskC2+ybqw==",
"version": "8.6.2",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-8.6.2.tgz",
"integrity": "sha512-CGZ9CDp/XHtm9WrK3wt0ZtR2f2B76qEvJIaF/juCqmpza9Al6u2L9R/NTEwInDRCWfbkAIF22nHNH54/VvN78Q==",
"requires": {
"@stencil/core": "4.36.2",
"ionicons": "^8.0.13",
"@stencil/core": "4.33.1",
"ionicons": "^7.2.2",
"tslib": "^2.1.0"
}
},
@@ -7529,9 +7528,9 @@
"optional": true
},
"@stencil/core": {
"version": "4.36.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.36.2.tgz",
"integrity": "sha512-PRFSpxNzX9Oi0Wfh02asztN9Sgev/MacfZwmd+VVyE6ZxW+a/kEpAYZhzGAmE+/aKVOGYuug7R9SulanYGxiDQ==",
"version": "4.33.1",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.33.1.tgz",
"integrity": "sha512-12k9xhAJBkpg598it+NRmaYIdEe6TSnsL/v6/KRXDcUyTK11VYwZQej2eHnMWtqot+znJ+GNTqb5YbiXi+5Low==",
"requires": {
"@rollup/rollup-darwin-arm64": "4.34.9",
"@rollup/rollup-darwin-x64": "4.34.9",
@@ -9240,11 +9239,11 @@
}
},
"ionicons": {
"version": "8.0.13",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-8.0.13.tgz",
"integrity": "sha512-2QQVyG2P4wszne79jemMjWYLp0DBbDhr4/yFroPCxvPP1wtMxgdIV3l5n+XZ5E9mgoXU79w7yTWpm2XzJsISxQ==",
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
"integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
"requires": {
"@stencil/core": "^4.35.3"
"@stencil/core": "^4.0.3"
}
},
"is-array-buffer": {
@@ -11286,4 +11285,4 @@
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular-server",
"version": "8.7.2",
"version": "8.6.2",
"description": "Angular SSR Module for Ionic",
"keywords": [
"ionic",
@@ -62,6 +62,6 @@
},
"prettier": "@ionic/prettier-config",
"dependencies": {
"@ionic/core": "^8.7.2"
"@ionic/core": "^8.6.2"
}
}

View File

@@ -3,81 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.7.2](https://github.com/ionic-team/ionic-framework/compare/v8.7.1...v8.7.2) (2025-08-06)
### Bug Fixes
* **reorder-group:** add children fallback for framework compatibility ([#30593](https://github.com/ionic-team/ionic-framework/issues/30593)) ([1cd81b9](https://github.com/ionic-team/ionic-framework/commit/1cd81b92301378d55bce63a01dfcf95a91c92652)), closes [#30592](https://github.com/ionic-team/ionic-framework/issues/30592)
## [8.7.1](https://github.com/ionic-team/ionic-framework/compare/v8.7.0...v8.7.1) (2025-07-31)
**Note:** Version bump only for package @ionic/angular
# [8.7.0](https://github.com/ionic-team/ionic-framework/compare/v8.6.7...v8.7.0) (2025-07-30)
### Features
* **deps:** update ionicons to v8 ([#30390](https://github.com/ionic-team/ionic-framework/issues/30390)) ([74cd71a](https://github.com/ionic-team/ionic-framework/commit/74cd71af243183aa738d11b280e155bdfd652126)), closes [#30445](https://github.com/ionic-team/ionic-framework/issues/30445)
* **modal:** add IonModalToken for injecting modal elements in Angular components ([#30474](https://github.com/ionic-team/ionic-framework/issues/30474)) ([30d1910](https://github.com/ionic-team/ionic-framework/commit/30d1910d6ea5428b414d0e127e7681f59426c538))
* **reorder-group:** add ionReorderStart, ionReorderMove, ionReorderEnd events ([#30471](https://github.com/ionic-team/ionic-framework/issues/30471)) ([b154f4e](https://github.com/ionic-team/ionic-framework/commit/b154f4ed095890f57ccab539fd9217976a5466e5)), closes [#23148](https://github.com/ionic-team/ionic-framework/issues/23148) [#27614](https://github.com/ionic-team/ionic-framework/issues/27614)
## [8.6.7](https://github.com/ionic-team/ionic-framework/compare/v8.6.6...v8.6.7) (2025-07-30)
**Note:** Version bump only for package @ionic/angular
## [8.6.6](https://github.com/ionic-team/ionic-framework/compare/v8.6.5...v8.6.6) (2025-07-30)
**Note:** Version bump only for package @ionic/angular
## [8.6.5](https://github.com/ionic-team/ionic-framework/compare/v8.6.4...v8.6.5) (2025-07-16)
**Note:** Version bump only for package @ionic/angular
## [8.6.4](https://github.com/ionic-team/ionic-framework/compare/v8.6.3...v8.6.4) (2025-07-09)
**Note:** Version bump only for package @ionic/angular
## [8.6.3](https://github.com/ionic-team/ionic-framework/compare/v8.6.2...v8.6.3) (2025-07-02)
### Bug Fixes
* **angular:** update schematics to support Angular's latest build system ([#30525](https://github.com/ionic-team/ionic-framework/issues/30525)) ([08e3e7a](https://github.com/ionic-team/ionic-framework/commit/08e3e7ab5165baea668571af9845933b5befeb46)), closes [ionic-team/ionic-docs#2091](https://github.com/ionic-team/ionic-docs/issues/2091)
## [8.6.2](https://github.com/ionic-team/ionic-framework/compare/v8.6.1...v8.6.2) (2025-06-18)
**Note:** Version bump only for package @ionic/angular

View File

@@ -1,34 +1,34 @@
export { DomController } from './providers/dom-controller';
export { MenuController } from './providers/menu-controller';
export { DomController } from './providers/dom-controller';
export { NavController } from './providers/nav-controller';
export { Config, ConfigToken } from './providers/config';
export { Platform } from './providers/platform';
export { AngularDelegate, bindLifecycleEvents, IonModalToken } from './providers/angular-delegate';
export { bindLifecycleEvents, AngularDelegate } from './providers/angular-delegate';
export type { IonicWindow } from './types/interfaces';
export type { ViewDidEnter, ViewDidLeave, ViewWillEnter, ViewWillLeave } from './types/ionic-lifecycle-hooks';
export type { ViewWillEnter, ViewWillLeave, ViewDidEnter, ViewDidLeave } from './types/ionic-lifecycle-hooks';
export { NavParams } from './directives/navigation/nav-params';
export { IonModal } from './overlays/modal';
export { IonPopover } from './overlays/popover';
export { IonModal } from './overlays/modal';
export { IonRouterOutlet, provideComponentInputBinding } from './directives/navigation/router-outlet';
export * from './directives/control-value-accessors';
export { IonBackButton } from './directives/navigation/back-button';
export { IonNav } from './directives/navigation/nav';
export {
RouterLinkDelegateDirective,
RouterLinkWithHrefDelegateDirective,
} from './directives/navigation/router-link-delegate';
export { IonNav } from './directives/navigation/nav';
export { IonTabs } from './directives/navigation/tabs';
export * from './directives/control-value-accessors';
export { ProxyCmp } from './utils/proxy';
export { OverlayBaseController } from './utils/overlay';
export { IonicRouteStrategy } from './utils/routing';
export { OverlayBaseController } from './utils/overlay';
export { raf } from './utils/util';

View File

@@ -1,13 +1,13 @@
import {
ApplicationRef,
ComponentRef,
createComponent,
NgZone,
Injectable,
Injector,
EnvironmentInjector,
inject,
Injectable,
createComponent,
InjectionToken,
Injector,
NgZone,
ComponentRef,
} from '@angular/core';
import {
FrameworkDelegate,
@@ -22,9 +22,6 @@ import { NavParams } from '../directives/navigation/nav-params';
import { ConfigToken } from './config';
// Token for injecting the modal element
export const IonModalToken = new InjectionToken<HTMLIonModalElement>('IonModalToken');
// TODO(FW-2827): types
@Injectable()
@@ -145,19 +142,8 @@ export const attachView = (
* The modern approach is to access the data directly
* from the component's class instance.
*/
const providers = getProviders(params);
// If this is an ion-modal, provide the modal element as an injectable
// so components inside the modal can inject it directly
if (container.tagName.toLowerCase() === 'ion-modal') {
providers.push({
provide: IonModalToken,
useValue: container,
});
}
const childInjector = Injector.create({
providers,
providers: getProviders(params),
parent: injector,
});

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular",
"version": "8.7.2",
"version": "8.6.2",
"description": "Angular specific wrappers for @ionic/core",
"keywords": [
"ionic",
@@ -47,8 +47,8 @@
}
},
"dependencies": {
"@ionic/core": "^8.7.2",
"ionicons": "^8.0.13",
"@ionic/core": "^8.6.2",
"ionicons": "^7.0.0",
"jsonc-parser": "^3.0.0",
"tslib": "^2.3.0"
},

View File

@@ -1895,40 +1895,20 @@ export class IonReorderGroup {
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
proxyOutputs(this, this.el, ['ionItemReorder', 'ionReorderStart', 'ionReorderMove', 'ionReorderEnd']);
proxyOutputs(this, this.el, ['ionItemReorder']);
}
}
import type { ItemReorderEventDetail as IIonReorderGroupItemReorderEventDetail } from '@ionic/core';
import type { ReorderMoveEventDetail as IIonReorderGroupReorderMoveEventDetail } from '@ionic/core';
import type { ReorderEndEventDetail as IIonReorderGroupReorderEndEventDetail } from '@ionic/core';
export declare interface IonReorderGroup extends Components.IonReorderGroup {
/**
* Event that needs to be listened to in order to complete the reorder action. @deprecated Use `ionReorderEnd` instead. If you are accessing
`event.detail.from` or `event.detail.to` and relying on them
being different you should now add checks as they are always emitted
in `ionReorderEnd`, even when they are the same.
*/
ionItemReorder: EventEmitter<CustomEvent<IIonReorderGroupItemReorderEventDetail>>;
/**
* Event that is emitted when the reorder gesture starts.
*/
ionReorderStart: EventEmitter<CustomEvent<void>>;
/**
* Event that is emitted as the reorder gesture moves.
*/
ionReorderMove: EventEmitter<CustomEvent<IIonReorderGroupReorderMoveEventDetail>>;
/**
* Event that is emitted when the reorder gesture ends.
The from and to properties are always available, regardless of
if the reorder gesture moved the item. If the item did not change
from its start position, the from and to properties will be the same.
* Event that needs to be listened to in order to complete the reorder action.
Once the event has been emitted, the `complete()` method then needs
to be called in order to finalize the reorder action.
*/
ionReorderEnd: EventEmitter<CustomEvent<IIonReorderGroupReorderEndEventDetail>>;
ionItemReorder: EventEmitter<CustomEvent<IIonReorderGroupItemReorderEventDetail>>;
}

View File

@@ -26,7 +26,6 @@ export {
AngularDelegate,
NavParams,
IonicRouteStrategy,
IonModalToken,
ViewWillEnter,
ViewWillLeave,
ViewDidEnter,
@@ -91,7 +90,6 @@ export {
InputOtpChangeEventDetail,
InputOtpCompleteEventDetail,
InputOtpInputEventDetail,
// TODO(FW-6590): Remove the next two lines once the deprecated event is removed
ItemReorderEventDetail,
ItemReorderCustomEvent,
ItemSlidingCustomEvent,
@@ -114,10 +112,6 @@ export {
RangeKnobMoveEndEventDetail,
RefresherCustomEvent,
RefresherEventDetail,
ReorderMoveCustomEvent,
ReorderMoveEventDetail,
ReorderEndCustomEvent,
ReorderEndEventDetail,
RouterEventDetail,
RouterCustomEvent,
ScrollBaseCustomEvent,

Some files were not shown because too many files have changed in this diff Show More