Compare commits

..

63 Commits

Author SHA1 Message Date
ShaneK
8899f9b4a7 fix(capacitor): reference todo ticket, fix boolean conversion 2025-02-28 11:38:36 -08:00
ShaneK
a9c20af0b7 fix(capacitor): fixing capacitor 2 support 2025-02-28 11:26:48 -08:00
ShaneK
e05865c694 fix(test): fixing mock for native platform check to support new capacitor way of checking 2025-02-28 10:56:56 -08:00
r-yanyo
cbd3a9f848 replace deprecated method 2025-02-17 15:19:46 +09:00
Pedro Lourenço
14b6538d98 fix(segment-button): protect connectedCallback for when segment-content has not yet been created (cherry-pick) (#30138)
Issue number: internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->
When the `connectedCallback` method is called for a segment-button and
its corresponding segment-content has not been created in that instant,
a console error is thrown and the method returns.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->
- `connectedCallback` will now wait, at most 1 second, for the
corresponding segment-content to be created.
- The new behaviour can be tested in segment-view/basic.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See

https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2025-02-10 18:09:25 +00:00
Brandy Smith
295fa00527 merge release-8.4.3 (#30166)
v8.4.3
2025-01-29 14:10:21 -05:00
ionitron
353159149a chore(): update package lock files 2025-01-29 18:57:36 +00:00
ionitron
87bde81a94 v8.4.3 2025-01-29 18:56:48 +00:00
Christian Bromann
eb725fce6e fix(vue): update Stencil Vue output target (#30159)
This patch includes some necessary updates for
`@stencil/vue-output-target@v0.9.0`:

- we started to export Stencils helpers as runtime via
`@stencil/vue-output-target/runtime` similar to what we did in React
- this version requires some updates to Vue and TypeScript as well
- adjustments related to that update
2025-01-29 16:31:31 +00:00
Brandy Smith
0030be8195 merge release-8.4.2 (#30150)
v8.4.2
2025-01-22 16:49:46 -05:00
ionitron
c2bc756ffc chore(): update package lock files 2025-01-22 21:34:04 +00:00
ionitron
f532a5d4b7 v8.4.2 2025-01-22 21:33:23 +00:00
renovate[bot]
b71f2e9189 chore(deps): update stencil (#29823)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@stencil/angular-output-target](https://stenciljs.com/)
([source](https://redirect.github.com/ionic-team/stencil-ds-output-targets))
| [`^0.8.4` ->
`^0.10.0`](https://renovatebot.com/diffs/npm/@stencil%2fangular-output-target/0.8.4/0.10.2)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@stencil%2fangular-output-target/0.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@stencil%2fangular-output-target/0.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@stencil%2fangular-output-target/0.8.4/0.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@stencil%2fangular-output-target/0.8.4/0.10.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [@stencil/vue-output-target](https://stenciljs.com/)
([source](https://redirect.github.com/ionic-team/stencil-ds-output-targets))
| [`^0.8.9` ->
`^0.9.0`](https://renovatebot.com/diffs/npm/@stencil%2fvue-output-target/0.8.9/0.9.2)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@stencil%2fvue-output-target/0.9.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@stencil%2fvue-output-target/0.9.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@stencil%2fvue-output-target/0.8.9/0.9.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@stencil%2fvue-output-target/0.8.9/0.9.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>ionic-team/stencil-ds-output-targets
(@&#8203;stencil/angular-output-target)</summary>

###
[`v0.10.2`](a3588e9051...a3588e9051)

[Compare
Source](a3588e9051...a3588e9051)

###
[`v0.10.1`](a3588e9051...a3588e9051)

[Compare
Source](a3588e9051...a3588e9051)

###
[`v0.10.0`](a3588e9051...a3588e9051)

[Compare
Source](a3588e9051...a3588e9051)

###
[`v0.9.1`](https://redirect.github.com/ionic-team/stencil-ds-output-targets/compare/@stencil/angular-output-target@0.9.0...a3588e905186a0e86e7f88418fd5b2f9531b55e0)

[Compare
Source](https://redirect.github.com/ionic-team/stencil-ds-output-targets/compare/@stencil/angular-output-target@0.9.0...a3588e905186a0e86e7f88418fd5b2f9531b55e0)

###
[`v0.9.0`](https://redirect.github.com/ionic-team/stencil-ds-output-targets/releases/tag/%40stencil/angular-output-target%400.9.0)

[Compare
Source](https://redirect.github.com/ionic-team/stencil-ds-output-targets/compare/@stencil/angular-output-target@0.8.4...@stencil/angular-output-target@0.9.0)

#### What's Changed

- feat(angular): Standalone Value Accessor for Angular OutputType by
[@&#8203;Samg983](https://redirect.github.com/Samg983) in
[https://github.com/ionic-team/stencil-ds-output-targets/pull/459](https://redirect.github.com/ionic-team/stencil-ds-output-targets/pull/459)

#### New Contributors

- [@&#8203;Samg983](https://redirect.github.com/Samg983) made their
first contribution in
[https://github.com/ionic-team/stencil-ds-output-targets/pull/459](https://redirect.github.com/ionic-team/stencil-ds-output-targets/pull/459)

**Full Changelog**:
https://github.com/ionic-team/stencil-ds-output-targets/compare/[@&#8203;stencil/react-output-target](https://redirect.github.com/stencil/react-output-target)[@&#8203;0](https://redirect.github.com/0).7.0...[@&#8203;stencil/angular-output-target](https://redirect.github.com/stencil/angular-output-target)[@&#8203;0](https://redirect.github.com/0).9.0

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "every weekday before 11am" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/ionic-team/ionic-framework).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC41Ni4wIiwidXBkYXRlZEluVmVyIjoiMzkuOTIuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-17 18:03:21 +00:00
renovate[bot]
709a816615 chore(deps): update dependency @clack/prompts to v0.9.1 (#30131)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[@clack/prompts](https://redirect.github.com/natemoo-re/clack/tree/main/packages/prompts#readme)
([source](https://redirect.github.com/natemoo-re/clack/tree/HEAD/packages/prompts))
| [`0.9.0` ->
`0.9.1`](https://renovatebot.com/diffs/npm/@clack%2fprompts/0.9.0/0.9.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@clack%2fprompts/0.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@clack%2fprompts/0.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@clack%2fprompts/0.9.0/0.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@clack%2fprompts/0.9.0/0.9.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>natemoo-re/clack (@&#8203;clack/prompts)</summary>

###
[`v0.9.1`](https://redirect.github.com/natemoo-re/clack/blob/HEAD/packages/prompts/CHANGELOG.md#091)

[Compare
Source](https://redirect.github.com/natemoo-re/clack/compare/@clack/prompts@0.9.0...@clack/prompts@0.9.1)

##### Patch Changes

-
[`8093f3c`](https://redirect.github.com/natemoo-re/clack/commit/8093f3c):
Adds `Error` support to the `validate` function
-
[`98925e3`](https://redirect.github.com/natemoo-re/clack/commit/98925e3):
Exports the `Option` type and improves JSDocannotations
-
[`1904e57`](https://redirect.github.com/natemoo-re/clack/commit/1904e57):
Replace custom utility for stripping ANSI control sequences with Node's
built-in
[`stripVTControlCharacters`](https://nodejs.org/docs/latest/api/util.html#utilstripvtcontrolcharactersstr)
utility.
- Updated dependencies
\[[`8093f3c`](https://redirect.github.com/natemoo-re/clack/commit/8093f3c)]
- Updated dependencies
\[[`e5ba09a`](https://redirect.github.com/natemoo-re/clack/commit/e5ba09a)]
- Updated dependencies
\[[`8cba8e3`](https://redirect.github.com/natemoo-re/clack/commit/8cba8e3)]
-
[@&#8203;clack/core](https://redirect.github.com/clack/core)[@&#8203;0](https://redirect.github.com/0).4.1

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "every weekday before 11am" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/ionic-team/ionic-framework).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS45Mi4wIiwidXBkYXRlZEluVmVyIjoiMzkuOTIuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-17 15:38:41 +00:00
renovate[bot]
e63028ee53 chore(deps): update dependency chalk to v5.4.1 (#30120)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [chalk](https://redirect.github.com/chalk/chalk) | [`5.4.0` ->
`5.4.1`](https://renovatebot.com/diffs/npm/chalk/5.4.0/5.4.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/chalk/5.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/chalk/5.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/chalk/5.4.0/5.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/chalk/5.4.0/5.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>chalk/chalk (chalk)</summary>

###
[`v5.4.1`](https://redirect.github.com/chalk/chalk/releases/tag/v5.4.1)

[Compare
Source](https://redirect.github.com/chalk/chalk/compare/v5.4.0...v5.4.1)

- Fix `navigator` not defined `ReferenceError`
([#&#8203;642](https://redirect.github.com/chalk/chalk/issues/642))
[`4ebb62d`](https://redirect.github.com/chalk/chalk/commit/4ebb62d)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "every weekday before 11am" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/ionic-team/ionic-framework).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS45Mi4wIiwidXBkYXRlZEluVmVyIjoiMzkuOTIuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-17 15:37:50 +00:00
renovate[bot]
bd266f09ef chore(deps): update pozil/auto-assign-issue action to v2.1.2 (#30121)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[pozil/auto-assign-issue](https://redirect.github.com/pozil/auto-assign-issue)
| action | patch | `v2.1.1` -> `v2.1.2` |

---

### Release Notes

<details>
<summary>pozil/auto-assign-issue (pozil/auto-assign-issue)</summary>

###
[`v2.1.2`](https://redirect.github.com/pozil/auto-assign-issue/releases/tag/v2.1.2):
- Fix failsIfUsersCannotBeAssigned flag support

[Compare
Source](https://redirect.github.com/pozil/auto-assign-issue/compare/v2.1.1...v2.1.2)

- fix: failsIfUsersCannotBeAssigned flag support and improves error
handling

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "every weekday before 11am" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/ionic-team/ionic-framework).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS45Mi4wIiwidXBkYXRlZEluVmVyIjoiMzkuOTIuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-17 15:37:38 +00:00
Maria Hutt
3f8346e718 fix(select-modal): match radio styles to iOS native (#30119)
Issue number: internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

When the `select-modal` displays radios, a bottom border is shown under
the radio icon and text. However, native iOS does not have the border
under the icon.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- The bottom border is displayed under the radio text only.
- The `ion-item` within `select-modal` has been given the prop of
`lines="none"` since border styling has been done through
`select-md.ios.scss` and because `md` doesn't use it.
- Updated snapshots

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

[Preview
(iOS)](https://ionic-framework-git-rou-11404-ionic1.vercel.app/src/components/select/test/basic?ionic%3Amode=ios)
[Preview
(md)](https://ionic-framework-git-rou-11404-ionic1.vercel.app/src/components/select/test/basic)
2025-01-09 18:32:56 +00:00
renovate[bot]
05928e3877 chore(deps): update capacitor to v6.2.0 (#30039)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@capacitor/core](https://capacitorjs.com)
([source](https://redirect.github.com/ionic-team/capacitor)) | [`6.1.2`
->
`6.2.0`](https://renovatebot.com/diffs/npm/@capacitor%2fcore/6.1.2/6.2.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@capacitor%2fcore/6.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@capacitor%2fcore/6.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@capacitor%2fcore/6.1.2/6.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@capacitor%2fcore/6.1.2/6.2.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[@capacitor/haptics](https://redirect.github.com/ionic-team/capacitor-plugins)
| [`6.0.1` ->
`6.0.2`](https://renovatebot.com/diffs/npm/@capacitor%2fhaptics/6.0.1/6.0.2)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@capacitor%2fhaptics/6.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@capacitor%2fhaptics/6.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@capacitor%2fhaptics/6.0.1/6.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@capacitor%2fhaptics/6.0.1/6.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[@capacitor/keyboard](https://redirect.github.com/ionic-team/capacitor-plugins)
| [`6.0.2` ->
`6.0.3`](https://renovatebot.com/diffs/npm/@capacitor%2fkeyboard/6.0.2/6.0.3)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@capacitor%2fkeyboard/6.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@capacitor%2fkeyboard/6.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@capacitor%2fkeyboard/6.0.2/6.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@capacitor%2fkeyboard/6.0.2/6.0.3?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[@capacitor/status-bar](https://redirect.github.com/ionic-team/capacitor-plugins)
| [`6.0.1` ->
`6.0.2`](https://renovatebot.com/diffs/npm/@capacitor%2fstatus-bar/6.0.1/6.0.2)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@capacitor%2fstatus-bar/6.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@capacitor%2fstatus-bar/6.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@capacitor%2fstatus-bar/6.0.1/6.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@capacitor%2fstatus-bar/6.0.1/6.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>ionic-team/capacitor (@&#8203;capacitor/core)</summary>

###
[`v6.2.0`](https://redirect.github.com/ionic-team/capacitor/releases/tag/6.2.0)

[Compare
Source](https://redirect.github.com/ionic-team/capacitor/compare/6.1.2...6.2.0)

##### Bug Fixes

- **cli:** make Cordova plugins use same default kotlin version as
Capacitor
([#&#8203;7756](https://redirect.github.com/ionic-team/capacitor/issues/7756))
([96dde8c](96dde8c33d))
- **cli:** replace app-store deprecated method on build
([#&#8203;7637](https://redirect.github.com/ionic-team/capacitor/issues/7637))
([942b108](942b108c1d))
- **ios:** fix retain cycle caused by CDVPluginManager
([#&#8203;7692](https://redirect.github.com/ionic-team/capacitor/issues/7692))
([#&#8203;7694](https://redirect.github.com/ionic-team/capacitor/issues/7694))
([dd068fe](dd068fe6b7))
- use Capacitor 6 for SPM dependency
([#&#8203;7737](https://redirect.github.com/ionic-team/capacitor/issues/7737))
([8e55ca5](8e55ca5817))

##### Features

- **core:** cherrypick: expose `methodName` via `CAPPluginCall`
([#&#8203;7641](https://redirect.github.com/ionic-team/capacitor/issues/7641))
([#&#8203;7684](https://redirect.github.com/ionic-team/capacitor/issues/7684))
([bf6ef8e](bf6ef8e147))
- **ios:** cherry-pick - JSValueEncoder/Decoder feature parity with
JSONEncoder/Decoder
([#&#8203;7657](https://redirect.github.com/ionic-team/capacitor/issues/7657))
([ce30924](ce30924da5))

</details>

<details>
<summary>ionic-team/capacitor-plugins
(@&#8203;capacitor/haptics)</summary>

###
[`v6.0.2`](https://redirect.github.com/ionic-team/capacitor-plugins/releases/tag/%40capacitor/haptics%406.0.2)

[Compare
Source](https://redirect.github.com/ionic-team/capacitor-plugins/compare/@capacitor/haptics@6.0.1...@capacitor/haptics@6.0.2)

**Note:** Version bump only for package
[@&#8203;capacitor/haptics](https://redirect.github.com/capacitor/haptics)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "every weekday before 11am" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/ionic-team/ionic-framework).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xOS4wIiwidXBkYXRlZEluVmVyIjoiMzkuMTkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 19:27:32 +00:00
renovate[bot]
64c1373f53 chore(deps): update dependency @clack/prompts to ^0.9.0 (#30098)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[@clack/prompts](https://redirect.github.com/natemoo-re/clack/tree/main/packages/prompts#readme)
([source](https://redirect.github.com/natemoo-re/clack/tree/HEAD/packages/prompts))
| [`^0.8.0` ->
`^0.9.0`](https://renovatebot.com/diffs/npm/@clack%2fprompts/0.8.1/0.9.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@clack%2fprompts/0.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@clack%2fprompts/0.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@clack%2fprompts/0.8.1/0.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@clack%2fprompts/0.8.1/0.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>natemoo-re/clack (@&#8203;clack/prompts)</summary>

###
[`v0.9.0`](https://redirect.github.com/natemoo-re/clack/blob/HEAD/packages/prompts/CHANGELOG.md#090)

[Compare
Source](https://redirect.github.com/natemoo-re/clack/compare/@clack/prompts@0.8.2...@clack/prompts@0.9.0)

##### Minor Changes

-
[`a83d2f8`](https://redirect.github.com/natemoo-re/clack/commit/a83d2f8):
Adds a new `updateSettings()` function to support new global
keybindings.

`updateSettings()` accepts an `aliases` object that maps custom keys to
an action (`up | down | left | right | space | enter | cancel`).

    ```ts
    import { updateSettings } from "@&#8203;clack/prompts";

    // Support custom keybindings
    updateSettings({
      aliases: {
        w: "up",
        a: "left",
        s: "down",
        d: "right",
      },
    });
    ```

> \[!WARNING]
> In order to enforce consistent, user-friendly defaults across the
ecosystem, `updateSettings` does not support disabling Clack's default
keybindings.

-
[`801246b`](https://redirect.github.com/natemoo-re/clack/commit/801246b):
Adds a new `signal` option to support programmatic prompt cancellation
with an [abort
controller](https://kettanaito.com/blog/dont-sleep-on-abort-controller).

One example use case is automatically cancelling a prompt after a
timeout.

    ```ts
    const shouldContinue = await confirm({
      message: "This message will self destruct in 5 seconds",
      signal: AbortSignal.timeout(5000),
    });
    ```

    Another use case is racing a long running task with a manual prompt.

    ```ts
    const abortController = new AbortController();

    const projectType = await Promise.race([
      detectProjectType({
        signal: abortController.signal,
      }),
      select({
        message: "Pick a project type.",
        options: [
          { value: "ts", label: "TypeScript" },
          { value: "js", label: "JavaScript" },
          { value: "coffee", label: "CoffeeScript", hint: "oh no" },
        ],
        signal: abortController.signal,
      }),
    ]);

    abortController.abort();
    ```

-
[`a83d2f8`](https://redirect.github.com/natemoo-re/clack/commit/a83d2f8):
Updates default keybindings to support Vim motion shortcuts and map the
`escape` key to cancel (`ctrl+c`).

    | alias | action |
    | ----- | ------ |
    | `k`   | up     |
    | `l`   | right  |
    | `j`   | down   |
    | `h`   | left   |
    | `esc` | cancel |

##### Patch Changes

-
[`f9f139d`](https://redirect.github.com/natemoo-re/clack/commit/f9f139d):
Adapts `spinner` output for static CI environments
- Updated dependencies
\[[`a83d2f8`](https://redirect.github.com/natemoo-re/clack/commit/a83d2f8)]
- Updated dependencies
\[[`801246b`](https://redirect.github.com/natemoo-re/clack/commit/801246b)]
- Updated dependencies
\[[`a83d2f8`](https://redirect.github.com/natemoo-re/clack/commit/a83d2f8)]
- Updated dependencies
\[[`51e12bc`](https://redirect.github.com/natemoo-re/clack/commit/51e12bc)]
-
[@&#8203;clack/core](https://redirect.github.com/clack/core)[@&#8203;0](https://redirect.github.com/0).4.0

###
[`v0.8.2`](https://redirect.github.com/natemoo-re/clack/blob/HEAD/packages/prompts/CHANGELOG.md#082)

[Compare
Source](https://redirect.github.com/natemoo-re/clack/compare/@clack/prompts@0.8.1...@clack/prompts@0.8.2)

##### Patch Changes

- Updated dependencies
\[[`4845f4f`](https://redirect.github.com/natemoo-re/clack/commit/4845f4f)]
- Updated dependencies
\[[`d7b2fb9`](https://redirect.github.com/natemoo-re/clack/commit/d7b2fb9)]
-
[@&#8203;clack/core](https://redirect.github.com/clack/core)[@&#8203;0](https://redirect.github.com/0).3.5

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "every weekday before 11am" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/ionic-team/ionic-framework).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS44MC4wIiwidXBkYXRlZEluVmVyIjoiMzkuODAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 19:27:28 +00:00
renovate[bot]
01917ee0ce chore(deps): update dependency chalk to v5.4.0 (#30099)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [chalk](https://redirect.github.com/chalk/chalk) | [`5.3.0` ->
`5.4.0`](https://renovatebot.com/diffs/npm/chalk/5.3.0/5.4.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/chalk/5.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/chalk/5.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/chalk/5.3.0/5.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/chalk/5.3.0/5.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>chalk/chalk (chalk)</summary>

###
[`v5.4.0`](https://redirect.github.com/chalk/chalk/releases/tag/v5.4.0)

[Compare
Source](https://redirect.github.com/chalk/chalk/compare/v5.3.0...v5.4.0)

- Update `CIRCLECI` environments to return level 3 color support
[`f838120`](https://redirect.github.com/chalk/chalk/commit/f838120)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "every weekday before 11am" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/ionic-team/ionic-framework).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS44MC4wIiwidXBkYXRlZEluVmVyIjoiMzkuODAuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 19:27:24 +00:00
renovate[bot]
cdfb4f37ad chore(deps): update pozil/auto-assign-issue action to v2.1.1 (#30110)
This PR contains the following updates:

| Package | Type | Update | Change | Pending |
|---|---|---|---|---|
|
[pozil/auto-assign-issue](https://redirect.github.com/pozil/auto-assign-issue)
| action | minor | `v2.0.1` -> `v2.1.1` | `v2.1.2` |

---

### Release Notes

<details>
<summary>pozil/auto-assign-issue (pozil/auto-assign-issue)</summary>

###
[`v2.1.1`](https://redirect.github.com/pozil/auto-assign-issue/compare/v2.1.0...v2.1.1)

[Compare
Source](https://redirect.github.com/pozil/auto-assign-issue/compare/v2.1.0...v2.1.1)

###
[`v2.1.0`](https://redirect.github.com/pozil/auto-assign-issue/releases/tag/v2.1.0):
- User assignment check

[Compare
Source](https://redirect.github.com/pozil/auto-assign-issue/compare/v2.0.1...v2.1.0)

- feat: added an optional user assignment check via the
`failsIfUsersCannotBeAssigned` flag as per
[#&#8203;148](https://redirect.github.com/pozil/auto-assign-issue/issues/148)
-   build: bump dependencies

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "every weekday before 11am" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/ionic-team/ionic-framework).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS44NS4wIiwidXBkYXRlZEluVmVyIjoiMzkuODUuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 19:27:08 +00:00
Maria Hutt
1b11b82eaa chore(ci): use node v20 for github actions (#30088)
Issue number: N/A

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Github actions are failing due to using node v18.

[Failing
example](https://github.com/ionic-team/ionic-framework/actions/runs/12379078209/job/34552448830)

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Github actions have been updated to use node v20.

[Passing
example](https://github.com/ionic-team/ionic-framework/actions/runs/12379780879/job/34554955539)

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

N/A
2024-12-17 20:46:55 +00:00
Brandy Carney
e101f2e022 test(angular): add ng19 test app (#30041)
Issue number: internal

---------

## What is the current behavior?
There are tests apps for Angular 16, 17 and 18

## What is the new behavior?
Adds a test app for Angular 19

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

---------

Co-authored-by: Brandy Carney <6577830+brandyscarney@users.noreply.github.com>
2024-12-04 17:02:06 +00:00
Tanner Reits
000f55303e fix(segment): add logic to connect to segment-view in componentDidLoad() callback (#30060)
Issue number: resolves #30000 

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

The "swipeable segments" feature does not work correctly in an Angular
environment (tested with both standalone and module architecture). The
issues is that the `ion-segment-view` element is not correctly
"attached" to the segment since it does not exist at the time the
`connectedCallback()` is first executed.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

Added the logic to connect the `ion-segment-view` to the
`componentDidLoad()` callback in addition to the `connectedCallback()`.
The existing logic was left in place for the case where the element is
removed and reattached to the DOM.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev Build: `8.4.2-dev.11733239325.140ef7c3`
2024-12-04 15:16:15 +00:00
renovate[bot]
6d0b4297dc chore(deps): update pozil/auto-assign-issue action to v2.0.1 (#30038)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[pozil/auto-assign-issue](https://redirect.github.com/pozil/auto-assign-issue)
| action | patch | `v2.0.0` -> `v2.0.1` |

---

### Release Notes

<details>
<summary>pozil/auto-assign-issue (pozil/auto-assign-issue)</summary>

###
[`v2.0.1`](https://redirect.github.com/pozil/auto-assign-issue/releases/tag/v2.0.1):
- Bump dependencies

[Compare
Source](https://redirect.github.com/pozil/auto-assign-issue/compare/v2.0.0...v2.0.1)

-   build: bump dependencies
-   feat: extra test for random PR reviewer

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "every weekday before 11am" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/ionic-team/ionic-framework).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xOS4wIiwidXBkYXRlZEluVmVyIjoiMzkuMTkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-02 14:39:10 +00:00
renovate[bot]
270526e4f2 chore(deps): update dependency @clack/prompts to ^0.8.0 (#30021)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[@clack/prompts](https://redirect.github.com/natemoo-re/clack/tree/main/packages/prompts#readme)
([source](https://redirect.github.com/natemoo-re/clack/tree/HEAD/packages/prompts))
| [`^0.7.0` ->
`^0.8.0`](https://renovatebot.com/diffs/npm/@clack%2fprompts/0.7.0/0.8.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@clack%2fprompts/0.8.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@clack%2fprompts/0.8.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@clack%2fprompts/0.7.0/0.8.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@clack%2fprompts/0.7.0/0.8.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>natemoo-re/clack (@&#8203;clack/prompts)</summary>

###
[`v0.8.1`](https://redirect.github.com/natemoo-re/clack/blob/HEAD/packages/prompts/CHANGELOG.md#081)

[Compare
Source](https://redirect.github.com/natemoo-re/clack/compare/@clack/prompts@0.7.0...@clack/prompts@0.8.1)

##### Patch Changes

-
[`360afeb`](https://redirect.github.com/natemoo-re/clack/commit/360afeb):
feat: adaptative max items

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "every weekday before 11am" (UTC),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/ionic-team/ionic-framework).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xOS4wIiwidXBkYXRlZEluVmVyIjoiMzkuMTkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-02 14:38:48 +00:00
Brandy Carney
234d14a32d merge release-8.4.1 (#30048)
v8.4.1
2024-11-27 13:44:41 -05:00
ionitron
a90097cdb1 chore(): update package lock files 2024-11-27 18:27:30 +00:00
ionitron
1c281dc4ee v8.4.1 2024-11-27 18:26:53 +00:00
Maria Hutt
845071c97a fix(menu): hide from screen readers while animating (#30036)
Issue number: internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

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.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- When an overlay is being animated (presenting or dismissing), the
overlay will hide from screen readers. This allows Talkback to display
the focus rings on the correct position.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev build: `8.4.1-dev.11732305980.19d90e1c`

Related to https://github.com/ionic-team/ionic-framework/pull/29951
2024-11-27 16:27:57 +00:00
Maria Hutt
f6188c47e9 fix(overlays): announce info after opening based on platform (#30025)
Issue number: internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

PR https://github.com/ionic-team/ionic-framework/pull/29951 would hide
the overlays from screen readers while animating. This allows the
element to navigate to its correct destination for screen readers to
interact with. Otherwise, the focus rings would never appear. However,
this ended up breaking the interaction for iOS.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Overlays are hidden from screen readers while animating only if the
platform is `android`. Since the original issue only applied to Android
devices.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev build: `8.4.1-dev.11732033492.170e160c`

Test on iOS and Android devices.
2024-11-22 17:20:22 +00:00
Tanner Reits
8ee42bbc1e fix(overlays): focus management with checkbox/radio (#30026)
Issue number: resolves internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Using `Tab` or `Shift + Tab` to focus through elements in a modal won't
behave as expected when using `ion-checkbox` or `ion-radio` within an
`ion-item`. Previously, the behavior would result in the last item in a
list getting focus styling, but `document.activeElement` would still be
the first actionable item in the overlay

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

For checkboxes, the `ion-checkbox` element itself will be focused rather
than the encapsulating `ion-item`

For radios, the `ion-radio-group` will be used to focus the appropriate
element. This will be the first `ion-radio` if there is no "checked"
item, or the "checked" item if one exists.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2024-11-21 22:28:22 +00:00
Maria Hutt
23763abf79 fix(header): use aria attributes to hide small title when collapsed (#30027)
Issue number: resolves #29347 

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Focusable elements like buttons cannot be accessed within the
`ion-header` when it's collapsed. They're only accessible once the small
title is displayed.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Moved the `aria-hidden` from the header to `ion-title`, this aligns
with native.
- Updated existing test.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev build: `8.4.1-dev.11732064156.12837790`
2024-11-21 18:57:25 +00:00
Alexander Harding
470decca7b fix(toast): swipe gesture works with custom container layout (#29999)
Issue number: resolves #29998

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?

Applying a custom layout to `ion-toast::part(container)`, for example
`width: 50%`, will make the part of the toast outside this element's
bounds non-interactive for swiping gestures.


## What is the new behavior?

Can swipe from anywhere on the toast with custom layout applied to
::part(container)

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

| Before | After                                         |
|-------|-----------------------------------------------------|
| <video
src="https://github.com/user-attachments/assets/fc450066-5fb1-4fd9-bfbd-7f2cd55ce855"></video>
| <video
src="https://github.com/user-attachments/assets/991f6a42-f6fe-479b-9f68-7e7e35dca799"></video>
|
2024-11-14 16:39:02 +00:00
Maria Hutt
3216108ca1 test(segment): fix flaky gesture test and re-enable (#30008)
Issue number: internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

There's a flaky test that was disabled for segment. It's been known that
gesture tests are prone to be flaky.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Fixed the test by switching to the improved `dragElementBy` function

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

N/A
2024-11-08 22:54:38 +00:00
Tanner Reits
4bffe976d9 merge release-8.4.0 (#29995)
v8.4.0
2024-11-04 17:02:06 -05:00
ionitron
ec14e13780 chore(): update package lock files 2024-11-04 21:29:59 +00:00
ionitron
fcc728faf2 v8.4.0 2024-11-04 21:29:16 +00:00
Tanner Reits
89508fb891 feat(segment-view): adds support for new ion-segment-view component (#29969)
Issue number: resolves internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Segments can only be changed by clicking a segment button, or dragging
the indicator

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

The segment/segment buttons can now be linked to segment content within
a segment view component. This content is scrollable/swipeable. Changing
the content will update the segment/indicator and vice-versa.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

**Limitations:**
- Segment buttons **cannot** be disabled when connected ton
`ion-segment-content` instances
- The `ion-segment` **cannot** be without a value when linked with an
`ion-segment-view`. If no value is provided, the value will default to
the value of the first `ion-segment-content`


[Preview](https://ionic-framework-jlt8by2io-ionic1.vercel.app/src/components/segment-view/test/basic)
[Preview (disabled
state)](https://ionic-framework-jlt8by2io-ionic1.vercel.app/src/components/segment-view/test/disabled)

---------

Co-authored-by: Brandy Carney <brandyscarney@gmail.com>
2024-11-04 16:10:58 -05:00
Tanner Reits
3628ea875a feat(select): add modal as interface (#29972)
Issue number: resolves internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Select only offers `alert`, `action-sheet`, and `popover` as interfaces

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

Adds `modal` as an interface option for `ion-select`

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

---------

Co-authored-by: Brandy Carney <brandyscarney@users.noreply.github.com>
2024-11-04 16:10:58 -05:00
Brandy Carney
0fdcb32ce0 fix(alert): use correct heading structure for subHeader when header exists (#29964)
- The `header` will continue to always render as an `<h2>` element.
- If there is no `header` defined, the `subHeader` will continue to render as an `<h2>` element.
- If there is a `header` defined, the `subHeader` will render as an `<h3>` element.
- Updates `ariaLabelledBy` to include both `header` and `subHeader` ids when both are defined.
- Updates the `a11y` e2e test to use new values & check for tag names.
2024-11-04 16:10:58 -05:00
Brandy Carney
ee2fa19a1e feat(menu): pass role to ionWillClose and ionDidClose events (#29954)
- Adds the `MenuCloseEventDetail` interface which includes an optional `role` property
- The `ionWillClose` and `ionDidClose` emit the `role` property for the following scenarios:
  - A role of `'gesture'` when dragging the menu closed
- A role of `'backdrop'` when clicking on the backdrop to close the menu
- A role of `'backdrop'` when the the menu is closed using the escape key
- A role of `undefined` when the menu is closed from a button inside of
the menu
2024-11-04 16:10:57 -05:00
Brandy Carney
2d6eeee267 test(alert): skip flaky test (#29985) 2024-10-31 10:46:12 -04:00
Brandy Carney
6dc52d2d7c merge release-8.3.4 (#29980)
v8.3.4
2024-10-30 12:56:17 -04:00
ionitron
ffdaa3b286 chore(): update package lock files 2024-10-30 16:40:24 +00:00
ionitron
93364b93c4 v8.3.4 2024-10-30 16:39:42 +00:00
Brandy Carney
c3b58f1620 fix(overlays): hide the focus trap div from screen readers (#29970)
Issue number: resolves #29858

---------

## What is the current behavior?
When swiping between elements using Android TalkBack, a green box is
shown for certain overlays and it gains focus at the beginning and end
of those overlays:

<img width="419" alt="Screenshot 2024-10-25 at 2 44 45 PM"
src="https://github.com/user-attachments/assets/dffd8126-ec1d-4b6f-87fc-6488c7c09aab">

## What is the new behavior?
The `aria-hidden` attribute is now added to the focus trap divs to hide
them from screen readers, without preventing these divs from trapping
keyboard focus.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information
Dev build: `8.3.4-dev.11729882231.1b2e7f13`
2024-10-29 14:04:43 +00:00
Maria Hutt
5a7314553a fix(input, textarea): ensure screen readers announce helper and error text when focused (#29958)
Issue number: internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Screen readers do not announce helper and error text when user is
focused on the input or textarea. This does not align with the
accessibility guidelines.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- The appropriate `aria` tags are added to the native input and textarea
in order to associate them to the helper and error texts.
- `aria-describedBy` will only be added to the native element based on
helper or error text. If helper text exists then the helper text ID will
be used. If the error text exists and the component has the `ion-touched
ion-invalid` classes, then the error text ID will be used.
- `aria-invalid` will only be added if the error text exists and the
component has the `ion-touched ion-invalid` classes.
- Added tests.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

How to test:
1. Navigate to the [input
page](https://ionic-framework-lio43tje7-ionic1.vercel.app/src/components/input/test/bottom-content)
on the `main` branch
2. Turn on the screen reader of your choice
3. Notice that the screen reader does not announce the helper or error
text when the input is focused
4. Navigate to the [input
page](https://ionic-framework-git-rou-11274-ionic1.vercel.app/src/components/input/test/bottom-content)
on the `ROU-11274` branch
5. Turn on the screen reader of your choice
6. Verify that the screen reader announces the helper or error text when
the input is focused on
7. Navigate to the [textarea
page](https://ionic-framework-lio43tje7-ionic1.vercel.app/src/components/textarea/test/bottom-content)
on the `main` branch
8. Repeat steps 2-3
9. Navigate to the [textarea
page](https://ionic-framework-git-rou-11274-ionic1.vercel.app/src/components/textarea/test/bottom-content)
on the `ROU-11274` branch
10. Repeat steps 5-6

Known Webkit issues:
This fix will not work on macOS
[16](https://bugs.webkit.org/show_bug.cgi?id=254081) and
[17](https://bugs.webkit.org/show_bug.cgi?id=262895) as VoiceOver will
not read any text using `aria-describedby`. Works fine on macOS 18.
2024-10-25 19:07:09 +00:00
Maria Hutt
322d7c98cf fix(overlays): do not hide root when toast appears (#29962)
Issue number: stemmed from #29773

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Certain Chrome and Edge versions (confirmed: Chrome v127, v128, v129 and
Edge v127) would indicate that certain elements have an accessibility
violation:

```
Blocked aria-hidden on a "ELEMENT NAME" element because the element that just received
focus must not be hidden from assistive technology users. Avoid using aria-hidden on a
focused element or its ancestor. Consider using the inert attribute instead, which will also
prevent focus. For more details, see the aria-hidden section of the WAI-ARIA specification at
```

This issue happens when a toast appears and the users clicks on any
element that is not related to toast. This is due to the main content
having an `aria-hidden` so users should not to be able to interact with
any of those elements. This isn't an issue when an overlay uses a
backdrop, like `ion-alert` because the backdrop prevents a user from
interacting with those elements.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- When toast is present, the main content no longer has an
`aria-hidden`. This aligns with accessibility guidelines. I also
verified with other Framework, MD states "Don't trap focus in the
snackbar. Users should be able to freely navigate in and out."

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev build: `8.3.4-dev.11729879684.1ea28919`

1. Clone this
[repo](https://github.com/brandyscarney/test-angular-overlays)
2. Install deps
3. Run the app on a private browser (Chrome v127, v128, v129 or Edge
v127)
4. Open browser console
5. Click on "Open Toast" button
6. Click on any element other than "Open Toast" button, like "Open
Popover"
7. Notice the error message
8. Close private browser
9. Install dev build: `npm install
@ionic/angular@8.3.4-dev.11729879684.1ea28919`
10. Repeat steps 4-7
11. Verify that the error message doesn't occur
2024-10-25 19:04:24 +00:00
Maria Hutt
cb6007363a fix(overlay): hide from screen readers while animating (#29951)
Issue number: resolves #29857 

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Screen readers like Android Talkback would not have the focus ring on
the correct element. For example, Talkback would announce the buttons
correctly within action sheet but the focus ring was no where to be
seen.

After digging around, the focus rings are located out of screen because
the action sheet is mounted to the DOM out of the screen first then
transitions into the screen. There are some screen readers that do not
behave as expected when an element uses `transform` styles like action
sheet.


https://github.com/user-attachments/assets/5a700bcc-3149-47a9-96ff-0aef99dd2bb3

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- When an overlay is being animated (presenting or dismissing), the
overlay will hide from screen readers. This allows the element to
navigate to its correct destination for screen readers to interact with.
Plus, we shouldn't allow screen readers to interact with content in the
middle of an animation. It could lead to some confusion.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev build: 8.3.3-dev.11729276019.194c165c

**A physical Android device will be needed, the issue does not appear in
simulators**

Components that need to be tested because they use overlays:
- Action sheet
- Alert
- Loading
- Modal
- Popover
- Select w/ action sheet interface
- Select w/ alert interface
- Select w/ popover interface
- Toast

How to test:
1. Create a starter app (any framework will do)
2. Add an action sheet
3. Build app for mobile devices
```
ionic build

ionic cap add ios
ionic cap add android

ionic cap copy && ionic cap sync
```
4. Open the app in Android Studio: `ionic cap open android`
5. Connect the Android device to Android Studio
6. Open app in Android device
7. Launch Talkback
8. Navigate back to the app
9. Open action sheet 
10. Swipe over to the action sheet's buttons
11. Notice that the buttons don't have a focus ring
12. Go back to the starter
4. Install the dev build
5. Add the components to the app
6. Sync app: `ionic cap copy && ionic cap sync`
13. Relaunch the app on the Android device
14. Verify that the focus ring appears on the action sheet's buttons
15. Verify that the other overlays are working as intended
2024-10-24 15:34:46 +00:00
Bentley O'Kane-Chase
e32fbe0210 fix(vue): incorrect view rendered when using router.go(-n) (#29877)
resolves #28201

This PR fixes the navigation issue related to `router.go` that was
identified in issue #28201. After working on this issue I realised that
@xxllxhdj has already created a PR for this in #29847. While their fix
is great, I have added tests to replicate the issue, reused existing
code and `undefined` will be returned in unexpected situations - which
matches the existing functionality.

## What is the current behavior?

If a user navigates from `/home` -> `/pageA` -> `/pageB` -> `/pageC` ->
back to `/pageB` -> then `router.go(-2)` is called the URL will be
updated to `/home` correctly, but the app will try to render `/pageA`.

This happens for any delta < -1.

## What is the new behavior?

The app will correctly render `/pageA`, which matches the URL.

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

---------

Co-authored-by: xxllxhdj <12881488+xxllxhdj@users.noreply.github.com>
2024-10-22 20:31:23 +00:00
Ehsan Barooni
47ba703a57 fix(angular): add missing 'compareWith' input to standalone ion-radio-group (#29870)
Issue number: resolves #29826

---------

## What is the current behavior?

When using `compareWith` on `ion-radio-group` in Ionic Angular
Standalone the following error is thrown:

```
NG8002: Can't bind to 'compareWith' since it isn't a known property of 'ion-radio-group'.
```

## What is the new behavior?

- `compareWith` on `ion-radio-group` in Angular Standalone is available

## Does this introduce a breaking change?

- [ ] Yes
- [x] No
2024-10-22 15:43:35 +00:00
Maria Hutt
7294e969bb fix(backdrop): remove tabindex for improved accessibility (#29956)
Issue number: resolves #29773

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Certain Chrome and Edge versions (confirmed: Chrome v127 and Edge v127)
would indicate that the backdrop has an accessibility violation:

```
Blocked aria-hidden on a <ion-backdrop> element because the element that just received
focus must not be hidden from assistive technology users. Avoid using aria-hidden on a
focused element or its ancestor. Consider using the inert attribute instead, which will also
prevent focus. For more details, see the aria-hidden section of the WAI-ARIA specification at
```

The error is happening because `tabindex` and `aria-hidden` are being
passed to `ion-backdrop`. The `tabindex` attribute is used to make an
element focusable, regardless of value. When `aria-hidden` is applied to
an element, then the element is hidden from screen readers. This
violates the accessibility standards since `ion-backdrop` would be
considered a focusable element but not visible to screen readers.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Removed `tabindex`, this aligns with frameworks known for
accessibility (Chakra UI)

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev build: `8.3.4-dev.11729533091.1ac77a0c`

How to test:

1. Use either Chrome v127 or Edge v127
2. Navigate to the [alert basic
page](https://ionic-framework-3pvgcj4b9-ionic1.vercel.app/src/components/alert/test/basic)
from the `main` branch
3. Open browser console
4. Click on the first button on the alert page
5. Click on the backdrop
6. Notice the error message in the browser console (if you don't see it,
then use a private browser or clear cache)
7. Navigate to the [alert basic
page](https://ionic-framework-git-rou-11175-ionic1.vercel.app/src/components/alert/test/basic)
from the `ROU-11175` branch
8. Repeat steps 2-6
9. Verify that the error does not appear
2024-10-22 15:07:36 +00:00
Maria Hutt
be7561d0d4 merge release-8.3.3 (#29943)
v8.3.3
2024-10-16 12:13:17 -07:00
ionitron
c67e6299d7 chore(): update package lock files 2024-10-16 18:57:07 +00:00
ionitron
bb1fb2877b v8.3.3 2024-10-16 18:55:50 +00:00
Maria Hutt
b7b383bee0 fix(tabs, tab-bar): use standalone tab bar in Vue, React (#29940)
Issue number: resolves #29885, resolves #29924

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

React and Vue:

Tab bar could be a standalone element within `IonTabs` and would
navigate without issues with a router outlet before v8.3:

```tsx
<IonTabs>
  <IonRouterOutlet></IonRouterOutlet>

  <IonTabBar></IonTabBar>
</IonTabs>
```

It would work as if it was written as:

```tsx
<IonTabs>
  <IonRouterOutlet></IonRouterOutlet>

  <IonTabBar slot="bottom">
    <!-- Buttons -->
  </IonTabBar>
</IonTabs>
```

After v8.3, any `ion-tab-bar` that was not a direct child of `ion-tabs`
would lose it's expected behavior when used with a router outlet. If a
user clicked on a tab button, then the content would not be redirected
to that expected view.

React only:

Users can no longer add a `ref` to the `IonRouterOutlet`, it always
returns undefined.

```
<IonTabs>
      <IonRouterOutlet ref={ref}>

     <IonTabBar slot="bottom">
    <!-- Buttons -->
  </IonTabBar>
</IonTabs>
```

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

The fixes were already reviewed through PR
https://github.com/ionic-team/ionic-framework/pull/29925 and PR
https://github.com/ionic-team/ionic-framework/pull/29927. I split them
to make it easier to review.

React and Vue:

The React tabs has been updated to pass data to the tab bar through
context instead of passing it through a ref. By using a context, the
data will be available for the tab bar to use regardless of its level.

React only:

Reverted the logic for `routerOutletRef` and added a comment of the
importance of it.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

N/A
2024-10-16 18:08:54 +00:00
Maria Hutt
cdb4456be2 test(styles): update button styles for test pages (#29931)
Issue number: internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Test styles causes native buttons to have [certain
styling](https://github.com/ionic-team/ionic-framework/blob/main/core/scripts/testing/styles.css#L52-L64).
This was done to spruce up the buttons used for testing purposes only.
However, this ended up adding styles to native buttons within Ionic
components.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Test styles for native buttons are only applied to buttons that are
not part of a Ionic component

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

How to test:

1. Run the project locally from the `main` branch
2.
[Comment](5d208e9daa/core/src/components/searchbar/searchbar.md.scss (L91))
out `border: 0` from `.searchbar-clear-button` from the searchbar `md`
theme file (`ios` also works)
3. Navigate to the basic test page:
`/src/components/searchbar/test/basic`
4. Notice a teal border around the clear buttons
5. Checkout to this PR's branch
6. Make sure steps 2-3 are done
7. Verify that the teal border is not being applied to the clear buttons
8. Verify that only native buttons outside of the Ionic components have
a teal appearance: `/src/components/loading/test/standalone` and
`/src/components/action-sheet/test/is-open`
2024-10-14 15:47:54 +00:00
Brandy Carney
bbcbf5c425 merge release-8.3.2 (#29920)
v8.3.2
2024-10-02 13:41:53 -04:00
ionitron
78fb1b9a06 chore(): update package lock files 2024-10-02 17:09:21 +00:00
ionitron
4d0e9c4186 v8.3.2 2024-10-02 17:08:43 +00:00
Brandy Carney
668b2dac57 docs(app): add setFocus to the documentation (#29916)
Issue number: resolves #29830

---------

## What is the current behavior?
The `setFocus` method on `ion-app` is marked internal.

## What is the new behavior?
Document the `setFocus` method as a way for developers to
programmatically focus elements.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information

The method isn’t new, it was just marked as internal, which prevented it
from being documented. I can mark this as a `feat` though if anyone
thinks it should be.

Related documentation PR:
https://github.com/ionic-team/ionic-docs/pull/3842
2024-10-01 18:27:03 +00:00
Maria Hutt
078ed0b86a fix(segment): prevent flickering for scrollable on iOS (#29884)
Issue number: resolves #29523

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

The scrollable segment flickers on iOS physical devices or simulators
when the active button is near the edge of the screen. The jump is due
to the button being scrolled to the center and snaps back to the edge
since the button was scrolled past the container.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Switched to `scrollTo` provides for a smoother transition.
- Gave co author credit to the original reporter since they provided
part of the solution
- No new tests were created since functionality stays the same and
testing on Playwright would be impossible to recreate

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Dev build: 8.3.2-dev.11726779768.16e1f1d2

How to test:
1. Create a new app through any starter
2. Add a scrollable segment with at least 6 buttons (code snippet
example below)
3. Recommended to change the segment mode to `md` since it's easier to
see the flicker
4. Build the app and open it in an iOS or simulator (if more
instructions on how to do this is needed, reach out to me)
5. Click on the third button
6. Click on the first button
7. Notice the flicker
8. Click over to the third to last button
9. Click on either the last two buttons
10. Notice the flicker
11. Install the dev build
12. Verify the load does not flicker
13. Repeat steps 4 and 5
14. Verify the flicker is no longer there
15. Repeat steps 7 and 8
16. Verify the flicker is no longer there


```js
<ion-segment value="2" scrollable="true" mode="md">
  <ion-segment-button value="1">
    <ion-label>Button 1</ion-label>
  </ion-segment-button>
  <ion-segment-button value="2">
    <ion-label>Button 2</ion-label>
  </ion-segment-button>
  <ion-segment-button value="3">
    <ion-label>Button 3</ion-label>
  </ion-segment-button>
  <ion-segment-button value="4">
    <ion-label>Button 4</ion-label>
  </ion-segment-button>
  <ion-segment-button value="5">
    <ion-label>Button 5</ion-label>
  </ion-segment-button>
  <ion-segment-button value="6">
    <ion-label>Button 6</ion-label>
  </ion-segment-button>
</ion-segment>
```

---------

Co-authored-by: rostislavcz <58735164+rostislavcz@users.noreply.github.com>
2024-09-23 17:33:55 +00:00
227 changed files with 23081 additions and 2149 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,101 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.4.3](https://github.com/ionic-team/ionic-framework/compare/v8.4.2...v8.4.3) (2025-01-29)
### Bug Fixes
* **vue:** update Stencil Vue output target ([#30159](https://github.com/ionic-team/ionic-framework/issues/30159)) ([eb725fc](https://github.com/ionic-team/ionic-framework/commit/eb725fce6eb15facd8a1c21be11a1b2d46336479))
## [8.4.2](https://github.com/ionic-team/ionic-framework/compare/v8.4.1...v8.4.2) (2025-01-22)
### Bug Fixes
* **segment:** add logic to connect to segment-view in `componentDidLoad()` callback ([#30060](https://github.com/ionic-team/ionic-framework/issues/30060)) ([000f553](https://github.com/ionic-team/ionic-framework/commit/000f55303e459c583e642337fb1894f419f37d48)), closes [#30000](https://github.com/ionic-team/ionic-framework/issues/30000)
* **select-modal:** match radio styles to iOS native ([#30119](https://github.com/ionic-team/ionic-framework/issues/30119)) ([3f8346e](https://github.com/ionic-team/ionic-framework/commit/3f8346e718ae3a6eb5008d739f10b6898b84ca9b))
## [8.4.1](https://github.com/ionic-team/ionic-framework/compare/v8.4.0...v8.4.1) (2024-11-27)
### Bug Fixes
* **header:** use aria attributes to hide small title when collapsed ([#30027](https://github.com/ionic-team/ionic-framework/issues/30027)) ([23763ab](https://github.com/ionic-team/ionic-framework/commit/23763abf797f9a4ba8262225760f718e9dcc4782)), closes [#29347](https://github.com/ionic-team/ionic-framework/issues/29347)
* **menu:** hide from screen readers while animating ([#30036](https://github.com/ionic-team/ionic-framework/issues/30036)) ([845071c](https://github.com/ionic-team/ionic-framework/commit/845071c97a856d45eb5e0bb81d9c270bc38bb604))
* **overlays:** announce info after opening based on platform ([#30025](https://github.com/ionic-team/ionic-framework/issues/30025)) ([f6188c4](https://github.com/ionic-team/ionic-framework/commit/f6188c47e9278fe69fd9d250c65156edbe5ef32e))
* **overlays:** focus management with checkbox/radio ([#30026](https://github.com/ionic-team/ionic-framework/issues/30026)) ([8ee42bb](https://github.com/ionic-team/ionic-framework/commit/8ee42bbc1e0bf4731d20040c7853756722f1a4b2))
* **toast:** swipe gesture works with custom container layout ([#29999](https://github.com/ionic-team/ionic-framework/issues/29999)) ([470decc](https://github.com/ionic-team/ionic-framework/commit/470decca7b6b89ef74095ef0bb7909b93640cd78)), closes [#29998](https://github.com/ionic-team/ionic-framework/issues/29998)
# [8.4.0](https://github.com/ionic-team/ionic-framework/compare/v8.3.4...v8.4.0) (2024-11-04)
### Bug Fixes
* **alert:** use correct heading structure for subHeader when header exists ([#29964](https://github.com/ionic-team/ionic-framework/issues/29964)) ([0fdcb32](https://github.com/ionic-team/ionic-framework/commit/0fdcb32ce0f99b284b314f79f7d0b071bc37faec))
### Features
* **menu:** pass role to ionWillClose and ionDidClose events ([#29954](https://github.com/ionic-team/ionic-framework/issues/29954)) ([ee2fa19](https://github.com/ionic-team/ionic-framework/commit/ee2fa19a1e9f09d492c7c08340d95ba6a56ebb2b))
* **segment-view:** adds support for new `ion-segment-view` component ([#29969](https://github.com/ionic-team/ionic-framework/issues/29969)) ([89508fb](https://github.com/ionic-team/ionic-framework/commit/89508fb89172900b1d11cc3fc18883f57a7fbab6))
* **select:** add `modal` as interface ([#29972](https://github.com/ionic-team/ionic-framework/issues/29972)) ([3628ea8](https://github.com/ionic-team/ionic-framework/commit/3628ea875a66a717783de5e0a4df440872339040))
## [8.3.4](https://github.com/ionic-team/ionic-framework/compare/v8.3.3...v8.3.4) (2024-10-30)
### Bug Fixes
* **angular:** add missing 'compareWith' input to standalone ion-radio-group ([#29870](https://github.com/ionic-team/ionic-framework/issues/29870)) ([47ba703](https://github.com/ionic-team/ionic-framework/commit/47ba703a57d1ca506f943f6b790d0bf7583d79cb)), closes [#29826](https://github.com/ionic-team/ionic-framework/issues/29826)
* **backdrop:** remove tabindex for improved accessibility ([#29956](https://github.com/ionic-team/ionic-framework/issues/29956)) ([7294e96](https://github.com/ionic-team/ionic-framework/commit/7294e969bb913692eaf28e54860614f445132713)), closes [#29773](https://github.com/ionic-team/ionic-framework/issues/29773)
* **input, textarea:** ensure screen readers announce helper and error text when focused ([#29958](https://github.com/ionic-team/ionic-framework/issues/29958)) ([5a73145](https://github.com/ionic-team/ionic-framework/commit/5a7314553a8def87bd19275640c92dd72a6ef1a4))
* **overlay:** hide from screen readers while animating ([#29951](https://github.com/ionic-team/ionic-framework/issues/29951)) ([cb60073](https://github.com/ionic-team/ionic-framework/commit/cb6007363a8d42b5f126945427c2bfc3d7209c21)), closes [#29857](https://github.com/ionic-team/ionic-framework/issues/29857)
* **overlays:** do not hide root when toast appears ([#29962](https://github.com/ionic-team/ionic-framework/issues/29962)) ([322d7c9](https://github.com/ionic-team/ionic-framework/commit/322d7c98cf6613df0b0db3f119e3f892e6a17e7b)), closes [#29773](https://github.com/ionic-team/ionic-framework/issues/29773)
* **overlays:** hide the focus trap div from screen readers ([#29970](https://github.com/ionic-team/ionic-framework/issues/29970)) ([c3b58f1](https://github.com/ionic-team/ionic-framework/commit/c3b58f1620bcb74db43e3983ef570b7b982abd83)), closes [#29858](https://github.com/ionic-team/ionic-framework/issues/29858)
* **vue:** incorrect view rendered when using router.go(-n) ([#29877](https://github.com/ionic-team/ionic-framework/issues/29877)) ([e32fbe0](https://github.com/ionic-team/ionic-framework/commit/e32fbe02102fe80db29f73c26496a40852032354)), closes [#28201](https://github.com/ionic-team/ionic-framework/issues/28201) [#28201](https://github.com/ionic-team/ionic-framework/issues/28201) [#29847](https://github.com/ionic-team/ionic-framework/issues/29847)
## [8.3.3](https://github.com/ionic-team/ionic-framework/compare/v8.3.2...v8.3.3) (2024-10-16)
### Bug Fixes
* **tabs, tab-bar:** use standalone tab bar in Vue, React ([#29940](https://github.com/ionic-team/ionic-framework/issues/29940)) ([b7b383b](https://github.com/ionic-team/ionic-framework/commit/b7b383bee080b72de2e6307ff9a9a051314c69ed)), closes [#29885](https://github.com/ionic-team/ionic-framework/issues/29885) [#29924](https://github.com/ionic-team/ionic-framework/issues/29924)
## [8.3.2](https://github.com/ionic-team/ionic-framework/compare/v8.3.1...v8.3.2) (2024-10-02)
### Bug Fixes
* **segment:** prevent flickering for scrollable on iOS ([#29884](https://github.com/ionic-team/ionic-framework/issues/29884)) ([078ed0b](https://github.com/ionic-team/ionic-framework/commit/078ed0b86a0d8e9f8457481cb739ea214195adce)), closes [#29523](https://github.com/ionic-team/ionic-framework/issues/29523)
## [8.3.1](https://github.com/ionic-team/ionic-framework/compare/v8.3.0...v8.3.1) (2024-09-17)

View File

@@ -3,6 +3,93 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.4.3](https://github.com/ionic-team/ionic-framework/compare/v8.4.2...v8.4.3) (2025-01-29)
**Note:** Version bump only for package @ionic/core
## [8.4.2](https://github.com/ionic-team/ionic-framework/compare/v8.4.1...v8.4.2) (2025-01-22)
### Bug Fixes
* **segment:** add logic to connect to segment-view in `componentDidLoad()` callback ([#30060](https://github.com/ionic-team/ionic-framework/issues/30060)) ([000f553](https://github.com/ionic-team/ionic-framework/commit/000f55303e459c583e642337fb1894f419f37d48)), closes [#30000](https://github.com/ionic-team/ionic-framework/issues/30000)
* **select-modal:** match radio styles to iOS native ([#30119](https://github.com/ionic-team/ionic-framework/issues/30119)) ([3f8346e](https://github.com/ionic-team/ionic-framework/commit/3f8346e718ae3a6eb5008d739f10b6898b84ca9b))
## [8.4.1](https://github.com/ionic-team/ionic-framework/compare/v8.4.0...v8.4.1) (2024-11-27)
### Bug Fixes
* **header:** use aria attributes to hide small title when collapsed ([#30027](https://github.com/ionic-team/ionic-framework/issues/30027)) ([23763ab](https://github.com/ionic-team/ionic-framework/commit/23763abf797f9a4ba8262225760f718e9dcc4782)), closes [#29347](https://github.com/ionic-team/ionic-framework/issues/29347)
* **menu:** hide from screen readers while animating ([#30036](https://github.com/ionic-team/ionic-framework/issues/30036)) ([845071c](https://github.com/ionic-team/ionic-framework/commit/845071c97a856d45eb5e0bb81d9c270bc38bb604))
* **overlays:** announce info after opening based on platform ([#30025](https://github.com/ionic-team/ionic-framework/issues/30025)) ([f6188c4](https://github.com/ionic-team/ionic-framework/commit/f6188c47e9278fe69fd9d250c65156edbe5ef32e))
* **overlays:** focus management with checkbox/radio ([#30026](https://github.com/ionic-team/ionic-framework/issues/30026)) ([8ee42bb](https://github.com/ionic-team/ionic-framework/commit/8ee42bbc1e0bf4731d20040c7853756722f1a4b2))
* **toast:** swipe gesture works with custom container layout ([#29999](https://github.com/ionic-team/ionic-framework/issues/29999)) ([470decc](https://github.com/ionic-team/ionic-framework/commit/470decca7b6b89ef74095ef0bb7909b93640cd78)), closes [#29998](https://github.com/ionic-team/ionic-framework/issues/29998)
# [8.4.0](https://github.com/ionic-team/ionic-framework/compare/v8.3.4...v8.4.0) (2024-11-04)
### Bug Fixes
* **alert:** use correct heading structure for subHeader when header exists ([#29964](https://github.com/ionic-team/ionic-framework/issues/29964)) ([0fdcb32](https://github.com/ionic-team/ionic-framework/commit/0fdcb32ce0f99b284b314f79f7d0b071bc37faec))
### Features
* **menu:** pass role to ionWillClose and ionDidClose events ([#29954](https://github.com/ionic-team/ionic-framework/issues/29954)) ([ee2fa19](https://github.com/ionic-team/ionic-framework/commit/ee2fa19a1e9f09d492c7c08340d95ba6a56ebb2b))
* **segment-view:** adds support for new `ion-segment-view` component ([#29969](https://github.com/ionic-team/ionic-framework/issues/29969)) ([89508fb](https://github.com/ionic-team/ionic-framework/commit/89508fb89172900b1d11cc3fc18883f57a7fbab6))
* **select:** add `modal` as interface ([#29972](https://github.com/ionic-team/ionic-framework/issues/29972)) ([3628ea8](https://github.com/ionic-team/ionic-framework/commit/3628ea875a66a717783de5e0a4df440872339040))
## [8.3.4](https://github.com/ionic-team/ionic-framework/compare/v8.3.3...v8.3.4) (2024-10-30)
### Bug Fixes
* **backdrop:** remove tabindex for improved accessibility ([#29956](https://github.com/ionic-team/ionic-framework/issues/29956)) ([7294e96](https://github.com/ionic-team/ionic-framework/commit/7294e969bb913692eaf28e54860614f445132713)), closes [#29773](https://github.com/ionic-team/ionic-framework/issues/29773)
* **input, textarea:** ensure screen readers announce helper and error text when focused ([#29958](https://github.com/ionic-team/ionic-framework/issues/29958)) ([5a73145](https://github.com/ionic-team/ionic-framework/commit/5a7314553a8def87bd19275640c92dd72a6ef1a4))
* **overlay:** hide from screen readers while animating ([#29951](https://github.com/ionic-team/ionic-framework/issues/29951)) ([cb60073](https://github.com/ionic-team/ionic-framework/commit/cb6007363a8d42b5f126945427c2bfc3d7209c21)), closes [#29857](https://github.com/ionic-team/ionic-framework/issues/29857)
* **overlays:** do not hide root when toast appears ([#29962](https://github.com/ionic-team/ionic-framework/issues/29962)) ([322d7c9](https://github.com/ionic-team/ionic-framework/commit/322d7c98cf6613df0b0db3f119e3f892e6a17e7b)), closes [#29773](https://github.com/ionic-team/ionic-framework/issues/29773)
* **overlays:** hide the focus trap div from screen readers ([#29970](https://github.com/ionic-team/ionic-framework/issues/29970)) ([c3b58f1](https://github.com/ionic-team/ionic-framework/commit/c3b58f1620bcb74db43e3983ef570b7b982abd83)), closes [#29858](https://github.com/ionic-team/ionic-framework/issues/29858)
## [8.3.3](https://github.com/ionic-team/ionic-framework/compare/v8.3.2...v8.3.3) (2024-10-16)
**Note:** Version bump only for package @ionic/core
## [8.3.2](https://github.com/ionic-team/ionic-framework/compare/v8.3.1...v8.3.2) (2024-10-02)
### Bug Fixes
* **segment:** prevent flickering for scrollable on iOS ([#29884](https://github.com/ionic-team/ionic-framework/issues/29884)) ([078ed0b](https://github.com/ionic-team/ionic-framework/commit/078ed0b86a0d8e9f8457481cb739ea214195adce)), closes [#29523](https://github.com/ionic-team/ionic-framework/issues/29523)
## [8.3.1](https://github.com/ionic-team/ionic-framework/compare/v8.3.0...v8.3.1) (2024-09-17)

View File

@@ -143,6 +143,7 @@ ion-alert,css-prop,--width,ios
ion-alert,css-prop,--width,md
ion-app,none
ion-app,method,setFocus,setFocus(elements: HTMLElement[]) => Promise<void>
ion-avatar,shadow
ion-avatar,css-prop,--border-radius,ios
@@ -999,15 +1000,15 @@ ion-menu,prop,menuId,string | undefined,undefined,false,true
ion-menu,prop,side,"end" | "start",'start',false,true
ion-menu,prop,swipeGesture,boolean,true,false,false
ion-menu,prop,type,"overlay" | "push" | "reveal" | undefined,undefined,false,false
ion-menu,method,close,close(animated?: boolean) => Promise<boolean>
ion-menu,method,close,close(animated?: boolean, role?: string) => Promise<boolean>
ion-menu,method,isActive,isActive() => Promise<boolean>
ion-menu,method,isOpen,isOpen() => Promise<boolean>
ion-menu,method,open,open(animated?: boolean) => Promise<boolean>
ion-menu,method,setOpen,setOpen(shouldOpen: boolean, animated?: boolean) => Promise<boolean>
ion-menu,method,setOpen,setOpen(shouldOpen: boolean, animated?: boolean, role?: string) => Promise<boolean>
ion-menu,method,toggle,toggle(animated?: boolean) => Promise<boolean>
ion-menu,event,ionDidClose,void,true
ion-menu,event,ionDidClose,MenuCloseEventDetail,true
ion-menu,event,ionDidOpen,void,true
ion-menu,event,ionWillClose,void,true
ion-menu,event,ionWillClose,MenuCloseEventDetail,true
ion-menu,event,ionWillOpen,void,true
ion-menu,css-prop,--background,ios
ion-menu,css-prop,--background,md
@@ -1608,14 +1609,10 @@ ion-segment-button,part,indicator-background
ion-segment-button,part,native
ion-segment-content,shadow
ion-segment-content,prop,disabled,boolean,false,false,false
ion-segment-view,shadow
ion-segment-view,prop,disabled,boolean,false,false,false
ion-segment-view,method,setContent,setContent(id: string, smoothScroll?: boolean) => Promise<void>
ion-segment-view,event,ionSegmentViewScroll,{ scrollDirection: string; scrollDistance: number; scrollDistancePercentage: number; },true
ion-segment-view,event,ionSegmentViewScrollEnd,{ activeContentId: string; },true
ion-segment-view,event,ionSegmentViewScrollStart,void,true
ion-segment-view,event,ionSegmentViewScroll,SegmentViewScrollEvent,true
ion-select,shadow
ion-select,prop,cancelText,string,'Cancel',false,false
@@ -1624,7 +1621,7 @@ ion-select,prop,compareWith,((currentValue: any, compareValue: any) => boolean)
ion-select,prop,disabled,boolean,false,false,false
ion-select,prop,expandedIcon,string | undefined,undefined,false,false
ion-select,prop,fill,"outline" | "solid" | undefined,undefined,false,false
ion-select,prop,interface,"action-sheet" | "alert" | "popover",'alert',false,false
ion-select,prop,interface,"action-sheet" | "alert" | "modal" | "popover",'alert',false,false
ion-select,prop,interfaceOptions,any,{},false,false
ion-select,prop,justify,"end" | "space-between" | "start" | undefined,undefined,false,false
ion-select,prop,label,string | undefined,undefined,false,false
@@ -1682,6 +1679,11 @@ ion-select,part,label
ion-select,part,placeholder
ion-select,part,text
ion-select-modal,scoped
ion-select-modal,prop,header,string | undefined,undefined,false,false
ion-select-modal,prop,multiple,boolean | undefined,undefined,false,false
ion-select-modal,prop,options,SelectModalOption[],[],false,false
ion-select-option,shadow
ion-select-option,prop,disabled,boolean,false,false,false
ion-select-option,prop,value,any,undefined,false,false

683
core/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@ionic/core",
"version": "8.3.1",
"version": "8.4.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/core",
"version": "8.3.1",
"version": "8.4.3",
"license": "MIT",
"dependencies": {
"@stencil/core": "4.20.0",
@@ -19,16 +19,16 @@
"@capacitor/haptics": "^6.0.0",
"@capacitor/keyboard": "^6.0.0",
"@capacitor/status-bar": "^6.0.0",
"@clack/prompts": "^0.7.0",
"@clack/prompts": "^0.9.0",
"@ionic/eslint-config": "^0.3.0",
"@ionic/prettier-config": "^2.0.0",
"@playwright/test": "^1.46.1",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/angular-output-target": "^0.8.4",
"@stencil/angular-output-target": "^0.10.0",
"@stencil/react-output-target": "0.5.3",
"@stencil/sass": "^3.0.9",
"@stencil/vue-output-target": "^0.8.9",
"@stencil/vue-output-target": "^0.9.0",
"@types/jest": "^29.5.6",
"@types/node": "^14.6.0",
"@typescript-eslint/eslint-plugin": "^6.7.2",
@@ -319,18 +319,18 @@
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
"dev": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -400,10 +400,13 @@
}
},
"node_modules/@babel/parser": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz",
"integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==",
"dev": true,
"dependencies": {
"@babel/types": "^7.26.5"
},
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -641,14 +644,13 @@
}
},
"node_modules/@babel/types": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz",
"integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
},
"engines": {
"node": ">=6.9.0"
@@ -661,45 +663,45 @@
"dev": true
},
"node_modules/@capacitor/core": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.1.2.tgz",
"integrity": "sha512-xFy1/4qLFLp5WCIzIhtwUuVNNoz36+V7/BzHmLqgVJcvotc4MMjswW/TshnPQaLLujEOaLkA4h8ZJ0uoK3ImGg==",
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.0.tgz",
"integrity": "sha512-B9IlJtDpUqhhYb+T8+cp2Db/3RETX36STgjeU2kQZBs/SLAcFiMama227o+msRjLeo3DO+7HJjWVA1+XlyyPEg==",
"dev": true,
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/@capacitor/haptics": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-6.0.1.tgz",
"integrity": "sha512-Q8hedLwfwTSWEYc3eoATzkdKHBaIceYe5bd7FjxQCENNH0is5Ft0EjSRPz/xpTn39ebK0ooZBDBCwsyl6tjiTA==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-6.0.2.tgz",
"integrity": "sha512-xcFdIH4iIIeW2+1lzmlYMVicqB9ytaiuZ9NE3a9laKFPvMGC7hdj6i6tHFezwPJ/96xkHOwXT2b0F8Mh9xtTWg==",
"dev": true,
"peerDependencies": {
"@capacitor/core": "^6.0.0"
}
},
"node_modules/@capacitor/keyboard": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-6.0.2.tgz",
"integrity": "sha512-fOfO3rQ0ZXuTHpK03INVTwmBnpqMiH8EHPpNaHjwjKwdrVRWBvtgIFhuyHNXh53rdcXw+uHB+1RIiNabnCrITw==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-6.0.3.tgz",
"integrity": "sha512-V/mURxBI68HvClYjrGBlOriWkwYN7r+cWid/igJz/3scNc/V81DgQ9fpoLr4W0I5NY7YxOesjIJLuLO+LT18mQ==",
"dev": true,
"peerDependencies": {
"@capacitor/core": "^6.0.0"
}
},
"node_modules/@capacitor/status-bar": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-6.0.1.tgz",
"integrity": "sha512-Usd9hZZQVAqy+jJfL7jRcYI7dcsxN09Na1yttwdl+F1bk3Ztoukk7CGPDm5VgKUSs53ihQBOy1+sczCACxhNiw==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-6.0.2.tgz",
"integrity": "sha512-AmRIX6QvFemItlY7/69ARkIAqitRQqJ2qwgZmD1KqgFb78pH+XFXm1guvS/a8CuOOm/IqZ4ddDbl20yxtBqzGA==",
"dev": true,
"peerDependencies": {
"@capacitor/core": "^6.0.0"
}
},
"node_modules/@clack/core": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.4.tgz",
"integrity": "sha512-H4hxZDXgHtWTwV3RAVenqcC4VbJZNegbBjlPvzOzCouXtS2y3sDvlO3IsbrPNWuLWPPlYVYPghQdSF64683Ldw==",
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.4.1.tgz",
"integrity": "sha512-Pxhij4UXg8KSr7rPek6Zowm+5M22rbd2g1nfojHJkxp5YkFqiZ2+YLEM/XGVIzvGOcM0nqjIFxrpDwWRZYWYjA==",
"dev": true,
"dependencies": {
"picocolors": "^1.0.0",
@@ -707,32 +709,16 @@
}
},
"node_modules/@clack/prompts": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.7.0.tgz",
"integrity": "sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==",
"bundleDependencies": [
"is-unicode-supported"
],
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.9.1.tgz",
"integrity": "sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==",
"dev": true,
"dependencies": {
"@clack/core": "^0.3.3",
"is-unicode-supported": "*",
"@clack/core": "0.4.1",
"picocolors": "^1.0.0",
"sisteransi": "^1.0.5"
}
},
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
"version": "1.3.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -1678,9 +1664,9 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
@@ -1815,9 +1801,9 @@
}
},
"node_modules/@stencil/angular-output-target": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-0.8.4.tgz",
"integrity": "sha512-QvmHTueXXs5vB9W2L12uEzFmAuR8sqATJV2b+SCFmYsjJSaymiSqR3dKo2wnr0tZiTgU1t16BWaUKiSh3wPXpw==",
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-0.10.2.tgz",
"integrity": "sha512-jPRa2NMAPtm/iMY+mUaWATbIhgY5zPJfUNQyF8nwC0rMrfXifPoRCf6BbH2S4Gy7SX0X4hlP+jAbVUjQNg/P+Q==",
"dev": true,
"peerDependencies": {
"@stencil/core": ">=2.0.0 || >=3 || >= 4.0.0-beta.0 || >= 4.0.0"
@@ -1860,12 +1846,21 @@
}
},
"node_modules/@stencil/vue-output-target": {
"version": "0.8.9",
"resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.8.9.tgz",
"integrity": "sha512-1yuapCWYViLlxGlEaeta2wryq4M5zZxxBa+4rEBp54VwW2W/trlzPv0IJyw6I3Il51rHYm2WmWlBLOGmoMyW9Q==",
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.9.2.tgz",
"integrity": "sha512-AeBmfo8bQhtob4VKpYTNiCoqh50MeXUwRgYLyO/JxRgAAK9GSfenNrUxXDrK0DK65SWsx/GCOsRwWbfOveorOQ==",
"dev": true,
"peerDependencies": {
"@stencil/core": ">=2.0.0 || >=3 || >= 4.0.0-beta.0 || >= 4.0.0"
"@stencil/core": ">=2.0.0 || >=3 || >= 4.0.0-beta.0 || >= 4.0.0",
"vue": "^3.4.38"
},
"peerDependenciesMeta": {
"@stencil/core": {
"optional": true
},
"vue": {
"optional": false
}
}
},
"node_modules/@stylelint/postcss-css-in-js": {
@@ -2499,6 +2494,171 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/parser": "^7.25.3",
"@vue/shared": "3.5.13",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-core/node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/@vue/compiler-core/node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"peer": true
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
"dev": true,
"peer": true,
"dependencies": {
"@vue/compiler-core": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/parser": "^7.25.3",
"@vue/compiler-core": "3.5.13",
"@vue/compiler-dom": "3.5.13",
"@vue/compiler-ssr": "3.5.13",
"@vue/shared": "3.5.13",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.11",
"postcss": "^8.4.48",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-sfc/node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"peer": true
},
"node_modules/@vue/compiler-sfc/node_modules/postcss": {
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
"dev": true,
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
"dev": true,
"peer": true,
"dependencies": {
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
"dev": true,
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
"dev": true,
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.13",
"@vue/runtime-core": "3.5.13",
"@vue/shared": "3.5.13",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
"dev": true,
"peer": true,
"dependencies": {
"@vue/compiler-ssr": "3.5.13",
"@vue/shared": "3.5.13"
},
"peerDependencies": {
"vue": "3.5.13"
}
},
"node_modules/@vue/shared": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"dev": true,
"peer": true
},
"node_modules/@zeit/schemas": {
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.21.0.tgz",
@@ -3183,9 +3343,9 @@
]
},
"node_modules/chalk": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
"dev": true,
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
@@ -3777,6 +3937,13 @@
"node": ">=4"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"peer": true
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -7584,6 +7751,16 @@
"node": ">=10"
}
},
"node_modules/magic-string": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"dev": true,
"peer": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
@@ -7915,6 +8092,25 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -8236,9 +8432,9 @@
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true
},
"node_modules/picomatch": {
@@ -9103,6 +9299,16 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
@@ -9683,15 +9889,6 @@
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
"dev": true
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -10025,6 +10222,28 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/vue": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
"dev": true,
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.13",
"@vue/compiler-sfc": "3.5.13",
"@vue/runtime-dom": "3.5.13",
"@vue/server-renderer": "3.5.13",
"@vue/shared": "3.5.13"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
@@ -10505,15 +10724,15 @@
}
},
"@babel/helper-string-parser": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
"dev": true
},
"@babel/helper-validator-identifier": {
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
"dev": true
},
"@babel/helper-validator-option": {
@@ -10567,10 +10786,13 @@
}
},
"@babel/parser": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
"dev": true
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz",
"integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==",
"dev": true,
"requires": {
"@babel/types": "^7.26.5"
}
},
"@babel/plugin-syntax-async-generators": {
"version": "7.8.4",
@@ -10739,14 +10961,13 @@
}
},
"@babel/types": {
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"version": "7.26.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz",
"integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==",
"dev": true,
"requires": {
"@babel/helper-string-parser": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
}
},
"@bcoe/v8-coverage": {
@@ -10756,39 +10977,39 @@
"dev": true
},
"@capacitor/core": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.1.2.tgz",
"integrity": "sha512-xFy1/4qLFLp5WCIzIhtwUuVNNoz36+V7/BzHmLqgVJcvotc4MMjswW/TshnPQaLLujEOaLkA4h8ZJ0uoK3ImGg==",
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-6.2.0.tgz",
"integrity": "sha512-B9IlJtDpUqhhYb+T8+cp2Db/3RETX36STgjeU2kQZBs/SLAcFiMama227o+msRjLeo3DO+7HJjWVA1+XlyyPEg==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
}
},
"@capacitor/haptics": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-6.0.1.tgz",
"integrity": "sha512-Q8hedLwfwTSWEYc3eoATzkdKHBaIceYe5bd7FjxQCENNH0is5Ft0EjSRPz/xpTn39ebK0ooZBDBCwsyl6tjiTA==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-6.0.2.tgz",
"integrity": "sha512-xcFdIH4iIIeW2+1lzmlYMVicqB9ytaiuZ9NE3a9laKFPvMGC7hdj6i6tHFezwPJ/96xkHOwXT2b0F8Mh9xtTWg==",
"dev": true,
"requires": {}
},
"@capacitor/keyboard": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-6.0.2.tgz",
"integrity": "sha512-fOfO3rQ0ZXuTHpK03INVTwmBnpqMiH8EHPpNaHjwjKwdrVRWBvtgIFhuyHNXh53rdcXw+uHB+1RIiNabnCrITw==",
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-6.0.3.tgz",
"integrity": "sha512-V/mURxBI68HvClYjrGBlOriWkwYN7r+cWid/igJz/3scNc/V81DgQ9fpoLr4W0I5NY7YxOesjIJLuLO+LT18mQ==",
"dev": true,
"requires": {}
},
"@capacitor/status-bar": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-6.0.1.tgz",
"integrity": "sha512-Usd9hZZQVAqy+jJfL7jRcYI7dcsxN09Na1yttwdl+F1bk3Ztoukk7CGPDm5VgKUSs53ihQBOy1+sczCACxhNiw==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-6.0.2.tgz",
"integrity": "sha512-AmRIX6QvFemItlY7/69ARkIAqitRQqJ2qwgZmD1KqgFb78pH+XFXm1guvS/a8CuOOm/IqZ4ddDbl20yxtBqzGA==",
"dev": true,
"requires": {}
},
"@clack/core": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.4.tgz",
"integrity": "sha512-H4hxZDXgHtWTwV3RAVenqcC4VbJZNegbBjlPvzOzCouXtS2y3sDvlO3IsbrPNWuLWPPlYVYPghQdSF64683Ldw==",
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.4.1.tgz",
"integrity": "sha512-Pxhij4UXg8KSr7rPek6Zowm+5M22rbd2g1nfojHJkxp5YkFqiZ2+YLEM/XGVIzvGOcM0nqjIFxrpDwWRZYWYjA==",
"dev": true,
"requires": {
"picocolors": "^1.0.0",
@@ -10796,22 +11017,14 @@
}
},
"@clack/prompts": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.7.0.tgz",
"integrity": "sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==",
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.9.1.tgz",
"integrity": "sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg==",
"dev": true,
"requires": {
"@clack/core": "^0.3.3",
"is-unicode-supported": "*",
"@clack/core": "0.4.1",
"picocolors": "^1.0.0",
"sisteransi": "^1.0.5"
},
"dependencies": {
"is-unicode-supported": {
"version": "1.3.0",
"bundled": true,
"dev": true
}
}
},
"@eslint-community/eslint-utils": {
@@ -11483,9 +11696,9 @@
"dev": true
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"dev": true
},
"@jridgewell/trace-mapping": {
@@ -11591,9 +11804,9 @@
}
},
"@stencil/angular-output-target": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-0.8.4.tgz",
"integrity": "sha512-QvmHTueXXs5vB9W2L12uEzFmAuR8sqATJV2b+SCFmYsjJSaymiSqR3dKo2wnr0tZiTgU1t16BWaUKiSh3wPXpw==",
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-0.10.2.tgz",
"integrity": "sha512-jPRa2NMAPtm/iMY+mUaWATbIhgY5zPJfUNQyF8nwC0rMrfXifPoRCf6BbH2S4Gy7SX0X4hlP+jAbVUjQNg/P+Q==",
"dev": true,
"requires": {}
},
@@ -11617,9 +11830,9 @@
"requires": {}
},
"@stencil/vue-output-target": {
"version": "0.8.9",
"resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.8.9.tgz",
"integrity": "sha512-1yuapCWYViLlxGlEaeta2wryq4M5zZxxBa+4rEBp54VwW2W/trlzPv0IJyw6I3Il51rHYm2WmWlBLOGmoMyW9Q==",
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@stencil/vue-output-target/-/vue-output-target-0.9.2.tgz",
"integrity": "sha512-AeBmfo8bQhtob4VKpYTNiCoqh50MeXUwRgYLyO/JxRgAAK9GSfenNrUxXDrK0DK65SWsx/GCOsRwWbfOveorOQ==",
"dev": true,
"requires": {}
},
@@ -12063,6 +12276,149 @@
"eslint-visitor-keys": "^3.4.1"
}
},
"@vue/compiler-core": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
"dev": true,
"peer": true,
"requires": {
"@babel/parser": "^7.25.3",
"@vue/shared": "3.5.13",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.0"
},
"dependencies": {
"entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
"peer": true
},
"estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"peer": true
}
}
},
"@vue/compiler-dom": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
"dev": true,
"peer": true,
"requires": {
"@vue/compiler-core": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"@vue/compiler-sfc": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
"dev": true,
"peer": true,
"requires": {
"@babel/parser": "^7.25.3",
"@vue/compiler-core": "3.5.13",
"@vue/compiler-dom": "3.5.13",
"@vue/compiler-ssr": "3.5.13",
"@vue/shared": "3.5.13",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.11",
"postcss": "^8.4.48",
"source-map-js": "^1.2.0"
},
"dependencies": {
"estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true,
"peer": true
},
"postcss": {
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
"dev": true,
"peer": true,
"requires": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
}
}
}
},
"@vue/compiler-ssr": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
"dev": true,
"peer": true,
"requires": {
"@vue/compiler-dom": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"@vue/reactivity": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
"dev": true,
"peer": true,
"requires": {
"@vue/shared": "3.5.13"
}
},
"@vue/runtime-core": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
"dev": true,
"peer": true,
"requires": {
"@vue/reactivity": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"@vue/runtime-dom": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
"dev": true,
"peer": true,
"requires": {
"@vue/reactivity": "3.5.13",
"@vue/runtime-core": "3.5.13",
"@vue/shared": "3.5.13",
"csstype": "^3.1.3"
}
},
"@vue/server-renderer": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
"dev": true,
"peer": true,
"requires": {
"@vue/compiler-ssr": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"@vue/shared": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"dev": true,
"peer": true
},
"@zeit/schemas": {
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.21.0.tgz",
@@ -12537,9 +12893,9 @@
"dev": true
},
"chalk": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
"dev": true
},
"chalk-template": {
@@ -12953,6 +13309,13 @@
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true
},
"csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"peer": true
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -15757,6 +16120,16 @@
"yallist": "^4.0.0"
}
},
"magic-string": {
"version": "0.30.17",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
"dev": true,
"peer": true,
"requires": {
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
@@ -15991,6 +16364,13 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"peer": true
},
"natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -16229,9 +16609,9 @@
"dev": true
},
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true
},
"picomatch": {
@@ -16850,6 +17230,13 @@
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"peer": true
},
"source-map-support": {
"version": "0.5.13",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
@@ -17305,12 +17692,6 @@
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
"dev": true
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
"dev": true
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -17570,6 +17951,20 @@
"unist-util-stringify-position": "^2.0.0"
}
},
"vue": {
"version": "3.5.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
"dev": true,
"peer": true,
"requires": {
"@vue/compiler-dom": "3.5.13",
"@vue/compiler-sfc": "3.5.13",
"@vue/runtime-dom": "3.5.13",
"@vue/server-renderer": "3.5.13",
"@vue/shared": "3.5.13"
}
},
"walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "8.3.1",
"version": "8.4.3",
"description": "Base components for Ionic",
"keywords": [
"ionic",
@@ -41,16 +41,16 @@
"@capacitor/haptics": "^6.0.0",
"@capacitor/keyboard": "^6.0.0",
"@capacitor/status-bar": "^6.0.0",
"@clack/prompts": "^0.7.0",
"@clack/prompts": "^0.9.0",
"@ionic/eslint-config": "^0.3.0",
"@ionic/prettier-config": "^2.0.0",
"@playwright/test": "^1.46.1",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/angular-output-target": "^0.8.4",
"@stencil/angular-output-target": "^0.10.0",
"@stencil/react-output-target": "0.5.3",
"@stencil/sass": "^3.0.9",
"@stencil/vue-output-target": "^0.8.9",
"@stencil/vue-output-target": "^0.9.0",
"@types/jest": "^29.5.6",
"@types/node": "^14.6.0",
"@typescript-eslint/eslint-plugin": "^6.7.2",

View File

@@ -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%;
}

View File

@@ -18,7 +18,7 @@ import { ScrollBaseDetail, ScrollDetail } from "./components/content/content-int
import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
import { SpinnerTypes } from "./components/spinner/spinner-configs";
import { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
import { MenuChangeEventDetail, MenuType, Side } from "./components/menu/menu-interface";
import { MenuChangeEventDetail, MenuCloseEventDetail, MenuType, Side } from "./components/menu/menu-interface";
import { ModalBreakpointChangeEventDetail, ModalHandleBehavior } from "./components/modal/modal-interface";
import { NavComponent, NavComponentWithProps, NavOptions, RouterOutletOptions, SwipeGestureHandler, TransitionDoneFn, TransitionInstruction } from "./components/nav/nav-interface";
import { ViewController } from "./components/nav/view-controller";
@@ -34,7 +34,9 @@ import { NavigationHookCallback } from "./components/route/route-interface";
import { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
import { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
import { SegmentButtonLayout } from "./components/segment-button/segment-button-interface";
import { SegmentViewScrollEvent } from "./components/segment-view/segment-view-interface";
import { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./components/select/select-interface";
import { SelectModalOption } from "./components/select-modal/select-modal-interface";
import { SelectPopoverOption } from "./components/select-popover/select-popover-interface";
import { TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout } from "./components/tab-bar/tab-bar-interface";
import { TextareaChangeEventDetail, TextareaInputEventDetail } from "./components/textarea/textarea-interface";
@@ -53,7 +55,7 @@ export { ScrollBaseDetail, ScrollDetail } from "./components/content/content-int
export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
export { SpinnerTypes } from "./components/spinner/spinner-configs";
export { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
export { MenuChangeEventDetail, MenuType, Side } from "./components/menu/menu-interface";
export { MenuChangeEventDetail, MenuCloseEventDetail, MenuType, Side } from "./components/menu/menu-interface";
export { ModalBreakpointChangeEventDetail, ModalHandleBehavior } from "./components/modal/modal-interface";
export { NavComponent, NavComponentWithProps, NavOptions, RouterOutletOptions, SwipeGestureHandler, TransitionDoneFn, TransitionInstruction } from "./components/nav/nav-interface";
export { ViewController } from "./components/nav/view-controller";
@@ -69,7 +71,9 @@ export { NavigationHookCallback } from "./components/route/route-interface";
export { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
export { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
export { SegmentButtonLayout } from "./components/segment-button/segment-button-interface";
export { SegmentViewScrollEvent } from "./components/segment-view/segment-view-interface";
export { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./components/select/select-interface";
export { SelectModalOption } from "./components/select-modal/select-modal-interface";
export { SelectPopoverOption } from "./components/select-popover/select-popover-interface";
export { TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout } from "./components/tab-bar/tab-bar-interface";
export { TextareaChangeEventDetail, TextareaInputEventDetail } from "./components/textarea/textarea-interface";
@@ -304,6 +308,9 @@ export namespace Components {
"trigger": string | undefined;
}
interface IonApp {
/**
* Used to set focus on an element that uses `ion-focusable`. Do not use this if focusing the element as a result of a keyboard event as the focus utility should handle this for us. This method should be used when we want to programmatically focus an element as a result of another user action. (Ex: We focus the first element inside of a popover when the user presents it, but the popover is not always presented as a result of keyboard action.)
*/
"setFocus": (elements: HTMLElement[]) => Promise<void>;
}
interface IonAvatar {
@@ -636,6 +643,7 @@ export namespace Components {
* The name of the control, which is submitted with the form data.
*/
"name": string;
"setFocus": () => Promise<void>;
/**
* The value of the checkbox does not mean if it's checked or not, use the `checked` property for that. The value of a checkbox is analogous to the value of an `<input type="checkbox">`, it's only used when the checkbox participates in a native `<form>`.
*/
@@ -1593,7 +1601,7 @@ export namespace Components {
/**
* Closes the menu. If the menu is already closed or it can't be closed, it returns `false`.
*/
"close": (animated?: boolean) => Promise<boolean>;
"close": (animated?: boolean, role?: string) => Promise<boolean>;
/**
* The `id` of the main content. When using a router this is typically `ion-router-outlet`. When not using a router, this is typically your main view's `ion-content`. This is not the id of the `ion-content` inside of your `ion-menu`.
*/
@@ -1625,7 +1633,7 @@ export namespace Components {
/**
* Opens or closes the button. If the operation can't be completed successfully, it returns `false`.
*/
"setOpen": (shouldOpen: boolean, animated?: boolean) => Promise<boolean>;
"setOpen": (shouldOpen: boolean, animated?: boolean, role?: string) => Promise<boolean>;
/**
* Which side of the view the menu should be placed.
*/
@@ -2276,7 +2284,7 @@ export namespace Components {
*/
"name": string;
"setButtonTabindex": (value: number) => Promise<void>;
"setFocus": (ev: globalThis.Event) => Promise<void>;
"setFocus": (ev?: globalThis.Event) => Promise<void>;
/**
* the value of the radio.
*/
@@ -2295,6 +2303,7 @@ export namespace Components {
* The name of the control, which is submitted with the form data.
*/
"name": string;
"setFocus": () => Promise<void>;
/**
* the value of the radio group.
*/
@@ -2676,10 +2685,6 @@ export namespace Components {
* If `true`, the segment buttons will overflow and the user can swipe to see them. In addition, this will disable the gesture to drag the indicator between the buttons in order to swipe to see hidden buttons.
*/
"scrollable": boolean;
/**
* The `id` of the `segment-view` element to be associated with this segment.
*/
"segmentViewId"?: string;
/**
* If `true`, navigating to an `ion-segment-button` with the keyboard will focus and select the element. If `false`, keyboard navigation will only focus the `ion-segment-button` element.
*/
@@ -2702,7 +2707,6 @@ export namespace Components {
* If `true`, the user cannot interact with the segment button.
*/
"disabled": boolean;
"hasIndicator": boolean;
/**
* Set the layout of the text and icon in the segment.
*/
@@ -2722,10 +2726,6 @@ export namespace Components {
"value": SegmentValue;
}
interface IonSegmentContent {
/**
* If `true`, the segment content will not be displayed.
*/
"disabled": boolean;
}
interface IonSegmentView {
/**
@@ -2733,7 +2733,6 @@ export namespace Components {
*/
"disabled": boolean;
/**
* This method is used to programmatically set the displayed segment content in the segment view. Calling this method will update the `value` of the corresponding segment button.
* @param id : The id of the segment content to display.
* @param smoothScroll : Whether to animate the scroll transition.
*/
@@ -2765,11 +2764,11 @@ export namespace Components {
*/
"fill"?: 'outline' | 'solid';
/**
* The interface the select should use: `action-sheet`, `popover` or `alert`.
* The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`.
*/
"interface": SelectInterface;
/**
* Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet) and the [ion-popover docs](./popover) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface.
* Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet), the [ion-popover docs](./popover), and the [ion-modal docs](./modal) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface.
*/
"interfaceOptions": any;
/**
@@ -2826,6 +2825,11 @@ export namespace Components {
*/
"value"?: any | null;
}
interface IonSelectModal {
"header"?: string;
"multiple"?: boolean;
"options": SelectModalOption[];
}
interface IonSelectOption {
/**
* If `true`, the user cannot interact with the select option. This property does not apply when `interface="action-sheet"` as `ion-action-sheet` does not allow for disabled buttons.
@@ -3997,9 +4001,9 @@ declare global {
};
interface HTMLIonMenuElementEventMap {
"ionWillOpen": void;
"ionWillClose": void;
"ionWillClose": MenuCloseEventDetail;
"ionDidOpen": void;
"ionDidClose": void;
"ionDidClose": MenuCloseEventDetail;
"ionMenuChange": MenuChangeEventDetail;
}
interface HTMLIonMenuElement extends Components.IonMenu, HTMLStencilElement {
@@ -4447,13 +4451,7 @@ declare global {
new (): HTMLIonSegmentContentElement;
};
interface HTMLIonSegmentViewElementEventMap {
"ionSegmentViewScroll": {
scrollDirection: string;
scrollDistance: number;
scrollDistancePercentage: number;
};
"ionSegmentViewScrollEnd": { activeContentId: string };
"ionSegmentViewScrollStart": void;
"ionSegmentViewScroll": SegmentViewScrollEvent;
}
interface HTMLIonSegmentViewElement extends Components.IonSegmentView, HTMLStencilElement {
addEventListener<K extends keyof HTMLIonSegmentViewElementEventMap>(type: K, listener: (this: HTMLIonSegmentViewElement, ev: IonSegmentViewCustomEvent<HTMLIonSegmentViewElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
@@ -4491,6 +4489,12 @@ declare global {
prototype: HTMLIonSelectElement;
new (): HTMLIonSelectElement;
};
interface HTMLIonSelectModalElement extends Components.IonSelectModal, HTMLStencilElement {
}
var HTMLIonSelectModalElement: {
prototype: HTMLIonSelectModalElement;
new (): HTMLIonSelectModalElement;
};
interface HTMLIonSelectOptionElement extends Components.IonSelectOption, HTMLStencilElement {
}
var HTMLIonSelectOptionElement: {
@@ -4781,6 +4785,7 @@ declare global {
"ion-segment-content": HTMLIonSegmentContentElement;
"ion-segment-view": HTMLIonSegmentViewElement;
"ion-select": HTMLIonSelectElement;
"ion-select-modal": HTMLIonSelectModalElement;
"ion-select-option": HTMLIonSelectOptionElement;
"ion-select-popover": HTMLIonSelectPopoverElement;
"ion-skeleton-text": HTMLIonSkeletonTextElement;
@@ -6423,7 +6428,7 @@ declare namespace LocalJSX {
/**
* Emitted when the menu is closed.
*/
"onIonDidClose"?: (event: IonMenuCustomEvent<void>) => void;
"onIonDidClose"?: (event: IonMenuCustomEvent<MenuCloseEventDetail>) => void;
/**
* Emitted when the menu is open.
*/
@@ -6435,7 +6440,7 @@ declare namespace LocalJSX {
/**
* Emitted when the menu is about to be closed.
*/
"onIonWillClose"?: (event: IonMenuCustomEvent<void>) => void;
"onIonWillClose"?: (event: IonMenuCustomEvent<MenuCloseEventDetail>) => void;
/**
* Emitted when the menu is about to be opened.
*/
@@ -7495,10 +7500,6 @@ declare namespace LocalJSX {
* If `true`, the segment buttons will overflow and the user can swipe to see them. In addition, this will disable the gesture to drag the indicator between the buttons in order to swipe to see hidden buttons.
*/
"scrollable"?: boolean;
/**
* The `id` of the `segment-view` element to be associated with this segment.
*/
"segmentViewId"?: string;
/**
* If `true`, navigating to an `ion-segment-button` with the keyboard will focus and select the element. If `false`, keyboard navigation will only focus the `ion-segment-button` element.
*/
@@ -7521,7 +7522,6 @@ declare namespace LocalJSX {
* If `true`, the user cannot interact with the segment button.
*/
"disabled"?: boolean;
"hasIndicator"?: boolean;
/**
* Set the layout of the text and icon in the segment.
*/
@@ -7540,10 +7540,6 @@ declare namespace LocalJSX {
"value"?: SegmentValue;
}
interface IonSegmentContent {
/**
* If `true`, the segment content will not be displayed.
*/
"disabled"?: boolean;
}
interface IonSegmentView {
/**
@@ -7553,16 +7549,7 @@ declare namespace LocalJSX {
/**
* Emitted when the segment view is scrolled.
*/
"onIonSegmentViewScroll"?: (event: IonSegmentViewCustomEvent<{
scrollDirection: string;
scrollDistance: number;
scrollDistancePercentage: number;
}>) => void;
/**
* Emitted when the segment view scroll has ended.
*/
"onIonSegmentViewScrollEnd"?: (event: IonSegmentViewCustomEvent<{ activeContentId: string }>) => void;
"onIonSegmentViewScrollStart"?: (event: IonSegmentViewCustomEvent<void>) => void;
"onIonSegmentViewScroll"?: (event: IonSegmentViewCustomEvent<SegmentViewScrollEvent>) => void;
}
interface IonSelect {
/**
@@ -7590,11 +7577,11 @@ declare namespace LocalJSX {
*/
"fill"?: 'outline' | 'solid';
/**
* The interface the select should use: `action-sheet`, `popover` or `alert`.
* The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`.
*/
"interface"?: SelectInterface;
/**
* Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet) and the [ion-popover docs](./popover) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface.
* Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet), the [ion-popover docs](./popover), and the [ion-modal docs](./modal) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface.
*/
"interfaceOptions"?: any;
/**
@@ -7670,6 +7657,11 @@ declare namespace LocalJSX {
*/
"value"?: any | null;
}
interface IonSelectModal {
"header"?: string;
"multiple"?: boolean;
"options"?: SelectModalOption[];
}
interface IonSelectOption {
/**
* If `true`, the user cannot interact with the select option. This property does not apply when `interface="action-sheet"` as `ion-action-sheet` does not allow for disabled buttons.
@@ -8258,6 +8250,7 @@ declare namespace LocalJSX {
"ion-segment-content": IonSegmentContent;
"ion-segment-view": IonSegmentView;
"ion-select": IonSelect;
"ion-select-modal": IonSelectModal;
"ion-select-option": IonSelectOption;
"ion-select-popover": IonSelectPopover;
"ion-skeleton-text": IonSkeletonText;
@@ -8359,6 +8352,7 @@ declare module "@stencil/core" {
"ion-segment-content": LocalJSX.IonSegmentContent & JSXBase.HTMLAttributes<HTMLIonSegmentContentElement>;
"ion-segment-view": LocalJSX.IonSegmentView & JSXBase.HTMLAttributes<HTMLIonSegmentViewElement>;
"ion-select": LocalJSX.IonSelect & JSXBase.HTMLAttributes<HTMLIonSelectElement>;
"ion-select-modal": LocalJSX.IonSelectModal & JSXBase.HTMLAttributes<HTMLIonSelectModalElement>;
"ion-select-option": LocalJSX.IonSelectOption & JSXBase.HTMLAttributes<HTMLIonSelectOptionElement>;
"ion-select-popover": LocalJSX.IonSelectPopover & JSXBase.HTMLAttributes<HTMLIonSelectPopoverElement>;
"ion-skeleton-text": LocalJSX.IonSkeletonText & JSXBase.HTMLAttributes<HTMLIonSkeletonTextElement>;

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

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

View File

@@ -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'],
});
}

View File

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

View File

@@ -51,7 +51,6 @@ export class Backdrop implements ComponentInterface {
const mode = getIonMode(this);
return (
<Host
tabindex="-1"
aria-hidden="true"
class={{
[mode]: true,

View File

@@ -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();
}

View File

@@ -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');
}
});
}
};

View File

@@ -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');
});
});
});

View File

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

View File

@@ -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', () => {

View File

@@ -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>
);
}

View File

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

View File

@@ -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);
}
}

View File

@@ -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');

View File

@@ -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');

View File

@@ -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 = '';

View File

@@ -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>
);
}

View File

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

View File

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

View File

@@ -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();
}

View File

@@ -2,7 +2,7 @@ import type { ComponentInterface } from '@stencil/core';
import { Component, Element, Host, Prop, Method, State, Watch, forceUpdate, h } from '@stencil/core';
import type { ButtonInterface } from '@utils/element-interface';
import type { Attributes } from '@utils/helpers';
import { addEventListener, removeEventListener, inheritAttributes } from '@utils/helpers';
import { addEventListener, removeEventListener, inheritAttributes, getNextSiblingOfType } from '@utils/helpers';
import { hostContext } from '@utils/theme';
import { getIonMode } from '../../global/ionic-global';
@@ -65,9 +65,41 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
this.updateState();
}
@Prop() hasIndicator = true;
private waitForSegmentContent(ionSegment: HTMLIonSegmentElement | null, contentId: string): Promise<HTMLElement> {
return new Promise((resolve, reject) => {
let timeoutId: NodeJS.Timeout | undefined = undefined;
let animationFrameId: number;
connectedCallback() {
const check = () => {
if (!ionSegment) {
reject(new Error(`Segment not found when looking for Segment Content`));
return;
}
const segmentView = getNextSiblingOfType<HTMLIonSegmentViewElement>(ionSegment); // Skip the text nodes
const segmentContent = segmentView?.querySelector(
`ion-segment-content[id="${contentId}"]`
) as HTMLIonSegmentContentElement | null;
if (segmentContent && timeoutId) {
clearTimeout(timeoutId); // Clear the timeout if the segmentContent is found
cancelAnimationFrame(animationFrameId);
resolve(segmentContent);
} else {
animationFrameId = requestAnimationFrame(check); // Keep checking on the next animation frame
}
};
check();
// Set a timeout to reject the promise
timeoutId = setTimeout(() => {
cancelAnimationFrame(animationFrameId);
reject(new Error(`Unable to find Segment Content with id="${contentId} within 1000 ms`));
}, 1000);
});
}
async connectedCallback() {
const segmentEl = (this.segmentEl = this.el.closest('ion-segment'));
if (segmentEl) {
this.updateState();
@@ -78,12 +110,13 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
// Return if there is no contentId defined
if (!this.contentId) return;
// Attempt to find the Segment Content by its contentId
const segmentContent = document.getElementById(this.contentId) as HTMLIonSegmentContentElement | null;
// If no associated Segment Content exists, log an error and return
if (!segmentContent) {
console.error(`Segment Button: Unable to find Segment Content with id="${this.contentId}".`);
let segmentContent;
try {
// Attempt to find the Segment Content by its contentId
segmentContent = await this.waitForSegmentContent(segmentEl, this.contentId);
} catch (error) {
// If no associated Segment Content exists, log an error and return
console.error('Segment Button: ', (error as Error).message);
return;
}
@@ -93,8 +126,11 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
return;
}
// Set the disabled state of the Segment Content based on the button's disabled state
segmentContent.disabled = this.disabled;
// Prevent buttons from being disabled when associated with segment content
if (this.disabled) {
console.warn(`Segment Button: Segment buttons cannot be disabled when associated with an <ion-segment-content>.`);
this.disabled = false;
}
}
disconnectedCallback() {
@@ -189,11 +225,9 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
</span>
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
</button>
{this.hasIndicator && (
<div part="indicator" class="segment-button-indicator segment-button-indicator-animated">
<div part="indicator-background" class="segment-button-indicator-background"></div>
</div>
)}
<div part="indicator" class="segment-button-indicator segment-button-indicator-animated">
<div part="indicator-background" class="segment-button-indicator-background"></div>
</div>
</Host>
);
}

View File

@@ -1,5 +0,0 @@
@import "./segment-content";
@import "../segment-button/segment-button.ios.vars";
// iOS Segment Content
// --------------------------------------------------

View File

@@ -1,5 +0,0 @@
@import "./segment-content";
@import "../segment-button/segment-button.md.vars";
// Material Design Segment Content
// --------------------------------------------------

View File

@@ -9,7 +9,3 @@
width: 100%;
}
:host(.segment-content-disabled) {
display: none;
}

View File

@@ -1,29 +1,15 @@
import type { ComponentInterface } from '@stencil/core';
import { Component, Host, Prop, h } from '@stencil/core';
import { Component, Host, h } from '@stencil/core';
@Component({
tag: 'ion-segment-content',
styleUrls: {
ios: 'segment-content.ios.scss',
md: 'segment-content.md.scss',
},
styleUrl: 'segment-content.scss',
shadow: true,
})
export class SegmentContent implements ComponentInterface {
/**
* If `true`, the segment content will not be displayed.
*/
@Prop() disabled = false;
render() {
const { disabled } = this;
return (
<Host
class={{
'segment-content-disabled': disabled,
}}
>
<Host>
<slot></slot>
</Host>
);

View File

@@ -0,0 +1,4 @@
export interface SegmentViewScrollEvent {
scrollRatio: number;
isManualScroll: boolean;
}

View File

@@ -9,8 +9,6 @@
overflow-x: scroll;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
/* Hide scrollbar in Firefox */
scrollbar-width: none;
@@ -27,3 +25,7 @@
touch-action: none;
overflow-x: hidden;
}
:host(.segment-view-scroll-disabled) {
pointer-events: none;
}

View File

@@ -1,5 +1,7 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Listen, Method, Prop, h } from '@stencil/core';
import { Component, Element, Event, Host, Listen, Method, Prop, State, h } from '@stencil/core';
import type { SegmentViewScrollEvent } from './segment-view-interface';
@Component({
tag: 'ion-segment-view',
@@ -10,8 +12,6 @@ import { Component, Element, Event, Host, Listen, Method, Prop, h } from '@stenc
shadow: true,
})
export class SegmentView implements ComponentInterface {
private initialScrollLeft?: number;
private previousScrollLeft = 0;
private scrollEndTimeout: ReturnType<typeof setTimeout> | null = null;
private isTouching = false;
@@ -23,69 +23,31 @@ export class SegmentView implements ComponentInterface {
@Prop() disabled = false;
/**
* Emitted when the segment view is scrolled.
* @internal
*
* If `true`, the segment view is scrollable.
* If `false`, pointer events will be disabled. This is to prevent issues with
* quickly scrolling after interacting with a segment button.
*/
@Event() ionSegmentViewScroll!: EventEmitter<{
scrollDirection: string;
scrollDistance: number;
scrollDistancePercentage: number;
}>;
@State() isManualScroll?: boolean;
/**
* Emitted when the segment view scroll has ended.
* Emitted when the segment view is scrolled.
*/
@Event() ionSegmentViewScrollEnd!: EventEmitter<{ activeContentId: string }>;
@Event() ionSegmentViewScrollStart!: EventEmitter<void>;
private activeContentId = '';
@Event() ionSegmentViewScroll!: EventEmitter<SegmentViewScrollEvent>;
@Listen('scroll')
handleScroll(ev: Event) {
const { previousScrollLeft } = this;
const { scrollLeft, offsetWidth } = ev.target as HTMLElement;
const { scrollLeft, scrollWidth, clientWidth } = ev.target as HTMLElement;
const scrollRatio = scrollLeft / (scrollWidth - clientWidth);
// Set initial scroll position if it's undefined
// Must be a multiple of the offset width
if (this.initialScrollLeft === undefined) {
this.initialScrollLeft = Math.round(scrollLeft / offsetWidth) * offsetWidth;
}
// Determine the scroll direction based on the previous scroll position
const scrollDirection = scrollLeft > previousScrollLeft ? 'right' : 'left';
this.previousScrollLeft = scrollLeft;
// Calculate the distance scrolled based on the initial scroll position
// and then transform it to a percentage of the segment view width
const scrollDistance = scrollLeft - this.initialScrollLeft;
const scrollDistancePercentage = Math.abs(scrollDistance) / offsetWidth;
// Emit the scroll direction and distance
this.ionSegmentViewScroll.emit({
scrollDirection,
scrollDistance,
scrollDistancePercentage,
scrollRatio,
isManualScroll: this.isManualScroll ?? true,
});
// Check if the scroll is at a snapping point and return if not
const atSnappingPoint = scrollLeft % offsetWidth === 0;
if (!atSnappingPoint) return;
// Find the current segment content based on the scroll position
const currentIndex = Math.round(scrollLeft / offsetWidth);
// // Update active content ID and scroll to the segment content
const activeContent = this.getSegmentContents().filter(
(ref) => !ref.classList.contains('segment-content-disabled')
)[currentIndex];
this.activeContentId = activeContent.id;
// Only emit scroll end event if the active content is not disabled and
// the user is not touching the segment view
if (activeContent?.disabled === false && !this.isTouching) {
this.ionSegmentViewScrollEnd.emit({ activeContentId: this.activeContentId });
this.initialScrollLeft = undefined;
}
// Reset the timeout to check for scroll end
this.resetScrollEndTimeout();
}
/**
@@ -93,8 +55,6 @@ export class SegmentView implements ComponentInterface {
*/
@Listen('touchstart')
handleScrollStart() {
this.ionSegmentViewScrollStart.emit();
if (this.scrollEndTimeout) {
clearTimeout(this.scrollEndTimeout);
this.scrollEndTimeout = null;
@@ -112,9 +72,45 @@ export class SegmentView implements ComponentInterface {
}
/**
* Reset the scroll end detection timer. This is called on every scroll event.
*/
private resetScrollEndTimeout() {
if (this.scrollEndTimeout) {
clearTimeout(this.scrollEndTimeout);
this.scrollEndTimeout = null;
}
this.scrollEndTimeout = setTimeout(
() => {
this.checkForScrollEnd();
},
// Setting this to a lower value may result in inconsistencies in behavior
// across browsers (particularly Firefox).
// Ideally, all of this logic is removed once the scroll end event is
// supported on all browsers (https://caniuse.com/?search=scrollend)
100
);
}
/**
* Check if the scroll has ended and the user is not actively touching.
* If the conditions are met (active content is enabled and no active touch),
* reset the scroll position and emit the scroll end event.
*/
private checkForScrollEnd() {
// Only emit scroll end event if the active content is not disabled and
// the user is not touching the segment view
if (!this.isTouching) {
this.isManualScroll = undefined;
}
}
/**
* @internal
*
* This method is used to programmatically set the displayed segment content
* in the segment view. Calling this method will update the `value` of the
* corresponding segment button.
*
* @param id: The id of the segment content to display.
* @param smoothScroll: Whether to animate the scroll transition.
*/
@@ -125,6 +121,9 @@ export class SegmentView implements ComponentInterface {
if (index === -1) return;
this.isManualScroll = false;
this.resetScrollEndTimeout();
const contentWidth = this.el.offsetWidth;
this.el.scrollTo({
top: 0,
@@ -138,12 +137,13 @@ export class SegmentView implements ComponentInterface {
}
render() {
const { disabled } = this;
const { disabled, isManualScroll } = this;
return (
<Host
class={{
'segment-view-disabled': disabled,
'segment-view-scroll-disabled': isManualScroll === false,
}}
>
<slot></slot>

View File

@@ -16,6 +16,8 @@
<style>
ion-segment-view {
height: 100px;
margin-bottom: 20px;
}
ion-segment-content {
@@ -24,15 +26,15 @@
align-items: center;
}
ion-segment-content:nth-of-type(1) {
ion-segment-content:nth-of-type(3n + 1) {
background: lightpink;
}
ion-segment-content:nth-of-type(2) {
ion-segment-content:nth-of-type(3n + 2) {
background: lightblue;
}
ion-segment-content:nth-of-type(3) {
ion-segment-content:nth-of-type(3n + 3) {
background: lightgreen;
}
</style>
@@ -60,24 +62,11 @@
<ion-segment-content id="value">Value</ion-segment-content>
</ion-segment-view>
<ion-segment value="all">
<ion-segment-button content-id="all" value="all">
<ion-label>All</ion-label>
</ion-segment-button>
<ion-segment-button content-id="favorites" value="favorites">
<ion-label>Favorites</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="all">All</ion-segment-content>
<ion-segment-content id="favorites">Favorites</ion-segment-content>
</ion-segment-view>
<ion-segment value="free">
<ion-segment-button content-id="paid" value="paid">
<ion-label>Paid</ion-label>
</ion-segment-button>
<ion-segment-button content-id="free" value="free">
<ion-segment-button style="min-width: 200px" content-id="free" value="free">
<ion-label>Free</ion-label>
</ion-segment-button>
<ion-segment-button content-id="top" value="top">
@@ -90,9 +79,52 @@
<ion-segment-content id="top">Top</ion-segment-content>
</ion-segment-view>
<ion-segment value="peach" scrollable>
<ion-segment-button content-id="orange" value="orange">
<ion-label>Orange</ion-label>
</ion-segment-button>
<ion-segment-button content-id="banana" value="banana">
<ion-label>Banana</ion-label>
</ion-segment-button>
<ion-segment-button content-id="pear" value="pear">
<ion-label>Pear</ion-label>
</ion-segment-button>
<ion-segment-button content-id="peach" value="peach">
<ion-label>Peach</ion-label>
</ion-segment-button>
<ion-segment-button content-id="grape" value="grape">
<ion-label>Grape</ion-label>
</ion-segment-button>
<ion-segment-button content-id="mango" value="mango">
<ion-label>Mango</ion-label>
</ion-segment-button>
<ion-segment-button content-id="apple" value="apple">
<ion-label>Apple</ion-label>
</ion-segment-button>
<ion-segment-button content-id="strawberry" value="strawberry">
<ion-label>Strawberry</ion-label>
</ion-segment-button>
<ion-segment-button content-id="cherry" value="cherry">
<ion-label>Cherry</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="orange">Orange</ion-segment-content>
<ion-segment-content id="banana">Banana</ion-segment-content>
<ion-segment-content id="pear">Pear</ion-segment-content>
<ion-segment-content id="peach">Peach</ion-segment-content>
<ion-segment-content id="grape">Grape</ion-segment-content>
<ion-segment-content id="mango">Mango</ion-segment-content>
<ion-segment-content id="apple">Apple</ion-segment-content>
<ion-segment-content id="strawberry">Strawberry</ion-segment-content>
<ion-segment-content id="cherry">Cherry</ion-segment-content>
</ion-segment-view>
<button class="expand" onClick="changeSegmentContent()">Change Segment Content</button>
<button class="expand" onClick="clearSegmentValue()">Clear Segment Value</button>
<button class="expand" onClick="addSegmentButtonAndContent()">Add New Segment Button & Content</button>
</ion-content>
<ion-footer>
@@ -114,7 +146,7 @@
currentValue = 'value';
}
segmentView.setContent(currentValue);
segment.value = currentValue;
}
async function clearSegmentValue() {
@@ -128,6 +160,34 @@
segment.value = undefined;
});
}
async function addSegmentButtonAndContent() {
const segment = document.querySelector('ion-segment');
const segmentView = document.querySelector('ion-segment-view');
const newButton = document.createElement('ion-segment-button');
const newId = `new-${Date.now()}`;
newButton.setAttribute('content-id', newId);
newButton.setAttribute('value', newId);
newButton.innerHTML = '<ion-label>New Button</ion-label>';
segment.appendChild(newButton);
setTimeout(() => {
// Timeout to test waitForSegmentContent() in segment-button
const newContent = document.createElement('ion-segment-content');
newContent.setAttribute('id', newId);
newContent.innerHTML = 'New Content';
segmentView.appendChild(newContent);
// Necessary timeout to ensure the value is set after the content is added.
// Otherwise, the transition is unsuccessful and the content is not shown.
setTimeout(() => {
segment.setAttribute('value', newId);
}, 200);
}, 200);
}
</script>
</ion-app>
</body>

View File

@@ -83,10 +83,91 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
config
);
await page.click('ion-segment-button[value="top"]');
await page.locator('ion-segment-button[value="top"]').click();
const segmentContent = page.locator('ion-segment-content[id="top"]');
await expect(segmentContent).toBeInViewport();
});
});
test('should set correct segment button as checked when changing the value by scrolling the segment content', async ({
page,
}) => {
await page.setContent(
`
<ion-segment value="free">
<ion-segment-button content-id="paid" value="paid">
<ion-label>Paid</ion-label>
</ion-segment-button>
<ion-segment-button content-id="free" value="free">
<ion-label>Free</ion-label>
</ion-segment-button>
<ion-segment-button content-id="top" value="top">
<ion-label>Top</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="paid">Paid</ion-segment-content>
<ion-segment-content id="free">Free</ion-segment-content>
<ion-segment-content id="top">Top</ion-segment-content>
</ion-segment-view>
`,
config
);
await page
.locator('ion-segment-view')
.evaluate(
(segmentView: HTMLIonSegmentViewElement) => !segmentView.classList.contains('segment-view-scroll-disabled')
);
await page.waitForChanges();
await page.locator('ion-segment-content[id="top"]').scrollIntoViewIfNeeded();
const segmentButton = page.locator('ion-segment-button[value="top"]');
await expect(segmentButton).toHaveClass(/segment-button-checked/);
});
test('should set correct segment button as checked and show correct content when programmatically setting the segment vale', async ({
page,
}) => {
await page.setContent(
`
<ion-segment value="free">
<ion-segment-button content-id="paid" value="paid">
<ion-label>Paid</ion-label>
</ion-segment-button>
<ion-segment-button content-id="free" value="free">
<ion-label>Free</ion-label>
</ion-segment-button>
<ion-segment-button content-id="top" value="top">
<ion-label>Top</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="paid">Paid</ion-segment-content>
<ion-segment-content id="free">Free</ion-segment-content>
<ion-segment-content id="top">Top</ion-segment-content>
</ion-segment-view>
`,
config
);
await page
.locator('ion-segment-view')
.evaluate(
(segmentView: HTMLIonSegmentViewElement) => !segmentView.classList.contains('segment-view-scroll-disabled')
);
await page.waitForChanges();
await page.locator('ion-segment').evaluate((segment: HTMLIonSegmentElement) => (segment.value = 'top'));
const segmentButton = page.locator('ion-segment-button[value="top"]');
await expect(segmentButton).toHaveClass(/segment-button-checked/);
const segmentContent = page.locator('ion-segment-content[id="top"]');
await expect(segmentContent).toBeInViewport();
});
});

View File

@@ -64,11 +64,11 @@
<ion-segment-content id="favorites">Favorites</ion-segment-content>
</ion-segment-view>
<ion-segment value="paid">
<ion-segment disabled value="paid">
<ion-segment-button content-id="paid" value="paid">
<ion-label>Paid</ion-label>
</ion-segment-button>
<ion-segment-button disabled content-id="free" value="free">
<ion-segment-button content-id="free" value="free">
<ion-label>Free</ion-label>
</ion-segment-button>
<ion-segment-button content-id="top" value="top">
@@ -81,28 +81,7 @@
<ion-segment-content id="top">Top</ion-segment-content>
</ion-segment-view>
<ion-segment value="a">
<ion-segment-button content-id="a" value="a">
<ion-label>a</ion-label>
</ion-segment-button>
<ion-segment-button disabled content-id="b" value="b">
<ion-label>b</ion-label>
</ion-segment-button>
<ion-segment-button disabled content-id="c" value="c">
<ion-label>c</ion-label>
</ion-segment-button>
<ion-segment-button content-id="d" value="d">
<ion-label>d</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="a">a</ion-segment-content>
<ion-segment-content id="b">b</ion-segment-content>
<ion-segment-content id="c">c</ion-segment-content>
<ion-segment-content id="d">d</ion-segment-content>
</ion-segment-view>
<ion-segment disabled value="reading-list">
<ion-segment value="reading-list">
<ion-segment-button content-id="bookmarks" value="bookmarks">
<ion-label>Bookmarks</ion-label>
</ion-segment-button>

View File

@@ -7,28 +7,9 @@ import { configs, test } from '@utils/test/playwright';
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('segment-view: disabled'), () => {
test('should not have visual regressions', async ({ page }) => {
await page.setContent(
`
<style>
/* Styles are here to show the disabled state */
ion-segment-view {
height: 100px;
background: #3880ff;
}
</style>
await page.goto('/src/components/segment-view/test/disabled', config);
<ion-segment-view disabled>
<ion-segment-content id="paid">Paid</ion-segment-content>
<ion-segment-content id="free">Free</ion-segment-content>
<ion-segment-content id="top">Top</ion-segment-content>
</ion-segment-view>
`,
config
);
const segment = page.locator('ion-segment-view');
await expect(segment).toHaveScreenshot(screenshot(`segment-view-disabled`));
await expect(page).toHaveScreenshot(screenshot(`segment-view-disabled`));
});
});
});
@@ -38,14 +19,14 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('segment-view: disabled'), () => {
test('should show the second content when first segment content is disabled', async ({ page }) => {
test('should keep button enabled even when disabled prop is set', async ({ page }) => {
await page.setContent(
`
<ion-segment disabled>
<ion-segment>
<ion-segment-button content-id="paid" value="paid">
<ion-label>Paid</ion-label>
</ion-segment-button>
<ion-segment-button content-id="free" value="free">
<ion-segment-button disabled content-id="free" value="free">
<ion-label>Free</ion-label>
</ion-segment-button>
<ion-segment-button content-id="top" value="top">
@@ -61,41 +42,8 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
config
);
const segmentContent = page.locator('ion-segment-content[id="free"]');
await expect(segmentContent).toBeInViewport();
});
test('should scroll to the third content and update the segment value when the second segment content is disabled', async ({
page,
}) => {
await page.setContent(
`
<ion-segment>
<ion-segment-button content-id="paid" value="paid">
<ion-label>Paid</ion-label>
</ion-segment-button>
<ion-segment-button disabled content-id="free" value="free">
<ion-label>Free</ion-label>
</ion-segment-button>
<ion-segment-button content-id="top" value="top">
<ion-label>Top</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="paid">Paid</ion-segment-content>
<ion-segment-content disabled id="free">Free</ion-segment-content>
<ion-segment-content id="top">Top</ion-segment-content>
</ion-segment-view>
`,
config
);
const segmentContent = page.locator('ion-segment-content[id="top"]');
await segmentContent.scrollIntoViewIfNeeded();
await expect(segmentContent).toBeInViewport();
const segment = page.locator('ion-segment');
expect(await segment.evaluate((el: HTMLIonSegmentElement) => el.value)).toBe('top');
const segmentButton = page.locator('ion-segment-button[value="free"]');
await expect(segmentButton).not.toHaveClass(/segment-button-disabled/);
});
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -6,14 +6,15 @@
:host {
--background: #{$segment-ios-background-color};
--indicator-height: 100%;
--indicator-box-shadow: 0 0 5px rgba(0, 0, 0, 0.16) @include border-radius($segment-ios-border-radius);
@include border-radius($segment-ios-border-radius);
overflow: hidden;
z-index: 0;
}
// Segment: Color
// --------------------------------------------------
@@ -21,6 +22,7 @@
background: #{current-color(base, 0.065)};
}
// Segment: Default Toolbar
// --------------------------------------------------
@@ -35,6 +37,7 @@
background: var(--ion-toolbar-segment-background, var(--background));
}
// Segment: Color Toolbar
// --------------------------------------------------
@@ -42,14 +45,3 @@
:host(.in-toolbar-color:not(.ion-color)) {
background: #{current-color(contrast, 0.11)};
}
.segment-indicator {
@include position(0, 0, 0, 0);
padding: 0 2px;
.segment-indicator-background {
transform: scale(0.95);
border-radius: 7px;
}
}

View File

@@ -6,7 +6,6 @@
:host {
--background: transparent;
--indicator-height: 2px;
grid-auto-columns: minmax(auto, $segment-button-md-max-width);
}
@@ -24,13 +23,10 @@
min-height: var(--min-height);
}
// Segment: Scrollable
// --------------------------------------------------
:host(.segment-scrollable) ::slotted(ion-segment-button) {
min-width: auto;
}
.segment-indicator {
@include position(null, 0, 0, 0);
}

View File

@@ -31,24 +31,9 @@
contain: paint;
user-select: none;
.segment-indicator {
@include transform-origin(left);
z-index: -1;
position: absolute;
.segment-indicator-background {
height: var(--indicator-height);
box-shadow: var(--indicator-box-shadow);
background-color: var(--indicator-color);
transition: background-color 0.3s linear;
}
}
}
// Segment: Scrollable
// --------------------------------------------------

View File

@@ -7,6 +7,7 @@ import { createColorClasses, hostContext } from '@utils/theme';
import { getIonMode } from '../../global/ionic-global';
import type { Color, StyleEventDetail } from '../../interface';
import type { SegmentViewScrollEvent } from '../segment-view/segment-view-interface';
import type { SegmentChangeEventDetail, SegmentValue } from './segment-interface';
@@ -28,20 +29,19 @@ export class Segment implements ComponentInterface {
private valueBeforeGesture?: SegmentValue;
private segmentViewEl?: HTMLIonSegmentViewElement | null = null;
private lastNextIndex?: number;
private nextButtonIndex?: number;
private io?: IntersectionObserver;
/**
* Whether to update the segment view, if exists, when the value changes.
* This behavior is enabled by default, but is set false when scrolling content views
* since we don't want to "double scroll" the segment view.
*/
private triggerScrollOnValueChange?: boolean;
@Element() el!: HTMLIonSegmentElement;
@State() activated = false;
/**
* The `id` of the `segment-view` element to be associated with this segment.
*/
@Prop() segmentViewId?: string;
/**
* The color to use from your application's color palette.
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
@@ -90,6 +90,12 @@ export class Segment implements ComponentInterface {
@Watch('value')
protected valueChanged(value: SegmentValue | undefined, oldValue?: SegmentValue | undefined) {
// Force a value to exist if we're using a segment view
if (this.segmentViewEl && value === undefined) {
this.value = this.getButtons()[0].value;
return;
}
if (oldValue !== undefined && value !== undefined) {
const buttons = this.getButtons();
const previous = buttons.find((button) => button.value === oldValue);
@@ -98,10 +104,12 @@ export class Segment implements ComponentInterface {
if (previous && current) {
if (!this.segmentViewEl) {
this.checkButton(previous, current);
} else {
this.setCheckedClasses();
} else if (this.triggerScrollOnValueChange !== false) {
this.updateSegmentView();
}
}
} else if (value !== undefined && oldValue === undefined && this.segmentViewEl) {
this.updateSegmentView();
}
/**
@@ -110,10 +118,12 @@ export class Segment implements ComponentInterface {
*/
this.ionSelect.emit({ value });
// The scroll listener should handle scrolling the active button into view as needed when there is a segment view
// The scroll listener should handle scrolling the active button into view as needed
if (!this.segmentViewEl) {
this.scrollActiveButtonIntoView();
}
this.triggerScrollOnValueChange = undefined;
}
/**
@@ -147,9 +157,13 @@ export class Segment implements ComponentInterface {
disabledChanged() {
this.gestureChanged();
const buttons = this.getButtons();
for (const button of buttons) {
button.disabled = this.disabled;
if (!this.segmentViewEl) {
const buttons = this.getButtons();
for (const button of buttons) {
button.disabled = this.disabled;
}
} else {
this.segmentViewEl.disabled = this.disabled;
}
}
@@ -163,22 +177,10 @@ export class Segment implements ComponentInterface {
this.emitStyle();
this.segmentViewEl = this.getSegmentView();
if (this.segmentViewEl) {
// Disable each button indicator when using a segment view
// Instead, a single indicator instance will be used
this.getButtons().forEach((ref) => (ref.hasIndicator = false));
this.addIntersectionObserver();
}
}
disconnectedCallback() {
this.segmentViewEl = null;
if (this.io) {
this.io.disconnect();
this.io = undefined;
}
}
componentWillLoad() {
@@ -186,6 +188,8 @@ export class Segment implements ComponentInterface {
}
async componentDidLoad() {
this.segmentViewEl = this.getSegmentView();
this.setCheckedClasses();
/**
@@ -217,6 +221,10 @@ export class Segment implements ComponentInterface {
if (this.disabled) {
this.disabledChanged();
}
// Update segment view based on the initial value,
// but do not animate the scroll
this.updateSegmentView(false);
}
onStart(detail: GestureDetail) {
@@ -238,69 +246,13 @@ export class Segment implements ComponentInterface {
const value = this.value;
if (value !== undefined) {
if (this.valueBeforeGesture !== value) {
if (this.segmentViewEl) {
this.updateSegmentView(value);
} else {
this.emitValueChange();
}
this.emitValueChange();
this.updateSegmentView();
}
}
this.valueBeforeGesture = undefined;
}
private distanceToButton(index: number) {
const buttons = this.getButtons();
const button = buttons[index];
return button.offsetLeft;
}
private addIntersectionObserver() {
if (
typeof (window as any) !== 'undefined' &&
'IntersectionObserver' in window &&
'IntersectionObserverEntry' in window &&
'isIntersecting' in window.IntersectionObserverEntry.prototype
) {
this.io = new IntersectionObserver((data) => {
/**
* On slower devices, it is possible for an intersection observer entry to contain multiple
* objects in the array. This happens when quickly scrolling an image into view and then out of
* view. In this case, the last object represents the current state of the component.
*/
if (data[data.length - 1].isIntersecting) {
this.updateSegmentView(this.value!, false);
this.initIndicator();
}
});
this.io.observe(this.el);
}
}
private initIndicator() {
writeTask(() => {
if (this.segmentViewEl) {
const buttons = this.getButtons();
const activeButtonIndex = buttons.findIndex((ref) => ref.value === this.value);
if (activeButtonIndex >= 0) {
const activeButtonPosition = buttons[activeButtonIndex].getBoundingClientRect();
const activeButtonStyles = getComputedStyle(buttons[activeButtonIndex]);
const indicator = this.el.shadowRoot!.querySelector('.segment-indicator') as HTMLDivElement | null;
if (indicator) {
const startingX = this.distanceToButton(activeButtonIndex);
indicator.style.width = `${activeButtonPosition.width}px`;
indicator.style.left = `${startingX}px`;
// Setting a CSS variable works around issue where background element might not be rendered yet
this.el.style.setProperty('--indicator-color', activeButtonStyles.getPropertyValue('--indicator-color'));
}
}
}
});
}
/**
* Emits an `ionChange` event.
*
@@ -393,6 +345,8 @@ export class Segment implements ComponentInterface {
// Remove the transform to slide the indicator back to the button clicked
currentIndicator.style.setProperty('transform', '');
this.scrollActiveButtonIntoView(true);
});
this.value = current.value;
@@ -413,27 +367,23 @@ export class Segment implements ComponentInterface {
}
private getSegmentView() {
if (!this.segmentViewId) {
return null;
}
const segmentViewEl = document.getElementById(this.segmentViewId);
if (!segmentViewEl) {
console.warn(`Segment: Unable to find 'ion-segment-view' with id="${this.segmentViewId}"`);
return null;
}
if (segmentViewEl.tagName !== 'ION-SEGMENT-VIEW') {
console.warn(`Segment: Element with id="${this.segmentViewId}" is not an <ion-segment-view> element.`);
return null;
}
return segmentViewEl as HTMLIonSegmentViewElement;
const buttons = this.getButtons();
// Get the first button with a contentId
const firstContentId = buttons.find((button: HTMLIonSegmentButtonElement) => button.contentId);
// Get the segment content with an id matching the button's contentId
const segmentContent = document.querySelector(`ion-segment-content[id="${firstContentId?.contentId}"]`);
// Return the segment view for that matching segment content
return segmentContent?.closest('ion-segment-view');
}
@Listen('ionSegmentViewScroll', { target: 'body' })
handleSegmentViewScroll(ev: CustomEvent) {
handleSegmentViewScroll(ev: CustomEvent<SegmentViewScrollEvent>) {
const { scrollRatio, isManualScroll } = ev.detail;
if (!isManualScroll) {
return;
}
const dispatchedFrom = ev.target as HTMLElement;
const segmentViewEl = this.segmentViewEl as EventTarget;
const segmentEl = this.el;
@@ -441,151 +391,25 @@ export class Segment implements ComponentInterface {
// Only update the indicator if the event was dispatched from the correct segment view
if (ev.composedPath().includes(segmentViewEl) || dispatchedFrom?.contains(segmentEl)) {
const buttons = this.getButtons();
const currentIndex = buttons.findIndex((button) => button.value === this.value);
const currentButton = buttons[currentIndex];
const indicator = this.el.shadowRoot!.querySelector('.segment-indicator') as HTMLDivElement | null;
// If no buttons are found or there is no value set then do nothing
if (!buttons.length || this.value === undefined || !indicator) return;
if (!buttons.length) return;
const { scrollDistancePercentage, scrollDistance } = ev.detail;
const index = buttons.findIndex((button) => button.value === this.value);
const current = buttons[index];
const findIndexFrom = (
array: HTMLIonSegmentButtonElement[],
predicate: (button: HTMLIonSegmentButtonElement) => boolean,
startIndex: number
) => {
for (let i = startIndex; i < array.length; i++) {
if (predicate(array[i])) {
return i;
}
}
return -1;
};
const nextIndex = Math.round(scrollRatio * (buttons.length - 1));
const findIndexFromReverse = (
array: HTMLIonSegmentButtonElement[],
predicate: (button: HTMLIonSegmentButtonElement) => boolean,
startIndex: number
) => {
for (let i = startIndex; i >= 0; i--) {
if (predicate(array[i])) {
return i;
}
}
return -1;
};
if (this.lastNextIndex === undefined || this.lastNextIndex !== nextIndex) {
this.lastNextIndex = nextIndex;
this.triggerScrollOnValueChange = false;
// Find the next valid button (i.e. we need to ignore any disabled buttons)
const indexOffset = Math.ceil(scrollDistancePercentage);
const nextIndex =
this.nextButtonIndex ??
(scrollDistance > 0
? findIndexFrom(buttons, (ref) => !ref.disabled, currentIndex + indexOffset)
: findIndexFromReverse(buttons, (ref) => !ref.disabled, currentIndex - indexOffset));
if (nextIndex >= 0 && nextIndex < buttons.length) {
// Figure out the number of disabled buttons between the current and next button
const disabledButtons = (
nextIndex > currentIndex ? buttons.slice(currentIndex, nextIndex) : buttons.slice(nextIndex, currentIndex)
).filter((button) => button.disabled).length;
// Adjust the scroll distance percentage based on the number of "views" scrolled
// We need to do this because all subsequent calculations are based on the assumption that
// only one view can be scrolled at a time, but this is not the case when clicking a segment button
const adjustedScrollDistancePercentage =
scrollDistancePercentage / (Math.abs(nextIndex - currentIndex) - disabledButtons);
const nextButton = buttons[nextIndex];
const nextButtonWidth = nextButton.getBoundingClientRect().width;
const currentButtonWidth = currentButton.getBoundingClientRect().width;
// Scale the width based on the width of the next button
const diff = nextButtonWidth - currentButtonWidth;
const width = currentButtonWidth + diff * adjustedScrollDistancePercentage;
const indicatorStyles = getComputedStyle(indicator);
const indicatorPadding =
parseFloat(indicatorStyles.paddingLeft.replace('px', '')) +
parseFloat(indicatorStyles.paddingRight.replace('px', ''));
indicator.style.width = `${width - indicatorPadding}px`;
// Translate the indicator based on the scroll distance
const distanceToNextButton = this.distanceToButton(nextIndex);
const distanceToCurrentButton = this.distanceToButton(currentIndex);
indicator.style.left =
scrollDistance > 0
? `${
distanceToCurrentButton +
(distanceToNextButton - distanceToCurrentButton) * adjustedScrollDistancePercentage
}px`
: `${
distanceToCurrentButton -
(distanceToCurrentButton - distanceToNextButton) * adjustedScrollDistancePercentage
}px`;
const standardize_color = (str: string) => {
const ctx = document.createElement('canvas').getContext('2d');
ctx!.fillStyle = str;
return ctx!.fillStyle;
};
// Helper function to convert hex to RGB
const hexToRgb = (hex: string) => {
const bigint = parseInt(hex.slice(1), 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return { r, g, b };
};
// Helper function to convert RGB to hex
const rgbToHex = (r: number, g: number, b: number) => {
const componentToHex = (c: number) => {
const hex = c.toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${componentToHex(r)}${componentToHex(g)}${componentToHex(b)}`;
};
// Function to calculate the color in between based on percentage
const interpolateColor = (percentage = adjustedScrollDistancePercentage) => {
const currentColor = standardize_color(getComputedStyle(currentButton).getPropertyValue('--indicator-color'));
const nextColor = standardize_color(getComputedStyle(nextButton).getPropertyValue('--indicator-color'));
const rgb1 = hexToRgb(currentColor);
const rgb2 = hexToRgb(nextColor);
const r = Math.round(rgb1.r + (rgb2.r - rgb1.r) * percentage);
const g = Math.round(rgb1.g + (rgb2.g - rgb1.g) * percentage);
const b = Math.round(rgb1.b + (rgb2.b - rgb1.b) * percentage);
return rgbToHex(r, g, b);
};
indicator.querySelector('div')!.style.backgroundColor = interpolateColor();
if (this.scrollable) {
// Scroll the segment container so the indicator is always in view
indicator.scrollIntoView({
behavior: 'instant',
});
}
this.checkButton(current, buttons[nextIndex]);
this.emitValueChange();
}
}
}
@Listen('ionSegmentViewScrollEnd', { target: 'body' })
onScrollEnd(ev: CustomEvent<{ activeContentId: string }>) {
const dispatchedFrom = ev.target as HTMLElement;
const segmentViewEl = this.segmentViewEl as EventTarget;
const segmentEl = this.el;
if (ev.composedPath().includes(segmentViewEl) || dispatchedFrom?.contains(segmentEl)) {
this.value = ev.detail.activeContentId;
this.nextButtonIndex = undefined;
this.emitValueChange();
}
}
/**
* Finds the related segment view and sets its current content
* based on the selected segment button. This method
@@ -593,9 +417,9 @@ export class Segment implements ComponentInterface {
* after the gesture is completed (if dragging between segments)
* and when a segment button is clicked directly.
*/
private updateSegmentView(value: SegmentValue, smoothScroll = true) {
private updateSegmentView(smoothScroll = true) {
const buttons = this.getButtons();
const button = buttons.find((btn) => btn.value === value);
const button = buttons.find((btn) => btn.value === this.value);
// If the button does not have a contentId then there is
// no associated segment view to update
@@ -603,8 +427,10 @@ export class Segment implements ComponentInterface {
return;
}
if (this.segmentViewEl) {
this.segmentViewEl.setContent(button.contentId, smoothScroll);
const segmentView = this.segmentViewEl;
if (segmentView) {
segmentView.setContent(button.contentId, smoothScroll);
}
}
@@ -638,21 +464,35 @@ export class Segment implements ComponentInterface {
const centeredX = activeButtonLeft - scrollContainerBox.width / 2 + activeButtonBox.width / 2;
/**
* We intentionally use scrollBy here instead of scrollIntoView
* newScrollPosition is the absolute scroll position that the
* container needs to move to in order to center the active button.
* It is calculated by adding the current scroll position
* (scrollLeft) to the offset needed to center the button
* (centeredX).
*/
const newScrollPosition = el.scrollLeft + centeredX;
/**
* We intentionally use scrollTo here instead of scrollIntoView
* to avoid a WebKit bug where accelerated animations break
* when using scrollIntoView. Using scrollIntoView will cause the
* segment container to jump during the transition and then snap into place.
* This is because scrollIntoView can potentially cause parent element
* containers to also scroll. scrollBy does not have this same behavior, so
* containers to also scroll. scrollTo does not have this same behavior, so
* we use this API instead.
*
* scrollTo is used instead of scrollBy because there is a
* Webkit bug that causes scrollBy to not work smoothly when
* the active button is near the edge of the scroll container.
* This leads to the buttons to jump around during the transition.
*
* Note that if there is not enough scrolling space to center the element
* within the scroll container, the browser will attempt
* to center by as much as it can.
*/
el.scrollBy({
el.scrollTo({
top: 0,
left: centeredX,
left: newScrollPosition,
behavior: smoothScroll ? 'smooth' : 'instant',
});
}
@@ -768,17 +608,19 @@ export class Segment implements ComponentInterface {
return;
}
this.value = current.value;
if (current !== previous) {
if (this.segmentViewEl) {
this.nextButtonIndex = this.getButtons().findIndex((button) => button.value === current.value);
this.updateSegmentView(current.value);
} else {
this.value = current.value;
this.emitValueChange();
}
this.emitValueChange();
}
if (this.scrollable || !this.swipeGesture) {
if (this.segmentViewEl) {
this.updateSegmentView();
if (this.scrollable && previous) {
this.checkButton(previous, current);
}
} else if (this.scrollable || !this.swipeGesture) {
if (previous) {
this.checkButton(previous, current);
} else {
@@ -874,11 +716,6 @@ export class Segment implements ComponentInterface {
'segment-scrollable': this.scrollable,
})}
>
{this.segmentViewEl && (
<div part="indicator" class="segment-indicator">
<div part="indicator-background" class="segment-indicator-background"></div>
</div>
)}
<slot onSlotchange={this.onSlottedItemsChange}></slot>
</Host>
);

View File

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

View File

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

View 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: "";
}

View File

@@ -0,0 +1,30 @@
@import "./select-modal";
@import "../../themes/ionic.mixins.scss";
@import "../item/item.md.vars";
ion-list ion-radio::part(container) {
display: none;
}
ion-list ion-radio::part(label) {
@include margin(0);
}
ion-item {
--inner-border-width: 0;
}
.item-radio-checked {
--background: #{ion-color(primary, base, 0.08)};
--background-focused: #{ion-color(primary, base)};
--background-focused-opacity: 0.2;
--background-hover: #{ion-color(primary, base)};
--background-hover-opacity: 0.12;
}
.item-checkbox-checked {
--background-activated: #{$item-md-color};
--background-focused: #{$item-md-color};
--background-hover: #{$item-md-color};
--color: #{ion-color(primary, base)};
}

View File

@@ -0,0 +1,3 @@
:host {
height: 100%;
}

View File

@@ -0,0 +1,162 @@
import { getIonMode } from '@global/ionic-global';
import type { ComponentInterface } from '@stencil/core';
import { Component, Element, Host, Prop, forceUpdate, h } from '@stencil/core';
import { safeCall } from '@utils/overlays';
import { getClassMap } from '@utils/theme';
import type { CheckboxCustomEvent } from '../checkbox/checkbox-interface';
import type { RadioGroupCustomEvent } from '../radio-group/radio-group-interface';
import type { SelectModalOption } from './select-modal-interface';
@Component({
tag: 'ion-select-modal',
styleUrls: {
ios: 'select-modal.ios.scss',
md: 'select-modal.md.scss',
ionic: 'select-modal.md.scss',
},
scoped: true,
})
export class SelectModal implements ComponentInterface {
@Element() el!: HTMLIonSelectModalElement;
@Prop() header?: string;
@Prop() multiple?: boolean;
@Prop() options: SelectModalOption[] = [];
private closeModal() {
const modal = this.el.closest('ion-modal');
if (modal) {
modal.dismiss();
}
}
private findOptionFromEvent(ev: CheckboxCustomEvent | RadioGroupCustomEvent) {
const { options } = this;
return options.find((o) => o.value === ev.target.value);
}
private getValues(ev?: CheckboxCustomEvent | RadioGroupCustomEvent): string | string[] | undefined {
const { multiple, options } = this;
if (multiple) {
// this is a modal with checkboxes (multiple value select)
// return an array of all the checked values
return options.filter((o) => o.checked).map((o) => o.value);
}
// this is a modal with radio buttons (single value select)
// return the value that was clicked, otherwise undefined
const option = ev ? this.findOptionFromEvent(ev) : null;
return option ? option.value : undefined;
}
private callOptionHandler(ev: CheckboxCustomEvent | RadioGroupCustomEvent) {
const option = this.findOptionFromEvent(ev);
const values = this.getValues(ev);
if (option?.handler) {
safeCall(option.handler, values);
}
}
private setChecked(ev: CheckboxCustomEvent): void {
const { multiple } = this;
const option = this.findOptionFromEvent(ev);
// this is a modal with checkboxes (multiple value select)
// we need to set the checked value for this option
if (multiple && option) {
option.checked = ev.detail.checked;
}
}
private renderRadioOptions() {
const checked = this.options.filter((o) => o.checked).map((o) => o.value)[0];
return (
<ion-radio-group value={checked} onIonChange={(ev) => this.callOptionHandler(ev)}>
{this.options.map((option) => (
<ion-item
lines="none"
class={{
// TODO FW-4784
'item-radio-checked': option.value === checked,
...getClassMap(option.cssClass),
}}
>
<ion-radio
value={option.value}
disabled={option.disabled}
justify="start"
labelPlacement="end"
onClick={() => this.closeModal()}
onKeyUp={(ev) => {
if (ev.key === ' ') {
/**
* Selecting a radio option with keyboard navigation,
* either through the Enter or Space keys, should
* dismiss the modal.
*/
this.closeModal();
}
}}
>
{option.text}
</ion-radio>
</ion-item>
))}
</ion-radio-group>
);
}
private renderCheckboxOptions() {
return this.options.map((option) => (
<ion-item
class={{
// TODO FW-4784
'item-checkbox-checked': option.checked,
...getClassMap(option.cssClass),
}}
>
<ion-checkbox
value={option.value}
disabled={option.disabled}
checked={option.checked}
justify="start"
labelPlacement="end"
onIonChange={(ev) => {
this.setChecked(ev);
this.callOptionHandler(ev);
// TODO FW-4784
forceUpdate(this);
}}
>
{option.text}
</ion-checkbox>
</ion-item>
));
}
render() {
return (
<Host class={getIonMode(this)}>
<ion-header>
<ion-toolbar>
{this.header !== undefined && <ion-title>{this.header}</ion-title>}
<ion-buttons slot="end">
<ion-button onClick={() => this.closeModal()}>Close</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>{this.multiple === true ? this.renderCheckboxOptions() : this.renderRadioOptions()}</ion-list>
</ion-content>
</Host>
);
}
}

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Select - Modal</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Select Modal - Basic</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-modal is-open="true">
<ion-select-modal multiple="false"></ion-select-modal>
</ion-modal>
</ion-content>
</ion-app>
<script>
const selectModal = document.querySelector('ion-select-modal');
selectModal.options = [
{ value: 'apple', text: 'Apple', disabled: false, checked: true },
{ value: 'banana', text: 'Banana', disabled: false, checked: false },
];
</script>
</body>
</html>

View File

@@ -0,0 +1,101 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
import type { SelectModalOption } from '../../select-modal-interface';
import { SelectModalPage } from '../fixtures';
const options: SelectModalOption[] = [
{ value: 'apple', text: 'Apple', disabled: false, checked: false },
{ value: 'banana', text: 'Banana', disabled: false, checked: false },
];
const checkedOptions: SelectModalOption[] = [
{ value: 'apple', text: 'Apple', disabled: false, checked: true },
{ value: 'banana', text: 'Banana', disabled: false, checked: false },
];
/**
* This behavior does not vary across modes/directions.
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('select-modal: basic'), () => {
test.beforeEach(({ browserName }) => {
test.skip(browserName === 'webkit', 'ROU-5437');
});
test.describe('single selection', () => {
let selectModalPage: SelectModalPage;
test.beforeEach(async ({ page }) => {
selectModalPage = new SelectModalPage(page);
});
test('clicking an unselected option should dismiss the modal', async () => {
await selectModalPage.setup(config, options, false);
await selectModalPage.clickOption('apple');
await selectModalPage.ionModalDidDismiss.next();
await expect(selectModalPage.modal).not.toBeVisible();
});
test('clicking a selected option should dismiss the modal', async () => {
await selectModalPage.setup(config, checkedOptions, false);
await selectModalPage.clickOption('apple');
await selectModalPage.ionModalDidDismiss.next();
await expect(selectModalPage.modal).not.toBeVisible();
});
test('pressing Space on an unselected option should dismiss the modal', async () => {
await selectModalPage.setup(config, options, false);
await selectModalPage.pressSpaceOnOption('apple');
await selectModalPage.ionModalDidDismiss.next();
await expect(selectModalPage.modal).not.toBeVisible();
});
test('pressing Space on a selected option should dismiss the modal', async ({ browserName }) => {
test.skip(browserName === 'firefox', 'Same behavior as ROU-5437');
await selectModalPage.setup(config, checkedOptions, false);
await selectModalPage.pressSpaceOnOption('apple');
await selectModalPage.ionModalDidDismiss.next();
await expect(selectModalPage.modal).not.toBeVisible();
});
test('clicking the close button should dismiss the modal', async () => {
await selectModalPage.setup(config, options, false);
const closeButton = selectModalPage.modal.locator('ion-header ion-toolbar ion-button');
await closeButton.click();
await selectModalPage.ionModalDidDismiss.next();
await expect(selectModalPage.modal).not.toBeVisible();
});
});
});
});
/**
* This behavior does not vary across directions.
* The components used inside of `ion-select-modal`
* do have RTL logic, but those are tested in their
* respective component test files.
*/
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('select-modal: rendering'), () => {
let selectModalPage: SelectModalPage;
test.beforeEach(async ({ page }) => {
selectModalPage = new SelectModalPage(page);
});
test('should not have visual regressions with single selection', async () => {
await selectModalPage.setup(config, checkedOptions, false);
await selectModalPage.screenshot(screenshot, 'select-modal-diff');
});
test('should not have visual regressions with multiple selection', async () => {
await selectModalPage.setup(config, checkedOptions, true);
await selectModalPage.screenshot(screenshot, 'select-modal-multiple-diff');
});
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -0,0 +1,73 @@
import { expect } from '@playwright/test';
import type { E2EPage, E2ELocator, EventSpy, E2EPageOptions, ScreenshotFn } from '@utils/test/playwright';
import type { SelectModalOption } from '../select-modal-interface';
export class SelectModalPage {
private page: E2EPage;
private multiple?: boolean;
private options: SelectModalOption[] = [];
// Locators
modal!: E2ELocator;
selectModal!: E2ELocator;
// Event spies
ionModalDidDismiss!: EventSpy;
constructor(page: E2EPage) {
this.page = page;
}
async setup(config: E2EPageOptions, options: SelectModalOption[], multiple = false) {
const { page } = this;
await page.setContent(
`
<ion-modal>
<ion-select-modal></ion-select-modal>
</ion-modal>
<script>
const selectModal = document.querySelector('ion-select-modal');
selectModal.options = ${JSON.stringify(options)};
selectModal.multiple = ${multiple};
</script>
`,
config
);
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
this.ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
this.modal = page.locator('ion-modal');
this.selectModal = page.locator('ion-select-modal');
this.multiple = multiple;
this.options = options;
await this.modal.evaluate((modal: HTMLIonModalElement) => modal.present());
await ionModalDidPresent.next();
}
async screenshot(screenshot: ScreenshotFn, name: string) {
await expect(this.selectModal).toHaveScreenshot(screenshot(name));
}
async clickOption(value: string) {
const option = this.getOption(value);
await option.click();
}
async pressSpaceOnOption(value: string) {
const option = this.getOption(value);
await option.press('Space');
}
private getOption(value: string) {
const { multiple, selectModal } = this;
const selector = multiple ? 'ion-checkbox' : 'ion-radio';
const index = this.options.findIndex((o) => o.value === value);
return selectModal.locator(selector).nth(index);
}
}

View File

@@ -1,4 +1,4 @@
export type SelectInterface = 'action-sheet' | 'popover' | 'alert';
export type SelectInterface = 'action-sheet' | 'popover' | 'alert' | 'modal';
export type SelectCompareFn = (currentValue: any, compareValue: any) => boolean;

View File

@@ -4,7 +4,7 @@ import type { NotchController } from '@utils/forms';
import { compareOptions, createNotchController, isOptionSelected } from '@utils/forms';
import { focusVisibleElement, renderHiddenInput, inheritAttributes } from '@utils/helpers';
import type { Attributes } from '@utils/helpers';
import { actionSheetController, alertController, popoverController } from '@utils/overlays';
import { actionSheetController, alertController, popoverController, modalController } from '@utils/overlays';
import type { OverlaySelect } from '@utils/overlays-interface';
import { isRTL } from '@utils/rtl';
import { createColorClasses, hostContext } from '@utils/theme';
@@ -19,6 +19,7 @@ import type {
CssClassMap,
PopoverOptions,
StyleEventDetail,
ModalOptions,
} from '../../interface';
import type { ActionSheetButton } from '../action-sheet/action-sheet-interface';
import type { AlertInput } from '../alert/alert-interface';
@@ -98,15 +99,15 @@ export class Select implements ComponentInterface {
@Prop() fill?: 'outline' | 'solid';
/**
* The interface the select should use: `action-sheet`, `popover` or `alert`.
* The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`.
*/
@Prop() interface: SelectInterface = 'alert';
/**
* Any additional options that the `alert`, `action-sheet` or `popover` interface
* can take. See the [ion-alert docs](./alert), the
* [ion-action-sheet docs](./action-sheet) and the
* [ion-popover docs](./popover) for the
* [ion-action-sheet docs](./action-sheet), the
* [ion-popover docs](./popover), and the [ion-modal docs](./modal) for the
* create options for each interface.
*
* Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface.
@@ -318,9 +319,9 @@ export class Select implements ComponentInterface {
await overlay.present();
// focus selected option for popovers
if (this.interface === 'popover') {
const indexOfSelected = this.childOpts.map((o) => o.value).indexOf(this.value);
// focus selected option for popovers and modals
if (this.interface === 'popover' || this.interface === 'modal') {
const indexOfSelected = this.childOpts.findIndex((o) => o.value === this.value);
if (indexOfSelected > -1) {
const selectedItem = overlay.querySelector<HTMLElement>(
@@ -328,8 +329,6 @@ export class Select implements ComponentInterface {
);
if (selectedItem) {
focusVisibleElement(selectedItem);
/**
* Browsers such as Firefox do not
* correctly delegate focus when manually
@@ -341,10 +340,17 @@ export class Select implements ComponentInterface {
* we only need to worry about those two components
* when focusing.
*/
const interactiveEl = selectedItem.querySelector<HTMLElement>('ion-radio, ion-checkbox');
const interactiveEl = selectedItem.querySelector<HTMLElement>('ion-radio, ion-checkbox') as
| HTMLIonRadioElement
| HTMLIonCheckboxElement
| null;
if (interactiveEl) {
interactiveEl.focus();
// Needs to be called before `focusVisibleElement` to prevent issue with focus event bubbling
// and removing `ion-focused` style
interactiveEl.setFocus();
}
focusVisibleElement(selectedItem);
}
} else {
/**
@@ -352,14 +358,18 @@ export class Select implements ComponentInterface {
*/
const firstEnabledOption = overlay.querySelector<HTMLElement>(
'ion-radio:not(.radio-disabled), ion-checkbox:not(.checkbox-disabled)'
);
if (firstEnabledOption) {
focusVisibleElement(firstEnabledOption.closest('ion-item')!);
) as HTMLIonRadioElement | HTMLIonCheckboxElement | null;
if (firstEnabledOption) {
/**
* Focus the option for the same reason as we do above.
*
* Needs to be called before `focusVisibleElement` to prevent issue with focus event bubbling
* and removing `ion-focused` style
*/
firstEnabledOption.focus();
firstEnabledOption.setFocus();
focusVisibleElement(firstEnabledOption.closest('ion-item')!);
}
}
}
@@ -389,6 +399,9 @@ export class Select implements ComponentInterface {
if (selectInterface === 'popover') {
return this.openPopover(ev!);
}
if (selectInterface === 'modal') {
return this.openModal();
}
return this.openAlert();
}
@@ -406,7 +419,13 @@ export class Select implements ComponentInterface {
case 'popover':
const popover = overlay.querySelector('ion-select-popover');
if (popover) {
popover.options = this.createPopoverOptions(childOpts, value);
popover.options = this.createOverlaySelectOptions(childOpts, value);
}
break;
case 'modal':
const modal = overlay.querySelector('ion-select-modal');
if (modal) {
modal.options = this.createOverlaySelectOptions(childOpts, value);
}
break;
case 'alert':
@@ -475,7 +494,7 @@ export class Select implements ComponentInterface {
return alertInputs;
}
private createPopoverOptions(data: HTMLIonSelectOptionElement[], selectValue: any): SelectPopoverOption[] {
private createOverlaySelectOptions(data: HTMLIonSelectOptionElement[], selectValue: any): SelectPopoverOption[] {
const popoverOptions = data.map((option) => {
const value = getOptionValue(option);
@@ -553,7 +572,7 @@ export class Select implements ComponentInterface {
message: interfaceOptions.message,
multiple,
value,
options: this.createPopoverOptions(this.childOpts, value),
options: this.createOverlaySelectOptions(this.childOpts, value),
},
};
@@ -647,6 +666,40 @@ export class Select implements ComponentInterface {
return alertController.create(alertOpts);
}
private openModal() {
const { multiple, value, interfaceOptions } = this;
const mode = getIonMode(this);
const modalOpts: ModalOptions = {
...interfaceOptions,
mode,
cssClass: ['select-modal', interfaceOptions.cssClass],
component: 'ion-select-modal',
componentProps: {
header: interfaceOptions.header,
multiple,
value,
options: this.createOverlaySelectOptions(this.childOpts, value),
},
};
/**
* Workaround for Stencil to autodefine
* ion-select-modal and ion-modal when
* using Custom Elements build.
*/
// eslint-disable-next-line
if (false) {
// eslint-disable-next-line
// @ts-ignore
document.createElement('ion-select-modal');
document.createElement('ion-modal');
}
return modalController.create(modalOpts);
}
/**
* Close the select interface.
*/

View File

@@ -51,6 +51,14 @@
<ion-select-option value="pears">Pears</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-select label="Modal" placeholder="Select one" interface="modal">
<ion-select-option value="apples">Apples</ion-select-option>
<ion-select-option value="oranges">Oranges</ion-select-option>
<ion-select-option value="pears">Pears</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
<ion-list>
@@ -76,6 +84,15 @@
<ion-select-option value="honeybadger">Honey Badger</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-select label="Modal" multiple="true" interface="modal">
<ion-select-option value="bird">Bird</ion-select-option>
<ion-select-option value="cat">Cat</ion-select-option>
<ion-select-option value="dog">Dog</ion-select-option>
<ion-select-option value="honeybadger">Honey Badger</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
<ion-list>
@@ -124,6 +141,16 @@
<ion-select-option value="onions">Onions</ion-select-option>
</ion-select>
</ion-item>
<ion-item color="secondary">
<ion-select label="Modal Sheet" id="customModalSelect" interface="modal" placeholder="Select One">
<ion-select-option value="pepperoni">Pepperoni</ion-select-option>
<ion-select-option value="bacon">Bacon</ion-select-option>
<ion-select-option value="xcheese">Extra Cheese</ion-select-option>
<ion-select-option value="mushrooms">Mushrooms</ion-select-option>
<ion-select-option value="onions">Onions</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
</ion-content>
@@ -152,6 +179,14 @@
message: '$1.50 charge for every topping',
};
customActionSheetSelect.interfaceOptions = customActionSheetOptions;
var customModalSelect = document.getElementById('customModalSelect');
var customModalSheetOptions = {
header: 'Pizza Toppings',
breakpoints: [0.5],
initialBreakpoint: 0.5,
};
customModalSelect.interfaceOptions = customModalSheetOptions;
</script>
</ion-app>
</body>

View File

@@ -58,6 +58,24 @@ configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
await expect(popover).toBeVisible();
});
});
test.describe('select: modal', () => {
test('it should open a modal select', async ({ page }) => {
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
await page.click('#customModalSelect');
await ionModalDidPresent.next();
const modal = page.locator('ion-modal');
// select has no value, so first option should be focused by default
const modalOption1 = modal.locator('.select-interface-option:first-of-type ion-radio');
await expect(modalOption1).toBeFocused();
await expect(modal).toBeVisible();
});
});
});
});

View File

@@ -27,6 +27,19 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
await expect(helperText).toHaveText('my helper');
await expect(errorText).toBeHidden();
});
test('textarea should have an aria-describedby attribute when helper text is present', async ({ page }) => {
await page.setContent(
`<ion-textarea helper-text="my helper" error-text="my error" label="my textarea"></ion-textarea>`,
config
);
const textarea = page.locator('ion-textarea textarea');
const helperText = page.locator('ion-textarea .helper-text');
const helperTextId = await helperText.getAttribute('id');
const ariaDescribedBy = await textarea.getAttribute('aria-describedby');
expect(ariaDescribedBy).toBe(helperTextId);
});
test('error text should be visible when textarea is invalid', async ({ page }) => {
await page.setContent(
`<ion-textarea class="ion-invalid ion-touched" helper-text="my helper" error-text="my error" label="my textarea"></ion-textarea>`,
@@ -55,6 +68,48 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
const errorText = page.locator('ion-textarea .error-text');
await expect(errorText).toHaveScreenshot(screenshot(`textarea-error-custom-color`));
});
test('textarea should have an aria-describedby attribute when error text is present', async ({ page }) => {
await page.setContent(
`<ion-textarea class="ion-invalid ion-touched" helper-text="my helper" error-text="my error" label="my textarea"></ion-textarea>`,
config
);
const textarea = page.locator('ion-textarea textarea');
const errorText = page.locator('ion-textarea .error-text');
const errorTextId = await errorText.getAttribute('id');
const ariaDescribedBy = await textarea.getAttribute('aria-describedby');
expect(ariaDescribedBy).toBe(errorTextId);
});
test('textarea should have aria-invalid attribute when input is invalid', async ({ page }) => {
await page.setContent(
`<ion-textarea class="ion-invalid ion-touched" helper-text="my helper" error-text="my error" label="my textarea"></ion-textarea>`,
config
);
const textarea = page.locator('ion-textarea textarea');
await expect(textarea).toHaveAttribute('aria-invalid');
});
test('textarea should not have aria-invalid attribute when input is valid', async ({ page }) => {
await page.setContent(
`<ion-textarea helper-text="my helper" error-text="my error" label="my textarea"></ion-textarea>`,
config
);
const textarea = page.locator('ion-textarea textarea');
await expect(textarea).not.toHaveAttribute('aria-invalid');
});
test('textarea should not have aria-describedby attribute when no hint or error text is present', async ({
page,
}) => {
await page.setContent(`<ion-textarea label="my textarea"></ion-textarea>`, config);
const textarea = page.locator('ion-textarea textarea');
await expect(textarea).not.toHaveAttribute('aria-describedby');
});
});
test.describe('textarea: hint text rendering', () => {
test.describe('regular textareas', () => {

View File

@@ -45,6 +45,8 @@ import type { TextareaChangeEventDetail, TextareaInputEventDetail } from './text
export class Textarea implements ComponentInterface {
private nativeInput?: HTMLTextAreaElement;
private inputId = `ion-textarea-${textareaIds++}`;
private helperTextId = `${this.inputId}-helper-text`;
private errorTextId = `${this.inputId}-error-text`;
/**
* `true` if the textarea was cleared as a result of the user typing
* with `clearOnEdit` enabled.
@@ -576,9 +578,30 @@ export class Textarea implements ComponentInterface {
* Renders the helper text or error text values
*/
private renderHintText() {
const { helperText, errorText } = this;
const { helperText, errorText, helperTextId, errorTextId } = this;
return [<div class="helper-text">{helperText}</div>, <div class="error-text">{errorText}</div>];
return [
<div id={helperTextId} class="helper-text">
{helperText}
</div>,
<div id={errorTextId} class="error-text">
{errorText}
</div>,
];
}
private getHintTextID(): string | undefined {
const { el, helperText, errorText, helperTextId, errorTextId } = this;
if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
return errorTextId;
}
if (helperText) {
return helperTextId;
}
return undefined;
}
private renderCounter() {
@@ -703,6 +726,8 @@ export class Textarea implements ComponentInterface {
onBlur={this.onBlur}
onFocus={this.onFocus}
onKeyDown={this.onKeyDown}
aria-describedby={this.getHintTextID()}
aria-invalid={this.getHintTextID() === this.errorTextId}
{...this.inheritedAttributes}
>
{value}

View File

@@ -97,6 +97,8 @@
background: var(--background);
box-shadow: var(--box-shadow);
pointer-events: auto;
}
.toast-wrapper.toast-top {
@@ -115,7 +117,6 @@
display: flex;
align-items: center;
pointer-events: auto;
height: inherit;
min-height: inherit;

View File

@@ -13,7 +13,7 @@ import { focusVisibleElement } from '@utils/helpers';
* valid usage for the disabled property on ion-button.
*/
export const focusableQueryString =
'[tabindex]:not([tabindex^="-"]):not([hidden]):not([disabled]), input:not([type=hidden]):not([tabindex^="-"]):not([hidden]):not([disabled]), textarea:not([tabindex^="-"]):not([hidden]):not([disabled]), button:not([tabindex^="-"]):not([hidden]):not([disabled]), select:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable[disabled="false"]:not([tabindex^="-"]):not([hidden])';
'[tabindex]:not([tabindex^="-"]):not([hidden]):not([disabled]), input:not([type=hidden]):not([tabindex^="-"]):not([hidden]):not([disabled]), textarea:not([tabindex^="-"]):not([hidden]):not([disabled]), button:not([tabindex^="-"]):not([hidden]):not([disabled]), select:not([tabindex^="-"]):not([hidden]):not([disabled]), ion-checkbox:not([tabindex^="-"]):not([hidden]):not([disabled]), ion-radio:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable[disabled="false"]:not([tabindex^="-"]):not([hidden])';
/**
* Focuses the first descendant in a context
@@ -78,7 +78,13 @@ const focusElementInContext = <T extends HTMLElement>(
}
if (elementToFocus) {
focusVisibleElement(elementToFocus);
const radioGroup = elementToFocus.closest('ion-radio-group');
if (radioGroup) {
radioGroup.setFocus();
} else {
focusVisibleElement(elementToFocus);
}
} else {
// Focus fallback element instead of letting focus escape
fallbackElement.focus();

View File

@@ -413,3 +413,14 @@ export const shallowEqualStringMap = (
return true;
};
export const getNextSiblingOfType = <T extends Element>(element: Element): T | null => {
let sibling = element.nextSibling;
while (sibling) {
if (sibling.nodeType === Node.ELEMENT_NODE && (sibling as T) !== null) {
return sibling as T;
}
sibling = sibling.nextSibling;
}
return null;
};

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