Compare commits

..

86 Commits

Author SHA1 Message Date
github-actions
1f86b4ee5a v6.0.9 2022-02-23 17:32:34 +00:00
Sean Perkins
62878238fc test(datetime): event for calendar month text change (#24836) 2022-02-23 12:28:13 -05:00
Liam DeBeasi
b6d7e1c757 fix(datetime): month picker no longer gives duplicate months on ios 14 and older (#24792)
resolves #24663
2022-02-23 08:42:51 -05:00
Sean Perkins
b0ac7de168 fix(datetime): improve datetime sizing in modals (#24762)
Resolves #23992
2022-02-22 16:05:39 -05:00
Sean Perkins
7f1086740b test(datetime): getToday unit test (#24830) 2022-02-22 10:30:06 -05:00
Nikhil Arroju
cd05961ab1 docs(datetime): ISO Format examples fix (#24818) 2022-02-22 09:22:15 -06:00
Mat Zaremba
1c3b3791d0 docs(infinite-scroll): use proper equality check (#24767) 2022-02-22 09:02:54 -06:00
Liam DeBeasi
8246112ca1 fix(toast): toast is now correctly excluded from focus trapping (#24816)
resolves #24733
2022-02-22 08:56:30 -05:00
Sean Perkins
19ac2389eb fix(img): draggable attribute is now inherited to inner img element (#24781)
Resolves #21325

Co-authored-by: Celilsemi Sam Erkiner <celilsemi@erkiner.com>
Co-authored-by: Liam DeBeasi <liamdebeasi@users.noreply.github.com>
2022-02-21 11:50:57 -05:00
Amanda Smith
243f67362f fix(popover): default alignment to 'center' for ios mode (#24815) 2022-02-18 13:39:03 -06:00
Liam DeBeasi
0a8f44359a test(): run e2e tests in production mode (#24812) 2022-02-18 14:21:43 -05:00
Liam DeBeasi
2fc2de5177 fix(select): interface components now show correctly (#24810)
resolves #24807
2022-02-18 13:53:56 -05:00
Liam DeBeasi
99c91eff87 fix(react, vue): scroll is no longer interrupted on ios (#24791)
resolves #24435
2022-02-17 16:41:36 -05:00
Liam DeBeasi
16647b2c72 fix(datetime): month and year column order is now locale aware (#24802)
resolves #24548
2022-02-17 11:28:09 -05:00
Liam DeBeasi
fca3f56ca4 fix(modal): backdropBreakpoint allows interactivity behind sheet (#24798)
resolves #24797
2022-02-16 11:25:08 -05:00
Liam DeBeasi
866f261f07 merge release-6.0.8
v6.0.8
2022-02-15 16:49:20 -05:00
github-actions
cd486e71ff v6.0.8 2022-02-15 21:26:38 +00:00
Sean Perkins
fd031aa1c3 fix(input): only set native input value if different (#24758)
Resolves #24753
2022-02-14 14:33:17 -05:00
Wael M.B
a093544fdf fix(back-button, breadcrumb, item): flip chevron icons on RTL (#24705) 2022-02-14 09:34:10 -06:00
Victor Berchet
82c41510de doc(router-outlet): the outlet can be used without any framework (#24664) 2022-02-14 10:12:37 -05:00
Amanda Smith
38627ff277 chore(template): clarify that issue should be a link in PR template (#24775) 2022-02-11 09:31:35 -06:00
Liam DeBeasi
b401de1ab3 fix(vue): preserve custom classes on IonPage (#24776)
resolves #24772

Co-authored-by: bnachtweh <bnachtweh@users.noreply.github.com>
2022-02-11 10:11:04 -05:00
Victor Berchet
abc36ae80b fix(router-outlet): navigating to same route with different params now activates component (#24760)
resolves #24653
2022-02-11 10:08:53 -05:00
Sean Perkins
7b3838cc67 fix(datetime): navigate to month within min range (#24759)
Resolves #24757
2022-02-09 13:57:55 -05:00
Victor Berchet
be2205e5a2 fix(router-outlet): getRouteId() returns the params set in setRouteId(). (#24656)
resolves #24652
2022-02-09 13:38:46 -05:00
Victor Berchet
c40ff12052 chore(nav): code cleanup (#24730) 2022-02-09 13:22:53 -05:00
Liam DeBeasi
bae49ec5d9 merge release-6.0.7
v6.0.7
2022-02-09 11:45:36 -05:00
github-actions
87eab37ca8 v6.0.7 2022-02-09 16:00:04 +00:00
Liam DeBeasi
ef46eafc94 fix(angular): inline modals now add .ion-page class correctly (#24751)
resolves #24750
2022-02-09 10:57:21 -05:00
Liam DeBeasi
0580d65821 merge release-6.0.6
v6.0.6
2022-02-09 10:43:26 -05:00
github-actions
da4f750f8d v6.0.6 2022-02-09 14:26:46 +00:00
Liam DeBeasi
34a6ce6d7e tests(vue): enable autounmount and make tests less brittle (#24744) 2022-02-09 09:11:58 -05:00
Adam Duren
6ee7d159ec fix(select): value is selected when given array (#24687)
Resolves #24742
2022-02-08 22:56:48 -05:00
Liam DeBeasi
59bbd52e35 chore(): bump to latest stencil (#24741) 2022-02-08 16:30:33 -05:00
Sean Perkins
596aad435b fix(modal): inline modals inherit ion-page styling (#24723)
Resolves #24706

Co-authored-by: Liam DeBeasi <liamdebeasi@users.noreply.github.com>
2022-02-08 13:47:51 -05:00
Sean Perkins
c6381ce4f9 fix(input): IME composition mode (#24735)
Resolves #24669
2022-02-08 13:14:05 -05:00
Sean Perkins
4ff9524e10 fix(input): cursor position does not jump to end (#24736)
Resolves #24727
2022-02-08 11:39:02 -05:00
Liam DeBeasi
721a461073 fix(vue): replacing routes now updates location state correctly (#24721)
resolves #24432

Co-authored-by: tigohenryschultz <tigohenryschultz@users.noreply.github.com>
Co-authored-by: yoyo930021 <yoyo930021@users.noreply.github.com>
2022-02-07 15:37:43 -05:00
Sean Perkins
8c22646d66 fix(action-sheet): background includes safe area margin (#24700)
Resolves #24699
2022-02-07 15:13:35 -05:00
Victor Berchet
d40c0c3a09 fix(router): router push with relative path (#24719)
Resolves #24718
2022-02-07 14:26:57 -05:00
Sean Perkins
231d6df622 fix(datetime): minutes only filtered when max hour matches current hour (#24710)
Resolves #24702
2022-02-04 11:40:02 -05:00
Sean Perkins
aab4d306f8 fix(datetime): disable intersection observer during month update (#24713)
Resolves #24712
2022-02-04 11:39:21 -05:00
Liam DeBeasi
df84d155ea ci(e2e): do not run sync automatically (#24717) 2022-02-04 11:27:16 -05:00
Sean Perkins
f5c5c3cffa fix(popover): use alignment with popover options (#24711)
Resolves #24709
2022-02-03 15:56:04 -05:00
Sean Perkins
34cae57acc docs(contributing): update requirements for contributing (#24708) 2022-02-03 14:41:13 -05:00
Liam DeBeasi
2a27befe46 fix(angular-server): publish only the dist directory to avoid import errors (#24701)
resolves #24605
2022-02-03 12:21:31 -05:00
Liam DeBeasi
897ae4a454 fix(angular, react, vue): overlays no longer throw errors when used inside tests (#24681)
resolves #24549, resolves #24590
2022-02-02 15:25:51 -05:00
Sean Perkins
b0c9f097d2 docs(datetime): example formatting date values in ISO-8601 (#24686) 2022-02-02 15:12:51 -05:00
Liam DeBeasi
928c5fbfcb fix(modal, popover): quickly opening modal/popover no longer presents duplicates (#24697) 2022-02-02 13:38:17 -05:00
Liam DeBeasi
0b18260da6 fix(vue): routing history is correctly replaced when overwriting browser history (#24670)
resolves #23873
2022-02-02 13:35:52 -05:00
Liam DeBeasi
bf9b4dfb4e merge release-6.0.5
v6.0.5
2022-02-02 10:03:07 -05:00
github-actions
7530143634 v6.0.5 2022-02-02 14:29:27 +00:00
Sean Perkins
b40fc4632e fix(datetime): prevent navigating to disabled months (#24421)
Resolves #24208, #24482
2022-02-01 12:57:03 -05:00
Augusto Destrero
6d4a07d05c docs(ripple-effect): add correct Material Design link 2022-02-01 09:05:40 -06:00
Sean Perkins
43aa6c11f4 fix(react): nested router outlets will not reanimate entered views (#24672)
Resolves #24107
2022-01-31 11:55:08 -05:00
Amanda Smith
484de5074d perf(various): don't use lazy-loaded icon names in components (#24671) 2022-01-28 09:13:39 -06:00
Sean Perkins
bdb5c421d2 fix(react-router): remove page transition flicker on outlet mounting (#24667)
Resolves #24666
2022-01-27 16:29:52 -05:00
Liam DeBeasi
6d7b1444b6 fix(vue): going back to a tabs outlet no loger causes classList error (#24665)
resolves #24654
2022-01-27 14:37:35 -05:00
Sean Perkins
1f918835f4 fix(input): min/max compatibility with react-hook-form (#24657)
Resolves #24489
2022-01-27 12:29:52 -05:00
Liam DeBeasi
a34ab420e3 merge release-6.0.4
v6.0.4
2022-01-26 15:36:05 -05:00
github-actions
43b19cf536 v6.0.4 2022-01-26 14:42:28 +00:00
Sean Perkins
378c632643 fix(datetime): timepicker popover will position relative to click target (#24616)
Resolves #24531, #24415

Co-authored-by: mixalbl4 <mixalbl4.127@gmail.com>>
2022-01-25 16:42:57 -05:00
Victor Berchet
7d5c6afd18 refactor(router): rename path to segments when dealing with an array of segments (#24627) 2022-01-25 12:51:43 -05:00
Sean Perkins
6f66d08ba8 refactor(modal): move keyboard behavior for gestures into component (#24650) 2022-01-25 12:25:16 -05:00
Sean Perkins
525f01f086 fix(modal): native keyboard will dismiss when bottom sheet is dragged (#24642)
Resolves #23878

Co-authored-by: EinfachHans <hanskrywaa@web.de>
2022-01-25 11:28:30 -05:00
Liam DeBeasi
632dafcd57 fix(menu): focus trapping with menu and overlays no longer results in errors (#24611)
resolves #24361, #24481
2022-01-25 10:09:37 -05:00
Amanda Smith
88ce010418 fix(spinner): ensure transform doesn't overwrite circular animation (#24643) 2022-01-24 15:25:53 -06:00
Sean Perkins
90a9a9c3e8 fix(angular): routerLink with null value works with Angular 13 (#24622)
Resolves #24586
2022-01-24 16:07:27 -05:00
Sean Perkins
fde35a361f chore(menu-toggle): autogenerated readme contents (#24644) 2022-01-24 15:39:18 -05:00
Sean Perkins
034d049209 chore(angular): upgrade test app to Angular 13 (#24621) 2022-01-24 11:50:27 -05:00
Sean Perkins
94d033c421 fix(item): label text aligns with input text (#24620)
Resolves #24404
2022-01-21 10:30:56 -05:00
Liam DeBeasi
d3311df967 fix(accordion): items inside of the content now correct display borders (#24618)
resolves #24613
2022-01-21 09:42:26 -05:00
Sean Perkins
43c5977d48 fix(input): ion-input accepts any string value (#24606)
Resolves #19884
2022-01-20 12:47:50 -05:00
Sean Perkins
5925e7608e fix(datetime): datetime locale with h23 will respect max time range (#24610)
Resolves #24588
2022-01-20 12:47:06 -05:00
Amanda Smith
f295134624 style(): fix shadowed vars (#24609) 2022-01-20 10:56:33 -06:00
Liam DeBeasi
af0135ce7d fix(col): col no longer errors when running in non-browser environment (#24603)
resolves #24446

Co-authored-by: Sean Perkins <sean@ionic.io>
2022-01-19 16:03:53 -05:00
Liam DeBeasi
360643d96a fix(react): setupIonicReact no longer crashes in SSR environment (#24604) 2022-01-19 15:48:46 -05:00
Sean Perkins
35e5235645 fix(searchbar): setting dir on ion-searchbar to enable rtl mode now supported (#24602) 2022-01-19 15:23:44 -05:00
Sean Perkins
2940e73a45 fix(segment): setting dir on ion-segment to enable rtl mode now supported (#24601)
Resolves #23978
2022-01-19 15:23:14 -05:00
Liam DeBeasi
3e2d04dcc6 chore(release): remove old release scripts and deps (#24599) 2022-01-19 13:41:22 -05:00
Sean Perkins
353dbc0537 fix(toggle): setting dir on ion-toggle to enable rtl mode now supported (#24600) 2022-01-19 13:33:47 -05:00
Sean Perkins
5dba4e5ce0 fix(range): setting dir on ion-range to enable rtl mode now supported (#24597)
Resolves #20201
2022-01-19 12:59:19 -05:00
Liam DeBeasi
bbbe778a8a chore(package): bump supported node version to 15+ (#24598) 2022-01-19 12:14:03 -05:00
Sean Perkins
29f1140384 fix(toast): allow input focus while toast is visible (#24572)
Resolves #24571
2022-01-19 12:01:14 -05:00
Sean Perkins
54db1a1e7c fix(item): match material design character counter (#24335)
Resolves #24334
2022-01-19 11:24:08 -05:00
Liam DeBeasi
eb7905cac9 merge release-6.0.3
v6.0.3
2022-01-19 10:21:31 -05:00
227 changed files with 12806 additions and 13015 deletions

View File

@@ -6,6 +6,7 @@ Thanks for your interest in contributing to the Ionic Framework! :tada:
- [Creating an Issue](#creating-an-issue)
* [Creating a Good Code Reproduction](#creating-a-good-code-reproduction)
- [Creating a Pull Request](#creating-a-pull-request)
* [Requirements](#requirements)
* [Setup](#setup)
* [Core](#core)
+ [Modifying Components](#modifying-components)
@@ -41,7 +42,7 @@ Please see our [Contributor Code of Conduct](https://github.com/ionic-team/ionic
## Creating an Issue
* If you have a question about using the framework, please ask on the [Ionic Forum](http://forum.ionicframework.com/) or in the [Ionic Worldwide Slack](http://ionicworldwide.herokuapp.com/) group.
* If you have a question about using the framework, please ask on the [Ionic Forum](http://forum.ionicframework.com/) or in the [Ionic Discord](https://ionic.link/discord).
* It is required that you clearly describe the steps necessary to reproduce the issue you are running into. Although we would love to help our users as much as possible, diagnosing issues without clear reproduction steps is extremely time-consuming and simply not sustainable.
@@ -83,10 +84,16 @@ Without a reliable code reproduction, it is unlikely we will be able to resolve
## Creating a Pull Request
* We appreciate you taking the time to contribute! Before submitting a pull request, we ask that you please [create an issue](#creating-an-issue) that explains the bug or feature request and let us know that you plan on creating a pull request for it. If an issue already exists, please comment on that issue letting us know you would like to submit a pull request for it. This helps us to keep track of the pull request and make sure there isn't duplicated effort.
Before creating a pull request, please read our requirements that explains the minimal details to have your PR considered and merged into the codebase.
* Looking for an issue to fix? Make sure to look through our issues with the [help wanted](https://github.com/ionic-team/ionic/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) label!
### Requirements
1. PRs must reference an existing issue that describes the issue or feature being submitted.
2. PRs must have a reproduction app or the issue must include a reproduction app to verify changes against.
3. PRs must include tests covering the changed behavior or a description of why tests cannot be written.
> Note: We appreciate you taking the time to contribute! Before submitting a pull request, please take the time to comment on the issue you are wanting to resolve. This helps us prevent duplicate effort or advise if the team is already addressing the issue.
* Looking for an issue to fix? Look through our issues with the [help wanted](https://github.com/ionic-team/ionic/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) label!
### Setup

View File

@@ -49,7 +49,7 @@ body:
label: Ionic Info
description: Please run `ionic info` from within your Ionic Framework project directory and paste the output below.
validations:
requred: true
required: true
- type: textarea
attributes:
label: Additional Information

View File

@@ -26,9 +26,11 @@ Please check the type of change your PR introduces:
## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
<!-- Please describe the current behavior that you are modifying. -->
Issue Number: N/A
<!-- Issues are required for both bug fixes and features. -->
Issue URL:
## What is the new behavior?

View File

@@ -5,7 +5,7 @@ runs:
steps:
- uses: actions/setup-node@v1
with:
node-version: 15.x
node-version: 16
- name: Cache Core Node Modules
uses: actions/cache@v2
@@ -33,6 +33,10 @@ runs:
run: npm install
shell: bash
working-directory: ./angular/test/test-app
- name: Sync Built Changes
run: npm run sync
shell: bash
working-directory: ./angular/test/test-app
- name: Run Tests
run: npm run test
shell: bash

View File

@@ -29,9 +29,19 @@ runs:
name: ionic-react-router
path: ./packages/react-router
filename: ReactRouterBuild.zip
- uses: cypress-io/github-action@v2
with:
browser: chrome
headless: true
start: npm run start.ci
working-directory: ./packages/react/test-app
- name: Install Dependencies
run: npm install
shell: bash
working-directory: ./packages/react/test-app
- name: Sync
run: npm run sync
shell: bash
working-directory: ./packages/react/test-app
- name: Build
run: npm run build
shell: bash
working-directory: ./packages/react/test-app
- name: Run E2E Tests
run: npm run e2e
shell: bash
working-directory: ./packages/react/test-app

View File

@@ -29,9 +29,19 @@ runs:
name: ionic-react-router
path: ./packages/react-router
filename: ReactRouterBuild.zip
- uses: cypress-io/github-action@v2
with:
browser: chrome
headless: true
start: npm run start.ci
working-directory: ./packages/react-router/test-app
- name: Install Dependencies
run: npm install
shell: bash
working-directory: ./packages/react-router/test-app
- name: Sync
run: npm run sync
shell: bash
working-directory: ./packages/react-router/test-app
- name: Build
run: npm run build
shell: bash
working-directory: ./packages/react-router/test-app
- name: Run E2E Tests
run: npm run e2e
shell: bash
working-directory: ./packages/react-router/test-app

View File

@@ -1,26 +0,0 @@
# Build Scripts
## Release
The deploy scripts at the root, make a new release of all the packages in this monorepo.
All packages will be released with the same version.
In order to make a new release:
1. `npm run release.prepare`
2. Review/update changelog
3. Commit updates using the package name and version number as the commit message.
4. `npm run release`
5. :tada:
## Prerelease
It's also possible to make prereleases of individual packages (@ionic/core, @ionic/angular).
In order to do so, move to the package you want to make a new release and execute:
```
npm run prerelease
```
It will publish a new prerelease in NPM, but it will not create any new git tag
or update the CHANGELOG.

View File

@@ -1,15 +0,0 @@
const common = require('./common');
const Listr = require('listr');
async function main() {
const tasks = [];
common.packages.forEach(package => {
common.preparePackage(tasks, package, false, false);
});
const listr = new Listr(tasks, { showSubtasks: true });
await listr.run();
}
main();

View File

@@ -1,404 +0,0 @@
const fs = require('fs-extra');
const path = require('path');
const execa = require('execa');
const inquirer = require('inquirer');
const Listr = require('listr');
const semver = require('semver');
const { bold, cyan, dim } = require('colorette');
const rootDir = path.join(__dirname, '../');
const packages = [
'core',
'docs',
'angular',
'packages/react',
'packages/react-router',
'packages/angular-server',
'packages/vue',
'packages/vue-router'
];
function readPkg(project) {
const packageJsonPath = packagePath(project);
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
}
function writePkg(project, pkg) {
const packageJsonPath = packagePath(project);
const text = JSON.stringify(pkg, null, 2);
return fs.writeFileSync(packageJsonPath, `${text}\n`);
}
function packagePath(project) {
return path.join(rootDir, project, 'package.json');
}
function projectPath(project) {
return path.join(rootDir, project);
}
async function askNpmTag(version) {
const prompts = [
{
type: 'list',
name: 'npmTag',
message: 'Select npm tag or specify a new tag',
choices: ['latest', 'next', 'v4-lts', 'v5-lts']
.concat([
new inquirer.Separator(),
{
name: 'Other (specify)',
value: null
}
])
},
{
type: 'confirm',
name: 'confirm',
message: answers => {
return `Will publish ${cyan(version)} to ${cyan(answers.npmTag)}. Continue?`;
}
}
];
const { npmTag, confirm } = await inquirer.prompt(prompts);
return { npmTag, confirm };
}
function checkGit(tasks) {
tasks.push(
{
title: 'Check current branch',
task: () =>
execa.stdout('git', ['symbolic-ref', '--short', 'HEAD']).then(branch => {
if (branch.indexOf('release') === -1 && branch.indexOf('hotfix') === -1) {
throw new Error(`Must be on a "release" or "hotfix" branch.`);
}
})
},
{
title: 'Check local working tree',
task: () =>
execa.stdout('git', ['status', '--porcelain']).then(status => {
if (status !== '') {
throw new Error(`Unclean working tree. Commit or stash changes first.`);
}
})
},
{
title: 'Check remote history',
task: () =>
execa.stdout('git', ['rev-list', '--count', '--left-only', '@{u}...HEAD']).then(result => {
if (result !== '0') {
throw new Error(`Remote history differs. Please pull changes.`);
}
})
}
);
}
function checkTestDist(tasks) {
tasks.push({
title: 'Check dist folders for required files',
task: () =>
execa.stdout('node', ['.scripts/test-dist.js']).then(status => {
if (status.indexOf('✅ test.dist') === -1) {
throw new Error(`Test Dist did not find some required files`);
}
})
});
}
const isValidVersion = input => Boolean(semver.valid(input));
function preparePackage(tasks, package, version, install) {
const projectRoot = projectPath(package);
const pkg = readPkg(package);
const projectTasks = [];
if (version) {
projectTasks.push({
title: `${pkg.name}: validate new version`,
task: () => {
if (!isVersionGreater(pkg.version, version)) {
throw new Error(
`New version \`${version}\` should be higher than current version \`${pkg.version}\``
);
}
}
});
if (install) {
projectTasks.push({
title: `${pkg.name}: install npm dependencies`,
task: async () => {
await fs.remove(path.join(projectRoot, 'node_modules'));
await execa('npm', ['i', '--legacy-peer-deps'], { cwd: projectRoot });
}
});
}
}
if (package !== 'docs') {
if (package !== 'core') {
projectTasks.push({
title: `${pkg.name}: npm link @ionic/core`,
task: () => execa('npm', ['link', '@ionic/core', '--legacy-peer-deps'], { cwd: projectRoot })
});
if (package === 'packages/react-router') {
projectTasks.push({
title: `${pkg.name}: npm link @ionic/react`,
task: () => execa('npm', ['link', '@ionic/react', '--legacy-peer-deps'], { cwd: projectRoot })
});
}
}
// Lint, Test, Bump Core dependency
if (version) {
projectTasks.push({
title: `${pkg.name}: lint`,
task: () => execa('npm', ['run', 'lint'], { cwd: projectRoot })
});
// TODO will not work due to https://github.com/ionic-team/ionic/issues/20136
// projectTasks.push({
// title: `${pkg.name}: test`,
// task: async () => await execa('npm', ['test'], { cwd: projectRoot })
// });
}
// Build
projectTasks.push({
title: `${pkg.name}: build`,
task: () => execa('npm', ['run', 'build'], { cwd: projectRoot })
});
// Link core or react for sub projects
if (package === 'core' || package === 'packages/react') {
projectTasks.push({
title: `${pkg.name}: npm link`,
task: () => execa('npm', ['link'], { cwd: projectRoot })
});
}
if (version) {
projectTasks.push({
title: `${pkg.name}: update ionic/core dep to ${version}`,
task: () => {
updateDependency(pkg, '@ionic/core', version);
writePkg(package, pkg);
}
});
}
}
// Add project tasks
tasks.push({
title: `Prepare ${bold(pkg.name)}`,
task: () => new Listr(projectTasks)
});
}
function prepareDevPackage(tasks, package, version) {
const projectRoot = projectPath(package);
const pkg = readPkg(package);
const projectTasks = [];
if (package !== 'docs') {
if (package !== 'core') {
projectTasks.push({
title: `${pkg.name}: npm link @ionic/core`,
task: () => execa('npm', ['link', '@ionic/core', '--legacy-peer-deps'], { cwd: projectRoot })
});
if (package === 'packages/react-router') {
projectTasks.push({
title: `${pkg.name}: npm link @ionic/react`,
task: () => execa('npm', ['link', '@ionic/react', '--legacy-peer-deps'], { cwd: projectRoot })
});
}
}
projectTasks.push({
title: `${pkg.name}: update ionic/core dep to ${version}`,
task: () => {
updateDependency(pkg, '@ionic/core', version);
writePkg(package, pkg);
}
});
projectTasks.push({
title: `${pkg.name}: build`,
task: () => execa('npm', ['run', 'build'], { cwd: projectRoot })
});
if (package === 'core' || package === 'packages/react') {
projectTasks.push({
title: `${pkg.name}: npm link`,
task: () => execa('npm', ['link'], { cwd: projectRoot })
});
}
}
// Add project tasks
tasks.push({
title: `Prepare dev build: ${bold(pkg.name)}`,
task: () => new Listr(projectTasks)
});
}
function updatePackageVersions(tasks, packages, version) {
packages.forEach(package => {
updatePackageVersion(tasks, package, version);
tasks.push({
title: `${package} update @ionic/core dependency, if present ${dim(`(${version})`)}`,
task: async () => {
if (package !== 'core') {
const pkg = readPkg(package);
updateDependency(pkg, '@ionic/core', version);
writePkg(package, pkg);
}
}
});
// angular & angular-server need to update their dist versions
if (package === 'angular' || package === 'packages/angular-server') {
const distPackage = path.join(package, 'dist');
updatePackageVersion(tasks, distPackage, version);
tasks.push({
title: `${package} update @ionic/core dependency, if present ${dim(`(${version})`)}`,
task: async () => {
const pkg = readPkg(distPackage);
updateDependency(pkg, '@ionic/core', version);
writePkg(distPackage, pkg);
}
});
}
if (package === 'packages/react-router') {
tasks.push({
title: `${package} update @ionic/react dependency, if present ${dim(`(${version})`)}`,
task: async () => {
const pkg = readPkg(package);
updateDependency(pkg, '@ionic/react', version);
writePkg(package, pkg);
}
});
}
});
}
function updatePackageVersion(tasks, package, version) {
const projectRoot = projectPath(package);
tasks.push({
title: `${package}: update package.json ${dim(`(${version})`)}`,
task: async () => {
await execa('npm', ['version', version], { cwd: projectRoot });
}
});
}
function copyPackageToDist(tasks, packages) {
packages.forEach(package => {
const projectRoot = projectPath(package);
// angular and angular-server are the only packages that publish dist
if (package !== 'angular' && package !== 'packages/angular-server') {
return;
}
tasks.push({
title: `${package}: Copy package.json to dist`,
task: () => execa('node', ['copy-package.js', package], { cwd: path.join(rootDir, '.scripts') })
});
});
}
function publishPackages(tasks, packages, version, npmTag = 'latest') {
// first verify version
packages.forEach(package => {
if (package === 'core') {
return;
}
tasks.push({
title: `${package}: check version (must match: ${version})`,
task: () => {
const pkg = readPkg(package);
if (version !== pkg.version) {
throw new Error(`${pkg.name} version ${pkg.version} must match ${version}`);
}
}
});
});
// Publish
packages.forEach(package => {
let projectRoot = projectPath(package);
if (package === 'packages/angular-server' || package === 'angular') {
projectRoot = path.join(projectRoot, 'dist')
}
tasks.push({
title: `${package}: publish to ${npmTag} tag`,
task: async () => {
await execa('npm', ['publish', '--tag', npmTag], { cwd: projectRoot });
}
});
});
}
function updateDependency(pkg, dependency, version) {
if (pkg.dependencies && pkg.dependencies[dependency]) {
pkg.dependencies[dependency] = version;
}
if (pkg.devDependencies && pkg.devDependencies[dependency]) {
pkg.devDependencies[dependency] = version;
}
if (pkg.peerDependencies && pkg.peerDependencies[dependency]) {
pkg.peerDependencies[dependency] = version;
}
}
function isVersionGreater(oldVersion, newVersion) {
if (!isValidVersion(newVersion)) {
throw new Error('Version should be a valid semver version.');
}
return true;
}
function copyCDNLoader(tasks, version) {
tasks.push({
title: `Copy CDN loader`,
task: () => execa('node', ['copy-cdn-loader.js', version], { cwd: path.join(rootDir, 'core', 'scripts') })
});
}
module.exports = {
checkTestDist,
checkGit,
askNpmTag,
isValidVersion,
isVersionGreater,
copyCDNLoader,
copyPackageToDist,
packages,
packagePath,
prepareDevPackage,
preparePackage,
projectPath,
publishPackages,
readPkg,
rootDir,
updateDependency,
updatePackageVersion,
updatePackageVersions,
writePkg
};

View File

@@ -1,214 +0,0 @@
/**
* Deploy script adopted from https://github.com/sindresorhus/np
* MIT License (c) Sindre Sorhus (sindresorhus.com)
*/
const { cyan, dim, red, reset } = require('colorette');
const execa = require('execa');
const inquirer = require('inquirer');
const Listr = require('listr');
const semver = require('semver');
const common = require('./common');
const path = require('path');
async function main() {
try {
if (!process.env.GH_TOKEN) {
throw new Error('env.GH_TOKEN is undefined');
}
const { version, confirm } = await askVersion();
const install = process.argv.indexOf('--no-install') < 0;
if (!confirm) {
return;
}
// compile and verify packages
await preparePackages(common.packages, version, install);
console.log(`\nionic ${version} prepared 🤖\n`);
console.log(`Next steps:`);
console.log(` Verify CHANGELOG.md`);
console.log(` git commit -m "${version}"`);
console.log(` npm run release\n`);
} catch(err) {
console.log('\n', red(err), '\n');
process.exit(1);
}
}
async function askVersion() {
const pkg = common.readPkg('core');
const oldVersion = pkg.version;
const prompts = [
{
type: 'list',
name: 'version',
message: 'Select semver increment or specify new version',
pageSize: SEMVER_INCREMENTS.length + 2,
choices: SEMVER_INCREMENTS
.map(inc => ({
name: `${inc} ${prettyVersionDiff(oldVersion, inc)}`,
value: inc
}))
.concat([
new inquirer.Separator(),
{
name: 'Other (specify)',
value: null
}
]),
filter: input => isValidVersionInput(input) ? getNewVersion(oldVersion, input) : input
},
{
type: 'input',
name: 'version',
message: 'Version',
when: answers => !answers.version,
filter: input => isValidVersionInput(input) ? getNewVersion(pkg.version, input) : input,
validate: input => {
if (!isValidVersionInput(input)) {
return 'Please specify a valid semver, for example, `1.2.3`. See http://semver.org';
} else if (!common.isVersionGreater(oldVersion, input)) {
return `Version must be greater than ${oldVersion}`;
}
return true;
}
},
{
type: 'confirm',
name: 'confirm',
message: answers => {
return `Will bump from ${cyan(oldVersion)} to ${cyan(answers.version)}. Continue?`;
}
}
];
const { version, confirm } = await inquirer.prompt(prompts);
return { version, confirm };
}
async function preparePackages(packages, version, install) {
// execution order matters
const tasks = [];
// check git is nice and clean local and remote
common.checkGit(tasks);
// test we're good with git
validateGit(tasks, version);
// add all the prepare scripts
// run all these tasks before updating package.json version
packages.forEach(package => {
common.preparePackage(tasks, package, version, install);
});
// add update package.json of each project
common.updatePackageVersions(tasks, packages, version);
// generate changelog
generateChangeLog(tasks);
// check dist folders
common.checkTestDist(tasks);
// update core readme with version number
updateCoreReadme(tasks, version);
common.copyCDNLoader(tasks, version);
const listr = new Listr(tasks, { showSubtasks: true });
await listr.run();
}
function validateGit(tasks, version) {
tasks.push(
{
title: `Validate git tag ${dim(`(v${version})`)}`,
task: () => execa('git', ['fetch'])
.then(() => {
return execa.stdout('npm', ['config', 'get', 'tag-version-prefix']);
})
.then(
output => {
tagPrefix = output;
},
() => {}
)
.then(() => execa.stdout('git', ['rev-parse', '--quiet', '--verify', `refs/tags/${tagPrefix}${version}`]))
.then(
output => {
if (output) {
throw new Error(`Git tag \`${tagPrefix}${version}\` already exists.`);
}
},
err => {
// Command fails with code 1 and no output if the tag does not exist, even though `--quiet` is provided
// https://github.com/sindresorhus/np/pull/73#discussion_r72385685
if (err.stdout !== '' || err.stderr !== '') {
throw err;
}
}
)
},
);
}
function generateChangeLog(tasks) {
tasks.push({
title: `Generate CHANGELOG.md`,
task: () => execa('npm', ['run', 'changelog'], { cwd: common.rootDir }),
});
}
function updateCoreReadme(tasks, version) {
tasks.push({
title: `Update core README.md`,
task: () => execa('node', ['update-readme.js', version], { cwd: path.join(common.rootDir, 'core', 'scripts') }),
});
}
const SEMVER_INCREMENTS = ['patch', 'minor', 'major'];
const isValidVersionInput = input => SEMVER_INCREMENTS.indexOf(input) !== -1 || common.isValidVersion(input);
function getNewVersion(oldVersion, input) {
if (!isValidVersionInput(input)) {
throw new Error(`Version should be either ${SEMVER_INCREMENTS.join(', ')} or a valid semver version.`);
}
return SEMVER_INCREMENTS.indexOf(input) === -1 ? input : semver.inc(oldVersion, input);
};
function prettyVersionDiff(oldVersion, inc) {
const newVersion = getNewVersion(oldVersion, inc).split('.');
oldVersion = oldVersion.split('.');
let firstVersionChange = false;
const output = [];
for (let i = 0; i < newVersion.length; i++) {
if ((newVersion[i] !== oldVersion[i] && !firstVersionChange)) {
output.push(`${dim(cyan(newVersion[i]))}`);
firstVersionChange = true;
} else if (newVersion[i].indexOf('-') >= 1) {
let preVersion = [];
preVersion = newVersion[i].split('-');
output.push(`${dim(cyan(`${preVersion[0]}-${preVersion[1]}`))}`);
} else {
output.push(reset(dim(newVersion[i])));
}
}
return output.join(reset(dim('.')));
}
main();

View File

@@ -1,107 +0,0 @@
const { cyan, red } = require('colorette');
const semver = require('semver');
const execa = require('execa');
const inquirer = require('inquirer');
const Listr = require('listr');
const fs = require('fs-extra');
const common = require('./common');
const DIST_NPM_TAG = 'dev';
const DIST_TAG = 'dev';
async function main() {
const { packages } = common;
const orgPkg = packages.map(package => {
const packageJsonPath = common.packagePath(package);
return {
filePath: packageJsonPath,
packageContent: fs.readFileSync(packageJsonPath, 'utf-8')
}
});
try {
const originalVersion = common.readPkg('core').version;
const devVersion = await getDevVersion(originalVersion);
const confirm = await askDevVersion(devVersion);
if (!confirm) {
console.log(``);
return;
}
const tasks = [];
await setPackageVersionChanges(packages, devVersion);
packages.forEach(package => {
common.prepareDevPackage(tasks, package, devVersion);
});
common.copyCDNLoader(tasks, devVersion);
common.publishPackages(tasks, packages, devVersion, DIST_NPM_TAG);
const listr = new Listr(tasks);
await listr.run();
console.log(`\nionic ${devVersion} published!! 🎉\n`);
} catch (err) {
console.log('\n', red(err), '\n');
process.exit(1);
}
orgPkg.forEach(pkg => {
fs.writeFileSync(pkg.filePath, pkg.packageContent);
});
}
async function askDevVersion(devVersion) {
const prompts = [
{
type: 'confirm',
name: 'confirm',
value: true,
message: () => {
return `Publish the dev build ${cyan(devVersion)}?`;
}
}
];
const { confirm } = await inquirer.prompt(prompts);
return confirm;
}
async function setPackageVersionChanges(packages, version) {
await Promise.all(packages.map(async package => {
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);
await execa('npm', ['version', version], { cwd: projectRoot });
}));
}
async function getDevVersion(originalVersion) {
const { stdout: sha } = await execa('git', ['log', '-1', '--format=%H']);
const shortSha = sha.substring(0, 7);
const baseVersion = semver.inc(originalVersion, 'minor');
const d = new Date();
let timestamp = d.getUTCFullYear().toString();
timestamp += (d.getUTCMonth() + 1).toString().padStart(2, '0');
timestamp += d.getUTCDate().toString().padStart(2, '0');
timestamp += d.getUTCHours().toString().padStart(2, '0');
timestamp += d.getUTCMinutes().toString().padStart(2, '0');
return `${baseVersion}-${DIST_TAG}.${timestamp}.${shortSha}`;
}
main();

View File

@@ -1,144 +0,0 @@
/**
* Deploy script adopted from https://github.com/sindresorhus/np
* MIT License (c) Sindre Sorhus (sindresorhus.com)
*/
const { cyan, dim, green, red, yellow } = require('colorette');
const execa = require('execa');
const Listr = require('listr');
const path = require('path');
const { Octokit } = require('@octokit/rest');
const common = require('./common');
const fs = require('fs-extra');
async function main() {
try {
const dryRun = process.argv.indexOf('--dry-run') > -1;
if (!process.env.GH_TOKEN) {
throw new Error('env.GH_TOKEN is undefined');
}
checkProductionRelease();
const tasks = [];
const { version } = common.readPkg('core');
const changelog = findChangelog();
// repo must be clean
common.checkGit(tasks);
const { npmTag, confirm } = await common.askNpmTag(version);
if (!confirm) {
return;
}
if(!dryRun) {
// publish each package in NPM
common.publishPackages(tasks, common.packages, version, npmTag);
// push tag to git remote
publishGit(tasks, version, changelog, npmTag);
}
const listr = new Listr(tasks);
await listr.run();
// Dry run doesn't publish to npm or git
if (dryRun) {
console.log(`
\n${yellow('Did not publish. Remove the "--dry-run" flag to publish:')}\n${green(version)} to ${cyan(npmTag)}\n
`);
} else {
console.log(`\nionic ${version} published to ${npmTag}!! 🎉\n`);
}
} catch (err) {
console.log('\n', red(err), '\n');
process.exit(1);
}
}
function checkProductionRelease() {
const corePath = common.projectPath('core');
const hasEsm = fs.existsSync(path.join(corePath, 'dist', 'esm'));
const hasEsmEs5 = fs.existsSync(path.join(corePath, 'dist', 'esm-es5'));
const hasCjs = fs.existsSync(path.join(corePath, 'dist', 'cjs'));
if (!hasEsm || !hasEsmEs5 || !hasCjs) {
throw new Error('core build is not a production build');
}
}
function publishGit(tasks, version, changelog, npmTag) {
const gitTag = `v${version}`;
tasks.push(
{
title: `Tag latest commit ${dim(`(${gitTag})`)}`,
task: () => execa('git', ['tag', `${gitTag}`], { cwd: common.rootDir })
},
{
title: 'Push branches to remote',
task: () => execa('git', ['push'], { cwd: common.rootDir })
},
{
title: 'Push tags to remove',
task: () => execa('git', ['push', '--follow-tags'], { cwd: common.rootDir })
},
{
title: 'Publish Github release',
task: () => publishGithub(version, gitTag, changelog, npmTag)
}
);
}
function findChangelog() {
const lines = fs.readFileSync('CHANGELOG.md', 'utf-8').toString().split('\n');
let start = -1;
let end = -1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('## [') || line.startsWith('# [')) {
if (start === -1) {
start = i + 1;
} else {
end = i - 1;
break;
}
}
}
if(start === -1 || end === -1) {
throw new Error('changelog diff was not found');
}
return lines.slice(start, end).join('\n').trim();
}
async function publishGithub(version, gitTag, changelog, npmTag) {
// If the npm tag is next then publish as a prerelease
const prerelease = npmTag === 'next' ? true : false;
const octokit = new Octokit({
auth: process.env.GH_TOKEN
});
let branch = await execa.stdout('git', ['symbolic-ref', '--short', 'HEAD']);
if (!branch) {
branch = 'main';
}
await octokit.repos.createRelease({
owner: 'ionic-team',
repo: 'ionic-framework',
target_commitish: branch,
tag_name: gitTag,
name: version,
body: changelog,
prerelease: prerelease
});
}
main();

View File

@@ -1,113 +0,0 @@
const path = require('path');
const fs = require('fs');
// Test dist build:
// Double-triple check all the packages
// and files are good to go before publishing
[
// core
{
files: [
'../core/css/core.css',
'../core/css/core.css.map',
'../core/css/normalize.css',
'../core/css/normalize.css.map',
'../core/components/index.js',
'../core/components/index.d.ts',
'../core/components/package.json',
'../core/dist/index.js',
'../core/dist/ionic/index.esm.js',
]
},
// hydrate
{
files: [
'../core/hydrate/index.js',
'../core/hydrate/index.d.ts',
'../core/hydrate/package.json'
]
},
// angular
{
files: [
'../angular/dist/schematics/collection.json',
'../angular/dist/fesm2015/ionic-angular.js',
'../angular/dist/esm2015/ionic-angular.js',
'../angular/dist/ionic-angular.d.ts'
]
},
// angular-server
{
files: [
'../packages/angular-server/dist/esm2015/ionic-angular-server.js',
'../packages/angular-server/dist/fesm2015/ionic-angular-server.js',
'../packages/angular-server/dist/ionic-angular-server.d.ts'
]
},
// react
{
files: ['../packages/react/dist/index.js']
},
// react-router
{
files: ['../packages/react-router/dist/index.js']
}
].forEach(testPackage);
function testPackage(testPkg) {
if (testPkg.packageJson) {
const pkgDir = path.dirname(testPkg.packageJson);
const pkgJson = require(testPkg.packageJson);
if (!pkgJson.name) {
throw new Error('missing package.json name: ' + testPkg.packageJson);
}
if (!pkgJson.main) {
throw new Error('missing package.json main: ' + testPkg.packageJson);
}
const pkgPath = path.join(pkgDir, pkgJson.main);
const pkgImport = require(pkgPath);
if (testPkg.files) {
if (!Array.isArray(pkgJson.files)) {
throw new Error(testPkg.packageJson + ' missing "files" property');
}
testPkg.files.forEach(testPkgFile => {
if (!pkgJson.files.includes(testPkgFile)) {
throw new Error(testPkg.packageJson + ' missing file ' + testPkgFile);
}
const filePath = path.join(__dirname, pkgDir, testPkgFile);
fs.accessSync(filePath);
});
}
if (pkgJson.module) {
const moduleIndex = path.join(__dirname, pkgDir, pkgJson.module);
fs.accessSync(moduleIndex);
}
if (pkgJson.types) {
const pkgTypes = path.join(__dirname, pkgDir, pkgJson.types);
fs.accessSync(pkgTypes);
}
if (testPkg.exports) {
testPkg.exports.forEach(exportName => {
const m = pkgImport[exportName];
if (!m) {
throw new Error('export "' + exportName + '" not found in: ' + testPkg.packageJson);
}
});
}
} else if (testPkg.files) {
testPkg.files.forEach(file => {
const filePath = path.join(__dirname, file);
fs.statSync(filePath);
});
}
}
console.log(`✅ test.dist`);

View File

@@ -3,6 +3,123 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [6.0.9](https://github.com/ionic-team/ionic-framework/compare/v6.0.8...v6.0.9) (2022-02-23)
### Bug Fixes
* **datetime:** improve datetime sizing in modals ([#24762](https://github.com/ionic-team/ionic-framework/issues/24762)) ([b0ac7de](https://github.com/ionic-team/ionic-framework/commit/b0ac7de168c353ba4899cb74a2b38e25fd0cc0f1)), closes [#23992](https://github.com/ionic-team/ionic-framework/issues/23992)
* **datetime:** month and year column order is now locale aware ([#24802](https://github.com/ionic-team/ionic-framework/issues/24802)) ([16647b2](https://github.com/ionic-team/ionic-framework/commit/16647b2c7290389755a4093145788f281c84b7e2)), closes [#24548](https://github.com/ionic-team/ionic-framework/issues/24548)
* **datetime:** month picker no longer gives duplicate months on ios 14 and older ([#24792](https://github.com/ionic-team/ionic-framework/issues/24792)) ([b6d7e1c](https://github.com/ionic-team/ionic-framework/commit/b6d7e1c75740a61dcd02c21692e4d4632fb8df5c)), closes [#24663](https://github.com/ionic-team/ionic-framework/issues/24663)
* **img:** draggable attribute is now inherited to inner img element ([#24781](https://github.com/ionic-team/ionic-framework/issues/24781)) ([19ac238](https://github.com/ionic-team/ionic-framework/commit/19ac2389eb0843173f51a12de41ac808cd8f0569)), closes [#21325](https://github.com/ionic-team/ionic-framework/issues/21325)
* **modal:** backdropBreakpoint allows interactivity behind sheet ([#24798](https://github.com/ionic-team/ionic-framework/issues/24798)) ([fca3f56](https://github.com/ionic-team/ionic-framework/commit/fca3f56ca4568e63fd493debda088263caa86c64)), closes [#24797](https://github.com/ionic-team/ionic-framework/issues/24797)
* **popover:** default alignment to 'center' for ios mode ([#24815](https://github.com/ionic-team/ionic-framework/issues/24815)) ([243f673](https://github.com/ionic-team/ionic-framework/commit/243f67362f25699bdb373be6b72cb9c14dc95a32))
* **react, vue:** scroll is no longer interrupted on ios ([#24791](https://github.com/ionic-team/ionic-framework/issues/24791)) ([99c91ef](https://github.com/ionic-team/ionic-framework/commit/99c91eff8764c9a1630adedab6a9765dd9754f7d)), closes [#24435](https://github.com/ionic-team/ionic-framework/issues/24435)
* **select:** interface components now show correctly ([#24810](https://github.com/ionic-team/ionic-framework/issues/24810)) ([2fc2de5](https://github.com/ionic-team/ionic-framework/commit/2fc2de51771f4a5c3f20c6071284096e5bf31ec8)), closes [#24807](https://github.com/ionic-team/ionic-framework/issues/24807)
* **toast:** toast is now correctly excluded from focus trapping ([#24816](https://github.com/ionic-team/ionic-framework/issues/24816)) ([8246112](https://github.com/ionic-team/ionic-framework/commit/8246112ca12f90edb98629ab82e27a792a1fafad)), closes [#24733](https://github.com/ionic-team/ionic-framework/issues/24733)
## [6.0.8](https://github.com/ionic-team/ionic-framework/compare/v6.0.7...v6.0.8) (2022-02-15)
### Bug Fixes
* **back-button, breadcrumb, item:** flip chevron icons on RTL ([#24705](https://github.com/ionic-team/ionic-framework/issues/24705)) ([a093544](https://github.com/ionic-team/ionic-framework/commit/a093544fdfc438ed03024285b2a35c5f645ea011))
* **datetime:** navigate to month within min range ([#24759](https://github.com/ionic-team/ionic-framework/issues/24759)) ([7b3838c](https://github.com/ionic-team/ionic-framework/commit/7b3838cc670de7845bb5937d204e86cdba93b6e6)), closes [#24757](https://github.com/ionic-team/ionic-framework/issues/24757)
* **input:** only set native input value if different ([#24758](https://github.com/ionic-team/ionic-framework/issues/24758)) ([fd031aa](https://github.com/ionic-team/ionic-framework/commit/fd031aa1c3f05b7bfa3e0a0ee2a4793e29e22df5)), closes [#24753](https://github.com/ionic-team/ionic-framework/issues/24753)
* **router-outlet:** getRouteId() returns the params set in setRouteId(). ([#24656](https://github.com/ionic-team/ionic-framework/issues/24656)) ([be2205e](https://github.com/ionic-team/ionic-framework/commit/be2205e5a2b2f8778bd1f7b4ea5cae0bf96f9ef3)), closes [#24652](https://github.com/ionic-team/ionic-framework/issues/24652)
* **router-outlet:** navigating to same route with different params now activates component ([#24760](https://github.com/ionic-team/ionic-framework/issues/24760)) ([abc36ae](https://github.com/ionic-team/ionic-framework/commit/abc36ae80b060a659f7557ad90efe98b78f5ead9)), closes [#24653](https://github.com/ionic-team/ionic-framework/issues/24653)
* **vue:** preserve custom classes on IonPage ([#24776](https://github.com/ionic-team/ionic-framework/issues/24776)) ([b401de1](https://github.com/ionic-team/ionic-framework/commit/b401de1ab3385c67cc476ff90971ce131cefcc3f)), closes [#24772](https://github.com/ionic-team/ionic-framework/issues/24772)
## [6.0.7](https://github.com/ionic-team/ionic-framework/compare/v6.0.6...v6.0.7) (2022-02-09)
### Bug Fixes
* **angular:** inline modals now add .ion-page class correctly ([#24751](https://github.com/ionic-team/ionic-framework/issues/24751)) ([ef46eaf](https://github.com/ionic-team/ionic-framework/commit/ef46eafc9476a85ea3369e542f528d01d3cca0a8)), closes [#24750](https://github.com/ionic-team/ionic-framework/issues/24750)
## [6.0.6](https://github.com/ionic-team/ionic-framework/compare/v6.0.5...v6.0.6) (2022-02-09)
### Bug Fixes
* **action-sheet:** background includes safe area margin ([#24700](https://github.com/ionic-team/ionic-framework/issues/24700)) ([8c22646](https://github.com/ionic-team/ionic-framework/commit/8c22646d66e2077fc88aaacf350330097733bb9b)), closes [#24699](https://github.com/ionic-team/ionic-framework/issues/24699)
* **angular-server:** publish only the dist directory to avoid import errors ([#24701](https://github.com/ionic-team/ionic-framework/issues/24701)) ([2a27bef](https://github.com/ionic-team/ionic-framework/commit/2a27befe463832b9ca7709ba22421abbdaa4cfa4)), closes [#24605](https://github.com/ionic-team/ionic-framework/issues/24605)
* **angular, react, vue:** overlays no longer throw errors when used inside tests ([#24681](https://github.com/ionic-team/ionic-framework/issues/24681)) ([897ae4a](https://github.com/ionic-team/ionic-framework/commit/897ae4a4546ac0dd811125d5513ef23d133a1589)), closes [#24549](https://github.com/ionic-team/ionic-framework/issues/24549) [#24590](https://github.com/ionic-team/ionic-framework/issues/24590)
* **datetime:** disable intersection observer during month update ([#24713](https://github.com/ionic-team/ionic-framework/issues/24713)) ([aab4d30](https://github.com/ionic-team/ionic-framework/commit/aab4d306f80851bfd8a02a6361e26d60faeaadb4)), closes [#24712](https://github.com/ionic-team/ionic-framework/issues/24712)
* **datetime:** minutes only filtered when max hour matches current hour ([#24710](https://github.com/ionic-team/ionic-framework/issues/24710)) ([231d6df](https://github.com/ionic-team/ionic-framework/commit/231d6df622c1f5dd9ecdff6fed8f61a4bff332df)), closes [#24702](https://github.com/ionic-team/ionic-framework/issues/24702)
* **input:** cursor position does not jump to end ([#24736](https://github.com/ionic-team/ionic-framework/issues/24736)) ([4ff9524](https://github.com/ionic-team/ionic-framework/commit/4ff9524e1057aa487069b5982c5f1ecdf51d982b)), closes [#24727](https://github.com/ionic-team/ionic-framework/issues/24727)
* **input:** IME composition mode ([#24735](https://github.com/ionic-team/ionic-framework/issues/24735)) ([c6381ce](https://github.com/ionic-team/ionic-framework/commit/c6381ce4f90707774d1c8662bba874f7b306bd1c)), closes [#24669](https://github.com/ionic-team/ionic-framework/issues/24669)
* **modal, popover:** quickly opening modal/popover no longer presents duplicates ([#24697](https://github.com/ionic-team/ionic-framework/issues/24697)) ([928c5fb](https://github.com/ionic-team/ionic-framework/commit/928c5fbfcbf3ef1b2c3074464fc20dcf1fe143ae))
* **modal:** inline modals inherit ion-page styling ([#24723](https://github.com/ionic-team/ionic-framework/issues/24723)) ([596aad4](https://github.com/ionic-team/ionic-framework/commit/596aad435b5102307da89dd626ca4682b78db452)), closes [#24706](https://github.com/ionic-team/ionic-framework/issues/24706)
* **popover:** use alignment with popover options ([#24711](https://github.com/ionic-team/ionic-framework/issues/24711)) ([f5c5c3c](https://github.com/ionic-team/ionic-framework/commit/f5c5c3cffa4f34046b0e9471a9f193db3772180e)), closes [#24709](https://github.com/ionic-team/ionic-framework/issues/24709)
* **router:** router push with relative path ([#24719](https://github.com/ionic-team/ionic-framework/issues/24719)) ([d40c0c3](https://github.com/ionic-team/ionic-framework/commit/d40c0c3a0993eaefbe5107e98958c6b0699a62c2)), closes [#24718](https://github.com/ionic-team/ionic-framework/issues/24718)
* **select:** value is selected when given array ([#24687](https://github.com/ionic-team/ionic-framework/issues/24687)) ([6ee7d15](https://github.com/ionic-team/ionic-framework/commit/6ee7d159ecfff3382fadb524c5c430172d40c267)), closes [#24742](https://github.com/ionic-team/ionic-framework/issues/24742)
* **vue:** replacing routes now updates location state correctly ([#24721](https://github.com/ionic-team/ionic-framework/issues/24721)) ([721a461](https://github.com/ionic-team/ionic-framework/commit/721a461073bbd8e7218cd5ce02965d673f5a03e8)), closes [#24432](https://github.com/ionic-team/ionic-framework/issues/24432)
* **vue:** routing history is correctly replaced when overwriting browser history ([#24670](https://github.com/ionic-team/ionic-framework/issues/24670)) ([0b18260](https://github.com/ionic-team/ionic-framework/commit/0b18260da64334d8211c5a0cd806f7416274fc5e)), closes [#23873](https://github.com/ionic-team/ionic-framework/issues/23873)
## [6.0.5](https://github.com/ionic-team/ionic-framework/compare/v6.0.4...v6.0.5) (2022-02-02)
### Bug Fixes
* **datetime:** prevent navigating to disabled months ([#24421](https://github.com/ionic-team/ionic-framework/issues/24421)) ([b40fc46](https://github.com/ionic-team/ionic-framework/commit/b40fc4632efcdc01f1062d9bcec826afff5cf4ea)), closes [#24208](https://github.com/ionic-team/ionic-framework/issues/24208) [#24482](https://github.com/ionic-team/ionic-framework/issues/24482)
* **input:** min/max compatibility with react-hook-form ([#24657](https://github.com/ionic-team/ionic-framework/issues/24657)) ([1f91883](https://github.com/ionic-team/ionic-framework/commit/1f918835f437a59f7a70fc59d9305647aa9c298d)), closes [#24489](https://github.com/ionic-team/ionic-framework/issues/24489)
* **react-router:** remove page transition flicker on outlet mounting ([#24667](https://github.com/ionic-team/ionic-framework/issues/24667)) ([bdb5c42](https://github.com/ionic-team/ionic-framework/commit/bdb5c421d2d1f72c123c254e50c6d31b3c1a8f42)), closes [#24666](https://github.com/ionic-team/ionic-framework/issues/24666)
* **react:** nested router outlets will not reanimate entered views ([#24672](https://github.com/ionic-team/ionic-framework/issues/24672)) ([43aa6c1](https://github.com/ionic-team/ionic-framework/commit/43aa6c11f42fd5cf455c50419d5f5fbb327b2901)), closes [#24107](https://github.com/ionic-team/ionic-framework/issues/24107)
* **vue:** going back to a tabs outlet no loger causes classList error ([#24665](https://github.com/ionic-team/ionic-framework/issues/24665)) ([6d7b144](https://github.com/ionic-team/ionic-framework/commit/6d7b1444b63cca03158789fcd41b33a527f6abac)), closes [#24654](https://github.com/ionic-team/ionic-framework/issues/24654)
### Performance Improvements
* **various:** don't use lazy-loaded icon names in components ([#24671](https://github.com/ionic-team/ionic-framework/issues/24671)) ([484de50](https://github.com/ionic-team/ionic-framework/commit/484de5074de212dffb4bf4f73bade7acec103fe8))
## [6.0.4](https://github.com/ionic-team/ionic-framework/compare/v6.0.3...v6.0.4) (2022-01-26)
### Bug Fixes
* **accordion:** items inside of the content now correct display borders ([#24618](https://github.com/ionic-team/ionic-framework/issues/24618)) ([d3311df](https://github.com/ionic-team/ionic-framework/commit/d3311df96765948d0a395e4ba99fb9117b44adcb)), closes [#24613](https://github.com/ionic-team/ionic-framework/issues/24613)
* **angular:** routerLink with null value works with Angular 13 ([#24622](https://github.com/ionic-team/ionic-framework/issues/24622)) ([90a9a9c](https://github.com/ionic-team/ionic-framework/commit/90a9a9c3e813c8db0a9d6b3b25c152929bea80fe)), closes [#24586](https://github.com/ionic-team/ionic-framework/issues/24586)
* **col:** col no longer errors when running in non-browser environment ([#24603](https://github.com/ionic-team/ionic-framework/issues/24603)) ([af0135c](https://github.com/ionic-team/ionic-framework/commit/af0135ce7dbe737b2df46094fd3dc8a41bdb60ae)), closes [#24446](https://github.com/ionic-team/ionic-framework/issues/24446)
* **datetime:** datetime locale with h23 will respect max time range ([#24610](https://github.com/ionic-team/ionic-framework/issues/24610)) ([5925e76](https://github.com/ionic-team/ionic-framework/commit/5925e7608ef04f8877e4dd8a80b2c8bdc1cfd4bd)), closes [#24588](https://github.com/ionic-team/ionic-framework/issues/24588)
* **datetime:** timepicker popover will position relative to click target ([#24616](https://github.com/ionic-team/ionic-framework/issues/24616)) ([378c632](https://github.com/ionic-team/ionic-framework/commit/378c63264366964e6ea11e1a2ff8791a40f182d4)), closes [#24531](https://github.com/ionic-team/ionic-framework/issues/24531) [#24415](https://github.com/ionic-team/ionic-framework/issues/24415)
* **input:** ion-input accepts any string value ([#24606](https://github.com/ionic-team/ionic-framework/issues/24606)) ([43c5977](https://github.com/ionic-team/ionic-framework/commit/43c5977d48cb12331c1d3107eb73f29b92c5e049)), closes [#19884](https://github.com/ionic-team/ionic-framework/issues/19884)
* **item:** label text aligns with input text ([#24620](https://github.com/ionic-team/ionic-framework/issues/24620)) ([94d033c](https://github.com/ionic-team/ionic-framework/commit/94d033c4216ae9978aed6346c1c0ea2aec4d375b)), closes [#24404](https://github.com/ionic-team/ionic-framework/issues/24404)
* **item:** match material design character counter ([#24335](https://github.com/ionic-team/ionic-framework/issues/24335)) ([54db1a1](https://github.com/ionic-team/ionic-framework/commit/54db1a1e7c71c78e843370848fc768582768333e)), closes [#24334](https://github.com/ionic-team/ionic-framework/issues/24334)
* **menu:** focus trapping with menu and overlays no longer results in errors ([#24611](https://github.com/ionic-team/ionic-framework/issues/24611)) ([632dafc](https://github.com/ionic-team/ionic-framework/commit/632dafcd57de5086deebdc7d586b01710aa1a3ce)), closes [#24361](https://github.com/ionic-team/ionic-framework/issues/24361) [#24481](https://github.com/ionic-team/ionic-framework/issues/24481)
* **modal:** native keyboard will dismiss when bottom sheet is dragged ([#24642](https://github.com/ionic-team/ionic-framework/issues/24642)) ([525f01f](https://github.com/ionic-team/ionic-framework/commit/525f01f086ebf95ab62af0162b876a25f50a3d4b)), closes [#23878](https://github.com/ionic-team/ionic-framework/issues/23878)
* **range:** setting dir on ion-range to enable rtl mode now supported ([#24597](https://github.com/ionic-team/ionic-framework/issues/24597)) ([5dba4e5](https://github.com/ionic-team/ionic-framework/commit/5dba4e5ce0a07f69a08f2b427e8010b311910f88)), closes [#20201](https://github.com/ionic-team/ionic-framework/issues/20201)
* **react:** setupIonicReact no longer crashes in SSR environment ([#24604](https://github.com/ionic-team/ionic-framework/issues/24604)) ([360643d](https://github.com/ionic-team/ionic-framework/commit/360643d96a03b345c83b88c9ff06e9aa254dee81))
* **searchbar:** setting dir on ion-searchbar to enable rtl mode now supported ([#24602](https://github.com/ionic-team/ionic-framework/issues/24602)) ([35e5235](https://github.com/ionic-team/ionic-framework/commit/35e523564561c0f3323efa761c4654016b97ef69))
* **segment:** setting dir on ion-segment to enable rtl mode now supported ([#24601](https://github.com/ionic-team/ionic-framework/issues/24601)) ([2940e73](https://github.com/ionic-team/ionic-framework/commit/2940e73a4504247eecb0de6c433104f529530850)), closes [#23978](https://github.com/ionic-team/ionic-framework/issues/23978)
* **spinner:** ensure transform doesn't overwrite circular animation ([#24643](https://github.com/ionic-team/ionic-framework/issues/24643)) ([88ce010](https://github.com/ionic-team/ionic-framework/commit/88ce010418eaca38786b51720c696860b417ab6d))
* **toast:** allow input focus while toast is visible ([#24572](https://github.com/ionic-team/ionic-framework/issues/24572)) ([29f1140](https://github.com/ionic-team/ionic-framework/commit/29f1140384ae7e1402b09c3760e168cf79833619)), closes [#24571](https://github.com/ionic-team/ionic-framework/issues/24571)
* **toggle:** setting dir on ion-toggle to enable rtl mode now supported ([#24600](https://github.com/ionic-team/ionic-framework/issues/24600)) ([353dbc0](https://github.com/ionic-team/ionic-framework/commit/353dbc0537ef3b46b9ba13a365ebc5da269de4c7))
## [6.0.3](https://github.com/ionic-team/ionic-framework/compare/v6.0.2...v6.0.3) (2022-01-19)

View File

@@ -3,6 +3,60 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [6.0.9](https://github.com/ionic-team/ionic/compare/v6.0.8...v6.0.9) (2022-02-23)
**Note:** Version bump only for package @ionic/angular
## [6.0.8](https://github.com/ionic-team/ionic/compare/v6.0.7...v6.0.8) (2022-02-15)
**Note:** Version bump only for package @ionic/angular
## [6.0.7](https://github.com/ionic-team/ionic/compare/v6.0.6...v6.0.7) (2022-02-09)
### Bug Fixes
* **angular:** inline modals now add .ion-page class correctly ([#24751](https://github.com/ionic-team/ionic/issues/24751)) ([ef46eaf](https://github.com/ionic-team/ionic/commit/ef46eafc9476a85ea3369e542f528d01d3cca0a8)), closes [#24750](https://github.com/ionic-team/ionic/issues/24750)
## [6.0.6](https://github.com/ionic-team/ionic/compare/v6.0.5...v6.0.6) (2022-02-09)
**Note:** Version bump only for package @ionic/angular
## [6.0.5](https://github.com/ionic-team/ionic/compare/v6.0.4...v6.0.5) (2022-02-02)
**Note:** Version bump only for package @ionic/angular
## [6.0.4](https://github.com/ionic-team/ionic/compare/v6.0.3...v6.0.4) (2022-01-26)
### Bug Fixes
* **angular:** routerLink with null value works with Angular 13 ([#24622](https://github.com/ionic-team/ionic/issues/24622)) ([90a9a9c](https://github.com/ionic-team/ionic/commit/90a9a9c3e813c8db0a9d6b3b25c152929bea80fe)), closes [#24586](https://github.com/ionic-team/ionic/issues/24586)
## [6.0.3](https://github.com/ionic-team/ionic/compare/v6.0.2...v6.0.3) (2022-01-19)

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular",
"version": "6.0.3",
"version": "6.0.9",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular",
"version": "6.0.3",
"version": "6.0.9",
"description": "Angular specific wrappers for @ionic/core",
"keywords": [
"ionic",
@@ -44,7 +44,7 @@
"validate": "npm i && npm run lint && npm run test && npm run build"
},
"dependencies": {
"@ionic/core": "^6.0.3",
"@ionic/core": "^6.0.9",
"jsonc-parser": "^3.0.0",
"tslib": "^2.0.0"
},

View File

@@ -41,7 +41,7 @@ export class RouterLinkDelegateDirective implements OnInit, OnChanges, OnDestroy
}
private updateTargetUrlAndHref() {
if (this.routerLink) {
if (this.routerLink?.urlTree) {
const href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.routerLink.urlTree));
this.elementRef.nativeElement.href = href;
}

View File

@@ -73,7 +73,7 @@ export declare interface IonModal extends Components.IonModal {
@Component({
selector: 'ion-modal',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<ng-container [ngTemplateOutlet]="template" *ngIf="isCmpOpen"></ng-container>`,
template: `<div class="ion-page"><ng-container [ngTemplateOutlet]="template" *ngIf="isCmpOpen"></ng-container></div>`,
inputs: [
'animated',
'backdropBreakpoint',

View File

@@ -29,6 +29,7 @@ speed-measure-plugin.json
!.vscode/extensions.json
# misc
/.angular/cache
/.sass-cache
/connect.lock
/coverage

View File

@@ -60,4 +60,8 @@ describe('Modals: Inline', () => {
cy.get('ion-list ion-item:nth-child(3)').should('have.text', 'C');
cy.get('ion-list ion-item:nth-child(4)').should('have.text', 'D');
});
it('should have a div with .ion-page', () => {
cy.get('ion-modal').children('.ion-page').should('exist');
});
});

View File

File diff suppressed because it is too large Load Diff

View File

@@ -4,31 +4,31 @@
"private": true,
"scripts": {
"ng": "ng",
"start": "npm run sync && ng serve",
"start": "ng serve",
"sync:build": "sh scripts/build-ionic.sh",
"sync": "sh scripts/sync.sh",
"build": "npm run sync && ng build --configuration production --no-progress",
"build": "ng build --configuration production --no-progress",
"lint": "ng lint",
"postinstall": "npm run sync && ngcc",
"postinstall": "ngcc",
"serve:ssr": "node dist/test-app/server/main.js",
"build:ssr": "ng build --prod && ng run test-app:server:production",
"dev:ssr": "ng run test-app:serve-ssr",
"prerender": "ng run test-app:prerender",
"cy.open": "cypress open",
"cy.run": "cypress run",
"test": "concurrently \"npm run start\" \"wait-on http-get://localhost:4200 && npm run cy.run\" --kill-others --success first",
"test": "concurrently \"npm run start -- --configuration production\" \"wait-on http-get://localhost:4200 && npm run cy.run\" --kill-others --success first",
"test.watch": "concurrently \"npm run start\" \"wait-on http-get://localhost:4200 && npm run cy.open\" --kill-others --success first"
},
"dependencies": {
"@angular/animations": "^12.2.8",
"@angular/common": "^12.2.8",
"@angular/compiler": "^12.2.8",
"@angular/core": "^12.2.8",
"@angular/forms": "^12.2.8",
"@angular/platform-browser": "^12.2.8",
"@angular/platform-browser-dynamic": "^12.2.8",
"@angular/platform-server": "^12.2.8",
"@angular/router": "^12.2.8",
"@angular/animations": "^13.1.3",
"@angular/common": "^13.1.3",
"@angular/compiler": "^13.1.3",
"@angular/core": "^13.1.3",
"@angular/forms": "^13.1.3",
"@angular/platform-browser": "^13.1.3",
"@angular/platform-browser-dynamic": "^13.1.3",
"@angular/platform-server": "^13.1.3",
"@angular/router": "^13.1.3",
"@ionic/angular": "^5.3.1",
"@ionic/angular-server": "^5.3.1",
"@nguniversal/express-engine": "^12.1.1",
@@ -41,16 +41,16 @@
"zone.js": "^0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^12.2.8",
"@angular-eslint/builder": "12.5.0",
"@angular-eslint/eslint-plugin": "12.5.0",
"@angular-eslint/eslint-plugin-template": "12.5.0",
"@angular-eslint/schematics": "12.5.0",
"@angular-eslint/template-parser": "12.5.0",
"@angular/cli": "^12.2.8",
"@angular/compiler-cli": "^12.2.8",
"@angular/language-service": "^12.2.8",
"@nguniversal/builders": "^12.1.1",
"@angular-devkit/build-angular": "^13.1.4",
"@angular-eslint/builder": "13.0.1",
"@angular-eslint/eslint-plugin": "13.0.1",
"@angular-eslint/eslint-plugin-template": "13.0.1",
"@angular-eslint/schematics": "13.0.1",
"@angular-eslint/template-parser": "13.0.1",
"@angular/cli": "^13.1.4",
"@angular/compiler-cli": "^13.1.3",
"@angular/language-service": "^13.1.3",
"@nguniversal/builders": "^13.0.2",
"@types/express": "^4.17.7",
"@types/node": "^12.12.54",
"@typescript-eslint/eslint-plugin": "4.28.2",
@@ -60,7 +60,7 @@
"eslint": "^7.26.0",
"ts-loader": "^6.2.2",
"ts-node": "^8.3.0",
"typescript": "^4.3.5",
"typescript": "^4.5.5",
"wait-on": "^5.2.1",
"webpack": "^5.61.0",
"webpack-cli": "^3.3.12"

View File

@@ -65,10 +65,6 @@ import { AlertComponent } from './alert/alert.component';
ReactiveFormsModule,
IonicModule.forRoot({ keyboardHeight: 12345 }),
],
entryComponents: [
ModalExampleComponent,
NavComponent
],
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
],

View File

@@ -6,6 +6,7 @@
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<p>ngOnInit: <span id="ngOnInit">{{onInit}}</span></p>
<p>ionViewWillEnter: <span id="ionViewWillEnter">{{willEnter}}</span></p>
<p>ionViewDidEnter: <span id="ionViewDidEnter">{{didEnter}}</span></p>
@@ -14,11 +15,15 @@
<p>Change Detections: <span id="counter">{{counter()}}</span></p>
<p>
<ion-button routerLink="/router-link-page" expand="block" color="dark" id="routerLink">ion-button[routerLink]</ion-button>
<ion-button routerLink="/router-link-page" routerDirection="root" expand="block" color="dark" id="routerLink-root">ion-button[routerLink] (direction:root)</ion-button>
<ion-button routerLink="/router-link-page" routerDirection="back" expand="block" color="dark" id="routerLink-back">ion-button[routerLink] (direction:back)</ion-button>
<ion-button routerLink="/router-link-page" expand="block" color="dark" id="routerLink">ion-button[routerLink]
</ion-button>
<ion-button routerLink="/router-link-page" routerDirection="root" expand="block" color="dark" id="routerLink-root">
ion-button[routerLink] (direction:root)</ion-button>
<ion-button routerLink="/router-link-page" routerDirection="back" expand="block" color="dark" id="routerLink-back">
ion-button[routerLink] (direction:back)</ion-button>
</p>
<p><a [routerLink]="null">null router link</a></p>
<p><a routerLink="/router-link-page" id="a">a[routerLink]</a></p>
<p><a routerLink="/router-link-page" routerDirection="root" id="a-root">a[routerLink] (direction:root)</a></p>
<p><a routerLink="/router-link-page" routerDirection="back" id="a-back">a[routerLink] (direction:back)</a></p>
@@ -28,6 +33,7 @@
<p><button (click)="navigateRoot()" id="button-root">navigateForward</button></p>
<p><button (click)="navigateBack()" id="button-back">navigateBack</button></p>
<p><button id="queryParamsFragment" routerLink="/router-link-page2/MyPageID==" [queryParams]="{ token: 'A&=#Y' }" fragment="myDiv1">Query Params and Fragment</button></p>
<p><button id="queryParamsFragment" routerLink="/router-link-page2/MyPageID==" [queryParams]="{ token: 'A&=#Y' }"
fragment="myDiv1">Query Params and Fragment</button></p>
</ion-content>

View File

@@ -18,16 +18,6 @@
* BROWSER POLYFILLS
*/
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags

View File

@@ -3,6 +3,114 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [6.0.9](https://github.com/ionic-team/ionic/compare/v6.0.8...v6.0.9) (2022-02-23)
### Bug Fixes
* **datetime:** improve datetime sizing in modals ([#24762](https://github.com/ionic-team/ionic/issues/24762)) ([b0ac7de](https://github.com/ionic-team/ionic/commit/b0ac7de168c353ba4899cb74a2b38e25fd0cc0f1)), closes [#23992](https://github.com/ionic-team/ionic/issues/23992)
* **datetime:** month and year column order is now locale aware ([#24802](https://github.com/ionic-team/ionic/issues/24802)) ([16647b2](https://github.com/ionic-team/ionic/commit/16647b2c7290389755a4093145788f281c84b7e2)), closes [#24548](https://github.com/ionic-team/ionic/issues/24548)
* **datetime:** month picker no longer gives duplicate months on ios 14 and older ([#24792](https://github.com/ionic-team/ionic/issues/24792)) ([b6d7e1c](https://github.com/ionic-team/ionic/commit/b6d7e1c75740a61dcd02c21692e4d4632fb8df5c)), closes [#24663](https://github.com/ionic-team/ionic/issues/24663)
* **img:** draggable attribute is now inherited to inner img element ([#24781](https://github.com/ionic-team/ionic/issues/24781)) ([19ac238](https://github.com/ionic-team/ionic/commit/19ac2389eb0843173f51a12de41ac808cd8f0569)), closes [#21325](https://github.com/ionic-team/ionic/issues/21325)
* **modal:** backdropBreakpoint allows interactivity behind sheet ([#24798](https://github.com/ionic-team/ionic/issues/24798)) ([fca3f56](https://github.com/ionic-team/ionic/commit/fca3f56ca4568e63fd493debda088263caa86c64)), closes [#24797](https://github.com/ionic-team/ionic/issues/24797)
* **popover:** default alignment to 'center' for ios mode ([#24815](https://github.com/ionic-team/ionic/issues/24815)) ([243f673](https://github.com/ionic-team/ionic/commit/243f67362f25699bdb373be6b72cb9c14dc95a32))
* **react, vue:** scroll is no longer interrupted on ios ([#24791](https://github.com/ionic-team/ionic/issues/24791)) ([99c91ef](https://github.com/ionic-team/ionic/commit/99c91eff8764c9a1630adedab6a9765dd9754f7d)), closes [#24435](https://github.com/ionic-team/ionic/issues/24435)
* **select:** interface components now show correctly ([#24810](https://github.com/ionic-team/ionic/issues/24810)) ([2fc2de5](https://github.com/ionic-team/ionic/commit/2fc2de51771f4a5c3f20c6071284096e5bf31ec8)), closes [#24807](https://github.com/ionic-team/ionic/issues/24807)
* **toast:** toast is now correctly excluded from focus trapping ([#24816](https://github.com/ionic-team/ionic/issues/24816)) ([8246112](https://github.com/ionic-team/ionic/commit/8246112ca12f90edb98629ab82e27a792a1fafad)), closes [#24733](https://github.com/ionic-team/ionic/issues/24733)
## [6.0.8](https://github.com/ionic-team/ionic/compare/v6.0.7...v6.0.8) (2022-02-15)
### Bug Fixes
* **back-button, breadcrumb, item:** flip chevron icons on RTL ([#24705](https://github.com/ionic-team/ionic/issues/24705)) ([a093544](https://github.com/ionic-team/ionic/commit/a093544fdfc438ed03024285b2a35c5f645ea011))
* **datetime:** navigate to month within min range ([#24759](https://github.com/ionic-team/ionic/issues/24759)) ([7b3838c](https://github.com/ionic-team/ionic/commit/7b3838cc670de7845bb5937d204e86cdba93b6e6)), closes [#24757](https://github.com/ionic-team/ionic/issues/24757)
* **input:** only set native input value if different ([#24758](https://github.com/ionic-team/ionic/issues/24758)) ([fd031aa](https://github.com/ionic-team/ionic/commit/fd031aa1c3f05b7bfa3e0a0ee2a4793e29e22df5)), closes [#24753](https://github.com/ionic-team/ionic/issues/24753)
* **router-outlet:** getRouteId() returns the params set in setRouteId(). ([#24656](https://github.com/ionic-team/ionic/issues/24656)) ([be2205e](https://github.com/ionic-team/ionic/commit/be2205e5a2b2f8778bd1f7b4ea5cae0bf96f9ef3)), closes [#24652](https://github.com/ionic-team/ionic/issues/24652)
* **router-outlet:** navigating to same route with different params now activates component ([#24760](https://github.com/ionic-team/ionic/issues/24760)) ([abc36ae](https://github.com/ionic-team/ionic/commit/abc36ae80b060a659f7557ad90efe98b78f5ead9)), closes [#24653](https://github.com/ionic-team/ionic/issues/24653)
## [6.0.7](https://github.com/ionic-team/ionic/compare/v6.0.6...v6.0.7) (2022-02-09)
### Bug Fixes
* **angular:** inline modals now add .ion-page class correctly ([#24751](https://github.com/ionic-team/ionic/issues/24751)) ([ef46eaf](https://github.com/ionic-team/ionic/commit/ef46eafc9476a85ea3369e542f528d01d3cca0a8)), closes [#24750](https://github.com/ionic-team/ionic/issues/24750)
## [6.0.6](https://github.com/ionic-team/ionic/compare/v6.0.5...v6.0.6) (2022-02-09)
### Bug Fixes
* **action-sheet:** background includes safe area margin ([#24700](https://github.com/ionic-team/ionic/issues/24700)) ([8c22646](https://github.com/ionic-team/ionic/commit/8c22646d66e2077fc88aaacf350330097733bb9b)), closes [#24699](https://github.com/ionic-team/ionic/issues/24699)
* **angular, react, vue:** overlays no longer throw errors when used inside tests ([#24681](https://github.com/ionic-team/ionic/issues/24681)) ([897ae4a](https://github.com/ionic-team/ionic/commit/897ae4a4546ac0dd811125d5513ef23d133a1589)), closes [#24549](https://github.com/ionic-team/ionic/issues/24549) [#24590](https://github.com/ionic-team/ionic/issues/24590)
* **datetime:** disable intersection observer during month update ([#24713](https://github.com/ionic-team/ionic/issues/24713)) ([aab4d30](https://github.com/ionic-team/ionic/commit/aab4d306f80851bfd8a02a6361e26d60faeaadb4)), closes [#24712](https://github.com/ionic-team/ionic/issues/24712)
* **datetime:** minutes only filtered when max hour matches current hour ([#24710](https://github.com/ionic-team/ionic/issues/24710)) ([231d6df](https://github.com/ionic-team/ionic/commit/231d6df622c1f5dd9ecdff6fed8f61a4bff332df)), closes [#24702](https://github.com/ionic-team/ionic/issues/24702)
* **input:** cursor position does not jump to end ([#24736](https://github.com/ionic-team/ionic/issues/24736)) ([4ff9524](https://github.com/ionic-team/ionic/commit/4ff9524e1057aa487069b5982c5f1ecdf51d982b)), closes [#24727](https://github.com/ionic-team/ionic/issues/24727)
* **input:** IME composition mode ([#24735](https://github.com/ionic-team/ionic/issues/24735)) ([c6381ce](https://github.com/ionic-team/ionic/commit/c6381ce4f90707774d1c8662bba874f7b306bd1c)), closes [#24669](https://github.com/ionic-team/ionic/issues/24669)
* **modal, popover:** quickly opening modal/popover no longer presents duplicates ([#24697](https://github.com/ionic-team/ionic/issues/24697)) ([928c5fb](https://github.com/ionic-team/ionic/commit/928c5fbfcbf3ef1b2c3074464fc20dcf1fe143ae))
* **modal:** inline modals inherit ion-page styling ([#24723](https://github.com/ionic-team/ionic/issues/24723)) ([596aad4](https://github.com/ionic-team/ionic/commit/596aad435b5102307da89dd626ca4682b78db452)), closes [#24706](https://github.com/ionic-team/ionic/issues/24706)
* **popover:** use alignment with popover options ([#24711](https://github.com/ionic-team/ionic/issues/24711)) ([f5c5c3c](https://github.com/ionic-team/ionic/commit/f5c5c3cffa4f34046b0e9471a9f193db3772180e)), closes [#24709](https://github.com/ionic-team/ionic/issues/24709)
* **router:** router push with relative path ([#24719](https://github.com/ionic-team/ionic/issues/24719)) ([d40c0c3](https://github.com/ionic-team/ionic/commit/d40c0c3a0993eaefbe5107e98958c6b0699a62c2)), closes [#24718](https://github.com/ionic-team/ionic/issues/24718)
* **select:** value is selected when given array ([#24687](https://github.com/ionic-team/ionic/issues/24687)) ([6ee7d15](https://github.com/ionic-team/ionic/commit/6ee7d159ecfff3382fadb524c5c430172d40c267)), closes [#24742](https://github.com/ionic-team/ionic/issues/24742)
## [6.0.5](https://github.com/ionic-team/ionic/compare/v6.0.4...v6.0.5) (2022-02-02)
### Bug Fixes
* **datetime:** prevent navigating to disabled months ([#24421](https://github.com/ionic-team/ionic/issues/24421)) ([b40fc46](https://github.com/ionic-team/ionic/commit/b40fc4632efcdc01f1062d9bcec826afff5cf4ea)), closes [#24208](https://github.com/ionic-team/ionic/issues/24208) [#24482](https://github.com/ionic-team/ionic/issues/24482)
* **input:** min/max compatibility with react-hook-form ([#24657](https://github.com/ionic-team/ionic/issues/24657)) ([1f91883](https://github.com/ionic-team/ionic/commit/1f918835f437a59f7a70fc59d9305647aa9c298d)), closes [#24489](https://github.com/ionic-team/ionic/issues/24489)
### Performance Improvements
* **various:** don't use lazy-loaded icon names in components ([#24671](https://github.com/ionic-team/ionic/issues/24671)) ([484de50](https://github.com/ionic-team/ionic/commit/484de5074de212dffb4bf4f73bade7acec103fe8))
## [6.0.4](https://github.com/ionic-team/ionic/compare/v6.0.3...v6.0.4) (2022-01-26)
### Bug Fixes
* **accordion:** items inside of the content now correct display borders ([#24618](https://github.com/ionic-team/ionic/issues/24618)) ([d3311df](https://github.com/ionic-team/ionic/commit/d3311df96765948d0a395e4ba99fb9117b44adcb)), closes [#24613](https://github.com/ionic-team/ionic/issues/24613)
* **col:** col no longer errors when running in non-browser environment ([#24603](https://github.com/ionic-team/ionic/issues/24603)) ([af0135c](https://github.com/ionic-team/ionic/commit/af0135ce7dbe737b2df46094fd3dc8a41bdb60ae)), closes [#24446](https://github.com/ionic-team/ionic/issues/24446)
* **datetime:** datetime locale with h23 will respect max time range ([#24610](https://github.com/ionic-team/ionic/issues/24610)) ([5925e76](https://github.com/ionic-team/ionic/commit/5925e7608ef04f8877e4dd8a80b2c8bdc1cfd4bd)), closes [#24588](https://github.com/ionic-team/ionic/issues/24588)
* **datetime:** timepicker popover will position relative to click target ([#24616](https://github.com/ionic-team/ionic/issues/24616)) ([378c632](https://github.com/ionic-team/ionic/commit/378c63264366964e6ea11e1a2ff8791a40f182d4)), closes [#24531](https://github.com/ionic-team/ionic/issues/24531) [#24415](https://github.com/ionic-team/ionic/issues/24415)
* **input:** ion-input accepts any string value ([#24606](https://github.com/ionic-team/ionic/issues/24606)) ([43c5977](https://github.com/ionic-team/ionic/commit/43c5977d48cb12331c1d3107eb73f29b92c5e049)), closes [#19884](https://github.com/ionic-team/ionic/issues/19884)
* **item:** label text aligns with input text ([#24620](https://github.com/ionic-team/ionic/issues/24620)) ([94d033c](https://github.com/ionic-team/ionic/commit/94d033c4216ae9978aed6346c1c0ea2aec4d375b)), closes [#24404](https://github.com/ionic-team/ionic/issues/24404)
* **item:** match material design character counter ([#24335](https://github.com/ionic-team/ionic/issues/24335)) ([54db1a1](https://github.com/ionic-team/ionic/commit/54db1a1e7c71c78e843370848fc768582768333e)), closes [#24334](https://github.com/ionic-team/ionic/issues/24334)
* **menu:** focus trapping with menu and overlays no longer results in errors ([#24611](https://github.com/ionic-team/ionic/issues/24611)) ([632dafc](https://github.com/ionic-team/ionic/commit/632dafcd57de5086deebdc7d586b01710aa1a3ce)), closes [#24361](https://github.com/ionic-team/ionic/issues/24361) [#24481](https://github.com/ionic-team/ionic/issues/24481)
* **modal:** native keyboard will dismiss when bottom sheet is dragged ([#24642](https://github.com/ionic-team/ionic/issues/24642)) ([525f01f](https://github.com/ionic-team/ionic/commit/525f01f086ebf95ab62af0162b876a25f50a3d4b)), closes [#23878](https://github.com/ionic-team/ionic/issues/23878)
* **range:** setting dir on ion-range to enable rtl mode now supported ([#24597](https://github.com/ionic-team/ionic/issues/24597)) ([5dba4e5](https://github.com/ionic-team/ionic/commit/5dba4e5ce0a07f69a08f2b427e8010b311910f88)), closes [#20201](https://github.com/ionic-team/ionic/issues/20201)
* **searchbar:** setting dir on ion-searchbar to enable rtl mode now supported ([#24602](https://github.com/ionic-team/ionic/issues/24602)) ([35e5235](https://github.com/ionic-team/ionic/commit/35e523564561c0f3323efa761c4654016b97ef69))
* **segment:** setting dir on ion-segment to enable rtl mode now supported ([#24601](https://github.com/ionic-team/ionic/issues/24601)) ([2940e73](https://github.com/ionic-team/ionic/commit/2940e73a4504247eecb0de6c433104f529530850)), closes [#23978](https://github.com/ionic-team/ionic/issues/23978)
* **spinner:** ensure transform doesn't overwrite circular animation ([#24643](https://github.com/ionic-team/ionic/issues/24643)) ([88ce010](https://github.com/ionic-team/ionic/commit/88ce010418eaca38786b51720c696860b417ab6d))
* **toast:** allow input focus while toast is visible ([#24572](https://github.com/ionic-team/ionic/issues/24572)) ([29f1140](https://github.com/ionic-team/ionic/commit/29f1140384ae7e1402b09c3760e168cf79833619)), closes [#24571](https://github.com/ionic-team/ionic/issues/24571)
* **toggle:** setting dir on ion-toggle to enable rtl mode now supported ([#24600](https://github.com/ionic-team/ionic/issues/24600)) ([353dbc0](https://github.com/ionic-team/ionic/commit/353dbc0537ef3b46b9ba13a365ebc5da269de4c7))
## [6.0.3](https://github.com/ionic-team/ionic/compare/v6.0.2...v6.0.3) (2022-01-19)

View File

@@ -44,19 +44,55 @@ The `@ionic/core` package can be used in simple HTML, or by vanilla JavaScript w
In addition to the default, self lazy-loading components built by Stencil, this package also comes with each component exported as a stand-alone custom element within `@ionic/core/components`. Each component extends `HTMLElement`, and does not lazy-load itself. Instead, this package is useful for projects already using a bundler such as Webpack or Rollup. While all components are available to be imported, the custom elements build also ensures bundlers only import what's used, and tree-shakes any unused components.
Below is an example of importing `ion-toggle`, and initializing Ionic so it's able to correctly load the "mode", such as Material Design or iOS. Additionally, the `initialize({...})` function can receive the Ionic config.
Below is an example of importing `ion-badge`, and initializing Ionic so it is able to correctly load the "mode", such as Material Design or iOS. Additionally, the `initialize({...})` function can receive the Ionic config.
```typescript
import { IonBadge } from "@ionic/core/components/ion-badge";
import { defineCustomElement } from "@ionic/core/components/ion-badge.js";
import { initialize } from "@ionic/core/components";
// Initializes the Ionic config and `mode` behavior
initialize();
customElements.define("ion-badge", IonBadge);
// Defines the `ion-badge` web component
defineCustomElement();
```
Notice how `IonBadge` is imported from `@ionic/core/components/ion-badge` rather than just `@ionic/core/components`. Additionally, the `initialize` function is imported from `@ionic/core/components` rather than `@ionic/core`. All of this helps to ensure bundlers do not pull in more code than is needed.
Notice how we import from `@ionic/core/components` as opposed to `@ionic/core`. This helps bundlers pull in only the code that is needed.
The `defineCustomElement` function will automatically define the component as well as any child components that may be required.
For example, if you wanted to use `ion-modal`, you would do the following:
```typescript
import { defineCustomElement } from "@ionic/core/components/ion-modal.js";
import { initialize } from "@ionic/core/components";
// Initializes the Ionic config and `mode` behavior
initialize();
// Defines the `ion-modal` and child `ion-backdrop` web components.
defineCustomElement();
```
The `defineCustomElement` function will define `ion-modal`, but it will also define `ion-backdrop`, which is a component that `ion-modal` uses internally.
### Using Overlay Controllers
When using an overlay controller, developers will need to define the overlay component before it can be used. Below is an example of using `modalController`:
```typescript
import { defineCustomElement } from '@ionic/core/components/ion-modal.js';
import { initialize, modalController } from '@ionic/core/components';
initialize();
defineCustomElement();
const showModal = async () => {
const modal = await modalController.create({ ... });
...
}
```
## How to contribute

View File

@@ -3,7 +3,7 @@ ion-accordion,shadow
ion-accordion,prop,disabled,boolean,false,false,false
ion-accordion,prop,mode,"ios" | "md",undefined,false,false
ion-accordion,prop,readonly,boolean,false,false,false
ion-accordion,prop,toggleIcon,string,'chevron-down',false,false
ion-accordion,prop,toggleIcon,string,chevronDown,false,false
ion-accordion,prop,toggleIconSlot,"end" | "start",'end',false,false
ion-accordion,prop,value,string,`ion-accordion-${accordionIds++}`,false,false
ion-accordion,part,content
@@ -416,7 +416,7 @@ ion-fab,method,close,close() => Promise<void>
ion-fab-button,shadow
ion-fab-button,prop,activated,boolean,false,false,false
ion-fab-button,prop,closeIcon,string,'close',false,false
ion-fab-button,prop,closeIcon,string,close,false,false
ion-fab-button,prop,color,string | undefined,undefined,false,true
ion-fab-button,prop,disabled,boolean,false,false,false
ion-fab-button,prop,download,string | undefined,undefined,false,false
@@ -519,9 +519,9 @@ ion-input,prop,debounce,number,0,false,false
ion-input,prop,disabled,boolean,false,false,false
ion-input,prop,enterkeyhint,"done" | "enter" | "go" | "next" | "previous" | "search" | "send" | undefined,undefined,false,false
ion-input,prop,inputmode,"decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined,undefined,false,false
ion-input,prop,max,string | undefined,undefined,false,false
ion-input,prop,max,number | string | undefined,undefined,false,false
ion-input,prop,maxlength,number | undefined,undefined,false,false
ion-input,prop,min,string | undefined,undefined,false,false
ion-input,prop,min,number | string | undefined,undefined,false,false
ion-input,prop,minlength,number | undefined,undefined,false,false
ion-input,prop,mode,"ios" | "md",undefined,false,false
ion-input,prop,multiple,boolean | undefined,undefined,false,false
@@ -557,7 +557,7 @@ ion-item,prop,button,boolean,false,false,false
ion-item,prop,color,string | undefined,undefined,false,true
ion-item,prop,counter,boolean,false,false,false
ion-item,prop,detail,boolean | undefined,undefined,false,false
ion-item,prop,detailIcon,string,'chevron-forward',false,false
ion-item,prop,detailIcon,string,chevronForward,false,false
ion-item,prop,disabled,boolean,false,false,false
ion-item,prop,download,string | undefined,undefined,false,false
ion-item,prop,fill,"outline" | "solid" | undefined,undefined,false,false
@@ -877,7 +877,7 @@ ion-picker,css-prop,--min-width
ion-picker,css-prop,--width
ion-popover,shadow
ion-popover,prop,alignment,"center" | "end" | "start",'start',false,false
ion-popover,prop,alignment,"center" | "end" | "start" | undefined,undefined,false,false
ion-popover,prop,animated,boolean,true,false,false
ion-popover,prop,arrow,boolean,true,false,false
ion-popover,prop,backdropDismiss,boolean,true,false,false
@@ -901,7 +901,7 @@ ion-popover,prop,triggerAction,"click" | "context-menu" | "hover",'click',false,
ion-popover,method,dismiss,dismiss(data?: any, role?: string | undefined, dismissParentPopover?: boolean) => Promise<boolean>
ion-popover,method,onDidDismiss,onDidDismiss<T = any>() => Promise<OverlayEventDetail<T>>
ion-popover,method,onWillDismiss,onWillDismiss<T = any>() => Promise<OverlayEventDetail<T>>
ion-popover,method,present,present(event?: MouseEvent | TouchEvent | PointerEvent | undefined) => Promise<void>
ion-popover,method,present,present(event?: MouseEvent | TouchEvent | PointerEvent | CustomEvent<any> | undefined) => Promise<void>
ion-popover,event,didDismiss,OverlayEventDetail<any>,true
ion-popover,event,didPresent,void,true
ion-popover,event,ionPopoverDidDismiss,OverlayEventDetail<any>,true
@@ -1045,7 +1045,7 @@ ion-router,none
ion-router,prop,root,string,'/',false,false
ion-router,prop,useHash,boolean,true,false,false
ion-router,method,back,back() => Promise<void>
ion-router,method,push,push(url: string, direction?: RouterDirection, animation?: AnimationBuilder | undefined) => Promise<boolean>
ion-router,method,push,push(path: string, direction?: RouterDirection, animation?: AnimationBuilder | undefined) => Promise<boolean>
ion-router,event,ionRouteDidChange,RouterEventDetail,true
ion-router,event,ionRouteWillChange,RouterEventDetail,true
@@ -1070,7 +1070,7 @@ ion-searchbar,scoped
ion-searchbar,prop,animated,boolean,false,false,false
ion-searchbar,prop,autocomplete,"off" | "on" | "name" | "honorific-prefix" | "given-name" | "additional-name" | "family-name" | "honorific-suffix" | "nickname" | "email" | "username" | "new-password" | "current-password" | "one-time-code" | "organization-title" | "organization" | "street-address" | "address-line1" | "address-line2" | "address-line3" | "address-level4" | "address-level3" | "address-level2" | "address-level1" | "country" | "country-name" | "postal-code" | "cc-name" | "cc-given-name" | "cc-additional-name" | "cc-family-name" | "cc-number" | "cc-exp" | "cc-exp-month" | "cc-exp-year" | "cc-csc" | "cc-type" | "transaction-currency" | "transaction-amount" | "language" | "bday" | "bday-day" | "bday-month" | "bday-year" | "sex" | "tel" | "tel-country-code" | "tel-national" | "tel-area-code" | "tel-local" | "tel-extension" | "impp" | "url" | "photo",'off',false,false
ion-searchbar,prop,autocorrect,"off" | "on",'off',false,false
ion-searchbar,prop,cancelButtonIcon,string,config.get('backButtonIcon', 'arrow-back-sharp') as string,false,false
ion-searchbar,prop,cancelButtonIcon,string,config.get('backButtonIcon', arrowBackSharp) as string,false,false
ion-searchbar,prop,cancelButtonText,string,'Cancel',false,false
ion-searchbar,prop,clearIcon,string | undefined,undefined,false,false
ion-searchbar,prop,color,string | undefined,undefined,false,true

18
core/package-lock.json generated
View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/core",
"version": "6.0.3",
"version": "6.0.9",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/core",
"version": "6.0.1",
"version": "6.0.5",
"license": "MIT",
"dependencies": {
"@stencil/core": "~2.12.0",
"@stencil/core": "~2.13.0",
"ionicons": "^6.0.0",
"tslib": "^2.1.0"
},
@@ -1366,9 +1366,9 @@
}
},
"node_modules/@stencil/core": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.12.0.tgz",
"integrity": "sha512-hQlQKh5CUJe8g3L5avLLsfgVu95HMS2LToTtS7gpvvP0eKes1VvAC56uhI+vH4u44GZl9ck/g1rJBVRmMWu0LA==",
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.13.0.tgz",
"integrity": "sha512-EEKHOHgYpg3/iFUKMXTZJjUayRul7sXDwNw0OGgkEOe4t7JWiibDkzUHuruvpbqEydX+z1+ez5K2bMMY76c2wA==",
"bin": {
"stencil": "bin/stencil"
},
@@ -15055,9 +15055,9 @@
"dev": true
},
"@stencil/core": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.12.0.tgz",
"integrity": "sha512-hQlQKh5CUJe8g3L5avLLsfgVu95HMS2LToTtS7gpvvP0eKes1VvAC56uhI+vH4u44GZl9ck/g1rJBVRmMWu0LA=="
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.13.0.tgz",
"integrity": "sha512-EEKHOHgYpg3/iFUKMXTZJjUayRul7sXDwNw0OGgkEOe4t7JWiibDkzUHuruvpbqEydX+z1+ez5K2bMMY76c2wA=="
},
"@stencil/react-output-target": {
"version": "0.2.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "6.0.3",
"version": "6.0.9",
"description": "Base components for Ionic",
"keywords": [
"ionic",
@@ -31,7 +31,7 @@
"loader/"
],
"dependencies": {
"@stencil/core": "~2.12.0",
"@stencil/core": "~2.13.0",
"ionicons": "^6.0.0",
"tslib": "^2.1.0"
},

View File

@@ -1051,7 +1051,7 @@ export namespace Components {
/**
* The maximum value, which must not be less than its minimum (min attribute) value.
*/
"max"?: string;
"max"?: string | number;
/**
* If the value of the type attribute is `text`, `email`, `search`, `password`, `tel`, or `url`, this attribute specifies the maximum number of characters that the user can enter.
*/
@@ -1059,7 +1059,7 @@ export namespace Components {
/**
* The minimum value, which must not be greater than its maximum (max attribute) value.
*/
"min"?: string;
"min"?: string | number;
/**
* If the value of the type attribute is `text`, `email`, `search`, `password`, `tel`, or `url`, this attribute specifies the minimum number of characters that the user can enter.
*/
@@ -1616,6 +1616,9 @@ export namespace Components {
* @param view The view to get.
*/
"getPrevious": (view?: ViewController | undefined) => Promise<ViewController | undefined>;
/**
* Called by <ion-router> to retrieve the current component.
*/
"getRouteId": () => Promise<RouteID | undefined>;
/**
* Inserts a component into the navigation stack at the specified index. This is useful to add a component at any point in the navigation stack.
@@ -1692,6 +1695,14 @@ export namespace Components {
* @param done The transition complete function.
*/
"setRoot": <T extends NavComponent>(component: T, componentProps?: ComponentProps<T> | null | undefined, opts?: NavOptions | null | undefined, done?: TransitionDoneFn | undefined) => Promise<boolean>;
/**
* Called by the router to update the view.
* @param id The component tag.
* @param params The component params.
* @param direction A direction hint.
* @param animation an AnimationBuilder.
* @return the status.
*/
"setRouteId": (id: string, params: ComponentProps | undefined, direction: RouterDirection, animation?: AnimationBuilder | undefined) => Promise<RouteWrite>;
/**
* If the nav component should allow for swipe-to-go-back.
@@ -1823,6 +1834,7 @@ export namespace Components {
* If `true`, tapping the picker will reveal a number input keyboard that lets the user type in values for each picker column. This is useful when working with time pickers.
*/
"numericInput": boolean;
"scrollActiveItemIntoView": () => Promise<void>;
/**
* The selected option in the picker.
*/
@@ -1836,9 +1848,9 @@ export namespace Components {
}
interface IonPopover {
/**
* Describes how to align the popover content with the `reference` point.
* Describes how to align the popover content with the `reference` point. Defaults to `'center'` for `ios` mode, and `'start'` for `md` mode.
*/
"alignment": PositionAlign;
"alignment"?: PositionAlign;
/**
* If `true`, the popover will animate.
*/
@@ -1918,7 +1930,7 @@ export namespace Components {
/**
* Present the popover overlay after it has been created. Developers can pass a mouse, touch, or pointer event to position the popover relative to where that event was dispatched.
*/
"present": (event?: MouseEvent | TouchEvent | PointerEvent | undefined) => Promise<void>;
"present": (event?: MouseEvent | TouchEvent | PointerEvent | CustomEvent<any> | undefined) => Promise<void>;
/**
* When opening a popover from a trigger, we should not be modifying the `event` prop from inside the component. Additionally, when pressing the "Right" arrow key, we need to shift focus to the first descendant in the newly presented popover.
*/
@@ -2196,11 +2208,11 @@ export namespace Components {
"navChanged": (direction: RouterDirection) => Promise<boolean>;
"printDebug": () => Promise<void>;
/**
* Navigate to the specified URL.
* @param url The url to navigate to.
* Navigate to the specified path.
* @param path The path to navigate to.
* @param direction The direction of the animation. Defaults to `"forward"`.
*/
"push": (url: string, direction?: RouterDirection, animation?: AnimationBuilder | undefined) => Promise<boolean>;
"push": (path: string, direction?: RouterDirection, animation?: AnimationBuilder | undefined) => Promise<boolean>;
/**
* The root path to use when matching URLs. By default, this is set to "/", but you can specify an alternate prefix for all URL paths.
*/
@@ -2242,7 +2254,7 @@ export namespace Components {
*/
"animated": boolean;
/**
* By default `ion-nav` animates transition between pages based in the mode (ios or material design). However, this property allows to create custom transition using `AnimateBuilder` functions.
* This property allows to create custom transition using AnimateBuilder functions.
*/
"animation"?: AnimationBuilder;
"commit": (enteringEl: HTMLElement, leavingEl: HTMLElement | undefined, opts?: RouterOutletOptions | undefined) => Promise<boolean>;
@@ -2271,7 +2283,7 @@ export namespace Components {
*/
"autocorrect": 'on' | 'off';
/**
* Set the cancel button icon. Only applies to `md` mode. Defaults to `"arrow-back-sharp"`.
* Set the cancel button icon. Only applies to `md` mode. Defaults to `arrow-back-sharp`.
*/
"cancelButtonIcon": string;
/**
@@ -2279,7 +2291,7 @@ export namespace Components {
*/
"cancelButtonText": string;
/**
* Set the clear icon. Defaults to `"close-circle"` for `ios` and `"close-sharp"` for `md`.
* Set the clear icon. Defaults to `close-circle` for `ios` and `close-sharp` for `md`.
*/
"clearIcon"?: string;
/**
@@ -2315,7 +2327,7 @@ export namespace Components {
*/
"placeholder": string;
/**
* The icon to use as the search icon. Defaults to `"search-outline"` in `ios` mode and `"search-sharp"` in `md` mode.
* The icon to use as the search icon. Defaults to `search-outline` in `ios` mode and `search-sharp` in `md` mode.
*/
"searchIcon"?: string;
/**
@@ -4752,7 +4764,7 @@ declare namespace LocalJSX {
/**
* The maximum value, which must not be less than its minimum (min attribute) value.
*/
"max"?: string;
"max"?: string | number;
/**
* If the value of the type attribute is `text`, `email`, `search`, `password`, `tel`, or `url`, this attribute specifies the maximum number of characters that the user can enter.
*/
@@ -4760,7 +4772,7 @@ declare namespace LocalJSX {
/**
* The minimum value, which must not be greater than its maximum (max attribute) value.
*/
"min"?: string;
"min"?: string | number;
/**
* If the value of the type attribute is `text`, `email`, `search`, `password`, `tel`, or `url`, this attribute specifies the minimum number of characters that the user can enter.
*/
@@ -5473,7 +5485,7 @@ declare namespace LocalJSX {
}
interface IonPopover {
/**
* Describes how to align the popover content with the `reference` point.
* Describes how to align the popover content with the `reference` point. Defaults to `'center'` for `ios` mode, and `'start'` for `md` mode.
*/
"alignment"?: PositionAlign;
/**
@@ -5913,7 +5925,7 @@ declare namespace LocalJSX {
*/
"animated"?: boolean;
/**
* By default `ion-nav` animates transition between pages based in the mode (ios or material design). However, this property allows to create custom transition using `AnimateBuilder` functions.
* This property allows to create custom transition using AnimateBuilder functions.
*/
"animation"?: AnimationBuilder;
"delegate"?: FrameworkDelegate;
@@ -5942,7 +5954,7 @@ declare namespace LocalJSX {
*/
"autocorrect"?: 'on' | 'off';
/**
* Set the cancel button icon. Only applies to `md` mode. Defaults to `"arrow-back-sharp"`.
* Set the cancel button icon. Only applies to `md` mode. Defaults to `arrow-back-sharp`.
*/
"cancelButtonIcon"?: string;
/**
@@ -5950,7 +5962,7 @@ declare namespace LocalJSX {
*/
"cancelButtonText"?: string;
/**
* Set the clear icon. Defaults to `"close-circle"` for `ios` and `"close-sharp"` for `md`.
* Set the clear icon. Defaults to `close-circle` for `ios` and `close-sharp` for `md`.
*/
"clearIcon"?: string;
/**
@@ -6010,7 +6022,7 @@ declare namespace LocalJSX {
*/
"placeholder"?: string;
/**
* The icon to use as the search icon. Defaults to `"search-outline"` in `ios` mode and `"search-sharp"` in `md` mode.
* The icon to use as the search icon. Defaults to `search-outline` in `ios` mode and `search-sharp` in `md` mode.
*/
"searchIcon"?: string;
/**

View File

@@ -1,4 +1,5 @@
import { Component, ComponentInterface, Element, Host, Prop, State, h } from '@stencil/core';
import { chevronDown } from 'ionicons/icons';
import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global';
@@ -71,7 +72,7 @@ export class Accordion implements ComponentInterface {
* rotated when the accordion is expanded
* or collapsed.
*/
@Prop() toggleIcon = 'chevron-down';
@Prop() toggleIcon = chevronDown;
/**
* The slot inside of `ion-item` to

View File

@@ -1626,7 +1626,7 @@ export const AccordionExample {
| `disabled` | `disabled` | If `true`, the accordion cannot be interacted with. | `boolean` | `false` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `readonly` | `readonly` | If `true`, the accordion cannot be interacted with, but does not alter the opacity. | `boolean` | `false` |
| `toggleIcon` | `toggle-icon` | The toggle icon to use. This icon will be rotated when the accordion is expanded or collapsed. | `string` | `'chevron-down'` |
| `toggleIcon` | `toggle-icon` | The toggle icon to use. This icon will be rotated when the accordion is expanded or collapsed. | `string` | `chevronDown` |
| `toggleIconSlot` | `toggle-icon-slot` | The slot inside of `ion-item` to place the toggle icon. Defaults to `'end'`. | `"end" \| "start"` | `'end'` |
| `value` | `value` | The value of the accordion. Defaults to an autogenerated value. | `string` | ``ion-accordion-${accordionIds++}`` |

View File

@@ -9,3 +9,12 @@ test('accordion: axe', async () => {
const results = await new AxePuppeteer(page).analyze();
expect(results.violations.length).toEqual(0);
});
test('accordion: standalone', async () => {
const page = await newE2EPage({
url: '/src/components/accordion/test/standalone?ionic:_testing=true'
});
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});

View File

@@ -85,6 +85,21 @@
</ion-accordion>
</ion-accordion-group>
</div>
<div class="grid-item">
<h2>Item In Content</h2>
<ion-accordion-group value="first">
<ion-accordion value="first">
<ion-item slot="header">
Accordion
</ion-item>
<div slot="content">
<ion-item lines="full">Some Item</ion-item>
</div>
</ion-accordion>
</ion-accordion-group>
</div>
</div>
</main>
</body>

View File

@@ -22,6 +22,13 @@
text-align: $action-sheet-ios-text-align;
}
// iOS Action Sheet Wrapper
// ---------------------------------------------------
.action-sheet-wrapper {
@include margin(var(--ion-safe-area-top, 0), auto, var(--ion-safe-area-bottom, 0), auto);
}
// iOS Action Sheet Container
// ---------------------------------------------------

View File

@@ -20,6 +20,14 @@
--color: #{$action-sheet-md-title-color};
}
// Material Design Action Sheet Wrapper
// -----------------------------------------
.action-sheet-wrapper {
@include margin(var(--ion-safe-area-top, 0), auto, 0, auto);
}
.action-sheet-title {
@include padding($action-sheet-md-title-padding-top, $action-sheet-md-title-padding-end, $action-sheet-md-title-padding-bottom, $action-sheet-md-title-padding-start);

View File

@@ -13,7 +13,7 @@ $action-sheet-md-background-color: $overlay-md-background-c
$action-sheet-md-padding-top: 0 !default;
/// @prop - Padding bottom of the action sheet
$action-sheet-md-padding-bottom: 0 !default;
$action-sheet-md-padding-bottom: var(--ion-safe-area-bottom) !default;
// Action Sheet Title

View File

@@ -67,7 +67,6 @@
.action-sheet-wrapper {
@include position(null, 0, 0, 0);
@include margin(var(--ion-safe-area-top, 0), auto, var(--ion-safe-area-bottom, 0), auto);
@include transform(translate3d(0, 100%, 0));
display: block;

View File

@@ -1,10 +1,11 @@
import { Component, ComponentInterface, Element, Host, Prop, h } from '@stencil/core';
import { arrowBackSharp, chevronBack } from 'ionicons/icons';
import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global';
import { AnimationBuilder, Color } from '../../interface';
import { ButtonInterface } from '../../utils/element-interface';
import { inheritAttributes } from '../../utils/helpers';
import { Attributes, inheritAttributes } from '../../utils/helpers';
import { createColorClasses, hostContext, openURL } from '../../utils/theme';
/**
@@ -23,7 +24,7 @@ import { createColorClasses, hostContext, openURL } from '../../utils/theme';
shadow: true
})
export class BackButton implements ComponentInterface, ButtonInterface {
private inheritedAttributes: { [k: string]: any } = {};
private inheritedAttributes: Attributes = {};
@Element() el!: HTMLElement;
@@ -83,11 +84,11 @@ export class BackButton implements ComponentInterface, ButtonInterface {
if (getIonMode(this) === 'ios') {
// default ios back button icon
return config.get('backButtonIcon', 'chevron-back');
return config.get('backButtonIcon', chevronBack);
}
// default md back button icon
return config.get('backButtonIcon', 'arrow-back-sharp');
return config.get('backButtonIcon', arrowBackSharp);
}
get backButtonText() {
@@ -120,7 +121,7 @@ export class BackButton implements ComponentInterface, ButtonInterface {
}
render() {
const { color, defaultHref, disabled, type, hasIconOnly, backButtonIcon, backButtonText, inheritedAttributes } = this;
const { color, defaultHref, disabled, type, hasIconOnly, backButtonIcon, backButtonText, icon, inheritedAttributes } = this;
const showBackButton = defaultHref !== undefined;
const mode = getIonMode(this);
const ariaLabel = inheritedAttributes['aria-label'] || backButtonText || 'back';
@@ -148,7 +149,7 @@ export class BackButton implements ComponentInterface, ButtonInterface {
aria-label={ariaLabel}
>
<span class="button-inner">
{backButtonIcon && <ion-icon part="icon" icon={backButtonIcon} aria-hidden="true" lazy={false}></ion-icon>}
{backButtonIcon && <ion-icon part="icon" icon={backButtonIcon} aria-hidden="true" lazy={false} flip-rtl={icon === undefined}></ion-icon>}
{backButtonText && <span part="text" aria-hidden="true" class="button-text">{backButtonText}</span>}
</span>
{mode === 'md' && <ion-ripple-effect type={this.rippleType}></ion-ripple-effect>}

View File

@@ -1,3 +1,5 @@
import { arrowBackSharp, chevronBack } from 'ionicons/icons';
import { newSpecPage } from '@stencil/core/testing';
import { BackButton } from "../back-button";
import { config } from "../../../global/config";
@@ -46,12 +48,12 @@ describe('back button', () => {
it('default icon for ios mode', async () => {
const bb = await newBackButton('ios');
expect(bb.backButtonIcon).toBe('chevron-back');
expect(bb.backButtonIcon).toBe(chevronBack);
});
it('default icon', async () => {
const bb = await newBackButton();
expect(bb.backButtonIcon).toBe('arrow-back-sharp');
expect(bb.backButtonIcon).toBe(arrowBackSharp);
});
});

View File

@@ -8,3 +8,12 @@ test('back-button: basic', async () => {
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});
test('back-button: basic-rtl', async () => {
const page = await newE2EPage({
url: '/src/components/back-button/test/basic?ionic:_testing=true&rtl=true'
});
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});

View File

@@ -3,7 +3,7 @@ import { chevronForwardOutline, ellipsisHorizontal } from 'ionicons/icons';
import { getIonMode } from '../../global/ionic-global';
import { AnimationBuilder, BreadcrumbCollapsedClickEventDetail, Color, RouterDirection } from '../../interface';
import { inheritAttributes } from '../../utils/helpers';
import { Attributes, inheritAttributes } from '../../utils/helpers';
import { createColorClasses, hostContext, openURL } from '../../utils/theme';
/**
@@ -22,7 +22,7 @@ import { createColorClasses, hostContext, openURL } from '../../utils/theme';
shadow: true
})
export class Breadcrumb implements ComponentInterface {
private inheritedAttributes: { [k: string]: any } = {};
private inheritedAttributes: Attributes = {};
private collapsedRef?: HTMLElement;
/** @internal */
@@ -212,7 +212,7 @@ export class Breadcrumb implements ComponentInterface {
<span class="breadcrumb-separator" part="separator">
<slot name="separator">
{ mode === 'ios'
? <ion-icon icon={chevronForwardOutline} lazy={false}></ion-icon>
? <ion-icon icon={chevronForwardOutline} lazy={false} flip-rtl></ion-icon>
: <span>/</span>
}
</slot>

View File

@@ -8,3 +8,12 @@ test('breadcrumbs: basic', async () => {
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});
test('breadcrumbs: basic-rtl', async () => {
const page = await newE2EPage({
url: '/src/components/breadcrumbs/test/basic?ionic:_testing=true&rtl=true'
});
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});

View File

@@ -3,7 +3,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop
import { getIonMode } from '../../global/ionic-global';
import { AnimationBuilder, Color, RouterDirection } from '../../interface';
import { AnchorInterface, ButtonInterface } from '../../utils/element-interface';
import { hasShadowDom, inheritAttributes } from '../../utils/helpers';
import { Attributes, hasShadowDom, inheritAttributes } from '../../utils/helpers';
import { createColorClasses, hostContext, openURL } from '../../utils/theme';
/**
@@ -28,7 +28,7 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
private inItem = false;
private inListHeader = false;
private inToolbar = false;
private inheritedAttributes: { [k: string]: any } = {};
private inheritedAttributes: Attributes = {};
@Element() el!: HTMLElement;

View File

@@ -3,8 +3,8 @@ import { Component, ComponentInterface, Host, Listen, Prop, forceUpdate, h } fro
import { getIonMode } from '../../global/ionic-global';
import { matchBreakpoint } from '../../utils/media';
const win = window as any;
const SUPPORTS_VARS = !!(win.CSS && win.CSS.supports && win.CSS.supports('--a: 0'));
const win = (typeof (window as any) !== 'undefined') ? window as any : undefined;
const SUPPORTS_VARS = win && !!(win.CSS && win.CSS.supports && win.CSS.supports('--a: 0'));
const BREAKPOINTS = ['', 'xs', 'sm', 'md', 'lg', 'xl'];
@Component({

View File

@@ -48,6 +48,35 @@
opacity: 1;
}
/**
* Changing the physical order of the
* picker columns in the DOM is added
* work, so we just use `order` instead.
*
* The picker automatically configures
* the text alignment, so when switching
* the order we need to manually switch
* the text alignment too.
*/
:host .datetime-year .order-month-first .month-column {
order: 1;
}
:host .datetime-year .order-month-first .year-column {
order: 2;
}
:host .datetime-year .order-year-first .month-column {
order: 2;
text-align: end;
}
:host .datetime-year .order-year-first .year-column {
order: 1;
text-align: start;
}
// Calendar
// -----------------------------------
@@ -241,6 +270,13 @@
width: 100%;
}
:host .calendar-body .calendar-month-disabled {
/**
* Disables swipe gesture snapping for scroll-snap containers
*/
scroll-snap-align: none;
}
/**
* Hide scrollbars on Chrome and Safari
*/

View File

@@ -32,7 +32,8 @@ import {
getMonthAndYear
} from './utils/format';
import {
is24Hour
is24Hour,
isMonthFirstLocale
} from './utils/helpers';
import {
calculateHourFromAMPM,
@@ -56,7 +57,10 @@ import {
} from './utils/parse';
import {
getCalendarDayState,
isDayDisabled
isDayDisabled,
isMonthDisabled,
isNextMonthDisabled,
isPrevMonthDisabled
} from './utils/state';
/**
@@ -714,6 +718,15 @@ export class Datetime implements ComponentInterface {
return;
}
const { month, year, day } = refMonthFn(this.workingParts);
if (isMonthDisabled({ month, year, day: null }, {
minParts: { ...this.minParts, day: null },
maxParts: { ...this.maxParts, day: null }
})) {
return;
}
/**
* On iOS, we need to set pointer-events: none
* when the user is almost done with the gesture
@@ -724,7 +737,8 @@ export class Datetime implements ComponentInterface {
*/
if (mode === 'ios') {
const ratio = ev.intersectionRatio;
const shouldDisable = Math.abs(ratio - 0.7) <= 0.1;
// `maxTouchPoints` will be 1 in device preview, but > 1 on device
const shouldDisable = Math.abs(ratio - 0.7) <= 0.1 && navigator.maxTouchPoints > 1;
if (shouldDisable) {
calendarBodyRef.style.setProperty('pointer-events', 'none');
@@ -757,19 +771,29 @@ export class Datetime implements ComponentInterface {
* if we did not do this.
*/
writeTask(() => {
const { month, year, day } = refMonthFn(this.workingParts);
this.setWorkingParts({
...this.workingParts,
month,
day: day!,
year
// Disconnect all active intersection observers
// to avoid a re-render causing a duplicate event.
if (this.destroyCalendarIO) {
this.destroyCalendarIO();
}
raf(() => {
this.setWorkingParts({
...this.workingParts,
month,
day: day!,
year
});
calendarBodyRef.scrollLeft = workingMonth.clientWidth * (isRTL(this.el) ? -1 : 1);
calendarBodyRef.style.removeProperty('overflow');
calendarBodyRef.style.removeProperty('pointer-events');
endIO?.observe(endMonth);
startIO?.observe(startMonth);
});
calendarBodyRef.scrollLeft = workingMonth.clientWidth * (isRTL(this.el) ? -1 : 1);
calendarBodyRef.style.removeProperty('overflow');
calendarBodyRef.style.removeProperty('pointer-events');
/**
* Now that state has been updated
* and the correct month is in view,
@@ -781,6 +805,18 @@ export class Datetime implements ComponentInterface {
});
}
const threshold = mode === 'ios' &&
// tslint:disable-next-line
typeof navigator !== 'undefined' &&
navigator.maxTouchPoints > 1 ?
[0.7, 1] : 1;
// Intersection observers cannot accurately detect the
// intersection with a threshold of 1, when the observed
// element width is a sub-pixel value (i.e. 334.05px).
// Setting a root margin to 1px solves the issue.
const rootMargin = '1px';
/**
* Listen on the first month to
* prepend a new month and on the last
@@ -799,15 +835,18 @@ export class Datetime implements ComponentInterface {
* it applies to active gestures which is not
* something WebKit does.
*/
endIO = new IntersectionObserver(ev => ioCallback('end', ev), {
threshold: mode === 'ios' ? [0.7, 1] : 1,
root: calendarBodyRef
threshold,
root: calendarBodyRef,
rootMargin
});
endIO.observe(endMonth);
startIO = new IntersectionObserver(ev => ioCallback('start', ev), {
threshold: mode === 'ios' ? [0.7, 1] : 1,
root: calendarBodyRef
threshold,
root: calendarBodyRef,
rootMargin
});
startIO.observe(startMonth);
@@ -963,9 +1002,9 @@ export class Datetime implements ComponentInterface {
}
componentWillLoad() {
this.processValue(this.value);
this.processMinParts();
this.processMaxParts();
this.processValue(this.value);
this.parsedHourValues = convertToArrayOfNumbers(this.hourValues);
this.parsedMinuteValues = convertToArrayOfNumbers(this.minuteValues);
this.parsedMonthValues = convertToArrayOfNumbers(this.monthValues);
@@ -1068,29 +1107,42 @@ export class Datetime implements ComponentInterface {
}
private renderYearView() {
const { presentation, workingParts } = this;
const { presentation, workingParts, locale } = this;
const calendarYears = getCalendarYears(this.todayParts, this.minParts, this.maxParts, this.parsedYearValues);
const showMonth = presentation !== 'year';
const showYear = presentation !== 'month';
const months = getPickerMonths(this.locale, workingParts, this.minParts, this.maxParts, this.parsedMonthValues);
const months = getPickerMonths(locale, workingParts, this.minParts, this.maxParts, this.parsedMonthValues);
const years = calendarYears.map(year => {
return {
text: `${year}`,
value: year
}
})
const showMonthFirst = isMonthFirstLocale(locale);
const columnOrder = showMonthFirst ? 'month-first' : 'year-first';
return (
<div class="datetime-year">
<div class="datetime-year-body">
<div class={{
'datetime-year-body': true,
[`order-${columnOrder}`]: true
}}>
<ion-picker-internal>
{
showMonth &&
<ion-picker-column-internal
class="month-column"
color={this.color}
items={months}
value={workingParts.month}
onIonChange={(ev: CustomEvent) => {
// Due to a Safari 14 issue we need to destroy
// the intersection observer before we update state
// and trigger a re-render.
if (this.destroyCalendarIO) {
this.destroyCalendarIO();
}
this.setWorkingParts({
...this.workingParts,
month: ev.detail.value
@@ -1103,6 +1155,10 @@ export class Datetime implements ComponentInterface {
});
}
// We can re-attach the intersection observer after
// the working parts have been updated.
this.initializeCalendarIOListeners();
ev.stopPropagation();
}}
></ion-picker-column-internal>
@@ -1110,10 +1166,18 @@ export class Datetime implements ComponentInterface {
{
showYear &&
<ion-picker-column-internal
class="year-column"
color={this.color}
items={years}
value={workingParts.year}
onIonChange={(ev: CustomEvent) => {
// Due to a Safari 14 issue we need to destroy
// the intersection observer before we update state
// and trigger a re-render.
if (this.destroyCalendarIO) {
this.destroyCalendarIO();
}
this.setWorkingParts({
...this.workingParts,
year: ev.detail.value
@@ -1126,6 +1190,10 @@ export class Datetime implements ComponentInterface {
});
}
// We can re-attach the intersection observer after
// the working parts have been updated.
this.initializeCalendarIOListeners();
ev.stopPropagation();
}}
></ion-picker-column-internal>
@@ -1139,6 +1207,10 @@ export class Datetime implements ComponentInterface {
private renderCalendarHeader(mode: Mode) {
const expandedIcon = mode === 'ios' ? chevronDown : caretUpSharp;
const collapsedIcon = mode === 'ios' ? chevronForward : caretDownSharp;
const prevMonthDisabled = isPrevMonthDisabled(this.workingParts, this.minParts, this.maxParts);
const nextMonthDisabled = isNextMonthDisabled(this.workingParts, this.maxParts);
return (
<div class="calendar-header">
<div class="calendar-action-buttons">
@@ -1152,10 +1224,14 @@ export class Datetime implements ComponentInterface {
<div class="calendar-next-prev">
<ion-buttons>
<ion-button onClick={() => this.prevMonth()}>
<ion-button
disabled={prevMonthDisabled}
onClick={() => this.prevMonth()}>
<ion-icon slot="icon-only" icon={chevronBack} lazy={false} flipRtl></ion-icon>
</ion-button>
<ion-button onClick={() => this.nextMonth()}>
<ion-button
disabled={nextMonthDisabled}
onClick={() => this.nextMonth()}>
<ion-icon slot="icon-only" icon={chevronForward} lazy={false} flipRtl></ion-icon>
</ion-button>
</ion-buttons>
@@ -1173,9 +1249,29 @@ export class Datetime implements ComponentInterface {
private renderMonth(month: number, year: number) {
const yearAllowed = this.parsedYearValues === undefined || this.parsedYearValues.includes(year);
const monthAllowed = this.parsedMonthValues === undefined || this.parsedMonthValues.includes(month);
const isMonthDisabled = !yearAllowed || !monthAllowed;
const isCalMonthDisabled = !yearAllowed || !monthAllowed;
const swipeDisabled = isMonthDisabled({
month,
year,
day: null
}, {
// The day is not used when checking if a month is disabled.
// Users should be able to access the min or max month, even if the
// min/max date is out of bounds (e.g. min is set to Feb 15, Feb should not be disabled).
minParts: { ...this.minParts, day: null },
maxParts: { ...this.maxParts, day: null }
});
// The working month should never have swipe disabled.
// Otherwise the CSS scroll snap will not work and the user
// can free-scroll the calendar.
const isWorkingMonth = this.workingParts.month === month && this.workingParts.year === year;
return (
<div class="calendar-month">
<div class={{
'calendar-month': true,
// Prevents scroll snap swipe gestures for months outside of the min/max bounds
'calendar-month-disabled': !isWorkingMonth && swipeDisabled
}}>
<div class="calendar-month-grid">
{getDaysOfMonth(month, year, this.firstDayOfWeek % 7).map((dateObject, index) => {
const { day, dayOfWeek } = dateObject;
@@ -1190,7 +1286,7 @@ export class Datetime implements ComponentInterface {
data-year={year}
data-index={index}
data-day-of-week={dayOfWeek}
disabled={isMonthDisabled || disabled}
disabled={isCalMonthDisabled || disabled}
class={{
'calendar-day-padding': day === null,
'calendar-day': true,
@@ -1345,9 +1441,13 @@ export class Datetime implements ComponentInterface {
const { popoverRef } = this;
if (popoverRef) {
this.isTimePopoverOpen = true;
popoverRef.present(ev);
popoverRef.present(new CustomEvent('ionShadowTarget', {
detail: {
ionShadowTarget: ev.target
}
}));
await popoverRef.onWillDismiss();
@@ -1362,6 +1462,19 @@ export class Datetime implements ComponentInterface {
translucent
overlayIndex={1}
arrow={false}
onWillPresent={ev => {
/**
* Intersection Observers do not consistently fire between Blink and Webkit
* when toggling the visibility of the popover and trying to scroll the picker
* column to the correct time value.
*
* This will correctly scroll the element position to the correct time value,
* before the popover is fully presented.
*/
const cols = (ev.target! as HTMLElement).querySelectorAll('ion-picker-column-internal');
// TODO (FW-615): Potentially remove this when intersection observers are fixed in picker column
cols.forEach(col => col.scrollActiveItemIntoView());
}}
style={{
'--offset-y': '-10px'
}}

View File

@@ -31,8 +31,8 @@ the same ISO format which datetime value was originally given as.
| Year and Month | YYYY-MM | 1994-12 |
| Complete Date | YYYY-MM-DD | 1994-12-15 |
| Date and Time | YYYY-MM-DDTHH:mm | 1994-12-15T13:47 |
| UTC Timezone | YYYY-MM-DDTHH:mm:ssTZD | 1994-12-15T13:47:20.789Z |
| Timezone Offset | YYYY-MM-DDTHH:mm:ssTZD | 1994-12-15T13:47:20.789+05:00 |
| UTC Timezone | YYYY-MM-DDTHH:mm:ssZ | 1994-12-15T13:47:20Z |
| Timezone Offset | YYYY-MM-DDTHH:mm:ssTZD | 1994-12-15T13:47:20+05:00 |
| Hour and Minute | HH:mm | 13:47 |
| Hour, Minute, Second | HH:mm:ss | 13:47:20 |
@@ -147,11 +147,27 @@ const zonedTime = dateFnsTz.utcToZonedTime(date, userTimeZone);
format(zonedTime, 'yyyy-MM-dd HH:mm:ssXXX', { timeZone: userTimeZone });
```
## Parsing Dates
## Timezones
When `ionChange` is emitted, we provide an ISO-8601 string in the event payload. From there, it is the developer's responsibility to format it as they see fit. We recommend using a library like [date-fns](https://date-fns.org) to format their dates properly.
### Assigning Date Values
Below is an example of formatting an ISO-8601 string to display the month, date, and year:
`ion-datetime` does not manipulate or read timezones. Developers will need to pass in a valid ISO-8601 string that is already configured for the user's timezone when assigning a value. If no value is provided, `ion-datetime` will default to the time specified on the user's machine (which will already be in the user's timezone). We recommend using [date-fns](https://date-fns.org) to format the date to ISO-8601.
```typescript
import { formatISO } from 'date-fns';
const dateString = '2021-01-14T15:00:00.000Z';
const formattedDateValue = formatISO(new Date(dateString));
// Assign `formattedDateValue` to your `ion-datetime` value.
```
### Parsing Date Values
The `ionChange` event will emit the date value as an ISO-8601 string in the event payload. It is the developer's responsibility to format it based on their application needs. We recommend using [date-fns](https://date-fns.org) to format the date value.
Below is an example of formatting an ISO-8601 string to display the month, date and year:
```typescript
import { format, parseISO } from 'date-fns';
@@ -159,6 +175,8 @@ import { format, parseISO } from 'date-fns';
/**
* This is provided in the event
* payload from the `ionChange` event.
*
* The value is an ISO-8601 date string.
*/
const dateFromIonDatetime = '2021-06-04T14:23:00-04:00';
const formattedString = format(parseISO(dateFromIonDatetime), 'MMM d, yyyy');

View File

@@ -1,7 +1,8 @@
import {
generateMonths,
getDaysOfWeek,
generateTime
generateTime,
getToday
} from '../utils/data';
describe('generateMonths()', () => {
@@ -95,7 +96,7 @@ describe('generateTime()', () => {
day: 19,
month: 5,
year: 2021,
hour: 5,
hour: 7,
minute: 43
}
const max = {
@@ -257,5 +258,99 @@ describe('generateTime()', () => {
expect(minutes.length).toEqual(60);
});
});
it('should respect the min & max bounds', () => {
const refValue = {
day: undefined,
month: undefined,
year: undefined,
hour: 20,
minute: 30
}
const minParts = {
day: undefined,
month: undefined,
year: undefined,
hour: 19,
minute: 30
}
const maxParts = {
day: undefined,
month: undefined,
year: undefined,
hour: 20,
minute: 40
};
const { hours } = generateTime(refValue, 'h23', minParts, maxParts);
expect(hours).toStrictEqual([19, 20]);
});
it('should return the filtered minutes when the max bound is set', () => {
const refValue = {
day: undefined,
month: undefined,
year: undefined,
hour: 13,
minute: 0
};
const maxParts = {
day: undefined,
month: undefined,
year: undefined,
hour: 13,
minute: 2
};
const { minutes } = generateTime(refValue, 'h23', undefined, maxParts);
expect(minutes).toStrictEqual([0, 1, 2]);
});
it('should not filter minutes when the current hour is less than the max hour bound', () => {
const refValue = {
day: undefined,
month: undefined,
year: undefined,
hour: 12,
minute: 0
};
const maxParts = {
day: undefined,
month: undefined,
year: undefined,
hour: 13,
minute: 2
};
const { minutes } = generateTime(refValue, 'h23', undefined, maxParts);
expect(minutes.length).toEqual(60);
});
})
})
describe('getToday', () => {
beforeAll(() => {
jest.useFakeTimers('modern');
// System time is zero based, 1 = February
jest.setSystemTime(new Date(2022, 1, 21));
});
it('should return today', () => {
const res = getToday();
const expected = new Date();
expected.setHours(expected.getHours() - (expected.getTimezoneOffset() / 60));
expect(res).toEqual('2022-02-21T00:00:00.000Z');
});
});

View File

@@ -27,6 +27,11 @@ describe('generateDayAriaLabel()', () => {
expect(generateDayAriaLabel('en-US', false, reference)).toEqual('Monday, May 31');
});
it('should return Saturday, April 1', () => {
const reference = { month: 4, day: 1, year: 2006 };
expect(generateDayAriaLabel('en-US', false, reference)).toEqual('Saturday, April 1');
});
});
describe('getMonthAndDay()', () => {
@@ -37,6 +42,14 @@ describe('getMonthAndDay()', () => {
it('should return mar, 11 may', () => {
expect(getMonthAndDay('es-ES', { month: 5, day: 11, year: 2021 })).toEqual('mar, 11 may');
});
it('should return Sat, Apr 1', () => {
expect(getMonthAndDay('en-US', { month: 4, day: 1, year: 2006 })).toEqual('Sat, Apr 1');
});
it('should return sáb, 1 abr', () => {
expect(getMonthAndDay('es-ES', { month: 4, day: 1, year: 2006 })).toEqual('sáb, 1 abr');
});
})
describe('getFormattedHour()', () => {
@@ -63,7 +76,15 @@ describe('getMonthAndYear()', () => {
expect(getMonthAndYear('en-US', { month: 5, day: 11, year: 2021 })).toEqual('May 2021');
});
it('should return mar, 11 may', () => {
it('should return mayo de 2021', () => {
expect(getMonthAndYear('es-ES', { month: 5, day: 11, year: 2021 })).toEqual('mayo de 2021');
});
it('should return April 2006', () => {
expect(getMonthAndYear('en-US', { month: 4, day: 1, year: 2006 })).toEqual('April 2006');
});
it('should return abril de 2006', () => {
expect(getMonthAndYear('es-ES', { month: 4, day: 1, year: 2006 })).toEqual('abril de 2006');
});
})

View File

@@ -1,7 +1,8 @@
import {
isLeapYear,
getNumDaysInMonth,
is24Hour
is24Hour,
isMonthFirstLocale
} from '../utils/helpers';
describe('daysInMonth()', () => {
@@ -51,3 +52,18 @@ describe('is24Hour()', () => {
expect(is24Hour('en-GB-u-hc-h12')).toBe(false);
})
})
describe('isMonthFirstLocale()', () => {
it('should return true if the locale shows months first', () => {
expect(isMonthFirstLocale('en-US')).toBe(true);
expect(isMonthFirstLocale('en-GB')).toBe(true);
expect(isMonthFirstLocale('es-ES')).toBe(true);
expect(isMonthFirstLocale('ro-RO')).toBe(true);
});
it('should return false if the locale shows years first', () => {
expect(isMonthFirstLocale('zh-CN')).toBe(false);
expect(isMonthFirstLocale('ja-JP')).toBe(false);
expect(isMonthFirstLocale('ko-KR')).toBe(false);
});
})

View File

@@ -19,3 +19,53 @@ test('locale', async () => {
expect(screenshotCompare).toMatchScreenshot();
}
});
test('it should render month and year with an en-US locale', async () => {
const page = await newE2EPage({
url: '/src/components/datetime/test/locale?ionic:_testing=true'
});
const screenshotCompares = [];
const datetime = await page.find('ion-datetime');
datetime.setProperty('locale', 'en-US');
await page.waitForChanges();
const button = await page.find('ion-datetime >>> .calendar-month-year ion-item');
await button.click();
await page.waitForChanges();
const yearBody = await page.find('ion-datetime >>> .datetime-year-body');
expect(yearBody).toHaveClass('order-month-first');
screenshotCompares.push(await page.compareScreenshot());
for (const screenshotCompare of screenshotCompares) {
expect(screenshotCompare).toMatchScreenshot();
}
});
test('it should render year and month with a ja-JP locale', async () => {
const page = await newE2EPage({
url: '/src/components/datetime/test/locale?ionic:_testing=true'
});
const screenshotCompares = [];
const datetime = await page.find('ion-datetime');
datetime.setProperty('locale', 'ja-JP');
await page.waitForChanges();
const button = await page.find('ion-datetime >>> .calendar-month-year ion-item');
await button.click();
await page.waitForChanges();
const yearBody = await page.find('ion-datetime >>> .datetime-year-body');
expect(yearBody).toHaveClass('order-year-first');
screenshotCompares.push(await page.compareScreenshot());
for (const screenshotCompare of screenshotCompares) {
expect(screenshotCompare).toMatchScreenshot();
}
});

View File

@@ -1,6 +1,6 @@
import { newE2EPage } from '@stencil/core/testing';
test('minmax', async () => {
test('datetime: minmax', async () => {
const page = await newE2EPage({
url: '/src/components/datetime/test/minmax?ionic:_testing=true'
});
@@ -20,3 +20,44 @@ test('minmax', async () => {
expect(screenshotCompare).toMatchScreenshot();
}
});
test('datetime: minmax months disabled', async () => {
const page = await newE2EPage({
url: '/src/components/datetime/test/minmax?ionic:_testing=true'
});
const calendarMonths = await page.findAll('ion-datetime#inside >>> .calendar-month');
await page.waitForChanges();
expect(calendarMonths[0]).not.toHaveClass('calendar-month-disabled');
expect(calendarMonths[1]).not.toHaveClass('calendar-month-disabled');
expect(calendarMonths[2]).toHaveClass('calendar-month-disabled');
});
test('datetime: minmax navigation disabled', async () => {
const page = await newE2EPage({
url: '/src/components/datetime/test/minmax?ionic:_testing=true'
});
const navButtons = await page.findAll('ion-datetime#outside >>> .calendar-next-prev ion-button');
expect(navButtons[0]).toHaveAttribute('disabled');
expect(navButtons[1]).toHaveAttribute('disabled');
});
test('datetime: min including day should not disable month', async () => {
const page = await newE2EPage({
url: '/src/components/datetime/test/minmax?ionic:_testing=true'
});
const calendarMonths = await page.findAll('ion-datetime#min-with-day >>> .calendar-month');
await page.waitForChanges();
expect(calendarMonths[0]).toHaveClass('calendar-month-disabled');
expect(calendarMonths[1]).not.toHaveClass('calendar-month-disabled');
expect(calendarMonths[2]).not.toHaveClass('calendar-month-disabled');
})

View File

@@ -1,89 +1,81 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Datetime - Min/Max</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(250px, 1fr));
grid-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
<head>
<meta charset="UTF-8">
<title>Datetime - Min/Max</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(250px, 1fr));
grid-gap: 20px;
}
margin-top: 10px;
margin-left: 5px;
}
h2 {
font-size: 12px;
font-weight: normal;
ion-datetime {
box-shadow: 0px 16px 32px rgba(0, 0, 0, 0.25), 0px 8px 16px rgba(0, 0, 0, 0.25);
border-radius: 8px;
}
</style>
</head>
<body>
<ion-app>
<ion-header translucent="true">
<ion-toolbar>
<ion-title>Datetime - Min/Max</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="grid">
<div class="grid-item">
<h2>Value inside Bounds</h2>
<ion-datetime
id="inside"
min="2021-09"
max="2021-10"
></ion-datetime>
</div>
<div class="grid-item">
<h2>Value Outside Bounds</h2>
<ion-datetime
id="outside"
min="2021-06-05"
max="2021-06-19"
value="2021-06-20"
></ion-datetime>
</div>
<div class="grid-item">
<h2>AM/PM Min/Max</h2>
<ion-datetime
presentation="time"
min="09:30"
max="14:50"
value="10:30"
></ion-datetime>
</div>
<div class="grid-item">
<h2>h23 Min</h2>
<ion-datetime
presentation="time"
hour-cycle="h23"
min="19:30"
value="20:30"
></ion-datetime>
</div>
color: #6f7378;
margin-top: 10px;
margin-left: 5px;
}
ion-datetime {
box-shadow: 0px 16px 32px rgba(0, 0, 0, 0.25), 0px 8px 16px rgba(0, 0, 0, 0.25);
border-radius: 8px;
}
</style>
</head>
<body>
<ion-app>
<ion-header translucent="true">
<ion-toolbar>
<ion-title>Datetime - Min/Max</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="grid">
<div class="grid-item">
<h2>Value inside Bounds</h2>
<ion-datetime id="inside" min="2021-09" max="2021-10" value="2021-10-01"></ion-datetime>
</div>
</ion-content>
<div class="grid-item">
<h2>Value Outside Bounds</h2>
<ion-datetime id="outside" min="2021-06-05" max="2021-06-19" value="2021-06-20"></ion-datetime>
</div>
<div class="grid-item">
<h2>AM/PM Min/Max</h2>
<ion-datetime presentation="time" min="09:30" max="14:50" value="10:30"></ion-datetime>
</div>
<div class="grid-item">
<h2>h23 Min</h2>
<ion-datetime presentation="time" hour-cycle="h23" min="19:30" value="20:30"></ion-datetime>
</div>
<div class="grid-item">
<h2>locale: en-GB</h2>
<ion-datetime locale="en-GB" presentation="time" min="19:30" value="20:30" max="20:40"></ion-datetime>
</div>
<div class="grid-item">
<h2>Min with year/month/day</h2>
<ion-datetime id="min-with-day" min="2022-02-09" value="2022-02-09"></ion-datetime>
</div>
</div>
</ion-content>
<script>
<script>
const datetime = document.querySelector('ion-datetime');
datetime.addEventListener('ionChange', (ev) => {
console.log('Change', ev.detail.value);
})
</script>
</ion-app>
</body>
</script>
</ion-app>
</body>
</html>

View File

@@ -0,0 +1,27 @@
import { newE2EPage } from '@stencil/core/testing';
describe('datetime: position', () => {
it('should position the time picker relative to the click target', async () => {
const page = await newE2EPage({
url: '/src/components/datetime/test/position?ionic:_testing=true'
});
const screenshotCompares = [];
const openDateTimeBtn = await page.find('ion-button');
await openDateTimeBtn.click();
screenshotCompares.push(await page.compareScreenshot());
const timepickerBtn = await page.find('ion-datetime >>> .time-body');
await timepickerBtn.click();
screenshotCompares.push(await page.compareScreenshot());
for (const screenshotCompare of screenshotCompares) {
expect(screenshotCompare).toMatchScreenshot();
}
});
});

View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Datetime - Position</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<ion-header translucent="true">
<ion-toolbar>
<ion-title>Datetime - Position</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-button onclick="presentPopover(defaultPopover, event)">Present Popover</ion-button>
<ion-popover class="datetime-popover" id="default-popover">
<ion-datetime></ion-datetime>
</ion-popover>
</ion-content>
<script>
const defaultPopover = document.querySelector('ion-popover#default-popover');
const presentPopover = (popover, ev) => {
popover.event = ev;
popover.showBackdrop = false;
popover.isOpen = true;
const dismiss = () => {
popover.isOpen = false;
popover.event = undefined;
popover.removeEventListener('didDismiss', dismiss);
}
popover.addEventListener('didDismiss', dismiss);
}
</script>
</ion-app>
</body>
</html>

View File

@@ -1,6 +1,8 @@
import {
getCalendarDayState,
isDayDisabled
isDayDisabled,
isNextMonthDisabled,
isPrevMonthDisabled
} from '../utils/state';
describe('getCalendarDayState()', () => {
@@ -73,3 +75,58 @@ describe('isDayDisabled()', () => {
expect(isDayDisabled(refDate, undefined, { month: 5, day: 11, year: 2021 })).toEqual(true);
})
});
describe('isPrevMonthDisabled()', () => {
it('should return true', () => {
// Date month is before min month, in the same year
expect(isPrevMonthDisabled({ month: 5, year: 2021, day: null }, { month: 6, year: 2021, day: null })).toEqual(true);
// Date month and year is the same as min month and year
expect(isPrevMonthDisabled({ month: 1, year: 2021, day: null }, { month: 1, year: 2021, day: null })).toEqual(true);
// Date year is the same as min year (month not provided)
expect(isPrevMonthDisabled({ month: 1, year: 2021, day: null }, { year: 2021, month: null, day: null })).toEqual(true);
// Date year is less than the min year (month not provided)
expect(isPrevMonthDisabled({ month: 5, year: 2021, day: null }, { year: 2022, month: null, day: null })).toEqual(true);
// Date is above the maximum bounds and the previous month does not does not fall within the
// min-max range.
expect(isPrevMonthDisabled({ month: 12, year: 2021, day: null }, { month: 9, year: 2021, day: null }, { month: 10, year: 2021, day: null })).toEqual(true);
// Date is above the maximum bounds and a year ahead of the max range. The previous month/year
// does not fall within the min-max range.
expect(isPrevMonthDisabled({ month: 1, year: 2022, day: null }, { month: 9, year: 2021, day: null }, { month: 10, year: 2021, day: null })).toEqual(true);
});
it('should return false', () => {
// No min range provided
expect(isPrevMonthDisabled({ month: 12, year: 2021, day: null })).toEqual(false);
// Date year is the same as min year,
// but can navigate to a previous month without reducing the year.
expect(isPrevMonthDisabled({ month: 12, year: 2021, day: null }, { year: 2021, month: null, day: null })).toEqual(false);
expect(isPrevMonthDisabled({ month: 2, year: 2021, day: null }, { year: 2021, month: null, day: null })).toEqual(false);
});
});
describe('isNextMonthDisabled()', () => {
it('should return true', () => {
// Date month is the same as max month (in the same year)
expect(isNextMonthDisabled({ month: 10, year: 2021, day: null }, { month: 10, year: 2021, day: null })).toEqual(true);
// Date month is after the max month (in the same year)
expect(isNextMonthDisabled({ month: 10, year: 2021, day: null }, { month: 9, year: 2021, day: null })).toEqual(true);
// Date year is after the max month and year
expect(isNextMonthDisabled({ month: 10, year: 2022, day: null }, { month: 12, year: 2021, day: null })).toEqual(true);
});
it('should return false', () => {
// No max range provided
expect(isNextMonthDisabled({ month: 10, year: 2021, day: null })).toBe(false);
// Date month is before max month and is the previous month,
// so that navigating the next month would re-enter the max range
expect(isNextMonthDisabled({ month: 10, year: 2021, day: null }, { month: 11, year: 2021, day: null })).toEqual(false);
});
});

View File

@@ -0,0 +1,55 @@
import { newE2EPage } from '@stencil/core/testing';
describe('datetime: sub-pixel width', () => {
test('should update the month when next button is clicked', async () => {
const page = await newE2EPage({
url: '/src/components/datetime/test/sub-pixel-width?ionic:_testing=true'
});
const openModalBtn = await page.find('#open-modal');
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const modal = await page.find('ion-modal');
await openModalBtn.click();
await modal.waitForVisible();
await ionModalDidPresent.next();
const buttons = await page.findAll('ion-datetime >>> .calendar-next-prev ion-button')
await buttons[1].click();
await page.waitForEvent('datetimeMonthDidChange');
const monthYear = await page.find('ion-datetime >>> .calendar-month-year');
expect(monthYear.textContent.trim()).toBe('March 2022');
});
test('should update the month when prev button is clicked', async () => {
const page = await newE2EPage({
url: '/src/components/datetime/test/sub-pixel-width?ionic:_testing=true'
});
const openModalBtn = await page.find('#open-modal');
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const modal = await page.find('ion-modal');
await openModalBtn.click();
await modal.waitForVisible();
await ionModalDidPresent.next();
const buttons = await page.findAll('ion-datetime >>> .calendar-next-prev ion-button')
await buttons[0].click();
await page.waitForEvent('datetimeMonthDidChange');
const monthYear = await page.find('ion-datetime >>> .calendar-month-year');
expect(monthYear.textContent.trim()).toBe('January 2022');
});
});

View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Datetime - Sub Pixel Width</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
ion-datetime {
width: 334.05px;
height: 500px;
}
#background {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<ion-app>
<ion-header translucent="true">
<ion-toolbar>
<ion-title>Datetime - Sub Pixel Width</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<h2>Modal</h2>
<ion-button id="open-modal">Present Modal</ion-button>
<ion-modal trigger="open-modal" id="modal">
<div id="background">
<ion-datetime id="picker" value="2022-02-01"></ion-datetime>
</div>
</ion-modal>
</ion-content>
</ion-app>
<script type="module">
import { InitMonthDidChangeEvent } from '../test/utils/month-did-change-event.js';
document.getElementById('open-modal').addEventListener('click', () => {
InitMonthDidChangeEvent();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,19 @@
/**
* Initializes a mutation observer to detect when the calendar month
* text is updated as a result of a month change in `ion-datetime`.
*
* @param {*} datetimeSelector The element selector for the `ion-datetime` component.
*/
export function InitMonthDidChangeEvent(datetimeSelector = 'ion-datetime') {
const observer = new MutationObserver(mutationRecords => {
if (mutationRecords[0].type === 'characterData') {
document.dispatchEvent(new CustomEvent('datetimeMonthDidChange'));
}
});
observer.observe(document.querySelector(datetimeSelector).shadowRoot.querySelector('.calendar-month-year'), {
characterData: true,
subtree: true
});
}

View File

@@ -0,0 +1,138 @@
import { newE2EPage } from '@stencil/core/testing';
/**
* This test emulates zoom behavior in the browser to make sure
* that key functions of the ion-datetime continue to function even
* if the page is zoomed in or out.
*/
describe('datetime: zoom interactivity', () => {
let deviceScaleFactor;
describe('zoom out', () => {
beforeEach(() => {
deviceScaleFactor = 0.75;
});
test('should update the month when next button is clicked', async () => {
const page = await newE2EPage({
url: '/src/components/datetime/test/zoom?ionic:_testing=true'
});
page.setViewport({
width: 640,
height: 480,
deviceScaleFactor
});
const openModalBtn = await page.find('#open-modal');
await openModalBtn.click();
const modal = await page.find('ion-modal');
await modal.waitForVisible();
await page.waitForTimeout(250);
const buttons = await page.findAll('ion-datetime >>> .calendar-next-prev ion-button')
await buttons[1].click();
await page.waitForEvent('datetimeMonthDidChange');
const monthYear = await page.find('ion-datetime >>> .calendar-month-year');
expect(monthYear.textContent.trim()).toBe('March 2022');
});
test('should update the month when prev button is clicked', async () => {
const page = await newE2EPage({
url: '/src/components/datetime/test/zoom?ionic:_testing=true'
});
const openModalBtn = await page.find('#open-modal');
await openModalBtn.click();
const modal = await page.find('ion-modal');
await modal.waitForVisible();
await page.waitForTimeout(250);
const buttons = await page.findAll('ion-datetime >>> .calendar-next-prev ion-button')
await buttons[0].click();
await page.waitForEvent('datetimeMonthDidChange');
const monthYear = await page.find('ion-datetime >>> .calendar-month-year');
expect(monthYear.textContent.trim()).toBe('January 2022');
});
});
describe('zoom in', () => {
beforeEach(() => {
deviceScaleFactor = 2;
});
test('should update the month when next button is clicked', async () => {
const page = await newE2EPage({
url: '/src/components/datetime/test/zoom?ionic:_testing=true'
});
page.setViewport({
width: 640,
height: 480,
deviceScaleFactor
});
const openModalBtn = await page.find('#open-modal');
const modal = await page.find('ion-modal');
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
await openModalBtn.click();
await modal.waitForVisible();
await ionModalDidPresent.next();
const buttons = await page.findAll('ion-datetime >>> .calendar-next-prev ion-button')
await buttons[1].click();
await page.waitForEvent('datetimeMonthDidChange');
const monthYear = await page.find('ion-datetime >>> .calendar-month-year');
expect(monthYear.textContent.trim()).toBe('March 2022');
});
test('should update the month when prev button is clicked', async () => {
const page = await newE2EPage({
url: '/src/components/datetime/test/zoom?ionic:_testing=true'
});
const openModalBtn = await page.find('#open-modal');
const modal = await page.find('ion-modal');
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
await openModalBtn.click();
await modal.waitForVisible();
await ionModalDidPresent.next();
const buttons = await page.findAll('ion-datetime >>> .calendar-next-prev ion-button')
await buttons[0].click();
await page.waitForEvent('datetimeMonthDidChange');
const monthYear = await page.find('ion-datetime >>> .calendar-month-year');
expect(monthYear.textContent.trim()).toBe('January 2022');
});
});
});

View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Datetime - Zoom</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
#background {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<ion-app>
<ion-header translucent="true">
<ion-toolbar>
<ion-title>Datetime - Zoom</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<h2>Modal</h2>
<ion-button id="open-modal">Present Modal</ion-button>
<ion-modal trigger="open-modal" id="modal">
<div id="background">
<ion-datetime id="picker" value="2022-02-01"></ion-datetime>
</div>
</ion-modal>
</ion-content>
</ion-app>
<script type="module">
import { InitMonthDidChangeEvent } from '../test/utils/month-did-change-event.js';
document.getElementById('open-modal').addEventListener('click', () => {
InitMonthDidChangeEvent();
});
</script>
</body>
</html>

View File

@@ -219,11 +219,15 @@ export const generateTime = (
if (maxParts.hour !== undefined) {
processedHours = processedHours.filter(hour => {
const convertedHour = refParts.ampm === 'pm' ? (hour + 12) % 24 : hour;
return convertedHour <= maxParts.hour!;
return (use24Hour ? hour : convertedHour) <= maxParts.hour!;
});
isPMAllowed = maxParts.hour >= 13;
}
if (maxParts.minute !== undefined) {
if (maxParts.minute !== undefined && refParts.hour === maxParts.hour) {
// The available minutes should only be filtered when the hour is the same as the max hour.
// For example if the max hour is 10:30 and the current hour is 10:00,
// users should be able to select 00-30 minutes.
// If the current hour is 09:00, users should be able to select 00-60 minutes.
processedMinutes = processedMinutes.filter(minute => minute <= maxParts.minute!);
}
@@ -278,9 +282,9 @@ export const getPickerMonths = (
}
processedMonths.forEach(processedMonth => {
const date = new Date(`${processedMonth}/1/${year}`);
const date = new Date(`${processedMonth}/1/${year} GMT+0000`);
const monthString = new Intl.DateTimeFormat(locale, { month: 'long' }).format(date);
const monthString = new Intl.DateTimeFormat(locale, { month: 'long', timeZone: 'UTC' }).format(date);
months.push({ text: monthString, value: processedMonth });
});
} else {
@@ -288,9 +292,34 @@ export const getPickerMonths = (
const minMonth = minParts && minParts.year === year ? minParts.month : 1;
for (let i = minMonth; i <= maxMonth; i++) {
const date = new Date(`${i}/1/${year}`);
const monthString = new Intl.DateTimeFormat(locale, { month: 'long' }).format(date);
/**
*
* There is a bug on iOS 14 where
* Intl.DateTimeFormat takes into account
* the local timezone offset when formatting dates.
*
* Forcing the timezone to 'UTC' fixes the issue. However,
* we should keep this workaround as it is safer. In the event
* this breaks in another browser, we will not be impacted
* because all dates will be interpreted in UTC.
*
* Example:
* new Intl.DateTimeFormat('en-US', { month: 'long' }).format(new Date('Sat Apr 01 2006 00:00:00 GMT-0400 (EDT)')) // "March"
* new Intl.DateTimeFormat('en-US', { month: 'long', timeZone: 'UTC' }).format(new Date('Sat Apr 01 2006 00:00:00 GMT-0400 (EDT)')) // "April"
*
* In certain timezones, iOS 14 shows the wrong
* date for .toUTCString(). To combat this, we
* force all of the timezones to GMT+0000 (UTC).
*
* Example:
* Time Zone: Central European Standard Time
* new Date('1/1/1992').toUTCString() // "Tue, 31 Dec 1991 23:00:00 GMT"
* new Date('1/1/1992 GMT+0000').toUTCString() // "Wed, 01 Jan 1992 00:00:00 GMT"
*/
const date = new Date(`${i}/1/${year} GMT+0000`);
const monthString = new Intl.DateTimeFormat(locale, { month: 'long', timeZone: 'UTC' }).format(date);
months.push({ text: monthString, value: i });
}
}

View File

@@ -56,9 +56,9 @@ export const generateDayAriaLabel = (locale: string, today: boolean, refParts: D
/**
* MM/DD/YYYY will return midnight in the user's timezone.
*/
const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year}`);
const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year} GMT+0000`);
const labelString = new Intl.DateTimeFormat(locale, { weekday: 'long', month: 'long', day: 'numeric' }).format(date);
const labelString = new Intl.DateTimeFormat(locale, { weekday: 'long', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(date);
/**
* If date is today, prepend "Today" so screen readers indicate
@@ -72,8 +72,8 @@ export const generateDayAriaLabel = (locale: string, today: boolean, refParts: D
* Used for the header in MD mode.
*/
export const getMonthAndDay = (locale: string, refParts: DatetimeParts) => {
const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year}`);
return new Intl.DateTimeFormat(locale, { weekday: 'short', month: 'short', day: 'numeric' }).format(date);
const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year} GMT+0000`);
return new Intl.DateTimeFormat(locale, { weekday: 'short', month: 'short', day: 'numeric', timeZone: 'UTC' }).format(date);
}
/**
@@ -83,6 +83,6 @@ export const getMonthAndDay = (locale: string, refParts: DatetimeParts) => {
* Example: May 2021
*/
export const getMonthAndYear = (locale: string, refParts: DatetimeParts) => {
const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year}`);
return new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(date);
const date = new Date(`${refParts.month}/${refParts.day}/${refParts.year} GMT+0000`);
return new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric', timeZone: 'UTC' }).format(date);
}

View File

@@ -62,3 +62,28 @@ export const is24Hour = (locale: string, hourCycle?: 'h23' | 'h12') => {
export const getNumDaysInMonth = (month: number, year: number) => {
return (month === 4 || month === 6 || month === 9 || month === 11) ? 30 : (month === 2) ? isLeapYear(year) ? 29 : 28 : 31;
}
/**
* Certain locales display month then year while
* others display year then month.
* We can use Intl.DateTimeFormat to determine
* the ordering for each locale.
*/
export const isMonthFirstLocale = (locale: string) => {
/**
* By setting month and year we guarantee that only
* month, year, and literal (slashes '/', for example)
* values are included in the formatToParts results.
*
* The ordering of the parts will be determined by
* the locale. So if the month is the first value,
* then we know month should be shown first. If the
* year is the first value, then we know year should be shown first.
*
* This ordering can be controlled by customizing the locale property.
*/
const parts = new Intl.DateTimeFormat(locale, { month: 'numeric', year: 'numeric' }).formatToParts(new Date());
return parts[0].type === 'month';
}

View File

@@ -2,6 +2,7 @@ import { DatetimeParts } from '../datetime-interface';
import { isAfter, isBefore, isSameDay } from './comparison';
import { generateDayAriaLabel } from './format';
import { getNextMonth, getPreviousMonth } from './manipulation';
export const isYearDisabled = (refYear: number, minParts?: DatetimeParts, maxParts?: DatetimeParts) => {
if (minParts && minParts.year > refYear) {
@@ -102,3 +103,52 @@ export const getCalendarDayState = (
ariaLabel: generateDayAriaLabel(locale, isToday, refParts)
}
}
/**
* Returns `true` if the month is disabled given the
* current date value and min/max date constraints.
*/
export const isMonthDisabled = (refParts: DatetimeParts, { minParts, maxParts }: {
minParts?: DatetimeParts,
maxParts?: DatetimeParts
}) => {
// If the year is disabled then the month is disabled.
if (isYearDisabled(refParts.year, minParts, maxParts)) {
return true;
}
// If the date value is before the min date, then the month is disabled.
// If the date value is after the max date, then the month is disabled.
if (minParts && isBefore(refParts, minParts) || maxParts && isAfter(refParts, maxParts)) {
return true;
}
return false;
}
/**
* Given a working date, an optional minimum date range,
* and an optional maximum date range; determine if the
* previous navigation button is disabled.
*/
export const isPrevMonthDisabled = (
refParts: DatetimeParts,
minParts?: DatetimeParts,
maxParts?: DatetimeParts) => {
const prevMonth = getPreviousMonth(refParts);
return isMonthDisabled(prevMonth, {
minParts,
maxParts
});
}
/**
* Given a working date and a maximum date range,
* determine if the next navigation button is disabled.
*/
export const isNextMonthDisabled = (
refParts: DatetimeParts,
maxParts?: DatetimeParts) => {
const nextMonth = getNextMonth(refParts);
return isMonthDisabled(nextMonth, {
maxParts
});
}

View File

@@ -1,4 +1,5 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop, h } from '@stencil/core';
import { close } from 'ionicons/icons';
import { getIonMode } from '../../global/ionic-global';
import { AnimationBuilder, Color, RouterDirection } from '../../interface';
@@ -105,7 +106,7 @@ export class FabButton implements ComponentInterface, AnchorInterface, ButtonInt
* is pressed. Only applies if it is the main button inside of a fab containing a
* fab list.
*/
@Prop() closeIcon = 'close';
@Prop() closeIcon = close;
/**
* Emitted when the button has focus.

View File

@@ -149,7 +149,7 @@ export default defineComponent({
| Property | Attribute | Description | Type | Default |
| ----------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------- |
| `activated` | `activated` | If `true`, the fab button will be show a close icon. | `boolean` | `false` |
| `closeIcon` | `close-icon` | The icon name to use for the close icon. This will appear when the fab button is pressed. Only applies if it is the main button inside of a fab containing a fab list. | `string` | `'close'` |
| `closeIcon` | `close-icon` | The icon name to use for the close icon. This will appear when the fab button is pressed. Only applies if it is the main button inside of a fab containing a fab list. | `string` | `close` |
| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` |
| `disabled` | `disabled` | If `true`, the user cannot interact with the fab button. | `boolean` | `false` |
| `download` | `download` | This attribute instructs browsers to download a URL instead of navigating to it, so the user will be prompted to save it as a local file. If the attribute has a value, it is used as the pre-filled file name in the Save prompt (the user can still change the file name if they want). | `string \| undefined` | `undefined` |

View File

@@ -1,7 +1,7 @@
import { Component, ComponentInterface, Element, Host, Prop, h, writeTask } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { componentOnReady, inheritAttributes } from '../../utils/helpers';
import { Attributes, componentOnReady, inheritAttributes } from '../../utils/helpers';
import { hostContext } from '../../utils/theme';
import { cloneElement, createHeaderIndex, handleContentScroll, handleHeaderFade, handleToolbarIntersection, setHeaderActive, setToolbarBackgroundOpacity } from './header.utils';
@@ -21,7 +21,7 @@ export class Header implements ComponentInterface {
private contentScrollCallback?: any;
private intersectionObserver?: any;
private collapsibleMainHeader?: HTMLElement;
private inheritedAttributes: { [k: string]: any } = {};
private inheritedAttributes: Attributes = {};
@Element() el!: HTMLElement;

View File

@@ -1,6 +1,7 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop, State, Watch, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { Attributes, inheritAttributes } from '../../utils/helpers';
/**
* @part image - The inner `img` element.
@@ -13,6 +14,7 @@ import { getIonMode } from '../../global/ionic-global';
export class Img implements ComponentInterface {
private io?: IntersectionObserver;
private inheritedAttributes: Attributes = {};
@Element() el!: HTMLElement;
@@ -45,6 +47,10 @@ export class Img implements ComponentInterface {
/** Emitted when the img fails to load */
@Event() ionError!: EventEmitter<void>;
componentWillLoad() {
this.inheritedAttributes = inheritAttributes(this.el, ['draggable']);
}
componentDidLoad() {
this.addIO();
}
@@ -100,17 +106,38 @@ export class Img implements ComponentInterface {
}
render() {
const { loadSrc, alt, onLoad, loadError, inheritedAttributes } = this;
const { draggable } = inheritedAttributes;
return (
<Host class={getIonMode(this)}>
<img
decoding="async"
src={this.loadSrc}
alt={this.alt}
onLoad={this.onLoad}
onError={this.loadError}
src={loadSrc}
alt={alt}
onLoad={onLoad}
onError={loadError}
part="image"
draggable={isDraggable(draggable)}
/>
</Host>
);
}
}
/**
* Enumerated strings must be set as booleans
* as Stencil will not render 'false' in the DOM.
* The need to explicitly render draggable="true"
* as only certain elements are draggable by default.
* https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/draggable.
*/
const isDraggable = (draggable?: string): boolean | undefined => {
switch (draggable) {
case 'true':
return true;
case 'false':
return false;
default:
return undefined;
}
}

View File

@@ -0,0 +1,17 @@
import { newE2EPage } from '@stencil/core/testing';
test('img: draggable', async () => {
const page = await newE2EPage({
url: '/src/components/img/test/draggable?ionic:_testing=true'
});
const imgDraggableTrue = await page.find('#img-draggable-true >>> img');
expect(imgDraggableTrue.getAttribute('draggable')).toEqual('true');
const imgDraggableFalse = await page.find('#img-draggable-false >>> img');
expect(imgDraggableFalse.getAttribute('draggable')).toEqual('false');
const imgDraggableUnset = await page.find('#img-draggable-unset >>> img');
expect(imgDraggableUnset.getAttribute('draggable')).toEqual(null);
});

View File

@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Img - Draggable</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
ion-img::part(image) {
border: 1px solid rgba(0, 0, 0, 0.5);
border-radius: 4px;
height: 100px;
width: 100px;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Img - Draggable</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-list>
<ion-item>
<ion-label>Draggable</ion-label>
<ion-img id="img-draggable-true" draggable="true"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB4AAAARQAQMAAAA2ut43AAAABlBMVEXMzMz////TjRV2AAAEPUlEQVR42u3RMREAMBDDsPAn/eXhavOq87Ztu7u7+6eBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBf2hvgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgatgn4GBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGr7S0wMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDBwFewzMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwcLW9BQYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBq6CfQYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgautrfAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAVbDPwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNX2FhgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGLgK9hkYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBi42t4CAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV8E+AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA1fbW2BgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYOAq2GdgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGDgansLDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMXAX7DAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDFxtb4GBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGrYJ+BgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBq+0tMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwcBXsMzAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMHC1vQUGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgaugn0GBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGjvYD5WuYrpZqdmcAAAAASUVORK5CYII=">
</ion-img>
</ion-item>
<ion-item>
<ion-label>Not draggable (draggable="false")</ion-label>
<ion-img id="img-draggable-false" draggable="false"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB4AAAARQAQMAAAA2ut43AAAABlBMVEXMzMz////TjRV2AAAEPUlEQVR42u3RMREAMBDDsPAn/eXhavOq87Ztu7u7+6eBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBf2hvgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgatgn4GBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGr7S0wMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDBwFewzMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwcLW9BQYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBq6CfQYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgautrfAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAVbDPwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNX2FhgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGLgK9hkYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBi42t4CAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV8E+AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA1fbW2BgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYOAq2GdgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGDgansLDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMXAX7DAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDFxtb4GBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGrYJ+BgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBq+0tMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwcBXsMzAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMHC1vQUGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgaugn0GBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGjvYD5WuYrpZqdmcAAAAASUVORK5CYII=">
</ion-img>
</ion-item>
<ion-item>
<ion-label>Draggable (draggable not set)</ion-label>
<ion-img id="img-draggable-unset"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB4AAAARQAQMAAAA2ut43AAAABlBMVEXMzMz////TjRV2AAAEPUlEQVR42u3RMREAMBDDsPAn/eXhavOq87Ztu7u7+6eBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBf2hvgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgatgn4GBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGr7S0wMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDBwFewzMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwcLW9BQYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBq6CfQYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgautrfAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAVbDPwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwNX2FhgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGLgK9hkYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBi42t4CAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV8E+AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA1fbW2BgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYOAq2GdgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGDgansLDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMXAX7DAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDFxtb4GBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGrYJ+BgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBq+0tMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwcBXsMzAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMHC1vQUGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgaugn0GBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGjvYD5WuYrpZqdmcAAAAASUVORK5CYII">
</ion-img>
</ion-item>
</ion-list>
</ion-content>
</ion-app>
</body>
</html>

View File

@@ -69,7 +69,7 @@ export class InfiniteScrollExample {
// App logic to determine if all data is loaded
// and disable the infinite scroll
if (data.length == 1000) {
if (data.length === 1000) {
event.target.disabled = true;
}
}, 500);
@@ -111,7 +111,7 @@ infiniteScroll.addEventListener('ionInfinite', function(event) {
// App logic to determine if all data is loaded
// and disable the infinite scroll
if (data.length == 1000) {
if (data.length === 1000) {
event.target.disabled = true;
}
}, 500);
@@ -164,7 +164,7 @@ const InfiniteScrollExample: React.FC = () => {
pushData();
console.log('Loaded data');
ev.target.complete();
if (data.length == 1000) {
if (data.length === 1000) {
setInfiniteDisabled(true);
}
}, 500);
@@ -263,7 +263,7 @@ export class InfiniteScrollExample {
// App logic to determine if all data is loaded
// and disable the infinite scroll
if (this.data.length == 1000) {
if (this.data.length === 1000) {
ev.target.disabled = true;
}
}, 500);
@@ -380,7 +380,7 @@ export default defineComponent({
// App logic to determine if all data is loaded
// and disable the infinite scroll
if (items.value.length == 1000) {
if (items.value.length === 1000) {
ev.target.disabled = true;
}
}, 500);

View File

@@ -36,7 +36,7 @@ export class InfiniteScrollExample {
// App logic to determine if all data is loaded
// and disable the infinite scroll
if (data.length == 1000) {
if (data.length === 1000) {
event.target.disabled = true;
}
}, 500);

View File

@@ -25,7 +25,7 @@ infiniteScroll.addEventListener('ionInfinite', function(event) {
// App logic to determine if all data is loaded
// and disable the infinite scroll
if (data.length == 1000) {
if (data.length === 1000) {
event.target.disabled = true;
}
}, 500);

View File

@@ -37,7 +37,7 @@ const InfiniteScrollExample: React.FC = () => {
pushData();
console.log('Loaded data');
ev.target.complete();
if (data.length == 1000) {
if (data.length === 1000) {
setInfiniteDisabled(true);
}
}, 500);

View File

@@ -38,7 +38,7 @@ export class InfiniteScrollExample {
// App logic to determine if all data is loaded
// and disable the infinite scroll
if (this.data.length == 1000) {
if (this.data.length === 1000) {
ev.target.disabled = true;
}
}, 500);

View File

@@ -73,7 +73,7 @@ export default defineComponent({
// App logic to determine if all data is loaded
// and disable the infinite scroll
if (items.value.length == 1000) {
if (items.value.length === 1000) {
ev.target.disabled = true;
}
}, 500);

View File

@@ -2,7 +2,7 @@ import { Build, Component, ComponentInterface, Element, Event, EventEmitter, Hos
import { getIonMode } from '../../global/ionic-global';
import { AutocompleteTypes, Color, InputChangeEventDetail, StyleEventDetail, TextFieldTypes } from '../../interface';
import { debounceEvent, findItemLabel, inheritAttributes } from '../../utils/helpers';
import { Attributes, debounceEvent, findItemLabel, inheritAttributes } from '../../utils/helpers';
import { createColorClasses } from '../../utils/theme';
/**
@@ -21,7 +21,8 @@ export class Input implements ComponentInterface {
private nativeInput?: HTMLInputElement;
private inputId = `ion-input-${inputIds++}`;
private didBlurAfterEdit = false;
private inheritedAttributes: { [k: string]: any } = {};
private inheritedAttributes: Attributes = {};
private isComposing = false;
/**
* This is required for a WebKit bug which requires us to
@@ -117,7 +118,7 @@ export class Input implements ComponentInterface {
/**
* The maximum value, which must not be less than its minimum (min attribute) value.
*/
@Prop() max?: string;
@Prop() max?: string | number;
/**
* If the value of the type attribute is `text`, `email`, `search`, `password`, `tel`, or `url`, this attribute specifies the maximum number of characters that the user can enter.
@@ -127,7 +128,7 @@ export class Input implements ComponentInterface {
/**
* The minimum value, which must not be greater than its maximum (max attribute) value.
*/
@Prop() min?: string;
@Prop() min?: string | number;
/**
* If the value of the type attribute is `text`, `email`, `search`, `password`, `tel`, or `url`, this attribute specifies the minimum number of characters that the user can enter.
@@ -231,6 +232,19 @@ export class Input implements ComponentInterface {
*/
@Watch('value')
protected valueChanged() {
const nativeInput = this.nativeInput;
const value = this.getValue();
if (nativeInput && nativeInput.value !== value && !this.isComposing) {
/**
* Assigning the native input's value on attribute
* value change, allows `ionInput` implementations
* to override the control's value.
*
* Used for patterns such as input trimming (removing whitespace),
* or input masking.
*/
nativeInput.value = value;
}
this.emitStyle();
this.ionChange.emit({ value: this.value == null ? this.value : this.value.toString() });
}
@@ -249,12 +263,27 @@ export class Input implements ComponentInterface {
}
}
componentDidLoad() {
const nativeInput = this.nativeInput;
if (nativeInput) {
// TODO: FW-729 Update to JSX bindings when Stencil resolves bug with:
// https://github.com/ionic-team/stencil/issues/3235
nativeInput.addEventListener('compositionstart', this.onCompositionStart);
nativeInput.addEventListener('compositionend', this.onCompositionEnd);
}
}
disconnectedCallback() {
if (Build.isBrowser) {
document.dispatchEvent(new CustomEvent('ionInputDidUnload', {
detail: this.el
}));
}
const nativeInput = this.nativeInput;
if (nativeInput) {
nativeInput.removeEventListener('compositionstart', this.onCompositionStart);
nativeInput.removeEventListener('compositionEnd', this.onCompositionEnd);
}
}
/**
@@ -353,6 +382,14 @@ export class Input implements ComponentInterface {
}
}
private onCompositionStart = () => {
this.isComposing = true;
}
private onCompositionEnd = () => {
this.isComposing = false;
}
private clearTextOnEnter = (ev: KeyboardEvent) => {
if (ev.key === 'Enter') { this.clearTextInput(ev); }
}

View File

@@ -332,9 +332,9 @@ export default defineComponent({
| `disabled` | `disabled` | If `true`, the user cannot interact with the input. | `boolean` | `false` |
| `enterkeyhint` | `enterkeyhint` | A hint to the browser for which enter key to display. Possible values: `"enter"`, `"done"`, `"go"`, `"next"`, `"previous"`, `"search"`, and `"send"`. | `"done" \| "enter" \| "go" \| "next" \| "previous" \| "search" \| "send" \| undefined` | `undefined` |
| `inputmode` | `inputmode` | A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`. | `"decimal" \| "email" \| "none" \| "numeric" \| "search" \| "tel" \| "text" \| "url" \| undefined` | `undefined` |
| `max` | `max` | The maximum value, which must not be less than its minimum (min attribute) value. | `string \| undefined` | `undefined` |
| `max` | `max` | The maximum value, which must not be less than its minimum (min attribute) value. | `number \| string \| undefined` | `undefined` |
| `maxlength` | `maxlength` | If the value of the type attribute is `text`, `email`, `search`, `password`, `tel`, or `url`, this attribute specifies the maximum number of characters that the user can enter. | `number \| undefined` | `undefined` |
| `min` | `min` | The minimum value, which must not be greater than its maximum (max attribute) value. | `string \| undefined` | `undefined` |
| `min` | `min` | The minimum value, which must not be greater than its maximum (max attribute) value. | `number \| string \| undefined` | `undefined` |
| `minlength` | `minlength` | If the value of the type attribute is `text`, `email`, `search`, `password`, `tel`, or `url`, this attribute specifies the minimum number of characters that the user can enter. | `number \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `multiple` | `multiple` | If `true`, the user can enter more than one value. This attribute applies when the type attribute is set to `"email"` or `"file"`, otherwise it is ignored. | `boolean \| undefined` | `undefined` |

View File

@@ -36,4 +36,29 @@ test('input: basic', async () => {
for (const compare of compares) {
expect(compare).toMatchScreenshot();
}
});
test('input: basic should not error on input', async () => {
const page = await newE2EPage({
url: '/src/components/input/test/basic?ionic:_testing=true'
});
const errors = [];
page.on('console', msg => {
if (msg.type() === 'error') {
errors.push(msg.text);
}
});
const inputs = await page.findAll('ion-input');
for (const input of inputs) {
await input.click();
await input.type('letters and 12345');
}
expect(errors.length).toEqual(0);
})

View File

@@ -0,0 +1,20 @@
import { newE2EPage } from '@stencil/core/testing';
test('input: masking', async () => {
const page = await newE2EPage({
url: '/src/components/input/test/masking?ionic:_testing=true'
});
const inputTrimmed = await page.find('#inputTrimmed');
await inputTrimmed.click();
await page.keyboard.type('S p a c e s');
const currentValue = await page.$eval('#inputTrimmed', (el: any) => {
return el.value;
});
expect(currentValue).toEqual('Spaces');
});

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Input - Masking</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Input - Masking</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item>
<ion-label position="stacked">Input with trimming</ion-label>
<ion-input id="inputTrimmed" placeholder="Trimmed Input" />
</ion-item>
</ion-content>
<script>
document
.getElementById('inputTrimmed')
.addEventListener('ionInput', e => {
e.target.value = e.target.value.trim();
});
</script>
</ion-app>
</body>
</html>

View File

@@ -471,3 +471,12 @@
--border-color: #{$item-md-input-fill-border-color-hover};
}
}
// Material Design Text Field Character Counter
// --------------------------------------------------
.item-counter {
color: #{$item-md-input-counter-color};
letter-spacing: #{$item-md-input-counter-letter-spacing};
}

View File

@@ -78,12 +78,17 @@ $item-md-input-fill-border-color: $background-color-step-500 !default;
/// @prop - Color of the item border when `fill` is set and hovered
$item-md-input-fill-border-color-hover: $background-color-step-750 !default;
/// @prop - Color of the item input counter
$item-md-input-counter-color: rgba(0, 0, 0, .6) !default;
/// @prop - Letter spacing of the item input counter
$item-md-input-counter-letter-spacing: .0333333333em !default;
// Item Label
// --------------------------------------------------
/// @prop - Margin top of the label
$item-md-label-margin-top: 11px !default;
$item-md-label-margin-top: 10px !default;
/// @prop - Margin end of the label
$item-md-label-margin-end: 0 !default;

View File

@@ -533,6 +533,16 @@ ion-ripple-effect {
z-index: 1;
}
// Item Max Length Counter
// --------------------------------------------------
.item-counter {
@include margin-horizontal(auto, null);
padding-inline-start: 16px;
white-space: nowrap;
}
// Item: Reduced Motion
// --------------------------------------------------

View File

@@ -1,4 +1,5 @@
import { Component, ComponentInterface, Element, Host, Listen, Prop, State, forceUpdate, h } from '@stencil/core';
import { chevronForward } from 'ionicons/icons';
import { getIonMode } from '../../global/ionic-global';
import { AnimationBuilder, Color, CssClassMap, RouterDirection, StyleEventDetail } from '../../interface';
@@ -61,7 +62,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
/**
* The icon to use when `detail` is set to `true`.
*/
@Prop() detailIcon = 'chevron-forward';
@Prop() detailIcon = chevronForward;
/**
* If `true`, the user cannot interact with the item.
@@ -297,7 +298,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
private updateCounterOutput(inputEl: HTMLIonInputElement | HTMLIonTextareaElement) {
if (this.counter && !this.multipleInputs && inputEl?.maxlength !== undefined) {
const length = inputEl?.value?.toString().length ?? '0';
this.counterString = `${length}/${inputEl.maxlength}`;
this.counterString = `${length} / ${inputEl.maxlength}`;
}
}
@@ -368,7 +369,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
<slot></slot>
</div>
<slot name="end"></slot>
{showDetail && <ion-icon icon={detailIcon} lazy={false} class="item-detail-icon" part="detail-icon" aria-hidden="true"></ion-icon>}
{showDetail && <ion-icon icon={detailIcon} lazy={false} class="item-detail-icon" part="detail-icon" aria-hidden="true" flip-rtl={detailIcon === chevronForward}></ion-icon>}
<div class="item-inner-highlight"></div>
</div>
{canActivate && mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}

View File

@@ -1929,25 +1929,25 @@ export default defineComponent({
## Properties
| Property | Attribute | Description | Type | Default |
| ----------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------------------- |
| `button` | `button` | If `true`, a button tag will be rendered and the item will be tappable. | `boolean` | `false` |
| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` |
| `counter` | `counter` | If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`. | `boolean` | `false` |
| `detail` | `detail` | If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode` is `ios` and an `href` or `button` property is present. | `boolean \| undefined` | `undefined` |
| `detailIcon` | `detail-icon` | The icon to use when `detail` is set to `true`. | `string` | `'chevron-forward'` |
| `disabled` | `disabled` | If `true`, the user cannot interact with the item. | `boolean` | `false` |
| `download` | `download` | This attribute instructs browsers to download a URL instead of navigating to it, so the user will be prompted to save it as a local file. If the attribute has a value, it is used as the pre-filled file name in the Save prompt (the user can still change the file name if they want). | `string \| undefined` | `undefined` |
| `fill` | `fill` | The fill for the item. If `'solid'` the item will have a background. If `'outline'` the item will be transparent with a border. Only available in `md` mode. | `"outline" \| "solid" \| undefined` | `undefined` |
| `href` | `href` | Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered. | `string \| undefined` | `undefined` |
| `lines` | `lines` | How the bottom border should be displayed on the item. | `"full" \| "inset" \| "none" \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `rel` | `rel` | Specifies the relationship of the target object to the link object. The value is a space-separated list of [link types](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types). | `string \| undefined` | `undefined` |
| `routerAnimation` | -- | When using a router, it specifies the transition animation when navigating to another page using `href`. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` |
| `routerDirection` | `router-direction` | When using a router, it specifies the transition direction when navigating to another page using `href`. | `"back" \| "forward" \| "root"` | `'forward'` |
| `shape` | `shape` | The shape of the item. If "round" it will have increased border radius. | `"round" \| undefined` | `undefined` |
| `target` | `target` | Specifies where to display the linked URL. Only applies when an `href` is provided. Special keywords: `"_blank"`, `"_self"`, `"_parent"`, `"_top"`. | `string \| undefined` | `undefined` |
| `type` | `type` | The type of the button. Only used when an `onclick` or `button` property is present. | `"button" \| "reset" \| "submit"` | `'button'` |
| Property | Attribute | Description | Type | Default |
| ----------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ---------------- |
| `button` | `button` | If `true`, a button tag will be rendered and the item will be tappable. | `boolean` | `false` |
| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` |
| `counter` | `counter` | If `true`, a character counter will display the ratio of characters used and the total character limit. Only applies when the `maxlength` property is set on the inner `ion-input` or `ion-textarea`. | `boolean` | `false` |
| `detail` | `detail` | If `true`, a detail arrow will appear on the item. Defaults to `false` unless the `mode` is `ios` and an `href` or `button` property is present. | `boolean \| undefined` | `undefined` |
| `detailIcon` | `detail-icon` | The icon to use when `detail` is set to `true`. | `string` | `chevronForward` |
| `disabled` | `disabled` | If `true`, the user cannot interact with the item. | `boolean` | `false` |
| `download` | `download` | This attribute instructs browsers to download a URL instead of navigating to it, so the user will be prompted to save it as a local file. If the attribute has a value, it is used as the pre-filled file name in the Save prompt (the user can still change the file name if they want). | `string \| undefined` | `undefined` |
| `fill` | `fill` | The fill for the item. If `'solid'` the item will have a background. If `'outline'` the item will be transparent with a border. Only available in `md` mode. | `"outline" \| "solid" \| undefined` | `undefined` |
| `href` | `href` | Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered. | `string \| undefined` | `undefined` |
| `lines` | `lines` | How the bottom border should be displayed on the item. | `"full" \| "inset" \| "none" \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `rel` | `rel` | Specifies the relationship of the target object to the link object. The value is a space-separated list of [link types](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types). | `string \| undefined` | `undefined` |
| `routerAnimation` | -- | When using a router, it specifies the transition animation when navigating to another page using `href`. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` |
| `routerDirection` | `router-direction` | When using a router, it specifies the transition direction when navigating to another page using `href`. | `"back" \| "forward" \| "root"` | `'forward'` |
| `shape` | `shape` | The shape of the item. If "round" it will have increased border radius. | `"round" \| undefined` | `undefined` |
| `target` | `target` | Specifies where to display the linked URL. Only applies when an `href` is provided. Special keywords: `"_blank"`, `"_self"`, `"_parent"`, `"_top"`. | `string \| undefined` | `undefined` |
| `type` | `type` | The type of the button. Only used when an `onclick` or `button` property is present. | `"button" \| "reset" \| "submit"` | `'button'` |
## Slots

View File

@@ -120,6 +120,10 @@
<ion-item class="overflow-scroll">
<ion-label class="ion-text-wrap">Item with overflow scroll. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Mattis molestie a iaculis at erat pellentesque adipiscing commodo. Vulputate enim nulla aliquet porttitor. Fermentum dui faucibus in ornare quam viverra orci sagittis eu. Faucibus scelerisque eleifend donec pretium vulputate sapien nec. Cras semper auctor neque vitae. Cursus eget nunc scelerisque viverra mauris in aliquam. Non sodales neque sodales ut etiam sit amet. Sit amet nulla facilisi morbi tempus. Accumsan in nisl nisi scelerisque eu. Sed elementum tempus egestas sed sed. Urna nunc id cursus metus aliquam. Gravida dictum fusce ut placerat orci nulla pellentesque. Id diam maecenas ultricies mi eget. Elementum nisi quis eleifend quam adipiscing vitae proin.</ion-label>
</ion-item>
<ion-item detail>
<ion-label>Item with details.</ion-label>
</ion-item>
</ion-content>
<ion-footer>

View File

@@ -0,0 +1,19 @@
import { newE2EPage } from '@stencil/core/testing';
test('item: counter', async () => {
const page = await newE2EPage({
url: '/src/components/item/test/counter?ionic:_testing=true'
});
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});
test('item: counter-rtl', async () => {
const page = await newE2EPage({
url: '/src/components/item/test/counter?ionic:_testing=true&rtl=true'
});
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Item - Counter</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Item counter</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-vertical">
<ion-list>
<ion-item counter="true">
<ion-label>Counter</ion-label>
<ion-input maxlength="20"></ion-input>
</ion-item>
<ion-item counter="true">
<ion-label>Counter with value</ion-label>
<ion-input maxlength="20" value="some value"></ion-input>
</ion-item>
</ion-list>
</ion-content>
</ion-app>
</html>

View File

@@ -1,10 +1,11 @@
import { Component, ComponentInterface, Element, Host, Listen, Prop, State, h } from '@stencil/core';
import { menuOutline, menuSharp } from 'ionicons/icons';
import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global';
import { Color } from '../../interface';
import { ButtonInterface } from '../../utils/element-interface';
import { inheritAttributes } from '../../utils/helpers';
import { Attributes, inheritAttributes } from '../../utils/helpers';
import { menuController } from '../../utils/menu-controller';
import { createColorClasses, hostContext } from '../../utils/theme';
import { updateVisibility } from '../menu-toggle/menu-toggle-util';
@@ -24,7 +25,7 @@ import { updateVisibility } from '../menu-toggle/menu-toggle-util';
shadow: true
})
export class MenuButton implements ComponentInterface, ButtonInterface {
private inheritedAttributes: { [k: string]: any } = {};
private inheritedAttributes: Attributes = {};
@Element() el!: HTMLIonSegmentElement;
@@ -78,7 +79,7 @@ export class MenuButton implements ComponentInterface, ButtonInterface {
render() {
const { color, disabled, inheritedAttributes } = this;
const mode = getIonMode(this);
const menuIcon = config.get('menuIcon', mode === 'ios' ? 'menu-outline' : 'menu-sharp');
const menuIcon = config.get('menuIcon', mode === 'ios' ? menuOutline : menuSharp);
const hidden = this.autoHide && !this.visible;
const attrs = {

View File

@@ -69,11 +69,11 @@ In case it's desired to keep `ion-menu-toggle` always visible, the `autoHide` pr
```tsx
import React from 'react';
import { IonMenu, IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem, IonMenuToggle, IonButton } from '@ionic/react';
import { IonMenu, IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem, IonMenuToggle, IonButton, IonPage } from '@ionic/react';
export const MenuExample: React.FC = () => (
<>
<IonMenu side="start" menuId="first">
<IonMenu side="start" menuId="first" contentId="main">
<IonHeader>
<IonToolbar color="primary">
<IonTitle>Example Menu</IonTitle>
@@ -85,11 +85,13 @@ export const MenuExample: React.FC = () => (
</IonList>
</IonContent>
</IonMenu>
<IonContent>
<IonMenuToggle>
<IonButton>Toggle Menu</IonButton>
</IonMenuToggle>
</IonContent>
<IonPage id="main">
<IonContent>
<IonMenuToggle>
<IonButton>Toggle Menu</IonButton>
</IonMenuToggle>
</IonContent>
</IonPage>
</>
);
```
@@ -131,8 +133,7 @@ import {
IonMenuToggle,
IonButton,
IonTitle,
IonToolbar,
menuController
IonToolbar
} from '@ionic/vue';
import { defineComponent } from 'vue';

View File

@@ -5,8 +5,9 @@ import { getIonMode } from '../../global/ionic-global';
import { Animation, Gesture, GestureDetail, MenuChangeEventDetail, MenuI, Side } from '../../interface';
import { getTimeGivenProgression } from '../../utils/animation/cubic-bezier';
import { GESTURE_CONTROLLER } from '../../utils/gesture';
import { assert, clamp, inheritAttributes, isEndSide as isEnd } from '../../utils/helpers';
import { Attributes, assert, clamp, inheritAttributes, isEndSide as isEnd } from '../../utils/helpers';
import { menuController } from '../../utils/menu-controller';
import { getOverlay } from '../../utils/overlays';
const iosEasing = 'cubic-bezier(0.32,0.72,0,1)';
const mdEasing = 'cubic-bezier(0.0,0.0,0.2,1)';
@@ -42,9 +43,23 @@ export class Menu implements ComponentInterface, MenuI {
contentEl?: HTMLElement;
lastFocus?: HTMLElement;
private inheritedAttributes: { [k: string]: any } = {};
private inheritedAttributes: Attributes = {};
private handleFocus = (ev: Event) => this.trapKeyboardFocus(ev, document);
private handleFocus = (ev: FocusEvent) => {
/**
* Overlays have their own focus trapping listener
* so we do not want the two listeners to conflict
* with each other. If the top-most overlay that is
* open does not contain this ion-menu, then ion-menu's
* focus trapping should not run.
*/
const lastOverlay = getOverlay(document);
if (lastOverlay && !lastOverlay.contains(this.el)) {
return;
}
this.trapKeyboardFocus(ev, document);
}
@Element() el!: HTMLIonMenuElement;

View File

@@ -0,0 +1,59 @@
import { newE2EPage } from '@stencil/core/testing';
const getActiveElementID = async (page) => {
const activeElement = await page.evaluateHandle(() => document.activeElement);
return await page.evaluate(el => el && el.id, activeElement);
}
test('menu: focus trap with overlays', async () => {
const page = await newE2EPage({
url: '/src/components/menu/test/focus-trap?ionic:_testing=true'
});
const ionDidOpen = await page.spyOnEvent('ionDidOpen');
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const ionModalDidDismiss= await page.spyOnEvent('ionModalDidDismiss');
const menu = await page.find('ion-menu');
await menu.callMethod('open');
await ionDidOpen.next();
expect(await getActiveElementID(page)).toEqual('open-modal-button');
const openModal = await page.find('#open-modal-button');
await openModal.click();
await ionModalDidPresent.next();
expect(await getActiveElementID(page)).toEqual('modal-element');
const modal = await page.find('ion-modal');
await modal.callMethod('dismiss');
await ionModalDidDismiss.next();
expect(await getActiveElementID(page)).toEqual('open-modal-button');
});
test('menu: focus trap with content inside overlays', async () => {
const page = await newE2EPage({
url: '/src/components/menu/test/focus-trap?ionic:_testing=true'
});
const ionDidOpen = await page.spyOnEvent('ionDidOpen');
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
const ionModalDidDismiss= await page.spyOnEvent('ionModalDidDismiss');
const menu = await page.find('ion-menu');
await menu.callMethod('open');
await ionDidOpen.next();
expect(await getActiveElementID(page)).toEqual('open-modal-button');
const openModal = await page.find('#open-modal-button');
await openModal.click();
await ionModalDidPresent.next();
const button = await page.find('#other-button');
await button.click();
expect(await getActiveElementID(page)).toEqual('other-button');
});

View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Menu - Focus Trap</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<ion-menu content-id="main">
<ion-header>
<ion-toolbar>
<ion-title>Menu</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-item>
<ion-button id="open-modal-button">Open Modal</ion-button>
</ion-item>
<ion-modal id="modal-element" trigger="open-modal-button">
<ion-content class="ion-padding">
Modal content
<ion-button onclick="dismissModal()">Dismiss Modal</ion-button>
<ion-button id="other-button">Other Button</ion-button>
</ion-content>
</ion-modal>
</ion-content>
</ion-menu>
<div class="ion-page" id="main">
<ion-header>
<ion-toolbar>
<ion-title>Menu - Basic</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-button onclick="openMenu()" id="open-menu-button">Open Menu</ion-button>
</ion-content>
</div>
</ion-app>
<script>
const menu = document.querySelector('ion-menu');
const modal = document.querySelector('ion-modal');
const openMenu = () => {
menu.open();
}
const dismissModal = () => {
modal.dismiss();
}
</script>
</body>
</html>

View File

@@ -7,7 +7,11 @@ import { createSheetEnterAnimation } from './sheet';
const createEnterAnimation = () => {
const backdropAnimation = createAnimation()
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)');
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
.beforeStyles({
'pointer-events': 'none'
})
.afterClearStyles(['pointer-events']);
const wrapperAnimation = createAnimation()
.fromTo('transform', 'translateY(100vh)', 'translateY(0vh)');
@@ -27,11 +31,7 @@ export const iosEnterAnimation = (
const { wrapperAnimation, backdropAnimation } = currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation();
backdropAnimation
.addElement(root.querySelector('ion-backdrop')!)
.beforeStyles({
'pointer-events': 'none'
})
.afterClearStyles(['pointer-events']);
.addElement(root.querySelector('ion-backdrop')!);
wrapperAnimation
.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')!)

View File

@@ -6,7 +6,11 @@ import { createSheetEnterAnimation } from './sheet';
const createEnterAnimation = () => {
const backdropAnimation = createAnimation()
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)');
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
.beforeStyles({
'pointer-events': 'none'
})
.afterClearStyles(['pointer-events']);
const wrapperAnimation = createAnimation()
.keyframes([
@@ -29,11 +33,7 @@ export const mdEnterAnimation = (
const { wrapperAnimation, backdropAnimation } = currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation();
backdropAnimation
.addElement(root.querySelector('ion-backdrop')!)
.beforeStyles({
'pointer-events': 'none'
})
.afterClearStyles(['pointer-events']);
.addElement(root.querySelector('ion-backdrop')!);
wrapperAnimation
.addElement(root.querySelector('.modal-wrapper')!);

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