Compare commits
73 Commits
ROU-10896v
...
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 | ||
|
|
221bb42c00 | ||
|
|
a743344f07 | ||
|
|
a87674a779 | ||
|
|
f64458dc63 | ||
|
|
ab7a0ab050 | ||
|
|
df39ceac6d | ||
|
|
35b1a45929 | ||
|
|
f4ee2bbfc4 | ||
|
|
88b701363c | ||
|
|
f1d50c0f64 |
@@ -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:
|
||||
|
||||
110
CHANGELOG.md
@@ -3,6 +3,116 @@
|
||||
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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **checkbox:** align checkbox properly in item using start alignment ([#29850](https://github.com/ionic-team/ionic-framework/issues/29850)) ([88b7013](https://github.com/ionic-team/ionic-framework/commit/88b701363c1865e81d7d136acab4b9fef3b1ab7a)), closes [#29837](https://github.com/ionic-team/ionic-framework/issues/29837)
|
||||
* **datetime:** display today's date and time when value is an empty string ([#29839](https://github.com/ionic-team/ionic-framework/issues/29839)) ([f1d50c0](https://github.com/ionic-team/ionic-framework/commit/f1d50c0f645b3adf51426c24a1da0bbd1223f1d5)), closes [#29669](https://github.com/ionic-team/ionic-framework/issues/29669)
|
||||
* **range:** disable scroll when range is being dragged ([#29241](https://github.com/ionic-team/ionic-framework/issues/29241)) ([35b1a45](https://github.com/ionic-team/ionic-framework/commit/35b1a459294657b609f683457e71babd4673a3a8))
|
||||
* **react, vue:** add default value for navManager on tabs ([#29865](https://github.com/ionic-team/ionic-framework/issues/29865)) ([df39cea](https://github.com/ionic-team/ionic-framework/commit/df39ceac6db6511518aed4766e6a59aa24fb53ec))
|
||||
* **react:** revert react output target version ([#29869](https://github.com/ionic-team/ionic-framework/issues/29869)) ([f64458d](https://github.com/ionic-team/ionic-framework/commit/f64458dc63a05acb955aa1f2ec3755c738466cf4)), closes [ionic-team/stencil-ds-output-targets#476](https://github.com/ionic-team/stencil-ds-output-targets/issues/476) [ionic-team/stencil-ds-output-targets#475](https://github.com/ionic-team/stencil-ds-output-targets/issues/475) [#29848](https://github.com/ionic-team/ionic-framework/issues/29848)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [8.3.0](https://github.com/ionic-team/ionic-framework/compare/v8.2.8...v8.3.0) (2024-09-05)
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,107 @@
|
||||
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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **checkbox:** align checkbox properly in item using start alignment ([#29850](https://github.com/ionic-team/ionic-framework/issues/29850)) ([88b7013](https://github.com/ionic-team/ionic-framework/commit/88b701363c1865e81d7d136acab4b9fef3b1ab7a)), closes [#29837](https://github.com/ionic-team/ionic-framework/issues/29837)
|
||||
* **datetime:** display today's date and time when value is an empty string ([#29839](https://github.com/ionic-team/ionic-framework/issues/29839)) ([f1d50c0](https://github.com/ionic-team/ionic-framework/commit/f1d50c0f645b3adf51426c24a1da0bbd1223f1d5)), closes [#29669](https://github.com/ionic-team/ionic-framework/issues/29669)
|
||||
* **range:** disable scroll when range is being dragged ([#29241](https://github.com/ionic-team/ionic-framework/issues/29241)) ([35b1a45](https://github.com/ionic-team/ionic-framework/commit/35b1a459294657b609f683457e71babd4673a3a8))
|
||||
* **react:** revert react output target version ([#29869](https://github.com/ionic-team/ionic-framework/issues/29869)) ([f64458d](https://github.com/ionic-team/ionic-framework/commit/f64458dc63a05acb955aa1f2ec3755c738466cf4)), closes [ionic-team/stencil-ds-output-targets#476](https://github.com/ionic-team/stencil-ds-output-targets/issues/476) [ionic-team/stencil-ds-output-targets#475](https://github.com/ionic-team/stencil-ds-output-targets/issues/475) [#29848](https://github.com/ionic-team/ionic-framework/issues/29848)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [8.3.0](https://github.com/ionic-team/ionic-framework/compare/v8.2.8...v8.3.0) (2024-09-05)
|
||||
|
||||
|
||||
|
||||
23
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
|
||||
@@ -1541,6 +1542,7 @@ ion-segment,css-prop,--background,ios
|
||||
ion-segment,css-prop,--background,md
|
||||
|
||||
ion-segment-button,shadow
|
||||
ion-segment-button,prop,contentId,string | undefined,undefined,false,true
|
||||
ion-segment-button,prop,disabled,boolean,false,false,false
|
||||
ion-segment-button,prop,layout,"icon-bottom" | "icon-end" | "icon-hide" | "icon-start" | "icon-top" | "label-hide" | undefined,'icon-top',false,false
|
||||
ion-segment-button,prop,mode,"ios" | "md",undefined,false,false
|
||||
@@ -1606,6 +1608,12 @@ ion-segment-button,part,indicator
|
||||
ion-segment-button,part,indicator-background
|
||||
ion-segment-button,part,native
|
||||
|
||||
ion-segment-content,shadow
|
||||
|
||||
ion-segment-view,shadow
|
||||
ion-segment-view,prop,disabled,boolean,false,false,false
|
||||
ion-segment-view,event,ionSegmentViewScroll,SegmentViewScrollEvent,true
|
||||
|
||||
ion-select,shadow
|
||||
ion-select,prop,cancelText,string,'Cancel',false,false
|
||||
ion-select,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
|
||||
@@ -1613,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
|
||||
@@ -1671,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
|
||||
|
||||
915
core/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "8.3.0",
|
||||
"version": "8.4.3",
|
||||
"description": "Base components for Ionic",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -31,7 +31,7 @@
|
||||
"loader/"
|
||||
],
|
||||
"dependencies": {
|
||||
"@stencil/core": "^4.19.2",
|
||||
"@stencil/core": "4.20.0",
|
||||
"ionicons": "^7.2.2",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
@@ -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/react-output-target": "^0.6.0",
|
||||
"@stencil/angular-output-target": "^0.10.0",
|
||||
"@stencil/react-output-target": "0.5.3",
|
||||
"@stencil/sass": "^3.0.9",
|
||||
"@stencil/vue-output-target": "^0.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%;
|
||||
}
|
||||
|
||||
120
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.
|
||||
*/
|
||||
@@ -2690,6 +2699,10 @@ export namespace Components {
|
||||
"value"?: SegmentValue;
|
||||
}
|
||||
interface IonSegmentButton {
|
||||
/**
|
||||
* The `id` of the segment content.
|
||||
*/
|
||||
"contentId"?: string;
|
||||
/**
|
||||
* If `true`, the user cannot interact with the segment button.
|
||||
*/
|
||||
@@ -2712,6 +2725,19 @@ export namespace Components {
|
||||
*/
|
||||
"value": SegmentValue;
|
||||
}
|
||||
interface IonSegmentContent {
|
||||
}
|
||||
interface IonSegmentView {
|
||||
/**
|
||||
* If `true`, the segment view cannot be interacted with.
|
||||
*/
|
||||
"disabled": boolean;
|
||||
/**
|
||||
* @param id : The id of the segment content to display.
|
||||
* @param smoothScroll : Whether to animate the scroll transition.
|
||||
*/
|
||||
"setContent": (id: string, smoothScroll?: boolean) => Promise<void>;
|
||||
}
|
||||
interface IonSelect {
|
||||
/**
|
||||
* The text to display on the cancel button.
|
||||
@@ -2738,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;
|
||||
/**
|
||||
@@ -2799,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.
|
||||
@@ -3413,6 +3444,10 @@ export interface IonSegmentCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLIonSegmentElement;
|
||||
}
|
||||
export interface IonSegmentViewCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLIonSegmentViewElement;
|
||||
}
|
||||
export interface IonSelectCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLIonSelectElement;
|
||||
@@ -3966,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 {
|
||||
@@ -4409,6 +4444,29 @@ declare global {
|
||||
prototype: HTMLIonSegmentButtonElement;
|
||||
new (): HTMLIonSegmentButtonElement;
|
||||
};
|
||||
interface HTMLIonSegmentContentElement extends Components.IonSegmentContent, HTMLStencilElement {
|
||||
}
|
||||
var HTMLIonSegmentContentElement: {
|
||||
prototype: HTMLIonSegmentContentElement;
|
||||
new (): HTMLIonSegmentContentElement;
|
||||
};
|
||||
interface HTMLIonSegmentViewElementEventMap {
|
||||
"ionSegmentViewScroll": SegmentViewScrollEvent;
|
||||
}
|
||||
interface HTMLIonSegmentViewElement extends Components.IonSegmentView, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLIonSegmentViewElementEventMap>(type: K, listener: (this: HTMLIonSegmentViewElement, ev: IonSegmentViewCustomEvent<HTMLIonSegmentViewElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLIonSegmentViewElementEventMap>(type: K, listener: (this: HTMLIonSegmentViewElement, ev: IonSegmentViewCustomEvent<HTMLIonSegmentViewElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLIonSegmentViewElement: {
|
||||
prototype: HTMLIonSegmentViewElement;
|
||||
new (): HTMLIonSegmentViewElement;
|
||||
};
|
||||
interface HTMLIonSelectElementEventMap {
|
||||
"ionChange": SelectChangeEventDetail;
|
||||
"ionCancel": void;
|
||||
@@ -4431,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: {
|
||||
@@ -4718,7 +4782,10 @@ declare global {
|
||||
"ion-searchbar": HTMLIonSearchbarElement;
|
||||
"ion-segment": HTMLIonSegmentElement;
|
||||
"ion-segment-button": HTMLIonSegmentButtonElement;
|
||||
"ion-segment-content": HTMLIonSegmentContentElement;
|
||||
"ion-segment-view": HTMLIonSegmentViewElement;
|
||||
"ion-select": HTMLIonSelectElement;
|
||||
"ion-select-modal": HTMLIonSelectModalElement;
|
||||
"ion-select-option": HTMLIonSelectOptionElement;
|
||||
"ion-select-popover": HTMLIonSelectPopoverElement;
|
||||
"ion-skeleton-text": HTMLIonSkeletonTextElement;
|
||||
@@ -6361,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.
|
||||
*/
|
||||
@@ -6373,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.
|
||||
*/
|
||||
@@ -7447,6 +7514,10 @@ declare namespace LocalJSX {
|
||||
"value"?: SegmentValue;
|
||||
}
|
||||
interface IonSegmentButton {
|
||||
/**
|
||||
* The `id` of the segment content.
|
||||
*/
|
||||
"contentId"?: string;
|
||||
/**
|
||||
* If `true`, the user cannot interact with the segment button.
|
||||
*/
|
||||
@@ -7468,6 +7539,18 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"value"?: SegmentValue;
|
||||
}
|
||||
interface IonSegmentContent {
|
||||
}
|
||||
interface IonSegmentView {
|
||||
/**
|
||||
* If `true`, the segment view cannot be interacted with.
|
||||
*/
|
||||
"disabled"?: boolean;
|
||||
/**
|
||||
* Emitted when the segment view is scrolled.
|
||||
*/
|
||||
"onIonSegmentViewScroll"?: (event: IonSegmentViewCustomEvent<SegmentViewScrollEvent>) => void;
|
||||
}
|
||||
interface IonSelect {
|
||||
/**
|
||||
* The text to display on the cancel button.
|
||||
@@ -7494,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;
|
||||
/**
|
||||
@@ -7574,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.
|
||||
@@ -8159,7 +8247,10 @@ declare namespace LocalJSX {
|
||||
"ion-searchbar": IonSearchbar;
|
||||
"ion-segment": IonSegment;
|
||||
"ion-segment-button": IonSegmentButton;
|
||||
"ion-segment-content": IonSegmentContent;
|
||||
"ion-segment-view": IonSegmentView;
|
||||
"ion-select": IonSelect;
|
||||
"ion-select-modal": IonSelectModal;
|
||||
"ion-select-option": IonSelectOption;
|
||||
"ion-select-popover": IonSelectPopover;
|
||||
"ion-skeleton-text": IonSkeletonText;
|
||||
@@ -8258,7 +8349,10 @@ declare module "@stencil/core" {
|
||||
"ion-searchbar": LocalJSX.IonSearchbar & JSXBase.HTMLAttributes<HTMLIonSearchbarElement>;
|
||||
"ion-segment": LocalJSX.IonSegment & JSXBase.HTMLAttributes<HTMLIonSegmentElement>;
|
||||
"ion-segment-button": LocalJSX.IonSegmentButton & JSXBase.HTMLAttributes<HTMLIonSegmentButtonElement>;
|
||||
"ion-segment-content": LocalJSX.IonSegmentContent & JSXBase.HTMLAttributes<HTMLIonSegmentContentElement>;
|
||||
"ion-segment-view": LocalJSX.IonSegmentView & JSXBase.HTMLAttributes<HTMLIonSegmentViewElement>;
|
||||
"ion-select": LocalJSX.IonSelect & JSXBase.HTMLAttributes<HTMLIonSelectElement>;
|
||||
"ion-select-modal": LocalJSX.IonSelectModal & JSXBase.HTMLAttributes<HTMLIonSelectModalElement>;
|
||||
"ion-select-option": LocalJSX.IonSelectOption & JSXBase.HTMLAttributes<HTMLIonSelectOptionElement>;
|
||||
"ion-select-popover": LocalJSX.IonSelectPopover & JSXBase.HTMLAttributes<HTMLIonSelectPopoverElement>;
|
||||
"ion-skeleton-text": LocalJSX.IonSkeletonText & JSXBase.HTMLAttributes<HTMLIonSkeletonTextElement>;
|
||||
|
||||
@@ -385,7 +385,7 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
>
|
||||
<ion-backdrop tappable={this.backdropDismiss} />
|
||||
|
||||
<div tabindex="0"></div>
|
||||
<div tabindex="0" aria-hidden="true"></div>
|
||||
|
||||
<div class="action-sheet-wrapper ion-overlay-wrapper" ref={(el) => (this.wrapperEl = el)}>
|
||||
<div class="action-sheet-container">
|
||||
@@ -446,7 +446,7 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
<div tabindex="0" aria-hidden="true"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -87,7 +87,10 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host(.in-item) .label-text-wrapper {
|
||||
// Checkboxes that are not slotted inside an item and are not used with a
|
||||
// stacked label should have margins equal to those of the label.
|
||||
:host(.in-item) .label-text-wrapper,
|
||||
:host(.in-item:not(.checkbox-label-placement-stacked):not([slot])) .native-wrapper {
|
||||
@include margin($checkbox-item-label-margin-top, null, $checkbox-item-label-margin-bottom, null);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
});
|
||||
});
|
||||
|
||||
configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, config }) => {
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('checkbox: long label in item'), () => {
|
||||
test('should render margins correctly when using long label in item', async ({ page }) => {
|
||||
await page.setContent(
|
||||
@@ -69,6 +69,28 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
|
||||
const list = page.locator('ion-list');
|
||||
await expect(list).toHaveScreenshot(screenshot(`checkbox-long-label-in-item`));
|
||||
});
|
||||
test('should render margins correctly when using long label in item with start alignment', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/29837',
|
||||
});
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-list>
|
||||
<ion-item>
|
||||
<ion-checkbox justify="start" alignment="start">
|
||||
<ion-label class="ion-text-wrap">Enable Notifications Enable Notifications Enable Notifications</ion-label>
|
||||
</ion-checkbox>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
`,
|
||||
config
|
||||
);
|
||||
const list = page.locator('ion-list');
|
||||
await expect(list).toHaveScreenshot(screenshot(`checkbox-long-label-in-item-align-start`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe(title('checkbox: stacked label in item'), () => {
|
||||
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
@@ -181,6 +181,15 @@
|
||||
</ion-checkbox>
|
||||
</ion-item>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<ion-item>
|
||||
<ion-checkbox justify="start" alignment="start">
|
||||
<ion-label class="ion-text-wrap">
|
||||
Enable Notifications Enable Notifications Enable Notifications
|
||||
</ion-label>
|
||||
</ion-checkbox>
|
||||
</ion-item>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
@@ -113,6 +113,24 @@
|
||||
<ion-checkbox label-placement="stacked" alignment="center">Enable Notifications</ion-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>Multiline Label</h1>
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<ion-checkbox justify="start">
|
||||
<ion-label class="ion-text-wrap">
|
||||
Enable Notifications Enable Notifications Enable Notifications
|
||||
</ion-label>
|
||||
</ion-checkbox>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<ion-checkbox justify="start" alignment="start">
|
||||
<ion-label class="ion-text-wrap">
|
||||
Enable Notifications Enable Notifications Enable Notifications
|
||||
</ion-label>
|
||||
</ion-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
</body>
|
||||
|
||||
@@ -1234,7 +1234,8 @@ export class Datetime implements ComponentInterface {
|
||||
}
|
||||
|
||||
private processValue = (value?: string | string[] | null) => {
|
||||
const hasValue = value !== null && value !== undefined && (!Array.isArray(value) || value.length > 0);
|
||||
const hasValue =
|
||||
value !== null && value !== undefined && value !== '' && (!Array.isArray(value) || value.length > 0);
|
||||
const valueToProcess = hasValue ? parseDate(value) : this.defaultParts;
|
||||
|
||||
const { minParts, maxParts, workingParts, el } = this;
|
||||
|
||||
@@ -121,6 +121,38 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
|
||||
await expect(datetime).toHaveJSProperty('value', '2022-10-01T16:22:00');
|
||||
});
|
||||
|
||||
test("should display today's date and time when value is an empty string", async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-datetime locale="en-US" presentation="date-time" value=""></ion-datetime>
|
||||
|
||||
<script>
|
||||
const mockToday = '2024-07-24T16:22';
|
||||
Date = class extends Date {
|
||||
constructor(...args) {
|
||||
if (args.length === 0) {
|
||||
super(mockToday)
|
||||
} else {
|
||||
super(...args);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
await page.locator('.datetime-ready').waitFor();
|
||||
|
||||
// July 24, 2024
|
||||
const todayButton = page.locator('.calendar-day[data-day="24"][data-month="7"][data-year="2024"]');
|
||||
await expect(todayButton).toHaveClass(/calendar-day-today/);
|
||||
|
||||
// 4:22 PM
|
||||
const timeBody = page.locator('ion-datetime .time-body');
|
||||
await expect(timeBody).toHaveText('4:22 PM');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
|
||||
@@ -11,7 +11,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await expect(page).toHaveScreenshot(screenshot(`modal-sheet-present`));
|
||||
await expect(page).toHaveScreenshot(screenshot(`modal-sheet-present`), {
|
||||
/**
|
||||
* Animations must be enabled to capture the screenshot.
|
||||
* By default, animations are disabled with toHaveScreenshot,
|
||||
* and when capturing the screenshot will call animation.finish().
|
||||
* This will cause the modal to close and the screenshot capture
|
||||
* to be invalid.
|
||||
*/
|
||||
animations: 'allow',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 34 KiB |
@@ -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();
|
||||
}
|
||||
|
||||
@@ -325,7 +325,8 @@ export class Range implements ComponentInterface {
|
||||
this.setupGesture();
|
||||
}
|
||||
|
||||
this.contentEl = findClosestIonContent(this.el);
|
||||
const ionContent = findClosestIonContent(this.el);
|
||||
this.contentEl = ionContent?.querySelector('.ion-content-scroll-host') ?? ionContent;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@@ -418,7 +419,7 @@ export class Range implements ComponentInterface {
|
||||
*
|
||||
* This only needs to be done once.
|
||||
*/
|
||||
if (contentEl && this.initialContentScrollY === undefined) {
|
||||
if (contentEl && this.pressedKnob === undefined) {
|
||||
this.initialContentScrollY = disableContentScrollY(contentEl);
|
||||
}
|
||||
|
||||
@@ -469,7 +470,7 @@ export class Range implements ComponentInterface {
|
||||
*
|
||||
* The user can now scroll on the view in the next gesture event.
|
||||
*/
|
||||
if (contentEl && initialContentScrollY !== undefined) {
|
||||
if (contentEl && this.pressedKnob !== undefined) {
|
||||
resetContentScrollY(contentEl, initialContentScrollY);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,7 @@ import { configs, dragElementBy, test } from '@utils/test/playwright';
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('range: events:'), () => {
|
||||
test.describe('range: knob events', () => {
|
||||
/**
|
||||
* The mouse events are flaky on CI
|
||||
*/
|
||||
test.fixme('should emit start/end events', async ({ page }) => {
|
||||
test('should emit start/end events', async ({ page }) => {
|
||||
/**
|
||||
* Requires padding to prevent the knob from being clipped.
|
||||
* If it's clipped, then the value might be one off.
|
||||
@@ -31,23 +28,34 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
|
||||
const rangeEl = page.locator('ion-range');
|
||||
|
||||
await dragElementBy(rangeEl, page, 300, 0);
|
||||
await page.waitForChanges();
|
||||
|
||||
/**
|
||||
* dragElementBy defaults to starting the drag from the middle of the el,
|
||||
* so the start value should jump to 50 despite the range defaulting to 20.
|
||||
* Verify both events fire if range is dragged.
|
||||
*/
|
||||
expect(rangeStart).toHaveReceivedEventDetail({ value: 50 });
|
||||
await dragElementBy(rangeEl, page, 180);
|
||||
|
||||
await rangeStart.next();
|
||||
await rangeEnd.next();
|
||||
|
||||
// Once the knob is dragged, the start event should fire with
|
||||
// the initial value.
|
||||
expect(rangeStart).toHaveReceivedEventDetail({ value: 20 });
|
||||
// Once the knob is released, the end event should fire with
|
||||
// the final value.
|
||||
expect(rangeEnd).toHaveReceivedEventDetail({ value: 100 });
|
||||
|
||||
/**
|
||||
* Verify both events fire if range is clicked without dragging.
|
||||
* Verify both events fire if range is tapped without dragging.
|
||||
*/
|
||||
await dragElementBy(rangeEl, page, 0, 0);
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(rangeStart).toHaveReceivedEventDetail({ value: 50 });
|
||||
await rangeStart.next();
|
||||
await rangeEnd.next();
|
||||
|
||||
// Once the tap is released, the start event should fire with
|
||||
// the initial value.
|
||||
expect(rangeStart).toHaveReceivedEventDetail({ value: 100 });
|
||||
// Once the tap is released, the end event should fire with
|
||||
// the final value.
|
||||
expect(rangeEnd).toHaveReceivedEventDetail({ value: 50 });
|
||||
});
|
||||
|
||||
@@ -99,31 +107,6 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
|
||||
expect(rangeEndSpy.length).toBe(1);
|
||||
});
|
||||
|
||||
// TODO FW-2873
|
||||
test.skip('should not scroll when the knob is swiped', async ({ page, skip }) => {
|
||||
skip.browser('webkit', 'mouse.wheel is not available in WebKit');
|
||||
|
||||
await page.goto(`/src/components/range/test/basic`, config);
|
||||
|
||||
const knobEl = page.locator('ion-range#stacked-range .range-knob-handle');
|
||||
const scrollEl = page.locator('ion-content .inner-scroll');
|
||||
|
||||
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
|
||||
|
||||
await dragElementBy(knobEl, page, 30, 0, undefined, undefined, false);
|
||||
|
||||
/**
|
||||
* Do not use scrollToBottom() or other scrolling methods
|
||||
* on ion-content as those will update the scroll position.
|
||||
* Setting scrollTop still works even with overflow-y: hidden.
|
||||
* However, simulating a user gesture should not scroll the content.
|
||||
*/
|
||||
await page.mouse.wheel(0, 100);
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('ionChange', () => {
|
||||
@@ -151,14 +134,26 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
expect(ionChangeSpy).toHaveReceivedEventTimes(0);
|
||||
});
|
||||
|
||||
// TODO FW-2873
|
||||
test.skip('should emit when the knob is released', async ({ page }) => {
|
||||
await page.setContent(`<ion-range aria-label="range"></ion-range>`, config);
|
||||
test('should emit when the knob is released', async ({ page }) => {
|
||||
/**
|
||||
* Requires padding to prevent the knob from being clipped.
|
||||
* If it's clipped, then the value might be one off.
|
||||
* For example, if the knob is clipped on the right, then the value
|
||||
* will be 99 instead of 100.
|
||||
*/
|
||||
await page.setContent(
|
||||
`
|
||||
<div style="padding: 0 20px">
|
||||
<ion-range aria-label="Range"></ion-range>
|
||||
</div>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const rangeHandle = page.locator('ion-range .range-knob-handle');
|
||||
const rangeEl = page.locator('ion-range');
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
await dragElementBy(rangeHandle, page, 100, 0);
|
||||
await dragElementBy(rangeEl, page, 100);
|
||||
|
||||
await ionChangeSpy.next();
|
||||
|
||||
@@ -196,16 +191,26 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
});
|
||||
|
||||
test.describe('ionInput', () => {
|
||||
// TODO(FW-2873) Enable this test when touch events/gestures are better supported in Playwright
|
||||
test.skip('should emit when the knob is dragged', async ({ page }) => {
|
||||
await page.setContent(`<ion-range aria-label="range"></ion-range>`, config);
|
||||
test('should emit when the knob is dragged', async ({ page }) => {
|
||||
/**
|
||||
* Requires padding to prevent the knob from being clipped.
|
||||
* If it's clipped, then the value might be one off.
|
||||
* For example, if the knob is clipped on the right, then the value
|
||||
* will be 99 instead of 100.
|
||||
*/
|
||||
await page.setContent(
|
||||
`
|
||||
<div style="padding: 0 20px">
|
||||
<ion-range aria-label="range"></ion-range>
|
||||
</div>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const rangeHandle = page.locator('ion-range .range-knob-handle');
|
||||
const rangeEl = page.locator('ion-range');
|
||||
const ionInputSpy = await page.spyOnEvent('ionInput');
|
||||
|
||||
await rangeHandle.hover();
|
||||
|
||||
await dragElementBy(rangeHandle, page, 100, 0, undefined, undefined, false);
|
||||
await dragElementBy(rangeEl, page, 100);
|
||||
|
||||
await ionInputSpy.next();
|
||||
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
import { configs, test, dragElementBy } from '@utils/test/playwright';
|
||||
|
||||
// TODO FW-2873
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe.skip(title('range: scroll-target'), () => {
|
||||
test.describe(title('range: scroll-target'), () => {
|
||||
test('should not scroll when the knob is swiped in custom scroll target', async ({ page, skip }) => {
|
||||
/**
|
||||
* The Playwright team has stated that they will not implement this feature:
|
||||
* https://github.com/microsoft/playwright/issues/28755
|
||||
*/
|
||||
skip.browser('webkit', 'mouse.wheel is not available in WebKit');
|
||||
|
||||
await page.goto(`/src/components/range/test/scroll-target`, config);
|
||||
|
||||
const knobEl = page.locator('ion-range .range-knob-handle');
|
||||
const rangeEl = page.locator('ion-range');
|
||||
const scrollEl = page.locator('.ion-content-scroll-host');
|
||||
|
||||
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
|
||||
|
||||
const box = (await knobEl.boundingBox())!;
|
||||
const centerX = box.x + box.width / 2;
|
||||
const centerY = box.y + box.height / 2;
|
||||
|
||||
await page.mouse.move(centerX, centerY);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(centerX + 30, centerY);
|
||||
await dragElementBy(rangeEl, page, 100, 0, undefined, undefined, false);
|
||||
|
||||
/**
|
||||
* Do not use scrollToBottom() or other scrolling methods
|
||||
|
||||
73
core/src/components/range/test/scroll/index.html
Normal file
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Range - Scroll</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>Range - Scroll</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
|
||||
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
|
||||
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
|
||||
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
|
||||
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.
|
||||
</p>
|
||||
|
||||
<ion-range value="40">
|
||||
<div slot="label">Range Label</div>
|
||||
<ion-label slot="start">Start</ion-label>
|
||||
<ion-label slot="end">End</ion-label>
|
||||
</ion-range>
|
||||
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
|
||||
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
|
||||
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
|
||||
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
|
||||
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
|
||||
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
|
||||
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
|
||||
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
|
||||
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
|
||||
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
|
||||
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
|
||||
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
|
||||
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
|
||||
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
|
||||
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
|
||||
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
|
||||
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.!
|
||||
</p>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
46
core/src/components/range/test/scroll/range.e2e.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, dragElementBy, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('range: scroll'), () => {
|
||||
test('should not scroll when the knob is being dragged', async ({ page, skip }) => {
|
||||
/**
|
||||
* The Playwright team has stated that they will not implement this feature:
|
||||
* https://github.com/microsoft/playwright/issues/28755
|
||||
*/
|
||||
skip.browser('webkit', 'mouse.wheel is not available in WebKit');
|
||||
|
||||
/**
|
||||
* Requires padding to prevent the knob from being clipped.
|
||||
* If it's clipped, then the value might be one off.
|
||||
* For example, if the knob is clipped on the right, then the value
|
||||
* will be 99 instead of 100.
|
||||
*
|
||||
* The ion-content is also required to be taller than the viewport
|
||||
* to allow for scrolling.
|
||||
*/
|
||||
await page.goto(`/src/components/range/test/scroll`, config);
|
||||
|
||||
const rangeEl = page.locator('ion-range');
|
||||
const scrollEl = page.locator('ion-content .inner-scroll');
|
||||
|
||||
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
|
||||
|
||||
await dragElementBy(rangeEl, page, 100, 0, undefined, undefined, false);
|
||||
|
||||
/**
|
||||
* Do not use scrollToBottom() or other scrolling methods
|
||||
* on ion-content as those will update the scroll position.
|
||||
* Setting scrollTop still works even with overflow-y: hidden.
|
||||
* However, simulating a user gesture should not scroll the content.
|
||||
*/
|
||||
await page.mouse.wheel(0, 100);
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
@@ -36,6 +36,11 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
|
||||
@State() checked = false;
|
||||
|
||||
/**
|
||||
* The `id` of the segment content.
|
||||
*/
|
||||
@Prop({ reflect: true }) contentId?: string;
|
||||
|
||||
/**
|
||||
* If `true`, the user cannot interact with the segment button.
|
||||
*/
|
||||
@@ -60,13 +65,72 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
private waitForSegmentContent(ionSegment: HTMLIonSegmentElement | null, contentId: string): Promise<HTMLElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutId: NodeJS.Timeout | undefined = undefined;
|
||||
let animationFrameId: number;
|
||||
|
||||
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();
|
||||
addEventListener(segmentEl, 'ionSelect', this.updateState);
|
||||
addEventListener(segmentEl, 'ionStyle', this.updateStyle);
|
||||
}
|
||||
|
||||
// Return if there is no contentId defined
|
||||
if (!this.contentId) return;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Ensure the found element is a valid ION-SEGMENT-CONTENT
|
||||
if (segmentContent.tagName !== 'ION-SEGMENT-CONTENT') {
|
||||
console.error(`Segment Button: Element with id="${this.contentId}" is not an <ion-segment-content> element.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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() {
|
||||
@@ -161,13 +225,7 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
|
||||
</span>
|
||||
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
|
||||
</button>
|
||||
<div
|
||||
part="indicator"
|
||||
class={{
|
||||
'segment-button-indicator': true,
|
||||
'segment-button-indicator-animated': true,
|
||||
}}
|
||||
>
|
||||
<div part="indicator" class="segment-button-indicator segment-button-indicator-animated">
|
||||
<div part="indicator-background" class="segment-button-indicator-background"></div>
|
||||
</div>
|
||||
</Host>
|
||||
|
||||
11
core/src/components/segment-content/segment-content.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
// Segment Content
|
||||
// --------------------------------------------------
|
||||
|
||||
:host {
|
||||
scroll-snap-align: center;
|
||||
scroll-snap-stop: always;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
17
core/src/components/segment-content/segment-content.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { ComponentInterface } from '@stencil/core';
|
||||
import { Component, Host, h } from '@stencil/core';
|
||||
|
||||
@Component({
|
||||
tag: 'ion-segment-content',
|
||||
styleUrl: 'segment-content.scss',
|
||||
shadow: true,
|
||||
})
|
||||
export class SegmentContent implements ComponentInterface {
|
||||
render() {
|
||||
return (
|
||||
<Host>
|
||||
<slot></slot>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface SegmentViewScrollEvent {
|
||||
scrollRatio: number;
|
||||
isManualScroll: boolean;
|
||||
}
|
||||
9
core/src/components/segment-view/segment-view.ios.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
@import "./segment-view";
|
||||
@import "../segment-button/segment-button.ios.vars";
|
||||
|
||||
// iOS Segment View
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.segment-view-disabled) {
|
||||
opacity: $segment-button-ios-opacity-disabled;
|
||||
}
|
||||
9
core/src/components/segment-view/segment-view.md.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
@import "./segment-view";
|
||||
@import "../segment-button/segment-button.md.vars";
|
||||
|
||||
// Material Design Segment View
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.segment-view-disabled) {
|
||||
opacity: $segment-button-md-opacity-disabled;
|
||||
}
|
||||
31
core/src/components/segment-view/segment-view.scss
Normal file
@@ -0,0 +1,31 @@
|
||||
// Segment View
|
||||
// --------------------------------------------------
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
|
||||
height: 100%;
|
||||
|
||||
overflow-x: scroll;
|
||||
scroll-snap-type: x mandatory;
|
||||
|
||||
/* Hide scrollbar in Firefox */
|
||||
scrollbar-width: none;
|
||||
|
||||
/* Hide scrollbar in IE and Edge */
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar in webkit */
|
||||
:host::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host(.segment-view-disabled) {
|
||||
touch-action: none;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
:host(.segment-view-scroll-disabled) {
|
||||
pointer-events: none;
|
||||
}
|
||||
153
core/src/components/segment-view/segment-view.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import type { ComponentInterface, EventEmitter } 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',
|
||||
styleUrls: {
|
||||
ios: 'segment-view.ios.scss',
|
||||
md: 'segment-view.md.scss',
|
||||
},
|
||||
shadow: true,
|
||||
})
|
||||
export class SegmentView implements ComponentInterface {
|
||||
private scrollEndTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
private isTouching = false;
|
||||
|
||||
@Element() el!: HTMLElement;
|
||||
|
||||
/**
|
||||
* If `true`, the segment view cannot be interacted with.
|
||||
*/
|
||||
@Prop() disabled = false;
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
@State() isManualScroll?: boolean;
|
||||
|
||||
/**
|
||||
* Emitted when the segment view is scrolled.
|
||||
*/
|
||||
@Event() ionSegmentViewScroll!: EventEmitter<SegmentViewScrollEvent>;
|
||||
|
||||
@Listen('scroll')
|
||||
handleScroll(ev: Event) {
|
||||
const { scrollLeft, scrollWidth, clientWidth } = ev.target as HTMLElement;
|
||||
const scrollRatio = scrollLeft / (scrollWidth - clientWidth);
|
||||
|
||||
this.ionSegmentViewScroll.emit({
|
||||
scrollRatio,
|
||||
isManualScroll: this.isManualScroll ?? true,
|
||||
});
|
||||
|
||||
// Reset the timeout to check for scroll end
|
||||
this.resetScrollEndTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle touch start event to know when the user is actively dragging the segment view.
|
||||
*/
|
||||
@Listen('touchstart')
|
||||
handleScrollStart() {
|
||||
if (this.scrollEndTimeout) {
|
||||
clearTimeout(this.scrollEndTimeout);
|
||||
this.scrollEndTimeout = null;
|
||||
}
|
||||
|
||||
this.isTouching = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle touch end event to know when the user is no longer dragging the segment view.
|
||||
*/
|
||||
@Listen('touchend')
|
||||
handleTouchEnd() {
|
||||
this.isTouching = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Method()
|
||||
async setContent(id: string, smoothScroll = true) {
|
||||
const contents = this.getSegmentContents();
|
||||
const index = contents.findIndex((content) => content.id === id);
|
||||
|
||||
if (index === -1) return;
|
||||
|
||||
this.isManualScroll = false;
|
||||
this.resetScrollEndTimeout();
|
||||
|
||||
const contentWidth = this.el.offsetWidth;
|
||||
this.el.scrollTo({
|
||||
top: 0,
|
||||
left: index * contentWidth,
|
||||
behavior: smoothScroll ? 'smooth' : 'instant',
|
||||
});
|
||||
}
|
||||
|
||||
private getSegmentContents(): HTMLIonSegmentContentElement[] {
|
||||
return Array.from(this.el.querySelectorAll('ion-segment-content'));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { disabled, isManualScroll } = this;
|
||||
|
||||
return (
|
||||
<Host
|
||||
class={{
|
||||
'segment-view-disabled': disabled,
|
||||
'segment-view-scroll-disabled': isManualScroll === false,
|
||||
}}
|
||||
>
|
||||
<slot></slot>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
}
|
||||
194
core/src/components/segment-view/test/basic/index.html
Normal file
@@ -0,0 +1,194 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Segment View - Basic</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
|
||||
<style>
|
||||
ion-segment-view {
|
||||
height: 100px;
|
||||
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
ion-segment-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ion-segment-content:nth-of-type(3n + 1) {
|
||||
background: lightpink;
|
||||
}
|
||||
|
||||
ion-segment-content:nth-of-type(3n + 2) {
|
||||
background: lightblue;
|
||||
}
|
||||
|
||||
ion-segment-content:nth-of-type(3n + 3) {
|
||||
background: lightgreen;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Segment View - Basic</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-segment id="noValueSegment">
|
||||
<ion-segment-button content-id="no" value="no">
|
||||
<ion-label>No</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="value" value="value">
|
||||
<ion-label>Value</ion-label>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
<ion-segment-view id="noValueSegmentView">
|
||||
<ion-segment-content id="no">No</ion-segment-content>
|
||||
<ion-segment-content id="value">Value</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 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">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<ion-toolbar>
|
||||
<ion-title>Footer</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
|
||||
<script>
|
||||
function changeSegmentContent() {
|
||||
const segment = document.querySelector('#noValueSegment');
|
||||
const segmentView = document.querySelector('#noValueSegmentView');
|
||||
|
||||
let currentValue = segment.value;
|
||||
|
||||
if (currentValue === 'value') {
|
||||
currentValue = 'no';
|
||||
} else {
|
||||
currentValue = 'value';
|
||||
}
|
||||
|
||||
segment.value = currentValue;
|
||||
}
|
||||
|
||||
async function clearSegmentValue() {
|
||||
const segmentView = document.querySelector('#noValueSegmentView');
|
||||
segmentView.setContent('no', false);
|
||||
|
||||
// Set timeout to ensure the value is cleared after
|
||||
// the segment content is updated
|
||||
setTimeout(() => {
|
||||
const segment = document.querySelector('#noValueSegment');
|
||||
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>
|
||||
</html>
|
||||
173
core/src/components/segment-view/test/basic/segment-view.e2e.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions
|
||||
*/
|
||||
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('segment-view: basic'), () => {
|
||||
test('should show the first content with no initial value', 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 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
|
||||
);
|
||||
|
||||
const segmentContent = page.locator('ion-segment-content[id="paid"]');
|
||||
await expect(segmentContent).toBeInViewport();
|
||||
});
|
||||
|
||||
test('should show the content matching the initial value', 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
|
||||
);
|
||||
|
||||
const segmentContent = page.locator('ion-segment-content[id="free"]');
|
||||
await expect(segmentContent).toBeInViewport();
|
||||
});
|
||||
|
||||
test('should update the content when changing the value by clicking a segment button', 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-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();
|
||||
});
|
||||
});
|
||||
103
core/src/components/segment-view/test/disabled/index.html
Normal file
@@ -0,0 +1,103 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Segment View - Disabled</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
|
||||
<style>
|
||||
ion-segment-view {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
ion-segment-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ion-segment-content:nth-of-type(1) {
|
||||
background: lightpink;
|
||||
}
|
||||
|
||||
ion-segment-content:nth-of-type(2) {
|
||||
background: lightblue;
|
||||
}
|
||||
|
||||
ion-segment-content:nth-of-type(3) {
|
||||
background: lightgreen;
|
||||
}
|
||||
|
||||
ion-segment-content:nth-of-type(4) {
|
||||
background: lightgoldenrodyellow;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Segment View - Disabled</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-segment>
|
||||
<ion-segment-button disabled 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 disabled value="paid">
|
||||
<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>
|
||||
|
||||
<ion-segment value="reading-list">
|
||||
<ion-segment-button content-id="bookmarks" value="bookmarks">
|
||||
<ion-label>Bookmarks</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="reading-list" value="reading-list">
|
||||
<ion-label>Reading List</ion-label>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button content-id="shared-links" value="shared-links">
|
||||
<ion-label>Shared Links</ion-label>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
<ion-segment-view disabled>
|
||||
<ion-segment-content id="bookmarks">Bookmarks</ion-segment-content>
|
||||
<ion-segment-content id="reading-list">Reading List</ion-segment-content>
|
||||
<ion-segment-content id="shared-links">Shared Links</ion-segment-content>
|
||||
</ion-segment-view>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,49 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across directions
|
||||
*/
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('segment-view: disabled'), () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.goto('/src/components/segment-view/test/disabled', config);
|
||||
|
||||
await expect(page).toHaveScreenshot(screenshot(`segment-view-disabled`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions
|
||||
*/
|
||||
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('segment-view: disabled'), () => {
|
||||
test('should keep button enabled even when disabled prop is set', 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 disabled 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 segmentButton = page.locator('ion-segment-button[value="free"]');
|
||||
await expect(segmentButton).not.toHaveClass(/segment-button-disabled/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -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';
|
||||
|
||||
@@ -27,6 +28,16 @@ export class Segment implements ComponentInterface {
|
||||
// Value before the segment is dragged
|
||||
private valueBeforeGesture?: SegmentValue;
|
||||
|
||||
private segmentViewEl?: HTMLIonSegmentViewElement | null = null;
|
||||
private lastNextIndex?: number;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@@ -78,13 +89,41 @@ export class Segment implements ComponentInterface {
|
||||
@Prop({ mutable: true }) value?: SegmentValue;
|
||||
|
||||
@Watch('value')
|
||||
protected valueChanged(value: SegmentValue | undefined) {
|
||||
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);
|
||||
const current = buttons.find((button) => button.value === value);
|
||||
|
||||
if (previous && current) {
|
||||
if (!this.segmentViewEl) {
|
||||
this.checkButton(previous, current);
|
||||
} else if (this.triggerScrollOnValueChange !== false) {
|
||||
this.updateSegmentView();
|
||||
}
|
||||
}
|
||||
} else if (value !== undefined && oldValue === undefined && this.segmentViewEl) {
|
||||
this.updateSegmentView();
|
||||
}
|
||||
|
||||
/**
|
||||
* `ionSelect` is emitted every time the value changes (internal or external changes).
|
||||
* Used by `ion-segment-button` to determine if the button should be checked.
|
||||
*/
|
||||
this.ionSelect.emit({ value });
|
||||
this.scrollActiveButtonIntoView();
|
||||
|
||||
// The scroll listener should handle scrolling the active button into view as needed
|
||||
if (!this.segmentViewEl) {
|
||||
this.scrollActiveButtonIntoView();
|
||||
}
|
||||
|
||||
this.triggerScrollOnValueChange = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +175,12 @@ export class Segment implements ComponentInterface {
|
||||
|
||||
connectedCallback() {
|
||||
this.emitStyle();
|
||||
|
||||
this.segmentViewEl = this.getSegmentView();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.segmentViewEl = null;
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
@@ -139,6 +188,8 @@ export class Segment implements ComponentInterface {
|
||||
}
|
||||
|
||||
async componentDidLoad() {
|
||||
this.segmentViewEl = this.getSegmentView();
|
||||
|
||||
this.setCheckedClasses();
|
||||
|
||||
/**
|
||||
@@ -170,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) {
|
||||
@@ -192,6 +247,7 @@ export class Segment implements ComponentInterface {
|
||||
if (value !== undefined) {
|
||||
if (this.valueBeforeGesture !== value) {
|
||||
this.emitValueChange();
|
||||
this.updateSegmentView();
|
||||
}
|
||||
}
|
||||
this.valueBeforeGesture = undefined;
|
||||
@@ -208,7 +264,7 @@ export class Segment implements ComponentInterface {
|
||||
this.ionChange.emit({ value });
|
||||
}
|
||||
|
||||
private getButtons() {
|
||||
private getButtons(): HTMLIonSegmentButtonElement[] {
|
||||
return Array.from(this.el.querySelectorAll('ion-segment-button'));
|
||||
}
|
||||
|
||||
@@ -224,11 +280,7 @@ export class Segment implements ComponentInterface {
|
||||
const buttons = this.getButtons();
|
||||
|
||||
buttons.forEach((button) => {
|
||||
if (activated) {
|
||||
button.classList.add('segment-button-activated');
|
||||
} else {
|
||||
button.classList.remove('segment-button-activated');
|
||||
}
|
||||
button.classList.toggle('segment-button-activated', activated);
|
||||
});
|
||||
this.activated = activated;
|
||||
}
|
||||
@@ -293,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;
|
||||
@@ -312,6 +366,74 @@ export class Segment implements ComponentInterface {
|
||||
}
|
||||
}
|
||||
|
||||
private getSegmentView() {
|
||||
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<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;
|
||||
|
||||
// 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();
|
||||
|
||||
// If no buttons are found or there is no value set then do nothing
|
||||
if (!buttons.length) return;
|
||||
|
||||
const index = buttons.findIndex((button) => button.value === this.value);
|
||||
const current = buttons[index];
|
||||
|
||||
const nextIndex = Math.round(scrollRatio * (buttons.length - 1));
|
||||
|
||||
if (this.lastNextIndex === undefined || this.lastNextIndex !== nextIndex) {
|
||||
this.lastNextIndex = nextIndex;
|
||||
this.triggerScrollOnValueChange = false;
|
||||
|
||||
this.checkButton(current, buttons[nextIndex]);
|
||||
this.emitValueChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the related segment view and sets its current content
|
||||
* based on the selected segment button. This method
|
||||
* should be called on initial load of the segment,
|
||||
* after the gesture is completed (if dragging between segments)
|
||||
* and when a segment button is clicked directly.
|
||||
*/
|
||||
private updateSegmentView(smoothScroll = true) {
|
||||
const buttons = this.getButtons();
|
||||
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
|
||||
if (!button?.contentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const segmentView = this.segmentViewEl;
|
||||
|
||||
if (segmentView) {
|
||||
segmentView.setContent(button.contentId, smoothScroll);
|
||||
}
|
||||
}
|
||||
|
||||
private scrollActiveButtonIntoView(smoothScroll = true) {
|
||||
const { scrollable, value, el } = this;
|
||||
|
||||
@@ -342,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',
|
||||
});
|
||||
}
|
||||
@@ -478,7 +614,13 @@ export class Segment implements ComponentInterface {
|
||||
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 {
|
||||
|
||||
@@ -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: "";
|
||||
}
|
||||