Compare commits

..

1 Commits

Author SHA1 Message Date
Liam DeBeasi
243cf92600 add poc 2024-02-09 10:04:11 -05:00
117 changed files with 2166 additions and 2229 deletions

View File

@@ -93,11 +93,6 @@ The following styles should be set for the CSS to work properly. Note that the `
The activated state should be enabled for elements with actions on "press". It usually changes the opacity or background of an element.
> [!WARNING]
>`:active` should not be used here as it is not received on mobile Safari unless the element has a `touchstart` listener (which we don't necessarily want to have to add to every element). From [Safari Web Content Guide](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/AdjustingtheTextSize/AdjustingtheTextSize.html):
>
>> On iOS, mouse events are sent so quickly that the down or active state is never received. Therefore, the `:active` pseudo state is triggered only when there is a touch event set on the HTML element
> Make sure the component has the correct [component structure](#component-structure) before continuing.
#### JavaScript
@@ -114,7 +109,7 @@ render() {
}
```
Once that is done, the element will get the `ion-activated` class added on press after a small delay. This delay exists so that the active state does not show up when an activatable element is tapped while scrolling.
Once that is done, the element will get the `ion-activated` class added on press.
In addition to setting that class, `ion-activatable-instant` can be set in order to have an instant press with no delay:
@@ -217,13 +212,7 @@ TODO
### Focused
The focused state should be enabled for elements with actions when tabbed to via the keyboard. This will only work inside of an `ion-app`. It usually changes the opacity or background of an element.
> [!WARNING]
> Do not use `:focus` because that will cause the focus to apply even when an element is tapped (because the element is now focused). Instead, we only want the focus state to be shown when it makes sense which is what the `.ion-focusable` utility mentioned below does.
> [!NOTE]
> The [`:focus-visible`](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible) pseudo-class mostly does the same thing as our JavaScript-driven utility. However, it does not work well with Shadow DOM components as the element that receives focus is typically inside of the Shadow DOM, but we usually want to set the `:focus-visible` state on the host so we can style other parts of the component. Using other combinations such as `:has(:focus-visible)` does not work because `:has` does not pierce the Shadow DOM (as that would leak implementation details about the Shadow DOM contents). `:focus-within` does work with the Shadow DOM, but that has the same problem as `:focus` that was mentioned before. Unfortunately, a [`:focus-visible-within` pseudo-class does not exist yet](https://github.com/WICG/focus-visible/issues/151).
The focused state should be enabled for elements with actions when tabbed to via the keyboard. This will only work inside of an `ion-app`. It usually changes the opacity or background of an element.
> Make sure the component has the correct [component structure](#component-structure) before continuing.
@@ -286,10 +275,7 @@ ion-button {
### Hover
The [hover state](https://developer.mozilla.org/en-US/docs/Web/CSS/:hover) happens when a user moves their cursor on top of an element without pressing on it. It should not happen on mobile, only on desktop devices that support hover.
> [!NOTE]
> Some Android devices [incorrectly report their inputs](https://issues.chromium.org/issues/40855702) which can result in certain devices receiving hover events when they should not.
The [hover state](https://developer.mozilla.org/en-US/docs/Web/CSS/:hover) happens when a user moves their cursor on top of an element without pressing on it. It should not happen on mobile, only on desktop devices that support hover.
> Make sure the component has the correct [component structure](#component-structure) before continuing.

View File

@@ -20,10 +20,12 @@ body:
id: affected-versions
attributes:
label: Ionic Framework Version
description: Which version(s) of Ionic Framework does this issue impact? [Ionic Framework 1.x to 6.x are no longer supported](https://ionicframework.com/docs/reference/support#framework-maintenance-and-support-status). For extended support, considering visiting [Ionic's Enterprise offering](https://ionic.io/enterprise).
description: Which version(s) of Ionic Framework does this issue impact? For Ionic Framework 1.x issues, please use https://github.com/ionic-team/ionic-v1. For Ionic Framework 2.x and 3.x issues, please use https://github.com/ionic-team/ionic-v3.
options:
- v4.x
- v5.x
- v6.x
- v7.x
- v8.x (Beta)
- Nightly
multiple: true
validations:
@@ -49,11 +51,11 @@ body:
id: steps-to-reproduce
attributes:
label: Steps to Reproduce
description: Explain the steps required to reproduce this issue.
description: Please explain the steps required to duplicate this issue.
placeholder: |
1. Go to '...'
2. Click on '...'
3. Observe: '...'
1.
2.
3.
validations:
required: true
@@ -61,15 +63,8 @@ body:
id: reproduction-url
attributes:
label: Code Reproduction URL
description: |
Reproduce this issue in a blank [Ionic Framework starter application](https://ionicframework.com/start#basics) or a Stackblitz example.
You can use the Stackblitz button available on any of the [component playgrounds](https://ionicframework.com/docs/components) to open an editable example. Remember to save your changes to obtain a link to copy.
Reproductions cases must be minimal and focused around the specific problem you are experiencing. This is the best way to ensure this issue is triaged quickly. Issues without a code reproduction may be closed if the Ionic Team cannot reproduce the issue you are reporting.
description: Please reproduce this issue in a blank Ionic Framework starter application and provide a link to the repo. Try out our [Getting Started Wizard](https://ionicframework.com/start#basics) to quickly spin up an Ionic Framework starter app. This is the best way to ensure this issue is triaged quickly. Issues without a code reproduction may be closed if the Ionic Team cannot reproduce the issue you are reporting.
placeholder: https://github.com/...
validations:
required: true
- type: textarea
id: ionic-info

20
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/core"
schedule:
interval: "daily"
versioning-strategy: increase
allow:
- dependency-name: "@playwright/test"
- dependency-name: "@axe-core/playwright"
- dependency-name: "@stencil/angular-output-target"
- dependency-name: "@stencil/core"
- dependency-name: "@stencil/react-output-target"
- dependency-name: "@stencil/sass"
- dependency-name: "@stencil/vue-output-target"
- dependency-name: "ionicons"
- dependency-name: "@capacitor/core"
- dependency-name: "@capacitor/keyboard"
- dependency-name: "@capacitor/haptics"
- dependency-name: "@capacitor/status-bar"

View File

@@ -3,7 +3,7 @@ description: 'Build Ionic Angular Server'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18.x

View File

@@ -3,7 +3,7 @@ description: 'Build Ionic Angular'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18.x
- uses: ./.github/workflows/actions/download-archive

View File

@@ -8,8 +8,8 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18.x
@@ -22,7 +22,7 @@ runs:
run: npm i @stencil/core@${{ inputs.stencil-version }}
shell: bash
- name: Build Core
run: npm run build -- --ci --debug --verbose
run: npm run build -- --ci
working-directory: ./core
shell: bash
- uses: ./.github/workflows/actions/upload-archive

View File

@@ -8,8 +8,8 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Install Dependencies

View File

@@ -3,7 +3,7 @@ description: 'Build Ionic React Router'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18.x
- uses: ./.github/workflows/actions/download-archive

View File

@@ -3,7 +3,7 @@ description: 'Build Ionic React'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18.x
- uses: ./.github/workflows/actions/download-archive

View File

@@ -3,7 +3,7 @@ description: 'Builds Ionic Vue Router'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18.x
- uses: ./.github/workflows/actions/download-archive

View File

@@ -3,7 +3,7 @@ description: 'Build Ionic Vue'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18.x
- uses: ./.github/workflows/actions/download-archive

View File

@@ -10,7 +10,7 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v3
with:
name: ${{ inputs.name }}
path: ${{ inputs.path }}

View File

@@ -19,7 +19,7 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18.x
# Provenance requires npm 9.5.0+

View File

@@ -6,7 +6,7 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18
- uses: ./.github/workflows/actions/download-archive

View File

@@ -3,7 +3,7 @@ description: 'Test Core Clean Build'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18.x

View File

@@ -3,7 +3,7 @@ description: 'Test Core Lint'
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Install Dependencies

View File

@@ -13,7 +13,7 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18.x
- uses: ./.github/workflows/actions/download-archive
@@ -82,7 +82,7 @@ runs:
working-directory: ./core
- name: Archive Updated Screenshots
if: inputs.update == 'true' && steps.test-and-update.outputs.hasUpdatedScreenshots == 'true'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: updated-screenshots-${{ inputs.shard }}-${{ inputs.totalShards }}
path: UpdatedScreenshots-${{ inputs.shard }}-${{ inputs.totalShards }}.zip

View File

@@ -6,7 +6,7 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Install Dependencies

View File

@@ -6,7 +6,7 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18.x
- uses: ./.github/workflows/actions/download-archive

View File

@@ -6,7 +6,7 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18.x
- uses: ./.github/workflows/actions/download-archive

View File

@@ -6,7 +6,7 @@ inputs:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18.x
- uses: ./.github/workflows/actions/download-archive

View File

@@ -7,10 +7,10 @@ on:
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18.x
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v3
with:
path: ./artifacts
- name: Extract Archives

View File

@@ -13,7 +13,7 @@ runs:
- name: Create Archive
run: zip -q -r ${{ inputs.output }} ${{ inputs.paths }}
shell: bash
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v3
with:
name: ${{ inputs.name }}
path: ${{ inputs.output }}

View File

@@ -22,7 +22,7 @@ jobs:
build-core:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/build-core
with:
ionicons-version: ${{ inputs.ionicons_npm_release_tag }}
@@ -31,21 +31,21 @@ jobs:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/test-core-clean-build
test-core-lint:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/test-core-lint
test-core-spec:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/test-core-spec
test-core-screenshot:
@@ -62,7 +62,7 @@ jobs:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/test-core-screenshot
with:
shard: ${{ matrix.shard }}
@@ -90,14 +90,14 @@ jobs:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/build-vue
build-vue-router:
needs: [build-vue]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/build-vue-router
test-vue-e2e:
@@ -108,7 +108,7 @@ jobs:
needs: [build-vue, build-vue-router]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/test-vue-e2e
with:
app: ${{ matrix.apps }}
@@ -126,14 +126,14 @@ jobs:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/build-angular
build-angular-server:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/build-angular-server
test-angular-e2e:
@@ -144,7 +144,7 @@ jobs:
needs: [build-angular, build-angular-server]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/test-angular-e2e
with:
app: ${{ matrix.apps }}
@@ -162,14 +162,14 @@ jobs:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/build-react
build-react-router:
needs: [build-react]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/build-react-router
test-react-router-e2e:
@@ -180,7 +180,7 @@ jobs:
needs: [build-react, build-react-router]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/test-react-router-e2e
with:
app: ${{ matrix.apps }}
@@ -202,7 +202,7 @@ jobs:
needs: [build-react, build-react-router]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/test-react-e2e
with:
app: ${{ matrix.apps }}

View File

@@ -14,8 +14,8 @@ jobs:
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
- uses: actions/checkout@v3
- uses: github/codeql-action/init@v2
with:
languages: javascript
- uses: github/codeql-action/analyze@v3
- uses: github/codeql-action/analyze@v2

View File

@@ -9,7 +9,7 @@ jobs:
outputs:
dev-hash: ${{ steps.create-dev-hash.outputs.DEV_HASH }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
# A 1 is required before the timestamp
# as lerna will fail when there is a leading 0
# See https://github.com/lerna/lerna/issues/2840

View File

@@ -12,7 +12,7 @@ jobs:
outputs:
nightly-hash: ${{ steps.create-nightly-hash.outputs.NIGHTLY_HASH }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
# A 1 is required before the timestamp
# as lerna will fail when there is a leading 0
# See https://github.com/lerna/lerna/issues/2840

View File

@@ -22,7 +22,7 @@ jobs:
release-core:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/publish-npm
with:
scope: '@ionic/core'
@@ -48,7 +48,7 @@ jobs:
needs: [release-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Restore @ionic/docs built cache
uses: ./.github/workflows/actions/download-archive
with:
@@ -68,7 +68,7 @@ jobs:
needs: [release-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Restore @ionic/core built cache
uses: ./.github/workflows/actions/download-archive
with:
@@ -95,7 +95,7 @@ jobs:
needs: [release-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Restore @ionic/core built cache
uses: ./.github/workflows/actions/download-archive
with:
@@ -121,7 +121,7 @@ jobs:
needs: [release-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Restore @ionic/core built cache
uses: ./.github/workflows/actions/download-archive
with:
@@ -147,7 +147,7 @@ jobs:
needs: [release-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Restore @ionic/core built cache
uses: ./.github/workflows/actions/download-archive
with:
@@ -168,7 +168,7 @@ jobs:
needs: [release-react]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Restore @ionic/core built cache
uses: ./.github/workflows/actions/download-archive
with:
@@ -194,7 +194,7 @@ jobs:
needs: [release-vue]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Restore @ionic/core built cache
uses: ./.github/workflows/actions/download-archive
with:

View File

@@ -50,7 +50,7 @@ jobs:
needs: [release-ionic]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
token: ${{ secrets.IONITRON_TOKEN }}
fetch-depth: 0
@@ -78,7 +78,7 @@ jobs:
needs: [finalize-release]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
# Pull the latest version of the reference
# branch instead of the revision that triggered
# the workflow otherwise we won't get the commit

View File

@@ -26,7 +26,7 @@ jobs:
build-core-with-stencil-nightly:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/build-core-stencil-prerelease
with:
stencil-version: ${{ inputs.npm_release_tag || 'nightly' }}
@@ -35,21 +35,21 @@ jobs:
needs: [build-core-with-stencil-nightly]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/test-core-clean-build
test-core-lint:
needs: [build-core-with-stencil-nightly]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/test-core-lint
test-core-spec:
needs: [build-core-with-stencil-nightly]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/test-core-spec
with:
stencil-version: ${{ inputs.npm_release_tag || 'nightly' }}
@@ -72,7 +72,7 @@ jobs:
needs: [build-core-with-stencil-nightly]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/test-core-screenshot
with:
shard: ${{ matrix.shard }}
@@ -100,14 +100,14 @@ jobs:
needs: [build-core-with-stencil-nightly]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/build-vue
build-vue-router:
needs: [build-vue]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/build-vue-router
test-vue-e2e:
@@ -118,7 +118,7 @@ jobs:
needs: [build-vue, build-vue-router]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/test-vue-e2e
with:
app: ${{ matrix.apps }}
@@ -136,14 +136,14 @@ jobs:
needs: [build-core-with-stencil-nightly]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/build-angular
build-angular-server:
needs: [build-core-with-stencil-nightly]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/build-angular-server
test-angular-e2e:
@@ -154,7 +154,7 @@ jobs:
needs: [build-angular, build-angular-server]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/test-angular-e2e
with:
app: ${{ matrix.apps }}
@@ -172,14 +172,14 @@ jobs:
needs: [build-core-with-stencil-nightly]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/build-react
build-react-router:
needs: [build-react]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/build-react-router
test-react-router-e2e:
@@ -190,7 +190,7 @@ jobs:
needs: [build-react, build-react-router]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/test-react-router-e2e
with:
app: ${{ matrix.apps }}
@@ -212,7 +212,7 @@ jobs:
needs: [build-react, build-react-router]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/test-react-e2e
with:
app: ${{ matrix.apps }}

View File

@@ -12,7 +12,7 @@ jobs:
build-core:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/build-core
test-core-screenshot:
@@ -33,7 +33,7 @@ jobs:
needs: [build-core]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- uses: ./.github/workflows/actions/test-core-screenshot
with:
shard: ${{ matrix.shard }}
@@ -45,7 +45,7 @@ jobs:
runs-on: ubuntu-latest
needs: [test-core-screenshot]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
# Normally, we could just push with the
# default GITHUB_TOKEN, but that will
# not cause the build workflow

View File

@@ -3,67 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [7.8.0](https://github.com/ionic-team/ionic-framework/compare/v7.7.5...v7.8.0) (2024-03-13)
### Features
* **datetime:** formatOptions property for Datetime ([#29065](https://github.com/ionic-team/ionic-framework/issues/29065)) ([7cdbc1b](https://github.com/ionic-team/ionic-framework/commit/7cdbc1b5ad004e17a7c51363653e0e67f50e6860))
* **searchbar:** autocapitalize, dir, lang, maxlength, and minlength are inherited to native input ([#29098](https://github.com/ionic-team/ionic-framework/issues/29098)) ([a0a77f7](https://github.com/ionic-team/ionic-framework/commit/a0a77f799df0732d9f7182f15866035a3ce5a1eb)), closes [#27606](https://github.com/ionic-team/ionic-framework/issues/27606)
## [7.7.5](https://github.com/ionic-team/ionic-framework/compare/v7.7.4...v7.7.5) (2024-03-13)
### Bug Fixes
* **angular:** add ionNavWillChange and ionNavDidChange types for nav ([#29122](https://github.com/ionic-team/ionic-framework/issues/29122)) ([85b9d5c](https://github.com/ionic-team/ionic-framework/commit/85b9d5c35f626ffc273d220549b0126ddc1f7e4b)), closes [#29114](https://github.com/ionic-team/ionic-framework/issues/29114)
* **checkbox:** set aria-checked of indeterminate checkbox to 'mixed' ([#29115](https://github.com/ionic-team/ionic-framework/issues/29115)) ([b2d636f](https://github.com/ionic-team/ionic-framework/commit/b2d636f14dcd33313f6604cfd4a64b542c831b34))
* **overlay:** do not hide overlay if toast is presented ([#29140](https://github.com/ionic-team/ionic-framework/issues/29140)) ([c0f5e5e](https://github.com/ionic-team/ionic-framework/commit/c0f5e5ebc0c9d45d71e10e09903b00b3ba8e6bba)), closes [#29139](https://github.com/ionic-team/ionic-framework/issues/29139)
## [7.7.4](https://github.com/ionic-team/ionic-framework/compare/v7.7.3...v7.7.4) (2024-03-06)
### Bug Fixes
* **modal:** ariaLabel and role are inherited when set via htmlAttributes ([#29099](https://github.com/ionic-team/ionic-framework/issues/29099)) ([de13633](https://github.com/ionic-team/ionic-framework/commit/de13633a182d963876434db773aa346833f956fd))
## [7.7.3](https://github.com/ionic-team/ionic-framework/compare/v7.7.2...v7.7.3) (2024-02-21)
### Bug Fixes
* **label:** do not grow when in end slot ([#29036](https://github.com/ionic-team/ionic-framework/issues/29036)) ([1fc4b76](https://github.com/ionic-team/ionic-framework/commit/1fc4b76f5940b38fd89e19561d6b4738dfb8ae5d)), closes [#29033](https://github.com/ionic-team/ionic-framework/issues/29033)
* **overlays:** focus is returned to last focus element when focusing toast ([#28950](https://github.com/ionic-team/ionic-framework/issues/28950)) ([2ed0ada](https://github.com/ionic-team/ionic-framework/commit/2ed0ada9237b3f4dbf5959746ce2d1744936eebe)), closes [#28261](https://github.com/ionic-team/ionic-framework/issues/28261)
## [7.7.2](https://github.com/ionic-team/ionic-framework/compare/v7.7.1...v7.7.2) (2024-02-14)
### Bug Fixes
* **overlays:** do not return focus if application has already moved focus manually ([#28850](https://github.com/ionic-team/ionic-framework/issues/28850)) ([a016670](https://github.com/ionic-team/ionic-framework/commit/a016670a8a46e101d23235b17bc8a2081fb992eb)), closes [#28849](https://github.com/ionic-team/ionic-framework/issues/28849)
* **overlays:** ensure that only topmost overlay is announced by screen readers ([#28997](https://github.com/ionic-team/ionic-framework/issues/28997)) ([ba4ba61](https://github.com/ionic-team/ionic-framework/commit/ba4ba6161c1a6c67f7804b07f49c64ac9ad2b14c)), closes [#23472](https://github.com/ionic-team/ionic-framework/issues/23472)
* **popover:** render arrow above backdrop ([#28986](https://github.com/ionic-team/ionic-framework/issues/28986)) ([0a8964d](https://github.com/ionic-team/ionic-framework/commit/0a8964d30c76218fe62f7f4aed4f81df7bb80cd0)), closes [#28985](https://github.com/ionic-team/ionic-framework/issues/28985)
## [7.7.1](https://github.com/ionic-team/ionic-framework/compare/v7.7.0...v7.7.1) (2024-02-07)

View File

@@ -3,66 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [7.8.0](https://github.com/ionic-team/ionic-framework/compare/v7.7.5...v7.8.0) (2024-03-13)
### Features
* **datetime:** formatOptions property for Datetime ([#29065](https://github.com/ionic-team/ionic-framework/issues/29065)) ([7cdbc1b](https://github.com/ionic-team/ionic-framework/commit/7cdbc1b5ad004e17a7c51363653e0e67f50e6860))
* **searchbar:** autocapitalize, dir, lang, maxlength, and minlength are inherited to native input ([#29098](https://github.com/ionic-team/ionic-framework/issues/29098)) ([a0a77f7](https://github.com/ionic-team/ionic-framework/commit/a0a77f799df0732d9f7182f15866035a3ce5a1eb)), closes [#27606](https://github.com/ionic-team/ionic-framework/issues/27606)
## [7.7.5](https://github.com/ionic-team/ionic-framework/compare/v7.7.4...v7.7.5) (2024-03-13)
### Bug Fixes
* **checkbox:** set aria-checked of indeterminate checkbox to 'mixed' ([#29115](https://github.com/ionic-team/ionic-framework/issues/29115)) ([b2d636f](https://github.com/ionic-team/ionic-framework/commit/b2d636f14dcd33313f6604cfd4a64b542c831b34))
* **overlay:** do not hide overlay if toast is presented ([#29140](https://github.com/ionic-team/ionic-framework/issues/29140)) ([c0f5e5e](https://github.com/ionic-team/ionic-framework/commit/c0f5e5ebc0c9d45d71e10e09903b00b3ba8e6bba)), closes [#29139](https://github.com/ionic-team/ionic-framework/issues/29139)
## [7.7.4](https://github.com/ionic-team/ionic-framework/compare/v7.7.3...v7.7.4) (2024-03-06)
### Bug Fixes
* **modal:** ariaLabel and role are inherited when set via htmlAttributes ([#29099](https://github.com/ionic-team/ionic-framework/issues/29099)) ([de13633](https://github.com/ionic-team/ionic-framework/commit/de13633a182d963876434db773aa346833f956fd))
## [7.7.3](https://github.com/ionic-team/ionic-framework/compare/v7.7.2...v7.7.3) (2024-02-21)
### Bug Fixes
* **label:** do not grow when in end slot ([#29036](https://github.com/ionic-team/ionic-framework/issues/29036)) ([1fc4b76](https://github.com/ionic-team/ionic-framework/commit/1fc4b76f5940b38fd89e19561d6b4738dfb8ae5d)), closes [#29033](https://github.com/ionic-team/ionic-framework/issues/29033)
* **overlays:** focus is returned to last focus element when focusing toast ([#28950](https://github.com/ionic-team/ionic-framework/issues/28950)) ([2ed0ada](https://github.com/ionic-team/ionic-framework/commit/2ed0ada9237b3f4dbf5959746ce2d1744936eebe)), closes [#28261](https://github.com/ionic-team/ionic-framework/issues/28261)
## [7.7.2](https://github.com/ionic-team/ionic-framework/compare/v7.7.1...v7.7.2) (2024-02-14)
### Bug Fixes
* **overlays:** do not return focus if application has already moved focus manually ([#28850](https://github.com/ionic-team/ionic-framework/issues/28850)) ([a016670](https://github.com/ionic-team/ionic-framework/commit/a016670a8a46e101d23235b17bc8a2081fb992eb)), closes [#28849](https://github.com/ionic-team/ionic-framework/issues/28849)
* **overlays:** ensure that only topmost overlay is announced by screen readers ([#28997](https://github.com/ionic-team/ionic-framework/issues/28997)) ([ba4ba61](https://github.com/ionic-team/ionic-framework/commit/ba4ba6161c1a6c67f7804b07f49c64ac9ad2b14c)), closes [#23472](https://github.com/ionic-team/ionic-framework/issues/23472)
* **popover:** render arrow above backdrop ([#28986](https://github.com/ionic-team/ionic-framework/issues/28986)) ([0a8964d](https://github.com/ionic-team/ionic-framework/commit/0a8964d30c76218fe62f7f4aed4f81df7bb80cd0)), closes [#28985](https://github.com/ionic-team/ionic-framework/issues/28985)
## [7.7.1](https://github.com/ionic-team/ionic-framework/compare/v7.7.0...v7.7.1) (2024-02-07)

View File

@@ -394,7 +394,6 @@ ion-datetime,prop,dayValues,number | number[] | string | undefined,undefined,fal
ion-datetime,prop,disabled,boolean,false,false,false
ion-datetime,prop,doneText,string,'Done',false,false
ion-datetime,prop,firstDayOfWeek,number,0,false,false
ion-datetime,prop,formatOptions,undefined | { date: DateTimeFormatOptions; time?: DateTimeFormatOptions | undefined; } | { date?: DateTimeFormatOptions | undefined; time: DateTimeFormatOptions; },undefined,false,false
ion-datetime,prop,highlightedDates,((dateIsoString: string) => DatetimeHighlightStyle | undefined) | DatetimeHighlight[] | undefined,undefined,false,false
ion-datetime,prop,hourCycle,"h11" | "h12" | "h23" | "h24" | undefined,undefined,false,false
ion-datetime,prop,hourValues,number | number[] | string | undefined,undefined,false,false
@@ -1158,7 +1157,6 @@ ion-row,shadow
ion-searchbar,scoped
ion-searchbar,prop,animated,boolean,false,false,false
ion-searchbar,prop,autocapitalize,string,undefined,true,false
ion-searchbar,prop,autocomplete,"name" | "email" | "tel" | "url" | "on" | "off" | "honorific-prefix" | "given-name" | "additional-name" | "family-name" | "honorific-suffix" | "nickname" | "username" | "new-password" | "current-password" | "one-time-code" | "organization-title" | "organization" | "street-address" | "address-line1" | "address-line2" | "address-line3" | "address-level4" | "address-level3" | "address-level2" | "address-level1" | "country" | "country-name" | "postal-code" | "cc-name" | "cc-given-name" | "cc-additional-name" | "cc-family-name" | "cc-number" | "cc-exp" | "cc-exp-month" | "cc-exp-year" | "cc-csc" | "cc-type" | "transaction-currency" | "transaction-amount" | "language" | "bday" | "bday-day" | "bday-month" | "bday-year" | "sex" | "tel-country-code" | "tel-national" | "tel-area-code" | "tel-local" | "tel-extension" | "impp" | "photo",'off',false,false
ion-searchbar,prop,autocorrect,"off" | "on",'off',false,false
ion-searchbar,prop,cancelButtonIcon,string,config.get('backButtonIcon', arrowBackSharp) as string,false,false
@@ -1169,8 +1167,6 @@ ion-searchbar,prop,debounce,number | undefined,undefined,false,false
ion-searchbar,prop,disabled,boolean,false,false,false
ion-searchbar,prop,enterkeyhint,"done" | "enter" | "go" | "next" | "previous" | "search" | "send" | undefined,undefined,false,false
ion-searchbar,prop,inputmode,"decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined,undefined,false,false
ion-searchbar,prop,maxlength,number | undefined,undefined,false,false
ion-searchbar,prop,minlength,number | undefined,undefined,false,false
ion-searchbar,prop,mode,"ios" | "md",undefined,false,false
ion-searchbar,prop,name,string,this.inputId,false,false
ion-searchbar,prop,placeholder,string,'Search',false,false

1684
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.8.0",
"version": "7.7.1",
"description": "Base components for Ionic",
"keywords": [
"ionic",
@@ -31,13 +31,13 @@
"loader/"
],
"dependencies": {
"@stencil/core": "^4.12.2",
"@stencil/core": "^4.12.1",
"ionicons": "^7.2.2",
"tslib": "^2.1.0"
},
"devDependencies": {
"@axe-core/playwright": "^4.8.5",
"@capacitor/core": "^5.7.0",
"@axe-core/playwright": "^4.8.4",
"@capacitor/core": "^5.6.0",
"@capacitor/haptics": "^5.0.7",
"@capacitor/keyboard": "^5.0.8",
"@capacitor/status-bar": "^5.0.7",
@@ -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

@@ -15,7 +15,7 @@ import { RouteID, RouterDirection, RouterEventDetail, RouteWrite } from "./compo
import { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/breadcrumb-interface";
import { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface";
import { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface";
import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
import { SpinnerTypes } from "./components/spinner/spinner-configs";
import { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
import { CounterFormatter } from "./components/item/item-interface";
@@ -51,7 +51,7 @@ export { RouteID, RouterDirection, RouterEventDetail, RouteWrite } from "./compo
export { BreadcrumbCollapsedClickEventDetail } from "./components/breadcrumb/breadcrumb-interface";
export { CheckboxChangeEventDetail } from "./components/checkbox/checkbox-interface";
export { ScrollBaseDetail, ScrollDetail } from "./components/content/content-interface";
export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
export { SpinnerTypes } from "./components/spinner/spinner-configs";
export { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
export { CounterFormatter } from "./components/item/item-interface";
@@ -858,10 +858,6 @@ export namespace Components {
* The first day of the week to use for `ion-datetime`. The default value is `0` and represents Sunday.
*/
"firstDayOfWeek": number;
/**
* Formatting options for dates and times. Should include a 'date' and/or 'time' object, each of which is of type [Intl.DateTimeFormatOptions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options).
*/
"formatOptions"?: FormatOptions;
/**
* Used to apply custom text and background colors to specific dates. Can be either an array of objects containing ISO strings and colors, or a callback that receives an ISO string and returns the colors. Only applies to the `date`, `date-time`, and `time-date` presentations, with `preferWheel="false"`.
*/
@@ -1952,6 +1948,9 @@ export namespace Components {
*/
"mode"?: "ios" | "md";
}
interface IonPasswordStrength {
"strength"?: 'weak' | 'medium' | 'strong';
}
interface IonPicker {
/**
* If `true`, the picker will animate.
@@ -2552,10 +2551,6 @@ export namespace Components {
* If `true`, enable searchbar animation.
*/
"animated": boolean;
/**
* Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user. Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`.
*/
"autocapitalize": string;
/**
* Set the input's autocomplete property.
*/
@@ -2600,14 +2595,6 @@ export namespace Components {
* A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`.
*/
"inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
/**
* This attribute specifies the maximum number of characters that the user can enter.
*/
"maxlength"?: number;
/**
* This attribute specifies the minimum number of characters that the user can enter.
*/
"minlength"?: number;
/**
* The mode determines which platform styles to use.
*/
@@ -4051,6 +4038,12 @@ declare global {
prototype: HTMLIonNoteElement;
new (): HTMLIonNoteElement;
};
interface HTMLIonPasswordStrengthElement extends Components.IonPasswordStrength, HTMLStencilElement {
}
var HTMLIonPasswordStrengthElement: {
prototype: HTMLIonPasswordStrengthElement;
new (): HTMLIonPasswordStrengthElement;
};
interface HTMLIonPickerElementEventMap {
"ionPickerDidPresent": void;
"ionPickerWillPresent": void;
@@ -4688,6 +4681,7 @@ declare global {
"ion-nav": HTMLIonNavElement;
"ion-nav-link": HTMLIonNavLinkElement;
"ion-note": HTMLIonNoteElement;
"ion-password-strength": HTMLIonPasswordStrengthElement;
"ion-picker": HTMLIonPickerElement;
"ion-picker-column": HTMLIonPickerColumnElement;
"ion-picker-column-internal": HTMLIonPickerColumnInternalElement;
@@ -5557,10 +5551,6 @@ declare namespace LocalJSX {
* The first day of the week to use for `ion-datetime`. The default value is `0` and represents Sunday.
*/
"firstDayOfWeek"?: number;
/**
* Formatting options for dates and times. Should include a 'date' and/or 'time' object, each of which is of type [Intl.DateTimeFormatOptions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options).
*/
"formatOptions"?: FormatOptions;
/**
* Used to apply custom text and background colors to specific dates. Can be either an array of objects containing ISO strings and colors, or a callback that receives an ISO string and returns the colors. Only applies to the `date`, `date-time`, and `time-date` presentations, with `preferWheel="false"`.
*/
@@ -6626,6 +6616,9 @@ declare namespace LocalJSX {
*/
"mode"?: "ios" | "md";
}
interface IonPasswordStrength {
"strength"?: 'weak' | 'medium' | 'strong';
}
interface IonPicker {
/**
* If `true`, the picker will animate.
@@ -7292,10 +7285,6 @@ declare namespace LocalJSX {
* If `true`, enable searchbar animation.
*/
"animated"?: boolean;
/**
* Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user. Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`.
*/
"autocapitalize": string;
/**
* Set the input's autocomplete property.
*/
@@ -7336,14 +7325,6 @@ declare namespace LocalJSX {
* A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`.
*/
"inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
/**
* This attribute specifies the maximum number of characters that the user can enter.
*/
"maxlength"?: number;
/**
* This attribute specifies the minimum number of characters that the user can enter.
*/
"minlength"?: number;
/**
* The mode determines which platform styles to use.
*/
@@ -8159,6 +8140,7 @@ declare namespace LocalJSX {
"ion-nav": IonNav;
"ion-nav-link": IonNavLink;
"ion-note": IonNote;
"ion-password-strength": IonPasswordStrength;
"ion-picker": IonPicker;
"ion-picker-column": IonPickerColumn;
"ion-picker-column-internal": IonPickerColumnInternal;
@@ -8256,6 +8238,7 @@ declare module "@stencil/core" {
"ion-nav": LocalJSX.IonNav & JSXBase.HTMLAttributes<HTMLIonNavElement>;
"ion-nav-link": LocalJSX.IonNavLink & JSXBase.HTMLAttributes<HTMLIonNavLinkElement>;
"ion-note": LocalJSX.IonNote & JSXBase.HTMLAttributes<HTMLIonNoteElement>;
"ion-password-strength": LocalJSX.IonPasswordStrength & JSXBase.HTMLAttributes<HTMLIonPasswordStrengthElement>;
"ion-picker": LocalJSX.IonPicker & JSXBase.HTMLAttributes<HTMLIonPickerElement>;
"ion-picker-column": LocalJSX.IonPickerColumn & JSXBase.HTMLAttributes<HTMLIonPickerColumnElement>;
"ion-picker-column-internal": LocalJSX.IonPickerColumnInternal & JSXBase.HTMLAttributes<HTMLIonPickerColumnInternalElement>;

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -1,7 +1,7 @@
import type { ComponentInterface } from '@stencil/core';
import { Build, Component, Element, Host, Method, h } from '@stencil/core';
import type { FocusVisibleUtility } from '@utils/focus-visible';
import { shouldUseCloseWatcher } from '@utils/hardware-back-button';
import { shoudUseCloseWatcher } from '@utils/hardware-back-button';
import { printIonWarning } from '@utils/logging';
import { isPlatform } from '@utils/platform';
@@ -36,7 +36,7 @@ export class App implements ComponentInterface {
import('../../utils/input-shims/input-shims').then((module) => module.startInputShims(config, platform));
}
const hardwareBackButtonModule = await import('../../utils/hardware-back-button');
const supportsHardwareBackButtonEvents = isHybrid || shouldUseCloseWatcher();
const supportsHardwareBackButtonEvents = isHybrid || shoudUseCloseWatcher();
if (config.getBoolean('hardwareBackButton', supportsHardwareBackButtonEvents)) {
hardwareBackButtonModule.startHardwareBackButton();
} else {
@@ -44,7 +44,7 @@ export class App implements ComponentInterface {
* If an app sets hardwareBackButton: false and experimentalCloseWatcher: true
* then the close watcher will not be used.
*/
if (shouldUseCloseWatcher()) {
if (shoudUseCloseWatcher()) {
printIonWarning(
'experimentalCloseWatcher was set to `true`, but hardwareBackButton was set to `false`. Both config options must be `true` for the Close Watcher API to be used.'
);

View File

@@ -251,7 +251,6 @@ export class Checkbox implements ComponentInterface {
return (
<Host
aria-checked={indeterminate ? 'mixed' : `${checked}`}
class={createColorClasses(color, {
[mode]: true,
'in-item': hostContext('ion-item', el),

View File

@@ -39,18 +39,3 @@ describe('ion-checkbox: disabled', () => {
expect(checkbox.checked).toBe(false);
});
});
describe('ion-checkbox: indeterminate', () => {
it('should have a mixed value for aria-checked', async () => {
const page = await newSpecPage({
components: [Checkbox],
html: `
<ion-checkbox indeterminate="true">Checkbox</ion-checkbox>
`,
});
const checkbox = page.body.querySelector('ion-checkbox')!;
expect(checkbox.getAttribute('aria-checked')).toBe('mixed');
});
});

View File

@@ -8,7 +8,7 @@ import { getIonMode } from '../../global/ionic-global';
import type { Color } from '../../interface';
import type { DatetimePresentation } from '../datetime/datetime-interface';
import { getToday } from '../datetime/utils/data';
import { getLocalizedDateTime, getLocalizedTime } from '../datetime/utils/format';
import { getMonthAndYear, getMonthDayAndYear, getLocalizedDateTime, getLocalizedTime } from '../datetime/utils/format';
import { getHourCycle } from '../datetime/utils/helpers';
import { parseDate } from '../datetime/utils/parse';
/**
@@ -196,7 +196,7 @@ export class DatetimeButton implements ComponentInterface {
return;
}
const { value, locale, formatOptions, hourCycle, preferWheel, multiple, titleSelectedDatesFormatter } = datetimeEl;
const { value, locale, hourCycle, preferWheel, multiple, titleSelectedDatesFormatter } = datetimeEl;
const parsedValues = this.getParsedDateValues(value);
@@ -225,12 +225,8 @@ export class DatetimeButton implements ComponentInterface {
switch (datetimePresentation) {
case 'date-time':
case 'time-date':
const dateText = getLocalizedDateTime(
locale,
firstParsedDatetime,
formatOptions?.date ?? { month: 'short', day: 'numeric', year: 'numeric' }
);
const timeText = getLocalizedTime(locale, firstParsedDatetime, computedHourCycle, formatOptions?.time);
const dateText = getMonthDayAndYear(locale, firstParsedDatetime);
const timeText = getLocalizedTime(locale, firstParsedDatetime, computedHourCycle);
if (preferWheel) {
this.dateText = `${dateText} ${timeText}`;
} else {
@@ -250,28 +246,20 @@ export class DatetimeButton implements ComponentInterface {
}
this.dateText = headerText;
} else {
this.dateText = getLocalizedDateTime(
locale,
firstParsedDatetime,
formatOptions?.date ?? { month: 'short', day: 'numeric', year: 'numeric' }
);
this.dateText = getMonthDayAndYear(locale, firstParsedDatetime);
}
break;
case 'time':
this.timeText = getLocalizedTime(locale, firstParsedDatetime, computedHourCycle, formatOptions?.time);
this.timeText = getLocalizedTime(locale, firstParsedDatetime, computedHourCycle);
break;
case 'month-year':
this.dateText = getLocalizedDateTime(
locale,
firstParsedDatetime,
formatOptions?.date ?? { month: 'long', year: 'numeric' }
);
this.dateText = getMonthAndYear(locale, firstParsedDatetime);
break;
case 'month':
this.dateText = getLocalizedDateTime(locale, firstParsedDatetime, formatOptions?.time ?? { month: 'long' });
this.dateText = getLocalizedDateTime(locale, firstParsedDatetime, { month: 'long' });
break;
case 'year':
this.dateText = getLocalizedDateTime(locale, firstParsedDatetime, formatOptions?.time ?? { year: 'numeric' });
this.dateText = getLocalizedDateTime(locale, firstParsedDatetime, { year: 'numeric' });
break;
}
};

View File

@@ -244,87 +244,4 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
await expect(page.locator('#time-button')).not.toBeVisible();
});
});
test.describe(title('datetime-button: formatOptions'), () => {
test('should include date and time for presentation date-time', async ({ page }) => {
await page.setContent(
`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" presentation="date-time" value="2023-11-02T01:22:00" locale="en-US"></ion-datetime>
<script>
const datetime = document.querySelector('ion-datetime');
datetime.formatOptions = {
date: {
weekday: "short",
month: "long",
day: "2-digit"
},
time: {
hour: "2-digit",
minute: "2-digit"
}
}
</script>
`,
config
);
await page.locator('.datetime-ready').waitFor();
await expect(page.locator('#date-button')).toContainText('Thu, November 02');
await expect(page.locator('#time-button')).toContainText('01:22 AM');
});
test('should include date for presentation date', async ({ page }) => {
await page.setContent(
`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" presentation="date" value="2023-11-02" locale="en-US"></ion-datetime>
<script>
const datetime = document.querySelector('ion-datetime');
datetime.formatOptions = {
date: {
weekday: "short",
month: "long",
day: "2-digit"
}
}
</script>
`,
config
);
await page.locator('.datetime-ready').waitFor();
await expect(page.locator('#date-button')).toContainText('Thu, November 02');
});
test('should include date and time in same button for preferWheel', async ({ page }) => {
await page.setContent(
`
<ion-datetime-button datetime="datetime"></ion-datetime-button>
<ion-datetime id="datetime" presentation="date-time" value="2023-11-02T01:22:00" locale="en-US" prefer-wheel="true"></ion-datetime>
<script>
const datetime = document.querySelector('ion-datetime');
datetime.formatOptions = {
date: {
weekday: "short",
month: "long",
day: "2-digit"
},
time: {
hour: "2-digit",
minute: "2-digit"
}
}
</script>
`,
config
);
await page.locator('.datetime-ready').waitFor();
await expect(page.locator('ion-datetime-button')).toContainText('Thu, November 02 01:22 AM');
});
});
});

View File

@@ -215,41 +215,8 @@
></ion-datetime>
</ion-popover>
</div>
<div class="grid-item">
<h2>formatOptions</h2>
<ion-item>
<ion-label>Start Date</ion-label>
<ion-datetime-button datetime="format-options" slot="end"></ion-datetime-button>
</ion-item>
<ion-popover arrow="false">
<ion-datetime
id="format-options"
presentation="date-time"
value="2023-11-02T01:22:00"
locale="en-US"
></ion-datetime>
</ion-popover>
</div>
</div>
</ion-content>
</ion-app>
</body>
<script>
const formatOptionsDatetime = document.querySelector('#format-options');
formatOptionsDatetime.formatOptions = {
date: {
weekday: 'short',
month: 'long',
day: '2-digit',
},
time: {
hour: '2-digit',
minute: '2-digit',
},
};
</script>
</html>

View File

@@ -36,16 +36,3 @@ export type DatetimeHighlight = { date: string } & DatetimeHighlightStyle;
export type DatetimeHighlightCallback = (dateIsoString: string) => DatetimeHighlightStyle | undefined;
export type DatetimeHourCycle = 'h11' | 'h12' | 'h23' | 'h24';
/**
* FormatOptions must include date and/or time; it cannot be an empty object
*/
export type FormatOptions =
| {
date: Intl.DateTimeFormatOptions;
time?: Intl.DateTimeFormatOptions;
}
| {
date?: Intl.DateTimeFormatOptions;
time: Intl.DateTimeFormatOptions;
};

View File

@@ -20,7 +20,6 @@ import type {
DatetimeHighlightStyle,
DatetimeHighlightCallback,
DatetimeHourCycle,
FormatOptions,
} from './datetime-interface';
import { isSameDay, warnIfValueOutOfBounds, isBefore, isAfter } from './utils/comparison';
import {
@@ -34,7 +33,7 @@ import {
getTimeColumnsData,
getCombinedDateColumnData,
} from './utils/data';
import { formatValue, getLocalizedDateTime, getLocalizedTime, getMonthAndYear } from './utils/format';
import { formatValue, getLocalizedTime, getMonthAndDay, getMonthAndYear } from './utils/format';
import { isLocaleDayPeriodRTL, isMonthFirstLocale, getNumDaysInMonth, getHourCycle } from './utils/helpers';
import {
calculateHourFromAMPM,
@@ -69,7 +68,6 @@ import {
isNextMonthDisabled,
isPrevMonthDisabled,
} from './utils/state';
import { checkForPresentationFormatMismatch, warnIfTimeZoneProvided } from './utils/validate';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
@@ -173,20 +171,6 @@ export class Datetime implements ComponentInterface {
*/
@Prop() disabled = false;
/**
* Formatting options for dates and times.
* Should include a 'date' and/or 'time' object, each of which is of type [Intl.DateTimeFormatOptions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options).
*
*/
@Prop() formatOptions?: FormatOptions;
@Watch('formatOptions')
protected formatOptionsChanged() {
const { el, formatOptions, presentation } = this;
checkForPresentationFormatMismatch(el, presentation, formatOptions);
warnIfTimeZoneProvided(el, formatOptions);
}
/**
* If `true`, the datetime appears normal but the selected date cannot be changed.
*/
@@ -251,12 +235,6 @@ export class Datetime implements ComponentInterface {
*/
@Prop() presentation: DatetimePresentation = 'date-time';
@Watch('presentation')
protected presentationChanged() {
const { el, formatOptions, presentation } = this;
checkForPresentationFormatMismatch(el, presentation, formatOptions);
}
private get isGridStyle() {
const { presentation, preferWheel } = this;
const hasDatePresentation = presentation === 'date' || presentation === 'date-time' || presentation === 'time-date';
@@ -1379,7 +1357,7 @@ export class Datetime implements ComponentInterface {
};
componentWillLoad() {
const { el, formatOptions, highlightedDates, multiple, presentation, preferWheel } = this;
const { el, highlightedDates, multiple, presentation, preferWheel } = this;
if (multiple) {
if (presentation !== 'date') {
@@ -1404,11 +1382,6 @@ export class Datetime implements ComponentInterface {
}
}
if (formatOptions) {
checkForPresentationFormatMismatch(el, presentation, formatOptions);
warnIfTimeZoneProvided(el, formatOptions);
}
const hourValues = (this.parsedHourValues = convertToArrayOfNumbers(this.hourValues));
const minuteValues = (this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues));
const monthValues = (this.parsedMonthValues = convertToArrayOfNumbers(this.monthValues));
@@ -2381,7 +2354,7 @@ export class Datetime implements ComponentInterface {
}
private renderTimeOverlay() {
const { disabled, hourCycle, isTimePopoverOpen, locale, formatOptions } = this;
const { disabled, hourCycle, isTimePopoverOpen, locale } = this;
const computedHourCycle = getHourCycle(locale, hourCycle);
const activePart = this.getActivePartsWithFallback();
@@ -2416,7 +2389,7 @@ export class Datetime implements ComponentInterface {
}
}}
>
{getLocalizedTime(locale, activePart, computedHourCycle, formatOptions?.time)}
{getLocalizedTime(locale, activePart, computedHourCycle)}
</button>,
<ion-popover
alignment="center"
@@ -2451,7 +2424,7 @@ export class Datetime implements ComponentInterface {
}
private getHeaderSelectedDateText() {
const { activeParts, formatOptions, multiple, titleSelectedDatesFormatter } = this;
const { activeParts, multiple, titleSelectedDatesFormatter } = this;
const isArray = Array.isArray(activeParts);
let headerText: string;
@@ -2466,11 +2439,7 @@ export class Datetime implements ComponentInterface {
}
} else {
// for exactly 1 day selected (multiple set or not), show a formatted version of that
headerText = getLocalizedDateTime(
this.locale,
this.getActivePartsWithFallback(),
formatOptions?.date ?? { weekday: 'short', month: 'short', day: 'numeric' }
);
headerText = getMonthAndDay(this.locale, this.getActivePartsWithFallback());
}
return headerText;

View File

@@ -323,12 +323,7 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
*/
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('datetime: visibility'), () => {
// TODO FW-6015 re-enable on webkit when bug is fixed
test('should reset month/year interface when hiding datetime', async ({ page, skip }) => {
skip.browser(
'webkit',
'This is buggy in a headless Linux environment: https://bugs.webkit.org/show_bug.cgi?id=270358'
);
test('should reset month/year interface when hiding datetime', async ({ page }) => {
await page.setContent(
`
<ion-datetime></ion-datetime>
@@ -570,107 +565,3 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
});
});
});
/**
* This behavior does not differ across
* directions.
*/
configs({ directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('datetime: formatOptions'), () => {
test('should format header and time button', async ({ page }) => {
await page.setContent(
`
<ion-datetime value="2022-02-01T16:30:00">
<span slot="title">Select Date</span>
</ion-datetime>
<script>
const datetime = document.querySelector('ion-datetime');
datetime.formatOptions = {
time: { hour: '2-digit', minute: '2-digit' },
date: { day: '2-digit', month: 'long', era: 'short' },
}
</script>
`,
config
);
await page.locator('.datetime-ready').waitFor();
const headerDate = page.locator('ion-datetime .datetime-selected-date');
await expect(headerDate).toHaveText('February 01 AD');
const timeBody = page.locator('ion-datetime .time-body');
await expect(timeBody).toHaveText('04:30 PM');
});
});
});
/**
* This behavior does not differ across
* modes/directions.
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('datetime: formatOptions misconfiguration errors'), () => {
test('should log a warning if time zone is provided', async ({ page }) => {
const logs: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'warning') {
logs.push(msg.text());
}
});
await page.setContent(
`
<ion-datetime value="2022-02-01T16:30:00" presentation="date">
<span slot="title">Select Date</span>
</ion-datetime>
<script>
const datetime = document.querySelector('ion-datetime');
datetime.formatOptions = {
date: { timeZone: 'UTC' },
}
</script>
`,
config
);
await page.locator('.datetime-ready').waitFor();
expect(logs.length).toBe(1);
expect(logs[0]).toContain(
'[Ionic Warning]: Datetime: "timeZone" and "timeZoneName" are not supported in "formatOptions".'
);
});
test('should log a warning if the required formatOptions are not provided for a presentation', async ({ page }) => {
const logs: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'warning') {
logs.push(msg.text());
}
});
await page.setContent(
`
<ion-datetime value="2022-02-01T16:30:00">
<span slot="title">Select Date</span>
</ion-datetime>
<script>
const datetime = document.querySelector('ion-datetime');
datetime.formatOptions = {}
</script>
`,
config
);
await page.locator('.datetime-ready').waitFor();
expect(logs.length).toBe(1);
expect(logs[0]).toContain(
"[Ionic Warning]: Datetime: The 'date-time' presentation requires either a date or time object (or both) in formatOptions."
);
});
});
});

View File

@@ -308,13 +308,6 @@
</ion-datetime>
</ion-modal>
</div>
<div class="grid-item">
<h2>formatOptions</h2>
<ion-datetime value="2020-03-14T14:23:00.000Z" id="format-options-datetime">
<span slot="title">Select Date</span>
</ion-datetime>
</div>
</div>
</ion-content>
<script>
@@ -410,12 +403,6 @@
app.appendChild(modalElement);
return modalElement;
};
const formatOptions = document.querySelector('#format-options-datetime');
formatOptions.formatOptions = {
time: { hour: '2-digit', minute: '2-digit' },
date: { day: '2-digit', month: 'long', era: 'short' },
};
</script>
</ion-app>
</body>

View File

@@ -1,12 +1,12 @@
import type { DatetimeParts } from '../datetime-interface';
import {
generateDayAriaLabel,
getMonthAndDay,
getFormattedHour,
addTimePadding,
getMonthAndYear,
getLocalizedDayPeriod,
getLocalizedTime,
stripTimeZone,
} from '../utils/format';
describe('generateDayAriaLabel()', () => {
@@ -37,6 +37,24 @@ describe('generateDayAriaLabel()', () => {
});
});
describe('getMonthAndDay()', () => {
it('should return Tue, May 11', () => {
expect(getMonthAndDay('en-US', { month: 5, day: 11, year: 2021 })).toEqual('Tue, May 11');
});
it('should return mar, 11 may', () => {
expect(getMonthAndDay('es-ES', { month: 5, day: 11, year: 2021 })).toEqual('mar, 11 may');
});
it('should return Sat, Apr 1', () => {
expect(getMonthAndDay('en-US', { month: 4, day: 1, year: 2006 })).toEqual('Sat, Apr 1');
});
it('should return sáb, 1 abr', () => {
expect(getMonthAndDay('es-ES', { month: 4, day: 1, year: 2006 })).toEqual('sáb, 1 abr');
});
});
describe('getFormattedHour()', () => {
it('should only add padding if using 24 hour time', () => {
expect(getFormattedHour(1, 'h11')).toEqual('1');
@@ -126,7 +144,6 @@ describe('getLocalizedTime', () => {
expect(getLocalizedTime('en-GB', datetimeParts, 'h12')).toEqual('12:00 am');
});
it('should parse time-only values correctly', () => {
const datetimeParts: Partial<DatetimeParts> = {
hour: 22,
@@ -136,79 +153,4 @@ describe('getLocalizedTime', () => {
expect(getLocalizedTime('en-US', datetimeParts as DatetimeParts, 'h12')).toEqual('10:40 PM');
expect(getLocalizedTime('en-US', datetimeParts as DatetimeParts, 'h23')).toEqual('22:40');
});
it('should use formatOptions', () => {
const datetimeParts: DatetimeParts = {
day: 1,
month: 1,
year: 2022,
hour: 9,
minute: 40,
};
const formatOptions: Intl.DateTimeFormatOptions = {
hour: '2-digit',
minute: '2-digit',
dayPeriod: 'short',
day: '2-digit',
};
// Even though this method is intended to be used for time, the date may be displayed as well when passing formatOptions
expect(getLocalizedTime('en-US', datetimeParts, 'h12', formatOptions)).toEqual('01, 09:40 in the morning');
});
it('should override provided time zone with UTC', () => {
const datetimeParts: DatetimeParts = {
day: 1,
month: 1,
year: 2022,
hour: 9,
minute: 40,
};
const formatOptions: Intl.DateTimeFormatOptions = {
timeZone: 'Australia/Sydney',
timeZoneName: 'long',
hour: 'numeric',
minute: 'numeric',
};
expect(getLocalizedTime('en-US', datetimeParts, 'h12', formatOptions)).toEqual('9:40 AM');
});
it('should not include time zone name', () => {
const datetimeParts: DatetimeParts = {
day: 1,
month: 1,
year: 2022,
hour: 9,
minute: 40,
};
const formatOptions: Intl.DateTimeFormatOptions = {
timeZone: 'America/Los_Angeles',
timeZoneName: 'long',
hour: 'numeric',
minute: 'numeric',
};
expect(getLocalizedTime('en-US', datetimeParts, 'h12', formatOptions)).toEqual('9:40 AM');
});
});
describe('stripTimeZone', () => {
it('should remove the time zone name from the options and set the time zone to UTC', () => {
const formatOptions: Intl.DateTimeFormatOptions = {
timeZone: 'America/Los_Angeles',
timeZoneName: 'long',
hour: 'numeric',
minute: 'numeric',
};
expect(stripTimeZone(formatOptions)).toEqual({
timeZone: 'UTC',
hour: 'numeric',
minute: 'numeric',
});
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -11,33 +11,7 @@ const getFormattedDayPeriod = (dayPeriod?: string) => {
return dayPeriod.toUpperCase();
};
/**
* Including time zone options may lead to the rendered text showing a
* different time from what was selected in the Datetime, which could cause
* confusion.
*/
export const stripTimeZone = (formatOptions: Intl.DateTimeFormatOptions): Intl.DateTimeFormatOptions => {
return {
...formatOptions,
/**
* Setting the time zone to UTC ensures that the value shown is always the
* same as what was selected and safeguards against older Safari bugs with
* Intl.DateTimeFormat.
*/
timeZone: 'UTC',
/**
* We do not want to display the time zone name
*/
timeZoneName: undefined,
};
};
export const getLocalizedTime = (
locale: string,
refParts: DatetimeParts,
hourCycle: DatetimeHourCycle,
formatOptions: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: 'numeric' }
): string => {
export const getLocalizedTime = (locale: string, refParts: DatetimeParts, hourCycle: DatetimeHourCycle): string => {
const timeParts: Pick<DatetimeParts, 'hour' | 'minute'> = {
hour: refParts.hour,
minute: refParts.minute,
@@ -48,7 +22,15 @@ export const getLocalizedTime = (
}
return new Intl.DateTimeFormat(locale, {
...stripTimeZone(formatOptions),
hour: 'numeric',
minute: 'numeric',
/**
* Setting the timeZone to UTC prevents
* new Intl.DatetimeFormat from subtracting
* the user's current timezone offset
* when formatting the time.
*/
timeZone: 'UTC',
/**
* We use hourCycle here instead of hour12 due to:
* https://bugs.chromium.org/p/chromium/issues/detail?id=1347316&q=hour12&can=2
@@ -164,6 +146,17 @@ export const generateDayAriaLabel = (locale: string, today: boolean, refParts: D
return today ? `Today, ${labelString}` : labelString;
};
/**
* Gets the day of the week, month, and day
* Used for the header in MD mode.
*/
export const getMonthAndDay = (locale: string, refParts: DatetimeParts) => {
const date = getNormalizedDate(refParts);
return new Intl.DateTimeFormat(locale, { weekday: 'short', month: 'short', day: 'numeric', timeZone: 'UTC' }).format(
date
);
};
/**
* Given a locale and a date object,
* return a formatted string that includes
@@ -175,6 +168,16 @@ export const getMonthAndYear = (locale: string, refParts: DatetimeParts) => {
return new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric', timeZone: 'UTC' }).format(date);
};
/**
* Given a locale and a date object,
* return a formatted string that includes
* the short month, numeric day, and full year.
* Example: Apr 22, 2021
*/
export const getMonthDayAndYear = (locale: string, refParts: DatetimeParts) => {
return getLocalizedDateTime(locale, refParts, { month: 'short', day: 'numeric', year: 'numeric' });
};
/**
* Given a locale and a date object,
* return a formatted string that includes
@@ -232,7 +235,7 @@ export const getLocalizedDateTime = (
options: Intl.DateTimeFormatOptions
): string => {
const date = getNormalizedDate(refParts);
return getDateTimeFormat(locale, stripTimeZone(options)).format(date);
return getDateTimeFormat(locale, options).format(date);
};
/**

View File

@@ -1,54 +0,0 @@
import { printIonWarning } from '@utils/logging';
import type { DatetimePresentation, FormatOptions } from '../datetime-interface';
/**
* If a time zone is provided in the format options, the rendered text could
* differ from what was selected in the Datetime, which could cause
* confusion.
*/
export const warnIfTimeZoneProvided = (el: HTMLElement, formatOptions?: FormatOptions) => {
if (
formatOptions?.date?.timeZone ||
formatOptions?.date?.timeZoneName ||
formatOptions?.time?.timeZone ||
formatOptions?.time?.timeZoneName
) {
printIonWarning('Datetime: "timeZone" and "timeZoneName" are not supported in "formatOptions".', el);
}
};
export const checkForPresentationFormatMismatch = (
el: HTMLElement,
presentation: DatetimePresentation,
formatOptions?: FormatOptions
) => {
// formatOptions is not required
if (!formatOptions) return;
// If formatOptions is provided, the date and/or time objects are required, depending on the presentation
switch (presentation) {
case 'date':
case 'month-year':
case 'month':
case 'year':
if (formatOptions.date === undefined) {
printIonWarning(`Datetime: The '${presentation}' presentation requires a date object in formatOptions.`, el);
}
break;
case 'time':
if (formatOptions.time === undefined) {
printIonWarning(`Datetime: The 'time' presentation requires a time object in formatOptions.`, el);
}
break;
case 'date-time':
case 'time-date':
if (formatOptions.date === undefined && formatOptions.time === undefined) {
printIonWarning(
`Datetime: The '${presentation}' presentation requires either a date or time object (or both) in formatOptions.`,
el
);
}
break;
}
};

View File

@@ -1,5 +1,5 @@
import type { ComponentInterface } from '@stencil/core';
import { Component, Element, Host, Listen, Prop, State, Watch, forceUpdate, h } from '@stencil/core';
import { Build, Component, Element, Host, Listen, Prop, State, Watch, forceUpdate, h } from '@stencil/core';
import type { AnchorInterface, ButtonInterface } from '@utils/element-interface';
import type { Attributes } from '@utils/helpers';
import { inheritAttributes, raf } from '@utils/helpers';
@@ -358,6 +358,15 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
}
private getFirstInteractive() {
if (Build.isTesting) {
/**
* Pseudo selectors can't be tested in unit tests.
* It will cause an error when running the tests.
*
* TODO: FW-5205 - Remove the build conditional when this is fixed in Stencil
*/
return undefined;
}
const controls = this.el.querySelectorAll<HTMLElement>(
'ion-toggle:not([disabled]), ion-checkbox:not([disabled]), ion-radio:not([disabled]), ion-select:not([disabled])'
);

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: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -29,6 +29,14 @@
overflow: hidden;
}
// TODO(FW-5289): move to :host-context(.item)
// Shouldn't need :not(.item-input) as this was
// only needed because of the specificity with
// :not(.item-legacy)
:host-context(.item:not(.item-input):not(.item-legacy)) {
flex-grow: 1;
}
:host(.ion-color) {
color: current-color(base);
}

View File

@@ -1,27 +0,0 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('label: in item'), () => {
test('should render correctly in an item', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/29033',
});
await page.setContent(
`
<ion-item>
<ion-label slot="start">Start</ion-label>
<ion-label>Default</ion-label>
<ion-label slot="end">End</ion-label>
</ion-item>
`,
config
);
const item = page.locator('ion-item');
await expect(item).toHaveScreenshot(screenshot(`label-item`));
});
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -2,7 +2,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Build, Component, Element, Event, Host, Listen, Method, Prop, State, Watch, h } from '@stencil/core';
import { getTimeGivenProgression } from '@utils/animation/cubic-bezier';
import { GESTURE_CONTROLLER } from '@utils/gesture';
import { shouldUseCloseWatcher } from '@utils/hardware-back-button';
import { shoudUseCloseWatcher } from '@utils/hardware-back-button';
import type { Attributes } from '@utils/helpers';
import { inheritAriaAttributes, assert, clamp, isEndSide as isEnd } from '@utils/helpers';
import { menuController } from '@utils/menu-controller';
@@ -788,7 +788,7 @@ export class Menu implements ComponentInterface, MenuI {
*/
return (
<Host
onKeyDown={shouldUseCloseWatcher() ? null : this.onKeydown}
onKeyDown={shoudUseCloseWatcher() ? null : this.onKeydown}
role="navigation"
aria-label={inheritedAttributes['aria-label'] || 'menu'}
class={{

View File

@@ -345,47 +345,10 @@ export class Modal implements ComponentInterface, OverlayInterface {
}
componentWillLoad() {
const { breakpoints, initialBreakpoint, el, htmlAttributes } = this;
const { breakpoints, initialBreakpoint, el } = this;
const isSheetModal = (this.isSheetModal = breakpoints !== undefined && initialBreakpoint !== undefined);
const attributesToInherit = ['aria-label', 'role'];
this.inheritedAttributes = inheritAttributes(el, attributesToInherit);
/**
* When using a controller modal you can set attributes
* using the htmlAttributes property. Since the above attributes
* need to be inherited inside of the modal, we need to look
* and see if these attributes are being set via htmlAttributes.
*
* We could alternatively move this to componentDidLoad to simplify the work
* here, but we'd then need to make inheritedAttributes a State variable,
* thus causing another render to always happen after the first render.
*/
if (htmlAttributes !== undefined) {
attributesToInherit.forEach((attribute) => {
const attributeValue = htmlAttributes[attribute];
if (attributeValue) {
/**
* If an attribute we need to inherit was
* set using htmlAttributes then add it to
* inheritedAttributes and remove it from htmlAttributes.
* This ensures the attribute is inherited and not
* set on the host.
*
* In this case, if an inherited attribute is set
* on the host element and using htmlAttributes then
* htmlAttributes wins, but that's not a pattern that we recommend.
* The only time you'd need htmlAttributes is when using modalController.
*/
this.inheritedAttributes = {
...this.inheritedAttributes,
[attribute]: htmlAttributes[attribute],
};
delete htmlAttributes[attribute];
}
});
}
this.inheritedAttributes = inheritAttributes(el, ['aria-label', 'role']);
if (isSheetModal) {
this.currentBreakpoint = this.initialBreakpoint;

View File

@@ -0,0 +1,23 @@
import { newSpecPage } from '@stencil/core/testing';
import { Modal } from '../../modal';
describe('modal: a11y', () => {
it('should allow for custom role', async () => {
/**
* Note: This example should not be used in production.
* This only serves to check that `role` can be customized.
*/
const page = await newSpecPage({
components: [Modal],
html: `
<ion-modal role="alertdialog"></ion-modal>
`,
});
const modal = page.body.querySelector('ion-modal')!;
const modalWrapper = modal.shadowRoot!.querySelector('.modal-wrapper')!;
await expect(modalWrapper.getAttribute('role')).toBe('alertdialog');
});
});

View File

@@ -1,39 +0,0 @@
import { h } from '@stencil/core';
import { newSpecPage } from '@stencil/core/testing';
import { Modal } from '../modal';
it('should inherit attributes', async () => {
/**
* Note: This example should not be used in production.
* This only serves to check that `role` can be customized.
*/
const page = await newSpecPage({
components: [Modal],
template: () => <ion-modal overlayIndex={1} aria-label="my label" role="presentation"></ion-modal>,
});
const modal = page.body.querySelector('ion-modal')!;
const contentWrapper = modal.shadowRoot!.querySelector('[part="content"]')!;
expect(contentWrapper.getAttribute('aria-label')).toBe('my label');
expect(contentWrapper.getAttribute('role')).toBe('presentation');
});
it('should inherit attributes when set via htmlAttributes', async () => {
const page = await newSpecPage({
components: [Modal],
template: () => (
<ion-modal overlayIndex={1} htmlAttributes={{ 'aria-label': 'my label', role: 'presentation' }}></ion-modal>
),
});
const modal = page.body.querySelector('ion-modal')!;
const contentWrapper = modal.shadowRoot!.querySelector('[part="content"]')!;
expect(contentWrapper.getAttribute('aria-label')).toBe('my label');
expect(contentWrapper.getAttribute('role')).toBe('presentation');
expect(modal.hasAttribute('aria-label')).toBe(false);
expect(modal.hasAttribute('role')).toBe(false);
});

View File

@@ -0,0 +1,34 @@
import type { ComponentInterface } from '@stencil/core';
import { Component, Host, Prop, h } from '@stencil/core';
const progressBarValue: any = {
'weak': {
value: 0.2,
},
'medium': {
value: 0.6,
},
'strong': {
value: 1,
}
}
@Component({
tag: 'ion-password-strength',
shadow: true
})
export class PasswordStrength implements ComponentInterface {
@Prop() strength?: 'weak' | 'medium' | 'strong';
render() {
// TODO need a mode virtual prop
// TODO need to add colors
const data = this.strength !== undefined ? progressBarValue[this.strength] : undefined;
return (
<Host>
<ion-progress-bar value={data?.value || 0}></ion-progress-bar>
{ this.strength !== undefined && <slot name={this.strength}></slot> }
</Host>
);
}
}

View File

@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Password Strength - Basic</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body class="ion-padding">
<ion-input label="Password" type="password" fill="outline"></ion-input>
<br />
<ion-password-strength>
<div slot="weak">Your password is weak</div>
<div slot="medium">Your password is medium</div>
<div slot="strong">Your password is strong</div>
</ion-password-strength>
<script>
const input = document.querySelector('ion-input');
const ps = document.querySelector('ion-password-strength');
input.addEventListener('ionInput', () => {
const val = input.value;
if (val.length === 0) {
ps.strength = undefined;
} else if (val.length < 4) {
ps.strength = 'weak';
} else if (val.length < 8) {
ps.strength = 'medium';
} else {
ps.strength = 'strong';
}
});
</script>
</body>
</html>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -299,14 +299,8 @@ export class Range implements ComponentInterface {
el: rangeSlider,
gestureName: 'range',
gesturePriority: 100,
/**
* Provide a threshold since the drag movement
* might be a user scrolling the view.
* If this is true, then the range
* should not move.
*/
threshold: 10,
onStart: () => this.onStart(),
threshold: 0,
onStart: (ev) => this.onStart(ev),
onMove: (ev) => this.onMove(ev),
onEnd: (ev) => this.onEnd(ev),
});
@@ -424,101 +418,42 @@ export class Range implements ComponentInterface {
this.ionChange.emit({ value: this.value });
}
/**
* The value should be updated on touch end or
* when the component is being dragged.
* This follows the native behavior of mobile devices.
*
* For example: When the user lifts their finger from the
* screen after tapping the bar or dragging the bar or knob.
*/
private onStart() {
this.ionKnobMoveStart.emit({ value: this.ensureValueInBounds(this.value) });
}
/**
* The value should be updated while dragging the
* bar or knob.
*
* While the user is dragging, the view
* should not scroll. This is to prevent the user from
* feeling disoriented while dragging.
*
* The user can scroll on the view if the knob or
* bar is not being dragged.
*
* @param detail The details of the gesture event.
*/
private onMove(detail: GestureDetail) {
const { contentEl, pressedKnob } = this;
const currentX = detail.currentX;
/**
* Since the user is dragging on the bar or knob, the view should not scroll.
*
* This only needs to be done once.
*/
if (contentEl && this.initialContentScrollY === undefined) {
private onStart(detail: GestureDetail) {
const { contentEl } = this;
if (contentEl) {
this.initialContentScrollY = disableContentScrollY(contentEl);
}
/**
* The `pressedKnob` can be undefined if the user just
* started dragging the knob.
*
* This is necessary to determine which knob the user is dragging,
* especially when it's a dual knob.
* Plus, it determines when to apply certain styles.
*
* This only needs to be done once since the knob won't change
* while the user is dragging.
*/
if (pressedKnob === undefined) {
this.setPressedKnob(currentX);
const rect = (this.rect = this.rangeSlider!.getBoundingClientRect() as any);
const currentX = detail.currentX;
// figure out which knob they started closer to
let ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
if (isRTL(this.el)) {
ratio = 1 - ratio;
}
this.update(currentX);
}
this.pressedKnob = !this.dualKnobs || Math.abs(this.ratioA - ratio) < Math.abs(this.ratioB - ratio) ? 'A' : 'B';
/**
* The value should be updated on touch end:
* - When the user lifts their finger from the screen after
* tapping the bar.
*
* @param detail The details of the gesture or mouse event.
*/
private onEnd(detail: GestureDetail | MouseEvent) {
const { contentEl, initialContentScrollY } = this;
const currentX = (detail as GestureDetail).currentX || (detail as MouseEvent).clientX;
/**
* The `pressedKnob` can be undefined if the user never
* dragged the knob. They just tapped on the bar.
*
* This is necessary to determine which knob the user is changing,
* especially when it's a dual knob.
* Plus, it determines when to apply certain styles.
*/
if (this.pressedKnob === undefined) {
this.setPressedKnob(currentX);
}
/**
* The user is no longer dragging the bar or
* knob (if they were dragging it).
*
* The user can now scroll on the view in the next gesture event.
*/
if (contentEl && initialContentScrollY !== undefined) {
resetContentScrollY(contentEl, initialContentScrollY);
}
this.setFocus(this.pressedKnob);
// update the active knob's position
this.update(currentX);
/**
* Reset the pressed knob to undefined since the user
* may start dragging a different knob in the next gesture event.
*/
this.ionKnobMoveStart.emit({ value: this.ensureValueInBounds(this.value) });
}
private onMove(detail: GestureDetail) {
this.update(detail.currentX);
}
private onEnd(detail: GestureDetail) {
const { contentEl, initialContentScrollY } = this;
if (contentEl) {
resetContentScrollY(contentEl, initialContentScrollY);
}
this.update(detail.currentX);
this.pressedKnob = undefined;
this.emitValueChange();
@@ -550,19 +485,6 @@ export class Range implements ComponentInterface {
this.updateValue();
}
private setPressedKnob(currentX: number) {
const rect = (this.rect = this.rangeSlider!.getBoundingClientRect() as any);
// figure out which knob they started closer to
let ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
if (isRTL(this.el)) {
ratio = 1 - ratio;
}
this.pressedKnob = !this.dualKnobs || Math.abs(this.ratioA - ratio) < Math.abs(this.ratioB - ratio) ? 'A' : 'B';
this.setFocus(this.pressedKnob);
}
private get valA() {
return ratioToValue(this.ratioA, this.min, this.max, this.step);
}
@@ -877,39 +799,7 @@ Developers can dismiss this warning by removing their usage of the "legacy" prop
}
return (
<div
class="range-slider"
ref={(rangeEl) => (this.rangeSlider = rangeEl)}
/**
* Since the gesture has a threshold, the value
* won't change until the user has dragged past
* the threshold. This is to prevent the range
* from moving when the user is scrolling.
*
* This results in the value not being updated
* and the event emitters not being triggered
* if the user taps on the range. This is why
* we need to listen for the "pointerUp" event.
*/
onPointerUp={(ev: PointerEvent) => {
/**
* If the user drags the knob on the web
* version (does not occur on mobile),
* the "pointerUp" event will be triggered
* along with the gesture's events.
* This leads to duplicate events.
*
* By checking if the pressedKnob is undefined,
* we can determine if the "pointerUp" event was
* triggered by a tap or a drag. If it was
* dragged, the pressedKnob will be defined.
*/
if (this.pressedKnob === undefined) {
this.onStart();
this.onEnd(ev);
}
}}
>
<div class="range-slider" ref={(rangeEl) => (this.rangeSlider = rangeEl)}>
{ticks.map((tick) => (
<div
style={tickStyle(tick)}

View File

@@ -67,39 +67,6 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
expect(rangeEnd).toHaveReceivedEventDetail({ value: 21 });
});
test('should emit end event on tap', async ({ page }, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/28487',
});
await page.setContent(`<ion-range aria-label="Range" value="20"></ion-range>`, config);
const range = page.locator('ion-range');
const rangeEndSpy = await page.spyOnEvent('ionKnobMoveEnd');
const rangeBoundingBox = await range.boundingBox();
/**
* Coordinates for the click event.
* These need to be near the end of the range
* (or anything that isn't the current value).
*
* The number 50 is arbitrary, but it should be
* less than the width of the range.
*/
const x = rangeBoundingBox!.width - 50;
// The y coordinate is the middle of the range.
const y = rangeBoundingBox!.height / 2;
// Click near the end of the range.
await range.click({
position: { x, y },
});
await rangeEndSpy.next();
expect(rangeEndSpy.length).toBe(1);
});
// TODO FW-2873
test.skip('should not scroll when the knob is swiped', async ({ page, skip }) => {
skip.browser('webkit', 'mouse.wheel is not available in WebKit');

View File

@@ -12,12 +12,6 @@
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
app-reorder {
display: block;
}
</style>
</head>
<body>

View File

@@ -1,7 +1,6 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Method, Prop, State, Watch, forceUpdate, h } from '@stencil/core';
import { debounceEvent, raf, componentOnReady, inheritAttributes } from '@utils/helpers';
import type { Attributes } from '@utils/helpers';
import { debounceEvent, raf, componentOnReady } from '@utils/helpers';
import { isRTL } from '@utils/rtl';
import { createColorClasses } from '@utils/theme';
import { arrowBackSharp, closeCircle, closeSharp, searchOutline, searchSharp } from 'ionicons/icons';
@@ -29,7 +28,6 @@ export class Searchbar implements ComponentInterface {
private shouldAlignLeft = true;
private originalIonInput?: EventEmitter<SearchbarInputEventDetail>;
private inputId = `ion-searchbar-${searchbarIds++}`;
private inheritedAttributes: Attributes = {};
/**
* The value of the input when the textarea is focused.
@@ -41,31 +39,6 @@ export class Searchbar implements ComponentInterface {
@State() focused = false;
@State() noAnimate = true;
/**
* lang and dir are globally enumerated attributes.
* As a result, creating these as properties
* can have unintended side effects. Instead, we
* listen for attribute changes and inherit them
* to the inner `<input>` element.
*/
@Watch('lang')
onLangChanged(newValue: string) {
this.inheritedAttributes = {
...this.inheritedAttributes,
lang: newValue,
};
forceUpdate(this);
}
@Watch('dir')
onDirChanged(newValue: string) {
this.inheritedAttributes = {
...this.inheritedAttributes,
dir: newValue,
};
forceUpdate(this);
}
/**
* The color to use from your application's color palette.
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
@@ -78,27 +51,6 @@ export class Searchbar implements ComponentInterface {
*/
@Prop() animated = false;
/**
* Prior to the addition of this property
* autocapitalize was enabled by default on iOS
* and disabled by default on Android
* for Searchbar. The autocapitalize type on HTMLElement
* requires that it be a string and never undefined.
* However, setting it to a string value would be a breaking change
* in behavior, so we use "!" to tell TypeScript that this property
* is always defined so we can rely on the browser defaults. Browsers
* will automatically set a default value if the developer does not set one.
*
* In the future, this property will default to "off" to align with
* Input and Textarea, and the "!" will not be needed.
*/
/**
* Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user.
* Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`.
*/
@Prop() autocapitalize!: string;
/**
* Set the input's autocomplete property.
*/
@@ -160,16 +112,6 @@ export class Searchbar implements ComponentInterface {
*/
@Prop() enterkeyhint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send';
/**
* This attribute specifies the maximum number of characters that the user can enter.
*/
@Prop() maxlength?: number;
/**
* This attribute specifies the minimum number of characters that the user can enter.
*/
@Prop() minlength?: number;
/**
* If used in a form, set the name of the control, which is submitted with the form data.
*/
@@ -290,12 +232,6 @@ export class Searchbar implements ComponentInterface {
this.emitStyle();
}
componentWillLoad() {
this.inheritedAttributes = {
...inheritAttributes(this.el, ['lang', 'dir']),
};
}
componentDidLoad() {
this.originalIonInput = this.ionInput;
this.positionElements();
@@ -678,16 +614,12 @@ export class Searchbar implements ComponentInterface {
onChange={this.onChange}
onBlur={this.onBlur}
onFocus={this.onFocus}
minLength={this.minlength}
maxLength={this.maxlength}
placeholder={this.placeholder}
type={this.type}
value={this.getValue()}
autoCapitalize={this.autocapitalize}
autoComplete={this.autocomplete}
autoCorrect={this.autocorrect}
spellcheck={this.spellcheck}
{...this.inheritedAttributes}
/>
{mode === 'md' && cancelButton}

View File

@@ -3,37 +3,13 @@ import { newSpecPage } from '@stencil/core/testing';
import { Searchbar } from '../searchbar';
describe('searchbar: rendering', () => {
it('should inherit properties on load', async () => {
it('should inherit attributes', async () => {
const page = await newSpecPage({
components: [Searchbar],
html: '<ion-searchbar autocapitalize="off" maxlength="4" minlength="2" name="search"></ion-searchbar>',
html: '<ion-searchbar name="search"></ion-searchbar>',
});
const nativeEl = page.body.querySelector('ion-searchbar input')!;
expect(nativeEl.getAttribute('name')).toBe('search');
expect(nativeEl.getAttribute('maxlength')).toBe('4');
expect(nativeEl.getAttribute('minlength')).toBe('2');
expect(nativeEl.getAttribute('autocapitalize')).toBe('off');
});
it('should inherit watched attributes', async () => {
const page = await newSpecPage({
components: [Searchbar],
html: '<ion-searchbar dir="ltr" lang="en-US"></ion-searchbar>',
});
const searchbarEl = page.body.querySelector('ion-searchbar')!;
const nativeEl = searchbarEl.querySelector('input')!;
expect(nativeEl.getAttribute('lang')).toBe('en-US');
expect(nativeEl.getAttribute('dir')).toBe('ltr');
searchbarEl.setAttribute('lang', 'es-ES');
searchbarEl.setAttribute('dir', 'rtl');
await page.waitForChanges();
expect(nativeEl.getAttribute('lang')).toBe('es-ES');
expect(nativeEl.getAttribute('dir')).toBe('rtl');
});
});

View File

@@ -2,7 +2,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Method, Prop, State, Watch, h, forceUpdate } from '@stencil/core';
import type { LegacyFormController, NotchController } from '@utils/forms';
import { compareOptions, createLegacyFormController, createNotchController, isOptionSelected } from '@utils/forms';
import { findItemLabel, focusVisibleElement, getAriaLabel, renderHiddenInput, inheritAttributes } from '@utils/helpers';
import { findItemLabel, focusElement, getAriaLabel, renderHiddenInput, inheritAttributes } from '@utils/helpers';
import type { Attributes } from '@utils/helpers';
import { printIonWarning } from '@utils/logging';
import { actionSheetController, alertController, popoverController } from '@utils/overlays';
@@ -329,7 +329,7 @@ export class Select implements ComponentInterface {
);
if (selectedItem) {
focusVisibleElement(selectedItem);
focusElement(selectedItem);
/**
* Browsers such as Firefox do not
@@ -355,7 +355,7 @@ export class Select implements ComponentInterface {
'ion-radio:not(.radio-disabled), ion-checkbox:not(.checkbox-disabled)'
);
if (firstEnabledOption) {
focusVisibleElement(firstEnabledOption.closest('ion-item')!);
focusElement(firstEnabledOption.closest('ion-item')!);
/**
* Focus the option for the same reason as we do above.

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -30,7 +30,7 @@ interface HandlerRegister {
* moment this file is evaluated which could be
* before the config is set.
*/
export const shouldUseCloseWatcher = () =>
export const shoudUseCloseWatcher = () =>
config.get('experimentalCloseWatcher', false) && win !== undefined && 'CloseWatcher' in win;
/**
@@ -109,7 +109,7 @@ export const startHardwareBackButton = () => {
* backbutton event otherwise we may get duplicate
* events firing.
*/
if (shouldUseCloseWatcher()) {
if (shoudUseCloseWatcher()) {
let watcher: CloseWatcher | undefined;
const configureWatcher = () => {

View File

@@ -262,7 +262,7 @@ export const findItemLabel = (componentEl: HTMLElement): HTMLIonLabelElement | n
return null;
};
export const focusVisibleElement = (el: HTMLElement) => {
export const focusElement = (el: HTMLElement) => {
el.focus();
/**

View File

@@ -1,6 +1,6 @@
import { doc } from '@utils/browser';
import type { BackButtonEvent } from '@utils/hardware-back-button';
import { shouldUseCloseWatcher } from '@utils/hardware-back-button';
import { shoudUseCloseWatcher } from '@utils/hardware-back-button';
import { config } from '../global/config';
import { getIonMode } from '../global/ionic-global';
@@ -22,13 +22,7 @@ import type {
import { CoreDelegate } from './framework-delegate';
import { OVERLAY_BACK_BUTTON_PRIORITY } from './hardware-back-button';
import {
addEventListener,
componentOnReady,
focusVisibleElement,
getElementRoot,
removeEventListener,
} from './helpers';
import { addEventListener, componentOnReady, focusElement, getElementRoot, removeEventListener } from './helpers';
import { printIonWarning } from './logging';
let lastOverlayIndex = 0;
@@ -137,55 +131,38 @@ export const createOverlay = <T extends HTMLIonOverlayElement>(
*/
const focusableQueryString =
'[tabindex]:not([tabindex^="-"]):not([hidden]):not([disabled]), input:not([type=hidden]):not([tabindex^="-"]):not([hidden]):not([disabled]), textarea:not([tabindex^="-"]):not([hidden]):not([disabled]), button:not([tabindex^="-"]):not([hidden]):not([disabled]), select:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable[disabled="false"]:not([tabindex^="-"]):not([hidden])';
const isOverlayHidden = (overlay: Element) => overlay.classList.contains('overlay-hidden');
/**
* Focuses the first descendant in an overlay
* that can receive focus. If none exists,
* the entire overlay will be focused.
*/
export const focusFirstDescendant = (ref: Element, overlay: HTMLIonOverlayElement) => {
const firstInput = ref.querySelector(focusableQueryString) as HTMLElement | null;
let firstInput = ref.querySelector(focusableQueryString) as HTMLElement | null;
focusElementInOverlay(firstInput, overlay);
};
/**
* Focuses the last descendant in an overlay
* that can receive focus. If none exists,
* the entire overlay will be focused.
*/
const focusLastDescendant = (ref: Element, overlay: HTMLIonOverlayElement) => {
const inputs = Array.from(ref.querySelectorAll(focusableQueryString)) as HTMLElement[];
const lastInput = inputs.length > 0 ? inputs[inputs.length - 1] : null;
focusElementInOverlay(lastInput, overlay);
};
/**
* Focuses a particular element in an overlay. If the element
* doesn't have anything focusable associated with it then
* the overlay itself will be focused.
* This should be used instead of the focus() method
* on most elements because the focusable element
* may not be the host element.
*
* For example, if an ion-button should be focused
* then we should actually focus the native <button>
* element inside of ion-button's shadow root, not
* the host element itself.
*/
const focusElementInOverlay = (hostToFocus: HTMLElement | null | undefined, overlay: HTMLIonOverlayElement) => {
let elementToFocus = hostToFocus;
const shadowRoot = hostToFocus?.shadowRoot;
const shadowRoot = firstInput?.shadowRoot;
if (shadowRoot) {
// If there are no inner focusable elements, just focus the host element.
elementToFocus = shadowRoot.querySelector<HTMLElement>(focusableQueryString) || hostToFocus;
firstInput = shadowRoot.querySelector(focusableQueryString) || firstInput;
}
if (elementToFocus) {
focusVisibleElement(elementToFocus);
if (firstInput) {
focusElement(firstInput);
} else {
// Focus overlay instead of letting focus escape
overlay.focus();
}
};
const isOverlayHidden = (overlay: Element) => overlay.classList.contains('overlay-hidden');
const focusLastDescendant = (ref: Element, overlay: HTMLIonOverlayElement) => {
const inputs = Array.from(ref.querySelectorAll(focusableQueryString)) as HTMLElement[];
let lastInput = inputs.length > 0 ? inputs[inputs.length - 1] : null;
const shadowRoot = lastInput?.shadowRoot;
if (shadowRoot) {
// If there are no inner focusable elements, just focus the host element.
lastInput = shadowRoot.querySelector(focusableQueryString) || lastInput;
}
if (lastInput) {
lastInput.focus();
} else {
// Focus overlay instead of letting focus escape
overlay.focus();
@@ -242,20 +219,6 @@ const trapKeyboardFocus = (ev: Event, doc: Document) => {
*/
if (lastOverlay === target) {
lastOverlay.lastFocus = undefined;
/**
* Toasts can be presented from an overlay.
* However, focus should still be returned to
* the overlay when clicking a toast. Normally,
* focus would be returned to the last focusable
* descendant in the overlay which may not always be
* the button that the toast was presented from. In this case,
* the focus may be returned to an unexpected element.
* To account for this, we make sure to return focus to the
* last focused element in the overlay if focus is
* moved to the toast.
*/
} else if (target.tagName === 'ION-TOAST') {
focusElementInOverlay(lastOverlay.lastFocus, lastOverlay);
/**
* Otherwise, we must be focusing an element
@@ -332,20 +295,6 @@ const trapKeyboardFocus = (ev: Event, doc: Document) => {
*/
if (lastOverlay.contains(target)) {
lastOverlay.lastFocus = target;
/**
* Toasts can be presented from an overlay.
* However, focus should still be returned to
* the overlay when clicking a toast. Normally,
* focus would be returned to the last focusable
* descendant in the overlay which may not always be
* the button that the toast was presented from. In this case,
* the focus may be returned to an unexpected element.
* To account for this, we make sure to return focus to the
* last focused element in the overlay if focus is
* moved to the toast.
*/
} else if (target.tagName === 'ION-TOAST') {
focusElementInOverlay(lastOverlay.lastFocus, lastOverlay);
} else {
/**
* Otherwise, we are about to have focus
@@ -428,7 +377,7 @@ const connectListeners = (doc: Document) => {
* this behavior will be handled via the ionBackButton
* event.
*/
if (!shouldUseCloseWatcher()) {
if (!shoudUseCloseWatcher()) {
doc.addEventListener('keydown', (ev) => {
if (ev.key === 'Escape') {
const lastOverlay = getPresentedOverlay(doc);
@@ -541,7 +490,6 @@ export const present = async <OverlayPresentOptions>(
}
setRootAriaHidden(true);
hideOverlaysFromScreenReaders(overlay.el);
overlay.presented = true;
overlay.willPresent.emit();
@@ -568,7 +516,7 @@ export const present = async <OverlayPresentOptions>(
* from returning focus as a result.
*/
if (overlay.el.tagName !== 'ION-TOAST') {
restoreElementFocus(overlay.el);
focusPreviousElementOnDismiss(overlay.el);
}
/**
@@ -580,15 +528,6 @@ export const present = async <OverlayPresentOptions>(
if (overlay.keyboardClose && (document.activeElement === null || !overlay.el.contains(document.activeElement))) {
overlay.el.focus();
}
/**
* If this overlay was previously dismissed without being
* the topmost one (such as by manually calling dismiss()),
* it would still have aria-hidden on being presented again.
* Removing it here ensures the overlay is visible to screen
* readers.
*/
overlay.el.removeAttribute('aria-hidden');
};
/**
@@ -601,7 +540,7 @@ export const present = async <OverlayPresentOptions>(
* to where they were before they
* opened the overlay.
*/
const restoreElementFocus = async (overlayEl: any) => {
const focusPreviousElementOnDismiss = async (overlayEl: any) => {
let previousElement = document.activeElement as HTMLElement | null;
if (!previousElement) {
return;
@@ -614,34 +553,7 @@ const restoreElementFocus = async (overlayEl: any) => {
}
await overlayEl.onDidDismiss();
/**
* After onDidDismiss, the overlay loses focus
* because it is removed from the document
*
* > An element will also lose focus [...]
* > if the element is removed from the document)
*
* https://developer.mozilla.org/en-US/docs/Web/API/Element/blur_event
*
* Additionally, `document.activeElement` returns:
*
* > The Element which currently has focus,
* > `<body>` or null if there is
* > no focused element.
*
* https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement#value
*
* However, if the user has already focused
* an element sometime between onWillDismiss
* and onDidDismiss (for example, focusing a
* text box after tapping a button in an
* action sheet) then don't restore focus to
* previous element
*/
if (document.activeElement === null || document.activeElement === document.body) {
previousElement.focus();
}
previousElement.focus();
};
export const dismiss = async <OverlayDismissOptions>(
@@ -713,9 +625,6 @@ export const dismiss = async <OverlayDismissOptions>(
}
overlay.el.remove();
revealOverlaysToScreenReaders();
return true;
};
@@ -951,65 +860,3 @@ export const createTriggerController = () => {
removeClickListener,
};
};
/**
* Ensure that underlying overlays have aria-hidden if necessary so that screen readers
* cannot move focus to these elements. Note that we cannot rely on focus/focusin/focusout
* events here because those events do not fire when the screen readers moves to a non-focusable
* element such as text.
* Without this logic screen readers would be able to move focus outside of the top focus-trapped overlay.
*
* @param newTopMostOverlay - The overlay that is being presented. Since the overlay has not been
* fully presented yet at the time this function is called it will not be included in the getPresentedOverlays result.
*/
const hideOverlaysFromScreenReaders = (newTopMostOverlay: HTMLIonOverlayElement) => {
if (doc === undefined) return;
const overlays = getPresentedOverlays(doc);
for (let i = overlays.length - 1; i >= 0; i--) {
const presentedOverlay = overlays[i];
const nextPresentedOverlay = overlays[i + 1] ?? newTopMostOverlay;
/**
* If next overlay has aria-hidden then all remaining overlays will have it too.
* Or, if the next overlay is a Toast that does not have aria-hidden then current overlay
* should not have aria-hidden either so focus can remain in the current overlay.
*/
if (nextPresentedOverlay.hasAttribute('aria-hidden') || nextPresentedOverlay.tagName !== 'ION-TOAST') {
presentedOverlay.setAttribute('aria-hidden', 'true');
}
}
};
/**
* When dismissing an overlay we need to reveal the new top-most overlay to screen readers.
* If the top-most overlay is a Toast we potentially need to reveal more overlays since
* focus is never automatically moved to the Toast.
*/
const revealOverlaysToScreenReaders = () => {
if (doc === undefined) return;
const overlays = getPresentedOverlays(doc);
for (let i = overlays.length - 1; i >= 0; i--) {
const currentOverlay = overlays[i];
/**
* If the current we are looking at is a Toast then we can remove aria-hidden.
* However, we potentially need to keep looking at the overlay stack because there
* could be more Toasts underneath. Additionally, we need to unhide the closest non-Toast
* overlay too so focus can move there since focus is never automatically moved to the Toast.
*/
currentOverlay.removeAttribute('aria-hidden');
/**
* If we found a non-Toast element then we can just remove aria-hidden and stop searching entirely
* since this overlay should always receive focus. As a result, all underlying overlays should still
* be hidden from screen readers.
*/
if (currentOverlay.tagName !== 'ION-TOAST') {
break;
}
}
};

View File

@@ -62,7 +62,7 @@
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
Modal ${id}
Modal Content
<ion-item>
<ion-input label="Text Input" class="modal-input modal-input-${id}"></ion-input>

View File

@@ -254,172 +254,5 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await expect(modalInputOne).toBeFocused();
});
test('focusing toast from a shadow overlay should return focus to the last focused element', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/28261',
});
/**
* Triggers for an overlay are typically buttons. However in this case,
* buttons are not considered keyboard focusable by WebKit. Inputs are,
* so we use an input here so we can still test on WebKit.
*/
await page.setContent(
`
<ion-modal>
<ion-content>
<input id="show-toast">Button A</input>
<button>Button B</button>
<ion-toast trigger="show-toast"></ion-toast>
</ion-content>
</ion-modal>
<script>
const toast = document.querySelector('ion-toast');
toast.buttons = ['Ok'];
</script>
`,
config
);
const modal = page.locator('ion-modal');
const showToastTrigger = page.locator('#show-toast');
const toast = page.locator('ion-toast');
const toastButton = toast.locator('button');
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
// Show overlay
await modal.evaluate((el: HTMLIonModalElement) => el.present());
// Click trigger to open toast
await showToastTrigger.click();
// Wait for toast to be presented
await ionToastDidPresent.next();
// Verify trigger in overlay is focused
await expect(showToastTrigger).toBeFocused();
// Click a button in the toast and therefore attempt to move focus
await toastButton.click();
// Verify trigger in overlay is still focused
await expect(showToastTrigger).toBeFocused();
});
test('focusing toast from a scoped overlay should return focus to the last focused element', async ({
page,
skip,
}) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/28261',
});
skip.browser('webkit', 'WebKit does not consider buttons to be focusable');
await page.setContent(
`
<ion-action-sheet></ion-action-sheet>
<ion-toast></ion-toast>
<script>
const actionSheet = document.querySelector('ion-action-sheet');
actionSheet.buttons = [
'Other Button',
{
text: 'Button',
id: 'show-toast',
handler: () => {
document.querySelector('ion-toast').present();
return false;
}
}
];
const toast = document.querySelector('ion-toast');
toast.buttons = ['Ok'];
</script>
`,
config
);
const actionSheet = page.locator('ion-action-sheet');
const showToastButton = page.locator('#show-toast');
const toast = page.locator('ion-toast');
const toastButton = toast.locator('button');
const ionToastDidPresent = await page.spyOnEvent('ionToastDidPresent');
// Show overlay
await actionSheet.evaluate((el: HTMLIonActionSheetElement) => el.present());
// Click button to open toast
await showToastButton.click();
// Wait for toast to be presented
await ionToastDidPresent.next();
// Verify button in overlay is focused
await expect(showToastButton).toBeFocused();
// Click a button in the toast and therefore attempt to move focus
await toastButton.click();
await page.pause();
// Verify button in overlay is still focused
await expect(showToastButton).toBeFocused();
});
test('should not return focus to another element if focus already manually returned', async ({
page,
skip,
}, testInfo) => {
skip.browser(
'webkit',
'WebKit does not consider buttons to be focusable, so this test always passes since the input is the only focusable element.'
);
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/28849',
});
await page.setContent(
`
<button id="open-action-sheet">open</button>
<ion-action-sheet trigger="open-action-sheet"></ion-action-sheet>
<input id="test-input" />
<script>
const actionSheet = document.querySelector('ion-action-sheet');
actionSheet.addEventListener('ionActionSheetWillDismiss', () => {
requestAnimationFrame(() => {
document.querySelector('#test-input').focus();
});
});
</script>
`,
config
);
const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent');
const actionSheet = page.locator('ion-action-sheet');
const input = page.locator('#test-input');
const trigger = page.locator('#open-action-sheet');
// present action sheet
await trigger.click();
await ionActionSheetDidPresent.next();
// dismiss action sheet
await actionSheet.evaluate((el: HTMLIonActionSheetElement) => el.dismiss());
// verify focus is in correct location
await expect(input).toBeFocused();
});
});
});

View File

@@ -1,7 +1,6 @@
import { newSpecPage } from '@stencil/core/testing';
import { Modal } from '../../../components/modal/modal';
import { Toast } from '../../../components/toast/toast';
import { Nav } from '../../../components/nav/nav';
import { RouterOutlet } from '../../../components/router-outlet/router-outlet';
import { setRootAriaHidden } from '../../overlays';
@@ -130,134 +129,3 @@ describe('setRootAriaHidden()', () => {
expect(routerOutlet.hasAttribute('aria-hidden')).toEqual(false);
});
});
describe('aria-hidden on individual overlays', () => {
it('should hide non-topmost overlays from screen readers', async () => {
const page = await newSpecPage({
components: [Modal],
html: `
<ion-modal id="one"></ion-modal>
<ion-modal id="two"></ion-modal>
`,
});
const modalOne = page.body.querySelector<HTMLIonModalElement>('ion-modal#one')!;
const modalTwo = page.body.querySelector<HTMLIonModalElement>('ion-modal#two')!;
await modalOne.present();
await modalTwo.present();
expect(modalOne.hasAttribute('aria-hidden')).toEqual(true);
expect(modalTwo.hasAttribute('aria-hidden')).toEqual(false);
});
it('should unhide new topmost overlay from screen readers when topmost is dismissed', async () => {
const page = await newSpecPage({
components: [Modal],
html: `
<ion-modal id="one"></ion-modal>
<ion-modal id="two"></ion-modal>
`,
});
const modalOne = page.body.querySelector<HTMLIonModalElement>('ion-modal#one')!;
const modalTwo = page.body.querySelector<HTMLIonModalElement>('ion-modal#two')!;
await modalOne.present();
await modalTwo.present();
// dismiss modalTwo so that modalOne becomes the new topmost overlay
await modalTwo.dismiss();
expect(modalOne.hasAttribute('aria-hidden')).toEqual(false);
});
it('should not keep overlays hidden from screen readers if presented after being dismissed while non-topmost', async () => {
const page = await newSpecPage({
components: [Modal],
html: `
<ion-modal id="one"></ion-modal>
<ion-modal id="two"></ion-modal>
`,
});
const modalOne = page.body.querySelector<HTMLIonModalElement>('ion-modal#one')!;
const modalTwo = page.body.querySelector<HTMLIonModalElement>('ion-modal#two')!;
await modalOne.present();
await modalTwo.present();
// modalOne is not the topmost overlay at this point and is hidden from screen readers
await modalOne.dismiss();
// modalOne will become the topmost overlay; ensure it isn't still hidden from screen readers
await modalOne.present();
expect(modalOne.hasAttribute('aria-hidden')).toEqual(false);
});
it('should not hide previous overlay if top-most overlay is toast', async () => {
const page = await newSpecPage({
components: [Modal, Toast],
html: `
<ion-modal id="m-one"></ion-modal>
<ion-modal id="m-two"></ion-modal>
<ion-toast id="t-one"></ion-toast>
<ion-toast id="t-two"></ion-toast>
`,
});
const modalOne = page.body.querySelector<HTMLIonModalElement>('ion-modal#m-one')!;
const modalTwo = page.body.querySelector<HTMLIonModalElement>('ion-modal#m-two')!;
const toastOne = page.body.querySelector<HTMLIonModalElement>('ion-toast#t-one')!;
const toastTwo = page.body.querySelector<HTMLIonModalElement>('ion-toast#t-two')!;
await modalOne.present();
await modalTwo.present();
await toastOne.present();
await toastTwo.present();
expect(modalOne.hasAttribute('aria-hidden')).toEqual(true);
expect(modalTwo.hasAttribute('aria-hidden')).toEqual(false);
expect(toastOne.hasAttribute('aria-hidden')).toEqual(false);
expect(toastTwo.hasAttribute('aria-hidden')).toEqual(false);
await toastTwo.dismiss();
expect(modalOne.hasAttribute('aria-hidden')).toEqual(true);
expect(modalTwo.hasAttribute('aria-hidden')).toEqual(false);
expect(toastOne.hasAttribute('aria-hidden')).toEqual(false);
await toastOne.dismiss();
expect(modalOne.hasAttribute('aria-hidden')).toEqual(true);
expect(modalTwo.hasAttribute('aria-hidden')).toEqual(false);
});
it('should hide previous overlay even with a toast that is not the top-most overlay', async () => {
const page = await newSpecPage({
components: [Modal, Toast],
html: `
<ion-modal id="m-one"></ion-modal>
<ion-toast id="t-one"></ion-toast>
<ion-modal id="m-two"></ion-modal>
`,
});
const modalOne = page.body.querySelector<HTMLIonModalElement>('ion-modal#m-one')!;
const modalTwo = page.body.querySelector<HTMLIonModalElement>('ion-modal#m-two')!;
const toastOne = page.body.querySelector<HTMLIonModalElement>('ion-toast#t-one')!;
await modalOne.present();
await toastOne.present();
await modalTwo.present();
expect(modalOne.hasAttribute('aria-hidden')).toEqual(true);
expect(toastOne.hasAttribute('aria-hidden')).toEqual(true);
expect(modalTwo.hasAttribute('aria-hidden')).toEqual(false);
await modalTwo.dismiss();
expect(modalOne.hasAttribute('aria-hidden')).toEqual(false);
expect(toastOne.hasAttribute('aria-hidden')).toEqual(false);
});
});

View File

@@ -15,8 +15,7 @@
"dom.iterable",
"es2017",
"es2020",
"es2021",
"esnext"
"es2021"
],
"module": "esnext",
"moduleResolution": "node",

View File

@@ -3,46 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [7.8.0](https://github.com/ionic-team/ionic-framework/compare/v7.7.5...v7.8.0) (2024-03-13)
**Note:** Version bump only for package @ionic/docs
## [7.7.5](https://github.com/ionic-team/ionic-framework/compare/v7.7.4...v7.7.5) (2024-03-13)
**Note:** Version bump only for package @ionic/docs
## [7.7.4](https://github.com/ionic-team/ionic-framework/compare/v7.7.3...v7.7.4) (2024-03-06)
**Note:** Version bump only for package @ionic/docs
## [7.7.3](https://github.com/ionic-team/ionic-framework/compare/v7.7.2...v7.7.3) (2024-02-21)
**Note:** Version bump only for package @ionic/docs
## [7.7.2](https://github.com/ionic-team/ionic-framework/compare/v7.7.1...v7.7.2) (2024-02-14)
**Note:** Version bump only for package @ionic/docs
## [7.7.1](https://github.com/ionic-team/ionic-framework/compare/v7.7.0...v7.7.1) (2024-02-07)
**Note:** Version bump only for package @ionic/docs

View File

@@ -1,12 +1,12 @@
{
"name": "@ionic/docs",
"version": "7.8.0",
"version": "7.7.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/docs",
"version": "7.8.0",
"version": "7.7.1",
"license": "MIT"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/docs",
"version": "7.8.0",
"version": "7.7.1",
"description": "Pre-packaged API documentation for the Ionic docs.",
"main": "core.json",
"types": "core.d.ts",

View File

@@ -4,5 +4,5 @@
"docs",
"packages/*"
],
"version": "7.8.0"
"version": "7.7.1"
}

View File

@@ -3,46 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [7.8.0](https://github.com/ionic-team/ionic-framework/compare/v7.7.5...v7.8.0) (2024-03-13)
**Note:** Version bump only for package @ionic/angular-server
## [7.7.5](https://github.com/ionic-team/ionic-framework/compare/v7.7.4...v7.7.5) (2024-03-13)
**Note:** Version bump only for package @ionic/angular-server
## [7.7.4](https://github.com/ionic-team/ionic-framework/compare/v7.7.3...v7.7.4) (2024-03-06)
**Note:** Version bump only for package @ionic/angular-server
## [7.7.3](https://github.com/ionic-team/ionic-framework/compare/v7.7.2...v7.7.3) (2024-02-21)
**Note:** Version bump only for package @ionic/angular-server
## [7.7.2](https://github.com/ionic-team/ionic-framework/compare/v7.7.1...v7.7.2) (2024-02-14)
**Note:** Version bump only for package @ionic/angular-server
## [7.7.1](https://github.com/ionic-team/ionic-framework/compare/v7.7.0...v7.7.1) (2024-02-07)
**Note:** Version bump only for package @ionic/angular-server

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/angular-server",
"version": "7.8.0",
"version": "7.7.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/angular-server",
"version": "7.8.0",
"version": "7.7.1",
"license": "MIT",
"dependencies": {
"@ionic/core": "^7.8.0"
"@ionic/core": "^7.7.1"
},
"devDependencies": {
"@angular-eslint/eslint-plugin": "^14.0.0",
@@ -1060,11 +1060,11 @@
"dev": true
},
"node_modules/@ionic/core": {
"version": "7.7.5",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.7.5.tgz",
"integrity": "sha512-/ozFH0Wxh415uER8vc4pcGeRLlh71TfL9gZTHRPAsRT7FnampW8Y45T2fDNNLSmN8UGrN9rj2sXr/QLVq1NbIA==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.7.1.tgz",
"integrity": "sha512-Su4VFwztN8vKyHsIgHMmzQ1ZrIajNzMR7hg2fcv15Y2yrzLpK06PcUf9QutBM9KyP0wQIngwfnO68vrAnqR+xQ==",
"dependencies": {
"@stencil/core": "^4.12.2",
"@stencil/core": "^4.12.0",
"ionicons": "^7.2.2",
"tslib": "^2.1.0"
}
@@ -1462,9 +1462,9 @@
"license": "MIT"
},
"node_modules/@stencil/core": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.2.tgz",
"integrity": "sha512-WEMpoqwMV4hY/ab2z9NxRhSeZwuKEugjyn6Vd+qA9xqZh6VNUL27QbP8vCa7IeqD4Zql4JBtKu3lVuBHutWE6w==",
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.0.tgz",
"integrity": "sha512-qAQcfNmp2sdxAh1DlyUhHfDmIUS7mhI+5LAhPphg74zK9sKgFL5vpLzgjs0wohpjlmI4msgJFYiRB8lxVPqjPg==",
"bin": {
"stencil": "bin/stencil"
},
@@ -7330,11 +7330,11 @@
"dev": true
},
"@ionic/core": {
"version": "7.7.5",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.7.5.tgz",
"integrity": "sha512-/ozFH0Wxh415uER8vc4pcGeRLlh71TfL9gZTHRPAsRT7FnampW8Y45T2fDNNLSmN8UGrN9rj2sXr/QLVq1NbIA==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.7.1.tgz",
"integrity": "sha512-Su4VFwztN8vKyHsIgHMmzQ1ZrIajNzMR7hg2fcv15Y2yrzLpK06PcUf9QutBM9KyP0wQIngwfnO68vrAnqR+xQ==",
"requires": {
"@stencil/core": "^4.12.2",
"@stencil/core": "^4.12.0",
"ionicons": "^7.2.2",
"tslib": "^2.1.0"
}
@@ -7581,9 +7581,9 @@
}
},
"@stencil/core": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.2.tgz",
"integrity": "sha512-WEMpoqwMV4hY/ab2z9NxRhSeZwuKEugjyn6Vd+qA9xqZh6VNUL27QbP8vCa7IeqD4Zql4JBtKu3lVuBHutWE6w=="
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.12.0.tgz",
"integrity": "sha512-qAQcfNmp2sdxAh1DlyUhHfDmIUS7mhI+5LAhPphg74zK9sKgFL5vpLzgjs0wohpjlmI4msgJFYiRB8lxVPqjPg=="
},
"@types/estree": {
"version": "0.0.39",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular-server",
"version": "7.8.0",
"version": "7.7.1",
"description": "Angular SSR Module for Ionic",
"keywords": [
"ionic",
@@ -62,6 +62,6 @@
},
"prettier": "@ionic/prettier-config",
"dependencies": {
"@ionic/core": "^7.8.0"
"@ionic/core": "^7.7.1"
}
}

View File

@@ -3,53 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [7.8.0](https://github.com/ionic-team/ionic-framework/compare/v7.7.5...v7.8.0) (2024-03-13)
### Features
* **datetime:** formatOptions property for Datetime ([#29065](https://github.com/ionic-team/ionic-framework/issues/29065)) ([7cdbc1b](https://github.com/ionic-team/ionic-framework/commit/7cdbc1b5ad004e17a7c51363653e0e67f50e6860))
* **searchbar:** autocapitalize, dir, lang, maxlength, and minlength are inherited to native input ([#29098](https://github.com/ionic-team/ionic-framework/issues/29098)) ([a0a77f7](https://github.com/ionic-team/ionic-framework/commit/a0a77f799df0732d9f7182f15866035a3ce5a1eb)), closes [#27606](https://github.com/ionic-team/ionic-framework/issues/27606)
## [7.7.5](https://github.com/ionic-team/ionic-framework/compare/v7.7.4...v7.7.5) (2024-03-13)
### Bug Fixes
* **angular:** add ionNavWillChange and ionNavDidChange types for nav ([#29122](https://github.com/ionic-team/ionic-framework/issues/29122)) ([85b9d5c](https://github.com/ionic-team/ionic-framework/commit/85b9d5c35f626ffc273d220549b0126ddc1f7e4b)), closes [#29114](https://github.com/ionic-team/ionic-framework/issues/29114)
## [7.7.4](https://github.com/ionic-team/ionic-framework/compare/v7.7.3...v7.7.4) (2024-03-06)
**Note:** Version bump only for package @ionic/angular
## [7.7.3](https://github.com/ionic-team/ionic-framework/compare/v7.7.2...v7.7.3) (2024-02-21)
**Note:** Version bump only for package @ionic/angular
## [7.7.2](https://github.com/ionic-team/ionic-framework/compare/v7.7.1...v7.7.2) (2024-02-14)
**Note:** Version bump only for package @ionic/angular
## [7.7.1](https://github.com/ionic-team/ionic-framework/compare/v7.7.0...v7.7.1) (2024-02-07)
**Note:** Version bump only for package @ionic/angular

View File

@@ -1,12 +1,4 @@
import {
ElementRef,
Injector,
EnvironmentInjector,
NgZone,
ChangeDetectorRef,
Directive,
EventEmitter,
} from '@angular/core';
import { ElementRef, Injector, EnvironmentInjector, NgZone, ChangeDetectorRef, Directive } from '@angular/core';
import type { Components } from '@ionic/core';
import { AngularDelegate } from '../../providers/angular-delegate';
@@ -30,16 +22,8 @@ const NAV_METHODS = [
'getPrevious',
];
export declare interface IonNav extends Components.IonNav {
/**
* Event fired when the nav will change components
*/
ionNavWillChange: EventEmitter<CustomEvent<void>>;
/**
* Event fired when the nav has changed components
*/
ionNavDidChange: EventEmitter<CustomEvent<void>>;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export declare interface IonNav extends Components.IonNav {}
@ProxyCmp({
inputs: NAV_INPUTS,

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