mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4476817825 | ||
|
|
97e32f3b0b | ||
|
|
bfddb17065 | ||
|
|
3c9d6ea5f5 | ||
|
|
8e1178b98b | ||
|
|
470478d387 | ||
|
|
cc73a1063e | ||
|
|
349e8cd5b0 | ||
|
|
bcbe8cbb8d | ||
|
|
03ca0c5968 | ||
|
|
fff4aec6cf | ||
|
|
eb592b8917 | ||
|
|
a15cd01bc3 | ||
|
|
4199762d3e | ||
|
|
79518468dd | ||
|
|
f4a08b7ed4 | ||
|
|
dbe6853884 | ||
|
|
096eef4a79 | ||
|
|
d4a5fbd955 | ||
|
|
591c133344 | ||
|
|
d297ecb87a | ||
|
|
a625c837a6 | ||
|
|
77464ef21a | ||
|
|
fa93dffdb4 | ||
|
|
6f200ac751 | ||
|
|
7c2d0c981a | ||
|
|
1351b2eafc | ||
|
|
88f1828bd8 | ||
|
|
020f3cc56c | ||
|
|
3cbf9e7c4c | ||
|
|
2664587749 | ||
|
|
f00ad8a835 | ||
|
|
81ef3f1ecd | ||
|
|
c171ccbd37 | ||
|
|
a0735b97bf | ||
|
|
b4423a816f | ||
|
|
ff23e4f267 | ||
|
|
97123cd6c3 | ||
|
|
1dcd9de50a | ||
|
|
c458523b0d | ||
|
|
a5e4669c4b | ||
|
|
9413aa0bac | ||
|
|
ce6e637787 | ||
|
|
7a29e0e3c7 | ||
|
|
8c79e2c5b5 | ||
|
|
26674f1dfa | ||
|
|
88f23b1626 | ||
|
|
a5b3750ee2 | ||
|
|
fbcd3f8c08 | ||
|
|
04ce642369 | ||
|
|
84c421a4b0 | ||
|
|
1decc13cb8 | ||
|
|
17308f247f | ||
|
|
d8b377ffeb | ||
|
|
24cfdc308f | ||
|
|
bcccc217b8 | ||
|
|
e968bd029a | ||
|
|
7c8f621536 | ||
|
|
edceac0745 | ||
|
|
2969f9f9f2 | ||
|
|
9223abc1f8 | ||
|
|
b37c158eea |
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -19,7 +19,8 @@ assignees: ''
|
||||
**Ionic version:**
|
||||
<!-- (For Ionic 1.x issues, please use https://github.com/ionic-team/ionic-v1) -->
|
||||
<!-- (For Ionic 2.x & 3.x issues, please use https://github.com/ionic-team/ionic-v3) -->
|
||||
[x] **4.x**
|
||||
[ ] **4.x**
|
||||
[x] **5.x**
|
||||
|
||||
**Current behavior:**
|
||||
<!-- Describe how the bug manifests. -->
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -64,3 +64,4 @@ core/loader/
|
||||
core/www/
|
||||
.stencil/
|
||||
angular/build/
|
||||
core/components/
|
||||
|
||||
@@ -209,6 +209,13 @@ function prepareDevPackage(tasks, package, version) {
|
||||
title: `${pkg.name}: npm link @ionic/core`,
|
||||
task: () => execa('npm', ['link', '@ionic/core'], { cwd: projectRoot })
|
||||
});
|
||||
|
||||
if (package === 'packages/react-router') {
|
||||
projectTasks.push({
|
||||
title: `${pkg.name}: npm link @ionic/react`,
|
||||
task: () => execa('npm', ['link', '@ionic/react'], { cwd: projectRoot })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
projectTasks.push({
|
||||
|
||||
@@ -78,6 +78,9 @@ async function setPackageVersionChanges(packages, version) {
|
||||
if (package !== 'core') {
|
||||
const pkg = common.readPkg(package);
|
||||
common.updateDependency(pkg, '@ionic/core', version);
|
||||
if(package === 'packages/react-router') {
|
||||
common.updateDependency(pkg, '@ionic/react', version);
|
||||
}
|
||||
common.writePkg(package, pkg);
|
||||
}
|
||||
const projectRoot = common.projectPath(package);
|
||||
|
||||
@@ -132,7 +132,7 @@ async function publishGithub(version, gitTag, changelog, npmTag) {
|
||||
|
||||
await octokit.repos.createRelease({
|
||||
owner: 'ionic-team',
|
||||
repo: 'ionic',
|
||||
repo: 'ionic-framework',
|
||||
target_commitish: branch,
|
||||
tag_name: gitTag,
|
||||
name: version,
|
||||
|
||||
63
CHANGELOG.md
63
CHANGELOG.md
@@ -1,3 +1,66 @@
|
||||
## [5.3.1](https://github.com/ionic-team/ionic/compare/v5.3.0...v5.3.1) (2020-07-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **react:** properly extend HTMLElement for tabs ([bfddb17](https://github.com/ionic-team/ionic/commit/bfddb170659224d0f826762744dfe44a85813d36)), closes [#21803](https://github.com/ionic-team/ionic/issues/21803)
|
||||
|
||||
|
||||
|
||||
# [5.3.0 Phosphorus](https://github.com/ionic-team/ionic/compare/v5.2.3...v5.3.0) (2020-07-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **angular:** per-page animations now work with swipe to go back ([#21706](https://github.com/ionic-team/ionic/issues/21706)) ([2664587](https://github.com/ionic-team/ionic/commit/2664587749e45100a04f70796733de162b26cdf7)), closes [#21692](https://github.com/ionic-team/ionic/issues/21692)
|
||||
* **datetime:** remove unneeded combobox role ([#21708](https://github.com/ionic-team/ionic/issues/21708)) ([f00ad8a](https://github.com/ionic-team/ionic/commit/f00ad8a8357ccd7fe85631dad0c841f2d4c72487))
|
||||
* **input:** clear button can now be tabbed to ([#21633](https://github.com/ionic-team/ionic/issues/21633)) ([1dcd9de](https://github.com/ionic-team/ionic/commit/1dcd9de50ae16bfa102e98120a022de5b0287baa))
|
||||
* **ios:** improve scroll assist reliability on password inputs ([#21703](https://github.com/ionic-team/ionic/issues/21703)) ([3cbf9e7](https://github.com/ionic-team/ionic/commit/3cbf9e7c4c225d6b02237d8ea8f16fb924ba360e)), closes [#21688](https://github.com/ionic-team/ionic/issues/21688)
|
||||
* **keyboard:** keyboard events now consistently fire on android ([#21741](https://github.com/ionic-team/ionic/issues/21741)) ([020f3cc](https://github.com/ionic-team/ionic/commit/020f3cc56cb6dac23dd8766a3802422500b510e2)), closes [#21734](https://github.com/ionic-team/ionic/issues/21734)
|
||||
* **nav:** insertPages method correctly inserts multiple pages with props ([#21725](https://github.com/ionic-team/ionic/issues/21725)) ([eb592b8](https://github.com/ionic-team/ionic/commit/eb592b8917b8a7412d8c346f41b47d3b79002b95)), closes [#21724](https://github.com/ionic-team/ionic/issues/21724)
|
||||
* **overlays:** trap focus inside overlay components except toast ([#21716](https://github.com/ionic-team/ionic/issues/21716)) ([fff4aec](https://github.com/ionic-team/ionic/commit/fff4aec6cfbd48566594a05f4af57dd0578977a8)), closes [#21647](https://github.com/ionic-team/ionic/issues/21647)
|
||||
* **segment-button:** allow min-width to be overridden ([#21722](https://github.com/ionic-team/ionic/issues/21722)) ([88f1828](https://github.com/ionic-team/ionic/commit/88f1828bd8f6b9a1c1f3dcb220d93067bed7f404)), closes [#21105](https://github.com/ionic-team/ionic/issues/21105)
|
||||
* **title:** allow overriding of large title transform-origin ([#21770](https://github.com/ionic-team/ionic/issues/21770)) ([dbe6853](https://github.com/ionic-team/ionic/commit/dbe6853884bd76c3d8e229cd58e1571d9b3a7249)), closes [#21761](https://github.com/ionic-team/ionic/issues/21761)
|
||||
* **virtual-scroll:** properly calculate top offset when nested ([#21581](https://github.com/ionic-team/ionic/issues/21581)) ([d297ecb](https://github.com/ionic-team/ionic/commit/d297ecb87ad3e1c8f0988f0571a475081ce368f8))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **card:** expose global card css variables ([#21756](https://github.com/ionic-team/ionic/issues/21756)) ([096eef4](https://github.com/ionic-team/ionic/commit/096eef4a79c2d05c37eb224466c6d7d512d2be20)), closes [#21694](https://github.com/ionic-team/ionic/issues/21694)
|
||||
* **input:** accept datetime-local, month, and week type values ([#21758](https://github.com/ionic-team/ionic/issues/21758)) ([fa93dff](https://github.com/ionic-team/ionic/commit/fa93dffdb4f350e8db8acc7f06b06761974eea8e)), closes [#21757](https://github.com/ionic-team/ionic/issues/21757)
|
||||
* **input, textarea:** expose native events for ionBlur and ionFocus ([#21777](https://github.com/ionic-team/ionic/issues/21777)) ([a625c83](https://github.com/ionic-team/ionic/commit/a625c837a60abc07ad71c696196a89f1a25a4c27)), closes [#17363](https://github.com/ionic-team/ionic/issues/17363)
|
||||
* **react:** add custom history to IonReactRouter ([#21775](https://github.com/ionic-team/ionic/issues/21775)) ([d4a5fbd](https://github.com/ionic-team/ionic/commit/d4a5fbd955e8ecccba8b77491943d81fdf5a5ef4)), closes [#20297](https://github.com/ionic-team/ionic/issues/20297)
|
||||
* **react:** add new react router ([#21693](https://github.com/ionic-team/ionic/issues/21693)) ([c171ccb](https://github.com/ionic-team/ionic/commit/c171ccbd37c1ee4b4934758a3a759170ff357cb2))
|
||||
* **router:** add navigation hooks ([#21709](https://github.com/ionic-team/ionic/issues/21709)) ([77464ef](https://github.com/ionic-team/ionic/commit/77464ef21aaaa5afa7a02e5417f3ec295b240601))
|
||||
* **segment-button, toast:** expose additional shadow parts ([#21532](https://github.com/ionic-team/ionic/issues/21532)) ([a5e4669](https://github.com/ionic-team/ionic/commit/a5e4669c4bcbcb2cdd605ed17c35e42438bd5596))
|
||||
* **select:** add optional generic typings ([#21514](https://github.com/ionic-team/ionic/issues/21514)) ([7c2d0c9](https://github.com/ionic-team/ionic/commit/7c2d0c981ab91930478c4b76220ce4ec4ed4e471))
|
||||
|
||||
|
||||
|
||||
## [5.2.3](https://github.com/ionic-team/ionic/compare/v5.2.2...v5.2.3) (2020-07-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **angular:** expose createAnimation in addition to AnimationController ([#21616](https://github.com/ionic-team/ionic/issues/21616)) ([a5b3750](https://github.com/ionic-team/ionic/commit/a5b3750ee2a7c005f80f8453b03c67dd1a5622ca)), closes [#21615](https://github.com/ionic-team/ionic/issues/21615)
|
||||
* **select:** change role to listbox ([#21609](https://github.com/ionic-team/ionic/issues/21609)) ([8c79e2c](https://github.com/ionic-team/ionic/commit/8c79e2c5b58ad562967f2d559c6b548e57536936))
|
||||
* **slides:** enable keyboard integration ([#21608](https://github.com/ionic-team/ionic/issues/21608)) ([26674f1](https://github.com/ionic-team/ionic/commit/26674f1dfa8c9a28f5525f1b16070e8ec494c232)), closes [#21554](https://github.com/ionic-team/ionic/issues/21554)
|
||||
* **textarea:** add aria-labelledby to native textarea ([#21606](https://github.com/ionic-team/ionic/issues/21606)) ([88f23b1](https://github.com/ionic-team/ionic/commit/88f23b1626eb400336f2f52a3e0d34ac3c161e64)), closes [#21600](https://github.com/ionic-team/ionic/issues/21600)
|
||||
|
||||
|
||||
|
||||
## [5.2.2](https://github.com/ionic-team/ionic/compare/v5.2.1...v5.2.2) (2020-06-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **action-sheet, alert:** resolve double click issue when running in iOS mode on chrome ([#21506](https://github.com/ionic-team/ionic/issues/21506)) ([bcccc21](https://github.com/ionic-team/ionic/commit/bcccc217b8833a284a1781e287db5e46043b3548)), closes [#21503](https://github.com/ionic-team/ionic/issues/21503)
|
||||
* **angular:** fix issue where navAnimation was being incorrectly overridden ([#21508](https://github.com/ionic-team/ionic/issues/21508)) ([e968bd0](https://github.com/ionic-team/ionic/commit/e968bd029a4fb37b4001d96a490c6091a948785a)), closes [#21495](https://github.com/ionic-team/ionic/issues/21495)
|
||||
* **input:** add aria-label to clear button ([#21538](https://github.com/ionic-team/ionic/issues/21538)) ([d8b377f](https://github.com/ionic-team/ionic/commit/d8b377ffeb88eaae23b33eadeae5c8e54e1bc77c)), closes [#21537](https://github.com/ionic-team/ionic/issues/21537)
|
||||
* **ios:** respect toolbar opacity when doing nav transition ([#21512](https://github.com/ionic-team/ionic/issues/21512)) ([24cfdc3](https://github.com/ionic-team/ionic/commit/24cfdc308f63b7a55969ac58806eafd67116b017))
|
||||
* **segment:** ensure checked classes get set after not having a value ([#21547](https://github.com/ionic-team/ionic/issues/21547)) ([17308f2](https://github.com/ionic-team/ionic/commit/17308f247f8750029ece39548c9f457e15326189)), closes [#21546](https://github.com/ionic-team/ionic/issues/21546)
|
||||
|
||||
|
||||
## [5.2.1](https://github.com/ionic-team/ionic/compare/v5.2.0...v5.2.1) (2020-06-11)
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,129 @@
|
||||
# Contributor Code of Conduct
|
||||
|
||||
As contributors and maintainers of the Ionic project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities.
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
Communication through any of Ionic's channels (GitHub, Slack, Forum, IRC, mailing lists, Twitter, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
||||
## Our Pledge
|
||||
|
||||
We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the Ionic project to do the same.
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
If any member of the community violates this code of conduct, the maintainers of the Ionic project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate.
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
If you are subject to or witness unacceptable behavior, or have any other concerns, please email us at [hi@ionicframework.com](mailto:hi@ionicframework.com).
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[contact@ionic.io](mailto:contact@ionic.io).
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
|
||||
10
README.md
10
README.md
@@ -1,9 +1,9 @@
|
||||
# Ionic
|
||||
# Ionic Framework
|
||||
|
||||
[Ionic](https://ionicframework.com/) is the open-source mobile app development framework that makes it easy to
|
||||
[Ionic Framework](https://ionicframework.com/) is the open-source mobile app development framework that makes it easy to
|
||||
build top quality native and progressive web apps with web technologies.
|
||||
|
||||
Ionic is based on [Web Components](https://www.webcomponents.org/introduction) and comes with many significant performance, usability, and feature improvements over the past versions.
|
||||
Ionic Framework is based on [Web Components](https://www.webcomponents.org/introduction) and comes with many significant performance, usability, and feature improvements over the past versions.
|
||||
|
||||
|
||||
### Packages
|
||||
@@ -31,6 +31,8 @@ Thanks for your interest in contributing! Read up on our guidelines for
|
||||
and then look through our issues with a [help wanted](https://github.com/ionic-team/ionic/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22)
|
||||
label.
|
||||
|
||||
Please note that this project is released with a [Contributor Code of Conduct](https://github.com/ionic-team/ionic/blob/master/CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
@@ -40,7 +42,7 @@ It is the perfect starting point for learning and building your own app.
|
||||
|
||||
### Future Goals
|
||||
|
||||
As Ionic components migrate to the web component standard, a goal of ours is to have Ionic components easily work within all of the popular frameworks.
|
||||
As Ionic Framework components migrate to the web component standard, a goal of ours is to have Ionic Framework easily work within all of the popular frameworks.
|
||||
|
||||
### Earlier Versions
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/angular",
|
||||
"version": "5.2.1",
|
||||
"version": "5.3.1",
|
||||
"description": "Angular specific wrappers for @ionic/core",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -42,7 +42,7 @@
|
||||
"validate": "npm i && npm run lint && npm run test && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ionic/core": "5.2.1",
|
||||
"@ionic/core": "5.3.1",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
|
||||
export default {
|
||||
input: 'build/es2015/core.js',
|
||||
|
||||
@@ -127,7 +127,7 @@ export class StackController {
|
||||
* Save any custom animation so that navigating
|
||||
* back will use this custom animation by default.
|
||||
*/
|
||||
if (animationBuilder !== undefined && leavingView) {
|
||||
if (leavingView) {
|
||||
leavingView.animationBuilder = animationBuilder;
|
||||
}
|
||||
|
||||
@@ -190,13 +190,16 @@ export class StackController {
|
||||
if (leavingView) {
|
||||
const views = this.getStack(leavingView.stackId);
|
||||
const enteringView = views[views.length - 2];
|
||||
const customAnimation = enteringView.animationBuilder;
|
||||
|
||||
return this.wait(() => {
|
||||
return this.transition(
|
||||
enteringView, // entering view
|
||||
leavingView, // leaving view
|
||||
'back',
|
||||
this.canGoBack(2),
|
||||
true
|
||||
true,
|
||||
customAnimation
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export * from './types/ionic-lifecycle-hooks';
|
||||
export { IonicModule } from './ionic-module';
|
||||
|
||||
// UTILS
|
||||
export { IonicSafeString, getPlatforms, isPlatform } from '@ionic/core';
|
||||
export { IonicSafeString, getPlatforms, isPlatform, createAnimation } from '@ionic/core';
|
||||
|
||||
// CORE TYPES
|
||||
export { Animation, AnimationBuilder, AnimationCallbackOptions, AnimationDirection, AnimationFill, AnimationKeyFrames, AnimationLifecycle, Gesture, GestureConfig, GestureDetail, mdTransitionAnimation, iosTransitionAnimation } from '@ionic/core';
|
||||
export { Animation, AnimationBuilder, AnimationCallbackOptions, AnimationDirection, AnimationFill, AnimationKeyFrames, AnimationLifecycle, Gesture, GestureConfig, GestureDetail, mdTransitionAnimation, iosTransitionAnimation, NavComponentWithProps } from '@ionic/core';
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package-lock=false
|
||||
@@ -1,121 +0,0 @@
|
||||
# Contribution guide
|
||||
|
||||
## Contribute to Ionic Core
|
||||
|
||||
Ionic Core is the fundation of Ionic v4. It's based in [Stencil](https://stenciljs.com) and consist in a set of web components a long of JS and CSS utils.
|
||||
|
||||
|
||||
### Installing dependencies
|
||||
|
||||
Before you can build ionic, we assume the following list of software is already installed in your system
|
||||
|
||||
- Git
|
||||
- Node 8 or higher
|
||||
- Npm 6.0 or higher
|
||||
|
||||
|
||||
### Fork repository
|
||||
|
||||
In order to contributo to Ionic, you must have a github account so you can push code and create a new Pull Request (PR).
|
||||
|
||||
Once you are all setup, following the Github's guide of how to fork a repository: https://guides.github.com/activities/forking/
|
||||
|
||||
|
||||
### Build Ionic Core
|
||||
|
||||
```bash
|
||||
# Clone your Github repository:
|
||||
git clone https://github.com/<github username>/ionic.git
|
||||
|
||||
# Go to the core directory:
|
||||
cd ionic/core
|
||||
|
||||
# Install npm dependencies
|
||||
npm install
|
||||
|
||||
# Run Ionic dev server
|
||||
npm start
|
||||
```
|
||||
|
||||
### Development Workflow
|
||||
|
||||
#### 1. Run Dev Server
|
||||
|
||||
```bash
|
||||
# Move to the core folder
|
||||
cd core
|
||||
|
||||
# Run dev server
|
||||
npm start
|
||||
```
|
||||
|
||||
You should be able to navigate to `http://localhost:3333` which will look like a file browser.
|
||||
|
||||
E2E tests are located inside the `src/components` folder, in the following way: `http://localhost:3333/src/components/{COMPONENT}/test/`
|
||||
|
||||
|
||||
**Path examples:**
|
||||
|
||||
- ActionSheet basic test: http://localhost:3333/src/components/action-sheet/test/basic
|
||||
- Nav basic test: http://localhost:3333/src/components/nav/test/basic
|
||||
- Button basic test:
|
||||
http://localhost:3333/src/components/button/test/basic
|
||||
|
||||
|
||||
**IMPORTANT**
|
||||
|
||||
Leave the dev server running in the background while you make changes. The dev server listen for changes and automatically recompile Ionic for you.
|
||||
|
||||
|
||||
|
||||
#### 2. Open `core` folder in your IDE
|
||||
|
||||
Components implementations live inside the `core/src/components` folder.
|
||||
|
||||
You can find each ionic component inside their directory.
|
||||
|
||||
|
||||
#### 3. Run test suite
|
||||
|
||||
Before commiting your changes make sure tests are passing:
|
||||
|
||||
```
|
||||
npm run validate
|
||||
```
|
||||
|
||||
#### 4. Create a branch and commit
|
||||
|
||||
```bash
|
||||
# Create a git branch
|
||||
git checkout -b my-improvement
|
||||
|
||||
# Add changes
|
||||
git add .
|
||||
|
||||
# Create commit
|
||||
git commit -m "fix(component): message"
|
||||
```
|
||||
|
||||
Create a PR:
|
||||
https://guides.github.com/activities/forking/
|
||||
|
||||
|
||||
### Summary
|
||||
|
||||
```bash
|
||||
# Clone repo
|
||||
git clone git@github.com:ionic-team/ionic.git
|
||||
|
||||
# Move to ionic/core folder
|
||||
cd ionic/core
|
||||
|
||||
# Install npm dependencies
|
||||
npm i
|
||||
|
||||
# Run dev server
|
||||
npm start
|
||||
|
||||
# Run test suite
|
||||
npm run validate
|
||||
```
|
||||
|
||||
23
core/api.txt
23
core/api.txt
@@ -475,13 +475,13 @@ ion-input,prop,required,boolean,false,false,false
|
||||
ion-input,prop,size,number | undefined,undefined,false,false
|
||||
ion-input,prop,spellcheck,boolean,false,false,false
|
||||
ion-input,prop,step,string | undefined,undefined,false,false
|
||||
ion-input,prop,type,"date" | "email" | "number" | "password" | "search" | "tel" | "text" | "time" | "url",'text',false,false
|
||||
ion-input,prop,type,"date" | "datetime-local" | "email" | "month" | "number" | "password" | "search" | "tel" | "text" | "time" | "url" | "week",'text',false,false
|
||||
ion-input,prop,value,null | number | string | undefined,'',false,false
|
||||
ion-input,method,getInputElement,getInputElement() => Promise<HTMLInputElement>
|
||||
ion-input,method,setFocus,setFocus() => Promise<void>
|
||||
ion-input,event,ionBlur,void,true
|
||||
ion-input,event,ionBlur,FocusEvent,true
|
||||
ion-input,event,ionChange,InputChangeEventDetail,true
|
||||
ion-input,event,ionFocus,void,true
|
||||
ion-input,event,ionFocus,FocusEvent,true
|
||||
ion-input,event,ionInput,KeyboardEvent,true
|
||||
ion-input,css-prop,--background
|
||||
ion-input,css-prop,--color
|
||||
@@ -745,13 +745,13 @@ ion-nav,method,getActive,getActive() => Promise<ViewController | undefined>
|
||||
ion-nav,method,getByIndex,getByIndex(index: number) => Promise<ViewController | undefined>
|
||||
ion-nav,method,getPrevious,getPrevious(view?: ViewController | undefined) => Promise<ViewController | undefined>
|
||||
ion-nav,method,insert,insert<T extends NavComponent>(insertIndex: number, component: T, componentProps?: ComponentProps<T> | null | undefined, opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
|
||||
ion-nav,method,insertPages,insertPages(insertIndex: number, insertComponents: NavComponent[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
|
||||
ion-nav,method,insertPages,insertPages(insertIndex: number, insertComponents: NavComponent[] | NavComponentWithProps[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
|
||||
ion-nav,method,pop,pop(opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
|
||||
ion-nav,method,popTo,popTo(indexOrViewCtrl: number | ViewController, opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
|
||||
ion-nav,method,popToRoot,popToRoot(opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
|
||||
ion-nav,method,push,push<T extends NavComponent>(component: T, componentProps?: ComponentProps<T> | null | undefined, opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
|
||||
ion-nav,method,removeIndex,removeIndex(startIndex: number, removeCount?: number, opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
|
||||
ion-nav,method,setPages,setPages(views: any[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
|
||||
ion-nav,method,setPages,setPages(views: NavComponent[] | NavComponentWithProps[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
|
||||
ion-nav,method,setRoot,setRoot<T extends NavComponent>(component: T, componentProps?: ComponentProps<T> | null | undefined, opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>
|
||||
ion-nav,event,ionNavDidChange,void,false
|
||||
ion-nav,event,ionNavWillChange,void,false
|
||||
@@ -933,6 +933,8 @@ ion-ripple-effect,prop,type,"bounded" | "unbounded",'bounded',false,false
|
||||
ion-ripple-effect,method,addRipple,addRipple(x: number, y: number) => Promise<() => void>
|
||||
|
||||
ion-route,none
|
||||
ion-route,prop,beforeEnter,(() => boolean | NavigationHookOptions | Promise<NavigationHookResult>) | undefined,undefined,false,false
|
||||
ion-route,prop,beforeLeave,(() => boolean | NavigationHookOptions | Promise<NavigationHookResult>) | undefined,undefined,false,false
|
||||
ion-route,prop,component,string,undefined,true,false
|
||||
ion-route,prop,componentProps,undefined | { [key: string]: any; },undefined,false,false
|
||||
ion-route,prop,url,string,'',false,false
|
||||
@@ -1051,6 +1053,7 @@ ion-segment-button,css-prop,--padding-start
|
||||
ion-segment-button,css-prop,--padding-top
|
||||
ion-segment-button,css-prop,--transition
|
||||
ion-segment-button,part,indicator
|
||||
ion-segment-button,part,indicator-background
|
||||
ion-segment-button,part,native
|
||||
|
||||
ion-select,shadow
|
||||
@@ -1069,7 +1072,7 @@ ion-select,prop,value,any,undefined,false,false
|
||||
ion-select,method,open,open(event?: UIEvent | undefined) => Promise<any>
|
||||
ion-select,event,ionBlur,void,true
|
||||
ion-select,event,ionCancel,void,true
|
||||
ion-select,event,ionChange,SelectChangeEventDetail,true
|
||||
ion-select,event,ionChange,SelectChangeEventDetail<any>,true
|
||||
ion-select,event,ionFocus,void,true
|
||||
ion-select,css-prop,--padding-bottom
|
||||
ion-select,css-prop,--padding-end
|
||||
@@ -1226,9 +1229,9 @@ ion-textarea,prop,value,null | string | undefined,'',false,false
|
||||
ion-textarea,prop,wrap,"hard" | "off" | "soft" | undefined,undefined,false,false
|
||||
ion-textarea,method,getInputElement,getInputElement() => Promise<HTMLTextAreaElement>
|
||||
ion-textarea,method,setFocus,setFocus() => Promise<void>
|
||||
ion-textarea,event,ionBlur,void,true
|
||||
ion-textarea,event,ionBlur,FocusEvent,true
|
||||
ion-textarea,event,ionChange,TextareaChangeEventDetail,true
|
||||
ion-textarea,event,ionFocus,void,true
|
||||
ion-textarea,event,ionFocus,FocusEvent,true
|
||||
ion-textarea,event,ionInput,KeyboardEvent,true
|
||||
ion-textarea,css-prop,--background
|
||||
ion-textarea,css-prop,--border-radius
|
||||
@@ -1290,6 +1293,10 @@ ion-toast,css-prop,--min-width
|
||||
ion-toast,css-prop,--start
|
||||
ion-toast,css-prop,--white-space
|
||||
ion-toast,css-prop,--width
|
||||
ion-toast,part,button
|
||||
ion-toast,part,container
|
||||
ion-toast,part,header
|
||||
ion-toast,part,message
|
||||
|
||||
ion-toggle,shadow
|
||||
ion-toggle,prop,checked,boolean,false,false,false
|
||||
|
||||
11819
core/package-lock.json
generated
Normal file
11819
core/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "5.2.1",
|
||||
"version": "5.3.1",
|
||||
"description": "Base components for Ionic",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -30,29 +30,29 @@
|
||||
"loader/"
|
||||
],
|
||||
"dependencies": {
|
||||
"ionicons": "^5.0.1",
|
||||
"ionicons": "^5.1.2",
|
||||
"tslib": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@stencil/core": "1.14.0",
|
||||
"@stencil/sass": "1.3.1",
|
||||
"@types/jest": "24.9.1",
|
||||
"@types/node": "12.12.3",
|
||||
"@types/puppeteer": "1.19.1",
|
||||
"@rollup/plugin-node-resolve": "^8.4.0",
|
||||
"@rollup/plugin-virtual": "^2.0.3",
|
||||
"@stencil/core": "1.17.1",
|
||||
"@stencil/sass": "1.3.2",
|
||||
"@types/jest": "26.0.7",
|
||||
"@types/node": "14.0.25",
|
||||
"@types/puppeteer": "3.0.1",
|
||||
"@types/swiper": "5.3.1",
|
||||
"aws-sdk": "^2.497.0",
|
||||
"aws-sdk": "^2.719.0",
|
||||
"clean-css-cli": "^4.1.11",
|
||||
"domino": "^2.1.3",
|
||||
"fs-extra": "^8.0.1",
|
||||
"jest": "24.9.0",
|
||||
"jest-cli": "24.9.0",
|
||||
"domino": "^2.1.6",
|
||||
"fs-extra": "^9.0.1",
|
||||
"jest": "26.1.0",
|
||||
"jest-cli": "26.1.0",
|
||||
"np": "^5.0.3",
|
||||
"pixelmatch": "4.0.2",
|
||||
"puppeteer": "1.20.0",
|
||||
"rollup": "1.32.0",
|
||||
"rollup-plugin-node-resolve": "5.2.0",
|
||||
"rollup-plugin-virtual": "^1.0.1",
|
||||
"sass": "^1.22.9",
|
||||
"puppeteer": "5.2.1",
|
||||
"rollup": "^2.23.0",
|
||||
"sass": "^1.26.10",
|
||||
"stylelint": "10.1.0",
|
||||
"stylelint-order": "3.0.1",
|
||||
"swiper": "5.4.1",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
|
||||
export default {
|
||||
input: 'src/components/slides/swiper/swiper.js',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
const path = require('path');
|
||||
const { rollup } = require('rollup');
|
||||
const virtual = require('rollup-plugin-virtual');
|
||||
const virtual = require('@rollup/plugin-virtual');
|
||||
const fs = require('fs');
|
||||
|
||||
function main() {
|
||||
|
||||
36
core/src/components.d.ts
vendored
36
core/src/components.d.ts
vendored
@@ -5,8 +5,9 @@
|
||||
* It contains typing information for all components that exist in this project.
|
||||
*/
|
||||
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
|
||||
import { ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DatetimeOptions, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, MenuChangeEventDetail, NavComponent, NavOptions, OverlayEventDetail, PickerButton, PickerColumn, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, ViewController } from "./interface";
|
||||
import { ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DatetimeOptions, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, MenuChangeEventDetail, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerButton, PickerColumn, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, ViewController } from "./interface";
|
||||
import { IonicSafeString } from "./utils/sanitization";
|
||||
import { NavigationHookCallback } from "./components/route/route-interface";
|
||||
import { SelectCompareFn } from "./components/select/select-interface";
|
||||
export namespace Components {
|
||||
interface IonActionSheet {
|
||||
@@ -1412,7 +1413,7 @@ export namespace Components {
|
||||
* @param opts The navigation options.
|
||||
* @param done The transition complete function.
|
||||
*/
|
||||
"insertPages": (insertIndex: number, insertComponents: NavComponent[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>;
|
||||
"insertPages": (insertIndex: number, insertComponents: NavComponent[] | NavComponentWithProps[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>;
|
||||
/**
|
||||
* Pop a component off of the navigation stack. Navigates back from the current component.
|
||||
* @param opts The navigation options.
|
||||
@@ -1462,7 +1463,7 @@ export namespace Components {
|
||||
* @param opts The navigation options.
|
||||
* @param done The transition complete function.
|
||||
*/
|
||||
"setPages": (views: any[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>;
|
||||
"setPages": (views: NavComponent[] | NavComponentWithProps[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>;
|
||||
/**
|
||||
* Set the root for the current navigation stack to a component.
|
||||
* @param component The component to set as the root of the navigation stack.
|
||||
@@ -1849,6 +1850,14 @@ export namespace Components {
|
||||
"type": 'bounded' | 'unbounded';
|
||||
}
|
||||
interface IonRoute {
|
||||
/**
|
||||
* A navigation hook that is fired when the route tries to enter. Returning `true` allows the navigation to proceed, while returning `false` causes it to be cancelled. Returning a `NavigationHookOptions` object causes the router to redirect to the path specified.
|
||||
*/
|
||||
"beforeEnter"?: NavigationHookCallback;
|
||||
/**
|
||||
* A navigation hook that is fired when the route tries to leave. Returning `true` allows the navigation to proceed, while returning `false` causes it to be cancelled. Returning a `NavigationHookOptions` object causes the router to redirect to the path specified.
|
||||
*/
|
||||
"beforeLeave"?: NavigationHookCallback;
|
||||
/**
|
||||
* Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-nav`) when the route matches. The value of this property is not always the tagname of the component to load, in `ion-tabs` it actually refers to the name of the `ion-tab` to select.
|
||||
*/
|
||||
@@ -1877,6 +1886,7 @@ export namespace Components {
|
||||
* Go back to previous page in the window.history.
|
||||
*/
|
||||
"back": () => Promise<void>;
|
||||
"canTransition": () => Promise<string | boolean>;
|
||||
"navChanged": (direction: RouterDirection) => Promise<boolean>;
|
||||
"printDebug": () => Promise<void>;
|
||||
/**
|
||||
@@ -2124,7 +2134,7 @@ export namespace Components {
|
||||
}
|
||||
interface IonSelectOption {
|
||||
/**
|
||||
* If `true`, the user cannot interact with the select option.
|
||||
* If `true`, the user cannot interact with the select option. This property does not apply when `interface="action-sheet"` as `ion-action-sheet` does not allow for disabled buttons.
|
||||
*/
|
||||
"disabled": boolean;
|
||||
/**
|
||||
@@ -4215,7 +4225,7 @@ declare namespace LocalJSX {
|
||||
/**
|
||||
* Emitted when the input loses focus.
|
||||
*/
|
||||
"onIonBlur"?: (event: CustomEvent<void>) => void;
|
||||
"onIonBlur"?: (event: CustomEvent<FocusEvent>) => void;
|
||||
/**
|
||||
* Emitted when the value has changed.
|
||||
*/
|
||||
@@ -4223,7 +4233,7 @@ declare namespace LocalJSX {
|
||||
/**
|
||||
* Emitted when the input has focus.
|
||||
*/
|
||||
"onIonFocus"?: (event: CustomEvent<void>) => void;
|
||||
"onIonFocus"?: (event: CustomEvent<FocusEvent>) => void;
|
||||
/**
|
||||
* Emitted when a keyboard input occurred.
|
||||
*/
|
||||
@@ -5095,6 +5105,14 @@ declare namespace LocalJSX {
|
||||
"type"?: 'bounded' | 'unbounded';
|
||||
}
|
||||
interface IonRoute {
|
||||
/**
|
||||
* A navigation hook that is fired when the route tries to enter. Returning `true` allows the navigation to proceed, while returning `false` causes it to be cancelled. Returning a `NavigationHookOptions` object causes the router to redirect to the path specified.
|
||||
*/
|
||||
"beforeEnter"?: NavigationHookCallback;
|
||||
/**
|
||||
* A navigation hook that is fired when the route tries to leave. Returning `true` allows the navigation to proceed, while returning `false` causes it to be cancelled. Returning a `NavigationHookOptions` object causes the router to redirect to the path specified.
|
||||
*/
|
||||
"beforeLeave"?: NavigationHookCallback;
|
||||
/**
|
||||
* Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-nav`) when the route matches. The value of this property is not always the tagname of the component to load, in `ion-tabs` it actually refers to the name of the `ion-tab` to select.
|
||||
*/
|
||||
@@ -5421,7 +5439,7 @@ declare namespace LocalJSX {
|
||||
}
|
||||
interface IonSelectOption {
|
||||
/**
|
||||
* If `true`, the user cannot interact with the select option.
|
||||
* If `true`, the user cannot interact with the select option. This property does not apply when `interface="action-sheet"` as `ion-action-sheet` does not allow for disabled buttons.
|
||||
*/
|
||||
"disabled"?: boolean;
|
||||
/**
|
||||
@@ -5731,7 +5749,7 @@ declare namespace LocalJSX {
|
||||
/**
|
||||
* Emitted when the input loses focus.
|
||||
*/
|
||||
"onIonBlur"?: (event: CustomEvent<void>) => void;
|
||||
"onIonBlur"?: (event: CustomEvent<FocusEvent>) => void;
|
||||
/**
|
||||
* Emitted when the input value has changed.
|
||||
*/
|
||||
@@ -5739,7 +5757,7 @@ declare namespace LocalJSX {
|
||||
/**
|
||||
* Emitted when the input has focus.
|
||||
*/
|
||||
"onIonFocus"?: (event: CustomEvent<void>) => void;
|
||||
"onIonFocus"?: (event: CustomEvent<FocusEvent>) => void;
|
||||
/**
|
||||
* Emitted when a keyboard input occurred.
|
||||
*/
|
||||
|
||||
@@ -26,6 +26,7 @@ import { mdLeaveAnimation } from './animations/md.leave';
|
||||
export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
|
||||
presented = false;
|
||||
lastFocus?: HTMLElement;
|
||||
animation?: any;
|
||||
private wrapperEl?: HTMLElement;
|
||||
private groupEl?: HTMLElement;
|
||||
@@ -117,7 +118,7 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
return present(this, 'actionSheetEnter', iosEnterAnimation, mdEnterAnimation);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
connectedCallback() {
|
||||
prepareOverlay(this.el);
|
||||
}
|
||||
|
||||
@@ -250,7 +251,10 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
onIonBackdropTap={this.onBackdropTap}
|
||||
>
|
||||
<ion-backdrop tappable={this.backdropDismiss}/>
|
||||
<div class="action-sheet-wrapper" role="dialog" ref={el => this.wrapperEl = el}>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
|
||||
<div class="action-sheet-wrapper ion-overlay-wrapper" role="dialog" ref={el => this.wrapperEl = el}>
|
||||
<div class="action-sheet-container">
|
||||
<div class="action-sheet-group" ref={el => this.groupEl = el}>
|
||||
{this.header !== undefined &&
|
||||
@@ -292,6 +296,8 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,40 @@
|
||||
import { testActionSheet, testActionSheetAlert, testActionSheetBackdrop } from '../test.utils';
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
const DIRECTORY = 'basic';
|
||||
const getActiveElementText = async (page) => {
|
||||
const activeElement = await page.evaluateHandle(() => document.activeElement);
|
||||
return await page.evaluate(el => el && el.textContent, activeElement);
|
||||
}
|
||||
|
||||
test('action-sheet: focus trap', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/action-sheet/test/basic?ionic:_testing=true' });
|
||||
|
||||
await page.click('#basic');
|
||||
await page.waitForSelector('#basic');
|
||||
|
||||
let actionSheet = await page.find('ion-action-sheet');
|
||||
|
||||
expect(actionSheet).not.toBe(null);
|
||||
await actionSheet.waitForVisible();
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementText = await getActiveElementText(page);
|
||||
expect(activeElementText).toEqual('Delete');
|
||||
|
||||
await page.keyboard.down('Shift');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
const activeElementTextTwo = await getActiveElementText(page);
|
||||
expect(activeElementTextTwo).toEqual('Cancel');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementTextThree = await getActiveElementText(page);
|
||||
expect(activeElementTextThree).toEqual('Delete');
|
||||
});
|
||||
|
||||
test('action-sheet: basic', async () => {
|
||||
await testActionSheet(DIRECTORY, '#basic');
|
||||
|
||||
@@ -34,6 +34,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
private gesture?: Gesture;
|
||||
|
||||
presented = false;
|
||||
lastFocus?: HTMLElement;
|
||||
|
||||
@Element() el!: HTMLIonAlertElement;
|
||||
|
||||
@@ -167,7 +168,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
}) as AlertInput);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
connectedCallback() {
|
||||
prepareOverlay(this.el);
|
||||
}
|
||||
|
||||
@@ -514,7 +515,9 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
|
||||
<ion-backdrop tappable={this.backdropDismiss}/>
|
||||
|
||||
<div class="alert-wrapper" ref={el => this.wrapperEl = el}>
|
||||
<div tabindex="0"></div>
|
||||
|
||||
<div class="alert-wrapper ion-overlay-wrapper" ref={el => this.wrapperEl = el}>
|
||||
|
||||
<div class="alert-head">
|
||||
{header && <h2 id={hdrId} class="alert-title">{header}</h2>}
|
||||
@@ -527,6 +530,8 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
{this.renderAlertButtons()}
|
||||
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,40 @@
|
||||
import { testAlert } from '../test.utils';
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
const DIRECTORY = 'basic';
|
||||
const getActiveElementText = async (page) => {
|
||||
const activeElement = await page.evaluateHandle(() => document.activeElement);
|
||||
return await page.evaluate(el => el && el.textContent, activeElement);
|
||||
}
|
||||
|
||||
test('alert: focus trap', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/alert/test/basic?ionic:_testing=true' });
|
||||
|
||||
await page.click('#multipleButtons');
|
||||
await page.waitForSelector('#multipleButtons');
|
||||
|
||||
let alert = await page.find('ion-alert');
|
||||
|
||||
expect(alert).not.toBe(null);
|
||||
await alert.waitForVisible();
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementText = await getActiveElementText(page);
|
||||
expect(activeElementText).toEqual('Open Modal');
|
||||
|
||||
await page.keyboard.down('Shift');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
const activeElementTextTwo = await getActiveElementText(page);
|
||||
expect(activeElementTextTwo).toEqual('Cancel');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementTextThree = await getActiveElementText(page);
|
||||
expect(activeElementTextThree).toEqual('Open Modal');
|
||||
});
|
||||
|
||||
test(`alert: basic`, async () => {
|
||||
await testAlert(DIRECTORY, '#basic');
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
// --------------------------------------------------
|
||||
|
||||
:host {
|
||||
--background: #{$item-ios-background};
|
||||
--color: #{$card-ios-text-color};
|
||||
--background: #{$card-ios-background};
|
||||
--color: #{$card-ios-color};
|
||||
|
||||
@include margin($card-ios-margin-top, $card-ios-margin-end, $card-ios-margin-bottom, $card-ios-margin-start);
|
||||
@include border-radius($card-ios-border-radius);
|
||||
@@ -22,4 +22,4 @@
|
||||
|
||||
:host(.ion-activated) {
|
||||
transform: #{$card-ios-transform-activated};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,6 @@ $card-ios-margin-bottom: $card-ios-margin-top !default;
|
||||
/// @prop - Margin start of the card
|
||||
$card-ios-margin-start: 16px !default;
|
||||
|
||||
/// @prop - Background color of the card
|
||||
$card-ios-background-color: $item-ios-background !default;
|
||||
|
||||
/// @prop - Box shadow color of the card
|
||||
$card-ios-box-shadow-color: rgba(0, 0, 0, .12) !default;
|
||||
|
||||
@@ -30,11 +27,8 @@ $card-ios-border-radius: 8px !default;
|
||||
/// @prop - Font size of the card
|
||||
$card-ios-font-size: 14px !default;
|
||||
|
||||
/// @prop - Color of the card text
|
||||
$card-ios-text-color: $text-color-step-400 !default;
|
||||
|
||||
/// @prop - Transition timing function of the card
|
||||
$card-ios-transition-timing-function: cubic-bezier(0.12, 0.72, 0.29, 1) !default;
|
||||
|
||||
/// @prop - Transform of the card on activate
|
||||
$card-ios-transform-activated: scale3d(.97, .97, 1) !default;
|
||||
$card-ios-transform-activated: scale3d(.97, .97, 1) !default;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
// --------------------------------------------------
|
||||
|
||||
:host {
|
||||
--background: #{$item-md-background};
|
||||
--color: #{$card-md-text-color};
|
||||
--background: #{$card-md-background};
|
||||
--color: #{$card-md-color};
|
||||
|
||||
@include margin($card-md-margin-top, $card-md-margin-end, $card-md-margin-bottom, $card-md-margin-start);
|
||||
@include border-radius($card-md-border-radius);
|
||||
|
||||
@@ -15,9 +15,6 @@ $card-md-margin-bottom: 10px !default;
|
||||
/// @prop - Margin start of the card
|
||||
$card-md-margin-start: 10px !default;
|
||||
|
||||
/// @prop - Background color of the card
|
||||
$card-md-background-color: $item-md-background !default;
|
||||
|
||||
/// @prop - Box shadow of the card
|
||||
$card-md-box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12) !default;
|
||||
|
||||
@@ -29,6 +26,3 @@ $card-md-font-size: 14px !default;
|
||||
|
||||
/// @prop - Line height of the card
|
||||
$card-md-line-height: 1.5 !default;
|
||||
|
||||
/// @prop - Color of the card text
|
||||
$card-md-text-color: $text-color-step-450 !default;
|
||||
|
||||
@@ -632,7 +632,6 @@ export class Datetime implements ComponentInterface {
|
||||
return (
|
||||
<Host
|
||||
onClick={this.onClick}
|
||||
role="combobox"
|
||||
aria-disabled={disabled ? 'true' : null}
|
||||
aria-expanded={`${isExpanded}`}
|
||||
aria-haspopup="true"
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
const getActiveElementText = async (page) => {
|
||||
const activeElement = await page.evaluateHandle(() => document.activeElement);
|
||||
return await page.evaluate(el => el && el.textContent, activeElement);
|
||||
}
|
||||
|
||||
test('datetime/picker: focus trap', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/datetime/test/basic?ionic:_testing=true' });
|
||||
|
||||
await page.click('#datetime-part');
|
||||
await page.waitForSelector('#datetime-part');
|
||||
|
||||
let datetime = await page.find('ion-datetime');
|
||||
|
||||
expect(datetime).not.toBe(null);
|
||||
await datetime.waitForVisible();
|
||||
|
||||
// TODO fix
|
||||
await page.waitFor(100);
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementText = await getActiveElementText(page);
|
||||
expect(activeElementText).toEqual('Cancel');
|
||||
|
||||
await page.keyboard.down('Shift');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
const activeElementTextTwo = await getActiveElementText(page);
|
||||
expect(activeElementTextTwo).toEqual('1920');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementTextThree = await getActiveElementText(page);
|
||||
expect(activeElementTextThree).toEqual('Cancel');
|
||||
});
|
||||
|
||||
test('datetime: basic', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/datetime/test/basic?ionic:_testing=true'
|
||||
|
||||
@@ -150,7 +150,6 @@ export const scaleLargeTitles = (toolbars: ToolbarIndex[] = [], scale = 1, trans
|
||||
const titleDiv = toolbar.innerTitleEl;
|
||||
if (!ionTitle || ionTitle.size !== 'large') { return; }
|
||||
|
||||
titleDiv.style.transformOrigin = 'left center';
|
||||
titleDiv.style.transition = (transition) ? TRANSITION : '';
|
||||
titleDiv.style.transform = `scale3d(${scale}, ${scale}, 1)`;
|
||||
});
|
||||
|
||||
@@ -147,7 +147,11 @@
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
:host(.has-focus.has-value) .input-clear-icon {
|
||||
.input-clear-icon:focus {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
:host(.has-value) .input-clear-icon {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
|
||||
@@ -201,12 +201,12 @@ export class Input implements ComponentInterface {
|
||||
/**
|
||||
* Emitted when the input loses focus.
|
||||
*/
|
||||
@Event() ionBlur!: EventEmitter<void>;
|
||||
@Event() ionBlur!: EventEmitter<FocusEvent>;
|
||||
|
||||
/**
|
||||
* Emitted when the input has focus.
|
||||
*/
|
||||
@Event() ionFocus!: EventEmitter<void>;
|
||||
@Event() ionFocus!: EventEmitter<FocusEvent>;
|
||||
|
||||
/**
|
||||
* Emitted when the styles change.
|
||||
@@ -293,20 +293,20 @@ export class Input implements ComponentInterface {
|
||||
this.ionInput.emit(ev as KeyboardEvent);
|
||||
}
|
||||
|
||||
private onBlur = () => {
|
||||
private onBlur = (ev: FocusEvent) => {
|
||||
this.hasFocus = false;
|
||||
this.focusChanged();
|
||||
this.emitStyle();
|
||||
|
||||
this.ionBlur.emit();
|
||||
this.ionBlur.emit(ev);
|
||||
}
|
||||
|
||||
private onFocus = () => {
|
||||
private onFocus = (ev: FocusEvent) => {
|
||||
this.hasFocus = true;
|
||||
this.focusChanged();
|
||||
this.emitStyle();
|
||||
|
||||
this.ionFocus.emit();
|
||||
this.ionFocus.emit(ev);
|
||||
}
|
||||
|
||||
private onKeydown = (ev: KeyboardEvent) => {
|
||||
@@ -323,6 +323,10 @@ export class Input implements ComponentInterface {
|
||||
}
|
||||
}
|
||||
|
||||
private clearTextOnEnter = (ev: KeyboardEvent) => {
|
||||
if (ev.key === 'Enter') { this.clearTextInput(ev); }
|
||||
}
|
||||
|
||||
private clearTextInput = (ev?: Event) => {
|
||||
if (this.clearInput && !this.readonly && !this.disabled && ev) {
|
||||
ev.preventDefault();
|
||||
@@ -405,11 +409,12 @@ export class Input implements ComponentInterface {
|
||||
onKeyDown={this.onKeydown}
|
||||
/>
|
||||
{(this.clearInput && !this.readonly && !this.disabled) && <button
|
||||
aria-label="reset"
|
||||
type="button"
|
||||
class="input-clear-icon"
|
||||
tabindex="-1"
|
||||
onTouchStart={this.clearTextInput}
|
||||
onMouseDown={this.clearTextInput}
|
||||
onKeyDown={this.clearTextOnEnter}
|
||||
/>}
|
||||
</Host>
|
||||
);
|
||||
|
||||
@@ -317,7 +317,7 @@ export class InputExample {
|
||||
| `size` | `size` | The initial size of the control. This value is in pixels unless the value of the type attribute is `"text"` or `"password"`, in which case it is an integer number of characters. This attribute applies only when the `type` attribute is set to `"text"`, `"search"`, `"tel"`, `"url"`, `"email"`, or `"password"`, otherwise it is ignored. | `number \| undefined` | `undefined` |
|
||||
| `spellcheck` | `spellcheck` | If `true`, the element will have its spelling and grammar checked. | `boolean` | `false` |
|
||||
| `step` | `step` | Works with the min and max attributes to limit the increments at which a value can be set. Possible values are: `"any"` or a positive floating point number. | `string \| undefined` | `undefined` |
|
||||
| `type` | `type` | The type of control to display. The default type is text. | `"date" \| "email" \| "number" \| "password" \| "search" \| "tel" \| "text" \| "time" \| "url"` | `'text'` |
|
||||
| `type` | `type` | The type of control to display. The default type is text. | `"date" \| "datetime-local" \| "email" \| "month" \| "number" \| "password" \| "search" \| "tel" \| "text" \| "time" \| "url" \| "week"` | `'text'` |
|
||||
| `value` | `value` | The value of the input. | `null \| number \| string \| undefined` | `''` |
|
||||
|
||||
|
||||
@@ -325,9 +325,9 @@ export class InputExample {
|
||||
|
||||
| Event | Description | Type |
|
||||
| ----------- | --------------------------------------- | ------------------------------------- |
|
||||
| `ionBlur` | Emitted when the input loses focus. | `CustomEvent<void>` |
|
||||
| `ionBlur` | Emitted when the input loses focus. | `CustomEvent<FocusEvent>` |
|
||||
| `ionChange` | Emitted when the value has changed. | `CustomEvent<InputChangeEventDetail>` |
|
||||
| `ionFocus` | Emitted when the input has focus. | `CustomEvent<void>` |
|
||||
| `ionFocus` | Emitted when the input has focus. | `CustomEvent<FocusEvent>` |
|
||||
| `ionInput` | Emitted when a keyboard input occurred. | `CustomEvent<KeyboardEvent>` |
|
||||
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
</ion-content>
|
||||
|
||||
<script>
|
||||
document.querySelector('ion-input').addEventListener('ionBlur', (ev) => { console.log(ev)})
|
||||
function toggleBoolean(id, prop) {
|
||||
var el = document.getElementById(id);
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ export class Loading implements ComponentInterface, OverlayInterface {
|
||||
private durationTimeout: any;
|
||||
|
||||
presented = false;
|
||||
lastFocus?: HTMLElement;
|
||||
|
||||
@Element() el!: HTMLIonLoadingElement;
|
||||
|
||||
@@ -111,7 +112,7 @@ export class Loading implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Event({ eventName: 'ionLoadingDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
constructor() {
|
||||
connectedCallback() {
|
||||
prepareOverlay(this.el);
|
||||
}
|
||||
|
||||
@@ -194,7 +195,10 @@ export class Loading implements ComponentInterface, OverlayInterface {
|
||||
}}
|
||||
>
|
||||
<ion-backdrop visible={this.showBackdrop} tappable={this.backdropDismiss} />
|
||||
<div class="loading-wrapper" role="dialog">
|
||||
|
||||
<div tabindex="0"></div>
|
||||
|
||||
<div class="loading-wrapper ion-overlay-wrapper" role="dialog">
|
||||
{spinner && (
|
||||
<div class="loading-spinner">
|
||||
<ion-spinner name={spinner} aria-hidden="true" />
|
||||
@@ -203,6 +207,8 @@ export class Loading implements ComponentInterface, OverlayInterface {
|
||||
|
||||
{message && <div class="loading-content" innerHTML={sanitizeDOMString(message)}></div>}
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,6 @@ export class LoadingExample {
|
||||
|
||||
async presentLoadingWithOptions() {
|
||||
const loading = await this.loadingController.create({
|
||||
cssClass: 'my-custom-class',
|
||||
spinner: null,
|
||||
duration: 5000,
|
||||
message: 'Click the backdrop to dismiss early...',
|
||||
@@ -113,7 +112,6 @@ async function presentLoading() {
|
||||
async function presentLoadingWithOptions() {
|
||||
const loading = document.createElement('ion-loading');
|
||||
|
||||
loading.cssClass = 'my-custom-class';
|
||||
loading.spinner = null;
|
||||
loading.duration = 5000;
|
||||
loading.message = 'Click the backdrop to dismiss early...';
|
||||
@@ -185,7 +183,6 @@ export class LoadingExample {
|
||||
|
||||
async presentLoadingWithOptions() {
|
||||
const loading = await loadingController.create({
|
||||
cssClass: 'my-custom-class',
|
||||
spinner: null,
|
||||
duration: 5000,
|
||||
message: 'Click the backdrop to dismiss early...',
|
||||
@@ -245,7 +242,6 @@ export default {
|
||||
presentLoadingWithOptions() {
|
||||
return this.$ionic.loadingController
|
||||
.create({
|
||||
cssClass: 'my-custom-class',
|
||||
spinner: null,
|
||||
duration: this.timeout,
|
||||
message: 'Click the backdrop to dismiss early...',
|
||||
|
||||
@@ -1,6 +1,40 @@
|
||||
import { testLoading } from '../test.utils';
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
const DIRECTORY = 'basic';
|
||||
const getActiveElementText = async (page) => {
|
||||
const activeElement = await page.evaluateHandle(() => document.activeElement);
|
||||
return await page.evaluate(el => el && el.textContent, activeElement);
|
||||
}
|
||||
|
||||
test('loading: focus trap', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/loading/test/basic?ionic:_testing=true' });
|
||||
|
||||
await page.click('#html-content-loading');
|
||||
await page.waitForSelector('#html-content-loading');
|
||||
|
||||
let loading = await page.find('ion-loading');
|
||||
|
||||
expect(loading).not.toBe(null);
|
||||
await loading.waitForVisible();
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementText = await getActiveElementText(page);
|
||||
expect(activeElementText).toEqual('Click impatiently to load faster');
|
||||
|
||||
await page.keyboard.down('Shift');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
const activeElementTextTwo = await getActiveElementText(page);
|
||||
expect(activeElementTextTwo).toEqual('Click impatiently to load faster');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementTextThree = await getActiveElementText(page);
|
||||
expect(activeElementTextThree).toEqual('Click impatiently to load faster');
|
||||
});
|
||||
|
||||
test('loading: basic', async () => {
|
||||
await testLoading(DIRECTORY, '#basic-loading');
|
||||
|
||||
@@ -24,7 +24,6 @@ export class LoadingExample {
|
||||
|
||||
async presentLoadingWithOptions() {
|
||||
const loading = await this.loadingController.create({
|
||||
cssClass: 'my-custom-class',
|
||||
spinner: null,
|
||||
duration: 5000,
|
||||
message: 'Click the backdrop to dismiss early...',
|
||||
|
||||
@@ -16,7 +16,6 @@ async function presentLoading() {
|
||||
async function presentLoadingWithOptions() {
|
||||
const loading = document.createElement('ion-loading');
|
||||
|
||||
loading.cssClass = 'my-custom-class';
|
||||
loading.spinner = null;
|
||||
loading.duration = 5000;
|
||||
loading.message = 'Click the backdrop to dismiss early...';
|
||||
|
||||
@@ -22,7 +22,6 @@ export class LoadingExample {
|
||||
|
||||
async presentLoadingWithOptions() {
|
||||
const loading = await loadingController.create({
|
||||
cssClass: 'my-custom-class',
|
||||
spinner: null,
|
||||
duration: 5000,
|
||||
message: 'Click the backdrop to dismiss early...',
|
||||
|
||||
@@ -30,7 +30,6 @@ export default {
|
||||
presentLoadingWithOptions() {
|
||||
return this.$ionic.loadingController
|
||||
.create({
|
||||
cssClass: 'my-custom-class',
|
||||
spinner: null,
|
||||
duration: this.timeout,
|
||||
message: 'Click the backdrop to dismiss early...',
|
||||
|
||||
@@ -34,6 +34,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
// Whether or not modal is being dismissed via gesture
|
||||
private gestureAnimationDismissing = false;
|
||||
presented = false;
|
||||
lastFocus?: HTMLElement;
|
||||
animation?: Animation;
|
||||
|
||||
@Element() el!: HTMLIonModalElement;
|
||||
@@ -130,7 +131,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
connectedCallback() {
|
||||
prepareOverlay(this.el);
|
||||
}
|
||||
|
||||
@@ -289,11 +290,16 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
<ion-backdrop visible={this.showBackdrop} tappable={this.backdropDismiss}/>
|
||||
|
||||
{mode === 'ios' && <div class="modal-shadow"></div>}
|
||||
|
||||
<div tabindex="0"></div>
|
||||
|
||||
<div
|
||||
role="dialog"
|
||||
class="modal-wrapper"
|
||||
class="modal-wrapper ion-overlay-wrapper"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -173,6 +173,8 @@ export class CalendarComponentModule {}
|
||||
|
||||
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
|
||||
|
||||
> Card style modals when running on iPhone-sized devices do not have backdrops. As a result, the `--backdrop-opacity` variable will not have any effect.
|
||||
|
||||
If you are creating an application that uses `ion-tabs`, it is recommended that you get the parent `ion-router-outlet` using `this.routerOutlet.parentOutlet.nativeEl`, otherwise the tabbar will not scale down when the modal opens.
|
||||
|
||||
```javascript
|
||||
@@ -302,6 +304,8 @@ console.log(data);
|
||||
|
||||
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
|
||||
|
||||
> Card style modals when running on iPhone-sized devices do not have backdrops. As a result, the `--backdrop-opacity` variable will not have any effect.
|
||||
|
||||
```javascript
|
||||
const modalElement = document.createElement('ion-modal');
|
||||
modalElement.component = 'modal-page';
|
||||
@@ -346,19 +350,51 @@ export const ModalExample: React.FC = () => {
|
||||
|
||||
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
|
||||
|
||||
> Card style modals when running on iPhone-sized devices do not have backdrops. As a result, the `--backdrop-opacity` variable will not have any effect.
|
||||
|
||||
```tsx
|
||||
<IonModal
|
||||
isOpen={showModal}
|
||||
cssClass='my-custom-class'
|
||||
swipeToClose={true}
|
||||
presentingElement={pageRef.current}
|
||||
onDidDismiss={() => setShowModal(false)}>
|
||||
<p>This is modal content</p>
|
||||
<IonButton onClick={() => setShowModal(false)}>Close Modal</IonButton>
|
||||
</IonModal>
|
||||
const App: React.FC = () => {
|
||||
const routerRef = useRef<HTMLIonRouterOutletElement | null>(null);
|
||||
|
||||
return (
|
||||
<IonApp>
|
||||
<IonReactRouter>
|
||||
<IonRouterOutlet ref={routerRef}>
|
||||
<Route path="/home" render={() => <Home router={routerRef.current.current} />} exact={true} />
|
||||
</IonRouterOutlet>
|
||||
</IonReactRouter>
|
||||
</IonApp>
|
||||
)
|
||||
};
|
||||
|
||||
...
|
||||
|
||||
interface HomePageProps {
|
||||
router: HTMLIonRouterOutletElement | null;
|
||||
}
|
||||
|
||||
const Home: React.FC<HomePageProps> = ({ router }) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
return (
|
||||
...
|
||||
|
||||
<IonModal
|
||||
isOpen={showModal}
|
||||
cssClass='my-custom-class'
|
||||
swipeToClose={true}
|
||||
presentingElement={router || undefined}
|
||||
onDidDismiss={() => setShowModal(false)}>
|
||||
<p>This is modal content</p>
|
||||
</IonModal>
|
||||
|
||||
...
|
||||
);
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
In most scenarios, setting a ref on `IonPage` and passing that ref's `current` value to `presentingElement` is fine. In cases where you are presenting a card-style modal from within another modal, you should pass in the top-most `ion-modal` ref as the `presentingElement`.
|
||||
In most scenarios, setting a ref on `IonRouterOutlet` and passing that ref's `current` value to `presentingElement` is fine. In cases where you are presenting a card-style modal from within another modal, you should pass in the top-most `ion-modal` ref as the `presentingElement`.
|
||||
|
||||
```tsx
|
||||
<IonModal
|
||||
@@ -366,7 +402,7 @@ In most scenarios, setting a ref on `IonPage` and passing that ref's `current` v
|
||||
isOpen={showModal}
|
||||
cssClass='my-custom-class'
|
||||
swipeToClose={true}
|
||||
presentingElement={pageRef.current}
|
||||
presentingElement={router || undefined}
|
||||
onDidDismiss={() => setShowModal(false)}>
|
||||
<p>This is modal content</p>
|
||||
<IonButton onClick={() => setShow2ndModal(true)}>Show 2nd Modal</IonButton>
|
||||
@@ -496,6 +532,8 @@ console.log(data);
|
||||
|
||||
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
|
||||
|
||||
> Card style modals when running on iPhone-sized devices do not have backdrops. As a result, the `--backdrop-opacity` variable will not have any effect.
|
||||
|
||||
```tsx
|
||||
import { Component, Element, h } from '@stencil/core';
|
||||
|
||||
|
||||
@@ -1,6 +1,43 @@
|
||||
import { testModal } from '../test.utils';
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
const DIRECTORY = 'basic';
|
||||
const getActiveElementText = async (page) => {
|
||||
const activeElement = await page.evaluateHandle(() => document.activeElement);
|
||||
return await page.evaluate(el => el && el.textContent, activeElement);
|
||||
}
|
||||
|
||||
test('modal: focus trap', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/modal/test/basic?ionic:_testing=true' });
|
||||
|
||||
await page.click('#basic-modal');
|
||||
await page.waitForSelector('#basic-modal');
|
||||
|
||||
let modal = await page.find('ion-modal');
|
||||
|
||||
expect(modal).not.toBe(null);
|
||||
await modal.waitForVisible();
|
||||
|
||||
// TODO fix
|
||||
await page.waitFor(50);
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementText = await getActiveElementText(page);
|
||||
expect(activeElementText).toEqual('Dismiss Modal');
|
||||
|
||||
await page.keyboard.down('Shift');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
const activeElementTextTwo = await getActiveElementText(page);
|
||||
expect(activeElementTextTwo).toEqual('Dismiss Modal');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementTextThree = await getActiveElementText(page);
|
||||
expect(activeElementTextThree).toEqual('Dismiss Modal');
|
||||
});
|
||||
|
||||
test('modal: basic', async () => {
|
||||
await testModal(DIRECTORY, '#basic-modal');
|
||||
|
||||
@@ -130,6 +130,8 @@ export class CalendarComponentModule {}
|
||||
|
||||
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
|
||||
|
||||
> Card style modals when running on iPhone-sized devices do not have backdrops. As a result, the `--backdrop-opacity` variable will not have any effect.
|
||||
|
||||
If you are creating an application that uses `ion-tabs`, it is recommended that you get the parent `ion-router-outlet` using `this.routerOutlet.parentOutlet.nativeEl`, otherwise the tabbar will not scale down when the modal opens.
|
||||
|
||||
```javascript
|
||||
|
||||
@@ -84,6 +84,8 @@ console.log(data);
|
||||
|
||||
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
|
||||
|
||||
> Card style modals when running on iPhone-sized devices do not have backdrops. As a result, the `--backdrop-opacity` variable will not have any effect.
|
||||
|
||||
```javascript
|
||||
const modalElement = document.createElement('ion-modal');
|
||||
modalElement.component = 'modal-page';
|
||||
|
||||
@@ -21,19 +21,51 @@ export const ModalExample: React.FC = () => {
|
||||
|
||||
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
|
||||
|
||||
> Card style modals when running on iPhone-sized devices do not have backdrops. As a result, the `--backdrop-opacity` variable will not have any effect.
|
||||
|
||||
```tsx
|
||||
<IonModal
|
||||
isOpen={showModal}
|
||||
cssClass='my-custom-class'
|
||||
swipeToClose={true}
|
||||
presentingElement={pageRef.current}
|
||||
onDidDismiss={() => setShowModal(false)}>
|
||||
<p>This is modal content</p>
|
||||
<IonButton onClick={() => setShowModal(false)}>Close Modal</IonButton>
|
||||
</IonModal>
|
||||
const App: React.FC = () => {
|
||||
const routerRef = useRef<HTMLIonRouterOutletElement | null>(null);
|
||||
|
||||
return (
|
||||
<IonApp>
|
||||
<IonReactRouter>
|
||||
<IonRouterOutlet ref={routerRef}>
|
||||
<Route path="/home" render={() => <Home router={routerRef.current.current} />} exact={true} />
|
||||
</IonRouterOutlet>
|
||||
</IonReactRouter>
|
||||
</IonApp>
|
||||
)
|
||||
};
|
||||
|
||||
...
|
||||
|
||||
interface HomePageProps {
|
||||
router: HTMLIonRouterOutletElement | null;
|
||||
}
|
||||
|
||||
const Home: React.FC<HomePageProps> = ({ router }) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
return (
|
||||
...
|
||||
|
||||
<IonModal
|
||||
isOpen={showModal}
|
||||
cssClass='my-custom-class'
|
||||
swipeToClose={true}
|
||||
presentingElement={router || undefined}
|
||||
onDidDismiss={() => setShowModal(false)}>
|
||||
<p>This is modal content</p>
|
||||
</IonModal>
|
||||
|
||||
...
|
||||
);
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
In most scenarios, setting a ref on `IonPage` and passing that ref's `current` value to `presentingElement` is fine. In cases where you are presenting a card-style modal from within another modal, you should pass in the top-most `ion-modal` ref as the `presentingElement`.
|
||||
In most scenarios, setting a ref on `IonRouterOutlet` and passing that ref's `current` value to `presentingElement` is fine. In cases where you are presenting a card-style modal from within another modal, you should pass in the top-most `ion-modal` ref as the `presentingElement`.
|
||||
|
||||
```tsx
|
||||
<IonModal
|
||||
@@ -41,7 +73,7 @@ In most scenarios, setting a ref on `IonPage` and passing that ref's `current` v
|
||||
isOpen={showModal}
|
||||
cssClass='my-custom-class'
|
||||
swipeToClose={true}
|
||||
presentingElement={pageRef.current}
|
||||
presentingElement={router || undefined}
|
||||
onDidDismiss={() => setShowModal(false)}>
|
||||
<p>This is modal content</p>
|
||||
<IonButton onClick={() => setShow2ndModal(true)}>Show 2nd Modal</IonButton>
|
||||
|
||||
@@ -109,6 +109,8 @@ console.log(data);
|
||||
|
||||
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
|
||||
|
||||
> Card style modals when running on iPhone-sized devices do not have backdrops. As a result, the `--backdrop-opacity` variable will not have any effect.
|
||||
|
||||
```tsx
|
||||
import { Component, Element, h } from '@stencil/core';
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { Animation, AnimationBuilder, ComponentRef, FrameworkDelegate, Mode } from '../../interface';
|
||||
import { Animation, AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, Mode } from '../../interface';
|
||||
|
||||
import { ViewController } from './view-controller';
|
||||
|
||||
export type NavDirection = 'back' | 'forward';
|
||||
|
||||
export type NavComponent = ComponentRef | ViewController;
|
||||
export interface NavComponentWithProps<T = any> {
|
||||
component: NavComponent;
|
||||
componentProps?: ComponentProps<T> | null;
|
||||
}
|
||||
|
||||
export interface NavResult {
|
||||
hasCompleted: boolean;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Build, Component, Element, Event, EventEmitter, Method, Prop, Watch, h
|
||||
|
||||
import { config } from '../../global/config';
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import { Animation, AnimationBuilder, ComponentProps, FrameworkDelegate, Gesture, NavComponent, NavOptions, NavOutlet, NavResult, RouteID, RouteWrite, RouterDirection, TransitionDoneFn, TransitionInstruction, ViewController } from '../../interface';
|
||||
import { Animation, AnimationBuilder, ComponentProps, FrameworkDelegate, Gesture, NavComponent, NavComponentWithProps, NavOptions, NavOutlet, NavResult, RouteID, RouteWrite, RouterDirection, TransitionDoneFn, TransitionInstruction, ViewController } from '../../interface';
|
||||
import { getTimeGivenProgression } from '../../utils/animation/cubic-bezier';
|
||||
import { assert } from '../../utils/helpers';
|
||||
import { TransitionOptions, lifecycle, setPageHidden, transition } from '../../utils/transition';
|
||||
@@ -156,7 +156,7 @@ export class Nav implements NavOutlet {
|
||||
return this.queueTrns(
|
||||
{
|
||||
insertStart: -1,
|
||||
insertViews: [{ page: component, params: componentProps }],
|
||||
insertViews: [{ component, componentProps }],
|
||||
opts
|
||||
},
|
||||
done
|
||||
@@ -184,7 +184,7 @@ export class Nav implements NavOutlet {
|
||||
return this.queueTrns(
|
||||
{
|
||||
insertStart: insertIndex,
|
||||
insertViews: [{ page: component, params: componentProps }],
|
||||
insertViews: [{ component, componentProps }],
|
||||
opts
|
||||
},
|
||||
done
|
||||
@@ -204,7 +204,7 @@ export class Nav implements NavOutlet {
|
||||
@Method()
|
||||
insertPages(
|
||||
insertIndex: number,
|
||||
insertComponents: NavComponent[],
|
||||
insertComponents: NavComponent[] | NavComponentWithProps[],
|
||||
opts?: NavOptions | null,
|
||||
done?: TransitionDoneFn
|
||||
): Promise<boolean> {
|
||||
@@ -326,7 +326,7 @@ export class Nav implements NavOutlet {
|
||||
done?: TransitionDoneFn
|
||||
): Promise<boolean> {
|
||||
return this.setPages(
|
||||
[{ page: component, params: componentProps }],
|
||||
[{ component, componentProps }],
|
||||
opts,
|
||||
done
|
||||
);
|
||||
@@ -344,7 +344,7 @@ export class Nav implements NavOutlet {
|
||||
*/
|
||||
@Method()
|
||||
setPages(
|
||||
views: any[],
|
||||
views: NavComponent[] | NavComponentWithProps[],
|
||||
opts?: NavOptions | null,
|
||||
done?: TransitionDoneFn
|
||||
): Promise<boolean> {
|
||||
@@ -513,7 +513,7 @@ export class Nav implements NavOutlet {
|
||||
// 7. _transitionStart(): called once the transition actually starts, it initializes the Animation underneath.
|
||||
// 8. _transitionFinish(): called once the transition finishes
|
||||
// 9. _cleanup(): syncs the navigation internal state with the DOM. For example it removes the pages from the DOM or hides/show them.
|
||||
private queueTrns(
|
||||
private async queueTrns(
|
||||
ti: TransitionInstruction,
|
||||
done: TransitionDoneFn | undefined
|
||||
): Promise<boolean> {
|
||||
@@ -527,6 +527,25 @@ export class Nav implements NavOutlet {
|
||||
});
|
||||
ti.done = done;
|
||||
|
||||
/**
|
||||
* If using router, check to see if navigation hooks
|
||||
* will allow us to perform this transition. This
|
||||
* is required in order for hooks to work with
|
||||
* the ion-back-button or swipe to go back.
|
||||
*/
|
||||
if (ti.opts && ti.opts.updateURL !== false && this.useRouter) {
|
||||
const router = document.querySelector('ion-router');
|
||||
if (router) {
|
||||
const canTransition = await router.canTransition();
|
||||
if (canTransition === false) {
|
||||
return Promise.resolve(false);
|
||||
} else if (typeof canTransition === 'string') {
|
||||
router.push(canTransition, ti.opts!.direction || 'back');
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize empty
|
||||
if (ti.insertViews && ti.insertViews.length === 0) {
|
||||
ti.insertViews = undefined;
|
||||
@@ -939,16 +958,27 @@ export class Nav implements NavOutlet {
|
||||
|
||||
for (let i = views.length - 1; i >= 0; i--) {
|
||||
const view = views[i];
|
||||
|
||||
/**
|
||||
* When inserting multiple views via insertPages
|
||||
* the last page will be transitioned to, but the
|
||||
* others will not be. As a result, a DOM element
|
||||
* will only be created for the last page inserted.
|
||||
* As a result, it is possible to have views in the
|
||||
* stack that do not have `view.element` yet.
|
||||
*/
|
||||
const element = view.element;
|
||||
if (i > activeViewIndex) {
|
||||
// this view comes after the active view
|
||||
// let's unload it
|
||||
lifecycle(element, LIFECYCLE_WILL_UNLOAD);
|
||||
this.destroyView(view);
|
||||
} else if (i < activeViewIndex) {
|
||||
// this view comes before the active view
|
||||
// and it is not a portal then ensure it is hidden
|
||||
setPageHidden(element!, true);
|
||||
if (element) {
|
||||
if (i > activeViewIndex) {
|
||||
// this view comes after the active view
|
||||
// let's unload it
|
||||
lifecycle(element, LIFECYCLE_WILL_UNLOAD);
|
||||
this.destroyView(view);
|
||||
} else if (i < activeViewIndex) {
|
||||
// this view comes before the active view
|
||||
// and it is not a portal then ensure it is hidden
|
||||
setPageHidden(element!, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ Type: `Promise<boolean>`
|
||||
|
||||
|
||||
|
||||
### `insertPages(insertIndex: number, insertComponents: NavComponent[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>`
|
||||
### `insertPages(insertIndex: number, insertComponents: NavComponent[] | NavComponentWithProps[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>`
|
||||
|
||||
Inserts an array of components into the navigation stack at the specified index.
|
||||
The last component in the array will become instantiated as a view, and animate
|
||||
@@ -145,7 +145,7 @@ Type: `Promise<boolean>`
|
||||
|
||||
|
||||
|
||||
### `setPages(views: any[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>`
|
||||
### `setPages(views: NavComponent[] | NavComponentWithProps[], opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>`
|
||||
|
||||
Set the views of the current navigation stack and navigate to the last view.
|
||||
By default animations are disabled, but they can be enabled by passing options
|
||||
|
||||
@@ -119,9 +119,9 @@ describe('NavController', () => {
|
||||
mockViews(nav, [view1]);
|
||||
|
||||
const view2 = mockView(MockView2);
|
||||
|
||||
|
||||
await nav.push(view2, null, null, trnsDone);
|
||||
|
||||
|
||||
const hasCompleted = true;
|
||||
const requiresTransition = true;
|
||||
expect(trnsDone).toHaveBeenCalledWith(
|
||||
@@ -818,8 +818,8 @@ describe('NavController', () => {
|
||||
const view5 = mockView(MockView5);
|
||||
|
||||
await nav.setPages([
|
||||
{ page: view4 },
|
||||
{ page: view5 }
|
||||
{ component: view4 },
|
||||
{ component: view5 }
|
||||
], null, trnsDone);
|
||||
expect(instance1.ionViewWillUnload).toHaveBeenCalled();
|
||||
expect(instance2.ionViewWillUnload).toHaveBeenCalled();
|
||||
@@ -924,10 +924,10 @@ describe('NavController', () => {
|
||||
const MockView3 = 'mock-view3';
|
||||
const MockView4 = 'mock-view4';
|
||||
const MockView5 = 'mock-view5';
|
||||
|
||||
|
||||
const mockWebAnimation = (el: HTMLElement) => {
|
||||
Element.prototype.animate = () => {};
|
||||
|
||||
|
||||
el.animate = () => {
|
||||
const animation = {
|
||||
stop: () => {},
|
||||
@@ -935,13 +935,13 @@ describe('NavController', () => {
|
||||
cancel: () => {},
|
||||
onfinish: undefined
|
||||
}
|
||||
|
||||
|
||||
animation.play = () => {
|
||||
if (animation.onfinish) {
|
||||
animation.onfinish();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return animation;
|
||||
}
|
||||
}
|
||||
@@ -953,9 +953,9 @@ describe('NavController', () => {
|
||||
|
||||
const view = new ViewController(component, params);
|
||||
view.element = document.createElement(component) as HTMLElement;
|
||||
|
||||
|
||||
mockWebAnimation(view.element);
|
||||
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AnimationBuilder, ComponentProps, FrameworkDelegate } from '../../interface';
|
||||
import { AnimationBuilder, ComponentProps, FrameworkDelegate, NavComponentWithProps } from '../../interface';
|
||||
import { attachComponent } from '../../utils/framework-delegate';
|
||||
import { assert } from '../../utils/helpers';
|
||||
|
||||
@@ -89,13 +89,20 @@ export const convertToView = (page: any, params: ComponentProps | undefined): Vi
|
||||
return new ViewController(page, params);
|
||||
};
|
||||
|
||||
export const convertToViews = (pages: any[]): ViewController[] => {
|
||||
export const convertToViews = (pages: NavComponentWithProps[]): ViewController[] => {
|
||||
return pages.map(page => {
|
||||
if (page instanceof ViewController) {
|
||||
return page;
|
||||
}
|
||||
if ('page' in page) {
|
||||
return convertToView(page.page, page.params);
|
||||
if ('component' in page) {
|
||||
/**
|
||||
* TODO Ionic 6:
|
||||
* Consider switching to just using `undefined` here
|
||||
* as well as on the public interfaces and on
|
||||
* `NavComponentWithProps`. Previously `pages` was
|
||||
* of type `any[]` so TypeScript did not catch this.
|
||||
*/
|
||||
return convertToView(page.component, (page.componentProps === null) ? undefined : page.componentProps);
|
||||
}
|
||||
return convertToView(page, undefined);
|
||||
}).filter(v => v !== null) as ViewController[];
|
||||
|
||||
@@ -21,6 +21,7 @@ import { iosLeaveAnimation } from './animations/ios.leave';
|
||||
})
|
||||
export class Picker implements ComponentInterface, OverlayInterface {
|
||||
private durationTimeout: any;
|
||||
lastFocus?: HTMLElement;
|
||||
|
||||
@Element() el!: HTMLIonPickerElement;
|
||||
|
||||
@@ -100,7 +101,7 @@ export class Picker implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Event({ eventName: 'ionPickerDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
constructor() {
|
||||
connectedCallback() {
|
||||
prepareOverlay(this.el);
|
||||
}
|
||||
|
||||
@@ -236,7 +237,10 @@ export class Picker implements ComponentInterface, OverlayInterface {
|
||||
tappable={this.backdropDismiss}
|
||||
>
|
||||
</ion-backdrop>
|
||||
<div class="picker-wrapper" role="dialog">
|
||||
|
||||
<div tabindex="0"></div>
|
||||
|
||||
<div class="picker-wrapper ion-overlay-wrapper" role="dialog">
|
||||
<div class="picker-toolbar">
|
||||
{this.buttons.map(b => (
|
||||
<div class={buttonWrapperClass(b)}>
|
||||
@@ -259,6 +263,8 @@ export class Picker implements ComponentInterface, OverlayInterface {
|
||||
<div class="picker-below-highlight"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export class Popover implements ComponentInterface, OverlayInterface {
|
||||
private usersElement?: HTMLElement;
|
||||
|
||||
presented = false;
|
||||
lastFocus?: HTMLElement;
|
||||
|
||||
@Element() el!: HTMLIonPopoverElement;
|
||||
|
||||
@@ -115,7 +116,7 @@ export class Popover implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Event({ eventName: 'ionPopoverDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
constructor() {
|
||||
connectedCallback() {
|
||||
prepareOverlay(this.el);
|
||||
}
|
||||
|
||||
@@ -219,10 +220,15 @@ export class Popover implements ComponentInterface, OverlayInterface {
|
||||
onIonBackdropTap={this.onBackdropTap}
|
||||
>
|
||||
<ion-backdrop tappable={this.backdropDismiss} visible={this.showBackdrop}/>
|
||||
<div class="popover-wrapper">
|
||||
|
||||
<div tabindex="0"></div>
|
||||
|
||||
<div class="popover-wrapper ion-overlay-wrapper">
|
||||
<div class="popover-arrow"></div>
|
||||
<div class="popover-content"></div>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,42 @@
|
||||
import { testPopover } from '../test.utils';
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
const DIRECTORY = 'basic';
|
||||
|
||||
const getActiveElementText = async (page) => {
|
||||
const activeElement = await page.evaluateHandle(() => document.activeElement);
|
||||
return await page.evaluate(el => el && el.textContent, activeElement);
|
||||
}
|
||||
|
||||
test('popover: focus trap', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/popover/test/basic?ionic:_testing=true' });
|
||||
|
||||
await page.click('#basic-popover');
|
||||
await page.waitForSelector('#basic-popover');
|
||||
|
||||
let popover = await page.find('ion-popover');
|
||||
|
||||
expect(popover).not.toBe(null);
|
||||
await popover.waitForVisible();
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementText = await getActiveElementText(page);
|
||||
expect(activeElementText).toEqual('Item 0');
|
||||
|
||||
await page.keyboard.down('Shift');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.up('Shift');
|
||||
|
||||
const activeElementTextTwo = await getActiveElementText(page);
|
||||
expect(activeElementTextTwo).toEqual('Item 3');
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
const activeElementTextThree = await getActiveElementText(page);
|
||||
expect(activeElementTextThree).toEqual('Item 0');
|
||||
});
|
||||
|
||||
test('popover: basic', async () => {
|
||||
await testPopover(DIRECTORY, '#basic-popover');
|
||||
});
|
||||
|
||||
@@ -75,10 +75,10 @@
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-list-header><ion-label>Ionic</ion-label></ion-list-header>
|
||||
<ion-item><ion-label>Item 0</ion-label></ion-item>
|
||||
<ion-item><ion-label>Item 1</ion-label></ion-item>
|
||||
<ion-item><ion-label>Item 2</ion-label></ion-item>
|
||||
<ion-item><ion-label>Item 3</ion-label></ion-item>
|
||||
<ion-item button><ion-label>Item 0</ion-label></ion-item>
|
||||
<ion-item button><ion-label>Item 1</ion-label></ion-item>
|
||||
<ion-item button><ion-label>Item 2</ion-label></ion-item>
|
||||
<ion-item button><ion-label>Item 3</ion-label></ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
`;
|
||||
|
||||
@@ -1,19 +1,233 @@
|
||||
# ion-route
|
||||
|
||||
The route component takes a component and renders it when the Browser URl matches the url property.
|
||||
The route component takes a component and renders it when the Browser URL matches the url property.
|
||||
|
||||
> Note: this component should only be used with vanilla and Stencil JavaScript projects. For Angular projects, use [`ion-router-outlet`](../router-outlet) and the Angular router.
|
||||
|
||||
## Navigation Hooks
|
||||
|
||||
Navigation hooks can be used to perform tasks or act as navigation guards. Hooks are used by providing functions to the `beforeEnter` and `beforeLeave` properties on each `ion-route`. Returning `true` allows navigation to proceed, while returning `false` causes it to be cancelled. Returning an object of type `NavigationHookOptions` allows you to redirect navigation to another page.
|
||||
|
||||
## Interfaces
|
||||
|
||||
```typescript
|
||||
interface NavigationHookOptions {
|
||||
/**
|
||||
* A valid path to redirect navigation to.
|
||||
*/
|
||||
redirect: string;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
### Javascript
|
||||
|
||||
```html
|
||||
<ion-router>
|
||||
<ion-route url="/home" component="page-home"></ion-route>
|
||||
<ion-route url="/dashboard" component="page-dashboard"></ion-route>
|
||||
<ion-route url="/new-message" component="page-new-message"></ion-route>
|
||||
<ion-route url="/login" component="page-login"></ion-route>
|
||||
</ion-router>
|
||||
```
|
||||
|
||||
```javascript
|
||||
const dashboardPage = document.querySelector('ion-route[url="/dashboard"]');
|
||||
dashboardPage.beforeEnter = isLoggedInGuard;
|
||||
|
||||
const newMessagePage = document.querySelector('ion-route[url="/dashboard"]');
|
||||
newMessagePage.beforeLeave = hasUnsavedDataGuard;
|
||||
|
||||
const isLoggedInGuard = async () => {
|
||||
const isLoggedIn = await UserData.isLoggedIn(); // Replace this with actual login validation
|
||||
|
||||
if (isLoggedIn) {
|
||||
return true;
|
||||
} else {
|
||||
return { redirect: '/login' }; // If a user is not logged in, they will be redirected to the /login page
|
||||
}
|
||||
}
|
||||
|
||||
const hasUnsavedDataGuard = async () => {
|
||||
const hasUnsavedData = await checkData(); // Replace this with actual validation
|
||||
|
||||
if (hasUnsavedData) {
|
||||
return await confirmDiscardChanges();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDiscardChanges = async () => {
|
||||
const alert = document.createElement('ion-alert');
|
||||
alert.header = 'Discard Unsaved Changes?';
|
||||
alert.message = 'Are you sure you want to leave? Any unsaved changed will be lost.';
|
||||
alert.buttons = [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'Cancel',
|
||||
},
|
||||
{
|
||||
text: 'Discard',
|
||||
role: 'destructive',
|
||||
}
|
||||
];
|
||||
|
||||
document.body.appendChild(alert);
|
||||
|
||||
await alert.present();
|
||||
|
||||
const { role } = await alert.onDidDismiss();
|
||||
|
||||
return (role === 'Cancel') ? false : true;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Stencil
|
||||
|
||||
```typescript
|
||||
import { Component, h } from '@stencil/core';
|
||||
import { alertController } from '@ionic/core';
|
||||
|
||||
@Component({
|
||||
tag: 'router-example',
|
||||
styleUrl: 'router-example.css'
|
||||
})
|
||||
export class RouterExample {
|
||||
render() {
|
||||
return (
|
||||
<ion-router>
|
||||
<ion-route url="/home" component="page-home"></ion-route>
|
||||
<ion-route url="/dashboard" component="page-dashboard" beforeEnter={isLoggedInGuard}></ion-route>
|
||||
<ion-route url="/new-message" component="page-new-message" beforeLeave={hasUnsavedDataGuard}></ion-route>
|
||||
<ion-route url="/login" component="page-login"></ion-route>
|
||||
</ion-router>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const isLoggedInGuard = async () => {
|
||||
const isLoggedIn = await UserData.isLoggedIn(); // Replace this with actual login validation
|
||||
|
||||
if (isLoggedIn) {
|
||||
return true;
|
||||
} else {
|
||||
return { redirect: '/login' }; // If a user is not logged in, they will be redirected to the /login page
|
||||
}
|
||||
}
|
||||
|
||||
const hasUnsavedDataGuard = async () => {
|
||||
const hasUnsavedData = await checkData(); // Replace this with actual validation
|
||||
|
||||
if (hasUnsavedData) {
|
||||
return await confirmDiscardChanges();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDiscardChanges = async () => {
|
||||
const alert = await alertController.create({
|
||||
header: 'Discard Unsaved Changes?',
|
||||
message: 'Are you sure you want to leave? Any unsaved changed will be lost.',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'Cancel',
|
||||
},
|
||||
{
|
||||
text: 'Discard',
|
||||
role: 'destructive',
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await alert.present();
|
||||
|
||||
const { role } = await alert.onDidDismiss();
|
||||
|
||||
return (role === 'Cancel') ? false : true;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Vue
|
||||
|
||||
```html
|
||||
<template>
|
||||
<ion-router>
|
||||
<ion-route url="/home" component="page-home"></ion-route>
|
||||
<ion-route url="/dashboard" component="page-dashboard" :beforeEnter="isLoggedInGuard"></ion-route>
|
||||
<ion-route url="/new-message" component="page-new-message" :beforeLeave="hasUnsavedDataGuard"></ion-route>
|
||||
<ion-route url="/login" component="page-login"></ion-route>
|
||||
</ion-router>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { alertController } from '@ionic/vue';
|
||||
|
||||
const isLoggedInGuard = async () => {
|
||||
const isLoggedIn = await UserData.isLoggedIn(); // Replace this with actual login validation
|
||||
|
||||
if (isLoggedIn) {
|
||||
return true;
|
||||
} else {
|
||||
return { redirect: '/login' }; // If a user is not logged in, they will be redirected to the /login page
|
||||
}
|
||||
}
|
||||
|
||||
const hasUnsavedDataGuard = async () => {
|
||||
const hasUnsavedData = await checkData(); // Replace this with actual validation
|
||||
|
||||
if (hasUnsavedData) {
|
||||
return await confirmDiscardChanges();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDiscardChanges = async () => {
|
||||
const alert = await alertController.create({
|
||||
header: 'Discard Unsaved Changes?',
|
||||
message: 'Are you sure you want to leave? Any unsaved changed will be lost.',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'Cancel',
|
||||
},
|
||||
{
|
||||
text: 'Discard',
|
||||
role: 'destructive',
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await alert.present();
|
||||
|
||||
const { role } = await alert.onDidDismiss();
|
||||
|
||||
return (role === 'Cancel') ? false : true;
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ------------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ----------- |
|
||||
| `component` _(required)_ | `component` | Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-nav`) when the route matches. The value of this property is not always the tagname of the component to load, in `ion-tabs` it actually refers to the name of the `ion-tab` to select. | `string` | `undefined` |
|
||||
| `componentProps` | -- | A key value `{ 'red': true, 'blue': 'white'}` containing props that should be passed to the defined component when rendered. | `undefined \| { [key: string]: any; }` | `undefined` |
|
||||
| `url` | `url` | Relative path that needs to match in order for this route to apply. Accepts paths similar to expressjs so that you can define parameters in the url /foo/:bar where bar would be available in incoming props. | `string` | `''` |
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ------------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------- |
|
||||
| `beforeEnter` | -- | A navigation hook that is fired when the route tries to enter. Returning `true` allows the navigation to proceed, while returning `false` causes it to be cancelled. Returning a `NavigationHookOptions` object causes the router to redirect to the path specified. | `(() => boolean \| NavigationHookOptions \| Promise<NavigationHookResult>) \| undefined` | `undefined` |
|
||||
| `beforeLeave` | -- | A navigation hook that is fired when the route tries to leave. Returning `true` allows the navigation to proceed, while returning `false` causes it to be cancelled. Returning a `NavigationHookOptions` object causes the router to redirect to the path specified. | `(() => boolean \| NavigationHookOptions \| Promise<NavigationHookResult>) \| undefined` | `undefined` |
|
||||
| `component` _(required)_ | `component` | Name of the component to load/select in the navigation outlet (`ion-tabs`, `ion-nav`) when the route matches. The value of this property is not always the tagname of the component to load, in `ion-tabs` it actually refers to the name of the `ion-tab` to select. | `string` | `undefined` |
|
||||
| `componentProps` | -- | A key value `{ 'red': true, 'blue': 'white'}` containing props that should be passed to the defined component when rendered. | `undefined \| { [key: string]: any; }` | `undefined` |
|
||||
| `url` | `url` | Relative path that needs to match in order for this route to apply. Accepts paths similar to expressjs so that you can define parameters in the url /foo/:bar where bar would be available in incoming props. | `string` | `''` |
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
5
core/src/components/route/route-interface.ts
Normal file
5
core/src/components/route/route-interface.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type NavigationHookCallback = () => NavigationHookResult | Promise<NavigationHookResult>;
|
||||
export type NavigationHookResult = boolean | NavigationHookOptions;
|
||||
export interface NavigationHookOptions {
|
||||
redirect: string;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component, ComponentInterface, Event, EventEmitter, Prop, Watch } from '@stencil/core';
|
||||
|
||||
import { NavigationHookCallback } from './route-interface';
|
||||
|
||||
@Component({
|
||||
tag: 'ion-route'
|
||||
})
|
||||
@@ -28,6 +30,22 @@ export class Route implements ComponentInterface {
|
||||
*/
|
||||
@Prop() componentProps?: {[key: string]: any};
|
||||
|
||||
/**
|
||||
* A navigation hook that is fired when the route tries to leave.
|
||||
* Returning `true` allows the navigation to proceed, while returning
|
||||
* `false` causes it to be cancelled. Returning a `NavigationHookOptions`
|
||||
* object causes the router to redirect to the path specified.
|
||||
*/
|
||||
@Prop() beforeLeave?: NavigationHookCallback;
|
||||
|
||||
/**
|
||||
* A navigation hook that is fired when the route tries to enter.
|
||||
* Returning `true` allows the navigation to proceed, while returning
|
||||
* `false` causes it to be cancelled. Returning a `NavigationHookOptions`
|
||||
* object causes the router to redirect to the path specified.
|
||||
*/
|
||||
@Prop() beforeEnter?: NavigationHookCallback;
|
||||
|
||||
/**
|
||||
* Used internally by `ion-router` to know when this route did change.
|
||||
*/
|
||||
|
||||
60
core/src/components/route/usage/javascript.md
Normal file
60
core/src/components/route/usage/javascript.md
Normal file
@@ -0,0 +1,60 @@
|
||||
```html
|
||||
<ion-router>
|
||||
<ion-route url="/home" component="page-home"></ion-route>
|
||||
<ion-route url="/dashboard" component="page-dashboard"></ion-route>
|
||||
<ion-route url="/new-message" component="page-new-message"></ion-route>
|
||||
<ion-route url="/login" component="page-login"></ion-route>
|
||||
</ion-router>
|
||||
```
|
||||
|
||||
```javascript
|
||||
const dashboardPage = document.querySelector('ion-route[url="/dashboard"]');
|
||||
dashboardPage.beforeEnter = isLoggedInGuard;
|
||||
|
||||
const newMessagePage = document.querySelector('ion-route[url="/dashboard"]');
|
||||
newMessagePage.beforeLeave = hasUnsavedDataGuard;
|
||||
|
||||
const isLoggedInGuard = async () => {
|
||||
const isLoggedIn = await UserData.isLoggedIn(); // Replace this with actual login validation
|
||||
|
||||
if (isLoggedIn) {
|
||||
return true;
|
||||
} else {
|
||||
return { redirect: '/login' }; // If a user is not logged in, they will be redirected to the /login page
|
||||
}
|
||||
}
|
||||
|
||||
const hasUnsavedDataGuard = async () => {
|
||||
const hasUnsavedData = await checkData(); // Replace this with actual validation
|
||||
|
||||
if (hasUnsavedData) {
|
||||
return await confirmDiscardChanges();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDiscardChanges = async () => {
|
||||
const alert = document.createElement('ion-alert');
|
||||
alert.header = 'Discard Unsaved Changes?';
|
||||
alert.message = 'Are you sure you want to leave? Any unsaved changed will be lost.';
|
||||
alert.buttons = [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'Cancel',
|
||||
},
|
||||
{
|
||||
text: 'Discard',
|
||||
role: 'destructive',
|
||||
}
|
||||
];
|
||||
|
||||
document.body.appendChild(alert);
|
||||
|
||||
await alert.present();
|
||||
|
||||
const { role } = await alert.onDidDismiss();
|
||||
|
||||
return (role === 'Cancel') ? false : true;
|
||||
}
|
||||
```
|
||||
64
core/src/components/route/usage/stencil.md
Normal file
64
core/src/components/route/usage/stencil.md
Normal file
@@ -0,0 +1,64 @@
|
||||
```typescript
|
||||
import { Component, h } from '@stencil/core';
|
||||
import { alertController } from '@ionic/core';
|
||||
|
||||
@Component({
|
||||
tag: 'router-example',
|
||||
styleUrl: 'router-example.css'
|
||||
})
|
||||
export class RouterExample {
|
||||
render() {
|
||||
return (
|
||||
<ion-router>
|
||||
<ion-route url="/home" component="page-home"></ion-route>
|
||||
<ion-route url="/dashboard" component="page-dashboard" beforeEnter={isLoggedInGuard}></ion-route>
|
||||
<ion-route url="/new-message" component="page-new-message" beforeLeave={hasUnsavedDataGuard}></ion-route>
|
||||
<ion-route url="/login" component="page-login"></ion-route>
|
||||
</ion-router>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const isLoggedInGuard = async () => {
|
||||
const isLoggedIn = await UserData.isLoggedIn(); // Replace this with actual login validation
|
||||
|
||||
if (isLoggedIn) {
|
||||
return true;
|
||||
} else {
|
||||
return { redirect: '/login' }; // If a user is not logged in, they will be redirected to the /login page
|
||||
}
|
||||
}
|
||||
|
||||
const hasUnsavedDataGuard = async () => {
|
||||
const hasUnsavedData = await checkData(); // Replace this with actual validation
|
||||
|
||||
if (hasUnsavedData) {
|
||||
return await confirmDiscardChanges();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDiscardChanges = async () => {
|
||||
const alert = await alertController.create({
|
||||
header: 'Discard Unsaved Changes?',
|
||||
message: 'Are you sure you want to leave? Any unsaved changed will be lost.',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'Cancel',
|
||||
},
|
||||
{
|
||||
text: 'Discard',
|
||||
role: 'destructive',
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await alert.present();
|
||||
|
||||
const { role } = await alert.onDidDismiss();
|
||||
|
||||
return (role === 'Cancel') ? false : true;
|
||||
}
|
||||
```
|
||||
57
core/src/components/route/usage/vue.md
Normal file
57
core/src/components/route/usage/vue.md
Normal file
@@ -0,0 +1,57 @@
|
||||
```html
|
||||
<template>
|
||||
<ion-router>
|
||||
<ion-route url="/home" component="page-home"></ion-route>
|
||||
<ion-route url="/dashboard" component="page-dashboard" :beforeEnter="isLoggedInGuard"></ion-route>
|
||||
<ion-route url="/new-message" component="page-new-message" :beforeLeave="hasUnsavedDataGuard"></ion-route>
|
||||
<ion-route url="/login" component="page-login"></ion-route>
|
||||
</ion-router>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { alertController } from '@ionic/vue';
|
||||
|
||||
const isLoggedInGuard = async () => {
|
||||
const isLoggedIn = await UserData.isLoggedIn(); // Replace this with actual login validation
|
||||
|
||||
if (isLoggedIn) {
|
||||
return true;
|
||||
} else {
|
||||
return { redirect: '/login' }; // If a user is not logged in, they will be redirected to the /login page
|
||||
}
|
||||
}
|
||||
|
||||
const hasUnsavedDataGuard = async () => {
|
||||
const hasUnsavedData = await checkData(); // Replace this with actual validation
|
||||
|
||||
if (hasUnsavedData) {
|
||||
return await confirmDiscardChanges();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDiscardChanges = async () => {
|
||||
const alert = await alertController.create({
|
||||
header: 'Discard Unsaved Changes?',
|
||||
message: 'Are you sure you want to leave? Any unsaved changed will be lost.',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'Cancel',
|
||||
},
|
||||
{
|
||||
text: 'Discard',
|
||||
role: 'destructive',
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await alert.present();
|
||||
|
||||
const { role } = await alert.onDidDismiss();
|
||||
|
||||
return (role === 'Cancel') ? false : true;
|
||||
}
|
||||
</script>
|
||||
```
|
||||
@@ -187,7 +187,6 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
|
||||
await transition({
|
||||
mode,
|
||||
animated,
|
||||
animationBuilder,
|
||||
enteringEl,
|
||||
leavingEl,
|
||||
baseEl: el,
|
||||
@@ -195,7 +194,8 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
|
||||
? ani => this.ani = ani
|
||||
: undefined
|
||||
),
|
||||
...opts
|
||||
...opts,
|
||||
animationBuilder,
|
||||
});
|
||||
|
||||
// emit nav changed event
|
||||
|
||||
@@ -70,9 +70,17 @@ export class Router implements ComponentInterface {
|
||||
}
|
||||
|
||||
@Listen('popstate', { target: 'window' })
|
||||
protected onPopState() {
|
||||
protected async onPopState() {
|
||||
const direction = this.historyDirection();
|
||||
const path = this.getPath();
|
||||
let path = this.getPath();
|
||||
|
||||
const canProceed = await this.runGuards(path);
|
||||
if (canProceed !== true) {
|
||||
if (typeof canProceed === 'object') {
|
||||
path = parsePath(canProceed.redirect);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
console.debug('[ion-router] URL changed -> update nav', path, direction);
|
||||
return this.writeNavStateRoot(path, direction);
|
||||
}
|
||||
@@ -85,6 +93,21 @@ export class Router implements ComponentInterface {
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@Method()
|
||||
async canTransition() {
|
||||
const canProceed = await this.runGuards();
|
||||
if (canProceed !== true) {
|
||||
if (typeof canProceed === 'object') {
|
||||
return canProceed.redirect;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the specified URL.
|
||||
*
|
||||
@@ -92,14 +115,25 @@ export class Router implements ComponentInterface {
|
||||
* @param direction The direction of the animation. Defaults to `"forward"`.
|
||||
*/
|
||||
@Method()
|
||||
push(url: string, direction: RouterDirection = 'forward', animation?: AnimationBuilder) {
|
||||
async push(url: string, direction: RouterDirection = 'forward', animation?: AnimationBuilder) {
|
||||
if (url.startsWith('.')) {
|
||||
url = (new URL(url, window.location.href)).pathname;
|
||||
}
|
||||
console.debug('[ion-router] URL pushed -> updating nav', url, direction);
|
||||
|
||||
const path = parsePath(url);
|
||||
const queryString = url.split('?')[1];
|
||||
let path = parsePath(url);
|
||||
let queryString = url.split('?')[1];
|
||||
|
||||
const canProceed = await this.runGuards(path);
|
||||
if (canProceed !== true) {
|
||||
if (typeof canProceed === 'object') {
|
||||
path = parsePath(canProceed.redirect);
|
||||
queryString = canProceed.redirect.split('?')[1];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this.setPath(path, direction, queryString);
|
||||
return this.writeNavStateRoot(path, direction, animation);
|
||||
}
|
||||
@@ -191,6 +225,7 @@ export class Router implements ComponentInterface {
|
||||
// lookup redirect rule
|
||||
const redirects = readRedirects(this.el);
|
||||
const redirect = routeRedirect(path, redirects);
|
||||
|
||||
let redirectFrom: string[] | null = null;
|
||||
if (redirect) {
|
||||
this.setPath(redirect.to!, direction);
|
||||
@@ -237,6 +272,25 @@ export class Router implements ComponentInterface {
|
||||
}
|
||||
return resolve;
|
||||
}
|
||||
private async runGuards(to: string[] | null = this.getPath(), from: string[] | null = parsePath(this.previousPath)) {
|
||||
if (!to || !from) { return true; }
|
||||
|
||||
const routes = readRoutes(this.el);
|
||||
|
||||
const toChain = routerPathToChain(to, routes);
|
||||
const fromChain = routerPathToChain(from, routes);
|
||||
|
||||
const beforeEnterHook = toChain && toChain[toChain.length - 1].beforeEnter;
|
||||
const beforeLeaveHook = fromChain && fromChain[fromChain.length - 1].beforeLeave;
|
||||
|
||||
const canLeave = beforeLeaveHook ? await beforeLeaveHook() : true;
|
||||
if (canLeave === false || typeof canLeave === 'object') { return canLeave; }
|
||||
|
||||
const canEnter = beforeEnterHook ? await beforeEnterHook() : true;
|
||||
if (canEnter === false || typeof canEnter === 'object') { return canEnter; }
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async writeNavState(
|
||||
node: HTMLElement | undefined, chain: RouteChain, direction: RouterDirection,
|
||||
|
||||
128
core/src/components/router/test/guards/href.e2e.ts
Normal file
128
core/src/components/router/test/guards/href.e2e.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('router: guards - href - allow/allow', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/router/test/guards?ionic:_testing=true'
|
||||
});
|
||||
|
||||
// Test 1: beforeEnter: allow, beforeLeave: allow
|
||||
await setBeforeEnterHook(page, 'allow');
|
||||
|
||||
const href = await page.$('#href');
|
||||
await href.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/child');
|
||||
|
||||
const backButton = await page.$('ion-back-button');
|
||||
await backButton.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/home');
|
||||
});
|
||||
|
||||
test('router: guards - href - block/allow', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/router/test/guards?ionic:_testing=true'
|
||||
});
|
||||
|
||||
// Test 2: beforeEnter: block, beforeLeave: allow
|
||||
await setBeforeEnterHook(page, 'block');
|
||||
|
||||
const href = await page.$('#href');
|
||||
await href.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/home');
|
||||
});
|
||||
|
||||
test('router: guards - href - redirect/allow', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/router/test/guards?ionic:_testing=true'
|
||||
});
|
||||
|
||||
// Test 3: beforeEnter: redirect, beforeLeave: allow
|
||||
await setBeforeEnterHook(page, 'redirect');
|
||||
|
||||
const href = await page.$('#href');
|
||||
await href.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/test');
|
||||
|
||||
const backButton = await page.$('ion-back-button');
|
||||
await backButton.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/home');
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
test('router: guards - href - allow/block', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/router/test/guards?ionic:_testing=true'
|
||||
});
|
||||
|
||||
// Test 4: beforeEnter: allow, beforeLeave: block
|
||||
await setBeforeLeaveHook(page, 'block');
|
||||
|
||||
const href = await page.$('#href');
|
||||
await href.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/child');
|
||||
|
||||
const backButton = await page.$('ion-back-button');
|
||||
await backButton.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/child');
|
||||
});
|
||||
|
||||
// TODO this is an actual bug in the code.
|
||||
test('router: guards - href - allow/redirect', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/router/test/guards?ionic:_testing=true'
|
||||
});
|
||||
|
||||
// Test 5: beforeEnter: allow, beforeLeave: redirect
|
||||
await setBeforeLeaveHook(page, 'redirect');
|
||||
|
||||
const href = await page.$('#href');
|
||||
await href.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/child');
|
||||
|
||||
const backButton = await page.$('ion-back-button');
|
||||
await backButton.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/test');
|
||||
});
|
||||
|
||||
const checkUrl = async (page, url: string) => {
|
||||
const getUrl = await page.url();
|
||||
expect(getUrl).toContain(url);
|
||||
}
|
||||
|
||||
const setBeforeEnterHook = async (page, type: string) => {
|
||||
const button = await page.$(`ion-radio-group#beforeEnter ion-radio[value=${type}]`);
|
||||
await button.click();
|
||||
}
|
||||
|
||||
const setBeforeLeaveHook = async (page, type: string) => {
|
||||
const button = await page.$(`ion-radio-group#beforeLeave ion-radio[value=${type}]`);
|
||||
await button.click();
|
||||
}
|
||||
194
core/src/components/router/test/guards/index.html
Normal file
194
core/src/components/router/test/guards/index.html
Normal file
@@ -0,0 +1,194 @@
|
||||
<!DOCTYPE html>
|
||||
<html dir="ltr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Navigation Guards</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">
|
||||
<style>
|
||||
.toolbar {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 100;
|
||||
width: 200px;
|
||||
background: white;
|
||||
box-shadow: 0px 1px 10px rgba(0,0,0,0.2);
|
||||
}
|
||||
</style>
|
||||
<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> <script>
|
||||
|
||||
class HomePage extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.innerHTML = `
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Home Page</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
<ion-list>
|
||||
<ion-button id="router-push">router.push</ion-button><br>
|
||||
<ion-router-link href="/child">
|
||||
<ion-button id="router-link">ion-router-link</ion-button><br>
|
||||
</ion-router-link>
|
||||
<ion-button href="/child" id="href">href</ion-button>
|
||||
</ion-list>
|
||||
</ion-content>`;
|
||||
|
||||
const childButton = this.querySelector('#router-push');
|
||||
childButton.addEventListener('click', () => {
|
||||
const r = document.querySelector('ion-router');
|
||||
r.push('/child');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ChildPage extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.innerHTML = `
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons>
|
||||
<ion-back-button default-href="/test"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Child Page</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
</ion-content>`;
|
||||
}
|
||||
}
|
||||
|
||||
class TestPage extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.innerHTML = `
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons>
|
||||
<ion-back-button></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Test Page</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
</ion-content>`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('home-page', HomePage);
|
||||
customElements.define('child-page', ChildPage);
|
||||
customElements.define('test-page', TestPage);
|
||||
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="toolbar">
|
||||
<ion-radio-group id="beforeEnter" value="allow">
|
||||
<ion-list-header>
|
||||
<ion-label>
|
||||
beforeEnter Behavior
|
||||
</ion-label>
|
||||
</ion-list-header>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Allow Navigation</ion-label>
|
||||
<ion-radio value="allow"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Block Navigation</ion-label>
|
||||
<ion-radio value="block"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Redirect</ion-label>
|
||||
<ion-radio value="redirect"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
|
||||
<br><br>
|
||||
|
||||
<ion-radio-group id="beforeLeave" value="allow">
|
||||
<ion-list-header>
|
||||
<ion-label>
|
||||
beforeLeave Behavior
|
||||
</ion-label>
|
||||
</ion-list-header>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Allow Navigation</ion-label>
|
||||
<ion-radio value="allow"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Block Navigation</ion-label>
|
||||
<ion-radio value="block"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Redirect</ion-label>
|
||||
<ion-radio value="redirect"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
</div>
|
||||
|
||||
<ion-app>
|
||||
|
||||
<ion-router>
|
||||
<ion-route-redirect from="/" to="/home"></ion-route-redirect>
|
||||
<ion-route url="/home" component="home-page"></ion-route>
|
||||
<ion-route url="/test" component="test-page"></ion-route>
|
||||
<ion-route url="/child" component="child-page"></ion-route>
|
||||
</ion-router>
|
||||
|
||||
<ion-nav></ion-nav>
|
||||
|
||||
<script>
|
||||
const beforeEnterGroup = document.querySelector('ion-radio-group#beforeEnter');
|
||||
const beforeLeaveGroup = document.querySelector('ion-radio-group#beforeLeave');
|
||||
beforeEnterGroup.addEventListener('ionChange', (ev) => {
|
||||
switch (ev.detail.value) {
|
||||
case "redirect":
|
||||
page.beforeEnter = redirect;
|
||||
break;
|
||||
case "block":
|
||||
page.beforeEnter = block;
|
||||
break;
|
||||
default:
|
||||
page.beforeEnter = allow;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
beforeLeaveGroup.addEventListener('ionChange', (ev) => {
|
||||
switch (ev.detail.value) {
|
||||
case "redirect":
|
||||
page.beforeLeave = redirect;
|
||||
break;
|
||||
case "block":
|
||||
page.beforeLeave = block;
|
||||
break;
|
||||
default:
|
||||
page.beforeLeave = allow;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const redirect = (to = '/test') => { return { redirect: to }};
|
||||
const block = () => false;
|
||||
const allow = () => true;
|
||||
|
||||
const page = document.querySelector('ion-route[component="child-page"]');
|
||||
page.beforeEnter = allow;
|
||||
page.beforeLeave = allow;
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
128
core/src/components/router/test/guards/router-link.e2e.ts
Normal file
128
core/src/components/router/test/guards/router-link.e2e.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('router: guards - router-link - allow/allow', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/router/test/guards?ionic:_testing=true'
|
||||
});
|
||||
|
||||
// Test 1: beforeEnter: allow, beforeLeave: allow
|
||||
await setBeforeEnterHook(page, 'allow');
|
||||
|
||||
const routerLink = await page.$('#router-link');
|
||||
await routerLink.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/child');
|
||||
|
||||
const backButton = await page.$('ion-back-button');
|
||||
await backButton.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/home');
|
||||
});
|
||||
|
||||
test('router: guards - router-link - block/allow', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/router/test/guards?ionic:_testing=true'
|
||||
});
|
||||
|
||||
// Test 2: beforeEnter: block, beforeLeave: allow
|
||||
await setBeforeEnterHook(page, 'block');
|
||||
|
||||
const routerLink = await page.$('#router-link');
|
||||
await routerLink.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/home');
|
||||
});
|
||||
|
||||
test('router: guards - router-link - redirect/allow', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/router/test/guards?ionic:_testing=true'
|
||||
});
|
||||
|
||||
// Test 3: beforeEnter: redirect, beforeLeave: allow
|
||||
await setBeforeEnterHook(page, 'redirect');
|
||||
|
||||
const routerLink = await page.$('#router-link');
|
||||
await routerLink.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/test');
|
||||
|
||||
const backButton = await page.$('ion-back-button');
|
||||
await backButton.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/home');
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
test('router: guards - router-link - allow/block', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/router/test/guards?ionic:_testing=true'
|
||||
});
|
||||
|
||||
// Test 4: beforeEnter: allow, beforeLeave: block
|
||||
await setBeforeLeaveHook(page, 'block');
|
||||
|
||||
const routerLink = await page.$('#router-link');
|
||||
await routerLink.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/child');
|
||||
|
||||
const backButton = await page.$('ion-back-button');
|
||||
await backButton.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/child');
|
||||
});
|
||||
|
||||
// TODO this is an actual bug in the code.
|
||||
test('router: guards - router-link - allow/redirect', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/router/test/guards?ionic:_testing=true'
|
||||
});
|
||||
|
||||
// Test 5: beforeEnter: allow, beforeLeave: redirect
|
||||
await setBeforeLeaveHook(page, 'redirect');
|
||||
|
||||
const routerLink = await page.$('#router-link');
|
||||
await routerLink.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/child');
|
||||
|
||||
const backButton = await page.$('ion-back-button');
|
||||
await backButton.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/test');
|
||||
});
|
||||
|
||||
const checkUrl = async (page, url: string) => {
|
||||
const getUrl = await page.url();
|
||||
expect(getUrl).toContain(url);
|
||||
}
|
||||
|
||||
const setBeforeEnterHook = async (page, type: string) => {
|
||||
const button = await page.$(`ion-radio-group#beforeEnter ion-radio[value=${type}]`);
|
||||
await button.click();
|
||||
}
|
||||
|
||||
const setBeforeLeaveHook = async (page, type: string) => {
|
||||
const button = await page.$(`ion-radio-group#beforeLeave ion-radio[value=${type}]`);
|
||||
await button.click();
|
||||
}
|
||||
128
core/src/components/router/test/guards/router-push.e2e.ts
Normal file
128
core/src/components/router/test/guards/router-push.e2e.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('router: guards - router.push - allow/allow', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/router/test/guards?ionic:_testing=true'
|
||||
});
|
||||
|
||||
// Test 1: beforeEnter: allow, beforeLeave: allow
|
||||
await setBeforeEnterHook(page, 'allow');
|
||||
|
||||
const routerPush = await page.$('#router-push');
|
||||
await routerPush.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/child');
|
||||
|
||||
const backButton = await page.$('ion-back-button');
|
||||
await backButton.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/home');
|
||||
});
|
||||
|
||||
test('router: guards - router.push - block/allow', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/router/test/guards?ionic:_testing=true'
|
||||
});
|
||||
|
||||
// Test 2: beforeEnter: block, beforeLeave: allow
|
||||
await setBeforeEnterHook(page, 'block');
|
||||
|
||||
const routerPush = await page.$('#router-push');
|
||||
await routerPush.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/home');
|
||||
});
|
||||
|
||||
test('router: guards - router.push - redirect/allow', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/router/test/guards?ionic:_testing=true'
|
||||
});
|
||||
|
||||
// Test 3: beforeEnter: redirect, beforeLeave: allow
|
||||
await setBeforeEnterHook(page, 'redirect');
|
||||
|
||||
const routerPush = await page.$('#router-push');
|
||||
await routerPush.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/test');
|
||||
|
||||
const backButton = await page.$('ion-back-button');
|
||||
await backButton.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/home');
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
test('router: guards - router.push - allow/block', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/router/test/guards?ionic:_testing=true'
|
||||
});
|
||||
|
||||
// Test 4: beforeEnter: allow, beforeLeave: block
|
||||
await setBeforeLeaveHook(page, 'block');
|
||||
|
||||
const routerPush = await page.$('#router-push');
|
||||
await routerPush.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/child');
|
||||
|
||||
const backButton = await page.$('ion-back-button');
|
||||
await backButton.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/child');
|
||||
});
|
||||
|
||||
// TODO this is an actual bug in the code.
|
||||
test('router: guards - router.push - allow/redirect', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/router/test/guards?ionic:_testing=true'
|
||||
});
|
||||
|
||||
// Test 5: beforeEnter: allow, beforeLeave: redirect
|
||||
await setBeforeLeaveHook(page, 'redirect');
|
||||
|
||||
const routerPush = await page.$('#router-push');
|
||||
await routerPush.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/child');
|
||||
|
||||
const backButton = await page.$('ion-back-button');
|
||||
await backButton.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await checkUrl(page, '#/test');
|
||||
});
|
||||
|
||||
const checkUrl = async (page, url: string) => {
|
||||
const getUrl = await page.url();
|
||||
expect(getUrl).toContain(url);
|
||||
}
|
||||
|
||||
const setBeforeEnterHook = async (page, type: string) => {
|
||||
const button = await page.$(`ion-radio-group#beforeEnter ion-radio[value=${type}]`);
|
||||
await button.click();
|
||||
}
|
||||
|
||||
const setBeforeLeaveHook = async (page, type: string) => {
|
||||
const button = await page.$(`ion-radio-group#beforeLeave ion-radio[value=${type}]`);
|
||||
await button.click();
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AnimationBuilder, ComponentProps } from '../../../interface';
|
||||
import { NavigationHookCallback } from '../../route/route-interface';
|
||||
|
||||
export interface HTMLStencilElement extends HTMLElement {
|
||||
componentOnReady(): Promise<this>;
|
||||
@@ -36,6 +37,8 @@ export interface RouteEntry {
|
||||
id: string;
|
||||
path: string[];
|
||||
params: {[key: string]: any} | undefined;
|
||||
beforeLeave?: NavigationHookCallback;
|
||||
beforeEnter?: NavigationHookCallback;
|
||||
}
|
||||
|
||||
export interface RouteNode extends RouteEntry {
|
||||
|
||||
@@ -29,6 +29,8 @@ export const readRouteNodes = (root: Element, node = root): RouteTree => {
|
||||
path: parsePath(readProp(el, 'url')),
|
||||
id: component.toLowerCase(),
|
||||
params: el.componentProps,
|
||||
beforeLeave: el.beforeLeave,
|
||||
beforeEnter: el.beforeEnter,
|
||||
children: readRouteNodes(root, el)
|
||||
};
|
||||
});
|
||||
@@ -57,7 +59,9 @@ const flattenNode = (chain: RouteChain, routes: RouteChain[], node: RouteNode) =
|
||||
s.push({
|
||||
id: node.id,
|
||||
path: node.path,
|
||||
params: node.params
|
||||
params: node.params,
|
||||
beforeLeave: node.beforeLeave,
|
||||
beforeEnter: node.beforeEnter
|
||||
});
|
||||
|
||||
if (node.children.length === 0) {
|
||||
|
||||
@@ -796,10 +796,11 @@ export class SegmentButtonExample {
|
||||
|
||||
## Shadow Parts
|
||||
|
||||
| Part | Description |
|
||||
| ------------- | ------------------------------------------------------------- |
|
||||
| `"indicator"` | The indicator displayed on the checked segment button. |
|
||||
| `"native"` | The native HTML button element that wraps all child elements. |
|
||||
| Part | Description |
|
||||
| ------------------------ | --------------------------------------------------------------------------------- |
|
||||
| `"indicator"` | The indicator displayed on the checked segment button. |
|
||||
| `"indicator-background"` | The background element for the indicator displayed on the checked segment button. |
|
||||
| `"native"` | The native HTML button element that wraps all child elements. |
|
||||
|
||||
|
||||
## CSS Custom Properties
|
||||
|
||||
@@ -44,11 +44,6 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.button-native {
|
||||
min-width: $segment-button-md-min-width;
|
||||
}
|
||||
|
||||
|
||||
// Segment Button: Disabled
|
||||
// --------------------------------------------------
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ let ids = 0;
|
||||
*
|
||||
* @part native - The native HTML button element that wraps all child elements.
|
||||
* @part indicator - The indicator displayed on the checked segment button.
|
||||
* @part indicator-background - The background element for the indicator displayed on the checked segment button.
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-segment-button',
|
||||
|
||||
@@ -213,6 +213,7 @@ export class Segment implements ComponentInterface {
|
||||
// If there are no checked buttons, set the current button to checked
|
||||
if (!checked) {
|
||||
this.value = clicked.value;
|
||||
this.setCheckedClasses();
|
||||
}
|
||||
|
||||
// If the gesture began on the clicked button with the indicator
|
||||
@@ -375,8 +376,12 @@ export class Segment implements ComponentInterface {
|
||||
const previous = this.checked;
|
||||
this.value = current.value;
|
||||
|
||||
if (previous && this.scrollable) {
|
||||
this.checkButton(previous, current);
|
||||
if (this.scrollable) {
|
||||
if (previous) {
|
||||
this.checkButton(previous, current);
|
||||
} else {
|
||||
this.setCheckedClasses();
|
||||
}
|
||||
}
|
||||
|
||||
this.checked = current;
|
||||
|
||||
@@ -159,6 +159,40 @@
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
|
||||
<!-- Non Scrollable, Custom Min Width -->
|
||||
<ion-segment color="tertiary" value="heart" id="min-width-custom">
|
||||
<ion-segment-button value="home">
|
||||
<ion-icon name="home"></ion-icon>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button value="heart">
|
||||
<ion-icon name="heart"></ion-icon>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button value="pin">
|
||||
<ion-icon name="pin"></ion-icon>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button value="star">
|
||||
<ion-icon name="star"></ion-icon>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button value="call">
|
||||
<ion-icon name="call"></ion-icon>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button value="globe">
|
||||
<ion-icon name="globe"></ion-icon>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button value="basket">
|
||||
<ion-icon name="basket"></ion-icon>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button value="airplane">
|
||||
<ion-icon name="airplane"></ion-icon>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button value="boat">
|
||||
<ion-icon name="boat"></ion-icon>
|
||||
</ion-segment-button>
|
||||
<ion-segment-button value="baseball">
|
||||
<ion-icon name="baseball"></ion-icon>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
|
||||
<!-- Scrollable Icons -->
|
||||
<ion-segment scrollable color="tertiary" value="heart">
|
||||
<ion-segment-button value="home">
|
||||
@@ -212,6 +246,10 @@
|
||||
</ion-content>
|
||||
|
||||
<style>
|
||||
ion-segment#min-width-custom ion-segment-button {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
ion-content {
|
||||
--padding-top: 16px;
|
||||
}
|
||||
|
||||
@@ -654,10 +654,10 @@ To customize an individual option, set a class on the `ion-select-option`:
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ---------- | ---------- | ----------------------------------------------------------- | --------- | ----------- |
|
||||
| `disabled` | `disabled` | If `true`, the user cannot interact with the select option. | `boolean` | `false` |
|
||||
| `value` | `value` | The text value of the option. | `any` | `undefined` |
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
| ---------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- |
|
||||
| `disabled` | `disabled` | If `true`, the user cannot interact with the select option. This property does not apply when `interface="action-sheet"` as `ion-action-sheet` does not allow for disabled buttons. | `boolean` | `false` |
|
||||
| `value` | `value` | The text value of the option. | `any` | `undefined` |
|
||||
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
@@ -14,7 +14,7 @@ export class SelectOption implements ComponentInterface {
|
||||
@Element() el!: HTMLElement;
|
||||
|
||||
/**
|
||||
* If `true`, the user cannot interact with the select option.
|
||||
* If `true`, the user cannot interact with the select option. This property does not apply when `interface="action-sheet"` as `ion-action-sheet` does not allow for disabled buttons.
|
||||
*/
|
||||
@Prop() disabled = false;
|
||||
|
||||
|
||||
@@ -1225,12 +1225,12 @@ export class SelectExample {
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Description | Type |
|
||||
| ----------- | ---------------------------------------- | -------------------------------------- |
|
||||
| `ionBlur` | Emitted when the select loses focus. | `CustomEvent<void>` |
|
||||
| `ionCancel` | Emitted when the selection is cancelled. | `CustomEvent<void>` |
|
||||
| `ionChange` | Emitted when the value has changed. | `CustomEvent<SelectChangeEventDetail>` |
|
||||
| `ionFocus` | Emitted when the select has focus. | `CustomEvent<void>` |
|
||||
| Event | Description | Type |
|
||||
| ----------- | ---------------------------------------- | ------------------------------------------- |
|
||||
| `ionBlur` | Emitted when the select loses focus. | `CustomEvent<void>` |
|
||||
| `ionCancel` | Emitted when the selection is cancelled. | `CustomEvent<void>` |
|
||||
| `ionChange` | Emitted when the value has changed. | `CustomEvent<SelectChangeEventDetail<any>>` |
|
||||
| `ionFocus` | Emitted when the select has focus. | `CustomEvent<void>` |
|
||||
|
||||
|
||||
## Methods
|
||||
|
||||
@@ -2,6 +2,6 @@ export type SelectInterface = 'action-sheet' | 'popover' | 'alert';
|
||||
|
||||
export type SelectCompareFn = (currentValue: any, compareValue: any) => boolean;
|
||||
|
||||
export interface SelectChangeEventDetail {
|
||||
value: any | any[] | undefined | null;
|
||||
export interface SelectChangeEventDetail<T = any> {
|
||||
value: T;
|
||||
}
|
||||
|
||||
@@ -459,7 +459,7 @@ export class Select implements ComponentInterface {
|
||||
return (
|
||||
<Host
|
||||
onClick={this.onClick}
|
||||
role="combobox"
|
||||
role="listbox"
|
||||
aria-haspopup="dialog"
|
||||
aria-disabled={disabled ? 'true' : null}
|
||||
aria-expanded={`${isExpanded}`}
|
||||
|
||||
@@ -4494,6 +4494,120 @@ var Observer$1 = {
|
||||
},
|
||||
};
|
||||
|
||||
const Keyboard = {
|
||||
handle(event) {
|
||||
const swiper = this;
|
||||
const { rtlTranslate: rtl } = swiper;
|
||||
let e = event;
|
||||
if (e.originalEvent) e = e.originalEvent; // jquery fix
|
||||
const kc = e.keyCode || e.charCode;
|
||||
// Directions locks
|
||||
if (!swiper.allowSlideNext && ((swiper.isHorizontal() && kc === 39) || (swiper.isVertical() && kc === 40) || kc === 34)) {
|
||||
return false;
|
||||
}
|
||||
if (!swiper.allowSlidePrev && ((swiper.isHorizontal() && kc === 37) || (swiper.isVertical() && kc === 38) || kc === 33)) {
|
||||
return false;
|
||||
}
|
||||
if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) {
|
||||
return undefined;
|
||||
}
|
||||
if (doc.activeElement && doc.activeElement.nodeName && (doc.activeElement.nodeName.toLowerCase() === 'input' || doc.activeElement.nodeName.toLowerCase() === 'textarea')) {
|
||||
return undefined;
|
||||
}
|
||||
if (swiper.params.keyboard.onlyInViewport && (kc === 33 || kc === 34 || kc === 37 || kc === 39 || kc === 38 || kc === 40)) {
|
||||
let inView = false;
|
||||
// Check that swiper should be inside of visible area of window
|
||||
if (swiper.$el.parents(`.${swiper.params.slideClass}`).length > 0 && swiper.$el.parents(`.${swiper.params.slideActiveClass}`).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const windowWidth = win.innerWidth;
|
||||
const windowHeight = win.innerHeight;
|
||||
const swiperOffset = swiper.$el.offset();
|
||||
if (rtl) swiperOffset.left -= swiper.$el[0].scrollLeft;
|
||||
const swiperCoord = [
|
||||
[swiperOffset.left, swiperOffset.top],
|
||||
[swiperOffset.left + swiper.width, swiperOffset.top],
|
||||
[swiperOffset.left, swiperOffset.top + swiper.height],
|
||||
[swiperOffset.left + swiper.width, swiperOffset.top + swiper.height],
|
||||
];
|
||||
for (let i = 0; i < swiperCoord.length; i += 1) {
|
||||
const point = swiperCoord[i];
|
||||
if (
|
||||
point[0] >= 0 && point[0] <= windowWidth
|
||||
&& point[1] >= 0 && point[1] <= windowHeight
|
||||
) {
|
||||
inView = true;
|
||||
}
|
||||
}
|
||||
if (!inView) return undefined;
|
||||
}
|
||||
if (swiper.isHorizontal()) {
|
||||
if (kc === 33 || kc === 34 || kc === 37 || kc === 39) {
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
else e.returnValue = false;
|
||||
}
|
||||
if (((kc === 34 || kc === 39) && !rtl) || ((kc === 33 || kc === 37) && rtl)) swiper.slideNext();
|
||||
if (((kc === 33 || kc === 37) && !rtl) || ((kc === 34 || kc === 39) && rtl)) swiper.slidePrev();
|
||||
} else {
|
||||
if (kc === 33 || kc === 34 || kc === 38 || kc === 40) {
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
else e.returnValue = false;
|
||||
}
|
||||
if (kc === 34 || kc === 40) swiper.slideNext();
|
||||
if (kc === 33 || kc === 38) swiper.slidePrev();
|
||||
}
|
||||
swiper.emit('keyPress', kc);
|
||||
return undefined;
|
||||
},
|
||||
enable() {
|
||||
const swiper = this;
|
||||
if (swiper.keyboard.enabled) return;
|
||||
$(doc).on('keydown', swiper.keyboard.handle);
|
||||
swiper.keyboard.enabled = true;
|
||||
},
|
||||
disable() {
|
||||
const swiper = this;
|
||||
if (!swiper.keyboard.enabled) return;
|
||||
$(doc).off('keydown', swiper.keyboard.handle);
|
||||
swiper.keyboard.enabled = false;
|
||||
},
|
||||
};
|
||||
|
||||
var keyboard = {
|
||||
name: 'keyboard',
|
||||
params: {
|
||||
keyboard: {
|
||||
enabled: false,
|
||||
onlyInViewport: true,
|
||||
},
|
||||
},
|
||||
create() {
|
||||
const swiper = this;
|
||||
Utils.extend(swiper, {
|
||||
keyboard: {
|
||||
enabled: false,
|
||||
enable: Keyboard.enable.bind(swiper),
|
||||
disable: Keyboard.disable.bind(swiper),
|
||||
handle: Keyboard.handle.bind(swiper),
|
||||
},
|
||||
});
|
||||
},
|
||||
on: {
|
||||
init() {
|
||||
const swiper = this;
|
||||
if (swiper.params.keyboard.enabled) {
|
||||
swiper.keyboard.enable();
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
const swiper = this;
|
||||
if (swiper.keyboard.enabled) {
|
||||
swiper.keyboard.disable();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function isEventSupported() {
|
||||
const eventName = 'onwheel';
|
||||
let isSupported = eventName in doc;
|
||||
@@ -6299,6 +6413,6 @@ if (typeof Swiper.use === 'undefined') {
|
||||
|
||||
Swiper.use(components);
|
||||
|
||||
Swiper.use([pagination, scrollbar, autoplay, zoom]);
|
||||
Swiper.use([pagination, scrollbar, autoplay, keyboard, zoom]);
|
||||
|
||||
export { Swiper };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Autoplay, Pagination, Scrollbar, Swiper, Zoom } from 'swiper/js/swiper.esm';
|
||||
import { Autoplay, Pagination, Scrollbar, Swiper, Keyboard, Zoom } from 'swiper/js/swiper.esm';
|
||||
|
||||
Swiper.use([Pagination, Scrollbar, Autoplay, Zoom]);
|
||||
Swiper.use([Pagination, Scrollbar, Autoplay, Keyboard, Zoom]);
|
||||
export { Swiper };
|
||||
|
||||
@@ -56,7 +56,11 @@
|
||||
let slideCount = 4;
|
||||
const slides = document.getElementById('slides')
|
||||
slides.pager = false;
|
||||
slides.options = {}
|
||||
slides.options = {
|
||||
keyboard: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
|
||||
async function addSlide() {
|
||||
const slide = document.createElement('ion-slide');
|
||||
|
||||
@@ -286,9 +286,9 @@ export class TextareaExample {
|
||||
|
||||
| Event | Description | Type |
|
||||
| ----------- | ----------------------------------------- | ---------------------------------------- |
|
||||
| `ionBlur` | Emitted when the input loses focus. | `CustomEvent<void>` |
|
||||
| `ionBlur` | Emitted when the input loses focus. | `CustomEvent<FocusEvent>` |
|
||||
| `ionChange` | Emitted when the input value has changed. | `CustomEvent<TextareaChangeEventDetail>` |
|
||||
| `ionFocus` | Emitted when the input has focus. | `CustomEvent<void>` |
|
||||
| `ionFocus` | Emitted when the input has focus. | `CustomEvent<FocusEvent>` |
|
||||
| `ionInput` | Emitted when a keyboard input occurred. | `CustomEvent<KeyboardEvent>` |
|
||||
|
||||
|
||||
|
||||
@@ -177,12 +177,12 @@ export class Textarea implements ComponentInterface {
|
||||
/**
|
||||
* Emitted when the input loses focus.
|
||||
*/
|
||||
@Event() ionBlur!: EventEmitter<void>;
|
||||
@Event() ionBlur!: EventEmitter<FocusEvent>;
|
||||
|
||||
/**
|
||||
* Emitted when the input has focus.
|
||||
*/
|
||||
@Event() ionFocus!: EventEmitter<void>;
|
||||
@Event() ionFocus!: EventEmitter<FocusEvent>;
|
||||
|
||||
connectedCallback() {
|
||||
this.emitStyle();
|
||||
@@ -292,18 +292,18 @@ export class Textarea implements ComponentInterface {
|
||||
this.ionInput.emit(ev as KeyboardEvent);
|
||||
}
|
||||
|
||||
private onFocus = () => {
|
||||
private onFocus = (ev: FocusEvent) => {
|
||||
this.hasFocus = true;
|
||||
this.focusChange();
|
||||
|
||||
this.ionFocus.emit();
|
||||
this.ionFocus.emit(ev);
|
||||
}
|
||||
|
||||
private onBlur = () => {
|
||||
private onBlur = (ev: FocusEvent) => {
|
||||
this.hasFocus = false;
|
||||
this.focusChange();
|
||||
|
||||
this.ionBlur.emit();
|
||||
this.ionBlur.emit(ev);
|
||||
}
|
||||
|
||||
private onKeyDown = () => {
|
||||
@@ -333,6 +333,7 @@ export class Textarea implements ComponentInterface {
|
||||
>
|
||||
<textarea
|
||||
class="native-textarea"
|
||||
aria-labelledby={labelId}
|
||||
ref={el => this.nativeInput = el}
|
||||
autoCapitalize={this.autocapitalize}
|
||||
autoFocus={this.autofocus}
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
|
||||
:host(.title-large) {
|
||||
@include padding(0, 16px);
|
||||
@include transform-origin(start, center);
|
||||
|
||||
bottom: 0;
|
||||
|
||||
@@ -59,3 +60,7 @@
|
||||
:host(.title-large.ion-cloned-element) {
|
||||
--color: #{$text-color};
|
||||
}
|
||||
|
||||
:host(.title-large) .toolbar-title {
|
||||
@include transform-origin(inherit);
|
||||
}
|
||||
|
||||
@@ -288,6 +288,16 @@ Type: `Promise<void>`
|
||||
|
||||
|
||||
|
||||
## Shadow Parts
|
||||
|
||||
| Part | Description |
|
||||
| ------------- | --------------------------------------------------------- |
|
||||
| `"button"` | Any button element that is displayed inside of the toast. |
|
||||
| `"container"` | The element that wraps all child elements. |
|
||||
| `"header"` | The header text of the toast. |
|
||||
| `"message"` | The body text of the toast. |
|
||||
|
||||
|
||||
## CSS Custom Properties
|
||||
|
||||
| Name | Description |
|
||||
|
||||
@@ -13,6 +13,11 @@ import { mdLeaveAnimation } from './animations/md.leave';
|
||||
|
||||
/**
|
||||
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
|
||||
*
|
||||
* @part button - Any button element that is displayed inside of the toast.
|
||||
* @part container - The element that wraps all child elements.
|
||||
* @part header - The header text of the toast.
|
||||
* @part message - The body text of the toast.
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-toast',
|
||||
@@ -121,7 +126,7 @@ export class Toast implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Event({ eventName: 'ionToastDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
constructor() {
|
||||
connectedCallback() {
|
||||
prepareOverlay(this.el);
|
||||
}
|
||||
|
||||
|
||||
@@ -266,7 +266,7 @@ export class VirtualScroll implements ComponentInterface {
|
||||
let node: HTMLElement | null = el;
|
||||
while (node && node !== contentEl) {
|
||||
topOffset += node.offsetTop;
|
||||
node = node.parentElement;
|
||||
node = node.offsetParent as HTMLElement;
|
||||
}
|
||||
this.viewportOffset = topOffset;
|
||||
if (scrollEl) {
|
||||
|
||||
10
core/src/interface.d.ts
vendored
10
core/src/interface.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
// Components interfaces
|
||||
import {Components as IoniconsComponents} from 'ionicons';
|
||||
import { Components as IoniconsComponents, JSX as IoniconsJSX } from 'ionicons';
|
||||
export * from './components';
|
||||
export * from './index';
|
||||
export * from './components/alert/alert-interface';
|
||||
@@ -47,7 +47,7 @@ export type AutocompleteTypes = (
|
||||
| 'tel-extension' | 'impp' | 'url' | 'photo');
|
||||
|
||||
|
||||
export type TextFieldTypes = 'date' | 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url' | 'time';
|
||||
export type TextFieldTypes = 'date' | 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url' | 'time' | 'week' | 'month' | 'datetime-local';
|
||||
export type Side = 'start' | 'end';
|
||||
export type PredefinedColors = 'primary' | 'secondary' | 'tertiary' | 'success' | 'warning' | 'danger' | 'light' | 'medium' | 'dark';
|
||||
export type Color = PredefinedColors | string;
|
||||
@@ -80,3 +80,9 @@ declare module "./components" {
|
||||
export interface IonIcon extends IoniconsComponents.IonIcon{}
|
||||
}
|
||||
}
|
||||
|
||||
declare module "./components" {
|
||||
export namespace JSX {
|
||||
export interface IonIcon extends IoniconsJSX.IonIcon {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,4 +29,9 @@ $toolbar-ios-color: var(--ion-toolbar-color, $text-color
|
||||
// --------------------------------------------------
|
||||
$item-ios-background: var(--ion-item-background, $background-color) !default;
|
||||
$item-ios-border-color: var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-250, #c8c7cc))) !default;
|
||||
$item-ios-color: var(--ion-item-color, $text-color) !default;
|
||||
$item-ios-color: var(--ion-item-color, $text-color) !default;
|
||||
|
||||
// iOS Card
|
||||
// --------------------------------------------------
|
||||
$card-ios-background: var(--ion-card-background, $item-ios-background) !default;
|
||||
$card-ios-color: var(--ion-card-color, var(--ion-item-color, $text-color-step-400)) !default;
|
||||
|
||||
@@ -31,3 +31,8 @@ $toolbar-md-color: var(--ion-toolbar-color, var(--ion-t
|
||||
$item-md-background: var(--ion-item-background, $background-color) !default;
|
||||
$item-md-border-color: var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, .13)))) !default;
|
||||
$item-md-color: var(--ion-item-color, $text-color) !default;
|
||||
|
||||
// Material Design Card
|
||||
// --------------------------------------------------
|
||||
$card-md-background: var(--ion-card-background, $item-md-background) !default;
|
||||
$card-md-color: var(--ion-card-color, var(--ion-item-color, $text-color-step-450)) !default;
|
||||
|
||||
@@ -8,7 +8,8 @@ export const createButtonActiveGesture = (
|
||||
el: HTMLElement,
|
||||
isButton: (refEl: HTMLElement) => boolean
|
||||
): Gesture => {
|
||||
let touchedButton: HTMLElement | undefined;
|
||||
let currentTouchedButton: HTMLElement | undefined;
|
||||
let initialTouchedButton: HTMLElement | undefined;
|
||||
|
||||
const activateButtonAtPoint = (x: number, y: number, hapticFeedbackFn: () => void) => {
|
||||
if (typeof (document as any) === 'undefined') { return; }
|
||||
@@ -18,30 +19,43 @@ export const createButtonActiveGesture = (
|
||||
return;
|
||||
}
|
||||
|
||||
if (target !== touchedButton) {
|
||||
if (target !== currentTouchedButton) {
|
||||
clearActiveButton();
|
||||
setActiveButton(target, hapticFeedbackFn);
|
||||
}
|
||||
};
|
||||
|
||||
const setActiveButton = (button: HTMLElement, hapticFeedbackFn: () => void) => {
|
||||
touchedButton = button;
|
||||
const buttonToModify = touchedButton;
|
||||
currentTouchedButton = button;
|
||||
|
||||
if (!initialTouchedButton) {
|
||||
initialTouchedButton = currentTouchedButton;
|
||||
}
|
||||
|
||||
const buttonToModify = currentTouchedButton;
|
||||
writeTask(() => buttonToModify.classList.add('ion-activated'));
|
||||
hapticFeedbackFn();
|
||||
};
|
||||
|
||||
const clearActiveButton = (dispatchClick = false) => {
|
||||
if (!touchedButton) { return; }
|
||||
if (!currentTouchedButton) { return; }
|
||||
|
||||
const buttonToModify = touchedButton;
|
||||
const buttonToModify = currentTouchedButton;
|
||||
writeTask(() => buttonToModify.classList.remove('ion-activated'));
|
||||
|
||||
if (dispatchClick) {
|
||||
touchedButton.click();
|
||||
/**
|
||||
* Clicking on one button, but releasing on another button
|
||||
* does not dispatch a click event in browsers, so we
|
||||
* need to do it manually here. Some browsers will
|
||||
* dispatch a click if clicking on one button, dragging over
|
||||
* another button, and releasing on the original button. In that
|
||||
* case, we need to make sure we do not cause a double click there.
|
||||
*/
|
||||
if (dispatchClick && initialTouchedButton !== currentTouchedButton) {
|
||||
currentTouchedButton.click();
|
||||
}
|
||||
|
||||
touchedButton = undefined;
|
||||
currentTouchedButton = undefined;
|
||||
};
|
||||
|
||||
return createGesture({
|
||||
@@ -53,6 +67,7 @@ export const createButtonActiveGesture = (
|
||||
onEnd: () => {
|
||||
clearActiveButton(true);
|
||||
hapticSelectionEnd();
|
||||
initialTouchedButton = undefined;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -74,6 +74,7 @@ const jsSetFocus = async (
|
||||
clearTimeout(scrollContentTimeout);
|
||||
}
|
||||
|
||||
window.removeEventListener('ionKeyboardDidShow', doubleKeyboardEventListener);
|
||||
window.removeEventListener('ionKeyboardDidShow', scrollContent);
|
||||
|
||||
// scroll the input into place
|
||||
@@ -89,6 +90,11 @@ const jsSetFocus = async (
|
||||
inputEl.focus();
|
||||
};
|
||||
|
||||
const doubleKeyboardEventListener = () => {
|
||||
window.removeEventListener('ionKeyboardDidShow', doubleKeyboardEventListener);
|
||||
window.addEventListener('ionKeyboardDidShow', scrollContent);
|
||||
};
|
||||
|
||||
if (contentEl) {
|
||||
const scrollEl = await contentEl.getScrollElement();
|
||||
|
||||
@@ -106,7 +112,20 @@ const jsSetFocus = async (
|
||||
*/
|
||||
const totalScrollAmount = scrollEl.scrollHeight - scrollEl.clientHeight;
|
||||
if (scrollData.scrollAmount > (totalScrollAmount - scrollEl.scrollTop)) {
|
||||
window.addEventListener('ionKeyboardDidShow', scrollContent);
|
||||
|
||||
/**
|
||||
* On iOS devices, the system will show a "Passwords" bar above the keyboard
|
||||
* after the initial keyboard is shown. This prevents the webview from resizing
|
||||
* until the "Passwords" bar is shown, so we need to wait for that to happen first.
|
||||
*/
|
||||
if (inputEl.type === 'password') {
|
||||
|
||||
// Add 50px to account for the "Passwords" bar
|
||||
scrollData.scrollAmount += 50;
|
||||
window.addEventListener('ionKeyboardDidShow', doubleKeyboardEventListener);
|
||||
} else {
|
||||
window.addEventListener('ionKeyboardDidShow', scrollContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* This should only fire in 2 instances:
|
||||
|
||||
@@ -5,9 +5,6 @@ const KEYBOARD_THRESHOLD = 150;
|
||||
let previousVisualViewport: any = {};
|
||||
let currentVisualViewport: any = {};
|
||||
|
||||
let previousLayoutViewport: any = {};
|
||||
let currentLayoutViewport: any = {};
|
||||
|
||||
let keyboardOpen = false;
|
||||
|
||||
/**
|
||||
@@ -16,8 +13,6 @@ let keyboardOpen = false;
|
||||
export const resetKeyboardAssist = () => {
|
||||
previousVisualViewport = {};
|
||||
currentVisualViewport = {};
|
||||
previousLayoutViewport = {};
|
||||
currentLayoutViewport = {};
|
||||
keyboardOpen = false;
|
||||
};
|
||||
|
||||
@@ -27,7 +22,6 @@ export const startKeyboardAssist = (win: Window) => {
|
||||
if (!(win as any).visualViewport) { return; }
|
||||
|
||||
currentVisualViewport = copyVisualViewport((win as any).visualViewport);
|
||||
currentLayoutViewport = copyLayoutViewport(win);
|
||||
|
||||
(win as any).visualViewport.onresize = () => {
|
||||
trackViewportChanges(win);
|
||||
@@ -67,7 +61,7 @@ export const setKeyboardClose = (win: Window) => {
|
||||
* of the previous visual viewport height minus the current
|
||||
* visual viewport height is greater than KEYBOARD_THRESHOLD
|
||||
*
|
||||
* We need to be able to accomodate users who have zooming
|
||||
* We need to be able to accommodate users who have zooming
|
||||
* enabled in their browser (or have zoomed in manually) which
|
||||
* is why we take into account the current visual viewport's
|
||||
* scale value.
|
||||
@@ -77,8 +71,7 @@ export const keyboardDidOpen = (): boolean => {
|
||||
return (
|
||||
!keyboardOpen &&
|
||||
previousVisualViewport.width === currentVisualViewport.width &&
|
||||
scaledHeightDifference > KEYBOARD_THRESHOLD &&
|
||||
!layoutViewportDidChange()
|
||||
scaledHeightDifference > KEYBOARD_THRESHOLD
|
||||
);
|
||||
};
|
||||
|
||||
@@ -100,20 +93,6 @@ export const keyboardDidClose = (win: Window): boolean => {
|
||||
return keyboardOpen && currentVisualViewport.height === win.innerHeight;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if the layout viewport has
|
||||
* changed since the last visual viewport change.
|
||||
* It is rare that a layout viewport change is not
|
||||
* associated with a visual viewport change so we
|
||||
* want to make sure we don't get any false positives.
|
||||
*/
|
||||
const layoutViewportDidChange = (): boolean => {
|
||||
return (
|
||||
currentLayoutViewport.width !== previousLayoutViewport.width ||
|
||||
currentLayoutViewport.height !== previousLayoutViewport.height
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatch a keyboard open event
|
||||
*/
|
||||
@@ -143,9 +122,6 @@ const fireKeyboardCloseEvent = (win: Window): void => {
|
||||
export const trackViewportChanges = (win: Window) => {
|
||||
previousVisualViewport = { ...currentVisualViewport };
|
||||
currentVisualViewport = copyVisualViewport((win as any).visualViewport);
|
||||
|
||||
previousLayoutViewport = { ...currentLayoutViewport };
|
||||
currentLayoutViewport = copyLayoutViewport(win);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -163,14 +139,3 @@ export const copyVisualViewport = (visualViewport: any): any => {
|
||||
scale: visualViewport.scale
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a deep copy of the layout viewport
|
||||
* at a given state
|
||||
*/
|
||||
export const copyLayoutViewport = (win: Window): any => {
|
||||
return {
|
||||
width: win.innerWidth,
|
||||
height: win.innerHeight
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { copyLayoutViewport, copyVisualViewport, setKeyboardClose, setKeyboardOpen, keyboardDidClose, keyboardDidOpen, keyboardDidResize, resetKeyboardAssist, startKeyboardAssist, trackViewportChanges, KEYBOARD_DID_OPEN, KEYBOARD_DID_CLOSE } from '../keyboard';
|
||||
import { copyVisualViewport, setKeyboardClose, setKeyboardOpen, keyboardDidClose, keyboardDidOpen, keyboardDidResize, resetKeyboardAssist, startKeyboardAssist, trackViewportChanges, KEYBOARD_DID_OPEN, KEYBOARD_DID_CLOSE } from '../keyboard';
|
||||
|
||||
const mockVisualViewport = (win: Window, visualViewport: any = { width: 320, height: 568 }, layoutViewport = { innerWidth: 320, innerHeight: 568 }): any => {
|
||||
win.visualViewport = {
|
||||
@@ -32,23 +32,11 @@ const resizeVisualViewport = (win: Window, visualViewport: any = {}) => {
|
||||
}
|
||||
}
|
||||
|
||||
const resizeLayoutViewport = (win: Window, layoutViewport: any = {}) => {
|
||||
win = Object.assign(win, { innerWidth: layoutViewport.width, innerHeight: layoutViewport.height });
|
||||
}
|
||||
|
||||
describe('Keyboard Assist Tests', () => {
|
||||
describe('copyLayoutViewport()', () => {
|
||||
it('should properly copy the layout viewport', () => {
|
||||
const win = {
|
||||
innerWidth: 100,
|
||||
innerHeight: 200
|
||||
};
|
||||
|
||||
const copiedViewport = copyLayoutViewport(win);
|
||||
|
||||
win.innerWidth = 400;
|
||||
win.innerHeight = 800;
|
||||
|
||||
expect(copiedViewport.width).toEqual(100);
|
||||
expect(copiedViewport.height).toEqual(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyVisualViewport()', () => {
|
||||
it('should properly copy the visual viewport', () => {
|
||||
@@ -110,6 +98,13 @@ describe('Keyboard Assist Tests', () => {
|
||||
expect(keyboardDidOpen(window)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return true if the layout and visual viewports resize', () => {
|
||||
resizeLayoutViewport(window, { width: 320, height: 300 });
|
||||
resizeVisualViewport(window, { width: 320, height: 300 });
|
||||
|
||||
expect(keyboardDidOpen(window)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false when visual viewport height < layout viewport heigh but does not meet the keyboard threshold', () => {
|
||||
resizeVisualViewport(window, { height: 500 });
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface OverlayController {
|
||||
export interface HTMLIonOverlayElement extends HTMLStencilElement {
|
||||
overlayIndex: number;
|
||||
backdropDismiss?: boolean;
|
||||
lastFocus?: HTMLElement;
|
||||
|
||||
dismiss(data?: any, role?: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getIonMode } from '../global/ionic-global';
|
||||
import { ActionSheetOptions, AlertOptions, Animation, AnimationBuilder, BackButtonEvent, HTMLIonOverlayElement, IonicConfig, LoadingOptions, ModalOptions, OverlayInterface, PickerOptions, PopoverOptions, ToastOptions } from '../interface';
|
||||
|
||||
import { OVERLAY_BACK_BUTTON_PRIORITY } from './hardware-back-button';
|
||||
import { getElementRoot } from './helpers';
|
||||
|
||||
let lastId = 0;
|
||||
|
||||
@@ -31,8 +32,10 @@ export const popoverController = /*@__PURE__*/createController<PopoverOptions, H
|
||||
export const toastController = /*@__PURE__*/createController<ToastOptions, HTMLIonToastElement>('ion-toast');
|
||||
|
||||
export const prepareOverlay = <T extends HTMLIonOverlayElement>(el: T) => {
|
||||
const doc = document;
|
||||
connectListeners(doc);
|
||||
/* tslint:disable-next-line */
|
||||
if (typeof document !== 'undefined') {
|
||||
connectListeners(document);
|
||||
}
|
||||
const overlayIndex = lastId++;
|
||||
el.overlayIndex = overlayIndex;
|
||||
if (!el.hasAttribute('id')) {
|
||||
@@ -41,35 +44,150 @@ export const prepareOverlay = <T extends HTMLIonOverlayElement>(el: T) => {
|
||||
};
|
||||
|
||||
export const createOverlay = <T extends HTMLIonOverlayElement>(tagName: string, opts: object | undefined): Promise<T> => {
|
||||
return customElements.whenDefined(tagName).then(() => {
|
||||
const doc = document;
|
||||
const element = doc.createElement(tagName) as HTMLIonOverlayElement;
|
||||
element.classList.add('overlay-hidden');
|
||||
/* tslint:disable-next-line */
|
||||
if (typeof customElements !== 'undefined') {
|
||||
return customElements.whenDefined(tagName).then(() => {
|
||||
const element = document.createElement(tagName) as HTMLIonOverlayElement;
|
||||
element.classList.add('overlay-hidden');
|
||||
|
||||
// convert the passed in overlay options into props
|
||||
// that get passed down into the new overlay
|
||||
Object.assign(element, opts);
|
||||
// convert the passed in overlay options into props
|
||||
// that get passed down into the new overlay
|
||||
Object.assign(element, opts);
|
||||
|
||||
// append the overlay element to the document body
|
||||
getAppRoot(doc).appendChild(element);
|
||||
// append the overlay element to the document body
|
||||
getAppRoot(document).appendChild(element);
|
||||
|
||||
return element.componentOnReady() as any;
|
||||
});
|
||||
return element.componentOnReady() as any;
|
||||
});
|
||||
}
|
||||
return Promise.resolve() as any;
|
||||
};
|
||||
|
||||
const focusableQueryString = '[tabindex]:not([tabindex^="-"]), input:not([type=hidden]), textarea, button, select, .ion-focusable';
|
||||
const innerFocusableQueryString = 'input:not([type=hidden]), textarea, button, select';
|
||||
|
||||
const focusFirstDescendant = (ref: Element, overlay: HTMLIonOverlayElement) => {
|
||||
let firstInput = ref.querySelector(focusableQueryString) as HTMLElement | null;
|
||||
|
||||
const shadowRoot = firstInput && firstInput.shadowRoot;
|
||||
if (shadowRoot) {
|
||||
// If there are no inner focusable elements, just focus the host element.
|
||||
firstInput = shadowRoot.querySelector(innerFocusableQueryString) || firstInput;
|
||||
}
|
||||
|
||||
if (firstInput) {
|
||||
firstInput.focus();
|
||||
} else {
|
||||
// Focus overlay instead of letting focus escape
|
||||
overlay.focus();
|
||||
}
|
||||
};
|
||||
|
||||
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 && lastInput.shadowRoot;
|
||||
if (shadowRoot) {
|
||||
// If there are no inner focusable elements, just focus the host element.
|
||||
lastInput = shadowRoot.querySelector(innerFocusableQueryString) || lastInput;
|
||||
}
|
||||
|
||||
if (lastInput) {
|
||||
lastInput.focus();
|
||||
} else {
|
||||
// Focus overlay instead of letting focus escape
|
||||
overlay.focus();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Traps keyboard focus inside of overlay components.
|
||||
* Based on https://w3c.github.io/aria-practices/examples/dialog-modal/alertdialog.html
|
||||
* This includes the following components: Action Sheet, Alert, Loading, Modal,
|
||||
* Picker, and Popover.
|
||||
* Should NOT include: Toast
|
||||
*/
|
||||
const trapKeyboardFocus = (ev: Event, doc: Document) => {
|
||||
const lastOverlay = getOverlay(doc);
|
||||
const target = ev.target as HTMLElement | null;
|
||||
|
||||
// If no active overlay, ignore this event
|
||||
if (!lastOverlay || !target) { return; }
|
||||
|
||||
/**
|
||||
* If we are focusing the overlay, clear
|
||||
* the last focused element so that hitting
|
||||
* tab activates the first focusable element
|
||||
* in the overlay wrapper.
|
||||
*/
|
||||
if (lastOverlay === target) {
|
||||
lastOverlay.lastFocus = undefined;
|
||||
|
||||
/**
|
||||
* Otherwise, we must be focusing an element
|
||||
* inside of the overlay. The two possible options
|
||||
* here are an input/button/etc or the ion-focus-trap
|
||||
* element. The focus trap element is used to prevent
|
||||
* the keyboard focus from leaving the overlay when
|
||||
* using Tab or screen assistants.
|
||||
*/
|
||||
} else {
|
||||
/**
|
||||
* We do not want to focus the traps, so get the overlay
|
||||
* wrapper element as the traps live outside of the wrapper.
|
||||
*/
|
||||
const overlayRoot = getElementRoot(lastOverlay);
|
||||
const overlayWrapper = overlayRoot.querySelector('.ion-overlay-wrapper');
|
||||
|
||||
if (!overlayWrapper) { return; }
|
||||
|
||||
/**
|
||||
* If the target is inside the wrapper, let the browser
|
||||
* focus as normal and keep a log of the last focused element.
|
||||
*/
|
||||
if (overlayWrapper.contains(target)) {
|
||||
lastOverlay.lastFocus = target;
|
||||
} else {
|
||||
/**
|
||||
* Otherwise, we must have focused one of the focus traps.
|
||||
* We need to wrap the focus to either the first element
|
||||
* or the last element.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Once we call `focusFirstDescendant` and focus the first
|
||||
* descendant, another focus event will fire which will
|
||||
* cause `lastOverlay.lastFocus` to be updated before
|
||||
* we can run the code after that. We will cache the value
|
||||
* here to avoid that.
|
||||
*/
|
||||
const lastFocus = lastOverlay.lastFocus;
|
||||
|
||||
// Focus the first element in the overlay wrapper
|
||||
focusFirstDescendant(overlayWrapper, lastOverlay);
|
||||
|
||||
/**
|
||||
* If the cached last focused element is the
|
||||
* same as the active element, then we need
|
||||
* to wrap focus to the last descendant. This happens
|
||||
* when the first descendant is focused, and the user
|
||||
* presses Shift + Tab. The previous line will focus
|
||||
* the same descendant again (the first one), causing
|
||||
* last focus to equal the active element.
|
||||
*/
|
||||
if (lastFocus === doc.activeElement) {
|
||||
focusLastDescendant(overlayWrapper, lastOverlay);
|
||||
}
|
||||
lastOverlay.lastFocus = doc.activeElement as HTMLElement;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const connectListeners = (doc: Document) => {
|
||||
if (lastId === 0) {
|
||||
lastId = 1;
|
||||
// trap focus inside overlays
|
||||
doc.addEventListener('focusin', ev => {
|
||||
const lastOverlay = getOverlay(doc);
|
||||
if (lastOverlay && lastOverlay.backdropDismiss && !isDescendant(lastOverlay, ev.target as HTMLElement)) {
|
||||
const firstInput = lastOverlay.querySelector('input,button') as HTMLElement | null;
|
||||
if (firstInput) {
|
||||
firstInput.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
doc.addEventListener('focus', ev => trapKeyboardFocus(ev, doc), true);
|
||||
|
||||
// handle back-button click
|
||||
doc.addEventListener('ionBackButton', ev => {
|
||||
@@ -242,16 +360,6 @@ export const isCancel = (role: string | undefined): boolean => {
|
||||
return role === 'cancel' || role === BACKDROP;
|
||||
};
|
||||
|
||||
const isDescendant = (parent: HTMLElement, child: HTMLElement | null) => {
|
||||
while (child) {
|
||||
if (child === parent) {
|
||||
return true;
|
||||
}
|
||||
child = child.parentElement;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const defaultGate = (h: any) => h();
|
||||
|
||||
export const safeCall = (handler: any, arg?: any) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user