Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a08a07cbab | ||
|
|
c2bcdcbec3 | ||
|
|
f02aff87af | ||
|
|
2af3702b3e | ||
|
|
783a653071 | ||
|
|
954baf3bef | ||
|
|
48b4cc36b8 | ||
|
|
3cef069078 | ||
|
|
82f8ca8dd1 | ||
|
|
6f276099fc | ||
|
|
f295b35671 | ||
|
|
1a9714783f | ||
|
|
84cf0f152f | ||
|
|
28bd4ba720 | ||
|
|
b6f43e0e72 | ||
|
|
87bc207dad | ||
|
|
8c6dc29133 | ||
|
|
3a263d14c3 | ||
|
|
0cb37430d3 | ||
|
|
e9fa30002b | ||
|
|
07dee74714 | ||
|
|
aa326a6eda | ||
|
|
1cf1eca002 | ||
|
|
eb19c289d6 | ||
|
|
a0e6ac6013 | ||
|
|
da55ab949e | ||
|
|
a0b3ef02af | ||
|
|
9500769f11 | ||
|
|
5992c619ad | ||
|
|
01857dd315 | ||
|
|
dbe6f390ef | ||
|
|
0c117cfe7f | ||
|
|
f14c440d63 | ||
|
|
bd1910ba69 | ||
|
|
824033f1d4 | ||
|
|
b8553d89f8 | ||
|
|
ba2f49b8a4 | ||
|
|
a08a5894ba | ||
|
|
0b8f1bc7dd | ||
|
|
bd71373f1a | ||
|
|
960adbbc5c | ||
|
|
db29871654 | ||
|
|
e9ee96a443 | ||
|
|
38626d9680 | ||
|
|
6e4919caff | ||
|
|
5122ced9e1 | ||
|
|
3c794d25d6 | ||
|
|
9c799a6eea | ||
|
|
08cc3d93ae |
45
CHANGELOG.md
@@ -3,6 +3,51 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [7.2.3](https://github.com/ionic-team/ionic-framework/compare/v7.2.2...v7.2.3) (2023-08-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **button:** hidden button is added when form is set async ([#27955](https://github.com/ionic-team/ionic-framework/issues/27955)) ([e9fa300](https://github.com/ionic-team/ionic-framework/commit/e9fa30002bd5dec4f2f56a15c84eec1b3e794942)), closes [#27952](https://github.com/ionic-team/ionic-framework/issues/27952)
|
||||
* **datetime:** changing months work if partially visible ([#27917](https://github.com/ionic-team/ionic-framework/issues/27917)) ([eb19c28](https://github.com/ionic-team/ionic-framework/commit/eb19c289d6581639f6df7aff002bebdf2b27d31c)), closes [#27913](https://github.com/ionic-team/ionic-framework/issues/27913)
|
||||
* **item-sliding:** account for options added before watcher ([#27915](https://github.com/ionic-team/ionic-framework/issues/27915)) ([a0b3ef0](https://github.com/ionic-team/ionic-framework/commit/a0b3ef02af718e232246515bb873ad8c090fa55d)), closes [#27910](https://github.com/ionic-team/ionic-framework/issues/27910)
|
||||
* **many:** overlays present if isOpen is true on load ([#27933](https://github.com/ionic-team/ionic-framework/issues/27933)) ([a0e6ac6](https://github.com/ionic-team/ionic-framework/commit/a0e6ac6013552c5e3acdde33575d4aaf4d4c0bda)), closes [#27928](https://github.com/ionic-team/ionic-framework/issues/27928)
|
||||
* **modal:** setCurrentBreakpoint respects animated prop ([#27924](https://github.com/ionic-team/ionic-framework/issues/27924)) ([da55ab9](https://github.com/ionic-team/ionic-framework/commit/da55ab949ef1894738da5a6241176089b7a2b6e3)), closes [#27923](https://github.com/ionic-team/ionic-framework/issues/27923)
|
||||
* **nav:** improve reliability of swipe back gesture when quickly swiping back ([#27904](https://github.com/ionic-team/ionic-framework/issues/27904)) ([9500769](https://github.com/ionic-team/ionic-framework/commit/9500769f114d180613f0340b1a328b5e631b7188)), closes [#27893](https://github.com/ionic-team/ionic-framework/issues/27893)
|
||||
* **tab-button:** update event type interface on React ([#27950](https://github.com/ionic-team/ionic-framework/issues/27950)) ([1cf1eca](https://github.com/ionic-team/ionic-framework/commit/1cf1eca00239f3e98854466116e42f9a2e7b4590)), closes [#27949](https://github.com/ionic-team/ionic-framework/issues/27949)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [7.2.2](https://github.com/ionic-team/ionic-framework/compare/v7.2.1...v7.2.2) (2023-08-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **datetime-button:** render correct text when passing partial date values ([#27816](https://github.com/ionic-team/ionic-framework/issues/27816)) ([bd1910b](https://github.com/ionic-team/ionic-framework/commit/bd1910ba69348877ad5f99d9db2b59d06693b91e)), closes [#27797](https://github.com/ionic-team/ionic-framework/issues/27797)
|
||||
* **input, textarea:** input does not block floating label ([#27870](https://github.com/ionic-team/ionic-framework/issues/27870)) ([f14c440](https://github.com/ionic-team/ionic-framework/commit/f14c440d6321ef9f168b272338e5cd21cab384ef)), closes [#27812](https://github.com/ionic-team/ionic-framework/issues/27812)
|
||||
* **item-options:** use correct safe area padding ([#27853](https://github.com/ionic-team/ionic-framework/issues/27853)) ([0b8f1bc](https://github.com/ionic-team/ionic-framework/commit/0b8f1bc7dd4170a2a8c9ed3aede173dd489b25ea))
|
||||
* **radio:** radios can be focused and are announced with group ([#27817](https://github.com/ionic-team/ionic-framework/issues/27817)) ([ba2f49b](https://github.com/ionic-team/ionic-framework/commit/ba2f49b8a460520d20ac198db800ea2d9e5b015f)), closes [#27438](https://github.com/ionic-team/ionic-framework/issues/27438)
|
||||
* **react, vue:** custom animations are used when going back ([#27895](https://github.com/ionic-team/ionic-framework/issues/27895)) ([824033f](https://github.com/ionic-team/ionic-framework/commit/824033f1d4b4a3e5d4c6a978a39e5bb1f33b5bb4)), closes [#27873](https://github.com/ionic-team/ionic-framework/issues/27873)
|
||||
* **select:** popover uses modern form syntax ([#27818](https://github.com/ionic-team/ionic-framework/issues/27818)) ([0c117cf](https://github.com/ionic-team/ionic-framework/commit/0c117cfe7f383b7c7837d27de5a6eee12ddd6c2f)), closes [#27071](https://github.com/ionic-team/ionic-framework/issues/27071) [#27786](https://github.com/ionic-team/ionic-framework/issues/27786)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [7.2.1](https://github.com/ionic-team/ionic-framework/compare/v7.2.0...v7.2.1) (2023-07-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **item-sliding:** buttons are not interactive on close ([#27829](https://github.com/ionic-team/ionic-framework/issues/27829)) ([6e4919c](https://github.com/ionic-team/ionic-framework/commit/6e4919caff90fc60988e5cc85ad7161844eb5b51)), closes [#22722](https://github.com/ionic-team/ionic-framework/issues/22722)
|
||||
* **modal:** body background is reset with inline card modals ([#27835](https://github.com/ionic-team/ionic-framework/issues/27835)) ([38626d9](https://github.com/ionic-team/ionic-framework/commit/38626d96809d1c6be523ea62a4fac1dec73ee891)), closes [#27830](https://github.com/ionic-team/ionic-framework/issues/27830)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [7.2.0](https://github.com/ionic-team/ionic-framework/compare/v7.1.4...v7.2.0) (2023-07-19)
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,49 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [7.2.3](https://github.com/ionic-team/ionic-framework/compare/v7.2.2...v7.2.3) (2023-08-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **button:** hidden button is added when form is set async ([#27955](https://github.com/ionic-team/ionic-framework/issues/27955)) ([e9fa300](https://github.com/ionic-team/ionic-framework/commit/e9fa30002bd5dec4f2f56a15c84eec1b3e794942)), closes [#27952](https://github.com/ionic-team/ionic-framework/issues/27952)
|
||||
* **datetime:** changing months work if partially visible ([#27917](https://github.com/ionic-team/ionic-framework/issues/27917)) ([eb19c28](https://github.com/ionic-team/ionic-framework/commit/eb19c289d6581639f6df7aff002bebdf2b27d31c)), closes [#27913](https://github.com/ionic-team/ionic-framework/issues/27913)
|
||||
* **item-sliding:** account for options added before watcher ([#27915](https://github.com/ionic-team/ionic-framework/issues/27915)) ([a0b3ef0](https://github.com/ionic-team/ionic-framework/commit/a0b3ef02af718e232246515bb873ad8c090fa55d)), closes [#27910](https://github.com/ionic-team/ionic-framework/issues/27910)
|
||||
* **many:** overlays present if isOpen is true on load ([#27933](https://github.com/ionic-team/ionic-framework/issues/27933)) ([a0e6ac6](https://github.com/ionic-team/ionic-framework/commit/a0e6ac6013552c5e3acdde33575d4aaf4d4c0bda)), closes [#27928](https://github.com/ionic-team/ionic-framework/issues/27928)
|
||||
* **modal:** setCurrentBreakpoint respects animated prop ([#27924](https://github.com/ionic-team/ionic-framework/issues/27924)) ([da55ab9](https://github.com/ionic-team/ionic-framework/commit/da55ab949ef1894738da5a6241176089b7a2b6e3)), closes [#27923](https://github.com/ionic-team/ionic-framework/issues/27923)
|
||||
* **nav:** improve reliability of swipe back gesture when quickly swiping back ([#27904](https://github.com/ionic-team/ionic-framework/issues/27904)) ([9500769](https://github.com/ionic-team/ionic-framework/commit/9500769f114d180613f0340b1a328b5e631b7188)), closes [#27893](https://github.com/ionic-team/ionic-framework/issues/27893)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [7.2.2](https://github.com/ionic-team/ionic-framework/compare/v7.2.1...v7.2.2) (2023-08-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **datetime-button:** render correct text when passing partial date values ([#27816](https://github.com/ionic-team/ionic-framework/issues/27816)) ([bd1910b](https://github.com/ionic-team/ionic-framework/commit/bd1910ba69348877ad5f99d9db2b59d06693b91e)), closes [#27797](https://github.com/ionic-team/ionic-framework/issues/27797)
|
||||
* **input, textarea:** input does not block floating label ([#27870](https://github.com/ionic-team/ionic-framework/issues/27870)) ([f14c440](https://github.com/ionic-team/ionic-framework/commit/f14c440d6321ef9f168b272338e5cd21cab384ef)), closes [#27812](https://github.com/ionic-team/ionic-framework/issues/27812)
|
||||
* **item-options:** use correct safe area padding ([#27853](https://github.com/ionic-team/ionic-framework/issues/27853)) ([0b8f1bc](https://github.com/ionic-team/ionic-framework/commit/0b8f1bc7dd4170a2a8c9ed3aede173dd489b25ea))
|
||||
* **radio:** radios can be focused and are announced with group ([#27817](https://github.com/ionic-team/ionic-framework/issues/27817)) ([ba2f49b](https://github.com/ionic-team/ionic-framework/commit/ba2f49b8a460520d20ac198db800ea2d9e5b015f)), closes [#27438](https://github.com/ionic-team/ionic-framework/issues/27438)
|
||||
* **select:** popover uses modern form syntax ([#27818](https://github.com/ionic-team/ionic-framework/issues/27818)) ([0c117cf](https://github.com/ionic-team/ionic-framework/commit/0c117cfe7f383b7c7837d27de5a6eee12ddd6c2f)), closes [#27071](https://github.com/ionic-team/ionic-framework/issues/27071) [#27786](https://github.com/ionic-team/ionic-framework/issues/27786)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [7.2.1](https://github.com/ionic-team/ionic-framework/compare/v7.2.0...v7.2.1) (2023-07-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **item-sliding:** buttons are not interactive on close ([#27829](https://github.com/ionic-team/ionic-framework/issues/27829)) ([6e4919c](https://github.com/ionic-team/ionic-framework/commit/6e4919caff90fc60988e5cc85ad7161844eb5b51)), closes [#22722](https://github.com/ionic-team/ionic-framework/issues/22722)
|
||||
* **modal:** body background is reset with inline card modals ([#27835](https://github.com/ionic-team/ionic-framework/issues/27835)) ([38626d9](https://github.com/ionic-team/ionic-framework/commit/38626d96809d1c6be523ea62a4fac1dec73ee891)), closes [#27830](https://github.com/ionic-team/ionic-framework/issues/27830)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [7.2.0](https://github.com/ionic-team/ionic-framework/compare/v7.1.4...v7.2.0) (2023-07-19)
|
||||
|
||||
|
||||
|
||||
66
core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "7.2.0",
|
||||
"version": "7.2.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ionic/core",
|
||||
"version": "7.2.0",
|
||||
"version": "7.2.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "^3.4.0",
|
||||
@@ -15,19 +15,19 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.7.3",
|
||||
"@capacitor/core": "^5.2.1",
|
||||
"@capacitor/core": "^5.2.2",
|
||||
"@capacitor/haptics": "^5.0.6",
|
||||
"@capacitor/keyboard": "^5.0.6",
|
||||
"@capacitor/status-bar": "^5.0.6",
|
||||
"@ionic/eslint-config": "^0.3.0",
|
||||
"@ionic/prettier-config": "^2.0.0",
|
||||
"@jest/core": "^27.5.1",
|
||||
"@playwright/test": "^1.36.1",
|
||||
"@playwright/test": "^1.36.2",
|
||||
"@rollup/plugin-node-resolve": "^8.4.0",
|
||||
"@rollup/plugin-virtual": "^2.0.3",
|
||||
"@stencil/angular-output-target": "^0.7.1",
|
||||
"@stencil/react-output-target": "^0.5.3",
|
||||
"@stencil/sass": "^3.0.4",
|
||||
"@stencil/sass": "^3.0.5",
|
||||
"@stencil/vue-output-target": "^0.8.6",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^14.6.0",
|
||||
@@ -607,9 +607,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@capacitor/core": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.2.1.tgz",
|
||||
"integrity": "sha512-v7nzTQZj9l99Sp0v8C7Zq8QX6Cg5ljq7ASneWk/Hc5nBR5LOj/k3a+yEx/RoclWtkxJfs89Y5k+KJTFFQ6cLoA==",
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.2.2.tgz",
|
||||
"integrity": "sha512-3jKECZC5+YD2rljMZm1e/K3AYyoxUmLDZCyofTPbRYPBSI0wJh5ZCkX+XIGzNM0o/Wokl3Voa1JB8xsLC0MPxA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
@@ -1541,13 +1541,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.36.1.tgz",
|
||||
"integrity": "sha512-YK7yGWK0N3C2QInPU6iaf/L3N95dlGdbsezLya4n0ZCh3IL7VgPGxC6Gnznh9ApWdOmkJeleT2kMTcWPRZvzqg==",
|
||||
"version": "1.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.36.2.tgz",
|
||||
"integrity": "sha512-2rVZeyPRjxfPH6J0oGJqE8YxiM1IBRyM8hyrXYK7eSiAqmbNhxwcLa7dZ7fy9Kj26V7FYia5fh9XJRq4Dqme+g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.36.1"
|
||||
"playwright-core": "1.36.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -1655,10 +1655,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@stencil/sass": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/sass/-/sass-3.0.4.tgz",
|
||||
"integrity": "sha512-k1dP0A2QBx62m250FATc1hErXxXs6Jnf4TBxdL1C/dc32Kzz2n5aCT4SodBz0ebT5WMnITauZyFqYxzCzDoKag==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/sass/-/sass-3.0.5.tgz",
|
||||
"integrity": "sha512-9nyllMXOEvHywo6fP2iwXgnq32A+OOUE36Aq7iUjzwT3wdr04qsvupO1JNIyRvmvCDF15hOKXztrZH1/wDu2Zg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12.0.0",
|
||||
"npm": ">=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@stencil/core": ">=2.0.0 || >=3.0.0-beta.0 || >= 4.0.0-beta.0 || >= 4.0.0"
|
||||
}
|
||||
@@ -8190,9 +8194,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.36.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.36.1.tgz",
|
||||
"integrity": "sha512-7+tmPuMcEW4xeCL9cp9KxmYpQYHKkyjwoXRnoeTowaeNat8PoBMk/HwCYhqkH2fRkshfKEOiVus/IhID2Pg8kg==",
|
||||
"version": "1.36.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.36.2.tgz",
|
||||
"integrity": "sha512-sQYZt31dwkqxOrP7xy2ggDfEzUxM1lodjhsQ3NMMv5uGTRDsLxU0e4xf4wwMkF2gplIxf17QMBCodSFgm6bFVQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
@@ -10784,9 +10788,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@capacitor/core": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.2.1.tgz",
|
||||
"integrity": "sha512-v7nzTQZj9l99Sp0v8C7Zq8QX6Cg5ljq7ASneWk/Hc5nBR5LOj/k3a+yEx/RoclWtkxJfs89Y5k+KJTFFQ6cLoA==",
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-5.2.2.tgz",
|
||||
"integrity": "sha512-3jKECZC5+YD2rljMZm1e/K3AYyoxUmLDZCyofTPbRYPBSI0wJh5ZCkX+XIGzNM0o/Wokl3Voa1JB8xsLC0MPxA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
@@ -11451,14 +11455,14 @@
|
||||
}
|
||||
},
|
||||
"@playwright/test": {
|
||||
"version": "1.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.36.1.tgz",
|
||||
"integrity": "sha512-YK7yGWK0N3C2QInPU6iaf/L3N95dlGdbsezLya4n0ZCh3IL7VgPGxC6Gnznh9ApWdOmkJeleT2kMTcWPRZvzqg==",
|
||||
"version": "1.36.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.36.2.tgz",
|
||||
"integrity": "sha512-2rVZeyPRjxfPH6J0oGJqE8YxiM1IBRyM8hyrXYK7eSiAqmbNhxwcLa7dZ7fy9Kj26V7FYia5fh9XJRq4Dqme+g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"fsevents": "2.3.2",
|
||||
"playwright-core": "1.36.1"
|
||||
"playwright-core": "1.36.2"
|
||||
}
|
||||
},
|
||||
"@rollup/plugin-node-resolve": {
|
||||
@@ -11532,9 +11536,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"@stencil/sass": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/sass/-/sass-3.0.4.tgz",
|
||||
"integrity": "sha512-k1dP0A2QBx62m250FATc1hErXxXs6Jnf4TBxdL1C/dc32Kzz2n5aCT4SodBz0ebT5WMnITauZyFqYxzCzDoKag==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/sass/-/sass-3.0.5.tgz",
|
||||
"integrity": "sha512-9nyllMXOEvHywo6fP2iwXgnq32A+OOUE36Aq7iUjzwT3wdr04qsvupO1JNIyRvmvCDF15hOKXztrZH1/wDu2Zg==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
@@ -16328,9 +16332,9 @@
|
||||
}
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.36.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.36.1.tgz",
|
||||
"integrity": "sha512-7+tmPuMcEW4xeCL9cp9KxmYpQYHKkyjwoXRnoeTowaeNat8PoBMk/HwCYhqkH2fRkshfKEOiVus/IhID2Pg8kg==",
|
||||
"version": "1.36.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.36.2.tgz",
|
||||
"integrity": "sha512-sQYZt31dwkqxOrP7xy2ggDfEzUxM1lodjhsQ3NMMv5uGTRDsLxU0e4xf4wwMkF2gplIxf17QMBCodSFgm6bFVQ==",
|
||||
"dev": true
|
||||
},
|
||||
"postcss": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "7.2.0",
|
||||
"version": "7.2.3",
|
||||
"description": "Base components for Ionic",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -37,19 +37,19 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.7.3",
|
||||
"@capacitor/core": "^5.2.1",
|
||||
"@capacitor/core": "^5.2.2",
|
||||
"@capacitor/haptics": "^5.0.6",
|
||||
"@capacitor/keyboard": "^5.0.6",
|
||||
"@capacitor/status-bar": "^5.0.6",
|
||||
"@ionic/eslint-config": "^0.3.0",
|
||||
"@ionic/prettier-config": "^2.0.0",
|
||||
"@jest/core": "^27.5.1",
|
||||
"@playwright/test": "^1.36.1",
|
||||
"@playwright/test": "^1.36.2",
|
||||
"@rollup/plugin-node-resolve": "^8.4.0",
|
||||
"@rollup/plugin-virtual": "^2.0.3",
|
||||
"@stencil/angular-output-target": "^0.7.1",
|
||||
"@stencil/react-output-target": "^0.5.3",
|
||||
"@stencil/sass": "^3.0.4",
|
||||
"@stencil/sass": "^3.0.5",
|
||||
"@stencil/vue-output-target": "^0.8.6",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^14.6.0",
|
||||
|
||||
@@ -62,14 +62,16 @@ const config: PlaywrightTestConfig = {
|
||||
},
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Fail fast on CI */
|
||||
maxFailures: process.env.CI ? 1 : 0,
|
||||
/* Flaky test should be either addressed or disabled until we can address them */
|
||||
retries: 0,
|
||||
maxFailures: 0,
|
||||
retries: 2,
|
||||
reportSlowTests: null,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
reporter: [
|
||||
['html'],
|
||||
['github']
|
||||
],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
|
||||
22
core/src/components.d.ts
vendored
@@ -819,7 +819,7 @@ export namespace Components {
|
||||
}
|
||||
interface IonDatetime {
|
||||
/**
|
||||
* Emits the ionCancel event and optionally closes the popover or modal that the datetime was presented in.
|
||||
* The cancel method performs the following actions: 1. Emits the ionCancel event 2. Resets the internal state of the datetime 3. Closes the parent popover or modal if "closeOverlay" is true.
|
||||
*/
|
||||
"cancel": (closeOverlay?: boolean) => Promise<void>;
|
||||
/**
|
||||
@@ -915,15 +915,15 @@ export namespace Components {
|
||||
*/
|
||||
"readonly": boolean;
|
||||
/**
|
||||
* Resets the internal state of the datetime but does not update the value. Passing a valid ISO-8601 string will reset the state of the component to the provided date. If no value is provided, the internal state will be reset to the clamped value of the min, max and today.
|
||||
* Resets the internal state of the datetime but does not update the value property. Passing a valid ISO-8601 string will reset the state of the component to the provided date. If no date string was passed but the value property is set, then the internal state of datetime will be reset to that value. Otherwise, the internal state will be reset to the clamped value of the min, max and today.
|
||||
*/
|
||||
"reset": (startDate?: string) => Promise<void>;
|
||||
/**
|
||||
* If `true`, a "Clear" button will be rendered alongside the default "Cancel" and "OK" buttons at the bottom of the `ion-datetime` component. Developers can also use the `button` slot if they want to customize these buttons. If custom buttons are set in the `button` slot then the default buttons will not be rendered.
|
||||
* If `true`, a "Clear" button will be rendered alongside the default "Cancel" and "OK" buttons at the bottom of the `ion-datetime` component. Developers can also use the `button` slot if they want to customize these buttons. If custom buttons are set in the `button` slot then the default buttons will not be rendered. Pressing the "Clear" button will call the "reset" method.
|
||||
*/
|
||||
"showClearButton": boolean;
|
||||
/**
|
||||
* If `true`, the default "Cancel" and "OK" buttons will be rendered at the bottom of the `ion-datetime` component. Developers can also use the `button` slot if they want to customize these buttons. If custom buttons are set in the `button` slot then the default buttons will not be rendered.
|
||||
* If `true`, the default "Cancel" and "OK" buttons will be rendered at the bottom of the `ion-datetime` component. Developers can also use the `button` slot if they want to customize these buttons. If custom buttons are set in the `button` slot then the default buttons will not be rendered. Pressing the "Cancel" button will call the "cancel" method. Pressing the "OK" button will the "confirm" method.
|
||||
*/
|
||||
"showDefaultButtons": boolean;
|
||||
/**
|
||||
@@ -1277,9 +1277,6 @@ export namespace Components {
|
||||
* The shape of the input. If "round" it will have an increased border radius.
|
||||
*/
|
||||
"shape"?: 'round';
|
||||
/**
|
||||
* The initial size of the control. This value is in pixels unless the value of the type attribute is `"text"` or `"password"`, in which case it is an integer number of characters. This attribute applies only when the `type` attribute is set to `"text"`, `"search"`, `"tel"`, `"url"`, `"email"`, or `"password"`, otherwise it is ignored.
|
||||
*/
|
||||
"size"?: number;
|
||||
/**
|
||||
* If `true`, the element will have its spelling and grammar checked.
|
||||
@@ -2176,7 +2173,7 @@ export namespace Components {
|
||||
*/
|
||||
"side": PositionSide;
|
||||
/**
|
||||
* Describes how to calculate the popover width. If `"cover"`, the popover width will match the width of the trigger. If `"auto"`, the popover width will be determined by the content in the popover.
|
||||
* Describes how to calculate the popover width. If `"cover"`, the popover width will match the width of the trigger. If `"auto"`, the popover width will be set to a static default value.
|
||||
*/
|
||||
"size": PopoverSize;
|
||||
/**
|
||||
@@ -4957,11 +4954,11 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"readonly"?: boolean;
|
||||
/**
|
||||
* If `true`, a "Clear" button will be rendered alongside the default "Cancel" and "OK" buttons at the bottom of the `ion-datetime` component. Developers can also use the `button` slot if they want to customize these buttons. If custom buttons are set in the `button` slot then the default buttons will not be rendered.
|
||||
* If `true`, a "Clear" button will be rendered alongside the default "Cancel" and "OK" buttons at the bottom of the `ion-datetime` component. Developers can also use the `button` slot if they want to customize these buttons. If custom buttons are set in the `button` slot then the default buttons will not be rendered. Pressing the "Clear" button will call the "reset" method.
|
||||
*/
|
||||
"showClearButton"?: boolean;
|
||||
/**
|
||||
* If `true`, the default "Cancel" and "OK" buttons will be rendered at the bottom of the `ion-datetime` component. Developers can also use the `button` slot if they want to customize these buttons. If custom buttons are set in the `button` slot then the default buttons will not be rendered.
|
||||
* If `true`, the default "Cancel" and "OK" buttons will be rendered at the bottom of the `ion-datetime` component. Developers can also use the `button` slot if they want to customize these buttons. If custom buttons are set in the `button` slot then the default buttons will not be rendered. Pressing the "Cancel" button will call the "cancel" method. Pressing the "OK" button will the "confirm" method.
|
||||
*/
|
||||
"showDefaultButtons"?: boolean;
|
||||
/**
|
||||
@@ -5339,9 +5336,6 @@ declare namespace LocalJSX {
|
||||
* The shape of the input. If "round" it will have an increased border radius.
|
||||
*/
|
||||
"shape"?: 'round';
|
||||
/**
|
||||
* The initial size of the control. This value is in pixels unless the value of the type attribute is `"text"` or `"password"`, in which case it is an integer number of characters. This attribute applies only when the `type` attribute is set to `"text"`, `"search"`, `"tel"`, `"url"`, `"email"`, or `"password"`, otherwise it is ignored.
|
||||
*/
|
||||
"size"?: number;
|
||||
/**
|
||||
* If `true`, the element will have its spelling and grammar checked.
|
||||
@@ -6189,7 +6183,7 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"side"?: PositionSide;
|
||||
/**
|
||||
* Describes how to calculate the popover width. If `"cover"`, the popover width will match the width of the trigger. If `"auto"`, the popover width will be determined by the content in the popover.
|
||||
* Describes how to calculate the popover width. If `"cover"`, the popover width will match the width of the trigger. If `"auto"`, the popover width will be set to a static default value.
|
||||
*/
|
||||
"size"?: PopoverSize;
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Watch, Component, Element, Event, Host, Method, Prop, h, readTask } from '@stencil/core';
|
||||
import type { Gesture } from '@utils/gesture';
|
||||
import { createButtonActiveGesture } from '@utils/gesture/button-active';
|
||||
import { raf } from '@utils/helpers';
|
||||
import {
|
||||
BACKDROP,
|
||||
createDelegateController,
|
||||
@@ -318,25 +319,32 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
|
||||
componentDidLoad() {
|
||||
/**
|
||||
* Do not create gesture if:
|
||||
* 1. A gesture already exists
|
||||
* 2. App is running in MD mode
|
||||
* 3. A wrapper ref does not exist
|
||||
* Only create gesture if:
|
||||
* 1. A gesture does not already exist
|
||||
* 2. App is running in iOS mode
|
||||
* 3. A wrapper ref exists
|
||||
* 4. A group ref exists
|
||||
*/
|
||||
const { groupEl, wrapperEl } = this;
|
||||
if (this.gesture || getIonMode(this) === 'md' || !wrapperEl || !groupEl) {
|
||||
return;
|
||||
if (!this.gesture && getIonMode(this) === 'ios' && wrapperEl && groupEl) {
|
||||
readTask(() => {
|
||||
const isScrollable = groupEl.scrollHeight > groupEl.clientHeight;
|
||||
if (!isScrollable) {
|
||||
this.gesture = createButtonActiveGesture(wrapperEl, (refEl: HTMLElement) =>
|
||||
refEl.classList.contains('action-sheet-button')
|
||||
);
|
||||
this.gesture.enable(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
readTask(() => {
|
||||
const isScrollable = groupEl.scrollHeight > groupEl.clientHeight;
|
||||
if (!isScrollable) {
|
||||
this.gesture = createButtonActiveGesture(wrapperEl, (refEl: HTMLElement) =>
|
||||
refEl.classList.contains('action-sheet-button')
|
||||
);
|
||||
this.gesture.enable(true);
|
||||
}
|
||||
});
|
||||
/**
|
||||
* If action sheet was rendered with isOpen="true"
|
||||
* then we should open action sheet immediately.
|
||||
*/
|
||||
if (this.isOpen === true) {
|
||||
raf(() => this.present());
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -30,5 +30,10 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ config, title }) =>
|
||||
|
||||
await expect(actionSheet).toBeHidden();
|
||||
});
|
||||
|
||||
test('should open if isOpen is true on load', async ({ page }) => {
|
||||
await page.setContent('<ion-action-sheet is-open="true"></ion-action-sheet>', config);
|
||||
await expect(page.locator('ion-action-sheet')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Component, Element, Event, Host, Listen, Method, Prop, Watch, forceUpda
|
||||
import { ENABLE_HTML_CONTENT_DEFAULT } from '@utils/config';
|
||||
import type { Gesture } from '@utils/gesture';
|
||||
import { createButtonActiveGesture } from '@utils/gesture/button-active';
|
||||
import { raf } from '@utils/helpers';
|
||||
import {
|
||||
createDelegateController,
|
||||
createTriggerController,
|
||||
@@ -346,19 +347,25 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
|
||||
componentDidLoad() {
|
||||
/**
|
||||
* Do not create gesture if:
|
||||
* 1. A gesture already exists
|
||||
* 2. App is running in MD mode
|
||||
* 3. A wrapper ref does not exist
|
||||
* Only create gesture if:
|
||||
* 1. A gesture does not already exist
|
||||
* 2. App is running in iOS mode
|
||||
* 3. A wrapper ref exists
|
||||
*/
|
||||
if (this.gesture || getIonMode(this) === 'md' || !this.wrapperEl) {
|
||||
return;
|
||||
if (!this.gesture && getIonMode(this) === 'ios' && this.wrapperEl) {
|
||||
this.gesture = createButtonActiveGesture(this.wrapperEl, (refEl: HTMLElement) =>
|
||||
refEl.classList.contains('alert-button')
|
||||
);
|
||||
this.gesture.enable(true);
|
||||
}
|
||||
|
||||
this.gesture = createButtonActiveGesture(this.wrapperEl, (refEl: HTMLElement) =>
|
||||
refEl.classList.contains('alert-button')
|
||||
);
|
||||
this.gesture.enable(true);
|
||||
/**
|
||||
* If alert was rendered with isOpen="true"
|
||||
* then we should open alert immediately.
|
||||
*/
|
||||
if (this.isOpen === true) {
|
||||
raf(() => this.present());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,5 +29,10 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ config, title }) =>
|
||||
await ionAlertDidDismiss.next();
|
||||
await expect(alert).toBeHidden();
|
||||
});
|
||||
|
||||
test('should open if isOpen is true on load', async ({ page }) => {
|
||||
await page.setContent('<ion-alert is-open="true"></ion-alert>', config);
|
||||
await expect(page.locator('ion-alert')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,19 +153,27 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
|
||||
*/
|
||||
@Event() ionBlur!: EventEmitter<void>;
|
||||
|
||||
connectedCallback(): void {
|
||||
// Allow form to be submitted through `ion-button`
|
||||
if (this.type !== 'button' && hasShadowDom(this.el)) {
|
||||
this.formEl = this.findForm();
|
||||
if (this.formEl) {
|
||||
// Create a hidden native button inside of the form
|
||||
this.formButtonEl = document.createElement('button');
|
||||
this.formButtonEl.type = this.type;
|
||||
this.formButtonEl.style.display = 'none';
|
||||
// Only submit if the button is not disabled.
|
||||
this.formButtonEl.disabled = this.disabled;
|
||||
this.formEl.appendChild(this.formButtonEl);
|
||||
private renderHiddenButton() {
|
||||
const formEl = (this.formEl = this.findForm());
|
||||
if (formEl) {
|
||||
const { formButtonEl } = this;
|
||||
|
||||
/**
|
||||
* If the form already has a rendered form button
|
||||
* then do not append a new one again.
|
||||
*/
|
||||
if (formButtonEl !== null && formEl.contains(formButtonEl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a hidden native button inside of the form
|
||||
const newFormButtonEl = (this.formButtonEl = document.createElement('button'));
|
||||
newFormButtonEl.type = this.type;
|
||||
newFormButtonEl.style.display = 'none';
|
||||
// Only submit if the button is not disabled.
|
||||
newFormButtonEl.disabled = this.disabled;
|
||||
|
||||
formEl.appendChild(newFormButtonEl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,6 +322,11 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
|
||||
if (fill == null) {
|
||||
fill = this.inToolbar || this.inListHeader ? 'clear' : 'solid';
|
||||
}
|
||||
|
||||
{
|
||||
type !== 'button' && this.renderHiddenButton();
|
||||
}
|
||||
|
||||
return (
|
||||
<Host
|
||||
onClick={this.handleClick}
|
||||
|
||||
@@ -130,6 +130,29 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, config }) =>
|
||||
|
||||
expect(submitEvent).not.toHaveReceivedEvent();
|
||||
});
|
||||
|
||||
test('should submit the form by id when form is set async', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/27952',
|
||||
});
|
||||
await page.setContent(
|
||||
`
|
||||
<form id="myForm"></form>
|
||||
<ion-button type="submit">Submit</ion-button>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const submitEvent = await page.spyOnEvent('submit');
|
||||
const button = page.locator('ion-button');
|
||||
|
||||
await button.evaluate((el: HTMLIonButtonElement) => (el.form = 'myForm'));
|
||||
|
||||
await page.click('ion-button');
|
||||
|
||||
expect(submitEvent).toHaveReceivedEvent();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe(title('should throw a warning if the form cannot be found'), () => {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
import { Button } from '../../button';
|
||||
|
||||
describe('Button: Hidden Form Button', () => {
|
||||
it('should not add multiple buttons to the form', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Button],
|
||||
html: `
|
||||
<form id="my-form"></form>
|
||||
<ion-button form="my-form" type="submit">Submit</ion-button>
|
||||
`,
|
||||
});
|
||||
|
||||
const getButtons = () => {
|
||||
return page.body.querySelectorAll('form button');
|
||||
};
|
||||
|
||||
const form = page.body.querySelectorAll('form');
|
||||
const button = page.body.querySelector('ion-button');
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(getButtons().length).toEqual(1);
|
||||
|
||||
// Re-render the component
|
||||
button.color = 'danger';
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(getButtons().length).toEqual(1);
|
||||
});
|
||||
});
|
||||
@@ -122,6 +122,42 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
await expect(dateTarget).toContainText('May 10, 2023');
|
||||
});
|
||||
test('should set only month and year when only passing month and year', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/27797',
|
||||
});
|
||||
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime-button locale="en-US" datetime="datetime"></ion-datetime-button>
|
||||
<ion-datetime id="datetime" value="2022-01" presentation="month-year"></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
await expect(page.locator('#date-button')).toContainText('January 2022');
|
||||
await expect(page.locator('#time-button')).toBeHidden();
|
||||
});
|
||||
test('should set only year when passing only year', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/27797',
|
||||
});
|
||||
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime-button locale="en-US" datetime="datetime"></ion-datetime-button>
|
||||
<ion-datetime id="datetime" value="2022" presentation="year"></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
await expect(page.locator('#date-button')).toContainText('2022');
|
||||
await expect(page.locator('#time-button')).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe(title('datetime-button: locale'), () => {
|
||||
|
||||
@@ -138,7 +138,6 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
@Element() el!: HTMLIonDatetimeElement;
|
||||
|
||||
@State() isPresented = false;
|
||||
@State() isTimePopoverOpen = false;
|
||||
|
||||
/**
|
||||
@@ -425,6 +424,9 @@ export class Datetime implements ComponentInterface {
|
||||
* if they want to customize these buttons. If custom
|
||||
* buttons are set in the `button` slot then the
|
||||
* default buttons will not be rendered.
|
||||
*
|
||||
* Pressing the "Cancel" button will call the "cancel" method.
|
||||
* Pressing the "OK" button will the "confirm" method.
|
||||
*/
|
||||
@Prop() showDefaultButtons = false;
|
||||
|
||||
@@ -435,6 +437,8 @@ export class Datetime implements ComponentInterface {
|
||||
* if they want to customize these buttons. If custom
|
||||
* buttons are set in the `button` slot then the
|
||||
* default buttons will not be rendered.
|
||||
*
|
||||
* Pressing the "Clear" button will call the "reset" method.
|
||||
*/
|
||||
@Prop() showClearButton = false;
|
||||
|
||||
@@ -541,25 +545,29 @@ export class Datetime implements ComponentInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the internal state of the datetime but does not update the value.
|
||||
* Resets the internal state of the datetime but does not update the value property.
|
||||
* Passing a valid ISO-8601 string will reset the state of the component to the provided date.
|
||||
* If no value is provided, the internal state will be reset to the clamped value of the min, max and today.
|
||||
* If no date string was passed but the value property is set, then the internal state of
|
||||
* datetime will be reset to that value. Otherwise, the internal state will be reset to the
|
||||
* clamped value of the min, max and today.
|
||||
*/
|
||||
@Method()
|
||||
async reset(startDate?: string) {
|
||||
this.processValue(startDate);
|
||||
this.processValue(startDate ?? this.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the ionCancel event and
|
||||
* optionally closes the popover
|
||||
* or modal that the datetime was
|
||||
* presented in.
|
||||
* The cancel method performs the following actions:
|
||||
* 1. Emits the ionCancel event
|
||||
* 2. Resets the internal state of the datetime
|
||||
* 3. Closes the parent popover or modal if "closeOverlay" is true.
|
||||
*/
|
||||
@Method()
|
||||
async cancel(closeOverlay = false) {
|
||||
this.ionCancel.emit();
|
||||
|
||||
this.reset();
|
||||
|
||||
if (closeOverlay) {
|
||||
this.closeParentOverlay();
|
||||
}
|
||||
@@ -883,21 +891,18 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
const getChangedMonth = (parts: DatetimeParts): DatetimeParts | undefined => {
|
||||
const box = calendarBodyRef.getBoundingClientRect();
|
||||
const root = this.el!.shadowRoot!;
|
||||
|
||||
/**
|
||||
* Get the element that is in the center of the calendar body.
|
||||
* This will be an element inside of the active month.
|
||||
* If the current scroll position is all the way to the left
|
||||
* then we have scrolled to the previous month.
|
||||
* Otherwise, assume that we have scrolled to the next
|
||||
* month. We have a tolerance of 2px to account for
|
||||
* sub pixel rendering.
|
||||
*
|
||||
* Check below the next line ensures that we did not
|
||||
* swipe and abort (i.e. we swiped but we are still on the current month).
|
||||
*/
|
||||
const elementAtCenter = root.elementFromPoint(box.x + box.width / 2, box.y + box.height / 2);
|
||||
/**
|
||||
* If there is no element then the
|
||||
* component may be re-rendering on a slow device.
|
||||
*/
|
||||
if (!elementAtCenter) return;
|
||||
|
||||
const month = elementAtCenter.closest('.calendar-month');
|
||||
if (!month) return;
|
||||
const month = calendarBodyRef.scrollLeft <= 2 ? startMonth : endMonth;
|
||||
|
||||
/**
|
||||
* The edge of the month must be lined up with
|
||||
@@ -1380,11 +1385,6 @@ export class Datetime implements ComponentInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
const clearButtonClick = () => {
|
||||
this.reset();
|
||||
this.setValue(undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
* By default we render two buttons:
|
||||
* Cancel - Dismisses the datetime and
|
||||
@@ -1410,7 +1410,7 @@ export class Datetime implements ComponentInterface {
|
||||
)}
|
||||
<div>
|
||||
{showClearButton && (
|
||||
<ion-button id="clear-button" color={this.color} onClick={() => clearButtonClick()}>
|
||||
<ion-button id="clear-button" color={this.color} onClick={() => this.reset()}>
|
||||
{this.clearText}
|
||||
</ion-button>
|
||||
)}
|
||||
@@ -2364,19 +2364,7 @@ export class Datetime implements ComponentInterface {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
disabled,
|
||||
el,
|
||||
color,
|
||||
isPresented,
|
||||
readonly,
|
||||
showMonthAndYear,
|
||||
preferWheel,
|
||||
presentation,
|
||||
size,
|
||||
} = this;
|
||||
const { name, value, disabled, el, color, readonly, showMonthAndYear, preferWheel, presentation, size } = this;
|
||||
const mode = getIonMode(this);
|
||||
const isMonthAndYearPresentation =
|
||||
presentation === 'year' || presentation === 'month' || presentation === 'month-year';
|
||||
@@ -2396,7 +2384,6 @@ export class Datetime implements ComponentInterface {
|
||||
class={{
|
||||
...createColorClasses(color, {
|
||||
[mode]: true,
|
||||
['datetime-presented']: isPresented,
|
||||
['datetime-readonly']: readonly,
|
||||
['datetime-disabled']: disabled,
|
||||
'show-month-and-year': shouldShowMonthAndYear,
|
||||
|
||||
@@ -380,40 +380,6 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This behavior does not differ across
|
||||
* modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('datetime: clear button'), () => {
|
||||
test('should clear the active calendar day', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/26258',
|
||||
});
|
||||
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2022-11-10" show-clear-button="true"></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const selectedDay = page.locator('ion-datetime .calendar-day-active');
|
||||
|
||||
await expect(selectedDay).toHaveText('10');
|
||||
|
||||
await page.click('ion-datetime #clear-button');
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(selectedDay).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This behavior does not differ across
|
||||
* modes/directions.
|
||||
|
||||
75
core/src/components/datetime/test/cancel/datetime.e2e.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('datetime: cancel method'), () => {
|
||||
test('should emit ionCancel', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const ionCancel = await page.spyOnEvent('ionCancel');
|
||||
const datetime = page.locator('ion-datetime');
|
||||
|
||||
await datetime.evaluate((el: HTMLIonDatetimeElement) => el.cancel());
|
||||
|
||||
await ionCancel.next();
|
||||
});
|
||||
|
||||
test('parent overlay should be dismissed when true is passed', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-modal>
|
||||
<ion-datetime></ion-datetime>
|
||||
</ion-modal>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
const modal = page.locator('ion-modal');
|
||||
|
||||
await modal.evaluate((el: HTMLIonModalElement) => el.present());
|
||||
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await datetime.evaluate((el: HTMLIonDatetimeElement) => el.cancel(true));
|
||||
|
||||
await ionModalDidDismiss.next();
|
||||
});
|
||||
|
||||
test('should reset the internal state of datetime', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/27975',
|
||||
});
|
||||
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2023-06-06T16:30" show-default-buttons="true"></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
|
||||
const dayOne = datetime.locator('.calendar-day[data-month="6"][data-day="1"][data-year="2023"]');
|
||||
const daySix = datetime.locator('.calendar-day[data-month="6"][data-day="6"][data-year="2023"]');
|
||||
await dayOne.click();
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(dayOne).toHaveClass(/calendar-day-active/);
|
||||
|
||||
await datetime.evaluate((el: HTMLIonDatetimeElement) => el.cancel());
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(daySix).toHaveClass(/calendar-day-active/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -301,21 +301,20 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, config }) =>
|
||||
<ion-datetime
|
||||
min="2022-01-15"
|
||||
value="2022-02-01"
|
||||
presentation="date"
|
||||
presentation="month-year"
|
||||
></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
const monthYearToggle = page.locator('ion-datetime .calendar-month-year');
|
||||
const monthColumnItems = page.locator('ion-datetime .month-column .picker-item:not(.picker-item-empty)');
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
|
||||
await monthYearToggle.click();
|
||||
await page.waitForChanges();
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
await monthColumnItems.nth(0).click(); // switch to January
|
||||
await page.waitForChanges();
|
||||
await ionChange.next();
|
||||
|
||||
await expect(datetime).toHaveJSProperty('value', '2022-01-15T00:00:00');
|
||||
});
|
||||
|
||||
100
core/src/components/datetime/test/reset/datetime.e2e.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('datetime: reset method'), () => {
|
||||
test('should reset the internal state of datetime to the set value', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime value="2023-06-06T16:30" show-default-buttons="true"></ion-datetime>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
|
||||
const dayOne = datetime.locator('.calendar-day[data-month="6"][data-day="1"][data-year="2023"]');
|
||||
const daySix = datetime.locator('.calendar-day[data-month="6"][data-day="6"][data-year="2023"]');
|
||||
await dayOne.click();
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(dayOne).toHaveClass(/calendar-day-active/);
|
||||
|
||||
await datetime.evaluate((el: HTMLIonDatetimeElement) => el.reset());
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(daySix).toHaveClass(/calendar-day-active/);
|
||||
});
|
||||
|
||||
test('should reset the internal state of datetime to the provided value', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime show-default-buttons="true"></ion-datetime>
|
||||
|
||||
<script>
|
||||
const mockToday = '2023-06-12T16:22';
|
||||
Date = class extends Date {
|
||||
constructor(...args) {
|
||||
if (args.length === 0) {
|
||||
super(mockToday)
|
||||
} else {
|
||||
super(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
|
||||
const dayOne = datetime.locator('.calendar-day[data-month="6"][data-day="1"][data-year="2023"]');
|
||||
const daySix = datetime.locator('.calendar-day[data-month="6"][data-day="6"][data-year="2023"]');
|
||||
await dayOne.click();
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(dayOne).toHaveClass(/calendar-day-active/);
|
||||
|
||||
await datetime.evaluate((el: HTMLIonDatetimeElement) => el.reset('2023-06-06T16:30'));
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(daySix).toHaveClass(/calendar-day-active/);
|
||||
});
|
||||
|
||||
test('should reset the internal state of datetime to today when value is set or passed', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime show-default-buttons="true"></ion-datetime>
|
||||
|
||||
<script>
|
||||
const mockToday = '2023-06-06T16:22';
|
||||
Date = class extends Date {
|
||||
constructor(...args) {
|
||||
if (args.length === 0) {
|
||||
super(mockToday)
|
||||
} else {
|
||||
super(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
|
||||
const dayOne = datetime.locator('.calendar-day[data-month="6"][data-day="1"][data-year="2023"]');
|
||||
await dayOne.click();
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(dayOne).toHaveClass(/calendar-day-active/);
|
||||
|
||||
await datetime.evaluate((el: HTMLIonDatetimeElement) => el.reset());
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(await datetime.locator('.calendar-day-active').count()).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -34,13 +34,14 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
const activeDayButton = page.locator('.calendar-day-active');
|
||||
const monthYearButton = page.locator('.calendar-month-year');
|
||||
const monthColumn = page.locator('.month-column');
|
||||
const yearColumn = page.locator('.year-column');
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
|
||||
await datetime.evaluate((el: HTMLIonDatetimeElement) => (el.value = '2021-10-05'));
|
||||
|
||||
@@ -49,11 +50,9 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
await page.waitForChanges();
|
||||
|
||||
// Select October 2021
|
||||
// The year will automatically switch to 2021 when selecting 10
|
||||
await monthColumn.locator('.picker-item[data-value="10"]').click();
|
||||
await page.waitForChanges();
|
||||
|
||||
await yearColumn.locator('.picker-item[data-value="2021"]').click();
|
||||
await page.waitForChanges();
|
||||
await ionChange.next();
|
||||
|
||||
// Close month/year picker
|
||||
await monthYearButton.click();
|
||||
|
||||
@@ -113,7 +113,7 @@ export const generateDayAriaLabel = (locale: string, today: boolean, refParts: D
|
||||
/**
|
||||
* MM/DD/YYYY will return midnight in the user's timezone.
|
||||
*/
|
||||
const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year} GMT+0000`);
|
||||
const date = getNormalizedDate(refParts);
|
||||
|
||||
const labelString = new Intl.DateTimeFormat(locale, {
|
||||
weekday: 'long',
|
||||
@@ -134,7 +134,7 @@ export const generateDayAriaLabel = (locale: string, today: boolean, refParts: D
|
||||
* Used for the header in MD mode.
|
||||
*/
|
||||
export const getMonthAndDay = (locale: string, refParts: DatetimeParts) => {
|
||||
const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year} GMT+0000`);
|
||||
const date = getNormalizedDate(refParts);
|
||||
return new Intl.DateTimeFormat(locale, { weekday: 'short', month: 'short', day: 'numeric', timeZone: 'UTC' }).format(
|
||||
date
|
||||
);
|
||||
@@ -147,7 +147,7 @@ export const getMonthAndDay = (locale: string, refParts: DatetimeParts) => {
|
||||
* Example: May 2021
|
||||
*/
|
||||
export const getMonthAndYear = (locale: string, refParts: DatetimeParts) => {
|
||||
const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year} GMT+0000`);
|
||||
const date = getNormalizedDate(refParts);
|
||||
return new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric', timeZone: 'UTC' }).format(date);
|
||||
};
|
||||
|
||||
@@ -183,11 +183,25 @@ export const getYear = (locale: string, refParts: DatetimeParts) => {
|
||||
return getLocalizedDateTime(locale, refParts, { year: 'numeric' });
|
||||
};
|
||||
|
||||
const getNormalizedDate = (refParts: DatetimeParts) => {
|
||||
/**
|
||||
* Given reference parts, return a JS Date object
|
||||
* with a normalized time.
|
||||
*/
|
||||
export const getNormalizedDate = (refParts: DatetimeParts) => {
|
||||
const timeString =
|
||||
refParts.hour !== undefined && refParts.minute !== undefined ? ` ${refParts.hour}:${refParts.minute}` : '';
|
||||
|
||||
return new Date(`${refParts.month}/${refParts.day}/${refParts.year}${timeString} GMT+0000`);
|
||||
/**
|
||||
* We use / notation here for the date
|
||||
* so we do not need to do extra work and pad values with zeroes.
|
||||
* Values such as YYYY-MM are still valid, so
|
||||
* we add fallback values so we still get
|
||||
* a valid date otherwise we will pass in a string
|
||||
* like "//2023". Some browsers, such as Chrome, will
|
||||
* account for this and still return a valid date. However,
|
||||
* this is not a consistent behavior across all browsers.
|
||||
*/
|
||||
return new Date(`${refParts.month ?? 1}/${refParts.day ?? 1}/${refParts.year ?? 2023}${timeString} GMT+0000`);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -82,8 +82,6 @@
|
||||
:host(.input-fill-outline) .label-text-wrapper,
|
||||
:host(.input-fill-outline) .label-text-wrapper {
|
||||
position: relative;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -581,6 +581,13 @@
|
||||
@include transform-origin(start, top);
|
||||
|
||||
max-width: 100%;
|
||||
|
||||
/**
|
||||
* The 2 ensures the label
|
||||
* remains on top of any browser
|
||||
* autofill background too.
|
||||
*/
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -268,9 +268,7 @@ export class Input implements ComponentInterface {
|
||||
*/
|
||||
@Prop() step?: string;
|
||||
|
||||
/**
|
||||
* The initial size of the control. This value is in pixels unless the value of the type attribute is `"text"` or `"password"`, in which case it is an integer number of characters. This attribute applies only when the `type` attribute is set to `"text"`, `"search"`, `"tel"`, `"url"`, `"email"`, or `"password"`, otherwise it is ignored.
|
||||
*/
|
||||
// FW-4914 Remove this property in Ionic 8
|
||||
@Prop() size?: number;
|
||||
|
||||
/**
|
||||
|
||||
@@ -186,9 +186,6 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
|
||||
expect(await input.screenshot()).toMatchSnapshot(screenshot(`input-label-slot-truncate`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('input: async label'), () => {
|
||||
test('input should re-render when label slot is added async', async ({ page }) => {
|
||||
await page.setContent(
|
||||
@@ -213,4 +210,27 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
|
||||
expect(await input.screenshot()).toMatchSnapshot(screenshot(`input-async-label`));
|
||||
});
|
||||
});
|
||||
test.describe(title('input: floating/stacked label layering'), () => {
|
||||
test('label should not be covered by text field', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/27812',
|
||||
});
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
.custom-input .native-wrapper {
|
||||
background: pink;
|
||||
}
|
||||
</style>
|
||||
<ion-input class="custom-input" label="My Label" label-placement="stacked"></ion-input>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
|
||||
expect(await input.screenshot()).toMatchSnapshot(screenshot(`input-label-layering`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
@@ -56,15 +56,17 @@ ion-item-options {
|
||||
}
|
||||
}
|
||||
|
||||
.item-options-start ion-item-option:first-child {
|
||||
@include padding-horizontal(null, var(--ion-safe-area-left));
|
||||
|
||||
/* stylelint-disable property-disallowed-list */
|
||||
[dir="ltr"] .item-options-start ion-item-option:first-child,
|
||||
[dir="rtl"] .item-options-start ion-item-option:last-child {
|
||||
padding-left: var(--ion-safe-area-left);
|
||||
}
|
||||
|
||||
.item-options-end ion-item-option:last-child {
|
||||
@include padding-horizontal(null, var(--ion-safe-area-right));
|
||||
|
||||
[dir="ltr"] .item-options-end ion-item-option:last-child,
|
||||
[dir="rtl"] .item-options-end ion-item-option:first-child {
|
||||
padding-right: var(--ion-safe-area-right);
|
||||
}
|
||||
/* stylelint-enable property-disallowed-list */
|
||||
|
||||
.item-sliding-active-slide {
|
||||
@include rtl() {
|
||||
|
||||
@@ -45,6 +45,14 @@ export class ItemOptions implements ComponentInterface {
|
||||
// Used internally for styling
|
||||
[`item-options-${mode}`]: true,
|
||||
|
||||
/**
|
||||
* Note: The "start" and "end" terms refer to the
|
||||
* direction ion-item-option instances within ion-item-options flow.
|
||||
* They do not refer to how ion-item-options flows within ion-item-sliding.
|
||||
* As a result, "item-options-start" means the ion-item-options container
|
||||
* always appears on the left, and "item-options-end" means the ion-item-options
|
||||
* container always appears on the right.
|
||||
*/
|
||||
'item-options-start': !isEnd,
|
||||
'item-options-end': isEnd,
|
||||
}}
|
||||
|
||||
@@ -29,6 +29,9 @@ ion-item-sliding .item {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.item-sliding-closing ion-item-options {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.item-sliding-active-swipe-end .item-options-end .item-option-expandable {
|
||||
@include multi-dir() {
|
||||
|
||||
@@ -76,12 +76,19 @@ export class ItemSliding implements ComponentInterface {
|
||||
this.item = el.querySelector('ion-item');
|
||||
this.contentEl = findClosestIonContent(el);
|
||||
|
||||
await this.updateOptions();
|
||||
|
||||
/**
|
||||
* The MutationObserver needs to be added before we
|
||||
* call updateOptions below otherwise we may miss
|
||||
* ion-item-option elements that are added to the DOM
|
||||
* while updateOptions is running and before the MutationObserver
|
||||
* has been initialized.
|
||||
*/
|
||||
this.mutationObserver = watchForOptions<HTMLIonItemOptionElement>(el, 'ion-item-option', async () => {
|
||||
await this.updateOptions();
|
||||
});
|
||||
|
||||
await this.updateOptions();
|
||||
|
||||
this.gesture = (await import('../../utils/gesture')).createGesture({
|
||||
el,
|
||||
gestureName: 'item-swipe',
|
||||
@@ -407,6 +414,9 @@ export class ItemSliding implements ComponentInterface {
|
||||
if (!this.item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { el } = this;
|
||||
|
||||
const style = this.item.style;
|
||||
this.openAmount = openAmount;
|
||||
|
||||
@@ -425,6 +435,12 @@ export class ItemSliding implements ComponentInterface {
|
||||
? SlidingState.Start | SlidingState.SwipeStart
|
||||
: SlidingState.Start;
|
||||
} else {
|
||||
/**
|
||||
* The sliding options should not be
|
||||
* clickable while the item is closing.
|
||||
*/
|
||||
el.classList.add('item-sliding-closing');
|
||||
|
||||
/**
|
||||
* Item sliding cannot be interrupted
|
||||
* while closing the item. If it did,
|
||||
@@ -441,6 +457,7 @@ export class ItemSliding implements ComponentInterface {
|
||||
if (this.gesture) {
|
||||
this.gesture.enable(!this.disabled);
|
||||
}
|
||||
el.classList.remove('item-sliding-closing');
|
||||
}, 600);
|
||||
|
||||
openSlidingItem = undefined;
|
||||
|
||||
@@ -398,7 +398,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
|
||||
});
|
||||
const ariaDisabled = disabled || childStyles['item-interactive-disabled'] ? 'true' : null;
|
||||
const fillValue = fill || 'none';
|
||||
const inList = hostContext('ion-list', this.el);
|
||||
const inList = hostContext('ion-list', this.el) && !hostContext('ion-radio-group', this.el);
|
||||
|
||||
return (
|
||||
<Host
|
||||
|
||||
51
core/src/components/item/test/a11y/item.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Radio } from '../../../radio/radio.tsx';
|
||||
import { RadioGroup } from '../../../radio-group/radio-group.tsx';
|
||||
import { Item } from '../../item.tsx';
|
||||
import { List } from '../../../list/list.tsx';
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
describe('ion-item', () => {
|
||||
it('should not have a role when used without list', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Item],
|
||||
html: `<ion-item>Hello World</ion-item>`,
|
||||
});
|
||||
|
||||
const item = page.body.querySelector('ion-item');
|
||||
expect(item.getAttribute('role')).toBe(null);
|
||||
});
|
||||
|
||||
it('should have a listitem role when used inside list', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Item, List],
|
||||
html: `
|
||||
<ion-list>
|
||||
<ion-item>
|
||||
Hello World
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
`,
|
||||
});
|
||||
|
||||
const item = page.body.querySelector('ion-item');
|
||||
expect(item.getAttribute('role')).toBe('listitem');
|
||||
});
|
||||
|
||||
it('should not have a role when used inside radio group and list', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Radio, RadioGroup, Item, List],
|
||||
html: `
|
||||
<ion-list>
|
||||
<ion-radio-group value="a">
|
||||
<ion-item>
|
||||
<ion-radio value="other-value" aria-label="my radio"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
</ion-list>
|
||||
`,
|
||||
});
|
||||
|
||||
const item = page.body.querySelector('ion-item');
|
||||
expect(item.getAttribute('role')).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -23,5 +23,10 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
await ionLoadingDidDismiss.next();
|
||||
await expect(loading).toBeHidden();
|
||||
});
|
||||
|
||||
test('should open if isOpen is true on load', async ({ page }) => {
|
||||
await page.setContent('<ion-loading is-open="true"></ion-loading>', config);
|
||||
await expect(page.locator('ion-loading')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,9 +51,9 @@ export const iosLeaveAnimation = (baseEl: HTMLElement, opts: ModalAnimationOptio
|
||||
|
||||
presentingEl.style.setProperty('overflow', '');
|
||||
|
||||
const numModals = Array.from(bodyEl.querySelectorAll('ion-modal')).filter(
|
||||
(m) => m.presentingElement !== undefined
|
||||
).length;
|
||||
const numModals = (
|
||||
Array.from(bodyEl.querySelectorAll('ion-modal:not(.overlay-hidden)')) as HTMLIonModalElement[]
|
||||
).filter((m) => m.presentingElement !== undefined).length;
|
||||
if (numModals <= 1) {
|
||||
bodyEl.style.setProperty('background-color', '');
|
||||
}
|
||||
|
||||
@@ -31,6 +31,12 @@ export interface MoveSheetToBreakpointOptions {
|
||||
* `true` if the sheet can be transitioned and dismissed off the view.
|
||||
*/
|
||||
canDismiss?: boolean;
|
||||
|
||||
/**
|
||||
* If `true`, the sheet will animate to the breakpoint.
|
||||
* If `false`, the sheet will jump directly to the breakpoint.
|
||||
*/
|
||||
animated: boolean;
|
||||
}
|
||||
|
||||
export const createSheetGesture = (
|
||||
@@ -246,11 +252,17 @@ export const createSheetGesture = (
|
||||
breakpoint: closest,
|
||||
breakpointOffset: offset,
|
||||
canDismiss: canDismissBlocksGesture,
|
||||
|
||||
/**
|
||||
* The swipe is user-driven, so we should
|
||||
* always animate when the gesture ends.
|
||||
*/
|
||||
animated: true,
|
||||
});
|
||||
};
|
||||
|
||||
const moveSheetToBreakpoint = (options: MoveSheetToBreakpointOptions) => {
|
||||
const { breakpoint, canDismiss, breakpointOffset } = options;
|
||||
const { breakpoint, canDismiss, breakpointOffset, animated } = options;
|
||||
/**
|
||||
* canDismiss should only prevent snapping
|
||||
* when users are trying to dismiss. If canDismiss
|
||||
@@ -360,7 +372,7 @@ export const createSheetGesture = (
|
||||
},
|
||||
{ oneTimeCallback: true }
|
||||
)
|
||||
.progressEnd(1, 0, 500);
|
||||
.progressEnd(1, 0, animated ? 500 : 0);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -761,7 +761,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
const { currentBreakpoint, moveSheetToBreakpoint, canDismiss, breakpoints } = this;
|
||||
const { currentBreakpoint, moveSheetToBreakpoint, canDismiss, breakpoints, animated } = this;
|
||||
|
||||
if (currentBreakpoint === breakpoint) {
|
||||
return;
|
||||
@@ -772,6 +772,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
breakpoint,
|
||||
breakpointOffset: 1 - currentBreakpoint!,
|
||||
canDismiss: canDismiss !== undefined && canDismiss !== true && breakpoints![0] === 0,
|
||||
animated,
|
||||
});
|
||||
await this.sheetTransition;
|
||||
this.sheetTransition = undefined;
|
||||
|
||||
@@ -20,5 +20,10 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
await ionModalDidDismiss.next();
|
||||
await expect(modal).toBeHidden();
|
||||
});
|
||||
|
||||
test('should open if isOpen is true on load', async ({ page }) => {
|
||||
await page.setContent('<ion-modal is-open="true"></ion-modal>', config);
|
||||
await expect(page.locator('ion-modal')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ import { VIEW_STATE_ATTACHED, VIEW_STATE_DESTROYED, VIEW_STATE_NEW, convertToVie
|
||||
export class Nav implements NavOutlet {
|
||||
private transInstr: TransitionInstruction[] = [];
|
||||
private sbAni?: Animation;
|
||||
private animationEnabled = true;
|
||||
private gestureOrAnimationInProgress = false;
|
||||
private useRouter = false;
|
||||
private isTransitioning = false;
|
||||
private destroyed = false;
|
||||
@@ -869,7 +869,36 @@ export class Nav implements NavOutlet {
|
||||
// or if it is a portal (modal, actionsheet, etc.)
|
||||
const opts = ti.opts!;
|
||||
|
||||
const progressCallback = opts.progressAnimation ? (ani: Animation | undefined) => (this.sbAni = ani) : undefined;
|
||||
const progressCallback = opts.progressAnimation
|
||||
? (ani: Animation | undefined) => {
|
||||
/**
|
||||
* Because this progress callback is called asynchronously
|
||||
* it is possible for the gesture to start and end before
|
||||
* the animation is ever set. In that scenario, we should
|
||||
* immediately call progressEnd so that the transition promise
|
||||
* resolves and the gesture does not get locked up.
|
||||
*/
|
||||
if (ani !== undefined && !this.gestureOrAnimationInProgress) {
|
||||
this.gestureOrAnimationInProgress = true;
|
||||
ani.onFinish(
|
||||
() => {
|
||||
this.gestureOrAnimationInProgress = false;
|
||||
},
|
||||
{ oneTimeCallback: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* Playing animation to beginning
|
||||
* with a duration of 0 prevents
|
||||
* any flickering when the animation
|
||||
* is later cleaned up.
|
||||
*/
|
||||
ani.progressEnd(0, 0, 0);
|
||||
} else {
|
||||
this.sbAni = ani;
|
||||
}
|
||||
}
|
||||
: undefined;
|
||||
const mode = getIonMode(this);
|
||||
const enteringEl = enteringView.element!;
|
||||
const leavingEl = leavingView && leavingView.element!;
|
||||
@@ -1008,15 +1037,16 @@ export class Nav implements NavOutlet {
|
||||
|
||||
private canStart(): boolean {
|
||||
return (
|
||||
!this.gestureOrAnimationInProgress &&
|
||||
!!this.swipeGesture &&
|
||||
!this.isTransitioning &&
|
||||
this.transInstr.length === 0 &&
|
||||
this.animationEnabled &&
|
||||
this.canGoBackSync()
|
||||
);
|
||||
}
|
||||
|
||||
private onStart() {
|
||||
this.gestureOrAnimationInProgress = true;
|
||||
this.pop({ direction: 'back', progressAnimation: true });
|
||||
}
|
||||
|
||||
@@ -1028,10 +1058,9 @@ export class Nav implements NavOutlet {
|
||||
|
||||
private onEnd(shouldComplete: boolean, stepValue: number, dur: number) {
|
||||
if (this.sbAni) {
|
||||
this.animationEnabled = false;
|
||||
this.sbAni.onFinish(
|
||||
() => {
|
||||
this.animationEnabled = true;
|
||||
this.gestureOrAnimationInProgress = false;
|
||||
},
|
||||
{ oneTimeCallback: true }
|
||||
);
|
||||
@@ -1055,6 +1084,8 @@ export class Nav implements NavOutlet {
|
||||
}
|
||||
|
||||
this.sbAni.progressEnd(shouldComplete ? 1 : 0, newStepValue, dur);
|
||||
} else {
|
||||
this.gestureOrAnimationInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Method, Prop, State, Watch, h } from '@stencil/core';
|
||||
import { raf } from '@utils/helpers';
|
||||
import {
|
||||
createDelegateController,
|
||||
createTriggerController,
|
||||
@@ -199,6 +200,16 @@ export class Picker implements ComponentInterface, OverlayInterface {
|
||||
setOverlayId(this.el);
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
/**
|
||||
* If picker was rendered with isOpen="true"
|
||||
* then we should open picker immediately.
|
||||
*/
|
||||
if (this.isOpen === true) {
|
||||
raf(() => this.present());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Present the picker overlay after it has been created.
|
||||
*/
|
||||
|
||||
@@ -23,5 +23,10 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
await ionPickerDidDismiss.next();
|
||||
await expect(picker).toBeHidden();
|
||||
});
|
||||
|
||||
test('should open if isOpen is true on load', async ({ page }) => {
|
||||
await page.setContent('<ion-picker is-open="true"></ion-picker>', config);
|
||||
await expect(page.locator('ion-picker')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -180,8 +180,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
/**
|
||||
* Describes how to calculate the popover width.
|
||||
* If `"cover"`, the popover width will match the width of the trigger.
|
||||
* If `"auto"`, the popover width will be determined by the content in
|
||||
* the popover.
|
||||
* If `"auto"`, the popover width will be set to a static default value.
|
||||
*/
|
||||
@Prop() size: PopoverSize = 'auto';
|
||||
|
||||
|
||||
11
core/src/components/popover/test/is-open/popover.e2e.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('popover: isOpen'), () => {
|
||||
test('should open if isOpen is true on load', async ({ page }) => {
|
||||
await page.setContent('<ion-popover is-open="true"></ion-popover>', config);
|
||||
await expect(page.locator('ion-popover')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -30,23 +30,19 @@
|
||||
</ion-list-header>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 1</ion-label>
|
||||
<ion-radio value="1" slot="start"></ion-radio>
|
||||
<ion-radio value="1">Item 1</ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 2</ion-label>
|
||||
<ion-radio value="2" slot="start"></ion-radio>
|
||||
<ion-radio value="2">Item 2</ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 3</ion-label>
|
||||
<ion-radio value="3" slot="start"></ion-radio>
|
||||
<ion-radio value="3">Item 3</ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 4</ion-label>
|
||||
<ion-radio value="4" slot="start"></ion-radio>
|
||||
<ion-radio value="4">Item 4</ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
</ion-list>
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import type { Locator } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
import type { E2EPage } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('radio-group: basic'), () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.goto(`/src/components/radio-group/test/basic`, config);
|
||||
|
||||
const list = page.locator('ion-list');
|
||||
|
||||
await expect(list).toHaveScreenshot(screenshot(`radio-group-diff`));
|
||||
});
|
||||
});
|
||||
});
|
||||
import { RadioFixture } from '../fixtures';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
@@ -31,8 +19,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
`
|
||||
<ion-radio-group value="one" allow-empty-selection="false">
|
||||
<ion-item>
|
||||
<ion-label>One</ion-label>
|
||||
<ion-radio id="one" value="one"></ion-radio>
|
||||
<ion-radio id="one" value="one">One</ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`,
|
||||
@@ -48,8 +35,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
`
|
||||
<ion-radio-group value="one" allow-empty-selection="true">
|
||||
<ion-item>
|
||||
<ion-label>One</ion-label>
|
||||
<ion-radio id="one" value="one"></ion-radio>
|
||||
<ion-radio id="one" value="one">One</ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`,
|
||||
@@ -65,8 +51,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
`
|
||||
<ion-radio-group value="one" allow-empty-selection="false">
|
||||
<ion-item>
|
||||
<ion-label>One</ion-label>
|
||||
<ion-radio id="one" value="one"></ion-radio>
|
||||
<ion-radio id="one" value="one">One</ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`,
|
||||
@@ -82,8 +67,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
`
|
||||
<ion-radio-group value="one" allow-empty-selection="true">
|
||||
<ion-item>
|
||||
<ion-label>One</ion-label>
|
||||
<ion-radio id="one" value="one"></ion-radio>
|
||||
<ion-radio id="one" value="one">One</ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`,
|
||||
@@ -99,18 +83,15 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
`
|
||||
<ion-radio-group value="1">
|
||||
<ion-item>
|
||||
<ion-label>Item 1</ion-label>
|
||||
<ion-radio value="1" slot="start"></ion-radio>
|
||||
<ion-radio value="1">Item 1</ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 2</ion-label>
|
||||
<ion-radio value="2" slot="start"></ion-radio>
|
||||
<ion-radio value="2">Item 2</ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 3</ion-label>
|
||||
<ion-radio value="3" slot="start"></ion-radio>
|
||||
<ion-radio value="3">Item 3</ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`,
|
||||
@@ -130,34 +111,3 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class RadioFixture {
|
||||
readonly page: E2EPage;
|
||||
|
||||
private radio!: Locator;
|
||||
|
||||
constructor(page: E2EPage) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async checkRadio(method: 'keyboard' | 'mouse', selector = 'ion-radio') {
|
||||
const { page } = this;
|
||||
const radio = (this.radio = page.locator(selector));
|
||||
|
||||
if (method === 'keyboard') {
|
||||
await radio.focus();
|
||||
await page.keyboard.press('Space');
|
||||
} else {
|
||||
await radio.click();
|
||||
}
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
return radio;
|
||||
}
|
||||
|
||||
async expectChecked(state: boolean) {
|
||||
const { radio } = this;
|
||||
await expect(radio.locator('input')).toHaveJSProperty('checked', state);
|
||||
}
|
||||
}
|
||||
|
||||
39
core/src/components/radio-group/test/fixtures.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
import type { E2EPage } from '@utils/test/playwright';
|
||||
|
||||
export class RadioFixture {
|
||||
readonly page: E2EPage;
|
||||
|
||||
private radio!: Locator;
|
||||
|
||||
constructor(page: E2EPage) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async checkRadio(method: 'keyboard' | 'mouse', selector = 'ion-radio') {
|
||||
const { page } = this;
|
||||
const radio = (this.radio = page.locator(selector));
|
||||
|
||||
if (method === 'keyboard') {
|
||||
await radio.focus();
|
||||
await page.keyboard.press('Space');
|
||||
} else {
|
||||
await radio.click();
|
||||
}
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
return radio;
|
||||
}
|
||||
|
||||
async expectChecked(state: boolean) {
|
||||
const { radio } = this;
|
||||
|
||||
if (state) {
|
||||
await expect(radio).toHaveClass(/radio-checked/);
|
||||
} else {
|
||||
await expect(radio).not.toHaveClass(/radio-checked/);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,32 +31,19 @@
|
||||
</ion-list-header>
|
||||
|
||||
<ion-item>
|
||||
<ion-label
|
||||
>Biff
|
||||
<span id="biff"></span>
|
||||
</ion-label>
|
||||
<ion-radio value="biff" slot="start"></ion-radio>
|
||||
<ion-radio value="biff">Biff</ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label
|
||||
>Griff
|
||||
<span id="griff"></span>
|
||||
</ion-label>
|
||||
<ion-radio value="griff" slot="start"></ion-radio>
|
||||
<ion-radio value="griff">Griff</ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label
|
||||
>Buford
|
||||
<span id="buford"></span>
|
||||
</ion-label>
|
||||
<ion-radio value="buford" slot="start"></ion-radio>
|
||||
<ion-radio value="buford">Buford</ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>George</ion-label>
|
||||
<ion-radio value="george" disabled slot="start"></ion-radio>
|
||||
<ion-radio value="george" disabled>George</ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
|
||||
56
core/src/components/radio-group/test/legacy/basic/index.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Radio Group - Basic</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>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Radio Group - Basic</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-radio-group name="items" id="group" value="1">
|
||||
<ion-list-header>
|
||||
<ion-label>Radio Group Header</ion-label>
|
||||
</ion-list-header>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 1</ion-label>
|
||||
<ion-radio value="1" slot="start"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 2</ion-label>
|
||||
<ion-radio value="2" slot="start"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 3</ion-label>
|
||||
<ion-radio value="3" slot="start"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 4</ion-label>
|
||||
<ion-radio value="4" slot="start"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,163 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import type { Locator } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
import type { E2EPage } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('radio-group: basic'), () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.goto(`/src/components/radio-group/test/legacy/basic`, config);
|
||||
|
||||
const list = page.locator('ion-list');
|
||||
|
||||
await expect(list).toHaveScreenshot(screenshot(`radio-group-diff`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('radio-group: interaction'), () => {
|
||||
let radioFixture: RadioFixture;
|
||||
|
||||
test.beforeEach(({ page }) => {
|
||||
radioFixture = new RadioFixture(page);
|
||||
});
|
||||
|
||||
test('spacebar should not deselect without allowEmptySelection', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-radio-group value="one" allow-empty-selection="false">
|
||||
<ion-item>
|
||||
<ion-label>One</ion-label>
|
||||
<ion-radio id="one" value="one"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await radioFixture.checkRadio('keyboard');
|
||||
await radioFixture.expectChecked(true);
|
||||
});
|
||||
|
||||
test('spacebar should deselect with allowEmptySelection', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-radio-group value="one" allow-empty-selection="true">
|
||||
<ion-item>
|
||||
<ion-label>One</ion-label>
|
||||
<ion-radio id="one" value="one"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await radioFixture.checkRadio('keyboard');
|
||||
await radioFixture.expectChecked(false);
|
||||
});
|
||||
|
||||
test('click should not deselect without allowEmptySelection', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-radio-group value="one" allow-empty-selection="false">
|
||||
<ion-item>
|
||||
<ion-label>One</ion-label>
|
||||
<ion-radio id="one" value="one"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await radioFixture.checkRadio('mouse');
|
||||
await radioFixture.expectChecked(true);
|
||||
});
|
||||
|
||||
test('click should deselect with allowEmptySelection', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-radio-group value="one" allow-empty-selection="true">
|
||||
<ion-item>
|
||||
<ion-label>One</ion-label>
|
||||
<ion-radio id="one" value="one"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await radioFixture.checkRadio('mouse');
|
||||
await radioFixture.expectChecked(false);
|
||||
});
|
||||
|
||||
test('programmatically assigning a value should update the checked radio', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-radio-group value="1">
|
||||
<ion-item>
|
||||
<ion-label>Item 1</ion-label>
|
||||
<ion-radio value="1" slot="start"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 2</ion-label>
|
||||
<ion-radio value="2" slot="start"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Item 3</ion-label>
|
||||
<ion-radio value="3" slot="start"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const radioGroup = page.locator('ion-radio-group');
|
||||
const radioOne = page.locator('ion-radio[value="1"]');
|
||||
const radioTwo = page.locator('ion-radio[value="2"]');
|
||||
|
||||
await radioGroup.evaluate((el: HTMLIonRadioGroupElement) => (el.value = '2'));
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(radioOne).not.toHaveClass(/radio-checked/);
|
||||
await expect(radioTwo).toHaveClass(/radio-checked/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class RadioFixture {
|
||||
readonly page: E2EPage;
|
||||
|
||||
private radio!: Locator;
|
||||
|
||||
constructor(page: E2EPage) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async checkRadio(method: 'keyboard' | 'mouse', selector = 'ion-radio') {
|
||||
const { page } = this;
|
||||
const radio = (this.radio = page.locator(selector));
|
||||
|
||||
if (method === 'keyboard') {
|
||||
await radio.focus();
|
||||
await page.keyboard.press('Space');
|
||||
} else {
|
||||
await radio.click();
|
||||
}
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
return radio;
|
||||
}
|
||||
|
||||
async expectChecked(state: boolean) {
|
||||
const { radio } = this;
|
||||
await expect(radio.locator('input')).toHaveJSProperty('checked', state);
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
114
core/src/components/radio-group/test/legacy/form/index.html
Normal file
@@ -0,0 +1,114 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Radio Group - Form</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>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Radio Group - Form</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="outer-content">
|
||||
<form>
|
||||
<ion-list>
|
||||
<ion-radio-group name="tannen" id="group" value="biff">
|
||||
<ion-list-header>
|
||||
<ion-label>Luckiest Man On Earth</ion-label>
|
||||
</ion-list-header>
|
||||
|
||||
<ion-item>
|
||||
<ion-label
|
||||
>Biff
|
||||
<span id="biff"></span>
|
||||
</ion-label>
|
||||
<ion-radio value="biff" slot="start"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label
|
||||
>Griff
|
||||
<span id="griff"></span>
|
||||
</ion-label>
|
||||
<ion-radio value="griff" slot="start"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label
|
||||
>Buford
|
||||
<span id="buford"></span>
|
||||
</ion-label>
|
||||
<ion-radio value="buford" slot="start"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>George</ion-label>
|
||||
<ion-radio value="george" disabled slot="start"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-button type="submit">Submit</ion-button>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
</ion-list>
|
||||
</form>
|
||||
|
||||
<p style="margin: 20px">
|
||||
Value:
|
||||
<span id="value"></span>
|
||||
</p>
|
||||
|
||||
<p style="margin: 20px">
|
||||
Changes:
|
||||
<span id="changes">0</span>
|
||||
</p>
|
||||
</ion-content>
|
||||
|
||||
<style>
|
||||
.outer-content {
|
||||
--background: #f2f2f2;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var changes = 0;
|
||||
|
||||
document.getElementById('group').addEventListener('ionChange', function (ev) {
|
||||
document.getElementById('value').textContent = ev.detail.value;
|
||||
changes++;
|
||||
document.getElementById('changes').textContent = changes;
|
||||
});
|
||||
|
||||
var biff = 0;
|
||||
document.querySelector('[value="biff"]').addEventListener('ionSelect', function (ev) {
|
||||
biff++;
|
||||
document.getElementById('biff').textContent = biff;
|
||||
});
|
||||
|
||||
var griff = 0;
|
||||
document.querySelector('[value="griff"]').addEventListener('ionSelect', function (ev) {
|
||||
griff++;
|
||||
document.getElementById('griff').textContent = griff;
|
||||
});
|
||||
|
||||
var buford = 0;
|
||||
document.querySelector('[value="buford"]').addEventListener('ionSelect', function (ev) {
|
||||
buford++;
|
||||
document.getElementById('buford').textContent = buford;
|
||||
});
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('radio-group: form'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/radio-group/test/legacy/form', config);
|
||||
});
|
||||
|
||||
test('selecting an option should update the value', async ({ page }) => {
|
||||
const radioGroup = page.locator('ion-radio-group');
|
||||
const ionChange = await page.spyOnEvent('ionChange');
|
||||
const griffRadio = page.locator('ion-radio[value="griff"]');
|
||||
await expect(radioGroup).toHaveAttribute('value', 'biff');
|
||||
|
||||
await griffRadio.click();
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(ionChange).toHaveReceivedEventDetail({ value: 'griff', event: { isTrusted: true } });
|
||||
});
|
||||
|
||||
test('selecting a disabled option should not update the value', async ({ page }) => {
|
||||
const value = page.locator('#value');
|
||||
const disabledRadio = page.locator('ion-radio[value="george"]');
|
||||
|
||||
await expect(value).toHaveText('');
|
||||
await expect(disabledRadio).toHaveAttribute('disabled', '');
|
||||
|
||||
await disabledRadio.click({ force: true });
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(value).toHaveText('');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Radio Group - Search</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>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Radio Group - Form</ion-title>
|
||||
</ion-toolbar>
|
||||
<ion-toolbar>
|
||||
<ion-searchbar placeholder="Type to filter..." debounce="0"></ion-searchbar>
|
||||
</ion-toolbar>
|
||||
<ion-toolbar>
|
||||
<ion-note id="value">Current value:</ion-note>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="outer-content">
|
||||
<ion-radio-group allow-empty-selection></ion-radio-group>
|
||||
</ion-content>
|
||||
|
||||
<style>
|
||||
ion-note {
|
||||
padding: 0 10px;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
const radioGroup = document.querySelector('ion-radio-group');
|
||||
const searchbar = document.querySelector('ion-searchbar');
|
||||
const currentValue = document.querySelector('#value');
|
||||
|
||||
radioGroup.addEventListener('ionChange', (ev) => {
|
||||
currentValue.innerText = `Current value: ${ev.detail.value}`;
|
||||
});
|
||||
|
||||
searchbar.addEventListener('ionChange', (ev) => {
|
||||
filter(ev.detail.value);
|
||||
});
|
||||
|
||||
const filter = (value) => {
|
||||
const query = value != null ? value.toLowerCase() : '';
|
||||
const data = [
|
||||
{ id: 0, value: 'zero' },
|
||||
{ id: 1, value: 'one' },
|
||||
{ id: 2, value: 'two' },
|
||||
{ id: 3, value: 'three' },
|
||||
];
|
||||
|
||||
let html = '';
|
||||
|
||||
data.forEach((d) => {
|
||||
if (d.value.includes(query)) {
|
||||
html += `
|
||||
<ion-item>
|
||||
<ion-label>Item ${d.value}</ion-label>
|
||||
<ion-radio value="${d.value}"></ion-radio>
|
||||
</ion-item>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
radioGroup.innerHTML = html;
|
||||
};
|
||||
|
||||
filter();
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,42 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not var across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('radio-group'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/radio-group/test/legacy/search', config);
|
||||
});
|
||||
|
||||
test.describe('radio-group: state', () => {
|
||||
test('radio should remain checked after being removed/readded to the dom', async ({ page }) => {
|
||||
const radioGroup = page.locator('ion-radio-group');
|
||||
const radio = page.locator('ion-radio[value=two]');
|
||||
const searchbarInput = page.locator('ion-searchbar input');
|
||||
|
||||
// select radio
|
||||
await radio.click();
|
||||
await expect(radio.locator('input')).toHaveJSProperty('checked', true);
|
||||
|
||||
// filter radio so it is not in DOM
|
||||
await page.fill('ion-searchbar input', 'zero');
|
||||
await searchbarInput.evaluate((el) => el.blur());
|
||||
await page.waitForChanges();
|
||||
await expect(radio).toBeHidden();
|
||||
|
||||
// ensure radio group has the same value
|
||||
await expect(radioGroup).toHaveJSProperty('value', 'two');
|
||||
|
||||
// clear the search so the radio appears
|
||||
await page.fill('ion-searchbar input', '');
|
||||
await searchbarInput.evaluate((el) => el.blur());
|
||||
await page.waitForChanges();
|
||||
|
||||
// ensure that the new radio instance is still checked
|
||||
await expect(radio.locator('input')).toHaveJSProperty('checked', true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -65,8 +65,7 @@
|
||||
if (d.value.includes(query)) {
|
||||
html += `
|
||||
<ion-item>
|
||||
<ion-label>Item ${d.value}</ion-label>
|
||||
<ion-radio value="${d.value}"></ion-radio>
|
||||
<ion-radio value="${d.value}">Item ${d.value}</ion-radio>
|
||||
</ion-item>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,42 +1,41 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
import { RadioFixture } from '../fixtures';
|
||||
|
||||
/**
|
||||
* This behavior does not var across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('radio-group'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.describe(title('radio-group: search'), () => {
|
||||
test('radio should remain checked after being removed/readded to the dom', async ({ page }) => {
|
||||
await page.goto('/src/components/radio-group/test/search', config);
|
||||
});
|
||||
|
||||
test.describe('radio-group: state', () => {
|
||||
test('radio should remain checked after being removed/readded to the dom', async ({ page }) => {
|
||||
const radioGroup = page.locator('ion-radio-group');
|
||||
const radio = page.locator('ion-radio[value=two]');
|
||||
const searchbarInput = page.locator('ion-searchbar input');
|
||||
const radioFixture = new RadioFixture(page);
|
||||
|
||||
// select radio
|
||||
await radio.click();
|
||||
await expect(radio.locator('input')).toHaveJSProperty('checked', true);
|
||||
const radioGroup = page.locator('ion-radio-group');
|
||||
const searchbarInput = page.locator('ion-searchbar input');
|
||||
|
||||
// filter radio so it is not in DOM
|
||||
await page.fill('ion-searchbar input', 'zero');
|
||||
await searchbarInput.evaluate((el) => el.blur());
|
||||
await page.waitForChanges();
|
||||
await expect(radio).toBeHidden();
|
||||
// select radio
|
||||
const radio = await radioFixture.checkRadio('mouse', 'ion-radio[value=two]');
|
||||
await radioFixture.expectChecked(true);
|
||||
|
||||
// ensure radio group has the same value
|
||||
await expect(radioGroup).toHaveJSProperty('value', 'two');
|
||||
// filter radio so it is not in DOM
|
||||
await page.fill('ion-searchbar input', 'zero');
|
||||
await searchbarInput.evaluate((el) => el.blur());
|
||||
await page.waitForChanges();
|
||||
await expect(radio).toBeHidden();
|
||||
|
||||
// clear the search so the radio appears
|
||||
await page.fill('ion-searchbar input', '');
|
||||
await searchbarInput.evaluate((el) => el.blur());
|
||||
await page.waitForChanges();
|
||||
// ensure radio group has the same value
|
||||
await expect(radioGroup).toHaveJSProperty('value', 'two');
|
||||
|
||||
// ensure that the new radio instance is still checked
|
||||
await expect(radio.locator('input')).toHaveJSProperty('checked', true);
|
||||
});
|
||||
// clear the search so the radio appears
|
||||
await page.fill('ion-searchbar input', '');
|
||||
await searchbarInput.evaluate((el) => el.blur());
|
||||
await page.waitForChanges();
|
||||
|
||||
// ensure that the new radio instance is still checked
|
||||
await radioFixture.expectChecked(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,8 +2,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Method, Prop, State, Watch, h } from '@stencil/core';
|
||||
import type { LegacyFormController } from '@utils/forms';
|
||||
import { createLegacyFormController } from '@utils/forms';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import { addEventListener, getAriaLabel, inheritAriaAttributes, removeEventListener } from '@utils/helpers';
|
||||
import { addEventListener, getAriaLabel, removeEventListener } from '@utils/helpers';
|
||||
import { printIonWarning } from '@utils/logging';
|
||||
import { createColorClasses, hostContext } from '@utils/theme';
|
||||
|
||||
@@ -31,7 +30,6 @@ export class Radio implements ComponentInterface {
|
||||
private radioGroup: HTMLIonRadioGroupElement | null = null;
|
||||
private nativeInput!: HTMLInputElement;
|
||||
private legacyFormController!: LegacyFormController;
|
||||
private inheritedAttributes: Attributes = {};
|
||||
|
||||
// This flag ensures we log the deprecation warning at most once.
|
||||
private hasLoggedDeprecationWarning = false;
|
||||
@@ -135,8 +133,7 @@ export class Radio implements ComponentInterface {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
const element = this.legacyFormController.hasLegacyControl() ? this.el : this.nativeInput;
|
||||
element.focus();
|
||||
this.el.focus();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@@ -167,12 +164,6 @@ export class Radio implements ComponentInterface {
|
||||
|
||||
componentWillLoad() {
|
||||
this.emitStyle();
|
||||
|
||||
if (!this.legacyFormController.hasLegacyControl()) {
|
||||
this.inheritedAttributes = {
|
||||
...inheritAriaAttributes(this.el),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('checked')
|
||||
@@ -201,7 +192,34 @@ export class Radio implements ComponentInterface {
|
||||
};
|
||||
|
||||
private onClick = () => {
|
||||
this.checked = this.nativeInput.checked;
|
||||
const { radioGroup, checked } = this;
|
||||
|
||||
/**
|
||||
* The legacy control uses a native input inside
|
||||
* of the radio host, so we can set this.checked
|
||||
* to the state of the nativeInput. RadioGroup
|
||||
* will prevent the native input from checking if
|
||||
* allowEmptySelection="false" by calling ev.preventDefault().
|
||||
*/
|
||||
if (this.legacyFormController.hasLegacyControl()) {
|
||||
this.checked = this.nativeInput.checked;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* The modern control does not use a native input
|
||||
* inside of the radio host, so we cannot rely on the
|
||||
* ev.preventDefault() behavior above. If the radio
|
||||
* is checked and the parent radio group allows for empty
|
||||
* selection, then we can set the checked state to false.
|
||||
* Otherwise, the checked state should always be set
|
||||
* to true because the checked state cannot be toggled.
|
||||
*/
|
||||
if (checked && radioGroup?.allowEmptySelection) {
|
||||
this.checked = false;
|
||||
} else {
|
||||
this.checked = true;
|
||||
}
|
||||
};
|
||||
|
||||
private onFocus = () => {
|
||||
@@ -232,23 +250,14 @@ export class Radio implements ComponentInterface {
|
||||
}
|
||||
|
||||
private renderRadio() {
|
||||
const {
|
||||
checked,
|
||||
disabled,
|
||||
inputId,
|
||||
color,
|
||||
el,
|
||||
justify,
|
||||
labelPlacement,
|
||||
inheritedAttributes,
|
||||
hasLabel,
|
||||
buttonTabindex,
|
||||
} = this;
|
||||
const { checked, disabled, color, el, justify, labelPlacement, hasLabel, buttonTabindex } = this;
|
||||
const mode = getIonMode(this);
|
||||
const inItem = hostContext('ion-item', el);
|
||||
|
||||
return (
|
||||
<Host
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
onClick={this.onClick}
|
||||
class={createColorClasses(color, {
|
||||
[mode]: true,
|
||||
@@ -261,21 +270,12 @@ export class Radio implements ComponentInterface {
|
||||
'ion-activatable': !inItem,
|
||||
'ion-focusable': !inItem,
|
||||
})}
|
||||
role="radio"
|
||||
aria-checked={checked ? 'true' : 'false'}
|
||||
aria-disabled={disabled ? 'true' : null}
|
||||
tabindex={buttonTabindex}
|
||||
>
|
||||
<label class="radio-wrapper">
|
||||
{/*
|
||||
The native control must be rendered
|
||||
before the visible label text due to https://bugs.webkit.org/show_bug.cgi?id=251951
|
||||
*/}
|
||||
<input
|
||||
type="radio"
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
id={inputId}
|
||||
tabindex={buttonTabindex}
|
||||
ref={(nativeEl) => (this.nativeInput = nativeEl as HTMLInputElement)}
|
||||
{...inheritedAttributes}
|
||||
/>
|
||||
<div
|
||||
class={{
|
||||
'label-text-wrapper': true,
|
||||
|
||||
@@ -16,10 +16,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
});
|
||||
|
||||
test.describe(title('radio: keyboard navigation'), () => {
|
||||
test.beforeEach(async ({ page, skip }) => {
|
||||
// TODO (FW-2979)
|
||||
skip.browser('webkit', 'Safari 16 only allows text fields and pop-up menus to be focused.');
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-app>
|
||||
|
||||
@@ -95,7 +95,7 @@ configs().forEach(({ title, screenshot, config }) => {
|
||||
test('should apply color correctly', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-radio slot="start" value="pepperoni" color="success"></ion-radio>
|
||||
<ion-radio legacy="true" value="pepperoni" color="success"></ion-radio>
|
||||
`,
|
||||
config
|
||||
);
|
||||
@@ -138,7 +138,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test('radio should be checked when activated even without a radio group', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-radio slot="start" value="pepperoni"></ion-radio>
|
||||
<ion-radio legacy="true" value="pepperoni"></ion-radio>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@import "./select-popover";
|
||||
@import "./select-popover.md.vars";
|
||||
|
||||
ion-list ion-radio {
|
||||
ion-list ion-radio::part(container) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComponentInterface } from '@stencil/core';
|
||||
import { Element, Component, Host, Prop, h } from '@stencil/core';
|
||||
import { Element, Component, Host, Prop, h, forceUpdate } from '@stencil/core';
|
||||
import { safeCall } from '@utils/overlays';
|
||||
import { getClassMap } from '@utils/theme';
|
||||
|
||||
@@ -116,19 +116,28 @@ export class SelectPopover implements ComponentInterface {
|
||||
|
||||
renderCheckboxOptions(options: SelectPopoverOption[]) {
|
||||
return options.map((option) => (
|
||||
<ion-item class={getClassMap(option.cssClass)}>
|
||||
<ion-item
|
||||
class={{
|
||||
// TODO FW-4784
|
||||
'item-checkbox-checked': option.checked,
|
||||
...getClassMap(option.cssClass),
|
||||
}}
|
||||
>
|
||||
<ion-checkbox
|
||||
slot="start"
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
checked={option.checked}
|
||||
legacy={true}
|
||||
justify="start"
|
||||
labelPlacement="end"
|
||||
onIonChange={(ev) => {
|
||||
this.setChecked(ev);
|
||||
this.callOptionHandler(ev);
|
||||
// TODO FW-4784
|
||||
forceUpdate(this);
|
||||
}}
|
||||
></ion-checkbox>
|
||||
<ion-label>{option.text}</ion-label>
|
||||
>
|
||||
{option.text}
|
||||
</ion-checkbox>
|
||||
</ion-item>
|
||||
));
|
||||
}
|
||||
@@ -139,12 +148,16 @@ export class SelectPopover implements ComponentInterface {
|
||||
return (
|
||||
<ion-radio-group value={checked} onIonChange={(ev) => this.callOptionHandler(ev)}>
|
||||
{options.map((option) => (
|
||||
<ion-item class={getClassMap(option.cssClass)}>
|
||||
<ion-label>{option.text}</ion-label>
|
||||
<ion-item
|
||||
class={{
|
||||
// TODO FW-4784
|
||||
'item-radio-checked': option.value === checked,
|
||||
...getClassMap(option.cssClass),
|
||||
}}
|
||||
>
|
||||
<ion-radio
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
legacy={true}
|
||||
onClick={() => this.dismissParentPopover()}
|
||||
onKeyUp={(ev) => {
|
||||
if (ev.key === ' ') {
|
||||
@@ -156,7 +169,9 @@ export class SelectPopover implements ComponentInterface {
|
||||
this.dismissParentPopover();
|
||||
}
|
||||
}}
|
||||
></ion-radio>
|
||||
>
|
||||
{option.text}
|
||||
</ion-radio>
|
||||
</ion-item>
|
||||
))}
|
||||
</ion-radio-group>
|
||||
|
||||
@@ -81,11 +81,11 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
selectPopoverPage = new SelectPopoverPage(page);
|
||||
});
|
||||
test('should not have visual regressions with single selection', async () => {
|
||||
await selectPopoverPage.setup(config, options, false);
|
||||
await selectPopoverPage.setup(config, checkedOptions, false);
|
||||
await selectPopoverPage.screenshot(screenshot, 'select-popover-diff');
|
||||
});
|
||||
test('should not have visual regressions with multiple selection', async () => {
|
||||
await selectPopoverPage.setup(config, options, true);
|
||||
await selectPopoverPage.setup(config, checkedOptions, true);
|
||||
await selectPopoverPage.screenshot(screenshot, 'select-popover-multiple-diff');
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -302,4 +302,27 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
|
||||
await expect(page.locator('.container')).toHaveScreenshot(screenshot(`textarea-multi-line-sizing`));
|
||||
});
|
||||
});
|
||||
test.describe(title('textarea: floating/stacked label layering'), () => {
|
||||
test('label should not be covered by text field', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/27812',
|
||||
});
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
.custom-textarea .native-wrapper {
|
||||
background: pink;
|
||||
}
|
||||
</style>
|
||||
<ion-textarea class="custom-textarea" label="My Label" label-placement="stacked"></ion-textarea>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const textarea = page.locator('ion-textarea');
|
||||
|
||||
expect(await textarea.screenshot()).toMatchSnapshot(screenshot(`textarea-label-layering`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
@@ -82,8 +82,6 @@
|
||||
:host(.textarea-fill-outline) .label-text-wrapper,
|
||||
:host(.textarea-fill-outline) .label-text-wrapper {
|
||||
position: relative;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -629,6 +629,13 @@
|
||||
@include padding(0px);
|
||||
|
||||
max-width: 100%;
|
||||
|
||||
/**
|
||||
* The 2 ensures the label
|
||||
* remains on top of any browser
|
||||
* autofill background too.
|
||||
*/
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,5 +24,10 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
await ionToastDidDismiss.next();
|
||||
await expect(toast).toBeHidden();
|
||||
});
|
||||
|
||||
test('should open if isOpen is true on load', async ({ page }) => {
|
||||
await page.setContent('<ion-toast is-open="true"></ion-toast>', config);
|
||||
await expect(page.locator('ion-toast')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { State, Watch, Component, Element, Event, h, Host, Method, Prop } from '@stencil/core';
|
||||
import { ENABLE_HTML_CONTENT_DEFAULT } from '@utils/config';
|
||||
import { raf } from '@utils/helpers';
|
||||
import { printIonWarning } from '@utils/logging';
|
||||
import {
|
||||
createDelegateController,
|
||||
@@ -253,6 +254,16 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
setOverlayId(this.el);
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
/**
|
||||
* If toast was rendered with isOpen="true"
|
||||
* then we should open toast immediately.
|
||||
*/
|
||||
if (this.isOpen === true) {
|
||||
raf(() => this.present());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Present the toast overlay after it has been created.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { doc } from '@utils/browser';
|
||||
|
||||
import type { Config } from '../../interface';
|
||||
import { now, pointerCoord } from '../helpers';
|
||||
|
||||
export const startTapClick = (config: Config) => {
|
||||
if (doc === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let lastTouch = -MOUSE_WAIT * 10;
|
||||
let lastActivated = 0;
|
||||
|
||||
@@ -143,7 +149,6 @@ export const startTapClick = (config: Config) => {
|
||||
}
|
||||
};
|
||||
|
||||
const doc = document;
|
||||
doc.addEventListener('ionGestureCaptured', cancelActive);
|
||||
|
||||
doc.addEventListener('touchstart', onTouchStart, true);
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [7.2.3](https://github.com/ionic-team/ionic-framework/compare/v7.2.2...v7.2.3) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @ionic/docs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [7.2.2](https://github.com/ionic-team/ionic-framework/compare/v7.2.1...v7.2.2) (2023-08-02)
|
||||
|
||||
**Note:** Version bump only for package @ionic/docs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [7.2.1](https://github.com/ionic-team/ionic-framework/compare/v7.2.0...v7.2.1) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @ionic/docs
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [7.2.0](https://github.com/ionic-team/ionic-framework/compare/v7.1.4...v7.2.0) (2023-07-19)
|
||||
|
||||
**Note:** Version bump only for package @ionic/docs
|
||||
|
||||
4
docs/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@ionic/docs",
|
||||
"version": "7.2.0",
|
||||
"version": "7.2.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ionic/docs",
|
||||
"version": "7.2.0",
|
||||
"version": "7.2.3",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/docs",
|
||||
"version": "7.2.0",
|
||||
"version": "7.2.3",
|
||||
"description": "Pre-packaged API documentation for the Ionic docs.",
|
||||
"main": "core.json",
|
||||
"types": "core.d.ts",
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"docs",
|
||||
"packages/*"
|
||||
],
|
||||
"version": "7.2.0"
|
||||
"version": "7.2.3"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [7.2.3](https://github.com/ionic-team/ionic-framework/compare/v7.2.2...v7.2.3) (2023-08-09)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular-server
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [7.2.2](https://github.com/ionic-team/ionic-framework/compare/v7.2.1...v7.2.2) (2023-08-02)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular-server
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [7.2.1](https://github.com/ionic-team/ionic-framework/compare/v7.2.0...v7.2.1) (2023-07-26)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular-server
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [7.2.0](https://github.com/ionic-team/ionic-framework/compare/v7.1.4...v7.2.0) (2023-07-19)
|
||||
|
||||
**Note:** Version bump only for package @ionic/angular-server
|
||||
|
||||
18
packages/angular-server/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@ionic/angular-server",
|
||||
"version": "7.2.0",
|
||||
"version": "7.2.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ionic/angular-server",
|
||||
"version": "7.2.0",
|
||||
"version": "7.2.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ionic/core": "^7.2.0"
|
||||
"@ionic/core": "^7.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-eslint/eslint-plugin": "^14.0.0",
|
||||
@@ -1060,9 +1060,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@ionic/core": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.1.4.tgz",
|
||||
"integrity": "sha512-/h32Sc7Jd8csdMZ6BhddoTGC/X7cO0tIOeDuyC/ypMarXajScHc0pgVEBneAA7PVBEVxsoeMDP7yuqJt6Duaiw==",
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.2.3.tgz",
|
||||
"integrity": "sha512-9ms4JTozhoFzOlgVRozuRcXiyW5YDNlPqJYZx6xVPYPfcRUTIANQ9PZbU/Xqb9RA1xucTSgbKy91nt/hzJIalg==",
|
||||
"dependencies": {
|
||||
"@stencil/core": "^3.4.0",
|
||||
"ionicons": "7.1.0",
|
||||
@@ -7342,9 +7342,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@ionic/core": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.1.4.tgz",
|
||||
"integrity": "sha512-/h32Sc7Jd8csdMZ6BhddoTGC/X7cO0tIOeDuyC/ypMarXajScHc0pgVEBneAA7PVBEVxsoeMDP7yuqJt6Duaiw==",
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.2.3.tgz",
|
||||
"integrity": "sha512-9ms4JTozhoFzOlgVRozuRcXiyW5YDNlPqJYZx6xVPYPfcRUTIANQ9PZbU/Xqb9RA1xucTSgbKy91nt/hzJIalg==",
|
||||
"requires": {
|
||||
"@stencil/core": "^3.4.0",
|
||||
"ionicons": "7.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/angular-server",
|
||||
"version": "7.2.0",
|
||||
"version": "7.2.3",
|
||||
"description": "Angular SSR Module for Ionic",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -61,6 +61,6 @@
|
||||
},
|
||||
"prettier": "@ionic/prettier-config",
|
||||
"dependencies": {
|
||||
"@ionic/core": "^7.2.0"
|
||||
"@ionic/core": "^7.2.3"
|
||||
}
|
||||
}
|
||||
|
||||