Compare commits

...

73 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
Tanner Reits
221bb42c00 merge release-8.3.1 (#29879)
v8.3.1
2024-09-17 11:08:27 -04:00
ionitron
a743344f07 chore(): update package lock files 2024-09-17 14:53:40 +00:00
ionitron
a87674a779 v8.3.1 2024-09-17 14:53:07 +00:00
Tanner Reits
f64458dc63 fix(react): revert react output target version (#29869)
Issue number: resolves ionic-team/stencil-ds-output-targets#476,
resolves ionic-team/stencil-ds-output-targets#475, resolves #29848

---------

<!-- 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. -->

In v0.6.0 of the [React output
target](https://www.npmjs.com/package/@stencil/react-output-target), the
implementation was changed to leverage Lit's utility for creating React
components from web components. This introduced some unforseen issues
and breaking changes.

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

- Reverts many of the changes from
https://github.com/ionic-team/ionic-framework/pull/29782 to downgrade
the React output target package to the last stable version (v0.5.3)
- Downgrades the version of Stencil to v4.20.0 (due to
https://github.com/ionic-team/stencil/issues/5983 causing problems with
the downgraded output target)
- Pins these versions and prevents Renovate from attempting to upgrade
until the associated issues are resolved

## 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 for this version: `8.3.1-dev.11726167750.15400355`

I tested the dev build against the use cases outlined in
https://github.com/ionic-team/stencil-ds-output-targets/issues/475 and
https://github.com/ionic-team/stencil-ds-output-targets/issues/476
2024-09-16 14:44:03 +00:00
Maria Hutt
ab7a0ab050 refactor(react): update tab-bar requirement on tabs (#29868)
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. -->

As mentioned in this
[PR](https://github.com/ionic-team/ionic-docs/pull/3797), React
`IonTabs` requires `IonTabBar` do be a child, else it doesn't render and
throws an error.

Angular, JS, and Vue doesn't have this requirement.

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

I didn't see any reason why React does not mimic the other frameworks.
In order to keep consistency, I've updated the React tabs. This would
allow `ion-tabs` and `ion-tab-bar` can be used as standalone elements as
mentioned in the [docs](https://ionicframework.com/docs/api/tabs).

- React follows the same structure as the other frameworks: `IonTabs`
doesn't require `IonTabBar` to be a child to render.

## 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.1-dev.11726159792.1a6f49de

How to test:
1. Create a Ionic React app through the Ionic CLI with tabs as the
starter
2. Run the app
3. Comment out the `IonTabBar`
4. Notice that the `IonTabs` does not render
5. Notice that there's an error in the console: "IonTabs needs a
IonTabBar"
6. Install the dev build: `npm install
@ionic/react@8.3.1-dev.11726159792.1a6f49de`
7. Make sure the `IonTabBar` is still commented out
8. Verify that `IonTabs` renders
9. Verify that there isn't an error in the console
2024-09-12 23:18:35 +00:00
Maria Hutt
df39ceac6d fix(react, vue): add default value for navManager on tabs (#29865)
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. -->

React: Refreshes the app upon a tab button press and returns to the
first tab content.
Vue: Does not render the tabs when the app doesn't use a router at all
because of `ionRouter` not being defined.

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

- React: Users can press on the tab button without having the app
refresh and displays the associated tab content.
- Vue: Renders the tabs when router is not present.

## 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.1-dev.11726094383.1cdbab6f

How to test:
I recommend using the playgrounds in the Ionic Framework docs repo since
those playgrounds are setup to not have any routers at a high level.

1. Navigate to the [basic usage
section](https://ionic-docs-git-rou-11138-ionic1.vercel.app/docs/api/tabs#basic-usage)
2. Select React
3. Open the StackBlitz example
4. Click on any tab button
5. Notice that the app refreshes and returns to the first tab content
6. Install the dev build: `npm install
@ionic/react@8.3.1-dev.11726094383.1cdbab6f`
7. Verify that the app doesn't refresh and reset the content after
clicking on any tab button
8. Navigate to the [basic usage
section](https://ionic-docs-git-rou-11138-ionic1.vercel.app/docs/api/tabs#basic-usage)
9. Select Vue
10. Open the StackBlitz example
11. Notice that tabs doesn't render
12. Open the console and notice that there's an error `ionRouter is
undefined`
13. Install the dev build: `npm install
@ionic/vue@8.3.1-dev.11726094383.1cdbab6f`
14. Verify that it renders
15. Verify that the console no longer shows the error
2024-09-12 17:35:30 +00:00
Maria Hutt
35b1a45929 fix(range): disable scroll when range is being dragged (#29241)
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 are a few tests that were disabled due to being flaky from
gestures.

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

While fixing the tests, I found a bug that the scroll was never being
disabled on scroll. Additionally, we were not taking into account that a
custom scroll target could be used so it was never disabled either.

- Fixed the flaky tests.
- Content doesn't scroll when range is being dragged.
- Content can be either `ion-content` or a custom scroll target.

## 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/.github/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 for
`ion-content`](https://ionic-framework-git-fw-2873-ionic1.vercel.app/src/components/range/test/scroll)
[Preview for custom scroll
target](https://ionic-framework-git-fw-2873-ionic1.vercel.app/src/components/range/test/scroll-target)

How to test:
1. Open either of the previews
2. Render the screen with the device simulator from the browser
3. Verify that you can scroll the page
4. Drag the range but don't let go
5. Verify that you cannot scroll the page
6. Repeat steps 2-5 with the other preview
2024-09-11 16:39:37 +00:00
Tanner Reits
f4ee2bbfc4 test(modal): allow animation and add updated snapshots (#29861)
Adds `{ animations: 'allow' }` to the sheet modal tests to allow the
sheet modal animations to execute before capturing their respective
screenshots.
2024-09-11 15:20:02 +00:00
Brandy Carney
88b701363c fix(checkbox): align checkbox properly in item using start alignment (#29850)
Issue number: resolves #29837

---------

## What is the current behavior?
The checkbox is not aligned properly to the top when using a long label
with `alignment="start"` inside of an `ion-item`:

```html
<ion-item>
  <ion-checkbox justify="start" alignment="start">
    <ion-label class="ion-text-wrap">
      Enable Notifications Enable Notifications Enable Notifications
    </ion-label>
  </ion-checkbox>
</ion-item>
```

## What is the new behavior?
- Applies the same margin to the `.native-wrapper` (checkbox) as the
label.
- Adds a screenshot test to verify the alignment

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information

| Before | After |
| --- | --- |
|
![before](https://github.com/user-attachments/assets/53579523-e8b5-4152-ae91-14847cb395e4)
|
![after](https://github.com/user-attachments/assets/0d7315ed-3294-4a27-82fe-6900eb9db1c0)
|
|
![before](https://github.com/user-attachments/assets/ca025a94-4ef7-44b4-85d0-5183e4326814)
|
![after](https://github.com/user-attachments/assets/fab60703-1196-48e8-a485-2f33c4893aba)
|

- [Label
Preview](https://ionic-framework-git-rou-11163-ionic1.vercel.app/src/components/checkbox/test/label)
- [Item
Preview](https://ionic-framework-git-rou-11163-ionic1.vercel.app/src/components/checkbox/test/item)

> [!NOTE]
> The alignment on the Material Design checkbox is still slightly off. I
could add margin directly to its checkbox but then it would change the
margin of the checkbox in all use cases.
2024-09-10 14:24:07 +00:00
Brandy Carney
f1d50c0f64 fix(datetime): display today's date and time when value is an empty string (#29839)
Issue number: resolves #29669

---------

## What is the current behavior?

Setting `value` to an empty string on `<ion-datetime>` renders a May
2021 calendar:

```html
<ion-datetime value=""></ion-datetime>
```

## What is the new behavior?

Show the month and time for today's date when value is an empty string.
This matches how a native `input` with `type="datetime-local"` works.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information

This can be tested by removing my fix in `datetime.tsx` and running the
e2e test for Datetime:

```bash
npm run test.e2e src/components/datetime/test/basic/datetime.e2e.ts
```

The `should display today's date and time when value is an empty string`
test should fail.

Alternatively, you can add a datetime with `value=""` and see the
calendar before & after my fix.

---------

Co-authored-by: Tanner Reits <47483144+tanner-reits@users.noreply.github.com>
2024-09-06 16:28:33 +00:00
260 changed files with 24595 additions and 3175 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,116 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [8.4.3](https://github.com/ionic-team/ionic-framework/compare/v8.4.2...v8.4.3) (2025-01-29)
### Bug Fixes
* **vue:** update Stencil Vue output target ([#30159](https://github.com/ionic-team/ionic-framework/issues/30159)) ([eb725fc](https://github.com/ionic-team/ionic-framework/commit/eb725fce6eb15facd8a1c21be11a1b2d46336479))
## [8.4.2](https://github.com/ionic-team/ionic-framework/compare/v8.4.1...v8.4.2) (2025-01-22)
### Bug Fixes
* **segment:** add logic to connect to segment-view in `componentDidLoad()` callback ([#30060](https://github.com/ionic-team/ionic-framework/issues/30060)) ([000f553](https://github.com/ionic-team/ionic-framework/commit/000f55303e459c583e642337fb1894f419f37d48)), closes [#30000](https://github.com/ionic-team/ionic-framework/issues/30000)
* **select-modal:** match radio styles to iOS native ([#30119](https://github.com/ionic-team/ionic-framework/issues/30119)) ([3f8346e](https://github.com/ionic-team/ionic-framework/commit/3f8346e718ae3a6eb5008d739f10b6898b84ca9b))
## [8.4.1](https://github.com/ionic-team/ionic-framework/compare/v8.4.0...v8.4.1) (2024-11-27)
### Bug Fixes
* **header:** use aria attributes to hide small title when collapsed ([#30027](https://github.com/ionic-team/ionic-framework/issues/30027)) ([23763ab](https://github.com/ionic-team/ionic-framework/commit/23763abf797f9a4ba8262225760f718e9dcc4782)), closes [#29347](https://github.com/ionic-team/ionic-framework/issues/29347)
* **menu:** hide from screen readers while animating ([#30036](https://github.com/ionic-team/ionic-framework/issues/30036)) ([845071c](https://github.com/ionic-team/ionic-framework/commit/845071c97a856d45eb5e0bb81d9c270bc38bb604))
* **overlays:** announce info after opening based on platform ([#30025](https://github.com/ionic-team/ionic-framework/issues/30025)) ([f6188c4](https://github.com/ionic-team/ionic-framework/commit/f6188c47e9278fe69fd9d250c65156edbe5ef32e))
* **overlays:** focus management with checkbox/radio ([#30026](https://github.com/ionic-team/ionic-framework/issues/30026)) ([8ee42bb](https://github.com/ionic-team/ionic-framework/commit/8ee42bbc1e0bf4731d20040c7853756722f1a4b2))
* **toast:** swipe gesture works with custom container layout ([#29999](https://github.com/ionic-team/ionic-framework/issues/29999)) ([470decc](https://github.com/ionic-team/ionic-framework/commit/470decca7b6b89ef74095ef0bb7909b93640cd78)), closes [#29998](https://github.com/ionic-team/ionic-framework/issues/29998)
# [8.4.0](https://github.com/ionic-team/ionic-framework/compare/v8.3.4...v8.4.0) (2024-11-04)
### Bug Fixes
* **alert:** use correct heading structure for subHeader when header exists ([#29964](https://github.com/ionic-team/ionic-framework/issues/29964)) ([0fdcb32](https://github.com/ionic-team/ionic-framework/commit/0fdcb32ce0f99b284b314f79f7d0b071bc37faec))
### Features
* **menu:** pass role to ionWillClose and ionDidClose events ([#29954](https://github.com/ionic-team/ionic-framework/issues/29954)) ([ee2fa19](https://github.com/ionic-team/ionic-framework/commit/ee2fa19a1e9f09d492c7c08340d95ba6a56ebb2b))
* **segment-view:** adds support for new `ion-segment-view` component ([#29969](https://github.com/ionic-team/ionic-framework/issues/29969)) ([89508fb](https://github.com/ionic-team/ionic-framework/commit/89508fb89172900b1d11cc3fc18883f57a7fbab6))
* **select:** add `modal` as interface ([#29972](https://github.com/ionic-team/ionic-framework/issues/29972)) ([3628ea8](https://github.com/ionic-team/ionic-framework/commit/3628ea875a66a717783de5e0a4df440872339040))
## [8.3.4](https://github.com/ionic-team/ionic-framework/compare/v8.3.3...v8.3.4) (2024-10-30)
### Bug Fixes
* **angular:** add missing 'compareWith' input to standalone ion-radio-group ([#29870](https://github.com/ionic-team/ionic-framework/issues/29870)) ([47ba703](https://github.com/ionic-team/ionic-framework/commit/47ba703a57d1ca506f943f6b790d0bf7583d79cb)), closes [#29826](https://github.com/ionic-team/ionic-framework/issues/29826)
* **backdrop:** remove tabindex for improved accessibility ([#29956](https://github.com/ionic-team/ionic-framework/issues/29956)) ([7294e96](https://github.com/ionic-team/ionic-framework/commit/7294e969bb913692eaf28e54860614f445132713)), closes [#29773](https://github.com/ionic-team/ionic-framework/issues/29773)
* **input, textarea:** ensure screen readers announce helper and error text when focused ([#29958](https://github.com/ionic-team/ionic-framework/issues/29958)) ([5a73145](https://github.com/ionic-team/ionic-framework/commit/5a7314553a8def87bd19275640c92dd72a6ef1a4))
* **overlay:** hide from screen readers while animating ([#29951](https://github.com/ionic-team/ionic-framework/issues/29951)) ([cb60073](https://github.com/ionic-team/ionic-framework/commit/cb6007363a8d42b5f126945427c2bfc3d7209c21)), closes [#29857](https://github.com/ionic-team/ionic-framework/issues/29857)
* **overlays:** do not hide root when toast appears ([#29962](https://github.com/ionic-team/ionic-framework/issues/29962)) ([322d7c9](https://github.com/ionic-team/ionic-framework/commit/322d7c98cf6613df0b0db3f119e3f892e6a17e7b)), closes [#29773](https://github.com/ionic-team/ionic-framework/issues/29773)
* **overlays:** hide the focus trap div from screen readers ([#29970](https://github.com/ionic-team/ionic-framework/issues/29970)) ([c3b58f1](https://github.com/ionic-team/ionic-framework/commit/c3b58f1620bcb74db43e3983ef570b7b982abd83)), closes [#29858](https://github.com/ionic-team/ionic-framework/issues/29858)
* **vue:** incorrect view rendered when using router.go(-n) ([#29877](https://github.com/ionic-team/ionic-framework/issues/29877)) ([e32fbe0](https://github.com/ionic-team/ionic-framework/commit/e32fbe02102fe80db29f73c26496a40852032354)), closes [#28201](https://github.com/ionic-team/ionic-framework/issues/28201) [#28201](https://github.com/ionic-team/ionic-framework/issues/28201) [#29847](https://github.com/ionic-team/ionic-framework/issues/29847)
## [8.3.3](https://github.com/ionic-team/ionic-framework/compare/v8.3.2...v8.3.3) (2024-10-16)
### Bug Fixes
* **tabs, tab-bar:** use standalone tab bar in Vue, React ([#29940](https://github.com/ionic-team/ionic-framework/issues/29940)) ([b7b383b](https://github.com/ionic-team/ionic-framework/commit/b7b383bee080b72de2e6307ff9a9a051314c69ed)), closes [#29885](https://github.com/ionic-team/ionic-framework/issues/29885) [#29924](https://github.com/ionic-team/ionic-framework/issues/29924)
## [8.3.2](https://github.com/ionic-team/ionic-framework/compare/v8.3.1...v8.3.2) (2024-10-02)
### Bug Fixes
* **segment:** prevent flickering for scrollable on iOS ([#29884](https://github.com/ionic-team/ionic-framework/issues/29884)) ([078ed0b](https://github.com/ionic-team/ionic-framework/commit/078ed0b86a0d8e9f8457481cb739ea214195adce)), closes [#29523](https://github.com/ionic-team/ionic-framework/issues/29523)
## [8.3.1](https://github.com/ionic-team/ionic-framework/compare/v8.3.0...v8.3.1) (2024-09-17)
### Bug Fixes
* **checkbox:** align checkbox properly in item using start alignment ([#29850](https://github.com/ionic-team/ionic-framework/issues/29850)) ([88b7013](https://github.com/ionic-team/ionic-framework/commit/88b701363c1865e81d7d136acab4b9fef3b1ab7a)), closes [#29837](https://github.com/ionic-team/ionic-framework/issues/29837)
* **datetime:** display today's date and time when value is an empty string ([#29839](https://github.com/ionic-team/ionic-framework/issues/29839)) ([f1d50c0](https://github.com/ionic-team/ionic-framework/commit/f1d50c0f645b3adf51426c24a1da0bbd1223f1d5)), closes [#29669](https://github.com/ionic-team/ionic-framework/issues/29669)
* **range:** disable scroll when range is being dragged ([#29241](https://github.com/ionic-team/ionic-framework/issues/29241)) ([35b1a45](https://github.com/ionic-team/ionic-framework/commit/35b1a459294657b609f683457e71babd4673a3a8))
* **react, vue:** add default value for navManager on tabs ([#29865](https://github.com/ionic-team/ionic-framework/issues/29865)) ([df39cea](https://github.com/ionic-team/ionic-framework/commit/df39ceac6db6511518aed4766e6a59aa24fb53ec))
* **react:** revert react output target version ([#29869](https://github.com/ionic-team/ionic-framework/issues/29869)) ([f64458d](https://github.com/ionic-team/ionic-framework/commit/f64458dc63a05acb955aa1f2ec3755c738466cf4)), closes [ionic-team/stencil-ds-output-targets#476](https://github.com/ionic-team/stencil-ds-output-targets/issues/476) [ionic-team/stencil-ds-output-targets#475](https://github.com/ionic-team/stencil-ds-output-targets/issues/475) [#29848](https://github.com/ionic-team/ionic-framework/issues/29848)
# [8.3.0](https://github.com/ionic-team/ionic-framework/compare/v8.2.8...v8.3.0) (2024-09-05)

View File

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

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
@@ -1541,6 +1542,7 @@ ion-segment,css-prop,--background,ios
ion-segment,css-prop,--background,md
ion-segment-button,shadow
ion-segment-button,prop,contentId,string | undefined,undefined,false,true
ion-segment-button,prop,disabled,boolean,false,false,false
ion-segment-button,prop,layout,"icon-bottom" | "icon-end" | "icon-hide" | "icon-start" | "icon-top" | "label-hide" | undefined,'icon-top',false,false
ion-segment-button,prop,mode,"ios" | "md",undefined,false,false
@@ -1606,6 +1608,12 @@ ion-segment-button,part,indicator
ion-segment-button,part,indicator-background
ion-segment-button,part,native
ion-segment-content,shadow
ion-segment-view,shadow
ion-segment-view,prop,disabled,boolean,false,false,false
ion-segment-view,event,ionSegmentViewScroll,SegmentViewScrollEvent,true
ion-select,shadow
ion-select,prop,cancelText,string,'Cancel',false,false
ion-select,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
@@ -1613,7 +1621,7 @@ ion-select,prop,compareWith,((currentValue: any, compareValue: any) => boolean)
ion-select,prop,disabled,boolean,false,false,false
ion-select,prop,expandedIcon,string | undefined,undefined,false,false
ion-select,prop,fill,"outline" | "solid" | undefined,undefined,false,false
ion-select,prop,interface,"action-sheet" | "alert" | "popover",'alert',false,false
ion-select,prop,interface,"action-sheet" | "alert" | "modal" | "popover",'alert',false,false
ion-select,prop,interfaceOptions,any,{},false,false
ion-select,prop,justify,"end" | "space-between" | "start" | undefined,undefined,false,false
ion-select,prop,label,string | undefined,undefined,false,false
@@ -1671,6 +1679,11 @@ ion-select,part,label
ion-select,part,placeholder
ion-select,part,text
ion-select-modal,scoped
ion-select-modal,prop,header,string | undefined,undefined,false,false
ion-select-modal,prop,multiple,boolean | undefined,undefined,false,false
ion-select-modal,prop,options,SelectModalOption[],[],false,false
ion-select-option,shadow
ion-select-option,prop,disabled,boolean,false,false,false
ion-select-option,prop,value,any,undefined,false,false

915
core/package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

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

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.
*/
@@ -2690,6 +2699,10 @@ export namespace Components {
"value"?: SegmentValue;
}
interface IonSegmentButton {
/**
* The `id` of the segment content.
*/
"contentId"?: string;
/**
* If `true`, the user cannot interact with the segment button.
*/
@@ -2712,6 +2725,19 @@ export namespace Components {
*/
"value": SegmentValue;
}
interface IonSegmentContent {
}
interface IonSegmentView {
/**
* If `true`, the segment view cannot be interacted with.
*/
"disabled": boolean;
/**
* @param id : The id of the segment content to display.
* @param smoothScroll : Whether to animate the scroll transition.
*/
"setContent": (id: string, smoothScroll?: boolean) => Promise<void>;
}
interface IonSelect {
/**
* The text to display on the cancel button.
@@ -2738,11 +2764,11 @@ export namespace Components {
*/
"fill"?: 'outline' | 'solid';
/**
* The interface the select should use: `action-sheet`, `popover` or `alert`.
* The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`.
*/
"interface": SelectInterface;
/**
* Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet) and the [ion-popover docs](./popover) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface.
* Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet), the [ion-popover docs](./popover), and the [ion-modal docs](./modal) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface.
*/
"interfaceOptions": any;
/**
@@ -2799,6 +2825,11 @@ export namespace Components {
*/
"value"?: any | null;
}
interface IonSelectModal {
"header"?: string;
"multiple"?: boolean;
"options": SelectModalOption[];
}
interface IonSelectOption {
/**
* If `true`, the user cannot interact with the select option. This property does not apply when `interface="action-sheet"` as `ion-action-sheet` does not allow for disabled buttons.
@@ -3413,6 +3444,10 @@ export interface IonSegmentCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLIonSegmentElement;
}
export interface IonSegmentViewCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLIonSegmentViewElement;
}
export interface IonSelectCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLIonSelectElement;
@@ -3966,9 +4001,9 @@ declare global {
};
interface HTMLIonMenuElementEventMap {
"ionWillOpen": void;
"ionWillClose": void;
"ionWillClose": MenuCloseEventDetail;
"ionDidOpen": void;
"ionDidClose": void;
"ionDidClose": MenuCloseEventDetail;
"ionMenuChange": MenuChangeEventDetail;
}
interface HTMLIonMenuElement extends Components.IonMenu, HTMLStencilElement {
@@ -4409,6 +4444,29 @@ declare global {
prototype: HTMLIonSegmentButtonElement;
new (): HTMLIonSegmentButtonElement;
};
interface HTMLIonSegmentContentElement extends Components.IonSegmentContent, HTMLStencilElement {
}
var HTMLIonSegmentContentElement: {
prototype: HTMLIonSegmentContentElement;
new (): HTMLIonSegmentContentElement;
};
interface HTMLIonSegmentViewElementEventMap {
"ionSegmentViewScroll": SegmentViewScrollEvent;
}
interface HTMLIonSegmentViewElement extends Components.IonSegmentView, HTMLStencilElement {
addEventListener<K extends keyof HTMLIonSegmentViewElementEventMap>(type: K, listener: (this: HTMLIonSegmentViewElement, ev: IonSegmentViewCustomEvent<HTMLIonSegmentViewElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLIonSegmentViewElementEventMap>(type: K, listener: (this: HTMLIonSegmentViewElement, ev: IonSegmentViewCustomEvent<HTMLIonSegmentViewElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLIonSegmentViewElement: {
prototype: HTMLIonSegmentViewElement;
new (): HTMLIonSegmentViewElement;
};
interface HTMLIonSelectElementEventMap {
"ionChange": SelectChangeEventDetail;
"ionCancel": void;
@@ -4431,6 +4489,12 @@ declare global {
prototype: HTMLIonSelectElement;
new (): HTMLIonSelectElement;
};
interface HTMLIonSelectModalElement extends Components.IonSelectModal, HTMLStencilElement {
}
var HTMLIonSelectModalElement: {
prototype: HTMLIonSelectModalElement;
new (): HTMLIonSelectModalElement;
};
interface HTMLIonSelectOptionElement extends Components.IonSelectOption, HTMLStencilElement {
}
var HTMLIonSelectOptionElement: {
@@ -4718,7 +4782,10 @@ declare global {
"ion-searchbar": HTMLIonSearchbarElement;
"ion-segment": HTMLIonSegmentElement;
"ion-segment-button": HTMLIonSegmentButtonElement;
"ion-segment-content": HTMLIonSegmentContentElement;
"ion-segment-view": HTMLIonSegmentViewElement;
"ion-select": HTMLIonSelectElement;
"ion-select-modal": HTMLIonSelectModalElement;
"ion-select-option": HTMLIonSelectOptionElement;
"ion-select-popover": HTMLIonSelectPopoverElement;
"ion-skeleton-text": HTMLIonSkeletonTextElement;
@@ -6361,7 +6428,7 @@ declare namespace LocalJSX {
/**
* Emitted when the menu is closed.
*/
"onIonDidClose"?: (event: IonMenuCustomEvent<void>) => void;
"onIonDidClose"?: (event: IonMenuCustomEvent<MenuCloseEventDetail>) => void;
/**
* Emitted when the menu is open.
*/
@@ -6373,7 +6440,7 @@ declare namespace LocalJSX {
/**
* Emitted when the menu is about to be closed.
*/
"onIonWillClose"?: (event: IonMenuCustomEvent<void>) => void;
"onIonWillClose"?: (event: IonMenuCustomEvent<MenuCloseEventDetail>) => void;
/**
* Emitted when the menu is about to be opened.
*/
@@ -7447,6 +7514,10 @@ declare namespace LocalJSX {
"value"?: SegmentValue;
}
interface IonSegmentButton {
/**
* The `id` of the segment content.
*/
"contentId"?: string;
/**
* If `true`, the user cannot interact with the segment button.
*/
@@ -7468,6 +7539,18 @@ declare namespace LocalJSX {
*/
"value"?: SegmentValue;
}
interface IonSegmentContent {
}
interface IonSegmentView {
/**
* If `true`, the segment view cannot be interacted with.
*/
"disabled"?: boolean;
/**
* Emitted when the segment view is scrolled.
*/
"onIonSegmentViewScroll"?: (event: IonSegmentViewCustomEvent<SegmentViewScrollEvent>) => void;
}
interface IonSelect {
/**
* The text to display on the cancel button.
@@ -7494,11 +7577,11 @@ declare namespace LocalJSX {
*/
"fill"?: 'outline' | 'solid';
/**
* The interface the select should use: `action-sheet`, `popover` or `alert`.
* The interface the select should use: `action-sheet`, `popover`, `alert`, or `modal`.
*/
"interface"?: SelectInterface;
/**
* Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet) and the [ion-popover docs](./popover) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface.
* Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet), the [ion-popover docs](./popover), and the [ion-modal docs](./modal) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface.
*/
"interfaceOptions"?: any;
/**
@@ -7574,6 +7657,11 @@ declare namespace LocalJSX {
*/
"value"?: any | null;
}
interface IonSelectModal {
"header"?: string;
"multiple"?: boolean;
"options"?: SelectModalOption[];
}
interface IonSelectOption {
/**
* If `true`, the user cannot interact with the select option. This property does not apply when `interface="action-sheet"` as `ion-action-sheet` does not allow for disabled buttons.
@@ -8159,7 +8247,10 @@ declare namespace LocalJSX {
"ion-searchbar": IonSearchbar;
"ion-segment": IonSegment;
"ion-segment-button": IonSegmentButton;
"ion-segment-content": IonSegmentContent;
"ion-segment-view": IonSegmentView;
"ion-select": IonSelect;
"ion-select-modal": IonSelectModal;
"ion-select-option": IonSelectOption;
"ion-select-popover": IonSelectPopover;
"ion-skeleton-text": IonSkeletonText;
@@ -8258,7 +8349,10 @@ declare module "@stencil/core" {
"ion-searchbar": LocalJSX.IonSearchbar & JSXBase.HTMLAttributes<HTMLIonSearchbarElement>;
"ion-segment": LocalJSX.IonSegment & JSXBase.HTMLAttributes<HTMLIonSegmentElement>;
"ion-segment-button": LocalJSX.IonSegmentButton & JSXBase.HTMLAttributes<HTMLIonSegmentButtonElement>;
"ion-segment-content": LocalJSX.IonSegmentContent & JSXBase.HTMLAttributes<HTMLIonSegmentContentElement>;
"ion-segment-view": LocalJSX.IonSegmentView & JSXBase.HTMLAttributes<HTMLIonSegmentViewElement>;
"ion-select": LocalJSX.IonSelect & JSXBase.HTMLAttributes<HTMLIonSelectElement>;
"ion-select-modal": LocalJSX.IonSelectModal & JSXBase.HTMLAttributes<HTMLIonSelectModalElement>;
"ion-select-option": LocalJSX.IonSelectOption & JSXBase.HTMLAttributes<HTMLIonSelectOptionElement>;
"ion-select-popover": LocalJSX.IonSelectPopover & JSXBase.HTMLAttributes<HTMLIonSelectPopoverElement>;
"ion-skeleton-text": LocalJSX.IonSkeletonText & JSXBase.HTMLAttributes<HTMLIonSkeletonTextElement>;

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

@@ -87,7 +87,10 @@
overflow: hidden;
}
:host(.in-item) .label-text-wrapper {
// Checkboxes that are not slotted inside an item and are not used with a
// stacked label should have margins equal to those of the label.
:host(.in-item) .label-text-wrapper,
:host(.in-item:not(.checkbox-label-placement-stacked):not([slot])) .native-wrapper {
@include margin($checkbox-item-label-margin-top, null, $checkbox-item-label-margin-bottom, null);
}

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

@@ -51,7 +51,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
});
});
configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, config }) => {
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('checkbox: long label in item'), () => {
test('should render margins correctly when using long label in item', async ({ page }) => {
await page.setContent(
@@ -69,6 +69,28 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
const list = page.locator('ion-list');
await expect(list).toHaveScreenshot(screenshot(`checkbox-long-label-in-item`));
});
test('should render margins correctly when using long label in item with start alignment', async ({
page,
}, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/29837',
});
await page.setContent(
`
<ion-list>
<ion-item>
<ion-checkbox justify="start" alignment="start">
<ion-label class="ion-text-wrap">Enable Notifications Enable Notifications Enable Notifications</ion-label>
</ion-checkbox>
</ion-item>
</ion-list>
`,
config
);
const list = page.locator('ion-list');
await expect(list).toHaveScreenshot(screenshot(`checkbox-long-label-in-item-align-start`));
});
});
test.describe(title('checkbox: stacked label in item'), () => {

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -181,6 +181,15 @@
</ion-checkbox>
</ion-item>
</div>
<div class="grid-item">
<ion-item>
<ion-checkbox justify="start" alignment="start">
<ion-label class="ion-text-wrap">
Enable Notifications Enable Notifications Enable Notifications
</ion-label>
</ion-checkbox>
</ion-item>
</div>
</div>
</ion-content>
</ion-app>

View File

@@ -113,6 +113,24 @@
<ion-checkbox label-placement="stacked" alignment="center">Enable Notifications</ion-checkbox>
</div>
</div>
<h1>Multiline Label</h1>
<div class="grid">
<div class="grid-item">
<ion-checkbox justify="start">
<ion-label class="ion-text-wrap">
Enable Notifications Enable Notifications Enable Notifications
</ion-label>
</ion-checkbox>
</div>
<div class="grid-item">
<ion-checkbox justify="start" alignment="start">
<ion-label class="ion-text-wrap">
Enable Notifications Enable Notifications Enable Notifications
</ion-label>
</ion-checkbox>
</div>
</div>
</ion-content>
</ion-app>
</body>

View File

@@ -1234,7 +1234,8 @@ export class Datetime implements ComponentInterface {
}
private processValue = (value?: string | string[] | null) => {
const hasValue = value !== null && value !== undefined && (!Array.isArray(value) || value.length > 0);
const hasValue =
value !== null && value !== undefined && value !== '' && (!Array.isArray(value) || value.length > 0);
const valueToProcess = hasValue ? parseDate(value) : this.defaultParts;
const { minParts, maxParts, workingParts, el } = this;

View File

@@ -121,6 +121,38 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await expect(datetime).toHaveJSProperty('value', '2022-10-01T16:22:00');
});
test("should display today's date and time when value is an empty string", async ({ page }) => {
await page.setContent(
`
<ion-datetime locale="en-US" presentation="date-time" value=""></ion-datetime>
<script>
const mockToday = '2024-07-24T16:22';
Date = class extends Date {
constructor(...args) {
if (args.length === 0) {
super(mockToday)
} else {
super(...args);
}
}
}
</script>
`,
config
);
await page.locator('.datetime-ready').waitFor();
// July 24, 2024
const todayButton = page.locator('.calendar-day[data-day="24"][data-month="7"][data-year="2024"]');
await expect(todayButton).toHaveClass(/calendar-day-today/);
// 4:22 PM
const timeBody = page.locator('ion-datetime .time-body');
await expect(timeBody).toHaveText('4:22 PM');
});
});
});

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

@@ -11,7 +11,16 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
await ionModalDidPresent.next();
await expect(page).toHaveScreenshot(screenshot(`modal-sheet-present`));
await expect(page).toHaveScreenshot(screenshot(`modal-sheet-present`), {
/**
* Animations must be enabled to capture the screenshot.
* By default, animations are disabled with toHaveScreenshot,
* and when capturing the screenshot will call animation.finish().
* This will cause the modal to close and the screenshot capture
* to be invalid.
*/
animations: 'allow',
});
});
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 34 KiB

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

@@ -325,7 +325,8 @@ export class Range implements ComponentInterface {
this.setupGesture();
}
this.contentEl = findClosestIonContent(this.el);
const ionContent = findClosestIonContent(this.el);
this.contentEl = ionContent?.querySelector('.ion-content-scroll-host') ?? ionContent;
}
disconnectedCallback() {
@@ -418,7 +419,7 @@ export class Range implements ComponentInterface {
*
* This only needs to be done once.
*/
if (contentEl && this.initialContentScrollY === undefined) {
if (contentEl && this.pressedKnob === undefined) {
this.initialContentScrollY = disableContentScrollY(contentEl);
}
@@ -469,7 +470,7 @@ export class Range implements ComponentInterface {
*
* The user can now scroll on the view in the next gesture event.
*/
if (contentEl && initialContentScrollY !== undefined) {
if (contentEl && this.pressedKnob !== undefined) {
resetContentScrollY(contentEl, initialContentScrollY);
}

View File

@@ -7,10 +7,7 @@ import { configs, dragElementBy, test } from '@utils/test/playwright';
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('range: events:'), () => {
test.describe('range: knob events', () => {
/**
* The mouse events are flaky on CI
*/
test.fixme('should emit start/end events', async ({ page }) => {
test('should emit start/end events', async ({ page }) => {
/**
* Requires padding to prevent the knob from being clipped.
* If it's clipped, then the value might be one off.
@@ -31,23 +28,34 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
const rangeEl = page.locator('ion-range');
await dragElementBy(rangeEl, page, 300, 0);
await page.waitForChanges();
/**
* dragElementBy defaults to starting the drag from the middle of the el,
* so the start value should jump to 50 despite the range defaulting to 20.
* Verify both events fire if range is dragged.
*/
expect(rangeStart).toHaveReceivedEventDetail({ value: 50 });
await dragElementBy(rangeEl, page, 180);
await rangeStart.next();
await rangeEnd.next();
// Once the knob is dragged, the start event should fire with
// the initial value.
expect(rangeStart).toHaveReceivedEventDetail({ value: 20 });
// Once the knob is released, the end event should fire with
// the final value.
expect(rangeEnd).toHaveReceivedEventDetail({ value: 100 });
/**
* Verify both events fire if range is clicked without dragging.
* Verify both events fire if range is tapped without dragging.
*/
await dragElementBy(rangeEl, page, 0, 0);
await page.waitForChanges();
expect(rangeStart).toHaveReceivedEventDetail({ value: 50 });
await rangeStart.next();
await rangeEnd.next();
// Once the tap is released, the start event should fire with
// the initial value.
expect(rangeStart).toHaveReceivedEventDetail({ value: 100 });
// Once the tap is released, the end event should fire with
// the final value.
expect(rangeEnd).toHaveReceivedEventDetail({ value: 50 });
});
@@ -99,31 +107,6 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
expect(rangeEndSpy.length).toBe(1);
});
// TODO FW-2873
test.skip('should not scroll when the knob is swiped', async ({ page, skip }) => {
skip.browser('webkit', 'mouse.wheel is not available in WebKit');
await page.goto(`/src/components/range/test/basic`, config);
const knobEl = page.locator('ion-range#stacked-range .range-knob-handle');
const scrollEl = page.locator('ion-content .inner-scroll');
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
await dragElementBy(knobEl, page, 30, 0, undefined, undefined, false);
/**
* Do not use scrollToBottom() or other scrolling methods
* on ion-content as those will update the scroll position.
* Setting scrollTop still works even with overflow-y: hidden.
* However, simulating a user gesture should not scroll the content.
*/
await page.mouse.wheel(0, 100);
await page.waitForChanges();
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
});
});
test.describe('ionChange', () => {
@@ -151,14 +134,26 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
expect(ionChangeSpy).toHaveReceivedEventTimes(0);
});
// TODO FW-2873
test.skip('should emit when the knob is released', async ({ page }) => {
await page.setContent(`<ion-range aria-label="range"></ion-range>`, config);
test('should emit when the knob is released', async ({ page }) => {
/**
* Requires padding to prevent the knob from being clipped.
* If it's clipped, then the value might be one off.
* For example, if the knob is clipped on the right, then the value
* will be 99 instead of 100.
*/
await page.setContent(
`
<div style="padding: 0 20px">
<ion-range aria-label="Range"></ion-range>
</div>
`,
config
);
const rangeHandle = page.locator('ion-range .range-knob-handle');
const rangeEl = page.locator('ion-range');
const ionChangeSpy = await page.spyOnEvent('ionChange');
await dragElementBy(rangeHandle, page, 100, 0);
await dragElementBy(rangeEl, page, 100);
await ionChangeSpy.next();
@@ -196,16 +191,26 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
});
test.describe('ionInput', () => {
// TODO(FW-2873) Enable this test when touch events/gestures are better supported in Playwright
test.skip('should emit when the knob is dragged', async ({ page }) => {
await page.setContent(`<ion-range aria-label="range"></ion-range>`, config);
test('should emit when the knob is dragged', async ({ page }) => {
/**
* Requires padding to prevent the knob from being clipped.
* If it's clipped, then the value might be one off.
* For example, if the knob is clipped on the right, then the value
* will be 99 instead of 100.
*/
await page.setContent(
`
<div style="padding: 0 20px">
<ion-range aria-label="range"></ion-range>
</div>
`,
config
);
const rangeHandle = page.locator('ion-range .range-knob-handle');
const rangeEl = page.locator('ion-range');
const ionInputSpy = await page.spyOnEvent('ionInput');
await rangeHandle.hover();
await dragElementBy(rangeHandle, page, 100, 0, undefined, undefined, false);
await dragElementBy(rangeEl, page, 100);
await ionInputSpy.next();

View File

@@ -1,29 +1,26 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
import { configs, test, dragElementBy } from '@utils/test/playwright';
// TODO FW-2873
/**
* This behavior does not vary across modes/directions.
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe.skip(title('range: scroll-target'), () => {
test.describe(title('range: scroll-target'), () => {
test('should not scroll when the knob is swiped in custom scroll target', async ({ page, skip }) => {
/**
* The Playwright team has stated that they will not implement this feature:
* https://github.com/microsoft/playwright/issues/28755
*/
skip.browser('webkit', 'mouse.wheel is not available in WebKit');
await page.goto(`/src/components/range/test/scroll-target`, config);
const knobEl = page.locator('ion-range .range-knob-handle');
const rangeEl = page.locator('ion-range');
const scrollEl = page.locator('.ion-content-scroll-host');
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
const box = (await knobEl.boundingBox())!;
const centerX = box.x + box.width / 2;
const centerY = box.y + box.height / 2;
await page.mouse.move(centerX, centerY);
await page.mouse.down();
await page.mouse.move(centerX + 30, centerY);
await dragElementBy(rangeEl, page, 100, 0, undefined, undefined, false);
/**
* Do not use scrollToBottom() or other scrolling methods

View File

@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Range - Scroll</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Range - Scroll</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.
</p>
<ion-range value="40">
<div slot="label">Range Label</div>
<ion-label slot="start">Start</ion-label>
<ion-label slot="end">End</ion-label>
</ion-range>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur faucibus nulla a nunc tincidunt semper. Nam
nibh lorem, pharetra ac ex ac, tempus fringilla est. Aenean tincidunt ipsum pellentesque, consequat libero id,
feugiat leo. In vestibulum faucibus velit, non tincidunt erat tincidunt in. Donec a diam sed nisl convallis
maximus. Aenean cursus sagittis lorem vitae tristique. Pellentesque pellentesque, quam eget lobortis finibus,
lectus lorem maximus purus, quis sagittis tortor sem sed tellus.!
</p>
</ion-content>
</ion-app>
</body>
</html>

View File

@@ -0,0 +1,46 @@
import { expect } from '@playwright/test';
import { configs, dragElementBy, test } from '@utils/test/playwright';
/**
* This behavior does not vary across modes/directions.
*/
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('range: scroll'), () => {
test('should not scroll when the knob is being dragged', async ({ page, skip }) => {
/**
* The Playwright team has stated that they will not implement this feature:
* https://github.com/microsoft/playwright/issues/28755
*/
skip.browser('webkit', 'mouse.wheel is not available in WebKit');
/**
* Requires padding to prevent the knob from being clipped.
* If it's clipped, then the value might be one off.
* For example, if the knob is clipped on the right, then the value
* will be 99 instead of 100.
*
* The ion-content is also required to be taller than the viewport
* to allow for scrolling.
*/
await page.goto(`/src/components/range/test/scroll`, config);
const rangeEl = page.locator('ion-range');
const scrollEl = page.locator('ion-content .inner-scroll');
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
await dragElementBy(rangeEl, page, 100, 0, undefined, undefined, false);
/**
* Do not use scrollToBottom() or other scrolling methods
* on ion-content as those will update the scroll position.
* Setting scrollTop still works even with overflow-y: hidden.
* However, simulating a user gesture should not scroll the content.
*/
await page.mouse.wheel(0, 100);
await page.waitForChanges();
expect(await scrollEl.evaluate((el: HTMLElement) => el.scrollTop)).toEqual(0);
});
});
});

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';
@@ -36,6 +36,11 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
@State() checked = false;
/**
* The `id` of the segment content.
*/
@Prop({ reflect: true }) contentId?: string;
/**
* If `true`, the user cannot interact with the segment button.
*/
@@ -60,13 +65,72 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
this.updateState();
}
connectedCallback() {
private waitForSegmentContent(ionSegment: HTMLIonSegmentElement | null, contentId: string): Promise<HTMLElement> {
return new Promise((resolve, reject) => {
let timeoutId: NodeJS.Timeout | undefined = undefined;
let animationFrameId: number;
const check = () => {
if (!ionSegment) {
reject(new Error(`Segment not found when looking for Segment Content`));
return;
}
const segmentView = getNextSiblingOfType<HTMLIonSegmentViewElement>(ionSegment); // Skip the text nodes
const segmentContent = segmentView?.querySelector(
`ion-segment-content[id="${contentId}"]`
) as HTMLIonSegmentContentElement | null;
if (segmentContent && timeoutId) {
clearTimeout(timeoutId); // Clear the timeout if the segmentContent is found
cancelAnimationFrame(animationFrameId);
resolve(segmentContent);
} else {
animationFrameId = requestAnimationFrame(check); // Keep checking on the next animation frame
}
};
check();
// Set a timeout to reject the promise
timeoutId = setTimeout(() => {
cancelAnimationFrame(animationFrameId);
reject(new Error(`Unable to find Segment Content with id="${contentId} within 1000 ms`));
}, 1000);
});
}
async connectedCallback() {
const segmentEl = (this.segmentEl = this.el.closest('ion-segment'));
if (segmentEl) {
this.updateState();
addEventListener(segmentEl, 'ionSelect', this.updateState);
addEventListener(segmentEl, 'ionStyle', this.updateStyle);
}
// Return if there is no contentId defined
if (!this.contentId) return;
let segmentContent;
try {
// Attempt to find the Segment Content by its contentId
segmentContent = await this.waitForSegmentContent(segmentEl, this.contentId);
} catch (error) {
// If no associated Segment Content exists, log an error and return
console.error('Segment Button: ', (error as Error).message);
return;
}
// Ensure the found element is a valid ION-SEGMENT-CONTENT
if (segmentContent.tagName !== 'ION-SEGMENT-CONTENT') {
console.error(`Segment Button: Element with id="${this.contentId}" is not an <ion-segment-content> element.`);
return;
}
// Prevent buttons from being disabled when associated with segment content
if (this.disabled) {
console.warn(`Segment Button: Segment buttons cannot be disabled when associated with an <ion-segment-content>.`);
this.disabled = false;
}
}
disconnectedCallback() {
@@ -161,13 +225,7 @@ export class SegmentButton implements ComponentInterface, ButtonInterface {
</span>
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
</button>
<div
part="indicator"
class={{
'segment-button-indicator': true,
'segment-button-indicator-animated': true,
}}
>
<div part="indicator" class="segment-button-indicator segment-button-indicator-animated">
<div part="indicator-background" class="segment-button-indicator-background"></div>
</div>
</Host>

View File

@@ -0,0 +1,11 @@
// Segment Content
// --------------------------------------------------
:host {
scroll-snap-align: center;
scroll-snap-stop: always;
flex-shrink: 0;
width: 100%;
}

View File

@@ -0,0 +1,17 @@
import type { ComponentInterface } from '@stencil/core';
import { Component, Host, h } from '@stencil/core';
@Component({
tag: 'ion-segment-content',
styleUrl: 'segment-content.scss',
shadow: true,
})
export class SegmentContent implements ComponentInterface {
render() {
return (
<Host>
<slot></slot>
</Host>
);
}
}

View File

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

View File

@@ -0,0 +1,9 @@
@import "./segment-view";
@import "../segment-button/segment-button.ios.vars";
// iOS Segment View
// --------------------------------------------------
:host(.segment-view-disabled) {
opacity: $segment-button-ios-opacity-disabled;
}

View File

@@ -0,0 +1,9 @@
@import "./segment-view";
@import "../segment-button/segment-button.md.vars";
// Material Design Segment View
// --------------------------------------------------
:host(.segment-view-disabled) {
opacity: $segment-button-md-opacity-disabled;
}

View File

@@ -0,0 +1,31 @@
// Segment View
// --------------------------------------------------
:host {
display: flex;
height: 100%;
overflow-x: scroll;
scroll-snap-type: x mandatory;
/* Hide scrollbar in Firefox */
scrollbar-width: none;
/* Hide scrollbar in IE and Edge */
-ms-overflow-style: none;
}
/* Hide scrollbar in webkit */
:host::-webkit-scrollbar {
display: none;
}
:host(.segment-view-disabled) {
touch-action: none;
overflow-x: hidden;
}
:host(.segment-view-scroll-disabled) {
pointer-events: none;
}

View File

@@ -0,0 +1,153 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Listen, Method, Prop, State, h } from '@stencil/core';
import type { SegmentViewScrollEvent } from './segment-view-interface';
@Component({
tag: 'ion-segment-view',
styleUrls: {
ios: 'segment-view.ios.scss',
md: 'segment-view.md.scss',
},
shadow: true,
})
export class SegmentView implements ComponentInterface {
private scrollEndTimeout: ReturnType<typeof setTimeout> | null = null;
private isTouching = false;
@Element() el!: HTMLElement;
/**
* If `true`, the segment view cannot be interacted with.
*/
@Prop() disabled = false;
/**
* @internal
*
* If `true`, the segment view is scrollable.
* If `false`, pointer events will be disabled. This is to prevent issues with
* quickly scrolling after interacting with a segment button.
*/
@State() isManualScroll?: boolean;
/**
* Emitted when the segment view is scrolled.
*/
@Event() ionSegmentViewScroll!: EventEmitter<SegmentViewScrollEvent>;
@Listen('scroll')
handleScroll(ev: Event) {
const { scrollLeft, scrollWidth, clientWidth } = ev.target as HTMLElement;
const scrollRatio = scrollLeft / (scrollWidth - clientWidth);
this.ionSegmentViewScroll.emit({
scrollRatio,
isManualScroll: this.isManualScroll ?? true,
});
// Reset the timeout to check for scroll end
this.resetScrollEndTimeout();
}
/**
* Handle touch start event to know when the user is actively dragging the segment view.
*/
@Listen('touchstart')
handleScrollStart() {
if (this.scrollEndTimeout) {
clearTimeout(this.scrollEndTimeout);
this.scrollEndTimeout = null;
}
this.isTouching = true;
}
/**
* Handle touch end event to know when the user is no longer dragging the segment view.
*/
@Listen('touchend')
handleTouchEnd() {
this.isTouching = false;
}
/**
* Reset the scroll end detection timer. This is called on every scroll event.
*/
private resetScrollEndTimeout() {
if (this.scrollEndTimeout) {
clearTimeout(this.scrollEndTimeout);
this.scrollEndTimeout = null;
}
this.scrollEndTimeout = setTimeout(
() => {
this.checkForScrollEnd();
},
// Setting this to a lower value may result in inconsistencies in behavior
// across browsers (particularly Firefox).
// Ideally, all of this logic is removed once the scroll end event is
// supported on all browsers (https://caniuse.com/?search=scrollend)
100
);
}
/**
* Check if the scroll has ended and the user is not actively touching.
* If the conditions are met (active content is enabled and no active touch),
* reset the scroll position and emit the scroll end event.
*/
private checkForScrollEnd() {
// Only emit scroll end event if the active content is not disabled and
// the user is not touching the segment view
if (!this.isTouching) {
this.isManualScroll = undefined;
}
}
/**
* @internal
*
* This method is used to programmatically set the displayed segment content
* in the segment view. Calling this method will update the `value` of the
* corresponding segment button.
*
* @param id: The id of the segment content to display.
* @param smoothScroll: Whether to animate the scroll transition.
*/
@Method()
async setContent(id: string, smoothScroll = true) {
const contents = this.getSegmentContents();
const index = contents.findIndex((content) => content.id === id);
if (index === -1) return;
this.isManualScroll = false;
this.resetScrollEndTimeout();
const contentWidth = this.el.offsetWidth;
this.el.scrollTo({
top: 0,
left: index * contentWidth,
behavior: smoothScroll ? 'smooth' : 'instant',
});
}
private getSegmentContents(): HTMLIonSegmentContentElement[] {
return Array.from(this.el.querySelectorAll('ion-segment-content'));
}
render() {
const { disabled, isManualScroll } = this;
return (
<Host
class={{
'segment-view-disabled': disabled,
'segment-view-scroll-disabled': isManualScroll === false,
}}
>
<slot></slot>
</Host>
);
}
}

View File

@@ -0,0 +1,194 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Segment View - Basic</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
ion-segment-view {
height: 100px;
margin-bottom: 20px;
}
ion-segment-content {
display: flex;
justify-content: center;
align-items: center;
}
ion-segment-content:nth-of-type(3n + 1) {
background: lightpink;
}
ion-segment-content:nth-of-type(3n + 2) {
background: lightblue;
}
ion-segment-content:nth-of-type(3n + 3) {
background: lightgreen;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Segment View - Basic</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-segment id="noValueSegment">
<ion-segment-button content-id="no" value="no">
<ion-label>No</ion-label>
</ion-segment-button>
<ion-segment-button content-id="value" value="value">
<ion-label>Value</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view id="noValueSegmentView">
<ion-segment-content id="no">No</ion-segment-content>
<ion-segment-content id="value">Value</ion-segment-content>
</ion-segment-view>
<ion-segment value="free">
<ion-segment-button content-id="paid" value="paid">
<ion-label>Paid</ion-label>
</ion-segment-button>
<ion-segment-button style="min-width: 200px" content-id="free" value="free">
<ion-label>Free</ion-label>
</ion-segment-button>
<ion-segment-button content-id="top" value="top">
<ion-label>Top</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="paid">Paid</ion-segment-content>
<ion-segment-content id="free">Free</ion-segment-content>
<ion-segment-content id="top">Top</ion-segment-content>
</ion-segment-view>
<ion-segment value="peach" scrollable>
<ion-segment-button content-id="orange" value="orange">
<ion-label>Orange</ion-label>
</ion-segment-button>
<ion-segment-button content-id="banana" value="banana">
<ion-label>Banana</ion-label>
</ion-segment-button>
<ion-segment-button content-id="pear" value="pear">
<ion-label>Pear</ion-label>
</ion-segment-button>
<ion-segment-button content-id="peach" value="peach">
<ion-label>Peach</ion-label>
</ion-segment-button>
<ion-segment-button content-id="grape" value="grape">
<ion-label>Grape</ion-label>
</ion-segment-button>
<ion-segment-button content-id="mango" value="mango">
<ion-label>Mango</ion-label>
</ion-segment-button>
<ion-segment-button content-id="apple" value="apple">
<ion-label>Apple</ion-label>
</ion-segment-button>
<ion-segment-button content-id="strawberry" value="strawberry">
<ion-label>Strawberry</ion-label>
</ion-segment-button>
<ion-segment-button content-id="cherry" value="cherry">
<ion-label>Cherry</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="orange">Orange</ion-segment-content>
<ion-segment-content id="banana">Banana</ion-segment-content>
<ion-segment-content id="pear">Pear</ion-segment-content>
<ion-segment-content id="peach">Peach</ion-segment-content>
<ion-segment-content id="grape">Grape</ion-segment-content>
<ion-segment-content id="mango">Mango</ion-segment-content>
<ion-segment-content id="apple">Apple</ion-segment-content>
<ion-segment-content id="strawberry">Strawberry</ion-segment-content>
<ion-segment-content id="cherry">Cherry</ion-segment-content>
</ion-segment-view>
<button class="expand" onClick="changeSegmentContent()">Change Segment Content</button>
<button class="expand" onClick="clearSegmentValue()">Clear Segment Value</button>
<button class="expand" onClick="addSegmentButtonAndContent()">Add New Segment Button & Content</button>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-title>Footer</ion-title>
</ion-toolbar>
</ion-footer>
<script>
function changeSegmentContent() {
const segment = document.querySelector('#noValueSegment');
const segmentView = document.querySelector('#noValueSegmentView');
let currentValue = segment.value;
if (currentValue === 'value') {
currentValue = 'no';
} else {
currentValue = 'value';
}
segment.value = currentValue;
}
async function clearSegmentValue() {
const segmentView = document.querySelector('#noValueSegmentView');
segmentView.setContent('no', false);
// Set timeout to ensure the value is cleared after
// the segment content is updated
setTimeout(() => {
const segment = document.querySelector('#noValueSegment');
segment.value = undefined;
});
}
async function addSegmentButtonAndContent() {
const segment = document.querySelector('ion-segment');
const segmentView = document.querySelector('ion-segment-view');
const newButton = document.createElement('ion-segment-button');
const newId = `new-${Date.now()}`;
newButton.setAttribute('content-id', newId);
newButton.setAttribute('value', newId);
newButton.innerHTML = '<ion-label>New Button</ion-label>';
segment.appendChild(newButton);
setTimeout(() => {
// Timeout to test waitForSegmentContent() in segment-button
const newContent = document.createElement('ion-segment-content');
newContent.setAttribute('id', newId);
newContent.innerHTML = 'New Content';
segmentView.appendChild(newContent);
// Necessary timeout to ensure the value is set after the content is added.
// Otherwise, the transition is unsuccessful and the content is not shown.
setTimeout(() => {
segment.setAttribute('value', newId);
}, 200);
}, 200);
}
</script>
</ion-app>
</body>
</html>

View File

@@ -0,0 +1,173 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
/**
* This behavior does not vary across modes/directions
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('segment-view: basic'), () => {
test('should show the first content with no initial value', async ({ page }) => {
await page.setContent(
`
<ion-segment>
<ion-segment-button content-id="paid" value="paid">
<ion-label>Paid</ion-label>
</ion-segment-button>
<ion-segment-button content-id="free" value="free">
<ion-label>Free</ion-label>
</ion-segment-button>
<ion-segment-button content-id="top" value="top">
<ion-label>Top</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="paid">Paid</ion-segment-content>
<ion-segment-content id="free">Free</ion-segment-content>
<ion-segment-content id="top">Top</ion-segment-content>
</ion-segment-view>
`,
config
);
const segmentContent = page.locator('ion-segment-content[id="paid"]');
await expect(segmentContent).toBeInViewport();
});
test('should show the content matching the initial value', async ({ page }) => {
await page.setContent(
`
<ion-segment value="free">
<ion-segment-button content-id="paid" value="paid">
<ion-label>Paid</ion-label>
</ion-segment-button>
<ion-segment-button content-id="free" value="free">
<ion-label>Free</ion-label>
</ion-segment-button>
<ion-segment-button content-id="top" value="top">
<ion-label>Top</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="paid">Paid</ion-segment-content>
<ion-segment-content id="free">Free</ion-segment-content>
<ion-segment-content id="top">Top</ion-segment-content>
</ion-segment-view>
`,
config
);
const segmentContent = page.locator('ion-segment-content[id="free"]');
await expect(segmentContent).toBeInViewport();
});
test('should update the content when changing the value by clicking a segment button', async ({ page }) => {
await page.setContent(
`
<ion-segment value="free">
<ion-segment-button content-id="paid" value="paid">
<ion-label>Paid</ion-label>
</ion-segment-button>
<ion-segment-button content-id="free" value="free">
<ion-label>Free</ion-label>
</ion-segment-button>
<ion-segment-button content-id="top" value="top">
<ion-label>Top</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="paid">Paid</ion-segment-content>
<ion-segment-content id="free">Free</ion-segment-content>
<ion-segment-content id="top">Top</ion-segment-content>
</ion-segment-view>
`,
config
);
await page.locator('ion-segment-button[value="top"]').click();
const segmentContent = page.locator('ion-segment-content[id="top"]');
await expect(segmentContent).toBeInViewport();
});
});
test('should set correct segment button as checked when changing the value by scrolling the segment content', async ({
page,
}) => {
await page.setContent(
`
<ion-segment value="free">
<ion-segment-button content-id="paid" value="paid">
<ion-label>Paid</ion-label>
</ion-segment-button>
<ion-segment-button content-id="free" value="free">
<ion-label>Free</ion-label>
</ion-segment-button>
<ion-segment-button content-id="top" value="top">
<ion-label>Top</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="paid">Paid</ion-segment-content>
<ion-segment-content id="free">Free</ion-segment-content>
<ion-segment-content id="top">Top</ion-segment-content>
</ion-segment-view>
`,
config
);
await page
.locator('ion-segment-view')
.evaluate(
(segmentView: HTMLIonSegmentViewElement) => !segmentView.classList.contains('segment-view-scroll-disabled')
);
await page.waitForChanges();
await page.locator('ion-segment-content[id="top"]').scrollIntoViewIfNeeded();
const segmentButton = page.locator('ion-segment-button[value="top"]');
await expect(segmentButton).toHaveClass(/segment-button-checked/);
});
test('should set correct segment button as checked and show correct content when programmatically setting the segment vale', async ({
page,
}) => {
await page.setContent(
`
<ion-segment value="free">
<ion-segment-button content-id="paid" value="paid">
<ion-label>Paid</ion-label>
</ion-segment-button>
<ion-segment-button content-id="free" value="free">
<ion-label>Free</ion-label>
</ion-segment-button>
<ion-segment-button content-id="top" value="top">
<ion-label>Top</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="paid">Paid</ion-segment-content>
<ion-segment-content id="free">Free</ion-segment-content>
<ion-segment-content id="top">Top</ion-segment-content>
</ion-segment-view>
`,
config
);
await page
.locator('ion-segment-view')
.evaluate(
(segmentView: HTMLIonSegmentViewElement) => !segmentView.classList.contains('segment-view-scroll-disabled')
);
await page.waitForChanges();
await page.locator('ion-segment').evaluate((segment: HTMLIonSegmentElement) => (segment.value = 'top'));
const segmentButton = page.locator('ion-segment-button[value="top"]');
await expect(segmentButton).toHaveClass(/segment-button-checked/);
const segmentContent = page.locator('ion-segment-content[id="top"]');
await expect(segmentContent).toBeInViewport();
});
});

View File

@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Segment View - Disabled</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
ion-segment-view {
height: 100px;
}
ion-segment-content {
display: flex;
justify-content: center;
align-items: center;
}
ion-segment-content:nth-of-type(1) {
background: lightpink;
}
ion-segment-content:nth-of-type(2) {
background: lightblue;
}
ion-segment-content:nth-of-type(3) {
background: lightgreen;
}
ion-segment-content:nth-of-type(4) {
background: lightgoldenrodyellow;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Segment View - Disabled</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-segment>
<ion-segment-button disabled content-id="all" value="all">
<ion-label>All</ion-label>
</ion-segment-button>
<ion-segment-button content-id="favorites" value="favorites">
<ion-label>Favorites</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="all">All</ion-segment-content>
<ion-segment-content id="favorites">Favorites</ion-segment-content>
</ion-segment-view>
<ion-segment disabled value="paid">
<ion-segment-button content-id="paid" value="paid">
<ion-label>Paid</ion-label>
</ion-segment-button>
<ion-segment-button content-id="free" value="free">
<ion-label>Free</ion-label>
</ion-segment-button>
<ion-segment-button content-id="top" value="top">
<ion-label>Top</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content id="paid">Paid</ion-segment-content>
<ion-segment-content id="free">Free</ion-segment-content>
<ion-segment-content id="top">Top</ion-segment-content>
</ion-segment-view>
<ion-segment value="reading-list">
<ion-segment-button content-id="bookmarks" value="bookmarks">
<ion-label>Bookmarks</ion-label>
</ion-segment-button>
<ion-segment-button content-id="reading-list" value="reading-list">
<ion-label>Reading List</ion-label>
</ion-segment-button>
<ion-segment-button content-id="shared-links" value="shared-links">
<ion-label>Shared Links</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view disabled>
<ion-segment-content id="bookmarks">Bookmarks</ion-segment-content>
<ion-segment-content id="reading-list">Reading List</ion-segment-content>
<ion-segment-content id="shared-links">Shared Links</ion-segment-content>
</ion-segment-view>
</ion-content>
</ion-app>
</body>
</html>

View File

@@ -0,0 +1,49 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
/**
* This behavior does not vary across directions
*/
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('segment-view: disabled'), () => {
test('should not have visual regressions', async ({ page }) => {
await page.goto('/src/components/segment-view/test/disabled', config);
await expect(page).toHaveScreenshot(screenshot(`segment-view-disabled`));
});
});
});
/**
* This behavior does not vary across modes/directions
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('segment-view: disabled'), () => {
test('should keep button enabled even when disabled prop is set', async ({ page }) => {
await page.setContent(
`
<ion-segment>
<ion-segment-button content-id="paid" value="paid">
<ion-label>Paid</ion-label>
</ion-segment-button>
<ion-segment-button disabled content-id="free" value="free">
<ion-label>Free</ion-label>
</ion-segment-button>
<ion-segment-button content-id="top" value="top">
<ion-label>Top</ion-label>
</ion-segment-button>
</ion-segment>
<ion-segment-view>
<ion-segment-content disabled id="paid">Paid</ion-segment-content>
<ion-segment-content id="free">Free</ion-segment-content>
<ion-segment-content id="top">Top</ion-segment-content>
</ion-segment-view>
`,
config
);
const segmentButton = page.locator('ion-segment-button[value="free"]');
await expect(segmentButton).not.toHaveClass(/segment-button-disabled/);
});
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

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';
@@ -27,6 +28,16 @@ export class Segment implements ComponentInterface {
// Value before the segment is dragged
private valueBeforeGesture?: SegmentValue;
private segmentViewEl?: HTMLIonSegmentViewElement | null = null;
private lastNextIndex?: number;
/**
* Whether to update the segment view, if exists, when the value changes.
* This behavior is enabled by default, but is set false when scrolling content views
* since we don't want to "double scroll" the segment view.
*/
private triggerScrollOnValueChange?: boolean;
@Element() el!: HTMLIonSegmentElement;
@State() activated = false;
@@ -78,13 +89,41 @@ export class Segment implements ComponentInterface {
@Prop({ mutable: true }) value?: SegmentValue;
@Watch('value')
protected valueChanged(value: SegmentValue | undefined) {
protected valueChanged(value: SegmentValue | undefined, oldValue?: SegmentValue | undefined) {
// Force a value to exist if we're using a segment view
if (this.segmentViewEl && value === undefined) {
this.value = this.getButtons()[0].value;
return;
}
if (oldValue !== undefined && value !== undefined) {
const buttons = this.getButtons();
const previous = buttons.find((button) => button.value === oldValue);
const current = buttons.find((button) => button.value === value);
if (previous && current) {
if (!this.segmentViewEl) {
this.checkButton(previous, current);
} else if (this.triggerScrollOnValueChange !== false) {
this.updateSegmentView();
}
}
} else if (value !== undefined && oldValue === undefined && this.segmentViewEl) {
this.updateSegmentView();
}
/**
* `ionSelect` is emitted every time the value changes (internal or external changes).
* Used by `ion-segment-button` to determine if the button should be checked.
*/
this.ionSelect.emit({ value });
this.scrollActiveButtonIntoView();
// The scroll listener should handle scrolling the active button into view as needed
if (!this.segmentViewEl) {
this.scrollActiveButtonIntoView();
}
this.triggerScrollOnValueChange = undefined;
}
/**
@@ -118,9 +157,13 @@ export class Segment implements ComponentInterface {
disabledChanged() {
this.gestureChanged();
const buttons = this.getButtons();
for (const button of buttons) {
button.disabled = this.disabled;
if (!this.segmentViewEl) {
const buttons = this.getButtons();
for (const button of buttons) {
button.disabled = this.disabled;
}
} else {
this.segmentViewEl.disabled = this.disabled;
}
}
@@ -132,6 +175,12 @@ export class Segment implements ComponentInterface {
connectedCallback() {
this.emitStyle();
this.segmentViewEl = this.getSegmentView();
}
disconnectedCallback() {
this.segmentViewEl = null;
}
componentWillLoad() {
@@ -139,6 +188,8 @@ export class Segment implements ComponentInterface {
}
async componentDidLoad() {
this.segmentViewEl = this.getSegmentView();
this.setCheckedClasses();
/**
@@ -170,6 +221,10 @@ export class Segment implements ComponentInterface {
if (this.disabled) {
this.disabledChanged();
}
// Update segment view based on the initial value,
// but do not animate the scroll
this.updateSegmentView(false);
}
onStart(detail: GestureDetail) {
@@ -192,6 +247,7 @@ export class Segment implements ComponentInterface {
if (value !== undefined) {
if (this.valueBeforeGesture !== value) {
this.emitValueChange();
this.updateSegmentView();
}
}
this.valueBeforeGesture = undefined;
@@ -208,7 +264,7 @@ export class Segment implements ComponentInterface {
this.ionChange.emit({ value });
}
private getButtons() {
private getButtons(): HTMLIonSegmentButtonElement[] {
return Array.from(this.el.querySelectorAll('ion-segment-button'));
}
@@ -224,11 +280,7 @@ export class Segment implements ComponentInterface {
const buttons = this.getButtons();
buttons.forEach((button) => {
if (activated) {
button.classList.add('segment-button-activated');
} else {
button.classList.remove('segment-button-activated');
}
button.classList.toggle('segment-button-activated', activated);
});
this.activated = activated;
}
@@ -293,6 +345,8 @@ export class Segment implements ComponentInterface {
// Remove the transform to slide the indicator back to the button clicked
currentIndicator.style.setProperty('transform', '');
this.scrollActiveButtonIntoView(true);
});
this.value = current.value;
@@ -312,6 +366,74 @@ export class Segment implements ComponentInterface {
}
}
private getSegmentView() {
const buttons = this.getButtons();
// Get the first button with a contentId
const firstContentId = buttons.find((button: HTMLIonSegmentButtonElement) => button.contentId);
// Get the segment content with an id matching the button's contentId
const segmentContent = document.querySelector(`ion-segment-content[id="${firstContentId?.contentId}"]`);
// Return the segment view for that matching segment content
return segmentContent?.closest('ion-segment-view');
}
@Listen('ionSegmentViewScroll', { target: 'body' })
handleSegmentViewScroll(ev: CustomEvent<SegmentViewScrollEvent>) {
const { scrollRatio, isManualScroll } = ev.detail;
if (!isManualScroll) {
return;
}
const dispatchedFrom = ev.target as HTMLElement;
const segmentViewEl = this.segmentViewEl as EventTarget;
const segmentEl = this.el;
// Only update the indicator if the event was dispatched from the correct segment view
if (ev.composedPath().includes(segmentViewEl) || dispatchedFrom?.contains(segmentEl)) {
const buttons = this.getButtons();
// If no buttons are found or there is no value set then do nothing
if (!buttons.length) return;
const index = buttons.findIndex((button) => button.value === this.value);
const current = buttons[index];
const nextIndex = Math.round(scrollRatio * (buttons.length - 1));
if (this.lastNextIndex === undefined || this.lastNextIndex !== nextIndex) {
this.lastNextIndex = nextIndex;
this.triggerScrollOnValueChange = false;
this.checkButton(current, buttons[nextIndex]);
this.emitValueChange();
}
}
}
/**
* Finds the related segment view and sets its current content
* based on the selected segment button. This method
* should be called on initial load of the segment,
* after the gesture is completed (if dragging between segments)
* and when a segment button is clicked directly.
*/
private updateSegmentView(smoothScroll = true) {
const buttons = this.getButtons();
const button = buttons.find((btn) => btn.value === this.value);
// If the button does not have a contentId then there is
// no associated segment view to update
if (!button?.contentId) {
return;
}
const segmentView = this.segmentViewEl;
if (segmentView) {
segmentView.setContent(button.contentId, smoothScroll);
}
}
private scrollActiveButtonIntoView(smoothScroll = true) {
const { scrollable, value, el } = this;
@@ -342,21 +464,35 @@ export class Segment implements ComponentInterface {
const centeredX = activeButtonLeft - scrollContainerBox.width / 2 + activeButtonBox.width / 2;
/**
* We intentionally use scrollBy here instead of scrollIntoView
* newScrollPosition is the absolute scroll position that the
* container needs to move to in order to center the active button.
* It is calculated by adding the current scroll position
* (scrollLeft) to the offset needed to center the button
* (centeredX).
*/
const newScrollPosition = el.scrollLeft + centeredX;
/**
* We intentionally use scrollTo here instead of scrollIntoView
* to avoid a WebKit bug where accelerated animations break
* when using scrollIntoView. Using scrollIntoView will cause the
* segment container to jump during the transition and then snap into place.
* This is because scrollIntoView can potentially cause parent element
* containers to also scroll. scrollBy does not have this same behavior, so
* containers to also scroll. scrollTo does not have this same behavior, so
* we use this API instead.
*
* scrollTo is used instead of scrollBy because there is a
* Webkit bug that causes scrollBy to not work smoothly when
* the active button is near the edge of the scroll container.
* This leads to the buttons to jump around during the transition.
*
* Note that if there is not enough scrolling space to center the element
* within the scroll container, the browser will attempt
* to center by as much as it can.
*/
el.scrollBy({
el.scrollTo({
top: 0,
left: centeredX,
left: newScrollPosition,
behavior: smoothScroll ? 'smooth' : 'instant',
});
}
@@ -478,7 +614,13 @@ export class Segment implements ComponentInterface {
this.emitValueChange();
}
if (this.scrollable || !this.swipeGesture) {
if (this.segmentViewEl) {
this.updateSegmentView();
if (this.scrollable && previous) {
this.checkButton(previous, current);
}
} else if (this.scrollable || !this.swipeGesture) {
if (previous) {
this.checkButton(previous, current);
} else {

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

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