Compare commits

...

30 Commits

Author SHA1 Message Date
ionitron
507d5ee1da v7.5.8 2023-12-06 15:04:23 +00:00
Maria Hutt
7871b56ecc fix(angular): add missing menu controller methods (#28618)
Issue number: resolves #20053 

---------

<!-- 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 methods that that are missing for the menu in the
Angular packages. This leads to users to not being able to use methods
like `isAnimating()`.

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

- The missing methods have been added by implementing `MenuControllerI`.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## 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: 7.5.8-dev.11701461830.1be851fd
2023-12-05 18:43:12 +00:00
dependabot[bot]
e2cbc9c15f chore(deps): Bump @stencil/core from 4.8.0 to 4.8.1 in /core (#28632)
Bumps [@stencil/core](https://github.com/ionic-team/stencil) from 4.8.0
to 4.8.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ionic-team/stencil/releases"><code>@​stencil/core</code>'s
releases</a>.</em></p>
<blockquote>
<h2>🍹 v4.8.1 (2023-12-04)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>runtime:</strong> apply nonce to data styles before DOM
insert (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5112">#5112</a>)
(<a
href="df46fdc0cb">df46fdc</a>),
closes <a
href="https://redirect.github.com/ionic-team/stencil/issues/5102">#5102</a></li>
<li><strong>runtime:</strong> call form-associated lifecycle callbacks
w/ <code>this</code> (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5104">#5104</a>)
(<a
href="1ac8aa3da1">1ac8aa3</a>)</li>
<li><strong>testing:</strong> re-add Puppeteer <code>asElement()</code>
calls (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5114">#5114</a>)
(<a
href="0c843f8d19">0c843f8</a>),
closes <a
href="https://redirect.github.com/ionic-team/stencil/issues/5113">#5113</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ionic-team/stencil/blob/main/CHANGELOG.md"><code>@​stencil/core</code>'s
changelog</a>.</em></p>
<blockquote>
<h2>🍹 <a
href="https://github.com/ionic-team/stencil/compare/v4.8.0...v4.8.1">4.8.1</a>
(2023-12-04)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>runtime:</strong> apply nonce to data styles before DOM
insert (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5112">#5112</a>)
(<a
href="df46fdc0cb">df46fdc</a>),
closes <a
href="https://redirect.github.com/ionic-team/stencil/issues/5102">#5102</a></li>
<li><strong>runtime:</strong> call form-associated lifecycle callbacks
w/ <code>this</code> (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5104">#5104</a>)
(<a
href="1ac8aa3da1">1ac8aa3</a>)</li>
<li><strong>testing:</strong> re-add Puppeteer <code>asElement()</code>
calls (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5114">#5114</a>)
(<a
href="0c843f8d19">0c843f8</a>),
closes <a
href="https://redirect.github.com/ionic-team/stencil/issues/5113">#5113</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="28f794ea0d"><code>28f794e</code></a>
Release v4.8.1 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5130">#5130</a>)</li>
<li><a
href="2fd398a6cf"><code>2fd398a</code></a>
chore(deps): update dependency <code>@​rollup/pluginutils</code> to
v5.1.0 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5126">#5126</a>)</li>
<li><a
href="84feb5aee2"><code>84feb5a</code></a>
chore(deps): update typescript-eslint to v6.13.1 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5128">#5128</a>)</li>
<li><a
href="cf4a70150c"><code>cf4a701</code></a>
fix(compiler): revert changes to style import transformer (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5125">#5125</a>)</li>
<li><a
href="df32ceb4ae"><code>df32ceb</code></a>
chore(deps): update dependency fs-extra to v11.2.0 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5127">#5127</a>)</li>
<li><a
href="4cdb725fb7"><code>4cdb725</code></a>
chore(deps): update dependency dts-bundle-generator to v9 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5129">#5129</a>)</li>
<li><a
href="3ef8fa070a"><code>3ef8fa0</code></a>
chore(deps): update dependency <code>@​types/eslint</code> to v8.44.8
(<a
href="https://redirect.github.com/ionic-team/stencil/issues/5122">#5122</a>)</li>
<li><a
href="1342eec577"><code>1342eec</code></a>
chore(deps): update dependency <code>@​types/node</code> to v20.10.1 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5123">#5123</a>)</li>
<li><a
href="7591dce85e"><code>7591dce</code></a>
fix(compiler): ensure not to import duplicate style identifier (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5119">#5119</a>)</li>
<li><a
href="7302e3f81a"><code>7302e3f</code></a>
chore(deps): update node.js to v20.10.0 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5117">#5117</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/ionic-team/stencil/compare/v4.8.0...v4.8.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@stencil/core&package-manager=npm_and_yarn&previous-version=4.8.0&new-version=4.8.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-05 00:58:11 +00:00
Liam DeBeasi
4847488090 chore(ci): update labeler workflow for labeler@v5 (#28627)
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. -->

The labeler action is currently failing:
https://github.com/ionic-team/ionic-framework/actions/runs/7090913880/job/19298918578?pr=28622.
This is happening due to a breaking change in v5 of the action.

We currently pull from `main` for this action so we are now receiving v5
of the action:
fe3c3d500a/.github/workflows/label.yml (L16)

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

- This PR updates the action explicitly to v5 so we don't unexpectedly
take on breaking changes
- This PR also updates the labeler yaml file to account for the v5
breaking changes

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2023-12-04 21:31:12 +00:00
Shawn Taylor
fe3c3d500a docs(input, searchbar, textarea): improve docs for managing focus (#28614)
Issue number: Related to #18132 

---------

<!-- 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 documentation about the `autofocus` prop is unclear and does not
accurately reflect how it actually works across browsers and devices.

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

- The documentation for `autofocus` and `setFocus` are more detailed.
- The documentation links to the relevant page in the docs.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2023-12-04 13:24:34 +00:00
Sean Perkins
1705d064cc fix(react): router creates new view instances of parameterized routes (#28616)
Issue number: Resolves #26524 

---------

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

## Definitions

**Parameterized routes**: A route that includes one or more variables in
the path segments, such as `/form/:index`.

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

When an application routes from a parameterized route, to an
intermediary route, to the same parameterized route, but with a
different value/url, Ionic's routing logic is incorrectly reusing the
view item from the first instance of the parameterized route instead of
calculating that the matched path is different. This results in the
wrong view item being recycled and rendered.

Another way of representing it:
- User navigates to `/form/0` which resolves `FormPage`
- User enters `0` into the form and submits the form
- User navigates to `/link`, which resolves `LinkPage`
- User navigates to `/form/1`, which resolves `FormPage`
- However, instead of creating a new instance of `FormPage` it is
reusing the instance of `FormPage` from `/form/0` which includes the
form having `0` in the input.
  - The user now sees a "new view", but with cached data in the form.

This is not expected or desired. 


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

- Ionic's routing logic will validate if the entering view item matches
the match route data before reusing it. This results in new instances of
the view item being constructed when using parameterized routes.


https://github.com/ionic-team/ionic-framework/assets/13732623/e7e3d03f-2848-4429-9f60-9074d0761e45


## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## 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: `7.5.8-dev.11701383555.17254408`
2023-12-01 22:11:41 +00:00
Éric Le Maître
60303aad23 fix(vue): nav component accepts kebab-case component properties (#28615)
Issue number: resolves #28611

---------

<!-- 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. -->
It's not possible to pass props that are not camelCase to the `IonNav`
component.

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

- It is now possible to set a props with kebab-case instead of camelCase
(for example, `root-params` instead of `rootParams`)

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
⚠️ This is my first PR for ionic so I hope I didn't miss important steps
into the process. I also checked on my project that the fix is working
well. Thank you! 🙂

---------

Co-authored-by: Sean Perkins <sean@ionic.io>
2023-12-01 15:30:13 +00:00
Liam DeBeasi
a3cd204f61 fix(overlays): trigger is configured on load (#28526)
Issue number: resolves #28524

---------

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

Watchers in Stencil are constructed sometime between `connectedCallback`
and `componentDidLoad`. If a property is set/changed during that time it
is possible for the callback associated with the watcher to not fire
because the watcher has not been setup yet. This is most often with
`dist-custom-elements` and frameworks such as Angular when using a
binding (i.e. `[trigger]` instead of `trigger`)

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

- The trigger callback associated with the watcher is manually called in
`componentDidLoad` for each overlay.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## 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: `7.5.5-dev.11699974376.13a15397`

Note: This is a timing related bug due to a behavior in Stencil, so I
did not write automated tests. However, I manually verified that this
issue a) reproduces on `main` and b) is fixed with this dev build for
each overlay component.
2023-11-29 21:53:33 +00:00
Sean Perkins
5c2a73b262 chore(playwright): dark mode support (#28593)
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. -->

Testing dark mode is manual per test in Playwright. Ionic developer
needs to setup the variables and assign them to a selector that applies
in the class.

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

- The `.setContent` API will now work with a new config option to test
dark mode automatically without additional configuration/test set-up.
- Default theme is no theme (fallback theme)
- Screenshot names and test titles remain the same for all existing
tests. Only tests that opt into a theme will be pre-pended with `-dark`
or `-light` (as an example for current themes).

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2023-11-29 20:48:16 +00:00
dependabot[bot]
34417a5835 chore(deps): Bump @stencil/core from 4.7.2 to 4.8.0 in /core (#28586)
Bumps [@stencil/core](https://github.com/ionic-team/stencil) from 4.7.2
to 4.8.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ionic-team/stencil/releases"><code>@​stencil/core</code>'s
releases</a>.</em></p>
<blockquote>
<h2>🌞 v4.8.0 (2023-11-27)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>hydrate:</strong> prevent dead code elimination of patch dom
implementation (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4966">#4966</a>)
(<a
href="5e36057795">5e36057</a>)</li>
<li><strong>mock-doc:</strong> add <code>getAttributeNode</code> to mock
elements (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5070">#5070</a>)
(<a
href="4e840e0e0e">4e840e0</a>)</li>
<li><strong>mock-doc:</strong> add inert to HTMLAttributes (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5072">#5072</a>)
(<a
href="71a4110bbc">71a4110</a>),
closes <a
href="https://redirect.github.com/ionic-team/stencil/issues/5071">#5071</a></li>
<li><strong>runtime:</strong> apply textnodes to shadow DOM instead of
light DOM (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4946">#4946</a>)
(<a
href="217d588949">217d588</a>)</li>
<li><strong>test:</strong> pass jest args correctly for v28/29 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5068">#5068</a>)
(<a
href="5c4ac32805">5c4ac32</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li><strong>declarations:</strong> add popover attributes to JSX
declarations (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5064">#5064</a>)
(<a
href="f73aa149f0">f73aa14</a>)</li>
<li><strong>runtime:</strong> proxy form associated custom element
lifecycle callbacks (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4939">#4939</a>)
(<a
href="ca53dbb02e">ca53dbb</a>)</li>
</ul>
<h2>Thanks</h2>
<p>🎉 Thanks <a
href="https://github.com/HansClaasen"><code>@​HansClaasen</code></a> for
their contributions! 🎉</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ionic-team/stencil/blob/main/CHANGELOG.md"><code>@​stencil/core</code>'s
changelog</a>.</em></p>
<blockquote>
<h1>🌞 <a
href="https://github.com/ionic-team/stencil/compare/v4.7.2...v4.8.0">4.8.0</a>
(2023-11-27)</h1>
<h3>Bug Fixes</h3>
<ul>
<li><strong>hydrate:</strong> prevent dead code elimination of patch dom
implementation (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4966">#4966</a>)
(<a
href="5e36057795">5e36057</a>)</li>
<li><strong>mock-doc:</strong> add <code>getAttributeNode</code> to mock
elements (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5070">#5070</a>)
(<a
href="4e840e0e0e">4e840e0</a>)</li>
<li><strong>mock-doc:</strong> add inert to HTMLAttributes (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5072">#5072</a>)
(<a
href="71a4110bbc">71a4110</a>),
closes <a
href="https://redirect.github.com/ionic-team/stencil/issues/5071">#5071</a></li>
<li><strong>runtime:</strong> apply textnodes to shadow DOM instead of
light DOM (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4946">#4946</a>)
(<a
href="217d588949">217d588</a>)</li>
<li><strong>test:</strong> pass jest args correctly for v28/29 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5068">#5068</a>)
(<a
href="5c4ac32805">5c4ac32</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li><strong>declarations:</strong> add popover attributes to JSX
declarations (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5064">#5064</a>)
(<a
href="f73aa149f0">f73aa14</a>)</li>
<li><strong>runtime:</strong> proxy form associated custom element
lifecycle callbacks (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4939">#4939</a>)
(<a
href="ca53dbb02e">ca53dbb</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="cddcad8ca2"><code>cddcad8</code></a>
v4.8.0 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5103">#5103</a>)</li>
<li><a
href="bcb17ac6cb"><code>bcb17ac</code></a>
docs(runtime): add interface docs to <code>HostRef</code> (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4871">#4871</a>)</li>
<li><a
href="edc9f384ac"><code>edc9f38</code></a>
chore(deps): update dependency npm to v10.2.4 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5085">#5085</a>)</li>
<li><a
href="49c7607687"><code>49c7607</code></a>
chore(deps): update typescript-eslint to v6.11.0 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5087">#5087</a>)</li>
<li><a
href="9c8790d601"><code>9c8790d</code></a>
test(vdom): adding tests for insertVdomAnnotations (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4867">#4867</a>)</li>
<li><a
href="13ec78e1bb"><code>13ec78e</code></a>
chore(deps): update dependency puppeteer to v21.5.2 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5086">#5086</a>)</li>
<li><a
href="9a3e81cb77"><code>9a3e81c</code></a>
chore(deps): update dependency <code>@​types/node</code> to v20.9.1 (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5084">#5084</a>)</li>
<li><a
href="217d588949"><code>217d588</code></a>
fix(runtime): apply textnodes to shadow DOM instead of light DOM (<a
href="https://redirect.github.com/ionic-team/stencil/issues/4946">#4946</a>)</li>
<li><a
href="ca53dbb02e"><code>ca53dbb</code></a>
feat(runtime): proxy form associated custom element lifecycle callbacks
(<a
href="https://redirect.github.com/ionic-team/stencil/issues/4939">#4939</a>)</li>
<li><a
href="7c86019e59"><code>7c86019</code></a>
refactor(build): stop removing process.binding in compiler (<a
href="https://redirect.github.com/ionic-team/stencil/issues/5078">#5078</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/ionic-team/stencil/compare/v4.7.2...v4.8.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@stencil/core&package-manager=npm_and_yarn&previous-version=4.7.2&new-version=4.8.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-29 16:39:17 +00:00
Liam DeBeasi
8be83f9f88 merge release-7.5.7
Release 7.5.7
2023-11-29 11:19:09 -05:00
ionitron
2e1261a52c chore(): update package lock files 2023-11-29 15:05:23 +00:00
ionitron
3a35fdd2f5 v7.5.7 2023-11-29 15:05:12 +00:00
Sean Perkins
bd27846b28 chore(playwright): resolve axe violations with setContent (#28592)
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. -->

When using `.setContent`, the initial page template is lacking key
metadata that will result in AXE violations.

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

- Adds a default `lang` to the `html` document root
- Adds a default `title` tag

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2023-11-29 00:41:00 +00:00
Liam DeBeasi
196a22eb74 chore: fallback to framework team with codeowners (#28588)
I noticed that Dependabot PRs never had anyone assigned:
https://github.com/ionic-team/ionic-framework/pull/28586

The reason was that we removed `* @ionic-team/framework` when we
adjusted the codeowners in
https://github.com/ionic-team/ionic-framework/pull/27573.

My original reasoning for why we should remove it was wrong. GitHub only
assigns reviewers within a team if the team itself is assigned. By
removing the team from being reviewed, only people listed in the
codeowners file would be assigned. As an added example, PRs that do not
match codeowners currently have no one assigned:
https://github.com/ionic-team/ionic-framework/pull/28430
2023-11-28 16:44:51 +00:00
Shawn Taylor
01130e12e1 fix(datetime): allow disabling datetime with prefer-wheel (#28511)
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. -->
It is possible to navigate the columns of a disabled Datetime with
`prefer-wheel` via the keyboard.



https://github.com/ionic-team/ionic-framework/assets/14926794/9c9dafc4-4b77-45a6-a276-70201c5c3ea5



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

- Picker Column Internal has a disabled state that disables the full
column
- When a Datetime is disabled with `prefer-wheel`, the columns in the
Datetime will be disabled
- It is no longer possible to navigate the wheels in a disabled Datetime
via the keyboard


Comparison of native & Ionic components:

![Screenshot 2023-11-10 at 10 58
25 AM](https://github.com/ionic-team/ionic-framework/assets/14926794/e2bec1b3-30f8-4f64-8658-27b971884b7a)


## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->

---------

Co-authored-by: ionitron <hi@ionicframework.com>
Co-authored-by: Liam DeBeasi <liamdebeasi@users.noreply.github.com>
2023-11-22 16:07:43 +00:00
Sean Perkins
b833f0e826 fix(alert): date inputs render correctly in mobile safari (#28495)
Issue number: resolves #28494

---------

<!-- 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 appearance of the input is being set to `none` which clears the
browser appearance settings for height on the control. This results in
the `<input type="date" />` to render without a height.

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

- Minimum height is assigned to the alert input, forcing it to render at
it's expected height
- Date input renders correctly in Mobile Safari

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

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

We have screenshots for this component, but Playwright runs an emulation
of Mobile Safari that does not reproduce this issue that is only present
on device. There for we cannot write a test on this change.

---------

Co-authored-by: ionitron <hi@ionicframework.com>
2023-11-21 15:29:51 +00:00
Liam DeBeasi
7392b1cd4b merge release-7.5.6
Release 7.5.6
2023-11-21 10:28:41 -05:00
ionitron
93ed25693c chore(): update package lock files 2023-11-21 14:31:53 +00:00
ionitron
f6a740dce5 v7.5.6 2023-11-21 14:31:40 +00:00
Liam DeBeasi
9453132aa8 fix(angular): overlays are defined when using standalone controllers (#28560)
Issue number: resolves #28385

---------

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

Overlay controllers do not register their respective overlays
components. This results in the overlay not appearing.

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

- Each standalone overlay controller manually calls
`defineCustomElement` for their respective overlay component to ensure
the component is loaded/registered.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## 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: `7.5.6-dev.11700492285.1581ed02`
2023-11-20 22:45:51 +00:00
Sean Perkins
c07312e5ed fix(angular): ng add @ionic/angular in standalone projects (#28523)
Issue number: Resolves #28514

---------

<!-- 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 using the `@ionic/angular` schematic in an Angular 17 project (`ng
add @ionic/angular`), developers will receive an error preventing the
schematic from running.

Additionally, the previous implementations of the schematic are out of
sync with the current state of the Ionic starters:
- `variables.css` is empty and missing Ionic's defaults
- `ionic.config.json` is not created
- Schematic does not have support for module vs. standalone projects.

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

- `ng add @ionic/angular` works with Angular 17 projects
- `ng add @ionic/angular` has fallback behavior for Angular 16 projects
using `AppModule`
- Schematics now includes the proper `variables.css` from Ionic starters
- Ionicons assets will no longer be copied when being added to a
standalone project
- Refactors a majority of the implementation to use the utilities that
come directly from `@angular-devkit/schematics` and
`@schematics/angular`.
- Sets the `@ionic/angular-toolkit` CLI configuration and schematics
configuration in the `angular.json`
- Creates missing `ionic.config.json`

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## 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: `7.5.5-dev.11700239837.1925bbdb`

To test this PR:

1. Install Angular CLI v17 - `npm install -g @angular/cli@17`
2. Create a new project - `ng new angular-17`
3. Use the dev-build: - `ng add
@ionic/angular@7.5.5-dev.11700239837.1925bbdb`
4. Confirm the prompts
5. Validate that `provideIonicAngular({})` is added to the
`app.config.ts`
6. Validate that `ionic.config.json` was created
7. Validate that `angular.json` was updated with the
`@ionic/angular-devkit` configurations

Now verify legacy behavior:

1. Install Angular CLI v16 - `npm install -g @angular/cli@16`
2. Create a new project - `ng new angular-16`
3. Use the dev-build - `ng add
@ionic/angular@7.5.5-dev.11700239837.1925bbdb`
4. Confirm the prompts
5. Validate that `IonicModule.forRoot({})` is added to the
`app.module.ts`
8. Validate the ionicons glob pattern is added to the `angular.json`
9. Validate the `ionic.config.json` was created
10. Validate the `angular.json` was updated with the
`@ionic/angular-devkit` configurations
2023-11-20 22:20:20 +00:00
Liam DeBeasi
388d19e04f fix(datetime): updating value with min scrolls to new value (#28549)
Issue number: resolves #28548

---------

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

Datetime was not scrolling at all when the `value` prop was changed
programmatically. This was due to some logic we had in
`componentDidRender` to work around a WebKit bug which was causing the
scroll position to be moved back to where it was prior to setting the
`value` prop. This caused the scroll position to never move.

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

- Datetime scrolls to the new value when `value` is updated
programmatically even if `min` is set.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## 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: `7.5.6-dev.11700169088.140f3e6a`

Co-authored-by: amandaejohnston
<amandaejohnston@users.noreply.github.com>
2023-11-20 14:19:44 +00:00
Shawn Taylor
adb01e2516 refactor(angular): loading controller uses correct core instance (#28543)
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?
As a takeaway from our learning session about a menuController bug in
Ionic Angular, the team would like to update our other providers to use
the same architecture as the menuController to prevent this kind of
issue from happening again in the future.

We also noticed that the common provider does not provide much value and
it's easier to just have two separate implementations in `src` and
`standalone`. (There wasn't much code we could de-duplicate)

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

- Removed the common loading provider in favor of separate ones in
src/standalone

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2023-11-17 21:16:56 +00:00
Sean Perkins
4f1b4cdc29 chore(core): type checking for unit tests (#28529)
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. -->

Type checking inside of the Stencil unit tests have been disabled for a
long time. This has resulted in a difficult developer experience and
numerous issues (both types and implementation) within our unit tests.

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

- Type checking is now enabled for all Stencil unit tests
- Tests have been updated to resolve type errors and implementation
errors
- Many `as any` casts were introduced, as many legacy tests test invalid
configurations of functions that require it (for example passing
`undefined` to an argument that cannot be `undefined`).

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

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

To test this PR you can checkout the branch locally. Install
dependencies in the `/core` directory to make sure you are on at least
`@stencil/core@4.7.2`.

Opening either a `.spec.ts` or `.spec.tsx` file, validate that your IDE
detects types and can provide auto completions for jest global types.

If you desire, you can provide an invalid type and try building the
project - you will observe the build will fail due to the invalid type.
2023-11-17 16:47:34 +00:00
Shawn Taylor
1a135ebd76 refactor(angular): alert controller uses correct core instance (#28538)
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?
As a takeaway from our learning session about a menuController bug in
Ionic Angular, the team would like to update our other providers to use
the same architecture as the menuController to prevent this kind of
issue from happening again in the future.

We also noticed that the common provider does not provide much value and
it's easier to just have two separate implementations in `src` and
`standalone`. (There wasn't much code we could de-duplicate)

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

- Removed the common alert provider in favor of separate ones in
src/standalone

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2023-11-16 16:50:43 +00:00
Liam DeBeasi
6a2be9fa3c fix(alert): match MD spec on tablet (#28501)
Issue number: resolves #23977

---------

<!-- 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 MD Alert on tablet dimensions does not match the MD spec

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

- MD Alert now follows the MD spec for tablet dimensions
- Added tablet and mobile viewport mixins for alert and the card modal.
(There should be no visual diffs for the card modal)

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

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

This supersedes https://github.com/ionic-team/ionic-framework/pull/27462
since I needed to add new screenshot tests. The author of that PR has
been given co-author credit here.

---------

Co-authored-by: GlenOttley <GlenOttley@users.noreply.github.com>
Co-authored-by: ionitron <hi@ionicframework.com>
Co-authored-by: Brandy Carney <brandyscarney@users.noreply.github.com>
2023-11-16 16:28:36 +00:00
Shawn Taylor
9d57758e3e refactor(angular): picker controller uses correct core instance (#28521)
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?
As a takeaway from our learning session about a menuController bug in
Ionic Angular, the team would like to update our other providers to use
the same architecture as the menuController to prevent this kind of
issue from happening again in the future.

We also noticed that the common provider does not provide much value and
it's easier to just have two separate implementations in `src` and
`standalone`. (There wasn't much code we could de-duplicate)

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

- Removed the common picker provider in favor of separate ones in
src/standalone

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2023-11-16 12:57:02 +00:00
Sean Perkins
f143bd0a11 chore(angular): remove tslint (#28528)
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. -->

Ionic Framework migrated to using `eslint` for linting all of our
projects awhile ago. However we left around an unused tslint config file
in the Angular project by accident.

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

- Removes the unused tslint configuration file

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2023-11-15 19:08:13 +00:00
Sean Perkins
1b6f15dee1 chore(angular): type checking for standalone directory (#28531)
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. -->

Type checking is disabled in the `standalone/` directory of the angular
project.

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

- Enables type checking in the standalone directories. 

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
2023-11-15 18:58:27 +00:00
161 changed files with 6764 additions and 1771 deletions

2
.github/CODEOWNERS vendored
View File

@@ -11,6 +11,8 @@
# In each subsection folders are ordered first by depth, then alphabetically.
# This should make it easy to add new rules without breaking existing ones.
* @ionic-team/framework
# Frameworks
## Angular

15
.github/labeler.yml vendored
View File

@@ -6,16 +6,17 @@
# https://github.com/actions/labeler
'package: core':
- core/**/*
- changed-files:
- any-glob-to-any-file: ['core/**/*']
'package: angular':
- packages/angular/**/*
- packages/angular-*/**/*
- changed-files:
- any-glob-to-any-file: ['packages/angular/**/*', 'packages/angular-*/**/*']
'package: react':
- packages/react/**/*
- packages/react-*/**/*
- changed-files:
- any-glob-to-any-file: ['packages/react/**/*', 'packages/react-*/**/*']
'package: vue':
- packages/vue/**/*
- packages/vue-*/**/*
- changed-files:
- any-glob-to-any-file: ['packages/vue/**/*', 'packages/vue-*/**/*']

View File

@@ -13,7 +13,7 @@ jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@main
- uses: actions/labeler@v5
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
sync-labels: true

View File

@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [7.5.8](https://github.com/ionic-team/ionic-framework/compare/v7.5.7...v7.5.8) (2023-12-06)
### Bug Fixes
* **angular:** add missing menu controller methods ([#28618](https://github.com/ionic-team/ionic-framework/issues/28618)) ([7871b56](https://github.com/ionic-team/ionic-framework/commit/7871b56eccfe63326b6dd4b56ade3b3afd444fce)), closes [#20053](https://github.com/ionic-team/ionic-framework/issues/20053)
* **overlays:** trigger is configured on load ([#28526](https://github.com/ionic-team/ionic-framework/issues/28526)) ([a3cd204](https://github.com/ionic-team/ionic-framework/commit/a3cd204f616606ccffc35082655e55fdfb19fe28)), closes [#28524](https://github.com/ionic-team/ionic-framework/issues/28524)
* **react:** router creates new view instances of parameterized routes ([#28616](https://github.com/ionic-team/ionic-framework/issues/28616)) ([1705d06](https://github.com/ionic-team/ionic-framework/commit/1705d064cc041e99f432a27207f3aab7fa62c778)), closes [#26524](https://github.com/ionic-team/ionic-framework/issues/26524)
* **vue:** nav component accepts kebab-case component properties ([#28615](https://github.com/ionic-team/ionic-framework/issues/28615)) ([60303aa](https://github.com/ionic-team/ionic-framework/commit/60303aad23f823488afc8f8824e9c72e3ab86acc)), closes [#28611](https://github.com/ionic-team/ionic-framework/issues/28611)
## [7.5.7](https://github.com/ionic-team/ionic-framework/compare/v7.5.6...v7.5.7) (2023-11-29)
### Bug Fixes
* **alert:** date inputs render correctly in mobile safari ([#28495](https://github.com/ionic-team/ionic-framework/issues/28495)) ([b833f0e](https://github.com/ionic-team/ionic-framework/commit/b833f0e826ddd261230e2e29b70e2dc884d8cb04)), closes [#28494](https://github.com/ionic-team/ionic-framework/issues/28494)
* **datetime:** allow disabling datetime with prefer-wheel ([#28511](https://github.com/ionic-team/ionic-framework/issues/28511)) ([01130e1](https://github.com/ionic-team/ionic-framework/commit/01130e12e1d73bbf558da9d4dffd7122822ff39c))
## [7.5.6](https://github.com/ionic-team/ionic-framework/compare/v7.5.5...v7.5.6) (2023-11-21)
### Bug Fixes
* **alert:** match MD spec on tablet ([#28501](https://github.com/ionic-team/ionic-framework/issues/28501)) ([6a2be9f](https://github.com/ionic-team/ionic-framework/commit/6a2be9fa3c12a893d98dc139a1575a6e7e3c7c26)), closes [#23977](https://github.com/ionic-team/ionic-framework/issues/23977)
* **angular:** ng add @ionic/angular in standalone projects ([#28523](https://github.com/ionic-team/ionic-framework/issues/28523)) ([c07312e](https://github.com/ionic-team/ionic-framework/commit/c07312e5ed931f6f825ccf083c9dead9fa815843)), closes [#28514](https://github.com/ionic-team/ionic-framework/issues/28514)
* **angular:** overlays are defined when using standalone controllers ([#28560](https://github.com/ionic-team/ionic-framework/issues/28560)) ([9453132](https://github.com/ionic-team/ionic-framework/commit/9453132aa8952b4adfa1326e61138b329e254f76)), closes [#28385](https://github.com/ionic-team/ionic-framework/issues/28385)
* **datetime:** updating value with min scrolls to new value ([#28549](https://github.com/ionic-team/ionic-framework/issues/28549)) ([388d19e](https://github.com/ionic-team/ionic-framework/commit/388d19e04f83f85abd4602adb04cc71ac575764a)), closes [#28548](https://github.com/ionic-team/ionic-framework/issues/28548)
## [7.5.5](https://github.com/ionic-team/ionic-framework/compare/v7.5.4...v7.5.5) (2023-11-15)

View File

@@ -3,6 +3,41 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [7.5.8](https://github.com/ionic-team/ionic-framework/compare/v7.5.7...v7.5.8) (2023-12-06)
### Bug Fixes
* **overlays:** trigger is configured on load ([#28526](https://github.com/ionic-team/ionic-framework/issues/28526)) ([a3cd204](https://github.com/ionic-team/ionic-framework/commit/a3cd204f616606ccffc35082655e55fdfb19fe28)), closes [#28524](https://github.com/ionic-team/ionic-framework/issues/28524)
## [7.5.7](https://github.com/ionic-team/ionic-framework/compare/v7.5.6...v7.5.7) (2023-11-29)
### Bug Fixes
* **alert:** date inputs render correctly in mobile safari ([#28495](https://github.com/ionic-team/ionic-framework/issues/28495)) ([b833f0e](https://github.com/ionic-team/ionic-framework/commit/b833f0e826ddd261230e2e29b70e2dc884d8cb04)), closes [#28494](https://github.com/ionic-team/ionic-framework/issues/28494)
* **datetime:** allow disabling datetime with prefer-wheel ([#28511](https://github.com/ionic-team/ionic-framework/issues/28511)) ([01130e1](https://github.com/ionic-team/ionic-framework/commit/01130e12e1d73bbf558da9d4dffd7122822ff39c))
## [7.5.6](https://github.com/ionic-team/ionic-framework/compare/v7.5.5...v7.5.6) (2023-11-21)
### Bug Fixes
* **alert:** match MD spec on tablet ([#28501](https://github.com/ionic-team/ionic-framework/issues/28501)) ([6a2be9f](https://github.com/ionic-team/ionic-framework/commit/6a2be9fa3c12a893d98dc139a1575a6e7e3c7c26)), closes [#23977](https://github.com/ionic-team/ionic-framework/issues/23977)
* **datetime:** updating value with min scrolls to new value ([#28549](https://github.com/ionic-team/ionic-framework/issues/28549)) ([388d19e](https://github.com/ionic-team/ionic-framework/commit/388d19e04f83f85abd4602adb04cc71ac575764a)), closes [#28548](https://github.com/ionic-team/ionic-framework/issues/28548)
## [7.5.5](https://github.com/ionic-team/ionic-framework/compare/v7.5.4...v7.5.5) (2023-11-15)

1608
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": "7.5.5",
"version": "7.5.8",
"description": "Base components for Ionic",
"keywords": [
"ionic",
@@ -31,7 +31,7 @@
"loader/"
],
"dependencies": {
"@stencil/core": "^4.7.2",
"@stencil/core": "^4.8.1",
"ionicons": "^7.2.1",
"tslib": "^2.1.0"
},
@@ -64,6 +64,7 @@
"jest": "^29.7.0",
"jest-cli": "^29.7.0",
"prettier": "^2.6.1",
"puppeteer": "21.1.1",
"rollup": "^2.26.4",
"sass": "^1.33.0",
"serve": "^14.0.1",

View File

@@ -0,0 +1,151 @@
/*
* Dark Colors
* -------------------------------------------
*/
:root {
--ion-color-primary: #428cff;
--ion-color-primary-rgb: 66, 140, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3a7be0;
--ion-color-primary-tint: #5598ff;
--ion-color-secondary: #50c8ff;
--ion-color-secondary-rgb: 80, 200, 255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255, 255, 255;
--ion-color-secondary-shade: #46b0e0;
--ion-color-secondary-tint: #62ceff;
--ion-color-tertiary: #6a64ff;
--ion-color-tertiary-rgb: 106, 100, 255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
--ion-color-tertiary-shade: #5d58e0;
--ion-color-tertiary-tint: #7974ff;
--ion-color-success: #2fdf75;
--ion-color-success-rgb: 47, 223, 117;
--ion-color-success-contrast: #000000;
--ion-color-success-contrast-rgb: 0, 0, 0;
--ion-color-success-shade: #29c467;
--ion-color-success-tint: #44e283;
--ion-color-warning: #ffd534;
--ion-color-warning-rgb: 255, 213, 52;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0, 0, 0;
--ion-color-warning-shade: #e0bb2e;
--ion-color-warning-tint: #ffd948;
--ion-color-danger: #ff4961;
--ion-color-danger-rgb: 255, 73, 97;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255, 255, 255;
--ion-color-danger-shade: #e04055;
--ion-color-danger-tint: #ff5b71;
--ion-color-dark: #f4f5f8;
--ion-color-dark-rgb: 244, 245, 248;
--ion-color-dark-contrast: #000000;
--ion-color-dark-contrast-rgb: 0, 0, 0;
--ion-color-dark-shade: #d7d8da;
--ion-color-dark-tint: #f5f6f9;
--ion-color-medium: #989aa2;
--ion-color-medium-rgb: 152, 154, 162;
--ion-color-medium-contrast: #000000;
--ion-color-medium-contrast-rgb: 0, 0, 0;
--ion-color-medium-shade: #86888f;
--ion-color-medium-tint: #a2a4ab;
--ion-color-light: #222428;
--ion-color-light-rgb: 34, 36, 40;
--ion-color-light-contrast: #ffffff;
--ion-color-light-contrast-rgb: 255, 255, 255;
--ion-color-light-shade: #1e2023;
--ion-color-light-tint: #383a3e;
}
/*
* iOS Dark Theme
* -------------------------------------------
*/
.ios body {
--ion-background-color: #000000;
--ion-background-color-rgb: 0, 0, 0;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255, 255, 255;
--ion-color-step-50: #0d0d0d;
--ion-color-step-100: #1a1a1a;
--ion-color-step-150: #262626;
--ion-color-step-200: #333333;
--ion-color-step-250: #404040;
--ion-color-step-300: #4d4d4d;
--ion-color-step-350: #595959;
--ion-color-step-400: #666666;
--ion-color-step-450: #737373;
--ion-color-step-500: #808080;
--ion-color-step-550: #8c8c8c;
--ion-color-step-600: #999999;
--ion-color-step-650: #a6a6a6;
--ion-color-step-700: #b3b3b3;
--ion-color-step-750: #bfbfbf;
--ion-color-step-800: #cccccc;
--ion-color-step-850: #d9d9d9;
--ion-color-step-900: #e6e6e6;
--ion-color-step-950: #f2f2f2;
--ion-toolbar-background: #0d0d0d;
--ion-item-background: #000000;
--ion-card-background: #1c1c1d;
}
/*
* Material Design Dark Theme
* -------------------------------------------
*/
.md body {
--ion-background-color: #121212;
--ion-background-color-rgb: 18, 18, 18;
--ion-text-color: #ffffff;
--ion-text-color-rgb: 255, 255, 255;
--ion-border-color: #222222;
--ion-color-step-50: #1e1e1e;
--ion-color-step-100: #2a2a2a;
--ion-color-step-150: #363636;
--ion-color-step-200: #414141;
--ion-color-step-250: #4d4d4d;
--ion-color-step-300: #595959;
--ion-color-step-350: #656565;
--ion-color-step-400: #717171;
--ion-color-step-450: #7d7d7d;
--ion-color-step-500: #898989;
--ion-color-step-550: #949494;
--ion-color-step-600: #a0a0a0;
--ion-color-step-650: #acacac;
--ion-color-step-700: #b8b8b8;
--ion-color-step-750: #c4c4c4;
--ion-color-step-800: #d0d0d0;
--ion-color-step-850: #dbdbdb;
--ion-color-step-900: #e7e7e7;
--ion-color-step-950: #f3f3f3;
--ion-item-background: #1e1e1e;
--ion-toolbar-background: #1f1f1f;
--ion-tab-bar-background: #1f1f1f;
--ion-card-background: #1e1e1e;
}

View File

@@ -1162,7 +1162,7 @@ export namespace Components {
*/
"autocorrect": 'on' | 'off';
/**
* This Boolean attribute lets you specify that a form control should have input focus when the page loads.
* Sets the [`autofocus` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) on the native input element. This may not be sufficient for the element to be focused on page load. See [managing focus](/docs/developing/managing-focus) for more information.
*/
"autofocus": boolean;
/**
@@ -1274,7 +1274,7 @@ export namespace Components {
*/
"required": boolean;
/**
* Sets focus on the native `input` in `ion-input`. Use this method instead of the global `input.focus()`. Developers who wish to focus an input when a page enters should call `setFocus()` in the `ionViewDidEnter()` lifecycle method. Developers who wish to focus an input when an overlay is presented should call `setFocus` after `didPresent` has resolved.
* Sets focus on the native `input` in `ion-input`. Use this method instead of the global `input.focus()`. Developers who wish to focus an input when a page enters should call `setFocus()` in the `ionViewDidEnter()` lifecycle method. Developers who wish to focus an input when an overlay is presented should call `setFocus` after `didPresent` has resolved. See [managing focus](/docs/developing/managing-focus) for more information.
*/
"setFocus": () => Promise<void>;
/**
@@ -2043,6 +2043,10 @@ export namespace Components {
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
*/
"color"?: Color;
/**
* If `true`, the user cannot interact with the picker.
*/
"disabled": boolean;
/**
* A list of options to be displayed in the picker
*/
@@ -2597,7 +2601,7 @@ export namespace Components {
*/
"searchIcon"?: string;
/**
* Sets focus on the native `input` in `ion-searchbar`. Use this method instead of the global `input.focus()`. Developers who wish to focus an input when a page enters should call `setFocus()` in the `ionViewDidEnter()` lifecycle method. Developers who wish to focus an input when an overlay is presented should call `setFocus` after `didPresent` has resolved.
* Sets focus on the native `input` in `ion-searchbar`. Use this method instead of the global `input.focus()`. Developers who wish to focus an input when a page enters should call `setFocus()` in the `ionViewDidEnter()` lifecycle method. Developers who wish to focus an input when an overlay is presented should call `setFocus` after `didPresent` has resolved. See [managing focus](/docs/developing/managing-focus) for more information.
*/
"setFocus": () => Promise<void>;
/**
@@ -2946,7 +2950,7 @@ export namespace Components {
*/
"autocapitalize": string;
/**
* This Boolean attribute lets you specify that a form control should have input focus when the page loads.
* Sets the [`autofocus` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) on the native input element. This may not be sufficient for the element to be focused on page load. See [managing focus](/docs/developing/managing-focus) for more information.
*/
"autofocus": boolean;
/**
@@ -3046,7 +3050,7 @@ export namespace Components {
*/
"rows"?: number;
/**
* Sets focus on the native `textarea` in `ion-textarea`. Use this method instead of the global `textarea.focus()`.
* Sets focus on the native `textarea` in `ion-textarea`. Use this method instead of the global `textarea.focus()`. See [managing focus](/docs/developing/managing-focus) for more information.
*/
"setFocus": () => Promise<void>;
/**
@@ -5850,7 +5854,7 @@ declare namespace LocalJSX {
*/
"autocorrect"?: 'on' | 'off';
/**
* This Boolean attribute lets you specify that a form control should have input focus when the page loads.
* Sets the [`autofocus` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) on the native input element. This may not be sufficient for the element to be focused on page load. See [managing focus](/docs/developing/managing-focus) for more information.
*/
"autofocus"?: boolean;
/**
@@ -6683,6 +6687,10 @@ declare namespace LocalJSX {
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
*/
"color"?: Color;
/**
* If `true`, the user cannot interact with the picker.
*/
"disabled"?: boolean;
/**
* A list of options to be displayed in the picker
*/
@@ -7691,7 +7699,7 @@ declare namespace LocalJSX {
*/
"autocapitalize"?: string;
/**
* This Boolean attribute lets you specify that a form control should have input focus when the page loads.
* Sets the [`autofocus` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) on the native input element. This may not be sufficient for the element to be focused on page load. See [managing focus](/docs/developing/managing-focus) for more information.
*/
"autofocus"?: boolean;
/**

View File

@@ -1,8 +1,8 @@
import { newSpecPage } from '@stencil/core/testing';
import { AccordionGroup } from '../../accordion-group/accordion-group.tsx';
import { Item } from '../../item/item.tsx';
import { Accordion } from '../accordion.tsx';
import { AccordionGroup } from '../../accordion-group/accordion-group';
import { Item } from '../../item/item';
import { Accordion } from '../accordion';
it('should open correct accordions when accordion group value is set', async () => {
const page = await newSpecPage({
@@ -25,7 +25,7 @@ it('should open correct accordions when accordion group value is set', async ()
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordions = accordionGroup.querySelectorAll('ion-accordion');
accordions.forEach((accordion) => {
@@ -61,7 +61,7 @@ it('should open correct accordions when accordion value is set', async () => {
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordions = accordionGroup.querySelectorAll('ion-accordion');
accordions.forEach((accordion) => {
@@ -97,7 +97,7 @@ it('should open more than one accordion when multiple="true"', async () => {
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordions = accordionGroup.querySelectorAll('ion-accordion');
accordions.forEach((accordion) => {
@@ -133,7 +133,7 @@ it('should render with accordion open', async () => {
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordions = accordionGroup.querySelectorAll('ion-accordion');
expect(accordions[0].classList.contains('accordion-collapsed')).toEqual(false);
@@ -162,7 +162,7 @@ it('should accept a string when multiple="true"', async () => {
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordions = accordionGroup.querySelectorAll('ion-accordion');
expect(accordions[0].classList.contains('accordion-collapsed')).toEqual(false);
@@ -183,8 +183,8 @@ it('should set default values if not provided', async () => {
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordion = accordionGroup.querySelector('ion-accordion');
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordion = accordionGroup.querySelector('ion-accordion')!;
/**
* ID is determined via an auto incrementing counter

View File

@@ -337,6 +337,17 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
if (this.isOpen === true) {
raf(() => this.present());
}
/**
* When binding values in frameworks such as Angular
* it is possible for the value to be set after the Web Component
* initializes but before the value watcher is set up in Stencil.
* As a result, the watcher callback may not be fired.
* We work around this by manually calling the watcher
* callback when the component has loaded and the watcher
* is configured.
*/
this.triggerChanged();
}
render() {

View File

@@ -7,10 +7,17 @@ describe('action sheet: htmlAttributes inheritance', () => {
it('should correctly inherit attributes on host', async () => {
const page = await newSpecPage({
components: [ActionSheet],
template: () => <ion-action-sheet htmlAttributes={{ 'data-testid': 'basic-action-sheet' }}></ion-action-sheet>,
template: () => (
<ion-action-sheet
htmlAttributes={{
'data-testid': 'basic-action-sheet',
}}
overlayIndex={1}
></ion-action-sheet>
),
});
const actionSheet = page.body.querySelector('ion-action-sheet');
const actionSheet = page.body.querySelector('ion-action-sheet')!;
await expect(actionSheet.getAttribute('data-testid')).toBe('basic-action-sheet');
});

View File

@@ -105,6 +105,17 @@
&::-ms-clear {
display: none;
}
&::-webkit-date-and-time-value {
/**
* The -webkit-date-and-time-value pseudo element is used
* to style the date/time input on iOS/Mobile Safari.
* To avoid layout shift between an empty state and a selected state,
* we set the height `18px` to match the native input height for
* date/time inputs on iOS/Mobile Safari.
*/
height: 18px;
}
}

View File

@@ -52,11 +52,20 @@
}
.alert-message {
max-height: $alert-md-content-max-height;
font-size: $alert-md-message-font-size;
}
/**
* MD Alerts on tablets can expand vertically up to
* a total maximum height. We only want to set a max-height
* on mobile phones.
*/
@include mobile-viewport() {
.alert-message {
max-height: $alert-md-content-max-height;
}
}
.alert-message:empty {
@include padding($alert-md-message-empty-padding-top, $alert-md-message-empty-padding-end, $alert-md-message-empty-padding-bottom, $alert-md-message-empty-padding-start);
}
@@ -102,14 +111,24 @@
.alert-checkbox-group {
position: relative;
max-height: $alert-md-content-max-height;
border-top: $alert-md-list-border-top;
border-bottom: $alert-md-list-border-bottom;
overflow: auto;
}
/**
* MD Alerts on tablets can expand vertically up to
* a total maximum height. We only want to set a max-height
* on mobile phones.
*/
@include mobile-viewport() {
.alert-radio-group,
.alert-checkbox-group {
max-height: $alert-md-content-max-height;
}
}
.alert-tappable {
position: relative;
@@ -282,3 +301,14 @@
.alert-button-inner {
justify-content: $alert-md-button-group-justify-content;
}
/**
* MD alerts should scale up to 560px x 560px
* on tablet dimensions.
*/
@include tablet-viewport() {
:host {
--max-width: #{$alert-md-max-width-tablet};
--max-height: #{$alert-md-max-height-tablet};
}
}

View File

@@ -10,6 +10,20 @@ $alert-md-font-size: dynamic-font(14px) !default;
/// @prop - Max width of the alert
$alert-md-max-width: 280px !default;
/// @prop - Max width of the alert on a tablet
/**
* Large display requirements for MD Alert:
* 1. Maintain a minimum of 48px distance from the leading and
* trailing edges of the screen. (48px * 2 = 96px)
* 2. The width can increase up to 560px.
* 3. The height can increase up to 560px.
* Source: https://m2.material.io/components/dialogs#behavior
*/
$alert-md-max-width-tablet: min(calc(100vw - 96px), 560px) !default;
/// @prop - Max width of the alert on a tablet
$alert-md-max-height-tablet: min(calc(100vh - 96px), 560px) !default;
/// @prop - Border radius of the alert
$alert-md-border-radius: 4px !default;

View File

@@ -84,7 +84,15 @@
font-weight: normal;
}
.alert-message {
/**
* Alert has a maximum height in scenarios
* such as the MD alert on tablet devices.
* As a result, we need to make sure the inner
* containers can scroll otherwise content
* may be cut off.
*/
.alert-message,
.alert-input-group {
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
overflow-y: auto;

View File

@@ -376,6 +376,17 @@ export class Alert implements ComponentInterface, OverlayInterface {
if (this.isOpen === true) {
raf(() => this.present());
}
/**
* When binding values in frameworks such as Angular
* it is possible for the value to be set after the Web Component
* initializes but before the value watcher is set up in Stencil.
* As a result, the watcher callback may not be fired.
* We work around this by manually calling the watcher
* callback when the component has loaded and the watcher
* is configured.
*/
this.triggerChanged();
}
/**

View File

@@ -1,6 +1,7 @@
import { newSpecPage } from '@stencil/core/testing';
import { Alert } from '../alert';
import { config } from '../../../global/config';
import { Alert } from '../alert';
describe('alert: custom html', () => {
it('should not allow for custom html by default', async () => {
@@ -9,7 +10,7 @@ describe('alert: custom html', () => {
html: `<ion-alert message="<button class='custom-html'>Custom Text</button>"></ion-alert>`,
});
const content = page.body.querySelector('.alert-message');
const content = page.body.querySelector('.alert-message')!;
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});
@@ -21,7 +22,7 @@ describe('alert: custom html', () => {
html: `<ion-alert message="<button class='custom-html'>Custom Text</button>"></ion-alert>`,
});
const content = page.body.querySelector('.alert-message');
const content = page.body.querySelector('.alert-message')!;
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).not.toBe(null);
});
@@ -33,7 +34,7 @@ describe('alert: custom html', () => {
html: `<ion-alert message="<button class='custom-html'>Custom Text</button>"></ion-alert>`,
});
const content = page.body.querySelector('.alert-message');
const content = page.body.querySelector('.alert-message')!;
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});

View File

@@ -0,0 +1,47 @@
import { expect } from '@playwright/test';
import { configs, test, Viewports } from '@utils/test/playwright';
/**
* This behavior does not vary across directions.
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('alert: rendering - tablet'), () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(Viewports.tablet.portrait);
await page.goto('/src/components/alert/test/basic', config);
});
test('should expand width and height on larger displays with text', async ({ page }) => {
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
const button = page.locator('#longMessage');
const alert = page.locator('ion-alert');
await button.click();
await ionAlertDidPresent.next();
await expect(alert).toHaveScreenshot(screenshot('alert-tablet-text'));
});
test('should expand width and height on larger displays with checkboxes', async ({ page }) => {
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
const button = page.locator('#checkbox');
const alert = page.locator('ion-alert');
await button.click();
await ionAlertDidPresent.next();
await expect(alert).toHaveScreenshot(screenshot('alert-tablet-checkboxes'));
});
test('should expand width and height on larger displays with radios', async ({ page }) => {
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
const button = page.locator('#radio');
const alert = page.locator('ion-alert');
await button.click();
await ionAlertDidPresent.next();
await expect(alert).toHaveScreenshot(screenshot('alert-tablet-radios'));
});
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,7 +1,7 @@
import { newSpecPage } from '@stencil/core/testing';
import { Breadcrumb } from '../../breadcrumb/breadcrumb.tsx';
import { Breadcrumbs } from '../breadcrumbs.tsx';
import { Breadcrumb } from '../../breadcrumb/breadcrumb';
import { Breadcrumbs } from '../breadcrumbs';
it('should correctly provide the collapsed breadcrumbs in the event payload', async () => {
const page = await newSpecPage({
@@ -18,8 +18,8 @@ it('should correctly provide the collapsed breadcrumbs in the event payload', as
});
const onCollapsedClick = jest.fn((ev) => ev);
const breadcrumbs = page.body.querySelector('ion-breadcrumbs');
const breadcrumb = page.body.querySelectorAll('ion-breadcrumb');
const breadcrumbs = page.body.querySelector('ion-breadcrumbs')!;
const breadcrumb = page.body.querySelectorAll('ion-breadcrumb')!;
breadcrumbs.addEventListener('ionCollapsedClick', onCollapsedClick);
@@ -46,8 +46,8 @@ it('should exclude the separator from narrators', async () => {
`,
});
const firstBreadcrumb = page.body.querySelector('ion-breadcrumb:first-of-type');
const separator = firstBreadcrumb.shadowRoot.querySelector('[part="separator"]');
const firstBreadcrumb = page.body.querySelector('ion-breadcrumb:first-of-type')!;
const separator = firstBreadcrumb.shadowRoot!.querySelector('[part="separator"]')!;
expect(separator.getAttribute('aria-hidden')).toBe('true');
});
@@ -62,7 +62,7 @@ it('should have color attribute', async () => {
`,
});
const breadcrumbs = page.body.querySelector('ion-breadcrumbs');
const breadcrumbs = page.body.querySelector('ion-breadcrumbs')!;
expect(breadcrumbs.hasAttribute('color')).toBe(true);
});

View File

@@ -1,4 +1,5 @@
import { newSpecPage } from '@stencil/core/testing';
import { Button } from '../../button';
describe('Button: Hidden Form Button', () => {
@@ -15,8 +16,7 @@ describe('Button: Hidden Form Button', () => {
return page.body.querySelectorAll('form button');
};
const form = page.body.querySelectorAll('form');
const button = page.body.querySelector('ion-button');
const button = page.body.querySelector('ion-button')!;
await page.waitForChanges();

View File

@@ -11,7 +11,7 @@ describe('ion-checkbox: disabled', () => {
`,
});
const checkbox = page.body.querySelector('ion-checkbox');
const checkbox = page.body.querySelector('ion-checkbox')!;
expect(checkbox.checked).toBe(false);

View File

@@ -1132,7 +1132,7 @@ export class Datetime implements ComponentInterface {
* so we need to re-init behavior with the new elements.
*/
componentDidRender() {
const { presentation, prevPresentation, calendarBodyRef, minParts, preferWheel } = this;
const { presentation, prevPresentation, calendarBodyRef, minParts, preferWheel, forceRenderDate } = this;
/**
* TODO(FW-2165)
@@ -1150,7 +1150,20 @@ export class Datetime implements ComponentInterface {
const hasCalendarGrid = !preferWheel && ['date-time', 'time-date', 'date'].includes(presentation);
if (minParts !== undefined && hasCalendarGrid && calendarBodyRef) {
const workingMonth = calendarBodyRef.querySelector('.calendar-month:nth-of-type(1)');
if (workingMonth) {
/**
* We need to make sure the datetime is not in the process
* of scrolling to a new datetime value if the value
* is updated programmatically.
* Otherwise, the datetime will appear to not scroll at all because
* we are resetting the scroll position to the center of the view.
* Prior to the datetime's value being updated programmatically,
* the calendarBodyRef is scrolled such that the middle month is centered
* in the view. The below code updates the scroll position so the middle
* month is also centered in the view. Since the scroll position did not change,
* the scroll callback in this file does not fire,
* and the resolveForceDateScrolling promise never resolves.
*/
if (workingMonth && forceRenderDate === undefined) {
calendarBodyRef.scrollLeft = workingMonth.clientWidth * (isRTL(this.el) ? -1 : 1);
}
}
@@ -1525,7 +1538,7 @@ export class Datetime implements ComponentInterface {
}
private renderCombinedDatePickerColumn() {
const { defaultParts, workingParts, locale, minParts, maxParts, todayParts, isDateEnabled } = this;
const { defaultParts, disabled, workingParts, locale, minParts, maxParts, todayParts, isDateEnabled } = this;
const activePart = this.getActivePartsWithFallback();
@@ -1604,6 +1617,7 @@ export class Datetime implements ComponentInterface {
<ion-picker-column-internal
class="date-column"
color={this.color}
disabled={disabled}
items={items}
value={todayString}
onIonChange={(ev: CustomEvent) => {
@@ -1715,7 +1729,7 @@ export class Datetime implements ComponentInterface {
return [];
}
const { workingParts } = this;
const { disabled, workingParts } = this;
const activePart = this.getActivePartsWithFallback();
@@ -1723,6 +1737,7 @@ export class Datetime implements ComponentInterface {
<ion-picker-column-internal
class="day-column"
color={this.color}
disabled={disabled}
items={days}
value={(workingParts.day !== null ? workingParts.day : this.defaultParts.day) ?? undefined}
onIonChange={(ev: CustomEvent) => {
@@ -1759,7 +1774,7 @@ export class Datetime implements ComponentInterface {
return [];
}
const { workingParts } = this;
const { disabled, workingParts } = this;
const activePart = this.getActivePartsWithFallback();
@@ -1767,6 +1782,7 @@ export class Datetime implements ComponentInterface {
<ion-picker-column-internal
class="month-column"
color={this.color}
disabled={disabled}
items={months}
value={workingParts.month}
onIonChange={(ev: CustomEvent) => {
@@ -1802,7 +1818,7 @@ export class Datetime implements ComponentInterface {
return [];
}
const { workingParts } = this;
const { disabled, workingParts } = this;
const activePart = this.getActivePartsWithFallback();
@@ -1810,6 +1826,7 @@ export class Datetime implements ComponentInterface {
<ion-picker-column-internal
class="year-column"
color={this.color}
disabled={disabled}
items={years}
value={workingParts.year}
onIonChange={(ev: CustomEvent) => {
@@ -1875,7 +1892,7 @@ export class Datetime implements ComponentInterface {
}
private renderHourPickerColumn(hoursData: PickerColumnItem[]) {
const { workingParts } = this;
const { disabled, workingParts } = this;
if (hoursData.length === 0) return [];
const activePart = this.getActivePartsWithFallback();
@@ -1883,6 +1900,7 @@ export class Datetime implements ComponentInterface {
return (
<ion-picker-column-internal
color={this.color}
disabled={disabled}
value={activePart.hour}
items={hoursData}
numericInput
@@ -1903,7 +1921,7 @@ export class Datetime implements ComponentInterface {
);
}
private renderMinutePickerColumn(minutesData: PickerColumnItem[]) {
const { workingParts } = this;
const { disabled, workingParts } = this;
if (minutesData.length === 0) return [];
const activePart = this.getActivePartsWithFallback();
@@ -1911,6 +1929,7 @@ export class Datetime implements ComponentInterface {
return (
<ion-picker-column-internal
color={this.color}
disabled={disabled}
value={activePart.minute}
items={minutesData}
numericInput
@@ -1931,7 +1950,7 @@ export class Datetime implements ComponentInterface {
);
}
private renderDayPeriodPickerColumn(dayPeriodData: PickerColumnItem[]) {
const { workingParts } = this;
const { disabled, workingParts } = this;
if (dayPeriodData.length === 0) {
return [];
}
@@ -1943,6 +1962,7 @@ export class Datetime implements ComponentInterface {
<ion-picker-column-internal
style={isDayPeriodRTL ? { order: '-1' } : {}}
color={this.color}
disabled={disabled}
value={activePart.ampm}
items={dayPeriodData}
onIonChange={(ev: CustomEvent) => {

View File

@@ -1,21 +1,22 @@
import type { DatetimeParts } from '../datetime-interface';
import { isSameDay, isBefore, isAfter } from '../utils/comparison';
describe('isSameDay()', () => {
it('should return correct results for month, day, and year', () => {
const reference = { month: 1, day: 1, year: 2021 };
const reference: DatetimeParts = { month: 1, day: 1, year: 2021 };
expect(isSameDay(reference, { month: 1, day: 1, year: 2021 })).toEqual(true);
expect(isSameDay(reference, { month: 2, day: 1, year: 2021 })).toEqual(false);
expect(isSameDay(reference, { month: 1, day: 2, year: 2021 })).toEqual(false);
expect(isSameDay(reference, { month: 1, day: 1, year: 2022 })).toEqual(false);
expect(isSameDay(reference, { month: 0, day: 0, year: 0 })).toEqual(false);
expect(isSameDay(reference, { month: null, day: null, year: null })).toEqual(false);
expect(isSameDay(reference, { month: null, day: null, year: null } as any)).toEqual(false);
});
});
describe('isBefore()', () => {
it('should return correct results for month, day, and year', () => {
const reference = { month: 1, day: 1, year: 2021 };
const reference: DatetimeParts = { month: 1, day: 1, year: 2021 };
expect(isBefore(reference, { month: 1, day: 1, year: 2021 })).toEqual(false);
expect(isBefore(reference, { month: 2, day: 1, year: 2021 })).toEqual(true);
@@ -23,13 +24,13 @@ describe('isBefore()', () => {
expect(isBefore(reference, { month: 1, day: 1, year: 2022 })).toEqual(true);
expect(isBefore(reference, { month: 1, day: 1, year: 2020 })).toEqual(false);
expect(isBefore(reference, { month: 0, day: 0, year: 0 })).toEqual(false);
expect(isBefore(reference, { month: null, day: null, year: null })).toEqual(false);
expect(isBefore(reference, { month: null, day: null, year: null } as any)).toEqual(false);
});
});
describe('isAfter()', () => {
it('should return correct results for month, day, and year', () => {
const reference = { month: 2, day: 2, year: 2021 };
const reference: DatetimeParts = { month: 2, day: 2, year: 2021 };
expect(isAfter(reference, { month: 2, day: 2, year: 2021 })).toEqual(false);
expect(isAfter(reference, { month: 2, day: 1, year: 2021 })).toEqual(true);
@@ -42,6 +43,6 @@ describe('isAfter()', () => {
* 2021 > undefined === false
* 2021 > null === true
*/
expect(isAfter(reference, { month: null, day: null, year: null })).toEqual(true);
expect(isAfter(reference, { month: null, day: null, year: null } as any)).toEqual(true);
});
});

View File

@@ -1,3 +1,4 @@
import type { DatetimeParts } from '../datetime-interface';
import {
generateMonths,
getDaysOfWeek,
@@ -364,7 +365,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 19,
minute: 50,
};
} as unknown as DatetimeParts;
const minParts = {
day: undefined,
@@ -372,7 +373,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 19,
minute: 50,
};
} as unknown as DatetimeParts;
const { hours } = generateTime('en-US', refValue, 'h23', minParts);
@@ -387,7 +388,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 20,
minute: 22,
};
} as unknown as DatetimeParts;
const minParts = {
day: undefined,
@@ -395,7 +396,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 19,
minute: 30,
};
} as unknown as DatetimeParts;
const { hours, minutes } = generateTime('en-US', refValue, 'h23', minParts);
@@ -411,7 +412,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 20,
minute: 30,
};
} as unknown as DatetimeParts;
const minParts = {
day: undefined,
@@ -419,7 +420,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 19,
minute: 30,
};
} as unknown as DatetimeParts;
const maxParts = {
day: undefined,
@@ -427,7 +428,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 20,
minute: 40,
};
} as unknown as DatetimeParts;
const { hours } = generateTime('en-US', refValue, 'h23', minParts, maxParts);
@@ -441,7 +442,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 13,
minute: 0,
};
} as unknown as DatetimeParts;
const maxParts = {
day: undefined,
@@ -449,7 +450,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 13,
minute: 2,
};
} as unknown as DatetimeParts;
const { minutes } = generateTime('en-US', refValue, 'h23', undefined, maxParts);
@@ -463,7 +464,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 12,
minute: 0,
};
} as unknown as DatetimeParts;
const maxParts = {
day: undefined,
@@ -471,7 +472,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 13,
minute: 2,
};
} as unknown as DatetimeParts;
const { minutes } = generateTime('en-US', refValue, 'h23', undefined, maxParts);
@@ -482,7 +483,7 @@ describe('generateTime()', () => {
describe('getToday', () => {
beforeAll(() => {
jest.useFakeTimers('modern');
jest.useFakeTimers();
// System time is zero based, 1 = February
jest.setSystemTime(new Date(2022, 1, 21, 18, 30));
});

View File

@@ -0,0 +1,39 @@
import { h } from '@stencil/core';
import { newSpecPage } from '@stencil/core/testing';
import { Datetime } from '../../../datetime/datetime';
import { PickerColumnInternal } from '../../../picker-column-internal/picker-column-internal';
import { PickerInternal } from '../../../picker-internal/picker-internal';
describe('ion-datetime disabled', () => {
beforeEach(() => {
// IntersectionObserver isn't available in test environment
const mockIntersectionObserver = jest.fn();
mockIntersectionObserver.mockReturnValue({
observe: () => null,
unobserve: () => null,
disconnect: () => null,
});
global.IntersectionObserver = mockIntersectionObserver;
});
it('picker should be disabled in prefer wheel mode', async () => {
const page = await newSpecPage({
components: [Datetime, PickerColumnInternal, PickerInternal],
template: () => (
<ion-datetime id="inline-datetime-wheel" disabled prefer-wheel value="2022-04-21T00:00:00"></ion-datetime>
),
});
await page.waitForChanges();
const datetime = page.body.querySelector('ion-datetime')!;
const columns = datetime.shadowRoot!.querySelectorAll('ion-picker-column-internal');
await expect(columns.length).toEqual(4);
columns.forEach((column) => {
expect(column.disabled).toBe(true);
});
});
});

View File

@@ -66,6 +66,11 @@
<h2>Inline - No Default Value</h2>
<ion-datetime id="inline-datetime-no-value" disabled></ion-datetime>
</div>
<div class="grid-item">
<h2>Inline - Prefer Wheel</h2>
<ion-datetime id="inline-datetime-wheel" disabled prefer-wheel value="2022-04-21T00:00:00"></ion-datetime>
</div>
</div>
</ion-content>
<script>

View File

@@ -1,3 +1,4 @@
import type { DatetimeParts } from '../datetime-interface';
import {
generateDayAriaLabel,
getMonthAndDay,
@@ -109,7 +110,7 @@ describe('getLocalizedDayPeriod', () => {
describe('getLocalizedTime', () => {
it('should localize the time to PM', () => {
const datetimeParts = {
const datetimeParts: DatetimeParts = {
day: 1,
month: 1,
year: 2022,
@@ -121,7 +122,7 @@ describe('getLocalizedTime', () => {
});
it('should localize the time to AM', () => {
const datetimeParts = {
const datetimeParts: DatetimeParts = {
day: 1,
month: 1,
year: 2022,
@@ -133,7 +134,7 @@ describe('getLocalizedTime', () => {
});
it('should avoid Chromium bug when using 12 hour time in a 24 hour locale', () => {
const datetimeParts = {
const datetimeParts: DatetimeParts = {
day: 1,
month: 1,
year: 2022,
@@ -144,12 +145,12 @@ describe('getLocalizedTime', () => {
expect(getLocalizedTime('en-GB', datetimeParts, 'h12')).toEqual('12:00 am');
});
it('should parse time-only values correctly', () => {
const datetimeParts = {
const datetimeParts: Partial<DatetimeParts> = {
hour: 22,
minute: 40,
};
expect(getLocalizedTime('en-US', datetimeParts, 'h12')).toEqual('10:40 PM');
expect(getLocalizedTime('en-US', datetimeParts, 'h23')).toEqual('22:40');
expect(getLocalizedTime('en-US', datetimeParts as DatetimeParts, 'h12')).toEqual('10:40 PM');
expect(getLocalizedTime('en-US', datetimeParts as DatetimeParts, 'h23')).toEqual('22:40');
});
});

View File

@@ -1,3 +1,4 @@
import type { DatetimeParts } from '../datetime-interface';
import {
getPreviousYear,
getNextYear,
@@ -103,31 +104,31 @@ describe('getInternalHourValue()', () => {
describe('calculateHourFromAMPM()', () => {
it('should correctly convert from AM to PM', () => {
expect(calculateHourFromAMPM({ hour: 12, ampm: 'am' }, 'pm')).toEqual(12);
expect(calculateHourFromAMPM({ hour: 1, ampm: 'am' }, 'pm')).toEqual(13);
expect(calculateHourFromAMPM({ hour: 2, ampm: 'am' }, 'pm')).toEqual(14);
expect(calculateHourFromAMPM({ hour: 3, ampm: 'am' }, 'pm')).toEqual(15);
expect(calculateHourFromAMPM({ hour: 4, ampm: 'am' }, 'pm')).toEqual(16);
expect(calculateHourFromAMPM({ hour: 5, ampm: 'am' }, 'pm')).toEqual(17);
expect(calculateHourFromAMPM({ hour: 6, ampm: 'am' }, 'pm')).toEqual(18);
expect(calculateHourFromAMPM({ hour: 7, ampm: 'am' }, 'pm')).toEqual(19);
expect(calculateHourFromAMPM({ hour: 8, ampm: 'am' }, 'pm')).toEqual(20);
expect(calculateHourFromAMPM({ hour: 9, ampm: 'am' }, 'pm')).toEqual(21);
expect(calculateHourFromAMPM({ hour: 10, ampm: 'am' }, 'pm')).toEqual(22);
expect(calculateHourFromAMPM({ hour: 11, ampm: 'am' }, 'pm')).toEqual(23);
expect(calculateHourFromAMPM({ hour: 12, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(12);
expect(calculateHourFromAMPM({ hour: 1, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(13);
expect(calculateHourFromAMPM({ hour: 2, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(14);
expect(calculateHourFromAMPM({ hour: 3, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(15);
expect(calculateHourFromAMPM({ hour: 4, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(16);
expect(calculateHourFromAMPM({ hour: 5, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(17);
expect(calculateHourFromAMPM({ hour: 6, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(18);
expect(calculateHourFromAMPM({ hour: 7, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(19);
expect(calculateHourFromAMPM({ hour: 8, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(20);
expect(calculateHourFromAMPM({ hour: 9, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(21);
expect(calculateHourFromAMPM({ hour: 10, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(22);
expect(calculateHourFromAMPM({ hour: 11, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(23);
expect(calculateHourFromAMPM({ hour: 13, ampm: 'pm' }, 'am')).toEqual(1);
expect(calculateHourFromAMPM({ hour: 14, ampm: 'pm' }, 'am')).toEqual(2);
expect(calculateHourFromAMPM({ hour: 15, ampm: 'pm' }, 'am')).toEqual(3);
expect(calculateHourFromAMPM({ hour: 16, ampm: 'pm' }, 'am')).toEqual(4);
expect(calculateHourFromAMPM({ hour: 17, ampm: 'pm' }, 'am')).toEqual(5);
expect(calculateHourFromAMPM({ hour: 18, ampm: 'pm' }, 'am')).toEqual(6);
expect(calculateHourFromAMPM({ hour: 19, ampm: 'pm' }, 'am')).toEqual(7);
expect(calculateHourFromAMPM({ hour: 20, ampm: 'pm' }, 'am')).toEqual(8);
expect(calculateHourFromAMPM({ hour: 21, ampm: 'pm' }, 'am')).toEqual(9);
expect(calculateHourFromAMPM({ hour: 22, ampm: 'pm' }, 'am')).toEqual(10);
expect(calculateHourFromAMPM({ hour: 23, ampm: 'pm' }, 'am')).toEqual(11);
expect(calculateHourFromAMPM({ hour: 0, ampm: 'pm' }, 'am')).toEqual(12);
expect(calculateHourFromAMPM({ hour: 13, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(1);
expect(calculateHourFromAMPM({ hour: 14, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(2);
expect(calculateHourFromAMPM({ hour: 15, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(3);
expect(calculateHourFromAMPM({ hour: 16, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(4);
expect(calculateHourFromAMPM({ hour: 17, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(5);
expect(calculateHourFromAMPM({ hour: 18, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(6);
expect(calculateHourFromAMPM({ hour: 19, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(7);
expect(calculateHourFromAMPM({ hour: 20, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(8);
expect(calculateHourFromAMPM({ hour: 21, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(9);
expect(calculateHourFromAMPM({ hour: 22, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(10);
expect(calculateHourFromAMPM({ hour: 23, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(11);
expect(calculateHourFromAMPM({ hour: 0, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(12);
});
});

View File

@@ -42,7 +42,8 @@ describe('parseDate()', () => {
* See https://github.com/ionic-team/ionic-framework/commit/3fb4caf21ffac12f765c4c80bf1850e05d211c6a
*/
it('should return the correct time zone offset', () => {
expect(parseDate('2022-12-15T13:47:30-02:00').tzOffset).toEqual(undefined);
// Casting as any since `tzOffset` does not exist on DatetimeParts
expect((parseDate('2022-12-15T13:47:30-02:00') as any)?.tzOffset).toEqual(undefined);
});
it('should parse an array of dates', () => {
@@ -162,8 +163,8 @@ describe('parseMinParts()', () => {
minute: 4,
hour: 2,
};
expect(parseMinParts(undefined, today)).toEqual(undefined);
expect(parseMinParts(null, today)).toEqual(undefined);
expect(parseMinParts(undefined as any, today)).toEqual(undefined);
expect(parseMinParts(null as any, today)).toEqual(undefined);
expect(parseMinParts('foo', today)).toEqual(undefined);
});
});
@@ -225,8 +226,8 @@ describe('parseMaxParts()', () => {
minute: 4,
hour: 2,
};
expect(parseMaxParts(undefined, today)).toEqual(undefined);
expect(parseMaxParts(null, today)).toEqual(undefined);
expect(parseMaxParts(undefined as any, today)).toEqual(undefined);
expect(parseMaxParts(null as any, today)).toEqual(undefined);
expect(parseMaxParts('foo', today)).toEqual(undefined);
});
});

View File

@@ -84,13 +84,13 @@ describe('isPrevMonthDisabled()', () => {
// Date month and year is the same as min month and year
expect(isPrevMonthDisabled({ month: 1, year: 2021, day: null }, { month: 1, year: 2021, day: null })).toEqual(true);
// Date year is the same as min year (month not provided)
expect(isPrevMonthDisabled({ month: 1, year: 2021, day: null }, { year: 2021, month: null, day: null })).toEqual(
true
);
expect(
isPrevMonthDisabled({ month: 1, year: 2021, day: null }, { year: 2021, month: null, day: null } as any)
).toEqual(true);
// Date year is less than the min year (month not provided)
expect(isPrevMonthDisabled({ month: 5, year: 2021, day: null }, { year: 2022, month: null, day: null })).toEqual(
true
);
expect(
isPrevMonthDisabled({ month: 5, year: 2021, day: null }, { year: 2022, month: null, day: null } as any)
).toEqual(true);
// Date is above the maximum bounds and the previous month does not does not fall within the
// min-max range.
@@ -118,12 +118,12 @@ describe('isPrevMonthDisabled()', () => {
expect(isPrevMonthDisabled({ month: 12, year: 2021, day: null })).toEqual(false);
// Date year is the same as min year,
// but can navigate to a previous month without reducing the year.
expect(isPrevMonthDisabled({ month: 12, year: 2021, day: null }, { year: 2021, month: null, day: null })).toEqual(
false
);
expect(isPrevMonthDisabled({ month: 2, year: 2021, day: null }, { year: 2021, month: null, day: null })).toEqual(
false
);
expect(
isPrevMonthDisabled({ month: 12, year: 2021, day: null }, { year: 2021, month: null, day: null } as any)
).toEqual(false);
expect(
isPrevMonthDisabled({ month: 2, year: 2021, day: null }, { year: 2021, month: null, day: null } as any)
).toEqual(false);
});
});

View File

@@ -1,6 +1,7 @@
import { newSpecPage } from '@stencil/core/testing';
import { InfiniteScrollContent } from '../infinite-scroll-content';
import { config } from '../../../global/config';
import { InfiniteScrollContent } from '../infinite-scroll-content';
describe('infinite-scroll-content: custom html', () => {
it('should not allow for custom html by default', async () => {
@@ -9,7 +10,7 @@ describe('infinite-scroll-content: custom html', () => {
html: `<ion-infinite-scroll-content loading-text="<button class='custom-html'>Custom Text</button>"></ion-infinite-scroll-content>`,
});
const content = page.body.querySelector('.infinite-loading-text');
const content = page.body.querySelector('.infinite-loading-text')!;
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});
@@ -21,7 +22,7 @@ describe('infinite-scroll-content: custom html', () => {
html: `<ion-infinite-scroll-content loading-text="<button class='custom-html'>Custom Text</button>"></ion-infinite-scroll-content>`,
});
const content = page.body.querySelector('.infinite-loading-text');
const content = page.body.querySelector('.infinite-loading-text')!;
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).not.toBe(null);
});
@@ -33,7 +34,7 @@ describe('infinite-scroll-content: custom html', () => {
html: `<ion-infinite-scroll-content loading-text="<button class='custom-html'>Custom Text2</button>"></ion-infinite-scroll-content>`,
});
const content = page.body.querySelector('.infinite-loading-text');
const content = page.body.querySelector('.infinite-loading-text')!;
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});

View File

@@ -95,7 +95,9 @@ export class Input implements ComponentInterface {
@Prop() autocorrect: 'on' | 'off' = 'off';
/**
* This Boolean attribute lets you specify that a form control should have input focus when the page loads.
* Sets the [`autofocus` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) on the native input element.
*
* This may not be sufficient for the element to be focused on page load. See [managing focus](/docs/developing/managing-focus) for more information.
*/
@Prop() autofocus = false;
@@ -424,6 +426,8 @@ export class Input implements ComponentInterface {
*
* Developers who wish to focus an input when an overlay is presented
* should call `setFocus` after `didPresent` has resolved.
*
* See [managing focus](/docs/developing/managing-focus) for more information.
*/
@Method()
async setFocus() {

View File

@@ -1,4 +1,5 @@
import { newSpecPage } from '@stencil/core/testing';
import { Input } from '../input';
describe('input: rendering', () => {
@@ -8,7 +9,7 @@ describe('input: rendering', () => {
html: '<ion-input title="my title" tabindex="-1" data-form-type="password"></ion-input>',
});
const nativeEl = page.body.querySelector('ion-input input');
const nativeEl = page.body.querySelector('ion-input input')!;
expect(nativeEl.getAttribute('title')).toBe('my title');
expect(nativeEl.getAttribute('tabindex')).toBe('-1');
expect(nativeEl.getAttribute('data-form-type')).toBe('password');
@@ -63,9 +64,9 @@ describe('input: label rendering', () => {
`,
});
const input = page.body.querySelector('ion-input');
const input = page.body.querySelector('ion-input')!;
const labelText = input.querySelector('.label-text-wrapper');
const labelText = input.querySelector('.label-text-wrapper')!;
expect(labelText.textContent).toBe('Label Prop Text');
});
@@ -77,9 +78,9 @@ describe('input: label rendering', () => {
`,
});
const input = page.body.querySelector('ion-input');
const input = page.body.querySelector('ion-input')!;
const labelText = input.querySelector('.label-text-wrapper');
const labelText = input.querySelector('.label-text-wrapper')!;
expect(labelText.textContent).toBe('Label Slot Text');
});
@@ -91,9 +92,9 @@ describe('input: label rendering', () => {
`,
});
const input = page.body.querySelector('ion-input');
const input = page.body.querySelector('ion-input')!;
const labelText = input.querySelector('.label-text-wrapper');
const labelText = input.querySelector('.label-text-wrapper')!;
expect(labelText.textContent).toBe('Label Prop Text');
});

View File

@@ -1,6 +1,7 @@
import { newSpecPage } from '@stencil/core/testing';
import { Input } from '../../input';
import { Item } from '../../../item/item';
import { Input } from '../../input';
it('should render as modern when label is set asynchronously', async () => {
const page = await newSpecPage({
@@ -12,7 +13,7 @@ it('should render as modern when label is set asynchronously', async () => {
`,
});
const input = page.body.querySelector('ion-input');
const input = page.body.querySelector('ion-input')!;
// Template should be modern
expect(input.classList.contains('legacy-input')).toBe(false);

View File

@@ -1,9 +1,10 @@
import { Radio } from '../../../radio/radio.tsx';
import { RadioGroup } from '../../../radio-group/radio-group.tsx';
import { Item } from '../../item.tsx';
import { List } from '../../../list/list.tsx';
import { newSpecPage } from '@stencil/core/testing';
import { List } from '../../../list/list';
import { RadioGroup } from '../../../radio-group/radio-group';
import { Radio } from '../../../radio/radio';
import { Item } from '../../item';
describe('ion-item', () => {
it('should not have a role when used without list', async () => {
const page = await newSpecPage({
@@ -11,7 +12,7 @@ describe('ion-item', () => {
html: `<ion-item>Hello World</ion-item>`,
});
const item = page.body.querySelector('ion-item');
const item = page.body.querySelector('ion-item')!;
expect(item.getAttribute('role')).toBe(null);
});
@@ -27,7 +28,7 @@ describe('ion-item', () => {
`,
});
const item = page.body.querySelector('ion-item');
const item = page.body.querySelector('ion-item')!;
expect(item.getAttribute('role')).toBe('listitem');
});
@@ -45,7 +46,7 @@ describe('ion-item', () => {
`,
});
const item = page.body.querySelector('ion-item');
const item = page.body.querySelector('ion-item')!;
expect(item.getAttribute('role')).toBe(null);
});
});

View File

@@ -225,6 +225,17 @@ export class Loading implements ComponentInterface, OverlayInterface {
if (this.isOpen === true) {
raf(() => this.present());
}
/**
* When binding values in frameworks such as Angular
* it is possible for the value to be set after the Web Component
* initializes but before the value watcher is set up in Stencil.
* As a result, the watcher callback may not be fired.
* We work around this by manually calling the watcher
* callback when the component has loaded and the watcher
* is configured.
*/
this.triggerChanged();
}
disconnectedCallback() {

View File

@@ -7,10 +7,10 @@ describe('loading: htmlAttributes inheritance', () => {
it('should correctly inherit attributes on host', async () => {
const page = await newSpecPage({
components: [Loading],
template: () => <ion-loading htmlAttributes={{ 'data-testid': 'basic-loading' }}></ion-loading>,
template: () => <ion-loading overlayIndex={1} htmlAttributes={{ 'data-testid': 'basic-loading' }}></ion-loading>,
});
const loading = page.body.querySelector('ion-loading');
const loading = page.body.querySelector('ion-loading')!;
await expect(loading.getAttribute('data-testid')).toBe('basic-loading');
});

View File

@@ -27,7 +27,7 @@
// iOS Card Modal
// --------------------------------------------------
@media screen and (max-width: 767px) {
@include mobile-viewport() {
@supports (width: max(0px, 1px)) {
:host(.modal-card) {
--height: calc(100% - max(30px, var(--ion-safe-area-top)) - 10px);
@@ -60,7 +60,7 @@
}
}
@media screen and (min-width: 768px) {
@include tablet-viewport() {
:host(.modal-card) {
--width: calc(100% - 120px);
--height: calc(100% - (120px + var(--ion-safe-area-top) + var(--ion-safe-area-bottom)));

View File

@@ -368,6 +368,17 @@ export class Modal implements ComponentInterface, OverlayInterface {
raf(() => this.present());
}
this.breakpointsChanged(this.breakpoints);
/**
* When binding values in frameworks such as Angular
* it is possible for the value to be set after the Web Component
* initializes but before the value watcher is set up in Stencil.
* As a result, the watcher callback may not be fired.
* We work around this by manually calling the watcher
* callback when the component has loaded and the watcher
* is configured.
*/
this.triggerChanged();
}
/**

View File

@@ -15,8 +15,8 @@ describe('modal: a11y', () => {
`,
});
const modal = page.body.querySelector('ion-modal');
const modalWrapper = modal.shadowRoot.querySelector('.modal-wrapper');
const modal = page.body.querySelector('ion-modal')!;
const modalWrapper = modal.shadowRoot!.querySelector('.modal-wrapper')!;
await expect(modalWrapper.getAttribute('role')).toBe('alertdialog');
});

View File

@@ -7,10 +7,10 @@ describe('modal: htmlAttributes inheritance', () => {
it('should correctly inherit attributes on host', async () => {
const page = await newSpecPage({
components: [Modal],
template: () => <ion-modal htmlAttributes={{ 'data-testid': 'basic-modal' }}></ion-modal>,
template: () => <ion-modal htmlAttributes={{ 'data-testid': 'basic-modal' }} overlayIndex={1}></ion-modal>,
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await expect(modal.getAttribute('data-testid')).toBe('basic-modal');
});

View File

@@ -1,19 +1,18 @@
import { h } from '@stencil/core';
import { h, setMode } from '@stencil/core';
import { newSpecPage } from '@stencil/core/testing';
import { setMode } from '@stencil/core';
import { Modal } from '../../modal';
import { Content } from '../../../content/content';
import { Modal } from '../../modal';
describe('modal: canDismiss', () => {
describe('modal: regular modal', () => {
it('should dismiss when canDismiss is true', async () => {
const page = await newSpecPage({
components: [Modal],
template: () => <ion-modal animated={false} canDismiss={true}></ion-modal>,
template: () => <ion-modal overlayIndex={1} animated={false} canDismiss={true}></ion-modal>,
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
@@ -26,10 +25,10 @@ describe('modal: canDismiss', () => {
it('should not dismiss when canDismiss is false', async () => {
const page = await newSpecPage({
components: [Modal],
template: () => <ion-modal animated={false} canDismiss={false}></ion-modal>,
template: () => <ion-modal overlayIndex={1} animated={false} canDismiss={false}></ion-modal>,
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
@@ -44,6 +43,7 @@ describe('modal: canDismiss', () => {
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
animated={false}
canDismiss={() => {
return new Promise((resolve) => {
@@ -54,7 +54,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
@@ -69,6 +69,7 @@ describe('modal: canDismiss', () => {
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
animated={false}
canDismiss={() => {
return new Promise((resolve) => {
@@ -79,7 +80,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
@@ -94,19 +95,24 @@ describe('modal: canDismiss', () => {
/**
* Card modal is only available on iOS
*/
setMode((elm) => 'ios');
setMode(() => 'ios');
});
it('should dismiss when canDismiss is true', async () => {
const page = await newSpecPage({
components: [Content, Modal],
template: () => (
<ion-modal presentingElement={document.createElement('div')} animated={false} canDismiss={true}>
<ion-modal
overlayIndex={1}
presentingElement={document.createElement('div')}
animated={false}
canDismiss={true}
>
<ion-content>Test Content</ion-content>
</ion-modal>
),
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
@@ -120,13 +126,18 @@ describe('modal: canDismiss', () => {
const page = await newSpecPage({
components: [Content, Modal],
template: () => (
<ion-modal presentingElement={document.createElement('div')} animated={false} canDismiss={false}>
<ion-modal
overlayIndex={1}
presentingElement={document.createElement('div')}
animated={false}
canDismiss={false}
>
<ion-content>Test Content</ion-content>
</ion-modal>
),
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
@@ -141,6 +152,7 @@ describe('modal: canDismiss', () => {
components: [Content, Modal],
template: () => (
<ion-modal
overlayIndex={1}
presentingElement={document.createElement('div')}
animated={false}
canDismiss={() => {
@@ -154,7 +166,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
@@ -169,6 +181,7 @@ describe('modal: canDismiss', () => {
components: [Content, Modal],
template: () => (
<ion-modal
overlayIndex={1}
presentingElement={document.createElement('div')}
animated={false}
canDismiss={() => {
@@ -182,7 +195,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
@@ -197,11 +210,17 @@ describe('modal: canDismiss', () => {
const page = await newSpecPage({
components: [Modal],
template: () => (
<ion-modal breakpoints={[0, 1]} initialBreakpoint={1} animated={false} canDismiss={true}></ion-modal>
<ion-modal
overlayIndex={1}
breakpoints={[0, 1]}
initialBreakpoint={1}
animated={false}
canDismiss={true}
></ion-modal>
),
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
@@ -215,11 +234,17 @@ describe('modal: canDismiss', () => {
const page = await newSpecPage({
components: [Modal],
template: () => (
<ion-modal breakpoints={[0, 1]} initialBreakpoint={1} animated={false} canDismiss={false}></ion-modal>
<ion-modal
overlayIndex={1}
breakpoints={[0, 1]}
initialBreakpoint={1}
animated={false}
canDismiss={false}
></ion-modal>
),
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
@@ -234,6 +259,7 @@ describe('modal: canDismiss', () => {
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
breakpoints={[0, 1]}
initialBreakpoint={1}
animated={false}
@@ -246,7 +272,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
@@ -261,6 +287,7 @@ describe('modal: canDismiss', () => {
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
breakpoints={[0, 1]}
initialBreakpoint={1}
animated={false}
@@ -273,7 +300,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
@@ -288,15 +315,15 @@ describe('modal: canDismiss', () => {
const canDismiss = jest.fn();
const page = await newSpecPage({
components: [Modal],
template: () => <ion-modal animated={false} canDismiss={canDismiss}></ion-modal>,
template: () => <ion-modal overlayIndex={1} animated={false} canDismiss={canDismiss}></ion-modal>,
});
const modal = page.body.querySelector('ion-modal');
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
await page.waitForChanges();
const returnValue = await modal.dismiss('my data', 'my role');
await modal.dismiss('my data', 'my role');
expect(canDismiss).toHaveBeenCalledWith('my data', 'my role');
});

View File

@@ -1,6 +1,5 @@
import { newSpecPage } from '@stencil/core/testing';
import { Config } from '../../../global/config';
import type { ComponentProps } from '../../../interface';
import { Nav } from '../nav';
import type { NavOptions } from '../nav-interface';
@@ -197,7 +196,6 @@ describe('NavController', () => {
.insert(-1, null as any, null, null, trnsDone)
.then(() => {
fail('it should not succeed');
done();
})
.catch((err: Error) => {
const hasCompleted = false;
@@ -252,7 +250,6 @@ describe('NavController', () => {
.pop(null, trnsDone)
.then(() => {
fail('it should not succeed');
done();
})
.catch((err: any) => {
const hasCompleted = false;
@@ -819,15 +816,10 @@ describe('NavController', () => {
beforeEach(async () => {
trnsDone = jest.fn();
const config = new Config();
config.reset({ animated: false });
const page = await newSpecPage({
components: [Nav],
html: `<ion-nav></ion-nav>`,
autoApplyChanges: true,
context: {
config,
},
});
nav = page.rootInstance;
});
@@ -848,7 +840,7 @@ describe('NavController', () => {
pause: jest.fn(),
cancel: jest.fn(),
onfinish: undefined,
};
} as any;
animation.play = () => {
if (animation.onfinish) {

View File

@@ -71,13 +71,20 @@
}
:host .picker-item-empty,
:host .picker-item.picker-item-disabled {
scroll-snap-align: none;
:host .picker-item[disabled] {
cursor: default;
}
:host .picker-item.picker-item-disabled {
:host .picker-item-empty,
:host(:not([disabled])) .picker-item[disabled] {
scroll-snap-align: none;
}
:host([disabled]) {
overflow-y: hidden;
}
:host .picker-item[disabled] {
opacity: 0.4;
}

View File

@@ -35,6 +35,11 @@ export class PickerColumnInternal implements ComponentInterface {
@Element() el!: HTMLIonPickerColumnInternalElement;
/**
* If `true`, the user cannot interact with the picker.
*/
@Prop() disabled = false;
/**
* A list of options to be displayed in the picker
*/
@@ -408,13 +413,15 @@ export class PickerColumnInternal implements ComponentInterface {
};
get activeItem() {
return getElementRoot(this.el).querySelector(
`.picker-item[data-value="${this.value}"]:not([disabled])`
) as HTMLElement | null;
// If the whole picker column is disabled, the current value should appear active
// If the current value item is specifically disabled, it should not appear active
const selector = `.picker-item[data-value="${this.value}"]${this.disabled ? '' : ':not([disabled])'}`;
return getElementRoot(this.el).querySelector(selector) as HTMLElement | null;
}
render() {
const { items, color, isActive, numericInput } = this;
const { items, color, disabled: pickerDisabled, isActive, numericInput } = this;
const mode = getIonMode(this);
/**
@@ -423,10 +430,12 @@ export class PickerColumnInternal implements ComponentInterface {
* the attribute can be moved to datetime.tsx and set on every
* instance of ion-picker-column-internal there instead.
*/
return (
<Host
exportparts={`${PICKER_ITEM_PART}, ${PICKER_ITEM_ACTIVE_PART}`}
tabindex={0}
disabled={pickerDisabled}
tabindex={pickerDisabled ? null : 0}
class={createColorClasses(color, {
[mode]: true,
['picker-column-active']: isActive,
@@ -443,6 +452,8 @@ export class PickerColumnInternal implements ComponentInterface {
&nbsp;
</div>
{items.map((item, index) => {
const isItemDisabled = pickerDisabled || item.disabled || false;
{
/*
Users should be able to tab
@@ -458,14 +469,13 @@ export class PickerColumnInternal implements ComponentInterface {
tabindex="-1"
class={{
'picker-item': true,
'picker-item-disabled': item.disabled || false,
}}
data-value={item.value}
data-index={index}
onClick={(ev: Event) => {
this.centerPickerItemInView(ev.target as HTMLElement, true);
}}
disabled={item.disabled}
disabled={isItemDisabled}
part={PICKER_ITEM_PART}
>
{item.text}

View File

@@ -45,26 +45,39 @@
<ion-content class="ion-padding">
<div class="grid">
<div class="grid-item">
<h2>Default</h2>
<h2>Even items disabled</h2>
<ion-picker-internal>
<ion-picker-column-internal id="default"></ion-picker-column-internal>
<ion-picker-column-internal id="half-disabled"></ion-picker-column-internal>
</ion-picker-internal>
</div>
<div class="grid-item">
<h2>Column disabled</h2>
<ion-picker-internal>
<ion-picker-column-internal id="column-disabled" value="11" disabled></ion-picker-column-internal>
</ion-picker-internal>
</div>
</div>
</ion-content>
<script>
const defaultPickerColumn = document.getElementById('default');
const items = Array(24)
const halfDisabledPicker = document.getElementById('half-disabled');
const halfDisabledItems = Array(24)
.fill()
.map((_, i) => ({
text: `${i}`,
value: i,
disabled: i % 2 === 0,
}));
halfDisabledPicker.items = halfDisabledItems;
halfDisabledPicker.value = 12;
defaultPickerColumn.items = items;
defaultPickerColumn.value = 12;
const fullDisabledPicker = document.getElementById('column-disabled');
const items = Array(24)
.fill()
.map((_, i) => ({
text: `${i}`,
value: i,
}));
fullDisabledPicker.items = items;
</script>
</ion-app>
</body>

View File

@@ -35,7 +35,7 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
* This behavior does not vary across modes/directions.
*/
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('picker-column-internal: disabled'), () => {
test.describe(title('picker-column-internal: disabled items'), () => {
test('all picker items should be enabled by default', async ({ page }) => {
await page.setContent(
`
@@ -55,9 +55,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
config
);
const pickerItems = page.locator(
'ion-picker-column-internal .picker-item:not(.picker-item-empty, .picker-item-disabled)'
);
const pickerItems = page.locator('ion-picker-column-internal .picker-item:not(.picker-item-empty, [disabled])');
expect(await pickerItems.count()).toBe(3);
});
@@ -80,7 +78,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
config
);
const disabledItem = page.locator('ion-picker-column-internal .picker-item.picker-item-disabled');
const disabledItem = page.locator('ion-picker-column-internal .picker-item[disabled]');
await expect(disabledItem).not.toBeEnabled();
});
test('disabled picker item should not be considered active', async ({ page }) => {
@@ -130,7 +128,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await page.waitForChanges();
const disabledItem = page.locator('ion-picker-column-internal .picker-item[data-value="b"]');
await expect(disabledItem).toHaveClass(/picker-item-disabled/);
await expect(disabledItem).toBeDisabled();
await expect(disabledItem).not.toHaveClass(/picker-item-active/);
});
test('defaulting the value to a disabled item should not cause that item to be active', async ({ page }) => {
@@ -154,8 +152,42 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
);
const disabledItem = page.locator('ion-picker-column-internal .picker-item[data-value="b"]');
await expect(disabledItem).toHaveClass(/picker-item-disabled/);
await expect(disabledItem).toBeDisabled();
await expect(disabledItem).not.toHaveClass(/picker-item-active/);
});
});
});
/**
* This behavior does not vary across directions.
*/
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('picker-column-internal: disabled column rendering'), () => {
test.beforeEach(async ({ page }) => {
await page.goto('/src/components/picker-column-internal/test/disabled', config);
});
test('disabled column should not have visual regressions', async ({ page }) => {
const disabledColumn = page.locator('#column-disabled');
await page.waitForChanges();
await expect(disabledColumn).toHaveScreenshot(screenshot('picker-internal-disabled-column'));
});
});
});
/**
* This behavior does not vary across modes/directions.
*/
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('picker-column-internal: disabled column'), () => {
test.beforeEach(async ({ page }) => {
await page.goto('/src/components/picker-column-internal/test/disabled', config);
});
test('item in disabled column should not be interactive', async ({ page }) => {
const secondItem = page.locator('#column-disabled .picker-item:not(.picker-item-empty)').nth(1);
await expect(secondItem).toBeDisabled();
});
});
});

View File

@@ -19,6 +19,7 @@ describe('picker-column', () => {
text: 'Java',
},
],
name: 'programmingLanguages',
};
const page = await newSpecPage({
@@ -26,8 +27,8 @@ describe('picker-column', () => {
template: () => <ion-picker-column col={col}></ion-picker-column>,
});
const firstOption = page.body.querySelector('ion-picker-column .picker-opt:nth-child(1)');
const secondOption = page.body.querySelector('ion-picker-column .picker-opt:nth-child(2)');
const firstOption = page.body.querySelector('ion-picker-column .picker-opt:nth-child(1)')!;
const secondOption = page.body.querySelector('ion-picker-column .picker-opt:nth-child(2)')!;
expect(firstOption.getAttribute('aria-label')).toBe('C Sharp');
expect(secondOption.getAttribute('aria-label')).toBe(null);

View File

@@ -15,18 +15,19 @@ describe('picker-column: dynamic options', () => {
const page = await newSpecPage({
components: [PickerColumnCmp],
template: () => <ion-picker-column col={{ options: defaultOptions }}></ion-picker-column>,
template: () => <ion-picker-column col={{ options: defaultOptions, name: 'animals' }}></ion-picker-column>,
});
const pickerCol = page.body.querySelector('ion-picker-column');
const pickerCol = page.body.querySelector('ion-picker-column')!;
pickerCol.col = {
options: [...defaultOptions, { text: 'Carrot', value: 'carrot' }],
name: 'vegetables',
};
await page.waitForChanges();
const pickerOpt = pickerCol.querySelector('.picker-opt:nth(2)');
const pickerOpt = pickerCol.querySelector('.picker-opt:nth(2)')!;
expect(pickerOpt.getAttribute('style')).toContain('transform');
});
});

View File

@@ -5,14 +5,14 @@ import { PickerColumnCmp } from '../picker-column';
describe('picker-column', () => {
it('should add class to host of component', async () => {
const col = { cssClass: 'test-class', options: [] };
const col = { cssClass: 'test-class', options: [], name: 'col' };
const page = await newSpecPage({
components: [PickerColumnCmp],
template: () => <ion-picker-column col={col}></ion-picker-column>,
});
const pickerCol = page.body.querySelector('ion-picker-column');
const pickerCol = page.body.querySelector('ion-picker-column')!;
expect(pickerCol.classList.contains('test-class')).toBe(true);
});
});

View File

@@ -209,6 +209,17 @@ export class Picker implements ComponentInterface, OverlayInterface {
if (this.isOpen === true) {
raf(() => this.present());
}
/**
* When binding values in frameworks such as Angular
* it is possible for the value to be set after the Web Component
* initializes but before the value watcher is set up in Stencil.
* As a result, the watcher callback may not be fired.
* We work around this by manually calling the watcher
* callback when the component has loaded and the watcher
* is configured.
*/
this.triggerChanged();
}
/**

View File

@@ -370,6 +370,17 @@ export class Popover implements ComponentInterface, PopoverInterface {
this.dismiss(undefined, undefined, false);
});
}
/**
* When binding values in frameworks such as Angular
* it is possible for the value to be set after the Web Component
* initializes but before the value watcher is set up in Stencil.
* As a result, the watcher callback may not be fired.
* We work around this by manually calling the watcher
* callback when the component has loaded and the watcher
* is configured.
*/
this.configureTriggerInteraction();
}
/**

View File

@@ -7,10 +7,10 @@ describe('popover: htmlAttributes inheritance', () => {
it('should correctly inherit attributes on host', async () => {
const page = await newSpecPage({
components: [Popover],
template: () => <ion-popover htmlAttributes={{ 'data-testid': 'basic-popover' }}></ion-popover>,
template: () => <ion-popover overlayIndex={1} htmlAttributes={{ 'data-testid': 'basic-popover' }}></ion-popover>,
});
const popover = page.body.querySelector('ion-popover');
const popover = page.body.querySelector('ion-popover')!;
await expect(popover.getAttribute('data-testid')).toBe('basic-popover');
});

View File

@@ -17,20 +17,20 @@ describe('isTriggerElement', () => {
describe('getIndexOfItem', () => {
it('should return the correct index in an array of ion-items', () => {
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']);
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']) as HTMLIonItemElement[];
expect(getIndexOfItem(array, array[1])).toEqual(1);
});
it('should return -1 when ion-item not found', () => {
const el = document.createElement('ion-item');
const array = createArrayOfElements(['ion-item', 'ion-item']);
const array = createArrayOfElements(['ion-item', 'ion-item']) as HTMLIonItemElement[];
expect(getIndexOfItem(array, el)).toEqual(-1);
});
it('should return -1 if a non-ion-item is passed in', () => {
const array = createArrayOfElements(['ion-item', 'div', 'ion-item']);
const array = createArrayOfElements(['ion-item', 'div', 'ion-item']) as HTMLIonItemElement[];
expect(getIndexOfItem(array, array[1])).toEqual(-1);
});
@@ -38,24 +38,24 @@ describe('getIndexOfItem', () => {
describe('getNextItem', () => {
it('should get the next item in an array of ion-items', () => {
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']);
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']) as HTMLIonItemElement[];
expect(getNextItem(array, array[1])).toEqual(array[2]);
});
it('should return undefined if there is no next item', () => {
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']);
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']) as HTMLIonItemElement[];
expect(getNextItem(array, array[2])).toEqual(undefined);
});
});
describe('getPrevItem', () => {
it('should get the previous item in an array of ion-items', () => {
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']);
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']) as HTMLIonItemElement[];
expect(getPrevItem(array, array[1])).toEqual(array[0]);
});
it('should return undefined if there is no previous item', () => {
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']);
const array = createArrayOfElements(['ion-item', 'ion-item', 'ion-item']) as HTMLIonItemElement[];
expect(getPrevItem(array, array[0])).toEqual(undefined);
});
});

View File

@@ -1,7 +1,8 @@
import { Radio } from '../radio.tsx';
import { RadioGroup } from '../../radio-group/radio-group.tsx';
import { newSpecPage } from '@stencil/core/testing';
import { RadioGroup } from '../../radio-group/radio-group';
import { Radio } from '../radio';
describe('ion-radio', () => {
it('should set a default value', async () => {
const radio = new Radio();
@@ -21,7 +22,7 @@ describe('ion-radio', () => {
`,
});
const radio = page.root.querySelector('ion-radio');
const radio = page.body.querySelector('ion-radio')!;
expect(radio.classList.contains('radio-checked')).toBe(false);
radio.value = 'a';
@@ -43,8 +44,8 @@ describe('ion-radio: disabled', () => {
`,
});
const radio = page.body.querySelector('ion-radio');
const radioGroup = page.body.querySelector('ion-radio-group');
const radio = page.body.querySelector('ion-radio')!;
const radioGroup = page.body.querySelector('ion-radio-group')!;
expect(radioGroup.value).toBe(undefined);

View File

@@ -1,8 +1,10 @@
import { newSpecPage } from '@stencil/core/testing';
import { Range } from '../range';
import { Item } from '../../item/item';
let sharedRange;
import { Item } from '../../item/item';
import { Range } from '../range';
let sharedRange: Range;
describe('Range', () => {
beforeEach(() => {
sharedRange = new Range();
@@ -21,7 +23,8 @@ describe('Range', () => {
];
valueTests.forEach((test) => {
expect(sharedRange.ensureValueInBounds(test[0])).toBe(test[1]);
// Casting as any since we are accessing a private API on the range component
expect((sharedRange as any).ensureValueInBounds(test[0])).toBe(test[1]);
});
});
@@ -58,7 +61,8 @@ describe('Range', () => {
];
valueTests.forEach((test) => {
expect(sharedRange.ensureValueInBounds(test[0])).toEqual(test[1]);
// Casting as any since we are accessing a private API on the range component
expect((sharedRange as any).ensureValueInBounds(test[0])).toEqual(test[1]);
});
});
});
@@ -73,7 +77,7 @@ describe('range id', () => {
</ion-range>`,
});
const range = page.body.querySelector('ion-range');
const range = page.body.querySelector('ion-range')!;
expect(range.getAttribute('id')).toBe('my-custom-range');
});
});
@@ -89,7 +93,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range');
const range = page.body.querySelector('ion-range')!;
expect(range.classList.contains('range-item-start-adjustment')).toBe(true);
expect(range.classList.contains('range-item-end-adjustment')).toBe(true);
});
@@ -104,7 +108,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range');
const range = page.body.querySelector('ion-range')!;
expect(range.classList.contains('range-item-start-adjustment')).toBe(true);
expect(range.classList.contains('range-item-end-adjustment')).toBe(false);
});
@@ -119,7 +123,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range');
const range = page.body.querySelector('ion-range')!;
expect(range.classList.contains('range-item-start-adjustment')).toBe(false);
expect(range.classList.contains('range-item-end-adjustment')).toBe(true);
});
@@ -134,7 +138,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range');
const range = page.body.querySelector('ion-range')!;
expect(range.classList.contains('range-item-start-adjustment')).toBe(false);
expect(range.classList.contains('range-item-end-adjustment')).toBe(true);
});
@@ -149,7 +153,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range');
const range = page.body.querySelector('ion-range')!;
expect(range.classList.contains('range-item-start-adjustment')).toBe(true);
expect(range.classList.contains('range-item-end-adjustment')).toBe(true);
});
@@ -164,7 +168,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range');
const range = page.body.querySelector('ion-range')!;
expect(range.classList.contains('range-item-start-adjustment')).toBe(true);
expect(range.classList.contains('range-item-end-adjustment')).toBe(true);
});
@@ -177,7 +181,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range');
const range = page.body.querySelector('ion-range')!;
expect(range.classList.contains('range-item-start-adjustment')).toBe(false);
expect(range.classList.contains('range-item-end-adjustment')).toBe(false);
});
@@ -191,7 +195,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range');
const range = page.body.querySelector('ion-range')!;
expect(range.classList.contains('range-item-start-adjustment')).toBe(false);
expect(range.classList.contains('range-item-end-adjustment')).toBe(false);
});
@@ -206,7 +210,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range');
const range = page.body.querySelector('ion-range')!;
expect(range.classList.contains('range-item-start-adjustment')).toBe(false);
expect(range.classList.contains('range-item-end-adjustment')).toBe(false);
});
@@ -221,7 +225,7 @@ describe('range: item adjustments', () => {
`,
});
const range = page.body.querySelector('ion-range');
const range = page.body.querySelector('ion-range')!;
expect(range.classList.contains('range-item-start-adjustment')).toBe(false);
expect(range.classList.contains('range-item-end-adjustment')).toBe(false);
});

View File

@@ -1,6 +1,7 @@
import { newSpecPage } from '@stencil/core/testing';
import { RefresherContent } from '../refresher-content';
import { config } from '../../../global/config';
import { RefresherContent } from '../refresher-content';
describe('refresher-content: custom html', () => {
it('should not allow for custom html by default', async () => {
@@ -9,11 +10,11 @@ describe('refresher-content: custom html', () => {
html: `<ion-refresher-content pulling-text="<button class='custom-pulling-html'>Custom Pulling Text</button>" refreshing-text="<button class='custom-refreshing-html'>Custom Refreshing Text</button>"></ion-refresher-content>`,
});
const pullingContent = page.body.querySelector('.refresher-pulling-text');
const pullingContent = page.body.querySelector('.refresher-pulling-text')!;
expect(pullingContent.textContent).toContain('Custom Pulling Text');
expect(pullingContent.querySelector('button.custom-pulling-html')).toBe(null);
const refreshingContent = page.body.querySelector('.refresher-refreshing-text');
const refreshingContent = page.body.querySelector('.refresher-refreshing-text')!;
expect(refreshingContent.textContent).toContain('Custom Refreshing Text');
expect(refreshingContent.querySelector('button.custom-refreshing-html')).toBe(null);
});
@@ -25,11 +26,11 @@ describe('refresher-content: custom html', () => {
html: `<ion-refresher-content pulling-text="<button class='custom-pulling-html'>Custom Pulling Text</button>" refreshing-text="<button class='custom-refreshing-html'>Custom Refreshing Text</button>"></ion-refresher-content>`,
});
const pullingContent = page.body.querySelector('.refresher-pulling-text');
const pullingContent = page.body.querySelector('.refresher-pulling-text')!;
expect(pullingContent.textContent).toContain('Custom Pulling Text');
expect(pullingContent.querySelector('button.custom-pulling-html')).not.toBe(null);
const refreshingContent = page.body.querySelector('.refresher-refreshing-text');
const refreshingContent = page.body.querySelector('.refresher-refreshing-text')!;
expect(refreshingContent.textContent).toContain('Custom Refreshing Text');
expect(refreshingContent.querySelector('button.custom-refreshing-html')).not.toBe(null);
});
@@ -41,11 +42,11 @@ describe('refresher-content: custom html', () => {
html: `<ion-refresher-content pulling-text="<button class='custom-pulling-html'>Custom Pulling Text</button>" refreshing-text="<button class='custom-html'>Custom Refreshing Text</button>"></ion-refresher-content>`,
});
const pullingContent = page.body.querySelector('.refresher-pulling-text');
const pullingContent = page.body.querySelector('.refresher-pulling-text')!;
expect(pullingContent.textContent).toContain('Custom Pulling Text');
expect(pullingContent.querySelector('button.custom-pulling-html')).toBe(null);
const refreshingContent = page.body.querySelector('.refresher-refreshing-text');
const refreshingContent = page.body.querySelector('.refresher-refreshing-text')!;
expect(refreshingContent.textContent).toContain('Custom Refreshing Text');
expect(refreshingContent.querySelector('button.custom-refreshing-html')).toBe(null);
});

View File

@@ -203,17 +203,17 @@ describe('findChainForSegments', () => {
describe('mergeParams', () => {
it('should merge undefined', () => {
expect(mergeParams(undefined, undefined)).toBeUndefined();
expect(mergeParams(null, undefined)).toBeUndefined();
expect(mergeParams(undefined, null)).toBeUndefined();
expect(mergeParams(null, null)).toBeUndefined();
expect(mergeParams(null as any, undefined)).toBeUndefined();
expect(mergeParams(undefined, null as any)).toBeUndefined();
expect(mergeParams(null as any, null as any)).toBeUndefined();
});
it('should merge undefined with params', () => {
const params = { data: '1' };
expect(mergeParams(undefined, params)).toEqual(params);
expect(mergeParams(null, params)).toEqual(params);
expect(mergeParams(null as any, params)).toEqual(params);
expect(mergeParams(params, undefined)).toEqual(params);
expect(mergeParams(params, null)).toEqual(params);
expect(mergeParams(params, null as any)).toEqual(params);
});
it('should merge params with params', () => {
@@ -253,36 +253,44 @@ describe('RouterSegments', () => {
describe('matchesRedirect', () => {
it('should match empty redirect', () => {
expect(matchesRedirect([''], { from: [''], to: [''] })).toBeTruthy();
expect(matchesRedirect([''], { from: ['*'], to: [''] })).toBeTruthy();
expect(matchesRedirect([''], { from: [''], to: { segments: [''] } })).toBeTruthy();
expect(matchesRedirect([''], { from: ['*'], to: { segments: [''] } })).toBeTruthy();
expect(matchesRedirect([''], { from: ['hola'], to: [''] })).toBeFalsy();
expect(matchesRedirect([''], { from: ['hola', '*'], to: [''] })).toBeFalsy();
expect(matchesRedirect([''], { from: ['hola'], to: { segments: [] } })).toBeFalsy();
expect(matchesRedirect([''], { from: ['hola', '*'], to: { segments: [''] } })).toBeFalsy();
});
it('should match simple segment redirect', () => {
expect(matchesRedirect(['workouts'], { from: ['workouts'], to: [''] })).toBeTruthy();
expect(matchesRedirect(['workouts'], { from: ['*'], to: [''] })).toBeTruthy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts', '*'], to: [''] })).toBeTruthy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts', 'hola'], to: [''] })).toBeTruthy();
expect(matchesRedirect(['workouts'], { from: ['workouts'], to: { segments: [''] } })).toBeTruthy();
expect(matchesRedirect(['workouts'], { from: ['*'], to: { segments: [''] } })).toBeTruthy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts', '*'], to: { segments: [''] } })).toBeTruthy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts', 'hola'], to: { segments: [''] } })).toBeTruthy();
expect(matchesRedirect(['workouts'], { from: ['workouts', '*'], to: [''] })).toBeFalsy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts'], to: [''] })).toBeFalsy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts', 'adios'], to: [''] })).toBeFalsy();
expect(matchesRedirect(['workouts'], { from: ['workouts', '*'], to: { segments: [''] } })).toBeFalsy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts'], to: { segments: [''] } })).toBeFalsy();
expect(matchesRedirect(['workouts', 'hola'], { from: ['workouts', 'adios'], to: { segments: [''] } })).toBeFalsy();
});
it('should match long route', () => {
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['*'], to: [''] })).toBeTruthy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', '*'], to: [''] })).toBeTruthy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path', '*'], to: [''] })).toBeTruthy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path', 'to'], to: [''] })).toBeTruthy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['login'], to: [''] })).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['login', '*'], to: [''] })).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts'], to: [''] })).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path'], to: [''] })).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['*'], to: { segments: [''] } })).toBeTruthy();
expect(
matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path', 'to', '*'], to: [''] })
matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', '*'], to: { segments: [''] } })
).toBeTruthy();
expect(
matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path', '*'], to: { segments: [''] } })
).toBeTruthy();
expect(
matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path', 'to'], to: { segments: [''] } })
).toBeTruthy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['login'], to: { segments: [''] } })).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['login', '*'], to: { segments: [''] } })).toBeFalsy();
expect(matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts'], to: { segments: [''] } })).toBeFalsy();
expect(
matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path'], to: { segments: [''] } })
).toBeFalsy();
expect(
matchesRedirect(['workouts', 'path', 'to'], { from: ['workouts', 'path', 'to', '*'], to: { segments: [''] } })
).toBeFalsy();
});

View File

@@ -18,19 +18,21 @@ describe('ionic-conference-app', () => {
expect(getRouteIDs('/about', routes)).toEqual(['page-tabs', 'page-about']);
expect(getRouteIDs('/tutorial', routes)).toEqual(['page-tutorial']);
expect(getRoutePath([{ id: 'PAGE-TABS' }, { id: 'tab-schedule' }, { id: 'page-schedule' }], routes)).toEqual('/');
expect(
getRoutePath([{ id: 'PAGE-TABS' }, { id: 'tab-schedule' }, { id: 'page-schedule' }] as RouteID[], routes)
).toEqual('/');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'TAB-SPEAKER' }], routes)).toEqual('/speaker');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'TAB-SPEAKER' }] as RouteID[], routes)).toEqual('/speaker');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'TAB-SPEAKER' }, { id: 'page-speaker-list' }], routes)).toEqual(
'/speaker'
);
expect(
getRoutePath([{ id: 'page-tabs' }, { id: 'TAB-SPEAKER' }, { id: 'page-speaker-list' }] as RouteID[], routes)
).toEqual('/speaker');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'PAGE-MAP' }], routes)).toEqual('/map');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'PAGE-MAP' }] as RouteID[], routes)).toEqual('/map');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'page-about' }], routes)).toEqual('/about');
expect(getRoutePath([{ id: 'page-tabs' }, { id: 'page-about' }] as RouteID[], routes)).toEqual('/about');
expect(getRoutePath([{ id: 'page-tutorial' }], routes)).toEqual('/tutorial');
expect(getRoutePath([{ id: 'page-tutorial' }] as RouteID[], routes)).toEqual('/tutorial');
});
let win: Window;

View File

@@ -257,6 +257,8 @@ export class Searchbar implements ComponentInterface {
*
* Developers who wish to focus an input when an overlay is presented
* should call `setFocus` after `didPresent` has resolved.
*
* See [managing focus](/docs/developing/managing-focus) for more information.
*/
@Method()
async setFocus() {

View File

@@ -9,7 +9,7 @@ describe('searchbar: rendering', () => {
html: '<ion-searchbar name="search"></ion-searchbar>',
});
const nativeEl = page.body.querySelector('ion-searchbar input');
const nativeEl = page.body.querySelector('ion-searchbar input')!;
expect(nativeEl.getAttribute('name')).toBe('search');
});
});

View File

@@ -9,14 +9,14 @@ it('should disable segment buttons added to disabled segment async', async () =>
html: `<ion-segment disabled="true"></ion-segment>`,
});
const segment = page.body.querySelector('ion-segment');
const segment = page.body.querySelector('ion-segment')!;
segment.innerHTML = `
<ion-segment-button>
<ion-label>Segment Button</ion-label>
</ion-segment-button>`;
await page.waitForChanges();
const segmentButton = page.body.querySelector('ion-segment-button');
const segmentButton = page.body.querySelector('ion-segment-button')!;
expect(segmentButton.disabled).toBe(true);
});
@@ -32,7 +32,7 @@ it('should set checked state when value is set asynchronously', async () => {
`,
});
const segmentButton = page.root.querySelector('ion-segment-button');
const segmentButton = page.body.querySelector('ion-segment-button')!;
expect(segmentButton.classList.contains('segment-button-checked')).toBe(false);

View File

@@ -10,9 +10,9 @@ describe('ion-select', () => {
template: () => <ion-select value="my value" name="my name" disabled={true}></ion-select>,
});
const select = page.body.querySelector('ion-select');
const select = page.body.querySelector('ion-select')!;
const hiddenInput = select.querySelector('input[type="hidden"]');
const hiddenInput = select.querySelector<HTMLInputElement>('input[type="hidden"]')!;
expect(hiddenInput).not.toBe(null);
expect(hiddenInput.value).toBe('my value');
@@ -28,10 +28,10 @@ describe('ion-select', () => {
`,
});
const select = page.body.querySelector('ion-select');
const select = page.body.querySelector('ion-select')!;
const propEl = select.shadowRoot.querySelector('.label-text');
const slotEl = select.shadowRoot.querySelector('slot[name="label"]');
const propEl = select.shadowRoot!.querySelector('.label-text');
const slotEl = select.shadowRoot!.querySelector('slot[name="label"]');
expect(propEl).not.toBe(null);
expect(slotEl).toBe(null);
@@ -44,10 +44,10 @@ describe('ion-select', () => {
`,
});
const select = page.body.querySelector('ion-select');
const select = page.body.querySelector('ion-select')!;
const propEl = select.shadowRoot.querySelector('.label-text');
const slotEl = select.shadowRoot.querySelector('slot[name="label"]');
const propEl = select.shadowRoot!.querySelector('.label-text');
const slotEl = select.shadowRoot!.querySelector('slot[name="label"]');
expect(propEl).toBe(null);
expect(slotEl).not.toBe(null);
@@ -60,10 +60,10 @@ describe('ion-select', () => {
`,
});
const select = page.body.querySelector('ion-select');
const select = page.body.querySelector('ion-select')!;
const propEl = select.shadowRoot.querySelector('.label-text');
const slotEl = select.shadowRoot.querySelector('slot[name="label"]');
const propEl = select.shadowRoot!.querySelector('.label-text');
const slotEl = select.shadowRoot!.querySelector('slot[name="label"]');
expect(propEl).not.toBe(null);
expect(slotEl).toBe(null);

View File

@@ -1,4 +1,5 @@
import { newSpecPage } from '@stencil/core/testing';
import { Textarea } from '../textarea';
it('should inherit attributes', async () => {
@@ -7,7 +8,7 @@ it('should inherit attributes', async () => {
html: '<ion-textarea title="my title" tabindex="-1" data-form-type="password"></ion-textarea>',
});
const nativeEl = page.body.querySelector('ion-textarea textarea');
const nativeEl = page.body.querySelector('ion-textarea textarea')!;
expect(nativeEl.getAttribute('title')).toBe('my title');
expect(nativeEl.getAttribute('tabindex')).toBe('-1');
expect(nativeEl.getAttribute('data-form-type')).toBe('password');
@@ -31,9 +32,9 @@ describe('textarea: label rendering', () => {
`,
});
const textarea = page.body.querySelector('ion-textarea');
const textarea = page.body.querySelector('ion-textarea')!;
const labelText = textarea.querySelector('.label-text-wrapper');
const labelText = textarea.querySelector('.label-text-wrapper')!;
expect(labelText.textContent).toBe('Label Prop Text');
});
@@ -45,9 +46,9 @@ describe('textarea: label rendering', () => {
`,
});
const textarea = page.body.querySelector('ion-textarea');
const textarea = page.body.querySelector('ion-textarea')!;
const labelText = textarea.querySelector('.label-text-wrapper');
const labelText = textarea.querySelector('.label-text-wrapper')!;
expect(labelText.textContent).toBe('Label Prop Slot');
});
@@ -59,9 +60,9 @@ describe('textarea: label rendering', () => {
`,
});
const textarea = page.body.querySelector('ion-textarea');
const textarea = page.body.querySelector('ion-textarea')!;
const labelText = textarea.querySelector('.label-text-wrapper');
const labelText = textarea.querySelector('.label-text-wrapper')!;
expect(labelText.textContent).toBe('Label Prop Text');
});

View File

@@ -93,7 +93,9 @@ export class Textarea implements ComponentInterface {
@Prop() autocapitalize = 'none';
/**
* This Boolean attribute lets you specify that a form control should have input focus when the page loads.
* Sets the [`autofocus` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus) on the native input element.
*
* This may not be sufficient for the element to be focused on page load. See [managing focus](/docs/developing/managing-focus) for more information.
*/
@Prop() autofocus = false;
@@ -372,6 +374,8 @@ export class Textarea implements ComponentInterface {
/**
* Sets focus on the native `textarea` in `ion-textarea`. Use this method instead of the global
* `textarea.focus()`.
*
* See [managing focus](/docs/developing/managing-focus) for more information.
*/
@Method()
async setFocus() {

View File

@@ -1,8 +1,8 @@
import { h } from '@stencil/core';
import { newSpecPage } from '@stencil/core/testing';
import { Toast } from '../toast';
import { config } from '../../../global/config';
import { toastController } from '../../../utils/overlays';
import { Toast } from '../toast';
describe('toast: custom html', () => {
it('should not allow for custom html by default', async () => {
@@ -11,8 +11,8 @@ describe('toast: custom html', () => {
html: `<ion-toast message="<button class='custom-html'>Custom Text</button>"></ion-toast>`,
});
const toast = page.body.querySelector('ion-toast');
const content = toast.shadowRoot.querySelector('.toast-message');
const toast = page.body.querySelector('ion-toast')!;
const content = toast.shadowRoot!.querySelector('.toast-message')!;
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});
@@ -24,8 +24,8 @@ describe('toast: custom html', () => {
html: `<ion-toast message="<button class='custom-html'>Custom Text</button>"></ion-toast>`,
});
const toast = page.body.querySelector('ion-toast');
const content = toast.shadowRoot.querySelector('.toast-message');
const toast = page.body.querySelector('ion-toast')!;
const content = toast.shadowRoot!.querySelector('.toast-message')!;
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).not.toBe(null);
});
@@ -37,8 +37,8 @@ describe('toast: custom html', () => {
html: `<ion-toast message="<button class='custom-html'>Custom Text</button>"></ion-toast>`,
});
const toast = page.body.querySelector('ion-toast');
const content = toast.shadowRoot.querySelector('.toast-message');
const toast = page.body.querySelector('ion-toast')!;
const content = toast.shadowRoot!.querySelector('.toast-message')!;
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});
@@ -56,9 +56,9 @@ describe('toast: a11y smoke test', () => {
html: `<ion-toast message="Message" header="Header"></ion-toast>`,
});
const toast = page.body.querySelector('ion-toast');
const header = toast.shadowRoot.querySelector('.toast-header');
const message = toast.shadowRoot.querySelector('.toast-message');
const toast = page.body.querySelector('ion-toast')!;
const header = toast.shadowRoot!.querySelector('.toast-header')!;
const message = toast.shadowRoot!.querySelector('.toast-message')!;
expect(header.getAttribute('aria-hidden')).toBe('true');
expect(message.getAttribute('aria-hidden')).toBe('true');
@@ -74,7 +74,7 @@ describe('toast: a11y smoke test', () => {
`,
});
const toast = page.body.querySelector('ion-toast');
const toast = page.body.querySelector('ion-toast')!;
/**
* Wait for present method to resolve
@@ -83,8 +83,8 @@ describe('toast: a11y smoke test', () => {
await toast.present();
await page.waitForChanges();
const header = toast.shadowRoot.querySelector('.toast-header');
const message = toast.shadowRoot.querySelector('.toast-message');
const header = toast.shadowRoot!.querySelector('.toast-header')!;
const message = toast.shadowRoot!.querySelector('.toast-message')!;
expect(header.getAttribute('aria-hidden')).toBe(null);
expect(message.getAttribute('aria-hidden')).toBe(null);
@@ -98,7 +98,7 @@ describe('toast: duration config', () => {
html: `<ion-toast></ion-toast>`,
});
const toast = page.body.querySelector('ion-toast');
const toast = page.body.querySelector('ion-toast')!;
expect(toast.duration).toBe(0);
});
@@ -111,7 +111,7 @@ describe('toast: duration config', () => {
html: `<ion-toast></ion-toast>`,
});
const toast = page.body.querySelector('ion-toast');
const toast = page.body.querySelector('ion-toast')!;
expect(toast.duration).toBe(5000);
});
@@ -121,10 +121,10 @@ describe('toast: htmlAttributes', () => {
it('should correctly inherit attributes on host', async () => {
const page = await newSpecPage({
components: [Toast],
template: () => <ion-toast htmlAttributes={{ 'data-testid': 'basic-toast' }}></ion-toast>,
template: () => <ion-toast overlayIndex={1} htmlAttributes={{ 'data-testid': 'basic-toast' }}></ion-toast>,
});
const toast = page.body.querySelector('ion-toast');
const toast = page.body.querySelector('ion-toast')!;
await expect(toast.getAttribute('data-testid')).toBe('basic-toast');
});
@@ -134,12 +134,12 @@ describe('toast: button cancel', () => {
it('should render the cancel button with part button-cancel', async () => {
const page = await newSpecPage({
components: [Toast],
template: () => <ion-toast buttons={[{ text: 'Cancel', role: 'cancel' }]}></ion-toast>,
template: () => <ion-toast overlayIndex={1} buttons={[{ text: 'Cancel', role: 'cancel' }]}></ion-toast>,
});
const toast = page.body.querySelector('ion-toast');
const toast = page.body.querySelector('ion-toast')!;
const buttonCancel = toast?.shadowRoot?.querySelector('.toast-button-cancel');
const buttonCancel = toast.shadowRoot!.querySelector('.toast-button-cancel')!;
expect(buttonCancel.getAttribute('part')).toBe('button cancel');
});

View File

@@ -288,6 +288,17 @@ export class Toast implements ComponentInterface, OverlayInterface {
if (this.isOpen === true) {
raf(() => this.present());
}
/**
* When binding values in frameworks such as Angular
* it is possible for the value to be set after the Web Component
* initializes but before the value watcher is set up in Stencil.
* As a result, the watcher callback may not be fired.
* We work around this by manually calling the watcher
* callback when the component has loaded and the watcher
* is configured.
*/
this.triggerChanged();
}
/**

View File

@@ -51,7 +51,7 @@ describe('ion-toggle: disabled', () => {
`,
});
const toggle = page.body.querySelector('ion-toggle');
const toggle = page.body.querySelector('ion-toggle')!;
expect(toggle.checked).toBe(false);

View File

@@ -1,11 +1,12 @@
import type { IonicConfig } from '../../interface';
import { Config } from '../config';
describe('Config', () => {
it('should get a value from the config', () => {
const config = new Config();
config.reset({ name: 'Doc Brown' });
expect(config.get('name')).toEqual('Doc Brown');
expect(config.getBoolean('name')).toBe(false);
config.reset({ mode: 'ios' } as IonicConfig);
expect(config.get('mode')).toEqual('ios');
expect(config.getBoolean('mode')).toBe(false);
});
it('should get a boolean value', () => {
@@ -18,14 +19,14 @@ describe('Config', () => {
bool4: 'hola',
bool5: 0,
bool6: 1,
});
expect(config.getBoolean('bool0')).toEqual(false);
expect(config.getBoolean('bool1')).toEqual(false);
expect(config.getBoolean('bool2')).toEqual(true);
expect(config.getBoolean('bool3')).toEqual(true);
expect(config.getBoolean('bool4')).toEqual(false);
expect(config.getBoolean('bool5')).toEqual(false);
expect(config.getBoolean('bool6')).toEqual(true);
} as any);
expect(config.getBoolean('bool0' as any)).toEqual(false);
expect(config.getBoolean('bool1' as any)).toEqual(false);
expect(config.getBoolean('bool2' as any)).toEqual(true);
expect(config.getBoolean('bool3' as any)).toEqual(true);
expect(config.getBoolean('bool4' as any)).toEqual(false);
expect(config.getBoolean('bool5' as any)).toEqual(false);
expect(config.getBoolean('bool6' as any)).toEqual(true);
});
it('should get a number value', () => {
@@ -36,12 +37,12 @@ describe('Config', () => {
nu2: '200',
nu3: '2.3',
nu4: -100.2,
});
expect(config.getNumber('nu0')).toEqual(0);
expect(config.getNumber('nu1')).toEqual(-1);
expect(config.getNumber('nu2')).toEqual(200);
expect(config.getNumber('nu3')).toEqual(2.3);
expect(config.getNumber('nu4')).toEqual(-100.2);
} as any);
expect(config.getNumber('nu0' as any)).toEqual(0);
expect(config.getNumber('nu1' as any)).toEqual(-1);
expect(config.getNumber('nu2' as any)).toEqual(200);
expect(config.getNumber('nu3' as any)).toEqual(2.3);
expect(config.getNumber('nu4' as any)).toEqual(-100.2);
});
it('should not get fallback', () => {
@@ -56,29 +57,29 @@ describe('Config', () => {
nu0: '0',
nu1: 0,
nu2: 10,
});
expect(config.get('text0', 'HEY')).toEqual('');
expect(config.get('text1', 'HEY')).toEqual('hola');
} as any);
expect(config.get('text0' as any, 'HEY')).toEqual('');
expect(config.get('text1' as any, 'HEY')).toEqual('hola');
expect(config.getBoolean('bool0', true)).toEqual(false);
expect(config.getBoolean('bool1', true)).toEqual(false);
expect(config.getBoolean('bool0' as any, true)).toEqual(false);
expect(config.getBoolean('bool1' as any, true)).toEqual(false);
expect(config.getNumber('nu0', 100)).toEqual(0);
expect(config.getNumber('nu1', 100)).toEqual(0);
expect(config.getNumber('nu2', 100)).toEqual(10);
expect(config.getNumber('nu0' as any, 100)).toEqual(0);
expect(config.getNumber('nu1' as any, 100)).toEqual(0);
expect(config.getNumber('nu2' as any, 100)).toEqual(10);
});
it('should get fallback', () => {
const config = new Config();
expect(config.get('text0', 'HEY')).toEqual('HEY');
expect(config.getBoolean('bool0', true)).toEqual(true);
expect(config.getNumber('nu0', 100)).toEqual(100);
expect(config.get('text0' as any, 'HEY')).toEqual('HEY');
expect(config.getBoolean('bool0' as any, true)).toEqual(true);
expect(config.getNumber('nu0' as any, 100)).toEqual(100);
});
it('should set value', () => {
const config = new Config();
expect(config.get('text0', 'HEY')).toEqual('HEY');
config.set('text0', 'hola');
expect(config.get('text0', 'HEY')).toEqual('hola');
expect(config.get('text0' as any, 'HEY')).toEqual('HEY');
config.set('text0' as any, 'hola');
expect(config.get('text0' as any, 'HEY')).toEqual('hola');
});
});

View File

@@ -1,3 +1,38 @@
/**
* A heuristic that applies CSS to tablet
* viewports.
*
* Usage:
* @include tablet-viewport() {
* :host {
* background-color: green;
* }
* }
*/
@mixin tablet-viewport() {
@media screen and (min-width: 768px) {
@content;
}
}
/**
* A heuristic that applies CSS to mobile
* viewports (i.e. phones, not tablets).
*
* Usage:
* @include mobile-viewport() {
* :host {
* background-color: blue;
* }
* }
*/
@mixin mobile-viewport() {
@media screen and (max-width: 767px) {
@content;
}
}
@mixin input-cover() {
@include position(0, null, null, 0);
@include margin(0);
@@ -217,7 +252,7 @@
$restSelectors: append($restSelectors, $selector, comma);
}
}
// Supported by Chrome.
@if length($hostContextSelectors) > 0 {
@at-root #{$hostContextSelectors} {

View File

@@ -108,7 +108,7 @@ describe('Animation Class', () => {
animation.play();
animation.progressStart();
animation.progressEnd(1);
animation.progressEnd(1, 0);
expect(animation.isRunning()).toEqual(true);
});
@@ -125,9 +125,9 @@ describe('Animation Class', () => {
await animation.play();
animation.progressStart();
animation.progressEnd(0);
animation.progressEnd(0, 0);
await new Promise((resolve) => {
await new Promise<void>((resolve) => {
animation.onFinish(() => {
expect(animation.isRunning()).toEqual(false);
resolve();
@@ -161,8 +161,8 @@ describe('Animation Class', () => {
const el = document.createElement('p');
animation.addElement(el);
animation.addElement(null);
animation.addElement(undefined);
animation.addElement(null as any);
animation.addElement(undefined as any);
expect(animation.elements.length).toEqual(1);
});
@@ -188,8 +188,8 @@ describe('Animation Class', () => {
});
it('should not error when trying to add null or undefined', () => {
animation.addAnimation(null);
animation.addAnimation(undefined);
animation.addAnimation(null as any);
animation.addAnimation(undefined as any);
expect(animation.childAnimations.length).toEqual(0);
});
@@ -312,7 +312,7 @@ describe('Animation Class', () => {
animation.progressStart(true);
expect(animation.getEasing()).toEqual('linear');
animation.progressEnd();
animation.progressEnd(0, 0);
expect(animation.getEasing()).toEqual('ease-in-out');
});
@@ -428,9 +428,15 @@ describe('cubic-bezier conversion', () => {
[1, 1],
];
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.5), [0.16]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.97), [0.56]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.33), [0.11]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.5), [
0.16,
]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.97), [
0.56,
]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.33), [
0.11,
]);
});
it('cubic-bezier(1, 0, 0.68, 0.28)', () => {
@@ -441,9 +447,15 @@ describe('cubic-bezier conversion', () => {
[1, 1],
];
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.08), [0.6]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.5), [0.84]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.94), [0.98]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.08), [
0.6,
]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.5), [
0.84,
]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.94), [
0.98,
]);
});
it('cubic-bezier(0.4, 0, 0.6, 1)', () => {
@@ -454,9 +466,15 @@ describe('cubic-bezier conversion', () => {
[1, 1],
];
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.39), [0.43]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.03), [0.11]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.89), [0.78]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.39), [
0.43,
]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.03), [
0.11,
]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.89), [
0.78,
]);
});
it('cubic-bezier(0, 0, 0.2, 1)', () => {
@@ -467,9 +485,15 @@ describe('cubic-bezier conversion', () => {
[1, 1],
];
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.95), [0.71]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.1), [0.03]);
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 0.7), [0.35]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.95), [
0.71,
]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.1), [
0.03,
]);
shouldApproximatelyEqual(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 0.7), [
0.35,
]);
});
it('cubic-bezier(0.32, 0.72, 0, 1) (with out of bounds progression)', () => {
@@ -480,8 +504,8 @@ describe('cubic-bezier conversion', () => {
[1, 1],
];
expect(getTimeGivenProgression(...equation, 1.32)[0]).toBeUndefined();
expect(getTimeGivenProgression(...equation, -0.32)[0]).toBeUndefined();
expect(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 1.32)[0]).toBeUndefined();
expect(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], -0.32)[0]).toBeUndefined();
});
it('cubic-bezier(0.21, 1.71, 0.88, 0.9) (multiple solutions)', () => {
@@ -492,7 +516,10 @@ describe('cubic-bezier conversion', () => {
[1, 1],
];
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 1.02), [0.35, 0.87]);
shouldApproximatelyEqual(
getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 1.02),
[0.35, 0.87]
);
});
it('cubic-bezier(0.32, 0.72, 0, 1) (with out of bounds progression)', () => {
@@ -503,8 +530,8 @@ describe('cubic-bezier conversion', () => {
[1, 1],
];
expect(getTimeGivenProgression(...equation, 1.32)).toEqual([]);
expect(getTimeGivenProgression(...equation, -0.32)).toEqual([]);
expect(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], 1.32)).toEqual([]);
expect(getTimeGivenProgression(equation[0], equation[1], equation[2], equation[3], -0.32)).toEqual([]);
});
});
});

View File

@@ -16,8 +16,8 @@ const mockVisualViewport = (
win: Window,
visualViewport: any = { width: 320, height: 568 },
layoutViewport = { innerWidth: 320, innerHeight: 568 }
): any => {
win.visualViewport = {
) => {
(win as any).visualViewport = {
width: 320,
height: 568,
offsetTop: 0,
@@ -29,26 +29,32 @@ const mockVisualViewport = (
onscroll: undefined,
};
win.visualViewport = Object.assign(win.visualViewport, visualViewport);
(win as any).visualViewport = Object.assign(win.visualViewport!, visualViewport);
win = Object.assign(win, layoutViewport);
win.dispatchEvent = jest.fn();
const mockDispatchEvent = jest.fn();
win.dispatchEvent = mockDispatchEvent;
trackViewportChanges(win);
return win;
return {
win,
mockDispatchEvent,
};
};
const mockCapacitor = (win: Window) => {
win.Capacitor = {
(win as any).Capacitor = {
isPluginAvailable: () => false,
};
};
const resizeVisualViewport = (win: Window, visualViewport: any = {}) => {
win.visualViewport = Object.assign(win.visualViewport, visualViewport);
(win as any).visualViewport = Object.assign((win as any).visualViewport, visualViewport);
if (win.visualViewport.onresize) {
win.visualViewport.onresize();
if (win.visualViewport!.onresize) {
win.visualViewport!.onresize({} as any);
} else {
trackViewportChanges(win);
}
@@ -87,62 +93,64 @@ describe('Keyboard Assist Tests', () => {
describe('setKeyboardOpen()', () => {
it('should dispatch the keyboard open event on the window', () => {
window.dispatchEvent = jest.fn();
const mockDispatchEvent = jest.fn();
window.dispatchEvent = mockDispatchEvent;
setKeyboardOpen(window);
expect(window.dispatchEvent.mock.calls.length).toEqual(1);
expect(window.dispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
expect(mockDispatchEvent.mock.calls.length).toEqual(1);
expect(mockDispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
});
});
describe('setKeyboardClose()', () => {
it('should dispatch the keyboard close event on the window', () => {
window.dispatchEvent = jest.fn();
const mockDispatchEvent = jest.fn();
window.dispatchEvent = mockDispatchEvent;
setKeyboardClose(window);
expect(window.dispatchEvent.mock.calls.length).toEqual(1);
expect(window.dispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_CLOSE);
expect(mockDispatchEvent.mock.calls.length).toEqual(1);
expect(mockDispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_CLOSE);
});
});
describe('keyboardDidOpen()', () => {
beforeEach(() => {
resetKeyboardAssist(window);
resetKeyboardAssist();
mockVisualViewport(window);
});
it('should return true when visual viewport height < layout viewport height and meets or exceeds the keyboard threshold', () => {
resizeVisualViewport(window, { height: 200 });
expect(keyboardDidOpen(window)).toEqual(true);
expect(keyboardDidOpen()).toEqual(true);
});
it('should return true if the layout and visual viewports resize', () => {
resizeLayoutViewport(window, { width: 320, height: 300 });
resizeVisualViewport(window, { width: 320, height: 300 });
expect(keyboardDidOpen(window)).toEqual(true);
expect(keyboardDidOpen()).toEqual(true);
});
it('should return false when visual viewport height < layout viewport heigh but does not meet the keyboard threshold', () => {
resizeVisualViewport(window, { height: 500 });
expect(keyboardDidOpen(window)).toEqual(false);
expect(keyboardDidOpen()).toEqual(false);
});
it('should return false on orientation change', () => {
resizeVisualViewport(window, { width: 320, height: 250 });
resizeVisualViewport(window, { width: 250, height: 320 });
expect(keyboardDidOpen(window)).toEqual(false);
expect(keyboardDidOpen()).toEqual(false);
});
it('should return false when both the visual and layout viewports change', () => {
resizeVisualViewport(window, { width: 250, height: 320 }, { innerWidth: 250, innerHeight: 320 });
resizeVisualViewport(window, { width: 250, height: 320 });
expect(keyboardDidOpen(window)).toEqual(false);
expect(keyboardDidOpen()).toEqual(false);
});
it('should return true when the keyboard shows even if the user is zoomed in', () => {
@@ -152,13 +160,13 @@ describe('Keyboard Assist Tests', () => {
// User taps input and keyboard appears
resizeVisualViewport(window, { width: 160, height: 184, scale: 2 });
expect(keyboardDidOpen(window)).toEqual(true);
expect(keyboardDidOpen()).toEqual(true);
});
});
describe('keyboardDidClose()', () => {
beforeEach(() => {
resetKeyboardAssist(window);
resetKeyboardAssist();
mockVisualViewport(window);
});
@@ -222,54 +230,66 @@ describe('Keyboard Assist Tests', () => {
});
describe('Keyboard Assist Integration', () => {
let mockDispatchEvent: jest.Mock<any, any>;
beforeEach(() => {
resetKeyboardAssist(window);
mockVisualViewport(window);
resetKeyboardAssist();
mockDispatchEvent = mockVisualViewport(window).mockDispatchEvent;
startKeyboardAssist(window);
});
afterEach(() => {
mockDispatchEvent.mockReset();
});
it('should properly set the keyboard to be open', () => {
resizeVisualViewport(window, { width: 320, height: 350 });
expect(window.dispatchEvent.mock.calls.length).toEqual(1);
expect(window.dispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
expect(mockDispatchEvent.mock.calls.length).toEqual(1);
expect(mockDispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
});
it('should properly set the keyboard to be closed', () => {
resizeVisualViewport(window, { width: 320, height: 350 });
resizeVisualViewport(window, { width: 320, height: 568 });
expect(window.dispatchEvent.mock.calls.length).toEqual(2);
expect(window.dispatchEvent.mock.calls[1][0].type).toEqual(KEYBOARD_DID_CLOSE);
expect(mockDispatchEvent.mock.calls.length).toEqual(2);
expect(mockDispatchEvent.mock.calls[1][0].type).toEqual(KEYBOARD_DID_CLOSE);
});
it('should properly set the keyboard to be resized', () => {
resizeVisualViewport(window, { width: 320, height: 350 });
resizeVisualViewport(window, { width: 320, height: 360 });
expect(window.dispatchEvent.mock.calls.length).toEqual(2);
expect(window.dispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
expect(window.dispatchEvent.mock.calls[1][0].type).toEqual(KEYBOARD_DID_OPEN);
expect(mockDispatchEvent.mock.calls.length).toEqual(2);
expect(mockDispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
expect(mockDispatchEvent.mock.calls[1][0].type).toEqual(KEYBOARD_DID_OPEN);
});
it('should not set keyboard open on orientation change', () => {
resizeVisualViewport(window, { width: 568, height: 320 });
expect(window.dispatchEvent.mock.calls.length).toEqual(0);
expect(mockDispatchEvent.mock.calls.length).toEqual(0);
});
});
describe('Keyboard Assist with Capacitor', () => {
let mockDispatchEvent: jest.Mock<any, any>;
beforeEach(() => {
resetKeyboardAssist(window);
resetKeyboardAssist();
mockCapacitor(window);
mockVisualViewport(window);
mockDispatchEvent = mockVisualViewport(window).mockDispatchEvent;
startKeyboardAssist(window);
});
afterEach(() => {
mockDispatchEvent.mockReset();
});
it('should attach visual viewport listeners when Capacitor is available but the Keyboard plugin is not', () => {
resizeVisualViewport(window, { width: 320, height: 350 });
expect(window.dispatchEvent.mock.calls.length).toEqual(1);
expect(window.dispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
expect(mockDispatchEvent.mock.calls.length).toEqual(1);
expect(mockDispatchEvent.mock.calls[0][0].type).toEqual(KEYBOARD_DID_OPEN);
});
});

View File

@@ -65,7 +65,7 @@ describe('sanitizeDOMString', () => {
});
const enableSanitizer = (enable = true) => {
window.Ionic = {};
window.Ionic.config = {};
window.Ionic.config.sanitizerEnabled = enable;
(window as any).Ionic = {};
(window as any).Ionic.config = {};
(window as any).Ionic.config.sanitizerEnabled = enable;
};

View File

@@ -1,8 +1,8 @@
import { newSpecPage } from '@stencil/core/testing';
import { Item } from '../../components/item/item.tsx';
import { Label } from '../../components/label/label.tsx';
import { Toggle } from '../../components/toggle/toggle.tsx';
import { Item } from '../../components/item/item';
import { Label } from '../../components/label/label';
import { Toggle } from '../../components/toggle/toggle';
import { getAriaLabel } from '../helpers';
describe('getAriaLabel()', () => {
@@ -17,7 +17,7 @@ describe('getAriaLabel()', () => {
`,
});
const toggle = page.body.querySelector('ion-toggle');
const toggle = page.body.querySelector('ion-toggle')!;
const { label, labelId, labelText } = getAriaLabel(toggle, 'ion-tg-0');
@@ -35,7 +35,7 @@ describe('getAriaLabel()', () => {
`,
});
const toggle = page.body.querySelector('ion-toggle');
const toggle = page.body.querySelector('ion-toggle')!;
const { label, labelId, labelText } = getAriaLabel(toggle, 'ion-tg-0');
@@ -53,7 +53,7 @@ describe('getAriaLabel()', () => {
`,
});
const toggle = page.body.querySelector('ion-toggle');
const toggle = page.body.querySelector('ion-toggle')!;
const { labelId, labelText } = getAriaLabel(toggle, 'ion-tg-0');
@@ -70,7 +70,7 @@ describe('getAriaLabel()', () => {
`,
});
const toggle = page.body.querySelector('ion-toggle');
const toggle = page.body.querySelector('ion-toggle')!;
const { labelId, labelText } = getAriaLabel(toggle, 'ion-tg-0');
@@ -87,7 +87,7 @@ describe('getAriaLabel()', () => {
`,
});
const toggle = page.body.querySelector('ion-toggle');
const toggle = page.body.querySelector('ion-toggle')!;
const { labelId, labelText } = getAriaLabel(toggle, 'ion-tg-0');

View File

@@ -1,3 +1,4 @@
import type { BackButtonEvent } from '../../../src/interface';
import { startHardwareBackButton } from '../hardware-back-button';
describe('Hardware Back Button', () => {
@@ -5,7 +6,7 @@ describe('Hardware Back Button', () => {
it('should call handler', () => {
const cbSpy = jest.fn();
document.addEventListener('ionBackButton', (ev) => {
ev.detail.register(0, cbSpy);
(ev as BackButtonEvent).detail.register(0, cbSpy);
});
dispatchBackButtonEvent();
@@ -16,8 +17,8 @@ describe('Hardware Back Button', () => {
const cbSpy = jest.fn();
const cbSpyTwo = jest.fn();
document.addEventListener('ionBackButton', (ev) => {
ev.detail.register(100, cbSpy);
ev.detail.register(99, cbSpyTwo);
(ev as BackButtonEvent).detail.register(100, cbSpy);
(ev as BackButtonEvent).detail.register(99, cbSpyTwo);
});
dispatchBackButtonEvent();
@@ -29,8 +30,8 @@ describe('Hardware Back Button', () => {
const cbSpy = jest.fn();
const cbSpyTwo = jest.fn();
document.addEventListener('ionBackButton', (ev) => {
ev.detail.register(100, cbSpy);
ev.detail.register(100, cbSpyTwo);
(ev as BackButtonEvent).detail.register(100, cbSpy);
(ev as BackButtonEvent).detail.register(100, cbSpyTwo);
});
dispatchBackButtonEvent();
@@ -39,13 +40,13 @@ describe('Hardware Back Button', () => {
});
it('should call multiple callbacks', () => {
const cbSpy = (processNextHandler) => {
const cbSpy = (processNextHandler: () => void) => {
processNextHandler();
};
const cbSpyTwo = jest.fn();
document.addEventListener('ionBackButton', (ev) => {
ev.detail.register(100, cbSpy);
ev.detail.register(99, cbSpyTwo);
(ev as BackButtonEvent).detail.register(100, cbSpy);
(ev as BackButtonEvent).detail.register(99, cbSpyTwo);
});
dispatchBackButtonEvent();

View File

@@ -1,9 +1,8 @@
import { newSpecPage } from '@stencil/core/testing';
import { Modal } from '../../../components/modal/modal';
import { Nav } from '../../../components/nav/nav';
import { RouterOutlet } from '../../../components/router-outlet/router-outlet';
import { Modal } from '../../../components/modal/modal';
import { setRootAriaHidden } from '../../overlays';
describe('setRootAriaHidden()', () => {
@@ -15,7 +14,7 @@ describe('setRootAriaHidden()', () => {
`,
});
const routerOutlet = page.body.querySelector('ion-router-outlet');
const routerOutlet = page.body.querySelector('ion-router-outlet')!;
expect(routerOutlet.hasAttribute('aria-hidden')).toEqual(false);
@@ -34,7 +33,7 @@ describe('setRootAriaHidden()', () => {
`,
});
const nav = page.body.querySelector('ion-nav');
const nav = page.body.querySelector('ion-nav')!;
expect(nav.hasAttribute('aria-hidden')).toEqual(false);
@@ -54,8 +53,8 @@ describe('setRootAriaHidden()', () => {
`,
});
const containerRoot = page.body.querySelector('#ion-view-container-root');
const notContainerRoot = page.body.querySelector('#not-container-root');
const containerRoot = page.body.querySelector('#ion-view-container-root')!;
const notContainerRoot = page.body.querySelector('#not-container-root')!;
expect(containerRoot.hasAttribute('aria-hidden')).toEqual(false);
expect(notContainerRoot.hasAttribute('aria-hidden')).toEqual(false);
@@ -90,8 +89,8 @@ describe('setRootAriaHidden()', () => {
`,
});
const routerOutlet = page.body.querySelector('ion-router-outlet');
const modal = page.body.querySelector('ion-modal');
const routerOutlet = page.body.querySelector('ion-router-outlet')!;
const modal = page.body.querySelector('ion-modal')!;
await modal.present();
@@ -109,9 +108,9 @@ describe('setRootAriaHidden()', () => {
`,
});
const routerOutlet = page.body.querySelector('ion-router-outlet');
const modalOne = page.body.querySelector('ion-modal#one');
const modalTwo = page.body.querySelector('ion-modal#two');
const routerOutlet = page.body.querySelector('ion-router-outlet')!;
const modalOne = page.body.querySelector<HTMLIonModalElement>('ion-modal#one')!;
const modalTwo = page.body.querySelector<HTMLIonModalElement>('ion-modal#two')!;
await modalOne.present();

View File

@@ -1,5 +1,12 @@
export type Mode = 'ios' | 'md';
export type Direction = 'ltr' | 'rtl';
/**
* The theme to use for the playwright test.
*
* - `light`: The fallback theme values. Theme stylesheet will not be included.
* - `dark`: The dark theme values.
*/
export type Theme = 'light' | 'dark';
export type TitleFn = (title: string) => string;
export type ScreenshotFn = (fileName: string) => string;
@@ -7,6 +14,7 @@ export type ScreenshotFn = (fileName: string) => string;
export interface TestConfig {
mode: Mode;
direction: Direction;
theme: Theme;
}
interface TestUtilities {
@@ -18,6 +26,7 @@ interface TestUtilities {
interface TestConfigOption {
modes?: Mode[];
directions?: Direction[];
themes?: Theme[];
}
/**
@@ -27,9 +36,19 @@ interface TestConfigOption {
* each test title is unique.
*/
const generateTitle = (title: string, config: TestConfig): string => {
const { mode, direction } = config;
const { mode, direction, theme } = config;
return `${title} - ${mode}/${direction}`;
if (theme === 'light') {
/**
* Ionic has many existing tests that existed prior to
* the introduction of theme testing. To maintain backwards
* compatibility, we will not include the theme in the test
* title if the theme is set to light.
*/
return `${title} - ${mode}/${direction}`;
}
return `${title} - ${mode}/${direction}/${theme}`;
};
/**
@@ -37,9 +56,19 @@ const generateTitle = (title: string, config: TestConfig): string => {
* and a test config.
*/
const generateScreenshotName = (fileName: string, config: TestConfig): string => {
const { mode, direction } = config;
const { mode, direction, theme } = config;
return `${fileName}-${mode}-${direction}.png`;
if (theme === 'light') {
/**
* Ionic has many existing tests that existed prior to
* the introduction of theme testing. To maintain backwards
* compatibility, we will not include the theme in the screenshot
* name if the theme is set to light.
*/
return `${fileName}-${mode}-${direction}.png`;
}
return `${fileName}-${mode}-${direction}-${theme}.png`;
};
/**
@@ -54,12 +83,15 @@ export const configs = (testConfig: TestConfigOption = DEFAULT_TEST_CONFIG_OPTIO
* If certain options are not provided,
* fall back to the defaults.
*/
const processedMode: Mode[] = modes ?? DEFAULT_MODES;
const processedDirection: Direction[] = directions ?? DEFAULT_DIRECTIONS;
const processedMode = modes ?? DEFAULT_MODES;
const processedDirection = directions ?? DEFAULT_DIRECTIONS;
const processedTheme = testConfig.themes ?? DEFAULT_THEMES;
processedMode.forEach((mode: Mode) => {
processedDirection.forEach((direction: Direction) => {
configs.push({ mode, direction });
processedMode.forEach((mode) => {
processedDirection.forEach((direction) => {
processedTheme.forEach((theme) => {
configs.push({ mode, direction, theme });
});
});
});
@@ -74,6 +106,7 @@ export const configs = (testConfig: TestConfigOption = DEFAULT_TEST_CONFIG_OPTIO
const DEFAULT_MODES: Mode[] = ['ios', 'md'];
const DEFAULT_DIRECTIONS: Direction[] = ['ltr', 'rtl'];
const DEFAULT_THEMES: Theme[] = ['light'];
const DEFAULT_TEST_CONFIG_OPTION = {
modes: DEFAULT_MODES,

View File

@@ -1,5 +1,5 @@
import type { Page, TestInfo } from '@playwright/test';
import type { E2EPageOptions, Mode, Direction } from '@utils/test/playwright';
import type { E2EPageOptions, Mode, Direction, Theme } from '@utils/test/playwright';
/**
* Overwrites the default Playwright page.setContent method.
@@ -19,25 +19,30 @@ export const setContent = async (page: Page, html: string, testInfo: TestInfo, o
let mode: Mode;
let direction: Direction;
let theme: Theme;
if (options == undefined) {
mode = testInfo.project.metadata.mode;
direction = testInfo.project.metadata.rtl ? 'rtl' : 'ltr';
theme = testInfo.project.metadata.theme;
} else {
mode = options.mode;
direction = options.direction;
theme = options.theme;
}
const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL;
const output = `
<!DOCTYPE html>
<html dir="${direction}">
<html dir="${direction}" lang="en">
<head>
<title>Ionic Playwright Test</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
<link href="${baseUrl}/css/ionic.bundle.css" rel="stylesheet" />
<link href="${baseUrl}/scripts/testing/styles.css" rel="stylesheet" />
${theme !== 'light' ? `<link href="${baseUrl}/scripts/testing/themes/${theme}.css" rel="stylesheet" />` : ''}
<script src="${baseUrl}/scripts/testing/scripts.js"></script>
<script type="module" src="${baseUrl}/dist/ionic/ionic.esm.js"></script>
<script>
@@ -54,6 +59,11 @@ export const setContent = async (page: Page, html: string, testInfo: TestInfo, o
</html>
`;
testInfo.annotations.push({
type: 'theme',
description: theme,
});
if (baseUrl) {
await page.route(baseUrl, (route) => {
if (route.request().url() === `${baseUrl}/`) {

View File

@@ -12,7 +12,7 @@ describe('componentOnReady()', () => {
);
const component = document.createElement('hello-world');
componentOnReady(component, (el) => {
componentOnReady(component, (el: HTMLElement) => {
expect(el).toBe(component);
done();
});
@@ -39,7 +39,7 @@ describe('componentOnReady()', () => {
);
const component = document.createElement('hello-world');
componentOnReady(component, (el) => {
componentOnReady(component, (el: HTMLElement) => {
expect(el).toBe(component);
expect(cb).toHaveBeenCalledTimes(1);
done();

View File

@@ -37,9 +37,6 @@
"src",
],
"exclude": [
"node_modules",
"**/test/**/*.spec.ts",
"**/test/**/*.spec.tsx",
"**/test/**/e2e.ts"
"node_modules"
]
}

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