Compare commits
63 Commits
tr/segment
...
shane-patc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8899f9b4a7 | ||
|
|
a9c20af0b7 | ||
|
|
e05865c694 | ||
|
|
cbd3a9f848 | ||
|
|
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 | ||
|
|
4d0e9c4186 | ||
|
|
668b2dac57 | ||
|
|
078ed0b86a |
@@ -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
|
||||
|
||||
2
.github/workflows/assign-issues.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: 'Auto-assign issue'
|
||||
uses: pozil/auto-assign-issue@c5bca5027e680b9e8411b826d16947afd8c76b32 # v2.0.0
|
||||
uses: pozil/auto-assign-issue@c015a6a3f410f12f58255c3d085fd774312f7a2f # v2.1.2
|
||||
with:
|
||||
assignees: brandyscarney, thetaPC, joselrio, rugoncalves, BenOsodrac, JoaoFerreira-FrontEnd, OS-giulianasilva, tanner-reits
|
||||
numOfAssignee: 1
|
||||
|
||||
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:
|
||||
|
||||
95
CHANGELOG.md
@@ -3,6 +3,101 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **segment:** prevent flickering for scrollable on iOS ([#29884](https://github.com/ionic-team/ionic-framework/issues/29884)) ([078ed0b](https://github.com/ionic-team/ionic-framework/commit/078ed0b86a0d8e9f8457481cb739ea214195adce)), closes [#29523](https://github.com/ionic-team/ionic-framework/issues/29523)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.3.1](https://github.com/ionic-team/ionic-framework/compare/v8.3.0...v8.3.1) (2024-09-17)
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,93 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **segment:** prevent flickering for scrollable on iOS ([#29884](https://github.com/ionic-team/ionic-framework/issues/29884)) ([078ed0b](https://github.com/ionic-team/ionic-framework/commit/078ed0b86a0d8e9f8457481cb739ea214195adce)), closes [#29523](https://github.com/ionic-team/ionic-framework/issues/29523)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [8.3.1](https://github.com/ionic-team/ionic-framework/compare/v8.3.0...v8.3.1) (2024-09-17)
|
||||
|
||||
|
||||
|
||||
22
core/api.txt
@@ -143,6 +143,7 @@ ion-alert,css-prop,--width,ios
|
||||
ion-alert,css-prop,--width,md
|
||||
|
||||
ion-app,none
|
||||
ion-app,method,setFocus,setFocus(elements: HTMLElement[]) => Promise<void>
|
||||
|
||||
ion-avatar,shadow
|
||||
ion-avatar,css-prop,--border-radius,ios
|
||||
@@ -999,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
|
||||
@@ -1608,14 +1609,10 @@ ion-segment-button,part,indicator-background
|
||||
ion-segment-button,part,native
|
||||
|
||||
ion-segment-content,shadow
|
||||
ion-segment-content,prop,disabled,boolean,false,false,false
|
||||
|
||||
ion-segment-view,shadow
|
||||
ion-segment-view,prop,disabled,boolean,false,false,false
|
||||
ion-segment-view,method,setContent,setContent(id: string, smoothScroll?: boolean) => Promise<void>
|
||||
ion-segment-view,event,ionSegmentViewScroll,{ scrollDirection: string; scrollDistance: number; scrollDistancePercentage: number; },true
|
||||
ion-segment-view,event,ionSegmentViewScrollEnd,{ activeContentId: string; },true
|
||||
ion-segment-view,event,ionSegmentViewScrollStart,void,true
|
||||
ion-segment-view,event,ionSegmentViewScroll,SegmentViewScrollEvent,true
|
||||
|
||||
ion-select,shadow
|
||||
ion-select,prop,cancelText,string,'Cancel',false,false
|
||||
@@ -1624,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
|
||||
@@ -1682,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
|
||||
|
||||
683
core/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "8.3.1",
|
||||
"version": "8.4.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ionic/core",
|
||||
"version": "8.3.1",
|
||||
"version": "8.4.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "4.20.0",
|
||||
@@ -19,16 +19,16 @@
|
||||
"@capacitor/haptics": "^6.0.0",
|
||||
"@capacitor/keyboard": "^6.0.0",
|
||||
"@capacitor/status-bar": "^6.0.0",
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@clack/prompts": "^0.9.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.9.0",
|
||||
"@types/jest": "^29.5.6",
|
||||
"@types/node": "^14.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.2",
|
||||
@@ -319,18 +319,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
|
||||
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
||||
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
|
||||
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -400,10 +400,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
|
||||
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
|
||||
"version": "7.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz",
|
||||
"integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.26.5"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
@@ -641,14 +644,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
|
||||
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
|
||||
"version": "7.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz",
|
||||
"integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.22.5",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
"@babel/helper-string-parser": "^7.25.9",
|
||||
"@babel/helper-validator-identifier": "^7.25.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -661,45 +663,45 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@capacitor/core": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.1.2.tgz",
|
||||
"integrity": "sha512-xFy1/4qLFLp5WCIzIhtwUuVNNoz36+V7/BzHmLqgVJcvotc4MMjswW/TshnPQaLLujEOaLkA4h8ZJ0uoK3ImGg==",
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.0.tgz",
|
||||
"integrity": "sha512-B9IlJtDpUqhhYb+T8+cp2Db/3RETX36STgjeU2kQZBs/SLAcFiMama227o+msRjLeo3DO+7HJjWVA1+XlyyPEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/haptics": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-6.0.1.tgz",
|
||||
"integrity": "sha512-Q8hedLwfwTSWEYc3eoATzkdKHBaIceYe5bd7FjxQCENNH0is5Ft0EjSRPz/xpTn39ebK0ooZBDBCwsyl6tjiTA==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-6.0.2.tgz",
|
||||
"integrity": "sha512-xcFdIH4iIIeW2+1lzmlYMVicqB9ytaiuZ9NE3a9laKFPvMGC7hdj6i6tHFezwPJ/96xkHOwXT2b0F8Mh9xtTWg==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/keyboard": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-6.0.2.tgz",
|
||||
"integrity": "sha512-fOfO3rQ0ZXuTHpK03INVTwmBnpqMiH8EHPpNaHjwjKwdrVRWBvtgIFhuyHNXh53rdcXw+uHB+1RIiNabnCrITw==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-6.0.3.tgz",
|
||||
"integrity": "sha512-V/mURxBI68HvClYjrGBlOriWkwYN7r+cWid/igJz/3scNc/V81DgQ9fpoLr4W0I5NY7YxOesjIJLuLO+LT18mQ==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/status-bar": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-6.0.1.tgz",
|
||||
"integrity": "sha512-Usd9hZZQVAqy+jJfL7jRcYI7dcsxN09Na1yttwdl+F1bk3Ztoukk7CGPDm5VgKUSs53ihQBOy1+sczCACxhNiw==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-6.0.2.tgz",
|
||||
"integrity": "sha512-AmRIX6QvFemItlY7/69ARkIAqitRQqJ2qwgZmD1KqgFb78pH+XFXm1guvS/a8CuOOm/IqZ4ddDbl20yxtBqzGA==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@clack/core": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.4.tgz",
|
||||
"integrity": "sha512-H4hxZDXgHtWTwV3RAVenqcC4VbJZNegbBjlPvzOzCouXtS2y3sDvlO3IsbrPNWuLWPPlYVYPghQdSF64683Ldw==",
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.4.1.tgz",
|
||||
"integrity": "sha512-Pxhij4UXg8KSr7rPek6Zowm+5M22rbd2g1nfojHJkxp5YkFqiZ2+YLEM/XGVIzvGOcM0nqjIFxrpDwWRZYWYjA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picocolors": "^1.0.0",
|
||||
@@ -707,32 +709,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@clack/prompts": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.7.0.tgz",
|
||||
"integrity": "sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==",
|
||||
"bundleDependencies": [
|
||||
"is-unicode-supported"
|
||||
],
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.9.1.tgz",
|
||||
"integrity": "sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@clack/core": "^0.3.3",
|
||||
"is-unicode-supported": "*",
|
||||
"@clack/core": "0.4.1",
|
||||
"picocolors": "^1.0.0",
|
||||
"sisteransi": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
|
||||
"version": "1.3.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
|
||||
@@ -1678,9 +1664,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
@@ -1815,9 +1801,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@stencil/angular-output-target": {
|
||||
"version": "0.8.4",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-0.8.4.tgz",
|
||||
"integrity": "sha512-QvmHTueXXs5vB9W2L12uEzFmAuR8sqATJV2b+SCFmYsjJSaymiSqR3dKo2wnr0tZiTgU1t16BWaUKiSh3wPXpw==",
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-0.10.2.tgz",
|
||||
"integrity": "sha512-jPRa2NMAPtm/iMY+mUaWATbIhgY5zPJfUNQyF8nwC0rMrfXifPoRCf6BbH2S4Gy7SX0X4hlP+jAbVUjQNg/P+Q==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"@stencil/core": ">=2.0.0 || >=3 || >= 4.0.0-beta.0 || >= 4.0.0"
|
||||
@@ -1860,12 +1846,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@stencil/vue-output-target": {
|
||||
"version": "0.8.9",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.8.9.tgz",
|
||||
"integrity": "sha512-1yuapCWYViLlxGlEaeta2wryq4M5zZxxBa+4rEBp54VwW2W/trlzPv0IJyw6I3Il51rHYm2WmWlBLOGmoMyW9Q==",
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.9.2.tgz",
|
||||
"integrity": "sha512-AeBmfo8bQhtob4VKpYTNiCoqh50MeXUwRgYLyO/JxRgAAK9GSfenNrUxXDrK0DK65SWsx/GCOsRwWbfOveorOQ==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"@stencil/core": ">=2.0.0 || >=3 || >= 4.0.0-beta.0 || >= 4.0.0"
|
||||
"@stencil/core": ">=2.0.0 || >=3 || >= 4.0.0-beta.0 || >= 4.0.0",
|
||||
"vue": "^3.4.38"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@stencil/core": {
|
||||
"optional": true
|
||||
},
|
||||
"vue": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@stylelint/postcss-css-in-js": {
|
||||
@@ -2499,6 +2494,171 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
|
||||
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.3",
|
||||
"@vue/shared": "3.5.13",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core/node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core/node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
|
||||
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
|
||||
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.3",
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/compiler-ssr": "3.5.13",
|
||||
"@vue/shared": "3.5.13",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.11",
|
||||
"postcss": "^8.4.48",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc/node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc/node_modules/postcss": {
|
||||
"version": "8.4.49",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
|
||||
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
|
||||
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
|
||||
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
|
||||
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
|
||||
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.13",
|
||||
"@vue/runtime-core": "3.5.13",
|
||||
"@vue/shared": "3.5.13",
|
||||
"csstype": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
|
||||
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
|
||||
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@zeit/schemas": {
|
||||
"version": "2.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.21.0.tgz",
|
||||
@@ -3183,9 +3343,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
|
||||
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
|
||||
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
@@ -3777,6 +3937,13 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@@ -7584,6 +7751,16 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
@@ -7915,6 +8092,25 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@@ -8236,9 +8432,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
@@ -9103,6 +9299,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.13",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
|
||||
@@ -9683,15 +9889,6 @@
|
||||
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -10025,6 +10222,28 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
|
||||
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/compiler-sfc": "3.5.13",
|
||||
"@vue/runtime-dom": "3.5.13",
|
||||
"@vue/server-renderer": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/walker": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
||||
@@ -10505,15 +10724,15 @@
|
||||
}
|
||||
},
|
||||
"@babel/helper-string-parser": {
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
|
||||
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
||||
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
|
||||
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-validator-option": {
|
||||
@@ -10567,10 +10786,13 @@
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
|
||||
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
|
||||
"dev": true
|
||||
"version": "7.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz",
|
||||
"integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.26.5"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-syntax-async-generators": {
|
||||
"version": "7.8.4",
|
||||
@@ -10739,14 +10961,13 @@
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
|
||||
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
|
||||
"version": "7.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz",
|
||||
"integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-string-parser": "^7.22.5",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
"@babel/helper-string-parser": "^7.25.9",
|
||||
"@babel/helper-validator-identifier": "^7.25.9"
|
||||
}
|
||||
},
|
||||
"@bcoe/v8-coverage": {
|
||||
@@ -10756,39 +10977,39 @@
|
||||
"dev": true
|
||||
},
|
||||
"@capacitor/core": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.1.2.tgz",
|
||||
"integrity": "sha512-xFy1/4qLFLp5WCIzIhtwUuVNNoz36+V7/BzHmLqgVJcvotc4MMjswW/TshnPQaLLujEOaLkA4h8ZJ0uoK3ImGg==",
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.0.tgz",
|
||||
"integrity": "sha512-B9IlJtDpUqhhYb+T8+cp2Db/3RETX36STgjeU2kQZBs/SLAcFiMama227o+msRjLeo3DO+7HJjWVA1+XlyyPEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"@capacitor/haptics": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-6.0.1.tgz",
|
||||
"integrity": "sha512-Q8hedLwfwTSWEYc3eoATzkdKHBaIceYe5bd7FjxQCENNH0is5Ft0EjSRPz/xpTn39ebK0ooZBDBCwsyl6tjiTA==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-6.0.2.tgz",
|
||||
"integrity": "sha512-xcFdIH4iIIeW2+1lzmlYMVicqB9ytaiuZ9NE3a9laKFPvMGC7hdj6i6tHFezwPJ/96xkHOwXT2b0F8Mh9xtTWg==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@capacitor/keyboard": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-6.0.2.tgz",
|
||||
"integrity": "sha512-fOfO3rQ0ZXuTHpK03INVTwmBnpqMiH8EHPpNaHjwjKwdrVRWBvtgIFhuyHNXh53rdcXw+uHB+1RIiNabnCrITw==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-6.0.3.tgz",
|
||||
"integrity": "sha512-V/mURxBI68HvClYjrGBlOriWkwYN7r+cWid/igJz/3scNc/V81DgQ9fpoLr4W0I5NY7YxOesjIJLuLO+LT18mQ==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@capacitor/status-bar": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-6.0.1.tgz",
|
||||
"integrity": "sha512-Usd9hZZQVAqy+jJfL7jRcYI7dcsxN09Na1yttwdl+F1bk3Ztoukk7CGPDm5VgKUSs53ihQBOy1+sczCACxhNiw==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-6.0.2.tgz",
|
||||
"integrity": "sha512-AmRIX6QvFemItlY7/69ARkIAqitRQqJ2qwgZmD1KqgFb78pH+XFXm1guvS/a8CuOOm/IqZ4ddDbl20yxtBqzGA==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@clack/core": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.4.tgz",
|
||||
"integrity": "sha512-H4hxZDXgHtWTwV3RAVenqcC4VbJZNegbBjlPvzOzCouXtS2y3sDvlO3IsbrPNWuLWPPlYVYPghQdSF64683Ldw==",
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.4.1.tgz",
|
||||
"integrity": "sha512-Pxhij4UXg8KSr7rPek6Zowm+5M22rbd2g1nfojHJkxp5YkFqiZ2+YLEM/XGVIzvGOcM0nqjIFxrpDwWRZYWYjA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"picocolors": "^1.0.0",
|
||||
@@ -10796,22 +11017,14 @@
|
||||
}
|
||||
},
|
||||
"@clack/prompts": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.7.0.tgz",
|
||||
"integrity": "sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==",
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.9.1.tgz",
|
||||
"integrity": "sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@clack/core": "^0.3.3",
|
||||
"is-unicode-supported": "*",
|
||||
"@clack/core": "0.4.1",
|
||||
"picocolors": "^1.0.0",
|
||||
"sisteransi": "^1.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-unicode-supported": {
|
||||
"version": "1.3.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@eslint-community/eslint-utils": {
|
||||
@@ -11483,9 +11696,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@jridgewell/trace-mapping": {
|
||||
@@ -11591,9 +11804,9 @@
|
||||
}
|
||||
},
|
||||
"@stencil/angular-output-target": {
|
||||
"version": "0.8.4",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-0.8.4.tgz",
|
||||
"integrity": "sha512-QvmHTueXXs5vB9W2L12uEzFmAuR8sqATJV2b+SCFmYsjJSaymiSqR3dKo2wnr0tZiTgU1t16BWaUKiSh3wPXpw==",
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-0.10.2.tgz",
|
||||
"integrity": "sha512-jPRa2NMAPtm/iMY+mUaWATbIhgY5zPJfUNQyF8nwC0rMrfXifPoRCf6BbH2S4Gy7SX0X4hlP+jAbVUjQNg/P+Q==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
@@ -11617,9 +11830,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"@stencil/vue-output-target": {
|
||||
"version": "0.8.9",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.8.9.tgz",
|
||||
"integrity": "sha512-1yuapCWYViLlxGlEaeta2wryq4M5zZxxBa+4rEBp54VwW2W/trlzPv0IJyw6I3Il51rHYm2WmWlBLOGmoMyW9Q==",
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.9.2.tgz",
|
||||
"integrity": "sha512-AeBmfo8bQhtob4VKpYTNiCoqh50MeXUwRgYLyO/JxRgAAK9GSfenNrUxXDrK0DK65SWsx/GCOsRwWbfOveorOQ==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
@@ -12063,6 +12276,149 @@
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-core": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
|
||||
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@babel/parser": "^7.25.3",
|
||||
"@vue/shared": "3.5.13",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@vue/compiler-dom": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
|
||||
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-sfc": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
|
||||
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@babel/parser": "^7.25.3",
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/compiler-ssr": "3.5.13",
|
||||
"@vue/shared": "3.5.13",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.11",
|
||||
"postcss": "^8.4.48",
|
||||
"source-map-js": "^1.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.4.49",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
|
||||
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@vue/compiler-ssr": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
|
||||
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"@vue/reactivity": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
|
||||
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"@vue/runtime-core": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
|
||||
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@vue/reactivity": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"@vue/runtime-dom": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
|
||||
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@vue/reactivity": "3.5.13",
|
||||
"@vue/runtime-core": "3.5.13",
|
||||
"@vue/shared": "3.5.13",
|
||||
"csstype": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"@vue/server-renderer": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
|
||||
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@vue/compiler-ssr": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"@vue/shared": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
|
||||
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"@zeit/schemas": {
|
||||
"version": "2.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.21.0.tgz",
|
||||
@@ -12537,9 +12893,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"chalk": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
|
||||
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
|
||||
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
|
||||
"dev": true
|
||||
},
|
||||
"chalk-template": {
|
||||
@@ -12953,6 +13309,13 @@
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"dev": true
|
||||
},
|
||||
"csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@@ -15757,6 +16120,16 @@
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"magic-string": {
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
|
||||
@@ -15991,6 +16364,13 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@@ -16229,9 +16609,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true
|
||||
},
|
||||
"picomatch": {
|
||||
@@ -16850,6 +17230,13 @@
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"source-map-support": {
|
||||
"version": "0.5.13",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
|
||||
@@ -17305,12 +17692,6 @@
|
||||
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
|
||||
"dev": true
|
||||
},
|
||||
"to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
|
||||
"dev": true
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -17570,6 +17951,20 @@
|
||||
"unist-util-stringify-position": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"vue": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
|
||||
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/compiler-sfc": "3.5.13",
|
||||
"@vue/runtime-dom": "3.5.13",
|
||||
"@vue/server-renderer": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"walker": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "8.3.1",
|
||||
"version": "8.4.3",
|
||||
"description": "Base components for Ionic",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -41,16 +41,16 @@
|
||||
"@capacitor/haptics": "^6.0.0",
|
||||
"@capacitor/keyboard": "^6.0.0",
|
||||
"@capacitor/status-bar": "^6.0.0",
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@clack/prompts": "^0.9.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.9.0",
|
||||
"@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%;
|
||||
}
|
||||
|
||||
92
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";
|
||||
@@ -304,6 +308,9 @@ export namespace Components {
|
||||
"trigger": string | undefined;
|
||||
}
|
||||
interface IonApp {
|
||||
/**
|
||||
* Used to set focus on an element that uses `ion-focusable`. Do not use this if focusing the element as a result of a keyboard event as the focus utility should handle this for us. This method should be used when we want to programmatically focus an element as a result of another user action. (Ex: We focus the first element inside of a popover when the user presents it, but the popover is not always presented as a result of keyboard action.)
|
||||
*/
|
||||
"setFocus": (elements: HTMLElement[]) => Promise<void>;
|
||||
}
|
||||
interface IonAvatar {
|
||||
@@ -636,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>`.
|
||||
*/
|
||||
@@ -1593,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`.
|
||||
*/
|
||||
@@ -1625,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.
|
||||
*/
|
||||
@@ -2276,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.
|
||||
*/
|
||||
@@ -2295,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.
|
||||
*/
|
||||
@@ -2676,10 +2685,6 @@ export namespace Components {
|
||||
* If `true`, the segment buttons will overflow and the user can swipe to see them. In addition, this will disable the gesture to drag the indicator between the buttons in order to swipe to see hidden buttons.
|
||||
*/
|
||||
"scrollable": boolean;
|
||||
/**
|
||||
* The `id` of the `segment-view` element to be associated with this segment.
|
||||
*/
|
||||
"segmentViewId"?: string;
|
||||
/**
|
||||
* If `true`, navigating to an `ion-segment-button` with the keyboard will focus and select the element. If `false`, keyboard navigation will only focus the `ion-segment-button` element.
|
||||
*/
|
||||
@@ -2702,7 +2707,6 @@ export namespace Components {
|
||||
* If `true`, the user cannot interact with the segment button.
|
||||
*/
|
||||
"disabled": boolean;
|
||||
"hasIndicator": boolean;
|
||||
/**
|
||||
* Set the layout of the text and icon in the segment.
|
||||
*/
|
||||
@@ -2722,10 +2726,6 @@ export namespace Components {
|
||||
"value": SegmentValue;
|
||||
}
|
||||
interface IonSegmentContent {
|
||||
/**
|
||||
* If `true`, the segment content will not be displayed.
|
||||
*/
|
||||
"disabled": boolean;
|
||||
}
|
||||
interface IonSegmentView {
|
||||
/**
|
||||
@@ -2733,7 +2733,6 @@ export namespace Components {
|
||||
*/
|
||||
"disabled": boolean;
|
||||
/**
|
||||
* This method is used to programmatically set the displayed segment content in the segment view. Calling this method will update the `value` of the corresponding segment button.
|
||||
* @param id : The id of the segment content to display.
|
||||
* @param smoothScroll : Whether to animate the scroll transition.
|
||||
*/
|
||||
@@ -2765,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;
|
||||
/**
|
||||
@@ -2826,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.
|
||||
@@ -3997,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 {
|
||||
@@ -4447,13 +4451,7 @@ declare global {
|
||||
new (): HTMLIonSegmentContentElement;
|
||||
};
|
||||
interface HTMLIonSegmentViewElementEventMap {
|
||||
"ionSegmentViewScroll": {
|
||||
scrollDirection: string;
|
||||
scrollDistance: number;
|
||||
scrollDistancePercentage: number;
|
||||
};
|
||||
"ionSegmentViewScrollEnd": { activeContentId: string };
|
||||
"ionSegmentViewScrollStart": void;
|
||||
"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;
|
||||
@@ -4491,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: {
|
||||
@@ -4781,6 +4785,7 @@ declare global {
|
||||
"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;
|
||||
@@ -6423,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.
|
||||
*/
|
||||
@@ -6435,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.
|
||||
*/
|
||||
@@ -7495,10 +7500,6 @@ declare namespace LocalJSX {
|
||||
* If `true`, the segment buttons will overflow and the user can swipe to see them. In addition, this will disable the gesture to drag the indicator between the buttons in order to swipe to see hidden buttons.
|
||||
*/
|
||||
"scrollable"?: boolean;
|
||||
/**
|
||||
* The `id` of the `segment-view` element to be associated with this segment.
|
||||
*/
|
||||
"segmentViewId"?: string;
|
||||
/**
|
||||
* If `true`, navigating to an `ion-segment-button` with the keyboard will focus and select the element. If `false`, keyboard navigation will only focus the `ion-segment-button` element.
|
||||
*/
|
||||
@@ -7521,7 +7522,6 @@ declare namespace LocalJSX {
|
||||
* If `true`, the user cannot interact with the segment button.
|
||||
*/
|
||||
"disabled"?: boolean;
|
||||
"hasIndicator"?: boolean;
|
||||
/**
|
||||
* Set the layout of the text and icon in the segment.
|
||||
*/
|
||||
@@ -7540,10 +7540,6 @@ declare namespace LocalJSX {
|
||||
"value"?: SegmentValue;
|
||||
}
|
||||
interface IonSegmentContent {
|
||||
/**
|
||||
* If `true`, the segment content will not be displayed.
|
||||
*/
|
||||
"disabled"?: boolean;
|
||||
}
|
||||
interface IonSegmentView {
|
||||
/**
|
||||
@@ -7553,16 +7549,7 @@ declare namespace LocalJSX {
|
||||
/**
|
||||
* Emitted when the segment view is scrolled.
|
||||
*/
|
||||
"onIonSegmentViewScroll"?: (event: IonSegmentViewCustomEvent<{
|
||||
scrollDirection: string;
|
||||
scrollDistance: number;
|
||||
scrollDistancePercentage: number;
|
||||
}>) => void;
|
||||
/**
|
||||
* Emitted when the segment view scroll has ended.
|
||||
*/
|
||||
"onIonSegmentViewScrollEnd"?: (event: IonSegmentViewCustomEvent<{ activeContentId: string }>) => void;
|
||||
"onIonSegmentViewScrollStart"?: (event: IonSegmentViewCustomEvent<void>) => void;
|
||||
"onIonSegmentViewScroll"?: (event: IonSegmentViewCustomEvent<SegmentViewScrollEvent>) => void;
|
||||
}
|
||||
interface IonSelect {
|
||||
/**
|
||||
@@ -7590,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;
|
||||
/**
|
||||
@@ -7670,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.
|
||||
@@ -8258,6 +8250,7 @@ declare namespace LocalJSX {
|
||||
"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;
|
||||
@@ -8359,6 +8352,7 @@ declare module "@stencil/core" {
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -730,10 +730,12 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
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
|
||||
@@ -757,7 +759,7 @@ 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-head">
|
||||
@@ -766,11 +768,18 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
{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 +788,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
{this.renderAlertButtons()}
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
<div tabindex="0" aria-hidden="true"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,27 @@ const testAria = async (
|
||||
|
||||
const alert = page.locator('ion-alert');
|
||||
|
||||
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.
|
||||
@@ -124,16 +145,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 +308,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>
|
||||
|
||||
@@ -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'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ export class App implements ComponentInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Used to set focus on an element that uses `ion-focusable`.
|
||||
* Do not use this if focusing the element as a result of a keyboard
|
||||
* event as the focus utility should handle this for us. This method
|
||||
|
||||
@@ -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 } from '@stencil/core';
|
||||
import { Component, Element, Host, Prop, Method, State, Watch, forceUpdate, h } from '@stencil/core';
|
||||
import type { ButtonInterface } from '@utils/element-interface';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import { addEventListener, removeEventListener, inheritAttributes } from '@utils/helpers';
|
||||
import { addEventListener, removeEventListener, inheritAttributes, getNextSiblingOfType } from '@utils/helpers';
|
||||
import { hostContext } from '@utils/theme';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
@@ -65,9 +65,41 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
@Prop() hasIndicator = true;
|
||||
private waitForSegmentContent(ionSegment: HTMLIonSegmentElement | null, contentId: string): Promise<HTMLElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutId: NodeJS.Timeout | undefined = undefined;
|
||||
let animationFrameId: number;
|
||||
|
||||
connectedCallback() {
|
||||
const check = () => {
|
||||
if (!ionSegment) {
|
||||
reject(new Error(`Segment not found when looking for Segment Content`));
|
||||
return;
|
||||
}
|
||||
|
||||
const segmentView = getNextSiblingOfType<HTMLIonSegmentViewElement>(ionSegment); // Skip the text nodes
|
||||
const segmentContent = segmentView?.querySelector(
|
||||
`ion-segment-content[id="${contentId}"]`
|
||||
) as HTMLIonSegmentContentElement | null;
|
||||
if (segmentContent && timeoutId) {
|
||||
clearTimeout(timeoutId); // Clear the timeout if the segmentContent is found
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
resolve(segmentContent);
|
||||
} else {
|
||||
animationFrameId = requestAnimationFrame(check); // Keep checking on the next animation frame
|
||||
}
|
||||
};
|
||||
|
||||
check();
|
||||
|
||||
// Set a timeout to reject the promise
|
||||
timeoutId = setTimeout(() => {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
reject(new Error(`Unable to find Segment Content with id="${contentId} within 1000 ms`));
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
const segmentEl = (this.segmentEl = this.el.closest('ion-segment'));
|
||||
if (segmentEl) {
|
||||
this.updateState();
|
||||
@@ -78,12 +110,13 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
// Return if there is no contentId defined
|
||||
if (!this.contentId) return;
|
||||
|
||||
// Attempt to find the Segment Content by its contentId
|
||||
const segmentContent = document.getElementById(this.contentId) as HTMLIonSegmentContentElement | null;
|
||||
|
||||
// If no associated Segment Content exists, log an error and return
|
||||
if (!segmentContent) {
|
||||
console.error(`Segment Button: Unable to find Segment Content with id="${this.contentId}".`);
|
||||
let segmentContent;
|
||||
try {
|
||||
// Attempt to find the Segment Content by its contentId
|
||||
segmentContent = await this.waitForSegmentContent(segmentEl, this.contentId);
|
||||
} catch (error) {
|
||||
// If no associated Segment Content exists, log an error and return
|
||||
console.error('Segment Button: ', (error as Error).message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,8 +126,11 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the disabled state of the Segment Content based on the button's disabled state
|
||||
segmentContent.disabled = this.disabled;
|
||||
// Prevent buttons from being disabled when associated with segment content
|
||||
if (this.disabled) {
|
||||
console.warn(`Segment Button: Segment buttons cannot be disabled when associated with an <ion-segment-content>.`);
|
||||
this.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@@ -189,11 +225,9 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
</span>
|
||||
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
|
||||
</button>
|
||||
{this.hasIndicator && (
|
||||
<div part="indicator" class="segment-button-indicator segment-button-indicator-animated">
|
||||
<div part="indicator-background" class="segment-button-indicator-background"></div>
|
||||
</div>
|
||||
)}
|
||||
<div part="indicator" class="segment-button-indicator segment-button-indicator-animated">
|
||||
<div part="indicator-background" class="segment-button-indicator-background"></div>
|
||||
</div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
@import "./segment-content";
|
||||
@import "../segment-button/segment-button.ios.vars";
|
||||
|
||||
// iOS Segment Content
|
||||
// --------------------------------------------------
|
||||
@@ -1,5 +0,0 @@
|
||||
@import "./segment-content";
|
||||
@import "../segment-button/segment-button.md.vars";
|
||||
|
||||
// Material Design Segment Content
|
||||
// --------------------------------------------------
|
||||
@@ -9,7 +9,3 @@
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host(.segment-content-disabled) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,15 @@
|
||||
import type { ComponentInterface } from '@stencil/core';
|
||||
import { Component, Host, Prop, h } from '@stencil/core';
|
||||
import { Component, Host, h } from '@stencil/core';
|
||||
|
||||
@Component({
|
||||
tag: 'ion-segment-content',
|
||||
styleUrls: {
|
||||
ios: 'segment-content.ios.scss',
|
||||
md: 'segment-content.md.scss',
|
||||
},
|
||||
styleUrl: 'segment-content.scss',
|
||||
shadow: true,
|
||||
})
|
||||
export class SegmentContent implements ComponentInterface {
|
||||
/**
|
||||
* If `true`, the segment content will not be displayed.
|
||||
*/
|
||||
@Prop() disabled = false;
|
||||
|
||||
render() {
|
||||
const { disabled } = this;
|
||||
|
||||
return (
|
||||
<Host
|
||||
class={{
|
||||
'segment-content-disabled': disabled,
|
||||
}}
|
||||
>
|
||||
<Host>
|
||||
<slot></slot>
|
||||
</Host>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface SegmentViewScrollEvent {
|
||||
scrollRatio: number;
|
||||
isManualScroll: boolean;
|
||||
}
|
||||
@@ -9,8 +9,6 @@
|
||||
overflow-x: scroll;
|
||||
scroll-snap-type: x mandatory;
|
||||
|
||||
scroll-behavior: smooth;
|
||||
|
||||
/* Hide scrollbar in Firefox */
|
||||
scrollbar-width: none;
|
||||
|
||||
@@ -27,3 +25,7 @@
|
||||
touch-action: none;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
:host(.segment-view-scroll-disabled) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Listen, Method, Prop, h } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Listen, Method, Prop, State, h } from '@stencil/core';
|
||||
|
||||
import type { SegmentViewScrollEvent } from './segment-view-interface';
|
||||
|
||||
@Component({
|
||||
tag: 'ion-segment-view',
|
||||
@@ -10,8 +12,6 @@ import { Component, Element, Event, Host, Listen, Method, Prop, h } from '@stenc
|
||||
shadow: true,
|
||||
})
|
||||
export class SegmentView implements ComponentInterface {
|
||||
private initialScrollLeft?: number;
|
||||
private previousScrollLeft = 0;
|
||||
private scrollEndTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
private isTouching = false;
|
||||
|
||||
@@ -23,69 +23,31 @@ export class SegmentView implements ComponentInterface {
|
||||
@Prop() disabled = false;
|
||||
|
||||
/**
|
||||
* Emitted when the segment view is scrolled.
|
||||
* @internal
|
||||
*
|
||||
* If `true`, the segment view is scrollable.
|
||||
* If `false`, pointer events will be disabled. This is to prevent issues with
|
||||
* quickly scrolling after interacting with a segment button.
|
||||
*/
|
||||
@Event() ionSegmentViewScroll!: EventEmitter<{
|
||||
scrollDirection: string;
|
||||
scrollDistance: number;
|
||||
scrollDistancePercentage: number;
|
||||
}>;
|
||||
@State() isManualScroll?: boolean;
|
||||
|
||||
/**
|
||||
* Emitted when the segment view scroll has ended.
|
||||
* Emitted when the segment view is scrolled.
|
||||
*/
|
||||
@Event() ionSegmentViewScrollEnd!: EventEmitter<{ activeContentId: string }>;
|
||||
|
||||
@Event() ionSegmentViewScrollStart!: EventEmitter<void>;
|
||||
|
||||
private activeContentId = '';
|
||||
@Event() ionSegmentViewScroll!: EventEmitter<SegmentViewScrollEvent>;
|
||||
|
||||
@Listen('scroll')
|
||||
handleScroll(ev: Event) {
|
||||
const { previousScrollLeft } = this;
|
||||
const { scrollLeft, offsetWidth } = ev.target as HTMLElement;
|
||||
const { scrollLeft, scrollWidth, clientWidth } = ev.target as HTMLElement;
|
||||
const scrollRatio = scrollLeft / (scrollWidth - clientWidth);
|
||||
|
||||
// Set initial scroll position if it's undefined
|
||||
// Must be a multiple of the offset width
|
||||
if (this.initialScrollLeft === undefined) {
|
||||
this.initialScrollLeft = Math.round(scrollLeft / offsetWidth) * offsetWidth;
|
||||
}
|
||||
|
||||
// Determine the scroll direction based on the previous scroll position
|
||||
const scrollDirection = scrollLeft > previousScrollLeft ? 'right' : 'left';
|
||||
this.previousScrollLeft = scrollLeft;
|
||||
|
||||
// Calculate the distance scrolled based on the initial scroll position
|
||||
// and then transform it to a percentage of the segment view width
|
||||
const scrollDistance = scrollLeft - this.initialScrollLeft;
|
||||
const scrollDistancePercentage = Math.abs(scrollDistance) / offsetWidth;
|
||||
|
||||
// Emit the scroll direction and distance
|
||||
this.ionSegmentViewScroll.emit({
|
||||
scrollDirection,
|
||||
scrollDistance,
|
||||
scrollDistancePercentage,
|
||||
scrollRatio,
|
||||
isManualScroll: this.isManualScroll ?? true,
|
||||
});
|
||||
|
||||
// Check if the scroll is at a snapping point and return if not
|
||||
const atSnappingPoint = scrollLeft % offsetWidth === 0;
|
||||
if (!atSnappingPoint) return;
|
||||
|
||||
// Find the current segment content based on the scroll position
|
||||
const currentIndex = Math.round(scrollLeft / offsetWidth);
|
||||
|
||||
// // Update active content ID and scroll to the segment content
|
||||
const activeContent = this.getSegmentContents().filter(
|
||||
(ref) => !ref.classList.contains('segment-content-disabled')
|
||||
)[currentIndex];
|
||||
this.activeContentId = activeContent.id;
|
||||
|
||||
// Only emit scroll end event if the active content is not disabled and
|
||||
// the user is not touching the segment view
|
||||
if (activeContent?.disabled === false && !this.isTouching) {
|
||||
this.ionSegmentViewScrollEnd.emit({ activeContentId: this.activeContentId });
|
||||
this.initialScrollLeft = undefined;
|
||||
}
|
||||
// Reset the timeout to check for scroll end
|
||||
this.resetScrollEndTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,8 +55,6 @@ export class SegmentView implements ComponentInterface {
|
||||
*/
|
||||
@Listen('touchstart')
|
||||
handleScrollStart() {
|
||||
this.ionSegmentViewScrollStart.emit();
|
||||
|
||||
if (this.scrollEndTimeout) {
|
||||
clearTimeout(this.scrollEndTimeout);
|
||||
this.scrollEndTimeout = null;
|
||||
@@ -112,9 +72,45 @@ export class SegmentView implements ComponentInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the scroll end detection timer. This is called on every scroll event.
|
||||
*/
|
||||
private resetScrollEndTimeout() {
|
||||
if (this.scrollEndTimeout) {
|
||||
clearTimeout(this.scrollEndTimeout);
|
||||
this.scrollEndTimeout = null;
|
||||
}
|
||||
this.scrollEndTimeout = setTimeout(
|
||||
() => {
|
||||
this.checkForScrollEnd();
|
||||
},
|
||||
// Setting this to a lower value may result in inconsistencies in behavior
|
||||
// across browsers (particularly Firefox).
|
||||
// Ideally, all of this logic is removed once the scroll end event is
|
||||
// supported on all browsers (https://caniuse.com/?search=scrollend)
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the scroll has ended and the user is not actively touching.
|
||||
* If the conditions are met (active content is enabled and no active touch),
|
||||
* reset the scroll position and emit the scroll end event.
|
||||
*/
|
||||
private checkForScrollEnd() {
|
||||
// Only emit scroll end event if the active content is not disabled and
|
||||
// the user is not touching the segment view
|
||||
if (!this.isTouching) {
|
||||
this.isManualScroll = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* This method is used to programmatically set the displayed segment content
|
||||
* in the segment view. Calling this method will update the `value` of the
|
||||
* corresponding segment button.
|
||||
*
|
||||
* @param id: The id of the segment content to display.
|
||||
* @param smoothScroll: Whether to animate the scroll transition.
|
||||
*/
|
||||
@@ -125,6 +121,9 @@ export class SegmentView implements ComponentInterface {
|
||||
|
||||
if (index === -1) return;
|
||||
|
||||
this.isManualScroll = false;
|
||||
this.resetScrollEndTimeout();
|
||||
|
||||
const contentWidth = this.el.offsetWidth;
|
||||
this.el.scrollTo({
|
||||
top: 0,
|
||||
@@ -138,12 +137,13 @@ export class SegmentView implements ComponentInterface {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { disabled } = this;
|
||||
const { disabled, isManualScroll } = this;
|
||||
|
||||
return (
|
||||
<Host
|
||||
class={{
|
||||
'segment-view-disabled': disabled,
|
||||
'segment-view-scroll-disabled': isManualScroll === false,
|
||||
}}
|
||||
>
|
||||
<slot></slot>
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
<style>
|
||||
ion-segment-view {
|
||||
height: 100px;
|
||||
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
ion-segment-content {
|
||||
@@ -24,15 +26,15 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ion-segment-content:nth-of-type(1) {
|
||||
ion-segment-content:nth-of-type(3n + 1) {
|
||||
background: lightpink;
|
||||
}
|
||||
|
||||
ion-segment-content:nth-of-type(2) {
|
||||
ion-segment-content:nth-of-type(3n + 2) {
|
||||
background: lightblue;
|
||||
}
|
||||
|
||||
ion-segment-content:nth-of-type(3) {
|
||||
ion-segment-content:nth-of-type(3n + 3) {
|
||||
background: lightgreen;
|
||||
}
|
||||
</style>
|
||||
@@ -60,24 +62,11 @@
|
||||
<ion-segment-content id="value">Value</ion-segment-content>
|
||||
</ion-segment-view>
|
||||
|
||||
<ion-segment value="all">
|
||||
<ion-segment-button content-id="all" value="all">
|
||||
<ion-label>All</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="favorites" value="favorites">
|
||||
<ion-label>Favorites</ion-label>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
<ion-segment-view>
|
||||
<ion-segment-content id="all">All</ion-segment-content>
|
||||
<ion-segment-content id="favorites">Favorites</ion-segment-content>
|
||||
</ion-segment-view>
|
||||
|
||||
<ion-segment value="free">
|
||||
<ion-segment-button content-id="paid" value="paid">
|
||||
<ion-label>Paid</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="free" value="free">
|
||||
<ion-segment-button style="min-width: 200px" content-id="free" value="free">
|
||||
<ion-label>Free</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="top" value="top">
|
||||
@@ -90,9 +79,52 @@
|
||||
<ion-segment-content id="top">Top</ion-segment-content>
|
||||
</ion-segment-view>
|
||||
|
||||
<ion-segment value="peach" scrollable>
|
||||
<ion-segment-button content-id="orange" value="orange">
|
||||
<ion-label>Orange</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="banana" value="banana">
|
||||
<ion-label>Banana</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="pear" value="pear">
|
||||
<ion-label>Pear</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="peach" value="peach">
|
||||
<ion-label>Peach</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="grape" value="grape">
|
||||
<ion-label>Grape</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="mango" value="mango">
|
||||
<ion-label>Mango</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="apple" value="apple">
|
||||
<ion-label>Apple</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="strawberry" value="strawberry">
|
||||
<ion-label>Strawberry</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="cherry" value="cherry">
|
||||
<ion-label>Cherry</ion-label>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
<ion-segment-view>
|
||||
<ion-segment-content id="orange">Orange</ion-segment-content>
|
||||
<ion-segment-content id="banana">Banana</ion-segment-content>
|
||||
<ion-segment-content id="pear">Pear</ion-segment-content>
|
||||
<ion-segment-content id="peach">Peach</ion-segment-content>
|
||||
<ion-segment-content id="grape">Grape</ion-segment-content>
|
||||
<ion-segment-content id="mango">Mango</ion-segment-content>
|
||||
<ion-segment-content id="apple">Apple</ion-segment-content>
|
||||
<ion-segment-content id="strawberry">Strawberry</ion-segment-content>
|
||||
<ion-segment-content id="cherry">Cherry</ion-segment-content>
|
||||
</ion-segment-view>
|
||||
|
||||
<button class="expand" onClick="changeSegmentContent()">Change Segment Content</button>
|
||||
|
||||
<button class="expand" onClick="clearSegmentValue()">Clear Segment Value</button>
|
||||
|
||||
<button class="expand" onClick="addSegmentButtonAndContent()">Add New Segment Button & Content</button>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
@@ -114,7 +146,7 @@
|
||||
currentValue = 'value';
|
||||
}
|
||||
|
||||
segmentView.setContent(currentValue);
|
||||
segment.value = currentValue;
|
||||
}
|
||||
|
||||
async function clearSegmentValue() {
|
||||
@@ -128,6 +160,34 @@
|
||||
segment.value = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
async function addSegmentButtonAndContent() {
|
||||
const segment = document.querySelector('ion-segment');
|
||||
const segmentView = document.querySelector('ion-segment-view');
|
||||
|
||||
const newButton = document.createElement('ion-segment-button');
|
||||
const newId = `new-${Date.now()}`;
|
||||
newButton.setAttribute('content-id', newId);
|
||||
newButton.setAttribute('value', newId);
|
||||
newButton.innerHTML = '<ion-label>New Button</ion-label>';
|
||||
|
||||
segment.appendChild(newButton);
|
||||
|
||||
setTimeout(() => {
|
||||
// Timeout to test waitForSegmentContent() in segment-button
|
||||
const newContent = document.createElement('ion-segment-content');
|
||||
newContent.setAttribute('id', newId);
|
||||
newContent.innerHTML = 'New Content';
|
||||
|
||||
segmentView.appendChild(newContent);
|
||||
|
||||
// Necessary timeout to ensure the value is set after the content is added.
|
||||
// Otherwise, the transition is unsuccessful and the content is not shown.
|
||||
setTimeout(() => {
|
||||
segment.setAttribute('value', newId);
|
||||
}, 200);
|
||||
}, 200);
|
||||
}
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
|
||||
@@ -83,10 +83,91 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
config
|
||||
);
|
||||
|
||||
await page.click('ion-segment-button[value="top"]');
|
||||
await page.locator('ion-segment-button[value="top"]').click();
|
||||
|
||||
const segmentContent = page.locator('ion-segment-content[id="top"]');
|
||||
await expect(segmentContent).toBeInViewport();
|
||||
});
|
||||
});
|
||||
|
||||
test('should set correct segment button as checked when changing the value by scrolling the segment content', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-segment value="free">
|
||||
<ion-segment-button content-id="paid" value="paid">
|
||||
<ion-label>Paid</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="free" value="free">
|
||||
<ion-label>Free</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="top" value="top">
|
||||
<ion-label>Top</ion-label>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
<ion-segment-view>
|
||||
<ion-segment-content id="paid">Paid</ion-segment-content>
|
||||
<ion-segment-content id="free">Free</ion-segment-content>
|
||||
<ion-segment-content id="top">Top</ion-segment-content>
|
||||
</ion-segment-view>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await page
|
||||
.locator('ion-segment-view')
|
||||
.evaluate(
|
||||
(segmentView: HTMLIonSegmentViewElement) => !segmentView.classList.contains('segment-view-scroll-disabled')
|
||||
);
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await page.locator('ion-segment-content[id="top"]').scrollIntoViewIfNeeded();
|
||||
|
||||
const segmentButton = page.locator('ion-segment-button[value="top"]');
|
||||
await expect(segmentButton).toHaveClass(/segment-button-checked/);
|
||||
});
|
||||
|
||||
test('should set correct segment button as checked and show correct content when programmatically setting the segment vale', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-segment value="free">
|
||||
<ion-segment-button content-id="paid" value="paid">
|
||||
<ion-label>Paid</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="free" value="free">
|
||||
<ion-label>Free</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="top" value="top">
|
||||
<ion-label>Top</ion-label>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
<ion-segment-view>
|
||||
<ion-segment-content id="paid">Paid</ion-segment-content>
|
||||
<ion-segment-content id="free">Free</ion-segment-content>
|
||||
<ion-segment-content id="top">Top</ion-segment-content>
|
||||
</ion-segment-view>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await page
|
||||
.locator('ion-segment-view')
|
||||
.evaluate(
|
||||
(segmentView: HTMLIonSegmentViewElement) => !segmentView.classList.contains('segment-view-scroll-disabled')
|
||||
);
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await page.locator('ion-segment').evaluate((segment: HTMLIonSegmentElement) => (segment.value = 'top'));
|
||||
|
||||
const segmentButton = page.locator('ion-segment-button[value="top"]');
|
||||
await expect(segmentButton).toHaveClass(/segment-button-checked/);
|
||||
|
||||
const segmentContent = page.locator('ion-segment-content[id="top"]');
|
||||
await expect(segmentContent).toBeInViewport();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,11 +64,11 @@
|
||||
<ion-segment-content id="favorites">Favorites</ion-segment-content>
|
||||
</ion-segment-view>
|
||||
|
||||
<ion-segment value="paid">
|
||||
<ion-segment disabled value="paid">
|
||||
<ion-segment-button content-id="paid" value="paid">
|
||||
<ion-label>Paid</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button disabled content-id="free" value="free">
|
||||
<ion-segment-button content-id="free" value="free">
|
||||
<ion-label>Free</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="top" value="top">
|
||||
@@ -81,28 +81,7 @@
|
||||
<ion-segment-content id="top">Top</ion-segment-content>
|
||||
</ion-segment-view>
|
||||
|
||||
<ion-segment value="a">
|
||||
<ion-segment-button content-id="a" value="a">
|
||||
<ion-label>a</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button disabled content-id="b" value="b">
|
||||
<ion-label>b</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button disabled content-id="c" value="c">
|
||||
<ion-label>c</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="d" value="d">
|
||||
<ion-label>d</ion-label>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
<ion-segment-view>
|
||||
<ion-segment-content id="a">a</ion-segment-content>
|
||||
<ion-segment-content id="b">b</ion-segment-content>
|
||||
<ion-segment-content id="c">c</ion-segment-content>
|
||||
<ion-segment-content id="d">d</ion-segment-content>
|
||||
</ion-segment-view>
|
||||
|
||||
<ion-segment disabled value="reading-list">
|
||||
<ion-segment value="reading-list">
|
||||
<ion-segment-button content-id="bookmarks" value="bookmarks">
|
||||
<ion-label>Bookmarks</ion-label>
|
||||
</ion-segment-button>
|
||||
|
||||
@@ -7,28 +7,9 @@ import { configs, test } from '@utils/test/playwright';
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('segment-view: disabled'), () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
/* Styles are here to show the disabled state */
|
||||
ion-segment-view {
|
||||
height: 100px;
|
||||
background: #3880ff;
|
||||
}
|
||||
</style>
|
||||
await page.goto('/src/components/segment-view/test/disabled', config);
|
||||
|
||||
<ion-segment-view disabled>
|
||||
<ion-segment-content id="paid">Paid</ion-segment-content>
|
||||
<ion-segment-content id="free">Free</ion-segment-content>
|
||||
<ion-segment-content id="top">Top</ion-segment-content>
|
||||
</ion-segment-view>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const segment = page.locator('ion-segment-view');
|
||||
|
||||
await expect(segment).toHaveScreenshot(screenshot(`segment-view-disabled`));
|
||||
await expect(page).toHaveScreenshot(screenshot(`segment-view-disabled`));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -38,14 +19,14 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
*/
|
||||
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('segment-view: disabled'), () => {
|
||||
test('should show the second content when first segment content is disabled', async ({ page }) => {
|
||||
test('should keep button enabled even when disabled prop is set', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-segment disabled>
|
||||
<ion-segment>
|
||||
<ion-segment-button content-id="paid" value="paid">
|
||||
<ion-label>Paid</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="free" value="free">
|
||||
<ion-segment-button disabled content-id="free" value="free">
|
||||
<ion-label>Free</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="top" value="top">
|
||||
@@ -61,41 +42,8 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
config
|
||||
);
|
||||
|
||||
const segmentContent = page.locator('ion-segment-content[id="free"]');
|
||||
await expect(segmentContent).toBeInViewport();
|
||||
});
|
||||
|
||||
test('should scroll to the third content and update the segment value when the second segment content is disabled', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-segment>
|
||||
<ion-segment-button content-id="paid" value="paid">
|
||||
<ion-label>Paid</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button disabled content-id="free" value="free">
|
||||
<ion-label>Free</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="top" value="top">
|
||||
<ion-label>Top</ion-label>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
<ion-segment-view>
|
||||
<ion-segment-content id="paid">Paid</ion-segment-content>
|
||||
<ion-segment-content disabled id="free">Free</ion-segment-content>
|
||||
<ion-segment-content id="top">Top</ion-segment-content>
|
||||
</ion-segment-view>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const segmentContent = page.locator('ion-segment-content[id="top"]');
|
||||
await segmentContent.scrollIntoViewIfNeeded();
|
||||
await expect(segmentContent).toBeInViewport();
|
||||
|
||||
const segment = page.locator('ion-segment');
|
||||
expect(await segment.evaluate((el: HTMLIonSegmentElement) => el.value)).toBe('top');
|
||||
const segmentButton = page.locator('ion-segment-button[value="free"]');
|
||||
await expect(segmentButton).not.toHaveClass(/segment-button-disabled/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 16 KiB |
@@ -6,14 +6,15 @@
|
||||
|
||||
:host {
|
||||
--background: #{$segment-ios-background-color};
|
||||
--indicator-height: 100%;
|
||||
--indicator-box-shadow: 0 0 5px rgba(0, 0, 0, 0.16) @include border-radius($segment-ios-border-radius);
|
||||
|
||||
@include border-radius($segment-ios-border-radius);
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
|
||||
// Segment: Color
|
||||
// --------------------------------------------------
|
||||
|
||||
@@ -21,6 +22,7 @@
|
||||
background: #{current-color(base, 0.065)};
|
||||
}
|
||||
|
||||
|
||||
// Segment: Default Toolbar
|
||||
// --------------------------------------------------
|
||||
|
||||
@@ -35,6 +37,7 @@
|
||||
background: var(--ion-toolbar-segment-background, var(--background));
|
||||
}
|
||||
|
||||
|
||||
// Segment: Color Toolbar
|
||||
// --------------------------------------------------
|
||||
|
||||
@@ -42,14 +45,3 @@
|
||||
:host(.in-toolbar-color:not(.ion-color)) {
|
||||
background: #{current-color(contrast, 0.11)};
|
||||
}
|
||||
|
||||
.segment-indicator {
|
||||
@include position(0, 0, 0, 0);
|
||||
|
||||
padding: 0 2px;
|
||||
|
||||
.segment-indicator-background {
|
||||
transform: scale(0.95);
|
||||
border-radius: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
:host {
|
||||
--background: transparent;
|
||||
--indicator-height: 2px;
|
||||
|
||||
grid-auto-columns: minmax(auto, $segment-button-md-max-width);
|
||||
}
|
||||
@@ -24,13 +23,10 @@
|
||||
min-height: var(--min-height);
|
||||
}
|
||||
|
||||
|
||||
// Segment: Scrollable
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.segment-scrollable) ::slotted(ion-segment-button) {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.segment-indicator {
|
||||
@include position(null, 0, 0, 0);
|
||||
}
|
||||
|
||||
@@ -31,24 +31,9 @@
|
||||
contain: paint;
|
||||
|
||||
user-select: none;
|
||||
|
||||
.segment-indicator {
|
||||
@include transform-origin(left);
|
||||
|
||||
z-index: -1;
|
||||
|
||||
position: absolute;
|
||||
|
||||
.segment-indicator-background {
|
||||
height: var(--indicator-height);
|
||||
box-shadow: var(--indicator-box-shadow);
|
||||
background-color: var(--indicator-color);
|
||||
|
||||
transition: background-color 0.3s linear;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Segment: Scrollable
|
||||
// --------------------------------------------------
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createColorClasses, hostContext } from '@utils/theme';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import type { Color, StyleEventDetail } from '../../interface';
|
||||
import type { SegmentViewScrollEvent } from '../segment-view/segment-view-interface';
|
||||
|
||||
import type { SegmentChangeEventDetail, SegmentValue } from './segment-interface';
|
||||
|
||||
@@ -28,20 +29,19 @@ export class Segment implements ComponentInterface {
|
||||
private valueBeforeGesture?: SegmentValue;
|
||||
|
||||
private segmentViewEl?: HTMLIonSegmentViewElement | null = null;
|
||||
private lastNextIndex?: number;
|
||||
|
||||
private nextButtonIndex?: number;
|
||||
|
||||
private io?: IntersectionObserver;
|
||||
/**
|
||||
* Whether to update the segment view, if exists, when the value changes.
|
||||
* This behavior is enabled by default, but is set false when scrolling content views
|
||||
* since we don't want to "double scroll" the segment view.
|
||||
*/
|
||||
private triggerScrollOnValueChange?: boolean;
|
||||
|
||||
@Element() el!: HTMLIonSegmentElement;
|
||||
|
||||
@State() activated = false;
|
||||
|
||||
/**
|
||||
* The `id` of the `segment-view` element to be associated with this segment.
|
||||
*/
|
||||
@Prop() segmentViewId?: string;
|
||||
|
||||
/**
|
||||
* The color to use from your application's color palette.
|
||||
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
|
||||
@@ -90,6 +90,12 @@ export class Segment implements ComponentInterface {
|
||||
|
||||
@Watch('value')
|
||||
protected valueChanged(value: SegmentValue | undefined, oldValue?: SegmentValue | undefined) {
|
||||
// Force a value to exist if we're using a segment view
|
||||
if (this.segmentViewEl && value === undefined) {
|
||||
this.value = this.getButtons()[0].value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldValue !== undefined && value !== undefined) {
|
||||
const buttons = this.getButtons();
|
||||
const previous = buttons.find((button) => button.value === oldValue);
|
||||
@@ -98,10 +104,12 @@ export class Segment implements ComponentInterface {
|
||||
if (previous && current) {
|
||||
if (!this.segmentViewEl) {
|
||||
this.checkButton(previous, current);
|
||||
} else {
|
||||
this.setCheckedClasses();
|
||||
} else if (this.triggerScrollOnValueChange !== false) {
|
||||
this.updateSegmentView();
|
||||
}
|
||||
}
|
||||
} else if (value !== undefined && oldValue === undefined && this.segmentViewEl) {
|
||||
this.updateSegmentView();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,10 +118,12 @@ export class Segment implements ComponentInterface {
|
||||
*/
|
||||
this.ionSelect.emit({ value });
|
||||
|
||||
// The scroll listener should handle scrolling the active button into view as needed when there is a segment view
|
||||
// The scroll listener should handle scrolling the active button into view as needed
|
||||
if (!this.segmentViewEl) {
|
||||
this.scrollActiveButtonIntoView();
|
||||
}
|
||||
|
||||
this.triggerScrollOnValueChange = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,9 +157,13 @@ export class Segment implements ComponentInterface {
|
||||
disabledChanged() {
|
||||
this.gestureChanged();
|
||||
|
||||
const buttons = this.getButtons();
|
||||
for (const button of buttons) {
|
||||
button.disabled = this.disabled;
|
||||
if (!this.segmentViewEl) {
|
||||
const buttons = this.getButtons();
|
||||
for (const button of buttons) {
|
||||
button.disabled = this.disabled;
|
||||
}
|
||||
} else {
|
||||
this.segmentViewEl.disabled = this.disabled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,22 +177,10 @@ export class Segment implements ComponentInterface {
|
||||
this.emitStyle();
|
||||
|
||||
this.segmentViewEl = this.getSegmentView();
|
||||
|
||||
if (this.segmentViewEl) {
|
||||
// Disable each button indicator when using a segment view
|
||||
// Instead, a single indicator instance will be used
|
||||
this.getButtons().forEach((ref) => (ref.hasIndicator = false));
|
||||
|
||||
this.addIntersectionObserver();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.segmentViewEl = null;
|
||||
if (this.io) {
|
||||
this.io.disconnect();
|
||||
this.io = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
@@ -186,6 +188,8 @@ export class Segment implements ComponentInterface {
|
||||
}
|
||||
|
||||
async componentDidLoad() {
|
||||
this.segmentViewEl = this.getSegmentView();
|
||||
|
||||
this.setCheckedClasses();
|
||||
|
||||
/**
|
||||
@@ -217,6 +221,10 @@ export class Segment implements ComponentInterface {
|
||||
if (this.disabled) {
|
||||
this.disabledChanged();
|
||||
}
|
||||
|
||||
// Update segment view based on the initial value,
|
||||
// but do not animate the scroll
|
||||
this.updateSegmentView(false);
|
||||
}
|
||||
|
||||
onStart(detail: GestureDetail) {
|
||||
@@ -238,69 +246,13 @@ export class Segment implements ComponentInterface {
|
||||
const value = this.value;
|
||||
if (value !== undefined) {
|
||||
if (this.valueBeforeGesture !== value) {
|
||||
if (this.segmentViewEl) {
|
||||
this.updateSegmentView(value);
|
||||
} else {
|
||||
this.emitValueChange();
|
||||
}
|
||||
this.emitValueChange();
|
||||
this.updateSegmentView();
|
||||
}
|
||||
}
|
||||
this.valueBeforeGesture = undefined;
|
||||
}
|
||||
|
||||
private distanceToButton(index: number) {
|
||||
const buttons = this.getButtons();
|
||||
const button = buttons[index];
|
||||
|
||||
return button.offsetLeft;
|
||||
}
|
||||
|
||||
private addIntersectionObserver() {
|
||||
if (
|
||||
typeof (window as any) !== 'undefined' &&
|
||||
'IntersectionObserver' in window &&
|
||||
'IntersectionObserverEntry' in window &&
|
||||
'isIntersecting' in window.IntersectionObserverEntry.prototype
|
||||
) {
|
||||
this.io = new IntersectionObserver((data) => {
|
||||
/**
|
||||
* On slower devices, it is possible for an intersection observer entry to contain multiple
|
||||
* objects in the array. This happens when quickly scrolling an image into view and then out of
|
||||
* view. In this case, the last object represents the current state of the component.
|
||||
*/
|
||||
if (data[data.length - 1].isIntersecting) {
|
||||
this.updateSegmentView(this.value!, false);
|
||||
this.initIndicator();
|
||||
}
|
||||
});
|
||||
|
||||
this.io.observe(this.el);
|
||||
}
|
||||
}
|
||||
|
||||
private initIndicator() {
|
||||
writeTask(() => {
|
||||
if (this.segmentViewEl) {
|
||||
const buttons = this.getButtons();
|
||||
const activeButtonIndex = buttons.findIndex((ref) => ref.value === this.value);
|
||||
if (activeButtonIndex >= 0) {
|
||||
const activeButtonPosition = buttons[activeButtonIndex].getBoundingClientRect();
|
||||
const activeButtonStyles = getComputedStyle(buttons[activeButtonIndex]);
|
||||
const indicator = this.el.shadowRoot!.querySelector('.segment-indicator') as HTMLDivElement | null;
|
||||
if (indicator) {
|
||||
const startingX = this.distanceToButton(activeButtonIndex);
|
||||
|
||||
indicator.style.width = `${activeButtonPosition.width}px`;
|
||||
indicator.style.left = `${startingX}px`;
|
||||
|
||||
// Setting a CSS variable works around issue where background element might not be rendered yet
|
||||
this.el.style.setProperty('--indicator-color', activeButtonStyles.getPropertyValue('--indicator-color'));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an `ionChange` event.
|
||||
*
|
||||
@@ -393,6 +345,8 @@ export class Segment implements ComponentInterface {
|
||||
|
||||
// Remove the transform to slide the indicator back to the button clicked
|
||||
currentIndicator.style.setProperty('transform', '');
|
||||
|
||||
this.scrollActiveButtonIntoView(true);
|
||||
});
|
||||
|
||||
this.value = current.value;
|
||||
@@ -413,27 +367,23 @@ export class Segment implements ComponentInterface {
|
||||
}
|
||||
|
||||
private getSegmentView() {
|
||||
if (!this.segmentViewId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const segmentViewEl = document.getElementById(this.segmentViewId);
|
||||
|
||||
if (!segmentViewEl) {
|
||||
console.warn(`Segment: Unable to find 'ion-segment-view' with id="${this.segmentViewId}"`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (segmentViewEl.tagName !== 'ION-SEGMENT-VIEW') {
|
||||
console.warn(`Segment: Element with id="${this.segmentViewId}" is not an <ion-segment-view> element.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return segmentViewEl as HTMLIonSegmentViewElement;
|
||||
const buttons = this.getButtons();
|
||||
// Get the first button with a contentId
|
||||
const firstContentId = buttons.find((button: HTMLIonSegmentButtonElement) => button.contentId);
|
||||
// Get the segment content with an id matching the button's contentId
|
||||
const segmentContent = document.querySelector(`ion-segment-content[id="${firstContentId?.contentId}"]`);
|
||||
// Return the segment view for that matching segment content
|
||||
return segmentContent?.closest('ion-segment-view');
|
||||
}
|
||||
|
||||
@Listen('ionSegmentViewScroll', { target: 'body' })
|
||||
handleSegmentViewScroll(ev: CustomEvent) {
|
||||
handleSegmentViewScroll(ev: CustomEvent<SegmentViewScrollEvent>) {
|
||||
const { scrollRatio, isManualScroll } = ev.detail;
|
||||
|
||||
if (!isManualScroll) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dispatchedFrom = ev.target as HTMLElement;
|
||||
const segmentViewEl = this.segmentViewEl as EventTarget;
|
||||
const segmentEl = this.el;
|
||||
@@ -441,151 +391,25 @@ export class Segment implements ComponentInterface {
|
||||
// Only update the indicator if the event was dispatched from the correct segment view
|
||||
if (ev.composedPath().includes(segmentViewEl) || dispatchedFrom?.contains(segmentEl)) {
|
||||
const buttons = this.getButtons();
|
||||
const currentIndex = buttons.findIndex((button) => button.value === this.value);
|
||||
const currentButton = buttons[currentIndex];
|
||||
const indicator = this.el.shadowRoot!.querySelector('.segment-indicator') as HTMLDivElement | null;
|
||||
|
||||
// If no buttons are found or there is no value set then do nothing
|
||||
if (!buttons.length || this.value === undefined || !indicator) return;
|
||||
if (!buttons.length) return;
|
||||
|
||||
const { scrollDistancePercentage, scrollDistance } = ev.detail;
|
||||
const index = buttons.findIndex((button) => button.value === this.value);
|
||||
const current = buttons[index];
|
||||
|
||||
const findIndexFrom = (
|
||||
array: HTMLIonSegmentButtonElement[],
|
||||
predicate: (button: HTMLIonSegmentButtonElement) => boolean,
|
||||
startIndex: number
|
||||
) => {
|
||||
for (let i = startIndex; i < array.length; i++) {
|
||||
if (predicate(array[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
const nextIndex = Math.round(scrollRatio * (buttons.length - 1));
|
||||
|
||||
const findIndexFromReverse = (
|
||||
array: HTMLIonSegmentButtonElement[],
|
||||
predicate: (button: HTMLIonSegmentButtonElement) => boolean,
|
||||
startIndex: number
|
||||
) => {
|
||||
for (let i = startIndex; i >= 0; i--) {
|
||||
if (predicate(array[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
if (this.lastNextIndex === undefined || this.lastNextIndex !== nextIndex) {
|
||||
this.lastNextIndex = nextIndex;
|
||||
this.triggerScrollOnValueChange = false;
|
||||
|
||||
// Find the next valid button (i.e. we need to ignore any disabled buttons)
|
||||
const indexOffset = Math.ceil(scrollDistancePercentage);
|
||||
const nextIndex =
|
||||
this.nextButtonIndex ??
|
||||
(scrollDistance > 0
|
||||
? findIndexFrom(buttons, (ref) => !ref.disabled, currentIndex + indexOffset)
|
||||
: findIndexFromReverse(buttons, (ref) => !ref.disabled, currentIndex - indexOffset));
|
||||
|
||||
if (nextIndex >= 0 && nextIndex < buttons.length) {
|
||||
// Figure out the number of disabled buttons between the current and next button
|
||||
const disabledButtons = (
|
||||
nextIndex > currentIndex ? buttons.slice(currentIndex, nextIndex) : buttons.slice(nextIndex, currentIndex)
|
||||
).filter((button) => button.disabled).length;
|
||||
|
||||
// Adjust the scroll distance percentage based on the number of "views" scrolled
|
||||
// We need to do this because all subsequent calculations are based on the assumption that
|
||||
// only one view can be scrolled at a time, but this is not the case when clicking a segment button
|
||||
const adjustedScrollDistancePercentage =
|
||||
scrollDistancePercentage / (Math.abs(nextIndex - currentIndex) - disabledButtons);
|
||||
|
||||
const nextButton = buttons[nextIndex];
|
||||
const nextButtonWidth = nextButton.getBoundingClientRect().width;
|
||||
|
||||
const currentButtonWidth = currentButton.getBoundingClientRect().width;
|
||||
|
||||
// Scale the width based on the width of the next button
|
||||
const diff = nextButtonWidth - currentButtonWidth;
|
||||
const width = currentButtonWidth + diff * adjustedScrollDistancePercentage;
|
||||
const indicatorStyles = getComputedStyle(indicator);
|
||||
const indicatorPadding =
|
||||
parseFloat(indicatorStyles.paddingLeft.replace('px', '')) +
|
||||
parseFloat(indicatorStyles.paddingRight.replace('px', ''));
|
||||
indicator.style.width = `${width - indicatorPadding}px`;
|
||||
|
||||
// Translate the indicator based on the scroll distance
|
||||
const distanceToNextButton = this.distanceToButton(nextIndex);
|
||||
const distanceToCurrentButton = this.distanceToButton(currentIndex);
|
||||
indicator.style.left =
|
||||
scrollDistance > 0
|
||||
? `${
|
||||
distanceToCurrentButton +
|
||||
(distanceToNextButton - distanceToCurrentButton) * adjustedScrollDistancePercentage
|
||||
}px`
|
||||
: `${
|
||||
distanceToCurrentButton -
|
||||
(distanceToCurrentButton - distanceToNextButton) * adjustedScrollDistancePercentage
|
||||
}px`;
|
||||
|
||||
const standardize_color = (str: string) => {
|
||||
const ctx = document.createElement('canvas').getContext('2d');
|
||||
ctx!.fillStyle = str;
|
||||
return ctx!.fillStyle;
|
||||
};
|
||||
// Helper function to convert hex to RGB
|
||||
const hexToRgb = (hex: string) => {
|
||||
const bigint = parseInt(hex.slice(1), 16);
|
||||
const r = (bigint >> 16) & 255;
|
||||
const g = (bigint >> 8) & 255;
|
||||
const b = bigint & 255;
|
||||
|
||||
return { r, g, b };
|
||||
};
|
||||
// Helper function to convert RGB to hex
|
||||
const rgbToHex = (r: number, g: number, b: number) => {
|
||||
const componentToHex = (c: number) => {
|
||||
const hex = c.toString(16);
|
||||
return hex.length === 1 ? '0' + hex : hex;
|
||||
};
|
||||
|
||||
return `#${componentToHex(r)}${componentToHex(g)}${componentToHex(b)}`;
|
||||
};
|
||||
// Function to calculate the color in between based on percentage
|
||||
const interpolateColor = (percentage = adjustedScrollDistancePercentage) => {
|
||||
const currentColor = standardize_color(getComputedStyle(currentButton).getPropertyValue('--indicator-color'));
|
||||
const nextColor = standardize_color(getComputedStyle(nextButton).getPropertyValue('--indicator-color'));
|
||||
|
||||
const rgb1 = hexToRgb(currentColor);
|
||||
const rgb2 = hexToRgb(nextColor);
|
||||
|
||||
const r = Math.round(rgb1.r + (rgb2.r - rgb1.r) * percentage);
|
||||
const g = Math.round(rgb1.g + (rgb2.g - rgb1.g) * percentage);
|
||||
const b = Math.round(rgb1.b + (rgb2.b - rgb1.b) * percentage);
|
||||
|
||||
return rgbToHex(r, g, b);
|
||||
};
|
||||
indicator.querySelector('div')!.style.backgroundColor = interpolateColor();
|
||||
|
||||
if (this.scrollable) {
|
||||
// Scroll the segment container so the indicator is always in view
|
||||
indicator.scrollIntoView({
|
||||
behavior: 'instant',
|
||||
});
|
||||
}
|
||||
this.checkButton(current, buttons[nextIndex]);
|
||||
this.emitValueChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Listen('ionSegmentViewScrollEnd', { target: 'body' })
|
||||
onScrollEnd(ev: CustomEvent<{ activeContentId: string }>) {
|
||||
const dispatchedFrom = ev.target as HTMLElement;
|
||||
const segmentViewEl = this.segmentViewEl as EventTarget;
|
||||
const segmentEl = this.el;
|
||||
|
||||
if (ev.composedPath().includes(segmentViewEl) || dispatchedFrom?.contains(segmentEl)) {
|
||||
this.value = ev.detail.activeContentId;
|
||||
this.nextButtonIndex = undefined;
|
||||
this.emitValueChange();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the related segment view and sets its current content
|
||||
* based on the selected segment button. This method
|
||||
@@ -593,9 +417,9 @@ export class Segment implements ComponentInterface {
|
||||
* after the gesture is completed (if dragging between segments)
|
||||
* and when a segment button is clicked directly.
|
||||
*/
|
||||
private updateSegmentView(value: SegmentValue, smoothScroll = true) {
|
||||
private updateSegmentView(smoothScroll = true) {
|
||||
const buttons = this.getButtons();
|
||||
const button = buttons.find((btn) => btn.value === value);
|
||||
const button = buttons.find((btn) => btn.value === this.value);
|
||||
|
||||
// If the button does not have a contentId then there is
|
||||
// no associated segment view to update
|
||||
@@ -603,8 +427,10 @@ export class Segment implements ComponentInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.segmentViewEl) {
|
||||
this.segmentViewEl.setContent(button.contentId, smoothScroll);
|
||||
const segmentView = this.segmentViewEl;
|
||||
|
||||
if (segmentView) {
|
||||
segmentView.setContent(button.contentId, smoothScroll);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,21 +464,35 @@ export class Segment implements ComponentInterface {
|
||||
const centeredX = activeButtonLeft - scrollContainerBox.width / 2 + activeButtonBox.width / 2;
|
||||
|
||||
/**
|
||||
* We intentionally use scrollBy here instead of scrollIntoView
|
||||
* newScrollPosition is the absolute scroll position that the
|
||||
* container needs to move to in order to center the active button.
|
||||
* It is calculated by adding the current scroll position
|
||||
* (scrollLeft) to the offset needed to center the button
|
||||
* (centeredX).
|
||||
*/
|
||||
const newScrollPosition = el.scrollLeft + centeredX;
|
||||
|
||||
/**
|
||||
* We intentionally use scrollTo here instead of scrollIntoView
|
||||
* to avoid a WebKit bug where accelerated animations break
|
||||
* when using scrollIntoView. Using scrollIntoView will cause the
|
||||
* segment container to jump during the transition and then snap into place.
|
||||
* This is because scrollIntoView can potentially cause parent element
|
||||
* containers to also scroll. scrollBy does not have this same behavior, so
|
||||
* containers to also scroll. scrollTo does not have this same behavior, so
|
||||
* we use this API instead.
|
||||
*
|
||||
* scrollTo is used instead of scrollBy because there is a
|
||||
* Webkit bug that causes scrollBy to not work smoothly when
|
||||
* the active button is near the edge of the scroll container.
|
||||
* This leads to the buttons to jump around during the transition.
|
||||
*
|
||||
* Note that if there is not enough scrolling space to center the element
|
||||
* within the scroll container, the browser will attempt
|
||||
* to center by as much as it can.
|
||||
*/
|
||||
el.scrollBy({
|
||||
el.scrollTo({
|
||||
top: 0,
|
||||
left: centeredX,
|
||||
left: newScrollPosition,
|
||||
behavior: smoothScroll ? 'smooth' : 'instant',
|
||||
});
|
||||
}
|
||||
@@ -768,17 +608,19 @@ export class Segment implements ComponentInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
this.value = current.value;
|
||||
|
||||
if (current !== previous) {
|
||||
if (this.segmentViewEl) {
|
||||
this.nextButtonIndex = this.getButtons().findIndex((button) => button.value === current.value);
|
||||
this.updateSegmentView(current.value);
|
||||
} else {
|
||||
this.value = current.value;
|
||||
this.emitValueChange();
|
||||
}
|
||||
this.emitValueChange();
|
||||
}
|
||||
|
||||
if (this.scrollable || !this.swipeGesture) {
|
||||
if (this.segmentViewEl) {
|
||||
this.updateSegmentView();
|
||||
|
||||
if (this.scrollable && previous) {
|
||||
this.checkButton(previous, current);
|
||||
}
|
||||
} else if (this.scrollable || !this.swipeGesture) {
|
||||
if (previous) {
|
||||
this.checkButton(previous, current);
|
||||
} else {
|
||||
@@ -874,11 +716,6 @@ export class Segment implements ComponentInterface {
|
||||
'segment-scrollable': this.scrollable,
|
||||
})}
|
||||
>
|
||||
{this.segmentViewEl && (
|
||||
<div part="indicator" class="segment-indicator">
|
||||
<div part="indicator-background" class="segment-indicator-background"></div>
|
||||
</div>
|
||||
)}
|
||||
<slot onSlotchange={this.onSlottedItemsChange}></slot>
|
||||
</Host>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
import { configs, test, dragElementBy } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
@@ -105,8 +105,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
});
|
||||
});
|
||||
|
||||
// TODO FW-3021
|
||||
test.describe.skip('when the pointer is released', () => {
|
||||
test.describe('when the pointer is released', () => {
|
||||
test('should emit if the value has changed', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
@@ -136,14 +135,22 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
const segment = page.locator('ion-segment');
|
||||
const firstButton = page.locator('ion-segment-button[value="1"]');
|
||||
const lastButton = page.locator('ion-segment-button[value="3"]');
|
||||
|
||||
await firstButton.hover();
|
||||
await page.mouse.down();
|
||||
/*
|
||||
* `dragByX` should represent the total width of all segment buttons,
|
||||
* excluding the first half of the first button and the second half
|
||||
* of the last button. This calculation accounts for dragging from
|
||||
* the center of the first button to the center of the last button.
|
||||
*/
|
||||
const segmentWidth = await segment.boundingBox().then((box) => (box ? box.width : 0));
|
||||
const firstButtonWidth = await firstButton.boundingBox().then((box) => (box ? box.width : 0));
|
||||
const lastButtonWidth = await lastButton.boundingBox().then((box) => (box ? box.width : 0));
|
||||
const dragByX = segmentWidth - firstButtonWidth / 2 - lastButtonWidth / 2;
|
||||
|
||||
await lastButton.hover();
|
||||
await page.mouse.up();
|
||||
await dragElementBy(firstButton, page, dragByX);
|
||||
|
||||
expect(ionChangeSpy).toHaveReceivedEventDetail({ value: '3' });
|
||||
expect(ionChangeSpy).toHaveReceivedEventTimes(1);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface SelectModalOption {
|
||||
text: string;
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
checked: boolean;
|
||||
cssClass?: string | string[];
|
||||
handler?: (value: any) => boolean | void | { [key: string]: any };
|
||||
}
|
||||
24
core/src/components/select-modal/select-modal.ios.scss
Normal file
@@ -0,0 +1,24 @@
|
||||
@import "./select-modal";
|
||||
@import "../item/item.ios.vars";
|
||||
@import "../radio/radio.ios.vars";
|
||||
|
||||
ion-item {
|
||||
--inner-padding-end: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The bottom border of the item should only be displayed
|
||||
* under the text and not the radio icon.
|
||||
*/
|
||||
ion-radio::after {
|
||||
@include position(null, null, 0, calc($radio-ios-icon-width + $item-ios-padding-start));
|
||||
position: absolute;
|
||||
|
||||
width: calc(100% - $radio-ios-icon-width - $item-ios-padding-start); /* Adjust width based on the shift */
|
||||
|
||||
border-width: #{0px 0px $item-ios-border-bottom-width 0px};
|
||||
border-style: #{$item-ios-border-bottom-style};
|
||||
border-color: #{$item-ios-border-bottom-color};
|
||||
|
||||
content: "";
|
||||
}
|
||||
30
core/src/components/select-modal/select-modal.md.scss
Normal file
@@ -0,0 +1,30 @@
|
||||
@import "./select-modal";
|
||||
@import "../../themes/ionic.mixins.scss";
|
||||
@import "../item/item.md.vars";
|
||||
|
||||
ion-list ion-radio::part(container) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
ion-list ion-radio::part(label) {
|
||||
@include margin(0);
|
||||
}
|
||||
|
||||
ion-item {
|
||||
--inner-border-width: 0;
|
||||
}
|
||||
|
||||
.item-radio-checked {
|
||||
--background: #{ion-color(primary, base, 0.08)};
|
||||
--background-focused: #{ion-color(primary, base)};
|
||||
--background-focused-opacity: 0.2;
|
||||
--background-hover: #{ion-color(primary, base)};
|
||||
--background-hover-opacity: 0.12;
|
||||
}
|
||||
|
||||
.item-checkbox-checked {
|
||||
--background-activated: #{$item-md-color};
|
||||
--background-focused: #{$item-md-color};
|
||||
--background-hover: #{$item-md-color};
|
||||
--color: #{ion-color(primary, base)};
|
||||
}
|
||||
3
core/src/components/select-modal/select-modal.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
height: 100%;
|
||||
}
|
||||
162
core/src/components/select-modal/select-modal.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import { getIonMode } from '@global/ionic-global';
|
||||
import type { ComponentInterface } from '@stencil/core';
|
||||
import { Component, Element, Host, Prop, forceUpdate, h } from '@stencil/core';
|
||||
import { safeCall } from '@utils/overlays';
|
||||
import { getClassMap } from '@utils/theme';
|
||||
|
||||
import type { CheckboxCustomEvent } from '../checkbox/checkbox-interface';
|
||||
import type { RadioGroupCustomEvent } from '../radio-group/radio-group-interface';
|
||||
|
||||
import type { SelectModalOption } from './select-modal-interface';
|
||||
|
||||
@Component({
|
||||
tag: 'ion-select-modal',
|
||||
styleUrls: {
|
||||
ios: 'select-modal.ios.scss',
|
||||
md: 'select-modal.md.scss',
|
||||
ionic: 'select-modal.md.scss',
|
||||
},
|
||||
scoped: true,
|
||||
})
|
||||
export class SelectModal implements ComponentInterface {
|
||||
@Element() el!: HTMLIonSelectModalElement;
|
||||
|
||||
@Prop() header?: string;
|
||||
|
||||
@Prop() multiple?: boolean;
|
||||
|
||||
@Prop() options: SelectModalOption[] = [];
|
||||
|
||||
private closeModal() {
|
||||
const modal = this.el.closest('ion-modal');
|
||||
|
||||
if (modal) {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
private findOptionFromEvent(ev: CheckboxCustomEvent | RadioGroupCustomEvent) {
|
||||
const { options } = this;
|
||||
return options.find((o) => o.value === ev.target.value);
|
||||
}
|
||||
|
||||
private getValues(ev?: CheckboxCustomEvent | RadioGroupCustomEvent): string | string[] | undefined {
|
||||
const { multiple, options } = this;
|
||||
|
||||
if (multiple) {
|
||||
// this is a modal with checkboxes (multiple value select)
|
||||
// return an array of all the checked values
|
||||
return options.filter((o) => o.checked).map((o) => o.value);
|
||||
}
|
||||
|
||||
// this is a modal with radio buttons (single value select)
|
||||
// return the value that was clicked, otherwise undefined
|
||||
const option = ev ? this.findOptionFromEvent(ev) : null;
|
||||
return option ? option.value : undefined;
|
||||
}
|
||||
|
||||
private callOptionHandler(ev: CheckboxCustomEvent | RadioGroupCustomEvent) {
|
||||
const option = this.findOptionFromEvent(ev);
|
||||
const values = this.getValues(ev);
|
||||
if (option?.handler) {
|
||||
safeCall(option.handler, values);
|
||||
}
|
||||
}
|
||||
|
||||
private setChecked(ev: CheckboxCustomEvent): void {
|
||||
const { multiple } = this;
|
||||
const option = this.findOptionFromEvent(ev);
|
||||
|
||||
// this is a modal with checkboxes (multiple value select)
|
||||
// we need to set the checked value for this option
|
||||
if (multiple && option) {
|
||||
option.checked = ev.detail.checked;
|
||||
}
|
||||
}
|
||||
|
||||
private renderRadioOptions() {
|
||||
const checked = this.options.filter((o) => o.checked).map((o) => o.value)[0];
|
||||
|
||||
return (
|
||||
<ion-radio-group value={checked} onIonChange={(ev) => this.callOptionHandler(ev)}>
|
||||
{this.options.map((option) => (
|
||||
<ion-item
|
||||
lines="none"
|
||||
class={{
|
||||
// TODO FW-4784
|
||||
'item-radio-checked': option.value === checked,
|
||||
...getClassMap(option.cssClass),
|
||||
}}
|
||||
>
|
||||
<ion-radio
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
justify="start"
|
||||
labelPlacement="end"
|
||||
onClick={() => this.closeModal()}
|
||||
onKeyUp={(ev) => {
|
||||
if (ev.key === ' ') {
|
||||
/**
|
||||
* Selecting a radio option with keyboard navigation,
|
||||
* either through the Enter or Space keys, should
|
||||
* dismiss the modal.
|
||||
*/
|
||||
this.closeModal();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{option.text}
|
||||
</ion-radio>
|
||||
</ion-item>
|
||||
))}
|
||||
</ion-radio-group>
|
||||
);
|
||||
}
|
||||
|
||||
private renderCheckboxOptions() {
|
||||
return this.options.map((option) => (
|
||||
<ion-item
|
||||
class={{
|
||||
// TODO FW-4784
|
||||
'item-checkbox-checked': option.checked,
|
||||
...getClassMap(option.cssClass),
|
||||
}}
|
||||
>
|
||||
<ion-checkbox
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
checked={option.checked}
|
||||
justify="start"
|
||||
labelPlacement="end"
|
||||
onIonChange={(ev) => {
|
||||
this.setChecked(ev);
|
||||
this.callOptionHandler(ev);
|
||||
// TODO FW-4784
|
||||
forceUpdate(this);
|
||||
}}
|
||||
>
|
||||
{option.text}
|
||||
</ion-checkbox>
|
||||
</ion-item>
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Host class={getIonMode(this)}>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
{this.header !== undefined && <ion-title>{this.header}</ion-title>}
|
||||
|
||||
<ion-buttons slot="end">
|
||||
<ion-button onClick={() => this.closeModal()}>Close</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>{this.multiple === true ? this.renderCheckboxOptions() : this.renderRadioOptions()}</ion-list>
|
||||
</ion-content>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
}
|
||||
40
core/src/components/select-modal/test/basic/index.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Select - Modal</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Select Modal - Basic</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-modal is-open="true">
|
||||
<ion-select-modal multiple="false"></ion-select-modal>
|
||||
</ion-modal>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
const selectModal = document.querySelector('ion-select-modal');
|
||||
selectModal.options = [
|
||||
{ value: 'apple', text: 'Apple', disabled: false, checked: true },
|
||||
{ value: 'banana', text: 'Banana', disabled: false, checked: false },
|
||||
];
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
101
core/src/components/select-modal/test/basic/select-modal.e2e.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
import type { SelectModalOption } from '../../select-modal-interface';
|
||||
import { SelectModalPage } from '../fixtures';
|
||||
|
||||
const options: SelectModalOption[] = [
|
||||
{ value: 'apple', text: 'Apple', disabled: false, checked: false },
|
||||
{ value: 'banana', text: 'Banana', disabled: false, checked: false },
|
||||
];
|
||||
|
||||
const checkedOptions: SelectModalOption[] = [
|
||||
{ value: 'apple', text: 'Apple', disabled: false, checked: true },
|
||||
{ value: 'banana', text: 'Banana', disabled: false, checked: false },
|
||||
];
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('select-modal: basic'), () => {
|
||||
test.beforeEach(({ browserName }) => {
|
||||
test.skip(browserName === 'webkit', 'ROU-5437');
|
||||
});
|
||||
|
||||
test.describe('single selection', () => {
|
||||
let selectModalPage: SelectModalPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
selectModalPage = new SelectModalPage(page);
|
||||
});
|
||||
|
||||
test('clicking an unselected option should dismiss the modal', async () => {
|
||||
await selectModalPage.setup(config, options, false);
|
||||
|
||||
await selectModalPage.clickOption('apple');
|
||||
await selectModalPage.ionModalDidDismiss.next();
|
||||
await expect(selectModalPage.modal).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('clicking a selected option should dismiss the modal', async () => {
|
||||
await selectModalPage.setup(config, checkedOptions, false);
|
||||
|
||||
await selectModalPage.clickOption('apple');
|
||||
await selectModalPage.ionModalDidDismiss.next();
|
||||
await expect(selectModalPage.modal).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('pressing Space on an unselected option should dismiss the modal', async () => {
|
||||
await selectModalPage.setup(config, options, false);
|
||||
|
||||
await selectModalPage.pressSpaceOnOption('apple');
|
||||
await selectModalPage.ionModalDidDismiss.next();
|
||||
await expect(selectModalPage.modal).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('pressing Space on a selected option should dismiss the modal', async ({ browserName }) => {
|
||||
test.skip(browserName === 'firefox', 'Same behavior as ROU-5437');
|
||||
|
||||
await selectModalPage.setup(config, checkedOptions, false);
|
||||
|
||||
await selectModalPage.pressSpaceOnOption('apple');
|
||||
await selectModalPage.ionModalDidDismiss.next();
|
||||
await expect(selectModalPage.modal).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('clicking the close button should dismiss the modal', async () => {
|
||||
await selectModalPage.setup(config, options, false);
|
||||
|
||||
const closeButton = selectModalPage.modal.locator('ion-header ion-toolbar ion-button');
|
||||
await closeButton.click();
|
||||
await selectModalPage.ionModalDidDismiss.next();
|
||||
await expect(selectModalPage.modal).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This behavior does not vary across directions.
|
||||
* The components used inside of `ion-select-modal`
|
||||
* do have RTL logic, but those are tested in their
|
||||
* respective component test files.
|
||||
*/
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('select-modal: rendering'), () => {
|
||||
let selectModalPage: SelectModalPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
selectModalPage = new SelectModalPage(page);
|
||||
});
|
||||
test('should not have visual regressions with single selection', async () => {
|
||||
await selectModalPage.setup(config, checkedOptions, false);
|
||||
await selectModalPage.screenshot(screenshot, 'select-modal-diff');
|
||||
});
|
||||
test('should not have visual regressions with multiple selection', async () => {
|
||||
await selectModalPage.setup(config, checkedOptions, true);
|
||||
await selectModalPage.screenshot(screenshot, 'select-modal-multiple-diff');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
73
core/src/components/select-modal/test/fixtures.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import type { E2EPage, E2ELocator, EventSpy, E2EPageOptions, ScreenshotFn } from '@utils/test/playwright';
|
||||
|
||||
import type { SelectModalOption } from '../select-modal-interface';
|
||||
|
||||
export class SelectModalPage {
|
||||
private page: E2EPage;
|
||||
private multiple?: boolean;
|
||||
private options: SelectModalOption[] = [];
|
||||
|
||||
// Locators
|
||||
modal!: E2ELocator;
|
||||
selectModal!: E2ELocator;
|
||||
|
||||
// Event spies
|
||||
ionModalDidDismiss!: EventSpy;
|
||||
|
||||
constructor(page: E2EPage) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async setup(config: E2EPageOptions, options: SelectModalOption[], multiple = false) {
|
||||
const { page } = this;
|
||||
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-modal>
|
||||
<ion-select-modal></ion-select-modal>
|
||||
</ion-modal>
|
||||
<script>
|
||||
const selectModal = document.querySelector('ion-select-modal');
|
||||
selectModal.options = ${JSON.stringify(options)};
|
||||
selectModal.multiple = ${multiple};
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
this.ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
|
||||
this.modal = page.locator('ion-modal');
|
||||
this.selectModal = page.locator('ion-select-modal');
|
||||
this.multiple = multiple;
|
||||
this.options = options;
|
||||
|
||||
await this.modal.evaluate((modal: HTMLIonModalElement) => modal.present());
|
||||
|
||||
await ionModalDidPresent.next();
|
||||
}
|
||||
|
||||
async screenshot(screenshot: ScreenshotFn, name: string) {
|
||||
await expect(this.selectModal).toHaveScreenshot(screenshot(name));
|
||||
}
|
||||
|
||||
async clickOption(value: string) {
|
||||
const option = this.getOption(value);
|
||||
await option.click();
|
||||
}
|
||||
|
||||
async pressSpaceOnOption(value: string) {
|
||||
const option = this.getOption(value);
|
||||
await option.press('Space');
|
||||
}
|
||||
|
||||
private getOption(value: string) {
|
||||
const { multiple, selectModal } = this;
|
||||
const selector = multiple ? 'ion-checkbox' : 'ion-radio';
|
||||
const index = this.options.findIndex((o) => o.value === value);
|
||||
|
||||
return selectModal.locator(selector).nth(index);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export type SelectInterface = 'action-sheet' | 'popover' | 'alert';
|
||||
export type SelectInterface = 'action-sheet' | 'popover' | 'alert' | 'modal';
|
||||
|
||||
export type SelectCompareFn = (currentValue: any, compareValue: any) => boolean;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { NotchController } from '@utils/forms';
|
||||
import { compareOptions, createNotchController, isOptionSelected } from '@utils/forms';
|
||||
import { focusVisibleElement, renderHiddenInput, inheritAttributes } from '@utils/helpers';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import { actionSheetController, alertController, popoverController } from '@utils/overlays';
|
||||
import { actionSheetController, alertController, popoverController, modalController } from '@utils/overlays';
|
||||
import type { OverlaySelect } from '@utils/overlays-interface';
|
||||
import { isRTL } from '@utils/rtl';
|
||||
import { createColorClasses, hostContext } from '@utils/theme';
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
CssClassMap,
|
||||
PopoverOptions,
|
||||
StyleEventDetail,
|
||||
ModalOptions,
|
||||
} from '../../interface';
|
||||
import type { ActionSheetButton } from '../action-sheet/action-sheet-interface';
|
||||
import type { AlertInput } from '../alert/alert-interface';
|
||||
@@ -98,15 +99,15 @@ export class Select implements ComponentInterface {
|
||||
@Prop() 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`.
|
||||
*/
|
||||
@Prop() interface: SelectInterface = 'alert';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* [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.
|
||||
@@ -318,9 +319,9 @@ export class Select implements ComponentInterface {
|
||||
|
||||
await overlay.present();
|
||||
|
||||
// focus selected option for popovers
|
||||
if (this.interface === 'popover') {
|
||||
const indexOfSelected = this.childOpts.map((o) => o.value).indexOf(this.value);
|
||||
// focus selected option for popovers and modals
|
||||
if (this.interface === 'popover' || this.interface === 'modal') {
|
||||
const indexOfSelected = this.childOpts.findIndex((o) => o.value === this.value);
|
||||
|
||||
if (indexOfSelected > -1) {
|
||||
const selectedItem = overlay.querySelector<HTMLElement>(
|
||||
@@ -328,8 +329,6 @@ export class Select implements ComponentInterface {
|
||||
);
|
||||
|
||||
if (selectedItem) {
|
||||
focusVisibleElement(selectedItem);
|
||||
|
||||
/**
|
||||
* Browsers such as Firefox do not
|
||||
* correctly delegate focus when manually
|
||||
@@ -341,10 +340,17 @@ export class Select implements ComponentInterface {
|
||||
* we only need to worry about those two components
|
||||
* when focusing.
|
||||
*/
|
||||
const interactiveEl = selectedItem.querySelector<HTMLElement>('ion-radio, ion-checkbox');
|
||||
const interactiveEl = selectedItem.querySelector<HTMLElement>('ion-radio, ion-checkbox') as
|
||||
| HTMLIonRadioElement
|
||||
| HTMLIonCheckboxElement
|
||||
| null;
|
||||
if (interactiveEl) {
|
||||
interactiveEl.focus();
|
||||
// Needs to be called before `focusVisibleElement` to prevent issue with focus event bubbling
|
||||
// and removing `ion-focused` style
|
||||
interactiveEl.setFocus();
|
||||
}
|
||||
|
||||
focusVisibleElement(selectedItem);
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
@@ -352,14 +358,18 @@ export class Select implements ComponentInterface {
|
||||
*/
|
||||
const firstEnabledOption = overlay.querySelector<HTMLElement>(
|
||||
'ion-radio:not(.radio-disabled), ion-checkbox:not(.checkbox-disabled)'
|
||||
);
|
||||
if (firstEnabledOption) {
|
||||
focusVisibleElement(firstEnabledOption.closest('ion-item')!);
|
||||
) as HTMLIonRadioElement | HTMLIonCheckboxElement | null;
|
||||
|
||||
if (firstEnabledOption) {
|
||||
/**
|
||||
* Focus the option for the same reason as we do above.
|
||||
*
|
||||
* Needs to be called before `focusVisibleElement` to prevent issue with focus event bubbling
|
||||
* and removing `ion-focused` style
|
||||
*/
|
||||
firstEnabledOption.focus();
|
||||
firstEnabledOption.setFocus();
|
||||
|
||||
focusVisibleElement(firstEnabledOption.closest('ion-item')!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -389,6 +399,9 @@ export class Select implements ComponentInterface {
|
||||
if (selectInterface === 'popover') {
|
||||
return this.openPopover(ev!);
|
||||
}
|
||||
if (selectInterface === 'modal') {
|
||||
return this.openModal();
|
||||
}
|
||||
return this.openAlert();
|
||||
}
|
||||
|
||||
@@ -406,7 +419,13 @@ export class Select implements ComponentInterface {
|
||||
case 'popover':
|
||||
const popover = overlay.querySelector('ion-select-popover');
|
||||
if (popover) {
|
||||
popover.options = this.createPopoverOptions(childOpts, value);
|
||||
popover.options = this.createOverlaySelectOptions(childOpts, value);
|
||||
}
|
||||
break;
|
||||
case 'modal':
|
||||
const modal = overlay.querySelector('ion-select-modal');
|
||||
if (modal) {
|
||||
modal.options = this.createOverlaySelectOptions(childOpts, value);
|
||||
}
|
||||
break;
|
||||
case 'alert':
|
||||
@@ -475,7 +494,7 @@ export class Select implements ComponentInterface {
|
||||
return alertInputs;
|
||||
}
|
||||
|
||||
private createPopoverOptions(data: HTMLIonSelectOptionElement[], selectValue: any): SelectPopoverOption[] {
|
||||
private createOverlaySelectOptions(data: HTMLIonSelectOptionElement[], selectValue: any): SelectPopoverOption[] {
|
||||
const popoverOptions = data.map((option) => {
|
||||
const value = getOptionValue(option);
|
||||
|
||||
@@ -553,7 +572,7 @@ export class Select implements ComponentInterface {
|
||||
message: interfaceOptions.message,
|
||||
multiple,
|
||||
value,
|
||||
options: this.createPopoverOptions(this.childOpts, value),
|
||||
options: this.createOverlaySelectOptions(this.childOpts, value),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -647,6 +666,40 @@ export class Select implements ComponentInterface {
|
||||
return alertController.create(alertOpts);
|
||||
}
|
||||
|
||||
private openModal() {
|
||||
const { multiple, value, interfaceOptions } = this;
|
||||
const mode = getIonMode(this);
|
||||
|
||||
const modalOpts: ModalOptions = {
|
||||
...interfaceOptions,
|
||||
mode,
|
||||
|
||||
cssClass: ['select-modal', interfaceOptions.cssClass],
|
||||
component: 'ion-select-modal',
|
||||
componentProps: {
|
||||
header: interfaceOptions.header,
|
||||
multiple,
|
||||
value,
|
||||
options: this.createOverlaySelectOptions(this.childOpts, value),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Workaround for Stencil to autodefine
|
||||
* ion-select-modal and ion-modal when
|
||||
* using Custom Elements build.
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
if (false) {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
document.createElement('ion-select-modal');
|
||||
document.createElement('ion-modal');
|
||||
}
|
||||
|
||||
return modalController.create(modalOpts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the select interface.
|
||||
*/
|
||||
|
||||
@@ -51,6 +51,14 @@
|
||||
<ion-select-option value="pears">Pears</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-select label="Modal" placeholder="Select one" interface="modal">
|
||||
<ion-select-option value="apples">Apples</ion-select-option>
|
||||
<ion-select-option value="oranges">Oranges</ion-select-option>
|
||||
<ion-select-option value="pears">Pears</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list>
|
||||
@@ -76,6 +84,15 @@
|
||||
<ion-select-option value="honeybadger">Honey Badger</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-select label="Modal" multiple="true" interface="modal">
|
||||
<ion-select-option value="bird">Bird</ion-select-option>
|
||||
<ion-select-option value="cat">Cat</ion-select-option>
|
||||
<ion-select-option value="dog">Dog</ion-select-option>
|
||||
<ion-select-option value="honeybadger">Honey Badger</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list>
|
||||
@@ -124,6 +141,16 @@
|
||||
<ion-select-option value="onions">Onions</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
<ion-item color="secondary">
|
||||
<ion-select label="Modal Sheet" id="customModalSelect" interface="modal" placeholder="Select One">
|
||||
<ion-select-option value="pepperoni">Pepperoni</ion-select-option>
|
||||
<ion-select-option value="bacon">Bacon</ion-select-option>
|
||||
<ion-select-option value="xcheese">Extra Cheese</ion-select-option>
|
||||
<ion-select-option value="mushrooms">Mushrooms</ion-select-option>
|
||||
<ion-select-option value="onions">Onions</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
|
||||
@@ -152,6 +179,14 @@
|
||||
message: '$1.50 charge for every topping',
|
||||
};
|
||||
customActionSheetSelect.interfaceOptions = customActionSheetOptions;
|
||||
|
||||
var customModalSelect = document.getElementById('customModalSelect');
|
||||
var customModalSheetOptions = {
|
||||
header: 'Pizza Toppings',
|
||||
breakpoints: [0.5],
|
||||
initialBreakpoint: 0.5,
|
||||
};
|
||||
customModalSelect.interfaceOptions = customModalSheetOptions;
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
|
||||
@@ -58,6 +58,24 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
await expect(popover).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('select: modal', () => {
|
||||
test('it should open a modal select', async ({ page }) => {
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#customModalSelect');
|
||||
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const modal = page.locator('ion-modal');
|
||||
|
||||
// select has no value, so first option should be focused by default
|
||||
const modalOption1 = modal.locator('.select-interface-option:first-of-type ion-radio');
|
||||
await expect(modalOption1).toBeFocused();
|
||||
|
||||
await expect(modal).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -27,6 +27,19 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
|
||||
await expect(helperText).toHaveText('my helper');
|
||||
await expect(errorText).toBeHidden();
|
||||
});
|
||||
test('textarea should have an aria-describedby attribute when helper text is present', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`<ion-textarea helper-text="my helper" error-text="my error" label="my textarea"></ion-textarea>`,
|
||||
config
|
||||
);
|
||||
|
||||
const textarea = page.locator('ion-textarea textarea');
|
||||
const helperText = page.locator('ion-textarea .helper-text');
|
||||
const helperTextId = await helperText.getAttribute('id');
|
||||
const ariaDescribedBy = await textarea.getAttribute('aria-describedby');
|
||||
|
||||
expect(ariaDescribedBy).toBe(helperTextId);
|
||||
});
|
||||
test('error text should be visible when textarea is invalid', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`<ion-textarea class="ion-invalid ion-touched" helper-text="my helper" error-text="my error" label="my textarea"></ion-textarea>`,
|
||||
@@ -55,6 +68,48 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
|
||||
const errorText = page.locator('ion-textarea .error-text');
|
||||
await expect(errorText).toHaveScreenshot(screenshot(`textarea-error-custom-color`));
|
||||
});
|
||||
test('textarea should have an aria-describedby attribute when error text is present', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`<ion-textarea class="ion-invalid ion-touched" helper-text="my helper" error-text="my error" label="my textarea"></ion-textarea>`,
|
||||
config
|
||||
);
|
||||
|
||||
const textarea = page.locator('ion-textarea textarea');
|
||||
const errorText = page.locator('ion-textarea .error-text');
|
||||
const errorTextId = await errorText.getAttribute('id');
|
||||
const ariaDescribedBy = await textarea.getAttribute('aria-describedby');
|
||||
|
||||
expect(ariaDescribedBy).toBe(errorTextId);
|
||||
});
|
||||
test('textarea should have aria-invalid attribute when input is invalid', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`<ion-textarea class="ion-invalid ion-touched" helper-text="my helper" error-text="my error" label="my textarea"></ion-textarea>`,
|
||||
config
|
||||
);
|
||||
|
||||
const textarea = page.locator('ion-textarea textarea');
|
||||
|
||||
await expect(textarea).toHaveAttribute('aria-invalid');
|
||||
});
|
||||
test('textarea should not have aria-invalid attribute when input is valid', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`<ion-textarea helper-text="my helper" error-text="my error" label="my textarea"></ion-textarea>`,
|
||||
config
|
||||
);
|
||||
|
||||
const textarea = page.locator('ion-textarea textarea');
|
||||
|
||||
await expect(textarea).not.toHaveAttribute('aria-invalid');
|
||||
});
|
||||
test('textarea should not have aria-describedby attribute when no hint or error text is present', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setContent(`<ion-textarea label="my textarea"></ion-textarea>`, config);
|
||||
|
||||
const textarea = page.locator('ion-textarea textarea');
|
||||
|
||||
await expect(textarea).not.toHaveAttribute('aria-describedby');
|
||||
});
|
||||
});
|
||||
test.describe('textarea: hint text rendering', () => {
|
||||
test.describe('regular textareas', () => {
|
||||
|
||||
@@ -45,6 +45,8 @@ import type { TextareaChangeEventDetail, TextareaInputEventDetail } from './text
|
||||
export class Textarea implements ComponentInterface {
|
||||
private nativeInput?: HTMLTextAreaElement;
|
||||
private inputId = `ion-textarea-${textareaIds++}`;
|
||||
private helperTextId = `${this.inputId}-helper-text`;
|
||||
private errorTextId = `${this.inputId}-error-text`;
|
||||
/**
|
||||
* `true` if the textarea was cleared as a result of the user typing
|
||||
* with `clearOnEdit` enabled.
|
||||
@@ -576,9 +578,30 @@ export class Textarea 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() {
|
||||
@@ -703,6 +726,8 @@ export class Textarea implements ComponentInterface {
|
||||
onBlur={this.onBlur}
|
||||
onFocus={this.onFocus}
|
||||
onKeyDown={this.onKeyDown}
|
||||
aria-describedby={this.getHintTextID()}
|
||||
aria-invalid={this.getHintTextID() === this.errorTextId}
|
||||
{...this.inheritedAttributes}
|
||||
>
|
||||
{value}
|
||||
|
||||
@@ -97,6 +97,8 @@
|
||||
background: var(--background);
|
||||
|
||||
box-shadow: var(--box-shadow);
|
||||
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.toast-wrapper.toast-top {
|
||||
@@ -115,7 +117,6 @@
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
pointer-events: auto;
|
||||
|
||||
height: inherit;
|
||||
min-height: inherit;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { focusVisibleElement } from '@utils/helpers';
|
||||
* valid usage for the disabled property on ion-button.
|
||||
*/
|
||||
export const focusableQueryString =
|
||||
'[tabindex]:not([tabindex^="-"]):not([hidden]):not([disabled]), input:not([type=hidden]):not([tabindex^="-"]):not([hidden]):not([disabled]), textarea:not([tabindex^="-"]):not([hidden]):not([disabled]), button:not([tabindex^="-"]):not([hidden]):not([disabled]), select:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable[disabled="false"]:not([tabindex^="-"]):not([hidden])';
|
||||
'[tabindex]:not([tabindex^="-"]):not([hidden]):not([disabled]), input:not([type=hidden]):not([tabindex^="-"]):not([hidden]):not([disabled]), textarea:not([tabindex^="-"]):not([hidden]):not([disabled]), button:not([tabindex^="-"]):not([hidden]):not([disabled]), select:not([tabindex^="-"]):not([hidden]):not([disabled]), ion-checkbox:not([tabindex^="-"]):not([hidden]):not([disabled]), ion-radio:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable[disabled="false"]:not([tabindex^="-"]):not([hidden])';
|
||||
|
||||
/**
|
||||
* Focuses the first descendant in a context
|
||||
@@ -78,7 +78,13 @@ const focusElementInContext = <T extends HTMLElement>(
|
||||
}
|
||||
|
||||
if (elementToFocus) {
|
||||
focusVisibleElement(elementToFocus);
|
||||
const radioGroup = elementToFocus.closest('ion-radio-group');
|
||||
|
||||
if (radioGroup) {
|
||||
radioGroup.setFocus();
|
||||
} else {
|
||||
focusVisibleElement(elementToFocus);
|
||||
}
|
||||
} else {
|
||||
// Focus fallback element instead of letting focus escape
|
||||
fallbackElement.focus();
|
||||
|
||||
@@ -413,3 +413,14 @@ export const shallowEqualStringMap = (
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getNextSiblingOfType = <T extends Element>(element: Element): T | null => {
|
||||
let sibling = element.nextSibling;
|
||||
while (sibling) {
|
||||
if (sibling.nodeType === Node.ELEMENT_NODE && (sibling as T) !== null) {
|
||||
return sibling as T;
|
||||
}
|
||||
sibling = sibling.nextSibling;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||