Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08448573b1 | ||
|
|
3799d456d9 | ||
|
|
c6deb51b1e | ||
|
|
b04aea6fb6 | ||
|
|
267e21d63d | ||
|
|
4b10d7dc6f | ||
|
|
6dcb143307 | ||
|
|
11554a5d35 | ||
|
|
ba8d8f4896 | ||
|
|
322319397c | ||
|
|
4df0e0f4c0 | ||
|
|
2149ba2c8d | ||
|
|
b6b43ae292 | ||
|
|
7794a11215 | ||
|
|
8eaeb22e7a | ||
|
|
cd5c27a12a | ||
|
|
71e25ef549 | ||
|
|
14b6538d98 | ||
|
|
295fa00527 | ||
|
|
353159149a | ||
|
|
87bde81a94 | ||
|
|
eb725fce6e | ||
|
|
0030be8195 | ||
|
|
c2bc756ffc | ||
|
|
f532a5d4b7 | ||
|
|
b71f2e9189 | ||
|
|
709a816615 | ||
|
|
e63028ee53 | ||
|
|
bd266f09ef | ||
|
|
3f8346e718 | ||
|
|
05928e3877 | ||
|
|
64c1373f53 | ||
|
|
01917ee0ce | ||
|
|
cdfb4f37ad | ||
|
|
1b11b82eaa | ||
|
|
e101f2e022 | ||
|
|
000f55303e | ||
|
|
6d0b4297dc | ||
|
|
270526e4f2 | ||
|
|
234d14a32d | ||
|
|
a90097cdb1 | ||
|
|
1c281dc4ee | ||
|
|
845071c97a | ||
|
|
f6188c47e9 | ||
|
|
8ee42bbc1e | ||
|
|
23763abf79 | ||
|
|
470decca7b | ||
|
|
3216108ca1 | ||
|
|
4bffe976d9 | ||
|
|
ec14e13780 | ||
|
|
fcc728faf2 | ||
|
|
89508fb891 | ||
|
|
3628ea875a | ||
|
|
0fdcb32ce0 | ||
|
|
ee2fa19a1e | ||
|
|
2d6eeee267 | ||
|
|
6dc52d2d7c | ||
|
|
ffdaa3b286 | ||
|
|
93364b93c4 | ||
|
|
c3b58f1620 | ||
|
|
5a7314553a | ||
|
|
322d7c98cf | ||
|
|
cb6007363a | ||
|
|
e32fbe0210 | ||
|
|
47ba703a57 | ||
|
|
7294e969bb | ||
|
|
be7561d0d4 | ||
|
|
c67e6299d7 | ||
|
|
bb1fb2877b | ||
|
|
b7b383bee0 | ||
|
|
cdb4456be2 | ||
|
|
bbcbf5c425 | ||
|
|
78fb1b9a06 |
@@ -5,7 +5,7 @@ runs:
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
|
||||
@@ -5,7 +5,7 @@ runs:
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
|
||||
@@ -11,7 +11,7 @@ runs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
@@ -11,7 +11,7 @@ runs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
working-directory: ./core
|
||||
|
||||
@@ -5,7 +5,7 @@ runs:
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
|
||||
@@ -5,7 +5,7 @@ runs:
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
|
||||
@@ -5,7 +5,7 @@ runs:
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
|
||||
@@ -5,7 +5,7 @@ runs:
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
|
||||
@@ -21,7 +21,7 @@ runs:
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
# Provenance requires npm 9.5.0+
|
||||
- name: Install latest npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
@@ -5,7 +5,7 @@ runs:
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
|
||||
@@ -5,7 +5,7 @@ runs:
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
working-directory: ./core
|
||||
|
||||
@@ -15,7 +15,7 @@ runs:
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
|
||||
@@ -8,7 +8,7 @@ runs:
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
working-directory: ./core
|
||||
|
||||
@@ -8,7 +8,7 @@ runs:
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
|
||||
@@ -8,7 +8,7 @@ runs:
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
|
||||
@@ -8,7 +8,7 @@ runs:
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: ionic-core
|
||||
|
||||
@@ -9,7 +9,7 @@ runs:
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 20.x
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./artifacts
|
||||
|
||||
4
.github/workflows/assign-issues.yml
vendored
@@ -11,8 +11,8 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: 'Auto-assign issue'
|
||||
uses: pozil/auto-assign-issue@c5bca5027e680b9e8411b826d16947afd8c76b32 # v2.0.0
|
||||
uses: pozil/auto-assign-issue@39c06395cbac76e79afc4ad4e5c5c6db6ecfdd2e # v2.2.0
|
||||
with:
|
||||
assignees: brandyscarney, thetaPC, joselrio, rugoncalves, BenOsodrac, JoaoFerreira-FrontEnd, OS-giulianasilva, tanner-reits
|
||||
assignees: brandyscarney, thetaPC, ShaneK
|
||||
numOfAssignee: 1
|
||||
allowSelfAssign: false
|
||||
|
||||
2
.github/workflows/build.yml
vendored
@@ -140,7 +140,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
apps: [ng16, ng17, ng18]
|
||||
apps: [ng16, ng17, ng18, ng19]
|
||||
needs: [build-angular, build-angular-server]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
2
.github/workflows/stencil-nightly.yml
vendored
@@ -150,7 +150,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
apps: [ng16, ng17, ng18]
|
||||
apps: [ng16, ng17, ng18, ng19]
|
||||
needs: [build-angular, build-angular-server]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
114
CHANGELOG.md
@@ -3,6 +3,120 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [8.4.5](https://github.com/ionic-team/ionic-framework/compare/v8.4.4...v8.4.5) (2025-03-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **vue:** pin Vue output target to latest release ([#30248](https://github.com/ionic-team/ionic-framework/issues/30248)) ([3799d45](https://github.com/ionic-team/ionic-framework/commit/3799d456d9461faac9a5e2c44f187329f113b3db)), closes [#30221](https://github.com/ionic-team/ionic-framework/issues/30221)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.4.4](https://github.com/ionic-team/ionic-framework/compare/v8.4.3...v8.4.4) (2025-03-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **alert:** change focused element and improve keyboard navigation ([#30220](https://github.com/ionic-team/ionic-framework/issues/30220)) ([4df0e0f](https://github.com/ionic-team/ionic-framework/commit/4df0e0f4c00faec33f5ddc802945bf4ad9dc53d3))
|
||||
* **capacitor:** replace deprecated platform check method ([#30195](https://github.com/ionic-team/ionic-framework/issues/30195)) ([b6b43ae](https://github.com/ionic-team/ionic-framework/commit/b6b43ae2925f8a12b35cabd43abd9d838bc9714f))
|
||||
* **capacitor:** use proper types for capacitor v7 support ([#30228](https://github.com/ionic-team/ionic-framework/issues/30228)) ([2149ba2](https://github.com/ionic-team/ionic-framework/commit/2149ba2c8d56a3a8ef4a6de89de1292f6efe3031))
|
||||
* **range:** handle unsupported values for range min and max ([#30070](https://github.com/ionic-team/ionic-framework/issues/30070)) ([3223193](https://github.com/ionic-team/ionic-framework/commit/322319397ca46bafda7ca0d2e3fb4cc554432d6a)), closes [#29667](https://github.com/ionic-team/ionic-framework/issues/29667)
|
||||
* **segment-button:** protect connectedCallback for when segment-content has not yet been created ([#30138](https://github.com/ionic-team/ionic-framework/issues/30138)) ([14b6538](https://github.com/ionic-team/ionic-framework/commit/14b6538d98303cb753d881ec6978fb98f53ed54c))
|
||||
* **select:** auto-scroll to selected item for all interfaces ([#30202](https://github.com/ionic-team/ionic-framework/issues/30202)) ([8eaeb22](https://github.com/ionic-team/ionic-framework/commit/8eaeb22e7a967100ffaadae8c8221e2e4888a3b6)), closes [#19296](https://github.com/ionic-team/ionic-framework/issues/19296)
|
||||
* **toggle:** trigger focus and blur on click ([#30234](https://github.com/ionic-team/ionic-framework/issues/30234)) ([ba8d8f4](https://github.com/ionic-team/ionic-framework/commit/ba8d8f489607537b3dac915cfc9f2c32a74b994c))
|
||||
* **vue:** update output target and properly emit events ([#30227](https://github.com/ionic-team/ionic-framework/issues/30227)) ([11554a5](https://github.com/ionic-team/ionic-framework/commit/11554a5d3590c660dbf609931dcb63cc2daf79cb)), closes [#30206](https://github.com/ionic-team/ionic-framework/issues/30206) [#30178](https://github.com/ionic-team/ionic-framework/issues/30178) [#30177](https://github.com/ionic-team/ionic-framework/issues/30177) [#30170](https://github.com/ionic-team/ionic-framework/issues/30170)
|
||||
* **vue:** update output target and resolve type issues ([#30239](https://github.com/ionic-team/ionic-framework/issues/30239)) ([6dcb143](https://github.com/ionic-team/ionic-framework/commit/6dcb143307682793ac4fd46d03efa5868a49e87d)), closes [#30179](https://github.com/ionic-team/ionic-framework/issues/30179)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.4.3](https://github.com/ionic-team/ionic-framework/compare/v8.4.2...v8.4.3) (2025-01-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **vue:** update Stencil Vue output target ([#30159](https://github.com/ionic-team/ionic-framework/issues/30159)) ([eb725fc](https://github.com/ionic-team/ionic-framework/commit/eb725fce6eb15facd8a1c21be11a1b2d46336479))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.4.2](https://github.com/ionic-team/ionic-framework/compare/v8.4.1...v8.4.2) (2025-01-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **segment:** add logic to connect to segment-view in `componentDidLoad()` callback ([#30060](https://github.com/ionic-team/ionic-framework/issues/30060)) ([000f553](https://github.com/ionic-team/ionic-framework/commit/000f55303e459c583e642337fb1894f419f37d48)), closes [#30000](https://github.com/ionic-team/ionic-framework/issues/30000)
|
||||
* **select-modal:** match radio styles to iOS native ([#30119](https://github.com/ionic-team/ionic-framework/issues/30119)) ([3f8346e](https://github.com/ionic-team/ionic-framework/commit/3f8346e718ae3a6eb5008d739f10b6898b84ca9b))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.4.1](https://github.com/ionic-team/ionic-framework/compare/v8.4.0...v8.4.1) (2024-11-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **header:** use aria attributes to hide small title when collapsed ([#30027](https://github.com/ionic-team/ionic-framework/issues/30027)) ([23763ab](https://github.com/ionic-team/ionic-framework/commit/23763abf797f9a4ba8262225760f718e9dcc4782)), closes [#29347](https://github.com/ionic-team/ionic-framework/issues/29347)
|
||||
* **menu:** hide from screen readers while animating ([#30036](https://github.com/ionic-team/ionic-framework/issues/30036)) ([845071c](https://github.com/ionic-team/ionic-framework/commit/845071c97a856d45eb5e0bb81d9c270bc38bb604))
|
||||
* **overlays:** announce info after opening based on platform ([#30025](https://github.com/ionic-team/ionic-framework/issues/30025)) ([f6188c4](https://github.com/ionic-team/ionic-framework/commit/f6188c47e9278fe69fd9d250c65156edbe5ef32e))
|
||||
* **overlays:** focus management with checkbox/radio ([#30026](https://github.com/ionic-team/ionic-framework/issues/30026)) ([8ee42bb](https://github.com/ionic-team/ionic-framework/commit/8ee42bbc1e0bf4731d20040c7853756722f1a4b2))
|
||||
* **toast:** swipe gesture works with custom container layout ([#29999](https://github.com/ionic-team/ionic-framework/issues/29999)) ([470decc](https://github.com/ionic-team/ionic-framework/commit/470decca7b6b89ef74095ef0bb7909b93640cd78)), closes [#29998](https://github.com/ionic-team/ionic-framework/issues/29998)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [8.4.0](https://github.com/ionic-team/ionic-framework/compare/v8.3.4...v8.4.0) (2024-11-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **alert:** use correct heading structure for subHeader when header exists ([#29964](https://github.com/ionic-team/ionic-framework/issues/29964)) ([0fdcb32](https://github.com/ionic-team/ionic-framework/commit/0fdcb32ce0f99b284b314f79f7d0b071bc37faec))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **menu:** pass role to ionWillClose and ionDidClose events ([#29954](https://github.com/ionic-team/ionic-framework/issues/29954)) ([ee2fa19](https://github.com/ionic-team/ionic-framework/commit/ee2fa19a1e9f09d492c7c08340d95ba6a56ebb2b))
|
||||
* **segment-view:** adds support for new `ion-segment-view` component ([#29969](https://github.com/ionic-team/ionic-framework/issues/29969)) ([89508fb](https://github.com/ionic-team/ionic-framework/commit/89508fb89172900b1d11cc3fc18883f57a7fbab6))
|
||||
* **select:** add `modal` as interface ([#29972](https://github.com/ionic-team/ionic-framework/issues/29972)) ([3628ea8](https://github.com/ionic-team/ionic-framework/commit/3628ea875a66a717783de5e0a4df440872339040))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.3.4](https://github.com/ionic-team/ionic-framework/compare/v8.3.3...v8.3.4) (2024-10-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **angular:** add missing 'compareWith' input to standalone ion-radio-group ([#29870](https://github.com/ionic-team/ionic-framework/issues/29870)) ([47ba703](https://github.com/ionic-team/ionic-framework/commit/47ba703a57d1ca506f943f6b790d0bf7583d79cb)), closes [#29826](https://github.com/ionic-team/ionic-framework/issues/29826)
|
||||
* **backdrop:** remove tabindex for improved accessibility ([#29956](https://github.com/ionic-team/ionic-framework/issues/29956)) ([7294e96](https://github.com/ionic-team/ionic-framework/commit/7294e969bb913692eaf28e54860614f445132713)), closes [#29773](https://github.com/ionic-team/ionic-framework/issues/29773)
|
||||
* **input, textarea:** ensure screen readers announce helper and error text when focused ([#29958](https://github.com/ionic-team/ionic-framework/issues/29958)) ([5a73145](https://github.com/ionic-team/ionic-framework/commit/5a7314553a8def87bd19275640c92dd72a6ef1a4))
|
||||
* **overlay:** hide from screen readers while animating ([#29951](https://github.com/ionic-team/ionic-framework/issues/29951)) ([cb60073](https://github.com/ionic-team/ionic-framework/commit/cb6007363a8d42b5f126945427c2bfc3d7209c21)), closes [#29857](https://github.com/ionic-team/ionic-framework/issues/29857)
|
||||
* **overlays:** do not hide root when toast appears ([#29962](https://github.com/ionic-team/ionic-framework/issues/29962)) ([322d7c9](https://github.com/ionic-team/ionic-framework/commit/322d7c98cf6613df0b0db3f119e3f892e6a17e7b)), closes [#29773](https://github.com/ionic-team/ionic-framework/issues/29773)
|
||||
* **overlays:** hide the focus trap div from screen readers ([#29970](https://github.com/ionic-team/ionic-framework/issues/29970)) ([c3b58f1](https://github.com/ionic-team/ionic-framework/commit/c3b58f1620bcb74db43e3983ef570b7b982abd83)), closes [#29858](https://github.com/ionic-team/ionic-framework/issues/29858)
|
||||
* **vue:** incorrect view rendered when using router.go(-n) ([#29877](https://github.com/ionic-team/ionic-framework/issues/29877)) ([e32fbe0](https://github.com/ionic-team/ionic-framework/commit/e32fbe02102fe80db29f73c26496a40852032354)), closes [#28201](https://github.com/ionic-team/ionic-framework/issues/28201) [#28201](https://github.com/ionic-team/ionic-framework/issues/28201) [#29847](https://github.com/ionic-team/ionic-framework/issues/29847)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.3.3](https://github.com/ionic-team/ionic-framework/compare/v8.3.2...v8.3.3) (2024-10-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **tabs, tab-bar:** use standalone tab bar in Vue, React ([#29940](https://github.com/ionic-team/ionic-framework/issues/29940)) ([b7b383b](https://github.com/ionic-team/ionic-framework/commit/b7b383bee080b72de2e6307ff9a9a051314c69ed)), closes [#29885](https://github.com/ionic-team/ionic-framework/issues/29885) [#29924](https://github.com/ionic-team/ionic-framework/issues/29924)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.3.2](https://github.com/ionic-team/ionic-framework/compare/v8.3.1...v8.3.2) (2024-10-02)
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,112 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [8.4.5](https://github.com/ionic-team/ionic-framework/compare/v8.4.4...v8.4.5) (2025-03-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **vue:** pin Vue output target to latest release ([#30248](https://github.com/ionic-team/ionic-framework/issues/30248)) ([3799d45](https://github.com/ionic-team/ionic-framework/commit/3799d456d9461faac9a5e2c44f187329f113b3db)), closes [#30221](https://github.com/ionic-team/ionic-framework/issues/30221)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.4.4](https://github.com/ionic-team/ionic-framework/compare/v8.4.3...v8.4.4) (2025-03-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **alert:** change focused element and improve keyboard navigation ([#30220](https://github.com/ionic-team/ionic-framework/issues/30220)) ([4df0e0f](https://github.com/ionic-team/ionic-framework/commit/4df0e0f4c00faec33f5ddc802945bf4ad9dc53d3))
|
||||
* **capacitor:** replace deprecated platform check method ([#30195](https://github.com/ionic-team/ionic-framework/issues/30195)) ([b6b43ae](https://github.com/ionic-team/ionic-framework/commit/b6b43ae2925f8a12b35cabd43abd9d838bc9714f))
|
||||
* **capacitor:** use proper types for capacitor v7 support ([#30228](https://github.com/ionic-team/ionic-framework/issues/30228)) ([2149ba2](https://github.com/ionic-team/ionic-framework/commit/2149ba2c8d56a3a8ef4a6de89de1292f6efe3031))
|
||||
* **range:** handle unsupported values for range min and max ([#30070](https://github.com/ionic-team/ionic-framework/issues/30070)) ([3223193](https://github.com/ionic-team/ionic-framework/commit/322319397ca46bafda7ca0d2e3fb4cc554432d6a)), closes [#29667](https://github.com/ionic-team/ionic-framework/issues/29667)
|
||||
* **segment-button:** protect connectedCallback for when segment-content has not yet been created ([#30138](https://github.com/ionic-team/ionic-framework/issues/30138)) ([14b6538](https://github.com/ionic-team/ionic-framework/commit/14b6538d98303cb753d881ec6978fb98f53ed54c))
|
||||
* **select:** auto-scroll to selected item for all interfaces ([#30202](https://github.com/ionic-team/ionic-framework/issues/30202)) ([8eaeb22](https://github.com/ionic-team/ionic-framework/commit/8eaeb22e7a967100ffaadae8c8221e2e4888a3b6)), closes [#19296](https://github.com/ionic-team/ionic-framework/issues/19296)
|
||||
* **toggle:** trigger focus and blur on click ([#30234](https://github.com/ionic-team/ionic-framework/issues/30234)) ([ba8d8f4](https://github.com/ionic-team/ionic-framework/commit/ba8d8f489607537b3dac915cfc9f2c32a74b994c))
|
||||
* **vue:** update output target and properly emit events ([#30227](https://github.com/ionic-team/ionic-framework/issues/30227)) ([11554a5](https://github.com/ionic-team/ionic-framework/commit/11554a5d3590c660dbf609931dcb63cc2daf79cb)), closes [#30206](https://github.com/ionic-team/ionic-framework/issues/30206) [#30178](https://github.com/ionic-team/ionic-framework/issues/30178) [#30177](https://github.com/ionic-team/ionic-framework/issues/30177) [#30170](https://github.com/ionic-team/ionic-framework/issues/30170)
|
||||
* **vue:** update output target and resolve type issues ([#30239](https://github.com/ionic-team/ionic-framework/issues/30239)) ([6dcb143](https://github.com/ionic-team/ionic-framework/commit/6dcb143307682793ac4fd46d03efa5868a49e87d)), closes [#30179](https://github.com/ionic-team/ionic-framework/issues/30179)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.4.3](https://github.com/ionic-team/ionic-framework/compare/v8.4.2...v8.4.3) (2025-01-29)
|
||||
|
||||
**Note:** Version bump only for package @ionic/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.4.2](https://github.com/ionic-team/ionic-framework/compare/v8.4.1...v8.4.2) (2025-01-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **segment:** add logic to connect to segment-view in `componentDidLoad()` callback ([#30060](https://github.com/ionic-team/ionic-framework/issues/30060)) ([000f553](https://github.com/ionic-team/ionic-framework/commit/000f55303e459c583e642337fb1894f419f37d48)), closes [#30000](https://github.com/ionic-team/ionic-framework/issues/30000)
|
||||
* **select-modal:** match radio styles to iOS native ([#30119](https://github.com/ionic-team/ionic-framework/issues/30119)) ([3f8346e](https://github.com/ionic-team/ionic-framework/commit/3f8346e718ae3a6eb5008d739f10b6898b84ca9b))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.4.1](https://github.com/ionic-team/ionic-framework/compare/v8.4.0...v8.4.1) (2024-11-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **header:** use aria attributes to hide small title when collapsed ([#30027](https://github.com/ionic-team/ionic-framework/issues/30027)) ([23763ab](https://github.com/ionic-team/ionic-framework/commit/23763abf797f9a4ba8262225760f718e9dcc4782)), closes [#29347](https://github.com/ionic-team/ionic-framework/issues/29347)
|
||||
* **menu:** hide from screen readers while animating ([#30036](https://github.com/ionic-team/ionic-framework/issues/30036)) ([845071c](https://github.com/ionic-team/ionic-framework/commit/845071c97a856d45eb5e0bb81d9c270bc38bb604))
|
||||
* **overlays:** announce info after opening based on platform ([#30025](https://github.com/ionic-team/ionic-framework/issues/30025)) ([f6188c4](https://github.com/ionic-team/ionic-framework/commit/f6188c47e9278fe69fd9d250c65156edbe5ef32e))
|
||||
* **overlays:** focus management with checkbox/radio ([#30026](https://github.com/ionic-team/ionic-framework/issues/30026)) ([8ee42bb](https://github.com/ionic-team/ionic-framework/commit/8ee42bbc1e0bf4731d20040c7853756722f1a4b2))
|
||||
* **toast:** swipe gesture works with custom container layout ([#29999](https://github.com/ionic-team/ionic-framework/issues/29999)) ([470decc](https://github.com/ionic-team/ionic-framework/commit/470decca7b6b89ef74095ef0bb7909b93640cd78)), closes [#29998](https://github.com/ionic-team/ionic-framework/issues/29998)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [8.4.0](https://github.com/ionic-team/ionic-framework/compare/v8.3.4...v8.4.0) (2024-11-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **alert:** use correct heading structure for subHeader when header exists ([#29964](https://github.com/ionic-team/ionic-framework/issues/29964)) ([0fdcb32](https://github.com/ionic-team/ionic-framework/commit/0fdcb32ce0f99b284b314f79f7d0b071bc37faec))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **menu:** pass role to ionWillClose and ionDidClose events ([#29954](https://github.com/ionic-team/ionic-framework/issues/29954)) ([ee2fa19](https://github.com/ionic-team/ionic-framework/commit/ee2fa19a1e9f09d492c7c08340d95ba6a56ebb2b))
|
||||
* **segment-view:** adds support for new `ion-segment-view` component ([#29969](https://github.com/ionic-team/ionic-framework/issues/29969)) ([89508fb](https://github.com/ionic-team/ionic-framework/commit/89508fb89172900b1d11cc3fc18883f57a7fbab6))
|
||||
* **select:** add `modal` as interface ([#29972](https://github.com/ionic-team/ionic-framework/issues/29972)) ([3628ea8](https://github.com/ionic-team/ionic-framework/commit/3628ea875a66a717783de5e0a4df440872339040))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.3.4](https://github.com/ionic-team/ionic-framework/compare/v8.3.3...v8.3.4) (2024-10-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backdrop:** remove tabindex for improved accessibility ([#29956](https://github.com/ionic-team/ionic-framework/issues/29956)) ([7294e96](https://github.com/ionic-team/ionic-framework/commit/7294e969bb913692eaf28e54860614f445132713)), closes [#29773](https://github.com/ionic-team/ionic-framework/issues/29773)
|
||||
* **input, textarea:** ensure screen readers announce helper and error text when focused ([#29958](https://github.com/ionic-team/ionic-framework/issues/29958)) ([5a73145](https://github.com/ionic-team/ionic-framework/commit/5a7314553a8def87bd19275640c92dd72a6ef1a4))
|
||||
* **overlay:** hide from screen readers while animating ([#29951](https://github.com/ionic-team/ionic-framework/issues/29951)) ([cb60073](https://github.com/ionic-team/ionic-framework/commit/cb6007363a8d42b5f126945427c2bfc3d7209c21)), closes [#29857](https://github.com/ionic-team/ionic-framework/issues/29857)
|
||||
* **overlays:** do not hide root when toast appears ([#29962](https://github.com/ionic-team/ionic-framework/issues/29962)) ([322d7c9](https://github.com/ionic-team/ionic-framework/commit/322d7c98cf6613df0b0db3f119e3f892e6a17e7b)), closes [#29773](https://github.com/ionic-team/ionic-framework/issues/29773)
|
||||
* **overlays:** hide the focus trap div from screen readers ([#29970](https://github.com/ionic-team/ionic-framework/issues/29970)) ([c3b58f1](https://github.com/ionic-team/ionic-framework/commit/c3b58f1620bcb74db43e3983ef570b7b982abd83)), closes [#29858](https://github.com/ionic-team/ionic-framework/issues/29858)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.3.3](https://github.com/ionic-team/ionic-framework/compare/v8.3.2...v8.3.3) (2024-10-16)
|
||||
|
||||
**Note:** Version bump only for package @ionic/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.3.2](https://github.com/ionic-team/ionic-framework/compare/v8.3.1...v8.3.2) (2024-10-02)
|
||||
|
||||
|
||||
|
||||
22
core/api.txt
@@ -1000,15 +1000,15 @@ ion-menu,prop,menuId,string | undefined,undefined,false,true
|
||||
ion-menu,prop,side,"end" | "start",'start',false,true
|
||||
ion-menu,prop,swipeGesture,boolean,true,false,false
|
||||
ion-menu,prop,type,"overlay" | "push" | "reveal" | undefined,undefined,false,false
|
||||
ion-menu,method,close,close(animated?: boolean) => Promise<boolean>
|
||||
ion-menu,method,close,close(animated?: boolean, role?: string) => Promise<boolean>
|
||||
ion-menu,method,isActive,isActive() => Promise<boolean>
|
||||
ion-menu,method,isOpen,isOpen() => Promise<boolean>
|
||||
ion-menu,method,open,open(animated?: boolean) => Promise<boolean>
|
||||
ion-menu,method,setOpen,setOpen(shouldOpen: boolean, animated?: boolean) => Promise<boolean>
|
||||
ion-menu,method,setOpen,setOpen(shouldOpen: boolean, animated?: boolean, role?: string) => Promise<boolean>
|
||||
ion-menu,method,toggle,toggle(animated?: boolean) => Promise<boolean>
|
||||
ion-menu,event,ionDidClose,void,true
|
||||
ion-menu,event,ionDidClose,MenuCloseEventDetail,true
|
||||
ion-menu,event,ionDidOpen,void,true
|
||||
ion-menu,event,ionWillClose,void,true
|
||||
ion-menu,event,ionWillClose,MenuCloseEventDetail,true
|
||||
ion-menu,event,ionWillOpen,void,true
|
||||
ion-menu,css-prop,--background,ios
|
||||
ion-menu,css-prop,--background,md
|
||||
@@ -1542,6 +1542,7 @@ ion-segment,css-prop,--background,ios
|
||||
ion-segment,css-prop,--background,md
|
||||
|
||||
ion-segment-button,shadow
|
||||
ion-segment-button,prop,contentId,string | undefined,undefined,false,true
|
||||
ion-segment-button,prop,disabled,boolean,false,false,false
|
||||
ion-segment-button,prop,layout,"icon-bottom" | "icon-end" | "icon-hide" | "icon-start" | "icon-top" | "label-hide" | undefined,'icon-top',false,false
|
||||
ion-segment-button,prop,mode,"ios" | "md",undefined,false,false
|
||||
@@ -1607,6 +1608,12 @@ ion-segment-button,part,indicator
|
||||
ion-segment-button,part,indicator-background
|
||||
ion-segment-button,part,native
|
||||
|
||||
ion-segment-content,shadow
|
||||
|
||||
ion-segment-view,shadow
|
||||
ion-segment-view,prop,disabled,boolean,false,false,false
|
||||
ion-segment-view,event,ionSegmentViewScroll,SegmentViewScrollEvent,true
|
||||
|
||||
ion-select,shadow
|
||||
ion-select,prop,cancelText,string,'Cancel',false,false
|
||||
ion-select,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
|
||||
@@ -1614,7 +1621,7 @@ ion-select,prop,compareWith,((currentValue: any, compareValue: any) => boolean)
|
||||
ion-select,prop,disabled,boolean,false,false,false
|
||||
ion-select,prop,expandedIcon,string | undefined,undefined,false,false
|
||||
ion-select,prop,fill,"outline" | "solid" | undefined,undefined,false,false
|
||||
ion-select,prop,interface,"action-sheet" | "alert" | "popover",'alert',false,false
|
||||
ion-select,prop,interface,"action-sheet" | "alert" | "modal" | "popover",'alert',false,false
|
||||
ion-select,prop,interfaceOptions,any,{},false,false
|
||||
ion-select,prop,justify,"end" | "space-between" | "start" | undefined,undefined,false,false
|
||||
ion-select,prop,label,string | undefined,undefined,false,false
|
||||
@@ -1672,6 +1679,11 @@ ion-select,part,label
|
||||
ion-select,part,placeholder
|
||||
ion-select,part,text
|
||||
|
||||
ion-select-modal,scoped
|
||||
ion-select-modal,prop,header,string | undefined,undefined,false,false
|
||||
ion-select-modal,prop,multiple,boolean | undefined,undefined,false,false
|
||||
ion-select-modal,prop,options,SelectModalOption[],[],false,false
|
||||
|
||||
ion-select-option,shadow
|
||||
ion-select-option,prop,disabled,boolean,false,false,false
|
||||
ion-select-option,prop,value,any,undefined,false,false
|
||||
|
||||
733
core/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "8.3.2",
|
||||
"version": "8.4.5",
|
||||
"description": "Base components for Ionic",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -37,20 +37,20 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.10.0",
|
||||
"@capacitor/core": "^6.0.0",
|
||||
"@capacitor/haptics": "^6.0.0",
|
||||
"@capacitor/keyboard": "^6.0.0",
|
||||
"@capacitor/status-bar": "^6.0.0",
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@capacitor/core": "^7.0.0",
|
||||
"@capacitor/haptics": "^7.0.0",
|
||||
"@capacitor/keyboard": "^7.0.0",
|
||||
"@capacitor/status-bar": "^7.0.0",
|
||||
"@clack/prompts": "^0.10.0",
|
||||
"@ionic/eslint-config": "^0.3.0",
|
||||
"@ionic/prettier-config": "^2.0.0",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@rollup/plugin-node-resolve": "^8.4.0",
|
||||
"@rollup/plugin-virtual": "^2.0.3",
|
||||
"@stencil/angular-output-target": "^0.8.4",
|
||||
"@stencil/angular-output-target": "^0.10.0",
|
||||
"@stencil/react-output-target": "0.5.3",
|
||||
"@stencil/sass": "^3.0.9",
|
||||
"@stencil/vue-output-target": "^0.8.9",
|
||||
"@stencil/vue-output-target": "0.10.5",
|
||||
"@types/jest": "^29.5.6",
|
||||
"@types/node": "^14.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.2",
|
||||
|
||||
@@ -49,8 +49,19 @@ html.ios.ios {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "iosTestingFont", sans-serif;
|
||||
}
|
||||
|
||||
ion-content button,
|
||||
main button {
|
||||
/**
|
||||
* Button styles should only be applied
|
||||
* to native buttons that are not part of the
|
||||
* Ionic framework.
|
||||
* Otherwise, the styles may not appear correctly
|
||||
* when comparing between testing and production.
|
||||
* This issue occurs only with `scoped` components,
|
||||
* which is why `sc-ion-` is used as a filter,
|
||||
* since this class is specifically added to `scoped`
|
||||
* components.
|
||||
*/
|
||||
ion-content button:not([class*="sc-ion-"]),
|
||||
main button:not([class*="sc-ion-"]) {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
clear: both;
|
||||
@@ -63,8 +74,19 @@ main button {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
ion-content button.expand,
|
||||
main button.expand {
|
||||
/**
|
||||
* Button styles should only be applied
|
||||
* to native buttons that are not part of the
|
||||
* Ionic framework.
|
||||
* Otherwise, the styles may not appear correctly
|
||||
* when comparing between testing and production.
|
||||
* This issue occurs only with `scoped` components,
|
||||
* which is why `sc-ion-` is used as a filter,
|
||||
* since this class is specifically added to `scoped`
|
||||
* components.
|
||||
*/
|
||||
ion-content button.expand:not([class*="sc-ion-"]),
|
||||
main button.expand:not([class*="sc-ion-"]) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
117
core/src/components.d.ts
vendored
@@ -18,7 +18,7 @@ import { ScrollBaseDetail, ScrollDetail } from "./components/content/content-int
|
||||
import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
|
||||
import { SpinnerTypes } from "./components/spinner/spinner-configs";
|
||||
import { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
|
||||
import { MenuChangeEventDetail, MenuType, Side } from "./components/menu/menu-interface";
|
||||
import { MenuChangeEventDetail, MenuCloseEventDetail, MenuType, Side } from "./components/menu/menu-interface";
|
||||
import { ModalBreakpointChangeEventDetail, ModalHandleBehavior } from "./components/modal/modal-interface";
|
||||
import { NavComponent, NavComponentWithProps, NavOptions, RouterOutletOptions, SwipeGestureHandler, TransitionDoneFn, TransitionInstruction } from "./components/nav/nav-interface";
|
||||
import { ViewController } from "./components/nav/view-controller";
|
||||
@@ -34,7 +34,9 @@ import { NavigationHookCallback } from "./components/route/route-interface";
|
||||
import { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
|
||||
import { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
|
||||
import { SegmentButtonLayout } from "./components/segment-button/segment-button-interface";
|
||||
import { SegmentViewScrollEvent } from "./components/segment-view/segment-view-interface";
|
||||
import { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./components/select/select-interface";
|
||||
import { SelectModalOption } from "./components/select-modal/select-modal-interface";
|
||||
import { SelectPopoverOption } from "./components/select-popover/select-popover-interface";
|
||||
import { TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout } from "./components/tab-bar/tab-bar-interface";
|
||||
import { TextareaChangeEventDetail, TextareaInputEventDetail } from "./components/textarea/textarea-interface";
|
||||
@@ -53,7 +55,7 @@ export { ScrollBaseDetail, ScrollDetail } from "./components/content/content-int
|
||||
export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
|
||||
export { SpinnerTypes } from "./components/spinner/spinner-configs";
|
||||
export { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
|
||||
export { MenuChangeEventDetail, MenuType, Side } from "./components/menu/menu-interface";
|
||||
export { MenuChangeEventDetail, MenuCloseEventDetail, MenuType, Side } from "./components/menu/menu-interface";
|
||||
export { ModalBreakpointChangeEventDetail, ModalHandleBehavior } from "./components/modal/modal-interface";
|
||||
export { NavComponent, NavComponentWithProps, NavOptions, RouterOutletOptions, SwipeGestureHandler, TransitionDoneFn, TransitionInstruction } from "./components/nav/nav-interface";
|
||||
export { ViewController } from "./components/nav/view-controller";
|
||||
@@ -69,7 +71,9 @@ export { NavigationHookCallback } from "./components/route/route-interface";
|
||||
export { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
|
||||
export { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
|
||||
export { SegmentButtonLayout } from "./components/segment-button/segment-button-interface";
|
||||
export { SegmentViewScrollEvent } from "./components/segment-view/segment-view-interface";
|
||||
export { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./components/select/select-interface";
|
||||
export { SelectModalOption } from "./components/select-modal/select-modal-interface";
|
||||
export { SelectPopoverOption } from "./components/select-popover/select-popover-interface";
|
||||
export { TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout } from "./components/tab-bar/tab-bar-interface";
|
||||
export { TextareaChangeEventDetail, TextareaInputEventDetail } from "./components/textarea/textarea-interface";
|
||||
@@ -639,6 +643,7 @@ export namespace Components {
|
||||
* The name of the control, which is submitted with the form data.
|
||||
*/
|
||||
"name": string;
|
||||
"setFocus": () => Promise<void>;
|
||||
/**
|
||||
* The value of the checkbox does not mean if it's checked or not, use the `checked` property for that. The value of a checkbox is analogous to the value of an `<input type="checkbox">`, it's only used when the checkbox participates in a native `<form>`.
|
||||
*/
|
||||
@@ -1596,7 +1601,7 @@ export namespace Components {
|
||||
/**
|
||||
* Closes the menu. If the menu is already closed or it can't be closed, it returns `false`.
|
||||
*/
|
||||
"close": (animated?: boolean) => Promise<boolean>;
|
||||
"close": (animated?: boolean, role?: string) => Promise<boolean>;
|
||||
/**
|
||||
* The `id` of the main content. When using a router this is typically `ion-router-outlet`. When not using a router, this is typically your main view's `ion-content`. This is not the id of the `ion-content` inside of your `ion-menu`.
|
||||
*/
|
||||
@@ -1628,7 +1633,7 @@ export namespace Components {
|
||||
/**
|
||||
* Opens or closes the button. If the operation can't be completed successfully, it returns `false`.
|
||||
*/
|
||||
"setOpen": (shouldOpen: boolean, animated?: boolean) => Promise<boolean>;
|
||||
"setOpen": (shouldOpen: boolean, animated?: boolean, role?: string) => Promise<boolean>;
|
||||
/**
|
||||
* Which side of the view the menu should be placed.
|
||||
*/
|
||||
@@ -2279,7 +2284,7 @@ export namespace Components {
|
||||
*/
|
||||
"name": string;
|
||||
"setButtonTabindex": (value: number) => Promise<void>;
|
||||
"setFocus": (ev: globalThis.Event) => Promise<void>;
|
||||
"setFocus": (ev?: globalThis.Event) => Promise<void>;
|
||||
/**
|
||||
* the value of the radio.
|
||||
*/
|
||||
@@ -2298,6 +2303,7 @@ export namespace Components {
|
||||
* The name of the control, which is submitted with the form data.
|
||||
*/
|
||||
"name": string;
|
||||
"setFocus": () => Promise<void>;
|
||||
/**
|
||||
* the value of the radio group.
|
||||
*/
|
||||
@@ -2693,6 +2699,10 @@ export namespace Components {
|
||||
"value"?: SegmentValue;
|
||||
}
|
||||
interface IonSegmentButton {
|
||||
/**
|
||||
* The `id` of the segment content.
|
||||
*/
|
||||
"contentId"?: string;
|
||||
/**
|
||||
* If `true`, the user cannot interact with the segment button.
|
||||
*/
|
||||
@@ -2715,6 +2725,19 @@ export namespace Components {
|
||||
*/
|
||||
"value": SegmentValue;
|
||||
}
|
||||
interface IonSegmentContent {
|
||||
}
|
||||
interface IonSegmentView {
|
||||
/**
|
||||
* If `true`, the segment view cannot be interacted with.
|
||||
*/
|
||||
"disabled": boolean;
|
||||
/**
|
||||
* @param id : The id of the segment content to display.
|
||||
* @param smoothScroll : Whether to animate the scroll transition.
|
||||
*/
|
||||
"setContent": (id: string, smoothScroll?: boolean) => Promise<void>;
|
||||
}
|
||||
interface IonSelect {
|
||||
/**
|
||||
* The text to display on the cancel button.
|
||||
@@ -2741,11 +2764,11 @@ export namespace Components {
|
||||
*/
|
||||
"fill"?: 'outline' | 'solid';
|
||||
/**
|
||||
* The interface the select should use: `action-sheet`, `popover` or `alert`.
|
||||
* The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`.
|
||||
*/
|
||||
"interface": SelectInterface;
|
||||
/**
|
||||
* Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet) and the [ion-popover docs](./popover) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface.
|
||||
* Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet), the [ion-popover docs](./popover), and the [ion-modal docs](./modal) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface.
|
||||
*/
|
||||
"interfaceOptions": any;
|
||||
/**
|
||||
@@ -2802,6 +2825,11 @@ export namespace Components {
|
||||
*/
|
||||
"value"?: any | null;
|
||||
}
|
||||
interface IonSelectModal {
|
||||
"header"?: string;
|
||||
"multiple"?: boolean;
|
||||
"options": SelectModalOption[];
|
||||
}
|
||||
interface IonSelectOption {
|
||||
/**
|
||||
* If `true`, the user cannot interact with the select option. This property does not apply when `interface="action-sheet"` as `ion-action-sheet` does not allow for disabled buttons.
|
||||
@@ -3416,6 +3444,10 @@ export interface IonSegmentCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLIonSegmentElement;
|
||||
}
|
||||
export interface IonSegmentViewCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLIonSegmentViewElement;
|
||||
}
|
||||
export interface IonSelectCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLIonSelectElement;
|
||||
@@ -3969,9 +4001,9 @@ declare global {
|
||||
};
|
||||
interface HTMLIonMenuElementEventMap {
|
||||
"ionWillOpen": void;
|
||||
"ionWillClose": void;
|
||||
"ionWillClose": MenuCloseEventDetail;
|
||||
"ionDidOpen": void;
|
||||
"ionDidClose": void;
|
||||
"ionDidClose": MenuCloseEventDetail;
|
||||
"ionMenuChange": MenuChangeEventDetail;
|
||||
}
|
||||
interface HTMLIonMenuElement extends Components.IonMenu, HTMLStencilElement {
|
||||
@@ -4412,6 +4444,29 @@ declare global {
|
||||
prototype: HTMLIonSegmentButtonElement;
|
||||
new (): HTMLIonSegmentButtonElement;
|
||||
};
|
||||
interface HTMLIonSegmentContentElement extends Components.IonSegmentContent, HTMLStencilElement {
|
||||
}
|
||||
var HTMLIonSegmentContentElement: {
|
||||
prototype: HTMLIonSegmentContentElement;
|
||||
new (): HTMLIonSegmentContentElement;
|
||||
};
|
||||
interface HTMLIonSegmentViewElementEventMap {
|
||||
"ionSegmentViewScroll": SegmentViewScrollEvent;
|
||||
}
|
||||
interface HTMLIonSegmentViewElement extends Components.IonSegmentView, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLIonSegmentViewElementEventMap>(type: K, listener: (this: HTMLIonSegmentViewElement, ev: IonSegmentViewCustomEvent<HTMLIonSegmentViewElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLIonSegmentViewElementEventMap>(type: K, listener: (this: HTMLIonSegmentViewElement, ev: IonSegmentViewCustomEvent<HTMLIonSegmentViewElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLIonSegmentViewElement: {
|
||||
prototype: HTMLIonSegmentViewElement;
|
||||
new (): HTMLIonSegmentViewElement;
|
||||
};
|
||||
interface HTMLIonSelectElementEventMap {
|
||||
"ionChange": SelectChangeEventDetail;
|
||||
"ionCancel": void;
|
||||
@@ -4434,6 +4489,12 @@ declare global {
|
||||
prototype: HTMLIonSelectElement;
|
||||
new (): HTMLIonSelectElement;
|
||||
};
|
||||
interface HTMLIonSelectModalElement extends Components.IonSelectModal, HTMLStencilElement {
|
||||
}
|
||||
var HTMLIonSelectModalElement: {
|
||||
prototype: HTMLIonSelectModalElement;
|
||||
new (): HTMLIonSelectModalElement;
|
||||
};
|
||||
interface HTMLIonSelectOptionElement extends Components.IonSelectOption, HTMLStencilElement {
|
||||
}
|
||||
var HTMLIonSelectOptionElement: {
|
||||
@@ -4721,7 +4782,10 @@ declare global {
|
||||
"ion-searchbar": HTMLIonSearchbarElement;
|
||||
"ion-segment": HTMLIonSegmentElement;
|
||||
"ion-segment-button": HTMLIonSegmentButtonElement;
|
||||
"ion-segment-content": HTMLIonSegmentContentElement;
|
||||
"ion-segment-view": HTMLIonSegmentViewElement;
|
||||
"ion-select": HTMLIonSelectElement;
|
||||
"ion-select-modal": HTMLIonSelectModalElement;
|
||||
"ion-select-option": HTMLIonSelectOptionElement;
|
||||
"ion-select-popover": HTMLIonSelectPopoverElement;
|
||||
"ion-skeleton-text": HTMLIonSkeletonTextElement;
|
||||
@@ -6364,7 +6428,7 @@ declare namespace LocalJSX {
|
||||
/**
|
||||
* Emitted when the menu is closed.
|
||||
*/
|
||||
"onIonDidClose"?: (event: IonMenuCustomEvent<void>) => void;
|
||||
"onIonDidClose"?: (event: IonMenuCustomEvent<MenuCloseEventDetail>) => void;
|
||||
/**
|
||||
* Emitted when the menu is open.
|
||||
*/
|
||||
@@ -6376,7 +6440,7 @@ declare namespace LocalJSX {
|
||||
/**
|
||||
* Emitted when the menu is about to be closed.
|
||||
*/
|
||||
"onIonWillClose"?: (event: IonMenuCustomEvent<void>) => void;
|
||||
"onIonWillClose"?: (event: IonMenuCustomEvent<MenuCloseEventDetail>) => void;
|
||||
/**
|
||||
* Emitted when the menu is about to be opened.
|
||||
*/
|
||||
@@ -7450,6 +7514,10 @@ declare namespace LocalJSX {
|
||||
"value"?: SegmentValue;
|
||||
}
|
||||
interface IonSegmentButton {
|
||||
/**
|
||||
* The `id` of the segment content.
|
||||
*/
|
||||
"contentId"?: string;
|
||||
/**
|
||||
* If `true`, the user cannot interact with the segment button.
|
||||
*/
|
||||
@@ -7471,6 +7539,18 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"value"?: SegmentValue;
|
||||
}
|
||||
interface IonSegmentContent {
|
||||
}
|
||||
interface IonSegmentView {
|
||||
/**
|
||||
* If `true`, the segment view cannot be interacted with.
|
||||
*/
|
||||
"disabled"?: boolean;
|
||||
/**
|
||||
* Emitted when the segment view is scrolled.
|
||||
*/
|
||||
"onIonSegmentViewScroll"?: (event: IonSegmentViewCustomEvent<SegmentViewScrollEvent>) => void;
|
||||
}
|
||||
interface IonSelect {
|
||||
/**
|
||||
* The text to display on the cancel button.
|
||||
@@ -7497,11 +7577,11 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"fill"?: 'outline' | 'solid';
|
||||
/**
|
||||
* The interface the select should use: `action-sheet`, `popover` or `alert`.
|
||||
* The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`.
|
||||
*/
|
||||
"interface"?: SelectInterface;
|
||||
/**
|
||||
* Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet) and the [ion-popover docs](./popover) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface.
|
||||
* Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet), the [ion-popover docs](./popover), and the [ion-modal docs](./modal) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface.
|
||||
*/
|
||||
"interfaceOptions"?: any;
|
||||
/**
|
||||
@@ -7577,6 +7657,11 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"value"?: any | null;
|
||||
}
|
||||
interface IonSelectModal {
|
||||
"header"?: string;
|
||||
"multiple"?: boolean;
|
||||
"options"?: SelectModalOption[];
|
||||
}
|
||||
interface IonSelectOption {
|
||||
/**
|
||||
* If `true`, the user cannot interact with the select option. This property does not apply when `interface="action-sheet"` as `ion-action-sheet` does not allow for disabled buttons.
|
||||
@@ -8162,7 +8247,10 @@ declare namespace LocalJSX {
|
||||
"ion-searchbar": IonSearchbar;
|
||||
"ion-segment": IonSegment;
|
||||
"ion-segment-button": IonSegmentButton;
|
||||
"ion-segment-content": IonSegmentContent;
|
||||
"ion-segment-view": IonSegmentView;
|
||||
"ion-select": IonSelect;
|
||||
"ion-select-modal": IonSelectModal;
|
||||
"ion-select-option": IonSelectOption;
|
||||
"ion-select-popover": IonSelectPopover;
|
||||
"ion-skeleton-text": IonSkeletonText;
|
||||
@@ -8261,7 +8349,10 @@ declare module "@stencil/core" {
|
||||
"ion-searchbar": LocalJSX.IonSearchbar & JSXBase.HTMLAttributes<HTMLIonSearchbarElement>;
|
||||
"ion-segment": LocalJSX.IonSegment & JSXBase.HTMLAttributes<HTMLIonSegmentElement>;
|
||||
"ion-segment-button": LocalJSX.IonSegmentButton & JSXBase.HTMLAttributes<HTMLIonSegmentButtonElement>;
|
||||
"ion-segment-content": LocalJSX.IonSegmentContent & JSXBase.HTMLAttributes<HTMLIonSegmentContentElement>;
|
||||
"ion-segment-view": LocalJSX.IonSegmentView & JSXBase.HTMLAttributes<HTMLIonSegmentViewElement>;
|
||||
"ion-select": LocalJSX.IonSelect & JSXBase.HTMLAttributes<HTMLIonSelectElement>;
|
||||
"ion-select-modal": LocalJSX.IonSelectModal & JSXBase.HTMLAttributes<HTMLIonSelectModalElement>;
|
||||
"ion-select-option": LocalJSX.IonSelectOption & JSXBase.HTMLAttributes<HTMLIonSelectOptionElement>;
|
||||
"ion-select-popover": LocalJSX.IonSelectPopover & JSXBase.HTMLAttributes<HTMLIonSelectPopoverElement>;
|
||||
"ion-skeleton-text": LocalJSX.IonSkeletonText & JSXBase.HTMLAttributes<HTMLIonSkeletonTextElement>;
|
||||
|
||||
@@ -385,7 +385,7 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
>
|
||||
<ion-backdrop tappable={this.backdropDismiss} />
|
||||
|
||||
<div tabindex="0"></div>
|
||||
<div tabindex="0" aria-hidden="true"></div>
|
||||
|
||||
<div class="action-sheet-wrapper ion-overlay-wrapper" ref={(el) => (this.wrapperEl = el)}>
|
||||
<div class="action-sheet-container">
|
||||
@@ -446,7 +446,7 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
<div tabindex="0" aria-hidden="true"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -237,6 +237,18 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure when alert container is being focused, and the user presses the tab + shift keys, the focus will be set to the last alert button.
|
||||
*/
|
||||
if (ev.target.classList.contains('alert-wrapper')) {
|
||||
if (ev.key === 'Tab' && ev.shiftKey) {
|
||||
ev.preventDefault();
|
||||
const lastChildBtn = this.wrapperEl?.querySelector('.alert-button:last-child') as HTMLButtonElement;
|
||||
lastChildBtn.focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The only inputs we want to navigate between using arrow keys are the radios
|
||||
// ignore the keydown event if it is not on a radio button
|
||||
if (
|
||||
@@ -400,7 +412,19 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
|
||||
await this.delegateController.attachViewToDom();
|
||||
|
||||
await present(this, 'alertEnter', iosEnterAnimation, mdEnterAnimation);
|
||||
await present(this, 'alertEnter', iosEnterAnimation, mdEnterAnimation).then(() => {
|
||||
/**
|
||||
* Check if alert has only one button and no inputs.
|
||||
* If so, then focus on the button. Otherwise, focus the alert wrapper.
|
||||
* This will map to the default native alert behavior.
|
||||
*/
|
||||
if (this.buttons.length === 1 && this.inputs.length === 0) {
|
||||
const queryBtn = this.wrapperEl?.querySelector('.alert-button') as HTMLButtonElement;
|
||||
queryBtn.focus();
|
||||
} else {
|
||||
this.wrapperEl?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
unlock();
|
||||
}
|
||||
@@ -725,24 +749,21 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
const { overlayIndex, header, subHeader, message, htmlAttributes } = this;
|
||||
const mode = getIonMode(this);
|
||||
const hdrId = `alert-${overlayIndex}-hdr`;
|
||||
const subHdrId = `alert-${overlayIndex}-sub-hdr`;
|
||||
const msgId = `alert-${overlayIndex}-msg`;
|
||||
const subHdrId = `alert-${overlayIndex}-sub-hdr`;
|
||||
const role = this.inputs.length > 0 || this.buttons.length > 0 ? 'alertdialog' : 'alert';
|
||||
|
||||
/**
|
||||
* If the header is defined, use that. Otherwise, fall back to the subHeader.
|
||||
* If neither is defined, don't set aria-labelledby.
|
||||
* Use both the header and subHeader ids if they are defined.
|
||||
* If only the header is defined, use the header id.
|
||||
* If only the subHeader is defined, use the subHeader id.
|
||||
* If neither are defined, do not set aria-labelledby.
|
||||
*/
|
||||
const ariaLabelledBy = header ? hdrId : subHeader ? subHdrId : null;
|
||||
const ariaLabelledBy = header && subHeader ? `${hdrId} ${subHdrId}` : header ? hdrId : subHeader ? subHdrId : null;
|
||||
|
||||
return (
|
||||
<Host
|
||||
role={role}
|
||||
aria-modal="true"
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
aria-describedby={message !== undefined ? msgId : null}
|
||||
tabindex="-1"
|
||||
{...(htmlAttributes as any)}
|
||||
style={{
|
||||
zIndex: `${20000 + overlayIndex}`,
|
||||
}}
|
||||
@@ -757,20 +778,36 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
>
|
||||
<ion-backdrop tappable={this.backdropDismiss} />
|
||||
|
||||
<div tabindex="0"></div>
|
||||
<div tabindex="0" aria-hidden="true"></div>
|
||||
|
||||
<div class="alert-wrapper ion-overlay-wrapper" ref={(el) => (this.wrapperEl = el)}>
|
||||
<div
|
||||
class="alert-wrapper ion-overlay-wrapper"
|
||||
role={role}
|
||||
aria-modal="true"
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
aria-describedby={message !== undefined ? msgId : null}
|
||||
tabindex="0"
|
||||
ref={(el) => (this.wrapperEl = el)}
|
||||
{...(htmlAttributes as any)}
|
||||
>
|
||||
<div class="alert-head">
|
||||
{header && (
|
||||
<h2 id={hdrId} class="alert-title">
|
||||
{header}
|
||||
</h2>
|
||||
)}
|
||||
{subHeader && (
|
||||
{/* If no header exists, subHeader should be the highest heading level, h2 */}
|
||||
{subHeader && !header && (
|
||||
<h2 id={subHdrId} class="alert-sub-title">
|
||||
{subHeader}
|
||||
</h2>
|
||||
)}
|
||||
{/* If a header exists, subHeader should be one level below it, h3 */}
|
||||
{subHeader && header && (
|
||||
<h3 id={subHdrId} class="alert-sub-title">
|
||||
{subHeader}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{this.renderAlertMessage(msgId)}
|
||||
@@ -779,7 +816,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
{this.renderAlertButtons()}
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
<div tabindex="0" aria-hidden="true"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,13 +16,35 @@ const testAria = async (
|
||||
await didPresent.next();
|
||||
|
||||
const alert = page.locator('ion-alert');
|
||||
const alertwrapper = alert.locator('.alert-wrapper');
|
||||
|
||||
const header = alert.locator('.alert-title');
|
||||
const subHeader = alert.locator('.alert-sub-title');
|
||||
|
||||
// If a header exists, it should be an h2 element
|
||||
if ((await header.count()) > 0) {
|
||||
const headerTagName = await header.evaluate((el) => el.tagName);
|
||||
expect(headerTagName).toBe('H2');
|
||||
}
|
||||
|
||||
// If a header and subHeader exist, the subHeader should be an h3 element
|
||||
if ((await header.count()) > 0 && (await subHeader.count()) > 0) {
|
||||
const subHeaderTagName = await subHeader.evaluate((el) => el.tagName);
|
||||
expect(subHeaderTagName).toBe('H3');
|
||||
}
|
||||
|
||||
// If a subHeader exists without a header, the subHeader should be an h2 element
|
||||
if ((await header.count()) === 0 && (await subHeader.count()) > 0) {
|
||||
const subHeaderTagName = await subHeader.evaluate((el) => el.tagName);
|
||||
expect(subHeaderTagName).toBe('H2');
|
||||
}
|
||||
|
||||
/**
|
||||
* expect().toHaveAttribute() can't check for a null value, so grab and check
|
||||
* the values manually instead.
|
||||
*/
|
||||
const ariaLabelledBy = await alert.getAttribute('aria-labelledby');
|
||||
const ariaDescribedBy = await alert.getAttribute('aria-describedby');
|
||||
const ariaLabelledBy = await alertwrapper.getAttribute('aria-labelledby');
|
||||
const ariaDescribedBy = await alertwrapper.getAttribute('aria-describedby');
|
||||
|
||||
expect(ariaLabelledBy).toBe(expectedAriaLabelledBy);
|
||||
expect(ariaDescribedBy).toBe(expectedAriaDescribedBy);
|
||||
@@ -124,16 +146,24 @@ configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
|
||||
await page.goto(`/src/components/alert/test/a11y`, config);
|
||||
});
|
||||
|
||||
test('should have aria-labelledby when header is set', async ({ page }) => {
|
||||
await testAria(page, 'noMessage', 'alert-1-hdr', null);
|
||||
test('should have aria-labelledby set to both when header and subHeader are set', async ({ page }) => {
|
||||
await testAria(page, 'bothHeadersOnly', 'alert-1-hdr alert-1-sub-hdr', null);
|
||||
});
|
||||
|
||||
test('should have aria-labelledby set when only header is set', async ({ page }) => {
|
||||
await testAria(page, 'headerOnly', 'alert-1-hdr', null);
|
||||
});
|
||||
|
||||
test('should fall back to subHeader for aria-labelledby if header is not defined', async ({ page }) => {
|
||||
await testAria(page, 'subHeaderOnly', 'alert-1-sub-hdr', null);
|
||||
});
|
||||
|
||||
test('should have aria-describedby when message is set', async ({ page }) => {
|
||||
await testAria(page, 'noHeaders', null, 'alert-1-msg');
|
||||
});
|
||||
|
||||
test('should fall back to subHeader for aria-labelledby if header is not defined', async ({ page }) => {
|
||||
await testAria(page, 'subHeaderOnly', 'alert-1-sub-hdr', 'alert-1-msg');
|
||||
test('should have aria-labelledby and aria-describedby when headers and message are set', async ({ page }) => {
|
||||
await testAria(page, 'headersAndMessage', 'alert-1-hdr alert-1-sub-hdr', 'alert-1-msg');
|
||||
});
|
||||
|
||||
test('should allow for manually specifying aria attributes', async ({ page }) => {
|
||||
@@ -279,7 +309,10 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
|
||||
await expect(page).toHaveScreenshot(screenshot(`alert-radio-scale`));
|
||||
});
|
||||
test('should scale text on larger font sizes with text fields', async ({ page }) => {
|
||||
test('should scale text on larger font sizes with text fields', async ({ page, skip }) => {
|
||||
// TODO(ROU-8158): unskip this test when a solution is found
|
||||
skip.browser('chromium', 'Rendering is flaky in Chrome.');
|
||||
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
|
||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
@@ -19,10 +19,11 @@
|
||||
<main class="ion-padding">
|
||||
<h1>Alert - A11y</h1>
|
||||
|
||||
<button class="expand" id="bothHeaders" onclick="presentBothHeaders()">Both Headers</button>
|
||||
<button class="expand" id="bothHeadersOnly" onclick="presentBothHeadersOnly()">Both Headers Only</button>
|
||||
<button class="expand" id="headerOnly" onclick="presentHeaderOnly()">Header Only</button>
|
||||
<button class="expand" id="subHeaderOnly" onclick="presentSubHeaderOnly()">Subheader Only</button>
|
||||
<button class="expand" id="noHeaders" onclick="presentNoHeaders()">No Headers</button>
|
||||
<button class="expand" id="noMessage" onclick="presentNoMessage()">No Message</button>
|
||||
<button class="expand" id="headersAndMessage" onclick="presentHeadersAndMessage()">Headers and Message</button>
|
||||
<button class="expand" id="customAria" onclick="presentCustomAria()">Custom Aria</button>
|
||||
<button class="expand" id="ariaLabelButton" onclick="presentAriaLabelButton()">Aria Label Button</button>
|
||||
<button class="expand" id="checkbox" onclick="presentAlertCheckbox()">Checkbox</button>
|
||||
@@ -34,11 +35,17 @@
|
||||
await alert.present();
|
||||
}
|
||||
|
||||
function presentBothHeaders() {
|
||||
function presentBothHeadersOnly() {
|
||||
openAlert({
|
||||
header: 'Header',
|
||||
subHeader: 'Subtitle',
|
||||
message: 'This is an alert message.',
|
||||
buttons: ['OK'],
|
||||
});
|
||||
}
|
||||
|
||||
function presentHeaderOnly() {
|
||||
openAlert({
|
||||
header: 'Header',
|
||||
buttons: ['OK'],
|
||||
});
|
||||
}
|
||||
@@ -46,7 +53,6 @@
|
||||
function presentSubHeaderOnly() {
|
||||
openAlert({
|
||||
subHeader: 'Subtitle',
|
||||
message: 'This is an alert message.',
|
||||
buttons: ['OK'],
|
||||
});
|
||||
}
|
||||
@@ -58,10 +64,11 @@
|
||||
});
|
||||
}
|
||||
|
||||
function presentNoMessage() {
|
||||
function presentHeadersAndMessage() {
|
||||
openAlert({
|
||||
header: 'Header',
|
||||
subHeader: 'Subtitle',
|
||||
message: 'This is an alert message.',
|
||||
buttons: ['OK'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { h } from '@stencil/core';
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Alert } from '../alert';
|
||||
import { h } from '@stencil/core';
|
||||
|
||||
describe('alert: id', () => {
|
||||
it('alert should be assigned an incrementing id', async () => {
|
||||
@@ -49,7 +49,7 @@ describe('alert: id', () => {
|
||||
template: () => <ion-alert htmlAttributes={{ id }} overlayIndex={-1}></ion-alert>,
|
||||
});
|
||||
|
||||
const alert = page.body.querySelector('ion-alert')!;
|
||||
expect(alert.id).toBe(id);
|
||||
const alertwrapper = page.body.querySelector('.alert-wrapper')!;
|
||||
expect(alertwrapper.id).toBe(id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ configs({ directions: ['ltr'] }).forEach(({ config, screenshot, title }) => {
|
||||
await page.keyboard.press(tabKey);
|
||||
await expect(alertBtns.nth(0)).toBeFocused();
|
||||
|
||||
await page.keyboard.press(`Shift+${tabKey}`); // this will focus the alert-wrapper
|
||||
await page.keyboard.press(`Shift+${tabKey}`);
|
||||
await expect(alertBtns.nth(2)).toBeFocused();
|
||||
|
||||
@@ -30,7 +31,7 @@ configs({ directions: ['ltr'] }).forEach(({ config, screenshot, title }) => {
|
||||
const alertFixture = new AlertFixture(page, screenshot);
|
||||
|
||||
const alert = await alertFixture.open('#basic');
|
||||
await expect(alert).toHaveAttribute('data-testid', 'basic-alert');
|
||||
await expect(alert.locator('.alert-wrapper')).toHaveAttribute('data-testid', 'basic-alert');
|
||||
});
|
||||
|
||||
test('should dismiss when async handler resolves', async ({ page }) => {
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 10 KiB |
@@ -51,7 +51,6 @@ export class Backdrop implements ComponentInterface {
|
||||
const mode = getIonMode(this);
|
||||
return (
|
||||
<Host
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
class={{
|
||||
[mode]: true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Prop, h } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Method, Prop, h } from '@stencil/core';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import { inheritAriaAttributes, renderHiddenInput } from '@utils/helpers';
|
||||
import { createColorClasses, hostContext } from '@utils/theme';
|
||||
@@ -121,7 +121,9 @@ export class Checkbox implements ComponentInterface {
|
||||
};
|
||||
}
|
||||
|
||||
private setFocus() {
|
||||
/** @internal */
|
||||
@Method()
|
||||
async setFocus() {
|
||||
if (this.focusEl) {
|
||||
this.focusEl.focus();
|
||||
}
|
||||
|
||||
@@ -167,13 +167,34 @@ export const handleToolbarIntersection = (
|
||||
|
||||
export const setHeaderActive = (headerIndex: HeaderIndex, active = true) => {
|
||||
const headerEl = headerIndex.el;
|
||||
const toolbars = headerIndex.toolbars;
|
||||
const ionTitles = toolbars.map((toolbar) => toolbar.ionTitleEl);
|
||||
|
||||
if (active) {
|
||||
headerEl.classList.remove('header-collapse-condense-inactive');
|
||||
headerEl.removeAttribute('aria-hidden');
|
||||
|
||||
ionTitles.forEach((ionTitle) => {
|
||||
if (ionTitle) {
|
||||
ionTitle.removeAttribute('aria-hidden');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
headerEl.classList.add('header-collapse-condense-inactive');
|
||||
headerEl.setAttribute('aria-hidden', 'true');
|
||||
|
||||
/**
|
||||
* The small title should only be accessed by screen readers
|
||||
* when the large title collapses into the small title due
|
||||
* to scrolling.
|
||||
*
|
||||
* Originally, the header was given `aria-hidden="true"`
|
||||
* but this caused issues with screen readers not being
|
||||
* able to access any focusable elements within the header.
|
||||
*/
|
||||
ionTitles.forEach((ionTitle) => {
|
||||
if (ionTitle) {
|
||||
ionTitle.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,13 +3,19 @@ import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('header: condense'), () => {
|
||||
test('should be hidden from screen readers when collapsed', async ({ page }) => {
|
||||
test('should hide small title from screen readers when collapsed', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/29347',
|
||||
});
|
||||
|
||||
await page.goto('/src/components/header/test/condense', config);
|
||||
const largeTitleHeader = page.locator('#largeTitleHeader');
|
||||
const smallTitleHeader = page.locator('#smallTitleHeader');
|
||||
const smallTitle = smallTitleHeader.locator('ion-title');
|
||||
const content = page.locator('ion-content');
|
||||
|
||||
await expect(smallTitleHeader).toHaveAttribute('aria-hidden', 'true');
|
||||
await expect(smallTitle).toHaveAttribute('aria-hidden', 'true');
|
||||
|
||||
await expect(largeTitleHeader).toHaveScreenshot(screenshot(`header-condense-large-title-initial-diff`));
|
||||
|
||||
@@ -24,7 +30,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c
|
||||
* Playwright can't do .not.toHaveAttribute() because a value is expected,
|
||||
* and toHaveAttribute can't accept a value of type null.
|
||||
*/
|
||||
const ariaHidden = await smallTitleHeader.getAttribute('aria-hidden');
|
||||
const ariaHidden = await smallTitle.getAttribute('aria-hidden');
|
||||
expect(ariaHidden).toBeNull();
|
||||
|
||||
await content.evaluate(async (el: HTMLIonContentElement) => {
|
||||
@@ -32,7 +38,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c
|
||||
});
|
||||
await page.locator('#smallTitleHeader.header-collapse-condense-inactive').waitFor();
|
||||
|
||||
await expect(smallTitleHeader).toHaveAttribute('aria-hidden', 'true');
|
||||
await expect(smallTitle).toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,6 +33,8 @@ import { getCounterText } from './input.utils';
|
||||
export class Input implements ComponentInterface {
|
||||
private nativeInput?: HTMLInputElement;
|
||||
private inputId = `ion-input-${inputIds++}`;
|
||||
private helperTextId = `${this.inputId}-helper-text`;
|
||||
private errorTextId = `${this.inputId}-error-text`;
|
||||
private inheritedAttributes: Attributes = {};
|
||||
private isComposing = false;
|
||||
private slotMutationController?: SlotMutationController;
|
||||
@@ -573,9 +575,30 @@ export class Input implements ComponentInterface {
|
||||
* Renders the helper text or error text values
|
||||
*/
|
||||
private renderHintText() {
|
||||
const { helperText, errorText } = this;
|
||||
const { helperText, errorText, helperTextId, errorTextId } = this;
|
||||
|
||||
return [<div class="helper-text">{helperText}</div>, <div class="error-text">{errorText}</div>];
|
||||
return [
|
||||
<div id={helperTextId} class="helper-text">
|
||||
{helperText}
|
||||
</div>,
|
||||
<div id={errorTextId} class="error-text">
|
||||
{errorText}
|
||||
</div>,
|
||||
];
|
||||
}
|
||||
|
||||
private getHintTextID(): string | undefined {
|
||||
const { el, helperText, errorText, helperTextId, errorTextId } = this;
|
||||
|
||||
if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
|
||||
return errorTextId;
|
||||
}
|
||||
|
||||
if (helperText) {
|
||||
return helperTextId;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private renderCounter() {
|
||||
@@ -777,6 +800,8 @@ export class Input implements ComponentInterface {
|
||||
onKeyDown={this.onKeydown}
|
||||
onCompositionstart={this.onCompositionStart}
|
||||
onCompositionend={this.onCompositionEnd}
|
||||
aria-describedby={this.getHintTextID()}
|
||||
aria-invalid={this.getHintTextID() === this.errorTextId}
|
||||
{...this.inheritedAttributes}
|
||||
/>
|
||||
{this.clearInput && !readonly && !disabled && (
|
||||
|
||||
@@ -68,6 +68,19 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
|
||||
await expect(helperText).toHaveText('my helper');
|
||||
await expect(errorText).toBeHidden();
|
||||
});
|
||||
test('input should have an aria-describedby attribute when helper text is present', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`<ion-input helper-text="my helper" error-text="my error" label="my input"></ion-input>`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input input');
|
||||
const helperText = page.locator('ion-input .helper-text');
|
||||
const helperTextId = await helperText.getAttribute('id');
|
||||
const ariaDescribedBy = await input.getAttribute('aria-describedby');
|
||||
|
||||
expect(ariaDescribedBy).toBe(helperTextId);
|
||||
});
|
||||
test('error text should be visible when input is invalid', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`<ion-input class="ion-invalid ion-touched" helper-text="my helper" error-text="my error" label="my input"></ion-input>`,
|
||||
@@ -96,6 +109,48 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
|
||||
const errorText = page.locator('ion-input .error-text');
|
||||
await expect(errorText).toHaveScreenshot(screenshot(`input-error-custom-color`));
|
||||
});
|
||||
test('input should have an aria-describedby attribute when error text is present', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`<ion-input class="ion-invalid ion-touched" helper-text="my helper" error-text="my error" label="my input"></ion-input>`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input input');
|
||||
const errorText = page.locator('ion-input .error-text');
|
||||
const errorTextId = await errorText.getAttribute('id');
|
||||
const ariaDescribedBy = await input.getAttribute('aria-describedby');
|
||||
|
||||
expect(ariaDescribedBy).toBe(errorTextId);
|
||||
});
|
||||
test('input should have aria-invalid attribute when input is invalid', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`<ion-input class="ion-invalid ion-touched" helper-text="my helper" error-text="my error" label="my input"></ion-input>`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input input');
|
||||
|
||||
await expect(input).toHaveAttribute('aria-invalid');
|
||||
});
|
||||
test('input should not have aria-invalid attribute when input is valid', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`<ion-input helper-text="my helper" error-text="my error" label="my input"></ion-input>`,
|
||||
config
|
||||
);
|
||||
|
||||
const input = page.locator('ion-input input');
|
||||
|
||||
await expect(input).not.toHaveAttribute('aria-invalid');
|
||||
});
|
||||
test('input should not have aria-describedby attribute when no hint or error text is present', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setContent(`<ion-input label="my input"></ion-input>`, config);
|
||||
|
||||
const input = page.locator('ion-input input');
|
||||
|
||||
await expect(input).not.toHaveAttribute('aria-describedby');
|
||||
});
|
||||
});
|
||||
test.describe('input: hint text rendering', () => {
|
||||
test.describe('regular inputs', () => {
|
||||
|
||||
@@ -356,7 +356,7 @@ export class Loading implements ComponentInterface, OverlayInterface {
|
||||
>
|
||||
<ion-backdrop visible={this.showBackdrop} tappable={this.backdropDismiss} />
|
||||
|
||||
<div tabindex="0"></div>
|
||||
<div tabindex="0" aria-hidden="true"></div>
|
||||
|
||||
<div class="loading-wrapper ion-overlay-wrapper">
|
||||
{spinner && (
|
||||
@@ -368,7 +368,7 @@ export class Loading implements ComponentInterface, OverlayInterface {
|
||||
{message !== undefined && this.renderLoadingMessage(msgId)}
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
<div tabindex="0" aria-hidden="true"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface MenuI {
|
||||
close(animated?: boolean): Promise<boolean>;
|
||||
toggle(animated?: boolean): Promise<boolean>;
|
||||
setOpen(shouldOpen: boolean, animated?: boolean): Promise<boolean>;
|
||||
_setOpen(shouldOpen: boolean, animated?: boolean): Promise<boolean>;
|
||||
_setOpen(shouldOpen: boolean, animated?: boolean, role?: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface MenuControllerI {
|
||||
@@ -42,7 +42,7 @@ export interface MenuControllerI {
|
||||
_createAnimation(type: string, menuCmp: MenuI): Promise<Animation>;
|
||||
_register(menu: MenuI): void;
|
||||
_unregister(menu: MenuI): void;
|
||||
_setOpen(menu: MenuI, shouldOpen: boolean, animated: boolean): Promise<boolean>;
|
||||
_setOpen(menu: MenuI, shouldOpen: boolean, animated: boolean, role?: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface MenuChangeEventDetail {
|
||||
@@ -50,6 +50,10 @@ export interface MenuChangeEventDetail {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
export interface MenuCloseEventDetail {
|
||||
role?: string;
|
||||
}
|
||||
|
||||
export interface MenuCustomEvent<T = any> extends CustomEvent {
|
||||
detail: T;
|
||||
target: HTMLIonMenuElement;
|
||||
|
||||
@@ -7,14 +7,15 @@ import { shouldUseCloseWatcher } from '@utils/hardware-back-button';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import { inheritAriaAttributes, assert, clamp, isEndSide as isEnd } from '@utils/helpers';
|
||||
import { menuController } from '@utils/menu-controller';
|
||||
import { getPresentedOverlay } from '@utils/overlays';
|
||||
import { BACKDROP, GESTURE, getPresentedOverlay } from '@utils/overlays';
|
||||
import { isPlatform } from '@utils/platform';
|
||||
import { hostContext } from '@utils/theme';
|
||||
|
||||
import { config } from '../../global/config';
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import type { Animation, Gesture, GestureDetail } from '../../interface';
|
||||
|
||||
import type { MenuChangeEventDetail, MenuI, MenuType, Side } from './menu-interface';
|
||||
import type { MenuChangeEventDetail, MenuCloseEventDetail, MenuI, MenuType, Side } from './menu-interface';
|
||||
|
||||
const iosEasing = 'cubic-bezier(0.32,0.72,0,1)';
|
||||
const mdEasing = 'cubic-bezier(0.0,0.0,0.2,1)';
|
||||
@@ -179,7 +180,7 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
/**
|
||||
* Emitted when the menu is about to be closed.
|
||||
*/
|
||||
@Event() ionWillClose!: EventEmitter<void>;
|
||||
@Event() ionWillClose!: EventEmitter<MenuCloseEventDetail>;
|
||||
/**
|
||||
* Emitted when the menu is open.
|
||||
*/
|
||||
@@ -188,7 +189,7 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
/**
|
||||
* Emitted when the menu is closed.
|
||||
*/
|
||||
@Event() ionDidClose!: EventEmitter<void>;
|
||||
@Event() ionDidClose!: EventEmitter<MenuCloseEventDetail>;
|
||||
|
||||
/**
|
||||
* Emitted when the menu state is changed.
|
||||
@@ -331,14 +332,14 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
if (shouldClose) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.close();
|
||||
this.close(undefined, BACKDROP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === 'Escape') {
|
||||
this.close();
|
||||
this.close(undefined, BACKDROP);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,8 +376,8 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
* it returns `false`.
|
||||
*/
|
||||
@Method()
|
||||
close(animated = true): Promise<boolean> {
|
||||
return this.setOpen(false, animated);
|
||||
close(animated = true, role?: string): Promise<boolean> {
|
||||
return this.setOpen(false, animated, role);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -393,8 +394,8 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
* If the operation can't be completed successfully, it returns `false`.
|
||||
*/
|
||||
@Method()
|
||||
setOpen(shouldOpen: boolean, animated = true): Promise<boolean> {
|
||||
return menuController._setOpen(this, shouldOpen, animated);
|
||||
setOpen(shouldOpen: boolean, animated = true, role?: string): Promise<boolean> {
|
||||
return menuController._setOpen(this, shouldOpen, animated, role);
|
||||
}
|
||||
|
||||
private trapKeyboardFocus(ev: Event, doc: Document) {
|
||||
@@ -438,13 +439,13 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
}
|
||||
}
|
||||
|
||||
async _setOpen(shouldOpen: boolean, animated = true): Promise<boolean> {
|
||||
async _setOpen(shouldOpen: boolean, animated = true, role?: string): Promise<boolean> {
|
||||
// If the menu is disabled or it is currently being animated, let's do nothing
|
||||
if (!this._isActive() || this.isAnimating || shouldOpen === this._isOpen) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.beforeAnimation(shouldOpen);
|
||||
this.beforeAnimation(shouldOpen, role);
|
||||
|
||||
await this.loadAnimation();
|
||||
await this.startAnimation(shouldOpen, animated);
|
||||
@@ -459,7 +460,7 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.afterAnimation(shouldOpen);
|
||||
this.afterAnimation(shouldOpen, role);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -542,7 +543,7 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
}
|
||||
|
||||
private onWillStart(): Promise<void> {
|
||||
this.beforeAnimation(!this._isOpen);
|
||||
this.beforeAnimation(!this._isOpen, GESTURE);
|
||||
return this.loadAnimation();
|
||||
}
|
||||
|
||||
@@ -624,13 +625,30 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
|
||||
this.animation
|
||||
.easing('cubic-bezier(0.4, 0.0, 0.6, 1)')
|
||||
.onFinish(() => this.afterAnimation(shouldOpen), { oneTimeCallback: true })
|
||||
.onFinish(() => this.afterAnimation(shouldOpen, GESTURE), { oneTimeCallback: true })
|
||||
.progressEnd(playTo ? 1 : 0, this._isOpen ? 1 - newStepValue : newStepValue, 300);
|
||||
}
|
||||
|
||||
private beforeAnimation(shouldOpen: boolean) {
|
||||
private beforeAnimation(shouldOpen: boolean, role?: string) {
|
||||
assert(!this.isAnimating, '_before() should not be called while animating');
|
||||
|
||||
/**
|
||||
* When the menu is presented on an Android device, TalkBack's focus rings
|
||||
* may appear in the wrong position due to the transition (specifically
|
||||
* `transform` styles). This occurs because the focus rings are initially
|
||||
* displayed at the starting position of the elements before the transition
|
||||
* begins. This workaround ensures the focus rings do not appear in the
|
||||
* incorrect location.
|
||||
*
|
||||
* If this solution is applied to iOS devices, then it leads to a bug where
|
||||
* the overlays cannot be accessed by screen readers. This is due to
|
||||
* VoiceOver not being able to update the accessibility tree when the
|
||||
* `aria-hidden` is removed.
|
||||
*/
|
||||
if (isPlatform('android')) {
|
||||
this.el.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
// this places the menu into the correct location before it animates in
|
||||
// this css class doesn't actually kick off any animations
|
||||
this.el.classList.add(SHOW_MENU);
|
||||
@@ -671,11 +689,11 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
if (shouldOpen) {
|
||||
this.ionWillOpen.emit();
|
||||
} else {
|
||||
this.ionWillClose.emit();
|
||||
this.ionWillClose.emit({ role });
|
||||
}
|
||||
}
|
||||
|
||||
private afterAnimation(isOpen: boolean) {
|
||||
private afterAnimation(isOpen: boolean, role?: string) {
|
||||
// keep opening/closing the menu disabled for a touch more yet
|
||||
// only add listeners/css if it's enabled and isOpen
|
||||
// and only remove listeners/css if it's not open
|
||||
@@ -687,6 +705,17 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
/**
|
||||
* When the menu is presented on an Android device, TalkBack's focus rings
|
||||
* may appear in the wrong position due to the transition (specifically
|
||||
* `transform` styles). The menu is hidden from screen readers during the
|
||||
* transition to prevent this. Once the transition is complete, the menu
|
||||
* is shown again.
|
||||
*/
|
||||
if (isPlatform('android')) {
|
||||
this.el.removeAttribute('aria-hidden');
|
||||
}
|
||||
|
||||
// emit open event
|
||||
this.ionDidOpen.emit();
|
||||
|
||||
@@ -703,6 +732,8 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
// start focus trapping
|
||||
document.addEventListener('focus', this.handleFocus, true);
|
||||
} else {
|
||||
this.el.removeAttribute('aria-hidden');
|
||||
|
||||
// remove css classes and unhide content from screen readers
|
||||
this.el.classList.remove(SHOW_MENU);
|
||||
|
||||
@@ -731,7 +762,7 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
}
|
||||
|
||||
// emit close event
|
||||
this.ionDidClose.emit();
|
||||
this.ionDidClose.emit({ role });
|
||||
|
||||
// undo focus trapping so multiple menus don't collide
|
||||
document.removeEventListener('focus', this.handleFocus, true);
|
||||
@@ -767,7 +798,7 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
* If the menu is disabled then we should
|
||||
* forcibly close the menu even if it is open.
|
||||
*/
|
||||
this.afterAnimation(false);
|
||||
this.afterAnimation(false, GESTURE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,9 @@
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-button id="start-menu-button">Button</ion-button>
|
||||
<ion-menu-toggle>
|
||||
<ion-button id="start-menu-button">Button</ion-button>
|
||||
</ion-menu-toggle>
|
||||
<ion-item>Menu Item</ion-item>
|
||||
<ion-item>Menu Item</ion-item>
|
||||
<ion-item>Menu Item</ion-item>
|
||||
@@ -125,6 +127,19 @@
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
window.addEventListener('ionWillOpen', function (e) {
|
||||
console.log('ionWillOpen', e);
|
||||
});
|
||||
window.addEventListener('ionDidOpen', function (e) {
|
||||
console.log('ionDidOpen', e);
|
||||
});
|
||||
window.addEventListener('ionWillClose', function (e) {
|
||||
console.log('ionWillClose', e);
|
||||
});
|
||||
window.addEventListener('ionDidClose', function (e) {
|
||||
console.log('ionDidClose', e);
|
||||
});
|
||||
|
||||
async function openStart() {
|
||||
// Open the menu by menu-id
|
||||
await menuController.enable(true, 'start-menu');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
import type { E2EPage, ScreenshotFn } from '@utils/test/playwright';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
import { configs, dragElementBy, test } from '@utils/test/playwright';
|
||||
|
||||
configs().forEach(({ title, config, screenshot }) => {
|
||||
test.describe(title('menu: rendering'), () => {
|
||||
@@ -140,6 +140,97 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions
|
||||
*/
|
||||
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('menu: events'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(`/src/components/menu/test/basic`, config);
|
||||
});
|
||||
|
||||
test('should pass role when swiping to close', async ({ page }) => {
|
||||
const ionDidOpen = await page.spyOnEvent('ionDidOpen');
|
||||
const ionWillClose = await page.spyOnEvent('ionWillClose');
|
||||
const ionDidClose = await page.spyOnEvent('ionDidClose');
|
||||
|
||||
await page.click('#open-start');
|
||||
await ionDidOpen.next();
|
||||
|
||||
const menu = page.locator('#start-menu');
|
||||
await dragElementBy(menu, page, -150, 0);
|
||||
|
||||
await ionWillClose.next();
|
||||
await ionDidClose.next();
|
||||
await expect(ionWillClose).toHaveReceivedEventDetail({ role: 'gesture' });
|
||||
await expect(ionDidClose).toHaveReceivedEventDetail({ role: 'gesture' });
|
||||
});
|
||||
|
||||
test('should pass role when clicking backdrop to close', async ({ page }) => {
|
||||
const ionDidOpen = await page.spyOnEvent('ionDidOpen');
|
||||
const ionWillClose = await page.spyOnEvent('ionWillClose');
|
||||
const ionDidClose = await page.spyOnEvent('ionDidClose');
|
||||
|
||||
await page.click('#open-start');
|
||||
await ionDidOpen.next();
|
||||
|
||||
const menu = page.locator('#start-menu');
|
||||
const backdrop = menu.locator('ion-backdrop');
|
||||
|
||||
/**
|
||||
* Coordinates for the click event.
|
||||
* These need to be near the right edge of the backdrop
|
||||
* in order to avoid clicking on the menu.
|
||||
*/
|
||||
const backdropBoundingBox = await backdrop.boundingBox();
|
||||
const x = backdropBoundingBox!.width - 50;
|
||||
const y = backdropBoundingBox!.height - 50;
|
||||
|
||||
// Click near the right side of the backdrop.
|
||||
await backdrop.click({
|
||||
position: { x, y },
|
||||
});
|
||||
|
||||
await ionWillClose.next();
|
||||
await ionDidClose.next();
|
||||
await expect(ionWillClose).toHaveReceivedEventDetail({ role: 'backdrop' });
|
||||
await expect(ionDidClose).toHaveReceivedEventDetail({ role: 'backdrop' });
|
||||
});
|
||||
|
||||
test('should pass role when pressing escape key to close', async ({ page }) => {
|
||||
const ionDidOpen = await page.spyOnEvent('ionDidOpen');
|
||||
const ionWillClose = await page.spyOnEvent('ionWillClose');
|
||||
const ionDidClose = await page.spyOnEvent('ionDidClose');
|
||||
|
||||
await page.click('#open-start');
|
||||
await ionDidOpen.next();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await ionWillClose.next();
|
||||
await ionDidClose.next();
|
||||
await expect(ionWillClose).toHaveReceivedEventDetail({ role: 'backdrop' });
|
||||
await expect(ionDidClose).toHaveReceivedEventDetail({ role: 'backdrop' });
|
||||
});
|
||||
|
||||
test('should not pass role when clicking a menu toggle button to close', async ({ page }) => {
|
||||
const ionDidOpen = await page.spyOnEvent('ionDidOpen');
|
||||
const ionWillClose = await page.spyOnEvent('ionWillClose');
|
||||
const ionDidClose = await page.spyOnEvent('ionDidClose');
|
||||
|
||||
await page.click('#open-start');
|
||||
await ionDidOpen.next();
|
||||
|
||||
await page.click('#start-menu-button');
|
||||
|
||||
await ionWillClose.next();
|
||||
await ionDidClose.next();
|
||||
await expect(ionWillClose).toHaveReceivedEventDetail({ role: undefined });
|
||||
await expect(ionDidClose).toHaveReceivedEventDetail({ role: undefined });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function testMenu(page: E2EPage, menu: Locator, menuId: string, screenshot: ScreenshotFn) {
|
||||
const ionDidOpen = await page.spyOnEvent('ionDidOpen');
|
||||
const ionDidClose = await page.spyOnEvent('ionDidClose');
|
||||
|
||||
@@ -154,6 +154,13 @@
|
||||
</div>
|
||||
</ion-app>
|
||||
<script>
|
||||
window.addEventListener('ionModalDidDismiss', function (e) {
|
||||
console.log('DidDismiss', e);
|
||||
});
|
||||
window.addEventListener('ionModalWillDismiss', function (e) {
|
||||
console.log('WillDismiss', e);
|
||||
});
|
||||
|
||||
function createModal(options) {
|
||||
let items = '';
|
||||
|
||||
|
||||
@@ -375,7 +375,7 @@ export class Picker implements ComponentInterface, OverlayInterface {
|
||||
>
|
||||
<ion-backdrop visible={this.showBackdrop} tappable={this.backdropDismiss}></ion-backdrop>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
<div tabindex="0" aria-hidden="true"></div>
|
||||
|
||||
<div class="picker-wrapper ion-overlay-wrapper" role="dialog">
|
||||
<div class="picker-toolbar">
|
||||
@@ -395,7 +395,7 @@ export class Picker implements ComponentInterface, OverlayInterface {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
<div tabindex="0" aria-hidden="true"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
ion-app > ion-content {
|
||||
--background: #dddddd;
|
||||
}
|
||||
ion-content button {
|
||||
ion-content button.trigger {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.grid {
|
||||
@@ -57,26 +57,30 @@
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>Cover</h2>
|
||||
<button id="cover-trigger">Trigger</button>
|
||||
<button id="cover-trigger" class="trigger">Trigger</button>
|
||||
<ion-popover show-backdrop="false" class="cover-popover" trigger="cover-trigger" size="cover">
|
||||
<ion-content class="ion-padding"> My really really really really long content </ion-content>
|
||||
</ion-popover>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>With Event</h2>
|
||||
<button id="event-trigger" onclick="openPopover('event-popover', event, 'false')">Trigger</button>
|
||||
<button id="event-trigger" class="trigger" onclick="openPopover('event-popover', event, 'false')">
|
||||
Trigger
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid-item">
|
||||
<h2>Auto</h2>
|
||||
<button id="auto-trigger">Trigger</button>
|
||||
<button id="auto-trigger" class="trigger">Trigger</button>
|
||||
<ion-popover show-backdrop="false" class="auto-popover" trigger="auto-trigger">
|
||||
<ion-content class="ion-padding"> My really really really really long content </ion-content>
|
||||
</ion-popover>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>No Event</h2>
|
||||
<button id="no-event-trigger" onclick="openPopover('no-event-popover', null, 'true')">Trigger</button>
|
||||
<button id="no-event-trigger" class="trigger" onclick="openPopover('no-event-popover', null, 'true')">
|
||||
Trigger
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Listen, Prop, Watch, h } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Listen, Method, Prop, Watch, h } from '@stencil/core';
|
||||
import { renderHiddenInput } from '@utils/helpers';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
@@ -155,7 +155,9 @@ export class RadioGroup implements ComponentInterface {
|
||||
|
||||
@Listen('keydown', { target: 'document' })
|
||||
onKeydown(ev: KeyboardEvent) {
|
||||
const inSelectPopover = !!this.el.closest('ion-select-popover');
|
||||
// We don't want the value to automatically change/emit when the radio group is part of a select interface
|
||||
// as this will cause the interface to close when navigating through the radio group options
|
||||
const inSelectInterface = !!this.el.closest('ion-select-popover') || !!this.el.closest('ion-select-modal');
|
||||
|
||||
if (ev.target && !this.el.contains(ev.target as HTMLElement)) {
|
||||
return;
|
||||
@@ -187,7 +189,7 @@ export class RadioGroup implements ComponentInterface {
|
||||
if (next && radios.includes(next)) {
|
||||
next.setFocus(ev);
|
||||
|
||||
if (!inSelectPopover) {
|
||||
if (!inSelectInterface) {
|
||||
this.value = next.value;
|
||||
this.emitValueChange(ev);
|
||||
}
|
||||
@@ -215,6 +217,13 @@ export class RadioGroup implements ComponentInterface {
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@Method()
|
||||
async setFocus() {
|
||||
const radioToFocus = this.getRadios().find((r) => r.tabIndex !== -1);
|
||||
radioToFocus?.setFocus();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { label, labelId, el, name, value } = this;
|
||||
const mode = getIonMode(this);
|
||||
|
||||
@@ -126,9 +126,11 @@ export class Radio implements ComponentInterface {
|
||||
|
||||
/** @internal */
|
||||
@Method()
|
||||
async setFocus(ev: globalThis.Event) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
async setFocus(ev?: globalThis.Event) {
|
||||
if (ev !== undefined) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
this.el.focus();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core';
|
||||
import { findClosestIonContent, disableContentScrollY, resetContentScrollY } from '@utils/content';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import { inheritAriaAttributes, clamp, debounceEvent, renderHiddenInput } from '@utils/helpers';
|
||||
import { inheritAriaAttributes, clamp, debounceEvent, renderHiddenInput, isSafeNumber } from '@utils/helpers';
|
||||
import { printIonWarning } from '@utils/logging';
|
||||
import { isRTL } from '@utils/rtl';
|
||||
import { createColorClasses, hostContext } from '@utils/theme';
|
||||
@@ -109,7 +109,11 @@ export class Range implements ComponentInterface {
|
||||
*/
|
||||
@Prop() min = 0;
|
||||
@Watch('min')
|
||||
protected minChanged() {
|
||||
protected minChanged(newValue: number) {
|
||||
if (!isSafeNumber(newValue)) {
|
||||
this.min = 0;
|
||||
}
|
||||
|
||||
if (!this.noUpdate) {
|
||||
this.updateRatio();
|
||||
}
|
||||
@@ -120,7 +124,11 @@ export class Range implements ComponentInterface {
|
||||
*/
|
||||
@Prop() max = 100;
|
||||
@Watch('max')
|
||||
protected maxChanged() {
|
||||
protected maxChanged(newValue: number) {
|
||||
if (!isSafeNumber(newValue)) {
|
||||
this.max = 100;
|
||||
}
|
||||
|
||||
if (!this.noUpdate) {
|
||||
this.updateRatio();
|
||||
}
|
||||
@@ -151,6 +159,12 @@ export class Range implements ComponentInterface {
|
||||
* Specifies the value granularity.
|
||||
*/
|
||||
@Prop() step = 1;
|
||||
@Watch('step')
|
||||
protected stepChanged(newValue: number) {
|
||||
if (!isSafeNumber(newValue)) {
|
||||
this.step = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If `true`, tick marks are displayed based on the step value.
|
||||
@@ -300,6 +314,11 @@ export class Range implements ComponentInterface {
|
||||
}
|
||||
|
||||
this.inheritedAttributes = inheritAriaAttributes(this.el);
|
||||
// If min, max, or step are not safe, set them to 0, 100, and 1, respectively.
|
||||
// Each watch does this, but not before the initial load.
|
||||
this.min = isSafeNumber(this.min) ? this.min : 0;
|
||||
this.max = isSafeNumber(this.max) ? this.max : 100;
|
||||
this.step = isSafeNumber(this.step) ? this.step : 1;
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
|
||||