mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da4f750f8d | ||
|
|
34a6ce6d7e | ||
|
|
6ee7d159ec | ||
|
|
59bbd52e35 | ||
|
|
596aad435b | ||
|
|
c6381ce4f9 | ||
|
|
4ff9524e10 | ||
|
|
721a461073 | ||
|
|
8c22646d66 | ||
|
|
d40c0c3a09 | ||
|
|
231d6df622 | ||
|
|
aab4d306f8 | ||
|
|
df84d155ea | ||
|
|
f5c5c3cffa | ||
|
|
34cae57acc | ||
|
|
2a27befe46 | ||
|
|
897ae4a454 | ||
|
|
b0c9f097d2 | ||
|
|
928c5fbfcb | ||
|
|
0b18260da6 | ||
|
|
bf9b4dfb4e | ||
|
|
7530143634 | ||
|
|
b40fc4632e | ||
|
|
6d4a07d05c | ||
|
|
43aa6c11f4 | ||
|
|
484de5074d | ||
|
|
bdb5c421d2 | ||
|
|
6d7b1444b6 | ||
|
|
1f918835f4 | ||
|
|
a34ab420e3 | ||
|
|
43b19cf536 | ||
|
|
378c632643 | ||
|
|
7d5c6afd18 | ||
|
|
6f66d08ba8 | ||
|
|
525f01f086 | ||
|
|
632dafcd57 | ||
|
|
88ce010418 | ||
|
|
90a9a9c3e8 | ||
|
|
fde35a361f | ||
|
|
034d049209 | ||
|
|
94d033c421 | ||
|
|
d3311df967 | ||
|
|
43c5977d48 | ||
|
|
5925e7608e | ||
|
|
f295134624 | ||
|
|
af0135ce7d | ||
|
|
360643d96a | ||
|
|
35e5235645 | ||
|
|
2940e73a45 | ||
|
|
3e2d04dcc6 | ||
|
|
353dbc0537 | ||
|
|
5dba4e5ce0 | ||
|
|
bbbe778a8a | ||
|
|
29f1140384 | ||
|
|
54db1a1e7c | ||
|
|
eb7905cac9 | ||
|
|
5e448d9c74 | ||
|
|
be022f7de8 | ||
|
|
1a9be747f2 | ||
|
|
af01a8b307 | ||
|
|
e00c9cbd4c | ||
|
|
e284d7a2c7 | ||
|
|
c8a392aef5 | ||
|
|
273ae2cc08 | ||
|
|
9a15753fd9 | ||
|
|
a753d3438a | ||
|
|
7704ac3a37 | ||
|
|
8c442059b5 | ||
|
|
3d20959221 | ||
|
|
88602a9acf | ||
|
|
f5b4382fd5 | ||
|
|
c4745d24ac | ||
|
|
bce849c5f3 | ||
|
|
bb9e5f68b4 |
13
.github/CONTRIBUTING.md
vendored
13
.github/CONTRIBUTING.md
vendored
@@ -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
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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
|
||||
|
||||
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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 Number: #
|
||||
|
||||
|
||||
## What is the new behavior?
|
||||
|
||||
@@ -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
|
||||
|
||||
4
.github/workflows/dev-build.yml
vendored
4
.github/workflows/dev-build.yml
vendored
@@ -25,11 +25,11 @@ jobs:
|
||||
run: |
|
||||
echo "HASH=$(git log -1 --format=%H | cut -c 1-7)" >> $GITHUB_ENV
|
||||
echo "TIMESTAMP=$(date +%s)" >> $GITHUB_ENV
|
||||
echo "CURRENT_VERSION=$(node -p -e "require('./core/package.json').version")" >> $GITHUB_ENV
|
||||
echo "CURRENT_VERSION=$(node ./.scripts/bump-version.js)" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
- name: Create Dev Build
|
||||
run: |
|
||||
HUSKY_SKIP_HOOKS=1 lerna publish $(echo "${{ env.CURRENT_VERSION }}")-dev.$(echo "${{ env.TIMESTAMP }}").$(echo "${{ env.HASH }}") --no-verify-access --yes --force-publish='*' --dist-tag dev --no-git-tag-version --no-push
|
||||
HUSKY_SKIP_HOOKS=1 lerna publish $(echo "${{ env.CURRENT_VERSION }}")-dev.$(echo "${{ env.TIMESTAMP }}").$(echo "${{ env.HASH }}") --no-verify-access --yes --force-publish='*' --dist-tag dev --no-git-tag-version --no-push --exact
|
||||
shell: bash
|
||||
- id: dev-build
|
||||
run: echo "::set-output name=version::$(echo "${{ env.CURRENT_VERSION }}")-dev.$(echo "${{ env.TIMESTAMP }}").$(echo "${{ env.HASH }}")"
|
||||
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
10
.scripts/bump-version.js
Normal file
10
.scripts/bump-version.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const semver = require('semver');
|
||||
|
||||
const getDevVersion = () => {
|
||||
const originalVersion = require('../lerna.json').version;
|
||||
const baseVersion = semver.inc(originalVersion, 'patch');
|
||||
|
||||
return baseVersion;
|
||||
}
|
||||
|
||||
console.log(getDevVersion());
|
||||
@@ -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
|
||||
};
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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`);
|
||||
90
CHANGELOG.md
90
CHANGELOG.md
@@ -3,6 +3,96 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **angular-server:** use correct @ionic/angular dependency version ([#24593](https://github.com/ionic-team/ionic-framework/issues/24593)) ([be022f7](https://github.com/ionic-team/ionic-framework/commit/be022f7de8df85ae842b0e111722b03448d60387)), closes [#24592](https://github.com/ionic-team/ionic-framework/issues/24592)
|
||||
* **angular:** apply touch, dirty and pristine form control classes ([#24558](https://github.com/ionic-team/ionic-framework/issues/24558)) ([273ae2c](https://github.com/ionic-team/ionic-framework/commit/273ae2cc087b2a5a30fb50a1b0eaeb0a221900fc)), closes [#24483](https://github.com/ionic-team/ionic-framework/issues/24483)
|
||||
* **datetime:** showing calendar grid no longer causes month to switch on ios 15 ([#24554](https://github.com/ionic-team/ionic-framework/issues/24554)) ([3d20959](https://github.com/ionic-team/ionic-framework/commit/3d2095922147ea3763e977412977edd9586fec5d)), closes [#24405](https://github.com/ionic-team/ionic-framework/issues/24405)
|
||||
* **item:** error slot visible in Safari ([#24579](https://github.com/ionic-team/ionic-framework/issues/24579)) ([af01a8b](https://github.com/ionic-team/ionic-framework/commit/af01a8b3073dce784cc042923d712b9492638d32)), closes [#24575](https://github.com/ionic-team/ionic-framework/issues/24575)
|
||||
* **menu:** remove main attribute that was supposed to removed in v5 ([#24565](https://github.com/ionic-team/ionic-framework/issues/24565)) ([7704ac3](https://github.com/ionic-team/ionic-framework/commit/7704ac3a3710396248590daecb945b76825a0539)), closes [#24563](https://github.com/ionic-team/ionic-framework/issues/24563)
|
||||
* **modal:** life cycle events for controller modals ([#24508](https://github.com/ionic-team/ionic-framework/issues/24508)) ([9a15753](https://github.com/ionic-team/ionic-framework/commit/9a15753fd95e32155abdeb490ec57cb72385ad1a)), closes [#24460](https://github.com/ionic-team/ionic-framework/issues/24460)
|
||||
* **overlays:** getTop now returns the top-most presented overlay ([#24547](https://github.com/ionic-team/ionic-framework/issues/24547)) ([f5b4382](https://github.com/ionic-team/ionic-framework/commit/f5b4382fd5728365e4badf39bc1dd0c149b45c2c)), closes [#19111](https://github.com/ionic-team/ionic-framework/issues/19111)
|
||||
* **react:** add useRef wrapper to useIonOverlay state to avoid stale references ([#24553](https://github.com/ionic-team/ionic-framework/issues/24553)) ([bce849c](https://github.com/ionic-team/ionic-framework/commit/bce849c5f324522002eff7f8a5e5023150e9201c))
|
||||
* **react:** prevent errors when dismissing inline popover after containing element is removed ([#24569](https://github.com/ionic-team/ionic-framework/issues/24569)) ([c8a392a](https://github.com/ionic-team/ionic-framework/commit/c8a392aef5fbf25f59a573897d970c41abac04d2))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [6.0.2](https://github.com/ionic-team/ionic-framework/compare/v6.0.1...v6.0.2) (2022-01-11)
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,44 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **angular:** apply touch, dirty and pristine form control classes ([#24558](https://github.com/ionic-team/ionic/issues/24558)) ([273ae2c](https://github.com/ionic-team/ionic/commit/273ae2cc087b2a5a30fb50a1b0eaeb0a221900fc)), closes [#24483](https://github.com/ionic-team/ionic/issues/24483)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [6.0.2](https://github.com/ionic-team/ionic/compare/v6.0.1...v6.0.2) (2022-01-11)
|
||||
|
||||
|
||||
|
||||
2
angular/package-lock.json
generated
2
angular/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/angular",
|
||||
"version": "6.0.2",
|
||||
"version": "6.0.6",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/angular",
|
||||
"version": "6.0.2",
|
||||
"version": "6.0.6",
|
||||
"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.2",
|
||||
"@ionic/core": "^6.0.6",
|
||||
"jsonc-parser": "^3.0.0",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
|
||||
@@ -96,7 +96,7 @@ export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDes
|
||||
if (formControl) {
|
||||
const methodsToPatch = ['markAsTouched', 'markAllAsTouched', 'markAsUntouched', 'markAsDirty', 'markAsPristine'];
|
||||
methodsToPatch.forEach((method) => {
|
||||
if (formControl.get(method)) {
|
||||
if (typeof formControl[method] !== 'undefined') {
|
||||
const oldFn = formControl[method].bind(formControl);
|
||||
formControl[method] = (...params: any[]) => {
|
||||
oldFn(...params);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
1
angular/test/test-app/.gitignore
vendored
1
angular/test/test-app/.gitignore
vendored
@@ -29,6 +29,7 @@ speed-measure-plugin.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
/.angular/cache
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
|
||||
@@ -8,6 +8,16 @@ describe('Form', () => {
|
||||
cy.get('#input-touched').click();
|
||||
cy.get('#touched-input-test').should('have.class', 'ion-touched');
|
||||
});
|
||||
|
||||
describe('markAllAsTouched', () => {
|
||||
it('should apply .ion-touched to nearest ion-item', () => {
|
||||
cy.get('#mark-all-touched-button').click();
|
||||
cy.get('form ion-item').each(item => {
|
||||
cy.wrap(item).should('have.class', 'ion-touched');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('change', () => {
|
||||
|
||||
14992
angular/test/test-app/package-lock.json
generated
14992
angular/test/test-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,12 +4,12 @@
|
||||
"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",
|
||||
@@ -20,15 +20,15 @@
|
||||
"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"
|
||||
|
||||
@@ -65,10 +65,6 @@ import { AlertComponent } from './alert/alert.component';
|
||||
ReactiveFormsModule,
|
||||
IonicModule.forRoot({ keyboardHeight: 12345 }),
|
||||
],
|
||||
entryComponents: [
|
||||
ModalExampleComponent,
|
||||
NavComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
|
||||
],
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
|
||||
<ion-item>
|
||||
<ion-label>DateTime</ion-label>
|
||||
<ion-datetime formControlName="datetime" min="1994-03-14" max="2017-12-09" display-format="MM/DD/YYYY"></ion-datetime>
|
||||
<ion-datetime formControlName="datetime" min="1994-03-14" max="2017-12-09" display-format="MM/DD/YYYY">
|
||||
</ion-datetime>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
@@ -65,6 +66,7 @@
|
||||
<p>
|
||||
Form Submit: <span id="submit">{{submitted}}</span>
|
||||
</p>
|
||||
<ion-button id="mark-all-touched-button" (click)="markAllAsTouched()">Mark all as touched</ion-button>
|
||||
<ion-button id="submit-button" type="submit" [disabled]="!profileForm.valid">Submit</ion-button>
|
||||
|
||||
</form>
|
||||
|
||||
@@ -46,4 +46,8 @@ export class FormComponent {
|
||||
});
|
||||
}
|
||||
|
||||
markAllAsTouched() {
|
||||
this.profileForm.markAllAsTouched();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,84 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **datetime:** showing calendar grid no longer causes month to switch on ios 15 ([#24554](https://github.com/ionic-team/ionic/issues/24554)) ([3d20959](https://github.com/ionic-team/ionic/commit/3d2095922147ea3763e977412977edd9586fec5d)), closes [#24405](https://github.com/ionic-team/ionic/issues/24405)
|
||||
* **item:** error slot visible in Safari ([#24579](https://github.com/ionic-team/ionic/issues/24579)) ([af01a8b](https://github.com/ionic-team/ionic/commit/af01a8b3073dce784cc042923d712b9492638d32)), closes [#24575](https://github.com/ionic-team/ionic/issues/24575)
|
||||
* **menu:** remove main attribute that was supposed to removed in v5 ([#24565](https://github.com/ionic-team/ionic/issues/24565)) ([7704ac3](https://github.com/ionic-team/ionic/commit/7704ac3a3710396248590daecb945b76825a0539)), closes [#24563](https://github.com/ionic-team/ionic/issues/24563)
|
||||
* **modal:** life cycle events for controller modals ([#24508](https://github.com/ionic-team/ionic/issues/24508)) ([9a15753](https://github.com/ionic-team/ionic/commit/9a15753fd95e32155abdeb490ec57cb72385ad1a)), closes [#24460](https://github.com/ionic-team/ionic/issues/24460)
|
||||
* **overlays:** getTop now returns the top-most presented overlay ([#24547](https://github.com/ionic-team/ionic/issues/24547)) ([f5b4382](https://github.com/ionic-team/ionic/commit/f5b4382fd5728365e4badf39bc1dd0c149b45c2c)), closes [#19111](https://github.com/ionic-team/ionic/issues/19111)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [6.0.2](https://github.com/ionic-team/ionic/compare/v6.0.1...v6.0.2) (2022-01-11)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
14
core/api.txt
14
core/api.txt
@@ -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
|
||||
@@ -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
|
||||
@@ -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
18
core/package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "6.0.2",
|
||||
"version": "6.0.6",
|
||||
"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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "6.0.2",
|
||||
"version": "6.0.6",
|
||||
"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"
|
||||
},
|
||||
|
||||
37
core/src/components.d.ts
vendored
37
core/src/components.d.ts
vendored
@@ -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.
|
||||
*/
|
||||
@@ -1081,7 +1081,7 @@ export namespace Components {
|
||||
*/
|
||||
"pattern"?: string;
|
||||
/**
|
||||
* Instructional text that shows before the input has a value.
|
||||
* Instructional text that shows before the input has a value. This property applies only when the `type` property is set to `"email"`, `"number"`, `"password"`, `"search"`, `"tel"`, `"text"`, or `"url"`, otherwise it is ignored.
|
||||
*/
|
||||
"placeholder"?: string;
|
||||
/**
|
||||
@@ -1823,6 +1823,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.
|
||||
*/
|
||||
@@ -1918,7 +1919,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 +2197,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.
|
||||
*/
|
||||
@@ -2271,7 +2272,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 +2280,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 +2316,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;
|
||||
/**
|
||||
@@ -2722,7 +2723,7 @@ export namespace Components {
|
||||
*/
|
||||
"autoGrow": boolean;
|
||||
/**
|
||||
* Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user.
|
||||
* Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user. Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`.
|
||||
*/
|
||||
"autocapitalize": string;
|
||||
/**
|
||||
@@ -4752,7 +4753,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 +4761,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.
|
||||
*/
|
||||
@@ -4802,7 +4803,7 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"pattern"?: string;
|
||||
/**
|
||||
* Instructional text that shows before the input has a value.
|
||||
* Instructional text that shows before the input has a value. This property applies only when the `type` property is set to `"email"`, `"number"`, `"password"`, `"search"`, `"tel"`, `"text"`, or `"url"`, otherwise it is ignored.
|
||||
*/
|
||||
"placeholder"?: string;
|
||||
/**
|
||||
@@ -5942,7 +5943,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 +5951,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 +6011,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;
|
||||
/**
|
||||
@@ -6430,7 +6431,7 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"autoGrow"?: boolean;
|
||||
/**
|
||||
* Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user.
|
||||
* Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user. Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`.
|
||||
*/
|
||||
"autocapitalize"?: string;
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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++}`` |
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
// ---------------------------------------------------
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -79,24 +79,49 @@
|
||||
* unhiding the calendar content.
|
||||
* To workaround this, we set the opacity
|
||||
* of the content to 0 and hide it offscreen.
|
||||
* TODO: This is fixed in Safari 15+, so remove
|
||||
* when Safari 14 support is dropped.
|
||||
*
|
||||
* -webkit-named-image is something only WebKit supports
|
||||
* so we use this to detect general WebKit support.
|
||||
* aspect-ratio is only supported in Safari 15+
|
||||
* so by checking lack of aspect-ratio support, we know
|
||||
* that we are in a pre-Safari 15 browser.
|
||||
*
|
||||
* TODO(FW-554): Remove when iOS 14 support is dropped.
|
||||
*/
|
||||
:host(.show-month-and-year) .calendar-next-prev,
|
||||
:host(.show-month-and-year) .calendar-days-of-week,
|
||||
:host(.show-month-and-year) .calendar-body,
|
||||
:host(.show-month-and-year) .datetime-time {
|
||||
@include position(null, null, null, -99999px);
|
||||
@supports (background: -webkit-named-image(apple-pay-logo-black)) and (not (aspect-ratio: 1/1)) {
|
||||
:host(.show-month-and-year) .calendar-next-prev,
|
||||
:host(.show-month-and-year) .calendar-days-of-week,
|
||||
:host(.show-month-and-year) .calendar-body,
|
||||
:host(.show-month-and-year) .datetime-time {
|
||||
@include position(null, null, null, -99999px);
|
||||
|
||||
position: absolute;
|
||||
position: absolute;
|
||||
|
||||
/**
|
||||
* Use visibility instead of
|
||||
* opacity to ensure element
|
||||
* cannot receive focus
|
||||
*/
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
/**
|
||||
* Use visibility instead of
|
||||
* opacity to ensure element
|
||||
* cannot receive focus
|
||||
*/
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This support check two cases:
|
||||
* 1. A WebKit browser that supports aspect-ratio (Safari 15+)
|
||||
* 2. Any non-WebKit browser.
|
||||
* Note that just overriding this display: none is not
|
||||
* sufficient to resolve the issue mentioned above, which
|
||||
* is why we do another set of @supports checks.
|
||||
*/
|
||||
@supports (not (background: -webkit-named-image(apple-pay-logo-black))) or ((background: -webkit-named-image(apple-pay-logo-black)) and (aspect-ratio: 1/1)) {
|
||||
:host(.show-month-and-year) .calendar-next-prev,
|
||||
:host(.show-month-and-year) .calendar-days-of-week,
|
||||
:host(.show-month-and-year) .calendar-body,
|
||||
:host(.show-month-and-year) .datetime-time {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:host(.datetime-readonly),
|
||||
@@ -216,6 +241,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
|
||||
*/
|
||||
|
||||
@@ -56,7 +56,10 @@ import {
|
||||
} from './utils/parse';
|
||||
import {
|
||||
getCalendarDayState,
|
||||
isDayDisabled
|
||||
isDayDisabled,
|
||||
isMonthDisabled,
|
||||
isNextMonthDisabled,
|
||||
isPrevMonthDisabled
|
||||
} from './utils/state';
|
||||
|
||||
/**
|
||||
@@ -714,6 +717,15 @@ export class Datetime implements ComponentInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
const { month, year, day } = refMonthFn(this.workingParts);
|
||||
|
||||
if (isMonthDisabled({ month, year, day: null }, {
|
||||
minParts: this.minParts,
|
||||
maxParts: this.maxParts
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* On iOS, we need to set pointer-events: none
|
||||
* when the user is almost done with the gesture
|
||||
@@ -724,7 +736,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 +770,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 +804,12 @@ export class Datetime implements ComponentInterface {
|
||||
});
|
||||
}
|
||||
|
||||
const threshold = mode === 'ios' &&
|
||||
// tslint:disable-next-line
|
||||
typeof navigator !== 'undefined' &&
|
||||
navigator.maxTouchPoints > 1 ?
|
||||
[0.7, 1] : 1;
|
||||
|
||||
/**
|
||||
* Listen on the first month to
|
||||
* prepend a new month and on the last
|
||||
@@ -800,13 +829,13 @@ export class Datetime implements ComponentInterface {
|
||||
* something WebKit does.
|
||||
*/
|
||||
endIO = new IntersectionObserver(ev => ioCallback('end', ev), {
|
||||
threshold: mode === 'ios' ? [0.7, 1] : 1,
|
||||
threshold,
|
||||
root: calendarBodyRef
|
||||
});
|
||||
endIO.observe(endMonth);
|
||||
|
||||
startIO = new IntersectionObserver(ev => ioCallback('start', ev), {
|
||||
threshold: mode === 'ios' ? [0.7, 1] : 1,
|
||||
threshold,
|
||||
root: calendarBodyRef
|
||||
});
|
||||
startIO.observe(startMonth);
|
||||
@@ -963,9 +992,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);
|
||||
@@ -1091,6 +1120,13 @@ export class Datetime implements ComponentInterface {
|
||||
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 +1139,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>
|
||||
@@ -1114,6 +1154,13 @@ export class Datetime implements ComponentInterface {
|
||||
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 +1173,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 +1190,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 +1207,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 +1232,26 @@ 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
|
||||
}, {
|
||||
minParts: this.minParts,
|
||||
maxParts: this.maxParts
|
||||
});
|
||||
// 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 +1266,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 +1421,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 +1442,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'
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ion-datetime
|
||||
|
||||
Datetimes present a calendar interface and time wheel, making it easy for users to select dates and times. Datetimes are similar to the native `input` elements of `datetime-local`, however, Ionic Framework's Datetime component makes it easy to display the date and time in the a preferred format, and manage the datetime values.
|
||||
Datetimes present a calendar interface and time wheel, making it easy for users to select dates and times. Datetimes are similar to the native `input` elements of `datetime-local`, however, Ionic Framework's Datetime component makes it easy to display the date and time in the preferred format, and manage the datetime values.
|
||||
|
||||
### Datetime Data
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('generateTime()', () => {
|
||||
day: 19,
|
||||
month: 5,
|
||||
year: 2021,
|
||||
hour: 5,
|
||||
hour: 7,
|
||||
minute: 43
|
||||
}
|
||||
const max = {
|
||||
@@ -257,5 +257,80 @@ 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);
|
||||
});
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,30 @@ 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');
|
||||
|
||||
});
|
||||
|
||||
@@ -1,89 +1,77 @@
|
||||
<!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>
|
||||
</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>
|
||||
|
||||
27
core/src/components/datetime/test/position/e2e.ts
Normal file
27
core/src/components/datetime/test/position/e2e.ts
Normal 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();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
51
core/src/components/datetime/test/position/index.html
Normal file
51
core/src/components/datetime/test/position/index.html
Normal 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>
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -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!);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -22,6 +22,7 @@ export class Input implements ComponentInterface {
|
||||
private inputId = `ion-input-${inputIds++}`;
|
||||
private didBlurAfterEdit = false;
|
||||
private inheritedAttributes: { [k: string]: any } = {};
|
||||
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.
|
||||
@@ -151,6 +152,8 @@ export class Input implements ComponentInterface {
|
||||
|
||||
/**
|
||||
* Instructional text that shows before the input has a value.
|
||||
* This property applies only when the `type` property is set to `"email"`,
|
||||
* `"number"`, `"password"`, `"search"`, `"tel"`, `"text"`, or `"url"`, otherwise it is ignored.
|
||||
*/
|
||||
@Prop() placeholder?: string;
|
||||
|
||||
@@ -229,6 +232,21 @@ export class Input implements ComponentInterface {
|
||||
*/
|
||||
@Watch('value')
|
||||
protected valueChanged() {
|
||||
if (this.nativeInput && !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.
|
||||
*/
|
||||
const { selectionStart, selectionEnd } = this.nativeInput;
|
||||
this.nativeInput.value = this.getValue();
|
||||
// TODO: FW-727 Remove this when we drop support for iOS 15.3
|
||||
// Set the cursor position back to where it was before the value change
|
||||
this.nativeInput.setSelectionRange(selectionStart, selectionEnd);
|
||||
}
|
||||
this.emitStyle();
|
||||
this.ionChange.emit({ value: this.value == null ? this.value : this.value.toString() });
|
||||
}
|
||||
@@ -247,12 +265,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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -351,6 +384,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); }
|
||||
}
|
||||
|
||||
@@ -332,15 +332,15 @@ 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` |
|
||||
| `name` | `name` | The name of the control, which is submitted with the form data. | `string` | `this.inputId` |
|
||||
| `pattern` | `pattern` | A regular expression that the value is checked against. The pattern must match the entire value, not just some subset. Use the title attribute to describe the pattern to help the user. This attribute applies when the value of the type attribute is `"text"`, `"search"`, `"tel"`, `"url"`, `"email"`, `"date"`, or `"password"`, otherwise it is ignored. When the type attribute is `"date"`, `pattern` will only be used in browsers that do not support the `"date"` input type natively. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date for more information. | `string \| undefined` | `undefined` |
|
||||
| `placeholder` | `placeholder` | Instructional text that shows before the input has a value. | `string \| undefined` | `undefined` |
|
||||
| `placeholder` | `placeholder` | Instructional text that shows before the input has a value. This property applies only when the `type` property is set to `"email"`, `"number"`, `"password"`, `"search"`, `"tel"`, `"text"`, or `"url"`, otherwise it is ignored. | `string \| undefined` | `undefined` |
|
||||
| `readonly` | `readonly` | If `true`, the user cannot modify the value. | `boolean` | `false` |
|
||||
| `required` | `required` | If `true`, the user must fill in a value before submitting a form. | `boolean` | `false` |
|
||||
| `size` | `size` | The initial size of the control. This value is in pixels unless the value of the type attribute is `"text"` or `"password"`, in which case it is an integer number of characters. This attribute applies only when the `type` attribute is set to `"text"`, `"search"`, `"tel"`, `"url"`, `"email"`, or `"password"`, otherwise it is ignored. | `number \| undefined` | `undefined` |
|
||||
|
||||
20
core/src/components/input/test/masking/e2e.ts
Normal file
20
core/src/components/input/test/masking/e2e.ts
Normal 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');
|
||||
|
||||
});
|
||||
40
core/src/components/input/test/masking/index.html
Normal file
40
core/src/components/input/test/masking/index.html
Normal 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>
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -433,6 +433,12 @@ button, a {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::slotted([slot="error"]) {
|
||||
display: none;
|
||||
|
||||
color: var(--highlight-color-invalid);
|
||||
}
|
||||
|
||||
:host(.item-interactive.ion-invalid) ::slotted([slot="error"]) {
|
||||
display: block;
|
||||
}
|
||||
@@ -527,13 +533,17 @@ ion-ripple-effect {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
::slotted([slot="error"]) {
|
||||
display: none;
|
||||
// Item Max Length Counter
|
||||
// --------------------------------------------------
|
||||
|
||||
color: var(--highlight-color-invalid);
|
||||
.item-counter {
|
||||
@include margin-horizontal(auto, null);
|
||||
|
||||
padding-inline-start: 16px;
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
// Item: Reduced Motion
|
||||
// --------------------------------------------------
|
||||
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
19
core/src/components/item/test/counter/e2e.ts
Normal file
19
core/src/components/item/test/counter/e2e.ts
Normal 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();
|
||||
});
|
||||
42
core/src/components/item/test/counter/index.html
Normal file
42
core/src/components/item/test/counter/index.html
Normal 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>
|
||||
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -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 = {
|
||||
|
||||
@@ -5,6 +5,9 @@ import { menuController } from '../../utils/menu-controller';
|
||||
|
||||
import { updateVisibility } from './menu-toggle-util';
|
||||
|
||||
/**
|
||||
* @slot - Content is placed inside the toggle to act as the click target.
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-menu-toggle',
|
||||
styleUrl: 'menu-toggle.scss',
|
||||
|
||||
@@ -9,6 +9,152 @@ In case it's desired to keep `ion-menu-toggle` always visible, the `autoHide` pr
|
||||
<!-- Auto Generated Below -->
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
### Angular
|
||||
|
||||
```html
|
||||
<ion-app>
|
||||
<ion-menu side="start" menuId="first" contentId="main">
|
||||
<ion-header>
|
||||
<ion-toolbar color="secondary">
|
||||
<ion-title>Example Menu</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-item>Menu Item</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-menu>
|
||||
<div class="ion-page" id="main">
|
||||
<ion-content class="ion-padding">
|
||||
<ion-menu-toggle>
|
||||
<ion-button>Toggle Menu</ion-button>
|
||||
</ion-menu-toggle>
|
||||
</ion-content>
|
||||
</div>
|
||||
</ion-app>
|
||||
```
|
||||
|
||||
|
||||
### Javascript
|
||||
|
||||
```html
|
||||
<ion-app>
|
||||
<ion-menu side="start" menu-id="first" content-id="main">
|
||||
<ion-header>
|
||||
<ion-toolbar color="secondary">
|
||||
<ion-title>Example Menu</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-item>Menu Item</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-menu>
|
||||
<div class="ion-page" id="main">
|
||||
<ion-content class="ion-padding">
|
||||
<ion-menu-toggle>
|
||||
<ion-button>Toggle Menu</ion-button>
|
||||
</ion-menu-toggle>
|
||||
</ion-content>
|
||||
</div>
|
||||
</ion-app>
|
||||
```
|
||||
|
||||
|
||||
### React
|
||||
|
||||
```tsx
|
||||
import React from '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" contentId="main">
|
||||
<IonHeader>
|
||||
<IonToolbar color="primary">
|
||||
<IonTitle>Example Menu</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent>
|
||||
<IonList>
|
||||
<IonItem>Menu Item</IonItem>
|
||||
</IonList>
|
||||
</IonContent>
|
||||
</IonMenu>
|
||||
<IonPage id="main">
|
||||
<IonContent>
|
||||
<IonMenuToggle>
|
||||
<IonButton>Toggle Menu</IonButton>
|
||||
</IonMenuToggle>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
### Vue
|
||||
|
||||
```html
|
||||
<template>
|
||||
<ion-menu side="start" menu-id="first" content-id="main">
|
||||
<ion-header>
|
||||
<ion-toolbar color="primary">
|
||||
<ion-title>Example Menu</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-item>Menu Item</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-menu>
|
||||
|
||||
<div class="ion-page" id="main">
|
||||
<ion-content>
|
||||
<ion-menu-toggle>
|
||||
<ion-button>Toggle Menu</ion-button>
|
||||
</ion-menu-toggle>
|
||||
</ion-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonItem,
|
||||
IonList,
|
||||
IonMenu,
|
||||
IonMenuToggle,
|
||||
IonButton,
|
||||
IonTitle,
|
||||
IonToolbar
|
||||
} from '@ionic/vue';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonItem,
|
||||
IonList,
|
||||
IonMenu,
|
||||
IonMenuToggle,
|
||||
IonButton,
|
||||
IonTitle,
|
||||
IonToolbar
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Attribute | Description | Type | Default |
|
||||
@@ -17,6 +163,13 @@ In case it's desired to keep `ion-menu-toggle` always visible, the `autoHide` pr
|
||||
| `menu` | `menu` | Optional property that maps to a Menu's `menuId` prop. Can also be `start` or `end` for the menu side. This is used to find the correct menu to toggle. If this property is not used, `ion-menu-toggle` will toggle the first menu that is active. | `string \| undefined` | `undefined` |
|
||||
|
||||
|
||||
## Slots
|
||||
|
||||
| Slot | Description |
|
||||
| ---- | --------------------------------------------------------------- |
|
||||
| | Content is placed inside the toggle to act as the click target. |
|
||||
|
||||
|
||||
----------------------------------------------
|
||||
|
||||
*Built with [StencilJS](https://stenciljs.com/)*
|
||||
|
||||
24
core/src/components/menu-toggle/usage/angular.md
Normal file
24
core/src/components/menu-toggle/usage/angular.md
Normal file
@@ -0,0 +1,24 @@
|
||||
```html
|
||||
<ion-app>
|
||||
<ion-menu side="start" menuId="first" contentId="main">
|
||||
<ion-header>
|
||||
<ion-toolbar color="secondary">
|
||||
<ion-title>Example Menu</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-item>Menu Item</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-menu>
|
||||
<div class="ion-page" id="main">
|
||||
<ion-content class="ion-padding">
|
||||
<ion-menu-toggle>
|
||||
<ion-button>Toggle Menu</ion-button>
|
||||
</ion-menu-toggle>
|
||||
</ion-content>
|
||||
</div>
|
||||
</ion-app>
|
||||
```
|
||||
|
||||
24
core/src/components/menu-toggle/usage/javascript.md
Normal file
24
core/src/components/menu-toggle/usage/javascript.md
Normal file
@@ -0,0 +1,24 @@
|
||||
```html
|
||||
<ion-app>
|
||||
<ion-menu side="start" menu-id="first" content-id="main">
|
||||
<ion-header>
|
||||
<ion-toolbar color="secondary">
|
||||
<ion-title>Example Menu</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-item>Menu Item</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-menu>
|
||||
<div class="ion-page" id="main">
|
||||
<ion-content class="ion-padding">
|
||||
<ion-menu-toggle>
|
||||
<ion-button>Toggle Menu</ion-button>
|
||||
</ion-menu-toggle>
|
||||
</ion-content>
|
||||
</div>
|
||||
</ion-app>
|
||||
```
|
||||
|
||||
28
core/src/components/menu-toggle/usage/react.md
Normal file
28
core/src/components/menu-toggle/usage/react.md
Normal file
@@ -0,0 +1,28 @@
|
||||
```tsx
|
||||
import React from '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" contentId="main">
|
||||
<IonHeader>
|
||||
<IonToolbar color="primary">
|
||||
<IonTitle>Example Menu</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent>
|
||||
<IonList>
|
||||
<IonItem>Menu Item</IonItem>
|
||||
</IonList>
|
||||
</IonContent>
|
||||
</IonMenu>
|
||||
<IonPage id="main">
|
||||
<IonContent>
|
||||
<IonMenuToggle>
|
||||
<IonButton>Toggle Menu</IonButton>
|
||||
</IonMenuToggle>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
</>
|
||||
);
|
||||
```
|
||||
53
core/src/components/menu-toggle/usage/vue.md
Normal file
53
core/src/components/menu-toggle/usage/vue.md
Normal file
@@ -0,0 +1,53 @@
|
||||
```html
|
||||
<template>
|
||||
<ion-menu side="start" menu-id="first" content-id="main">
|
||||
<ion-header>
|
||||
<ion-toolbar color="primary">
|
||||
<ion-title>Example Menu</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-item>Menu Item</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-menu>
|
||||
|
||||
<div class="ion-page" id="main">
|
||||
<ion-content>
|
||||
<ion-menu-toggle>
|
||||
<ion-button>Toggle Menu</ion-button>
|
||||
</ion-menu-toggle>
|
||||
</ion-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonItem,
|
||||
IonList,
|
||||
IonMenu,
|
||||
IonMenuToggle,
|
||||
IonButton,
|
||||
IonTitle,
|
||||
IonToolbar
|
||||
} from '@ionic/vue';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonItem,
|
||||
IonList,
|
||||
IonMenu,
|
||||
IonMenuToggle,
|
||||
IonButton,
|
||||
IonTitle,
|
||||
IonToolbar
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
@@ -7,6 +7,7 @@ import { getTimeGivenProgression } from '../../utils/animation/cubic-bezier';
|
||||
import { GESTURE_CONTROLLER } from '../../utils/gesture';
|
||||
import { 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)';
|
||||
@@ -44,7 +45,21 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
|
||||
private inheritedAttributes: { [k: string]: any } = {};
|
||||
|
||||
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;
|
||||
|
||||
@@ -169,26 +184,11 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = this.el;
|
||||
const parent = el.parentNode as any;
|
||||
|
||||
if (this.contentId === undefined) {
|
||||
console.warn(`[DEPRECATED][ion-menu] Using the [main] attribute is deprecated, please use the "contentId" property instead:
|
||||
BEFORE:
|
||||
<ion-menu>...</ion-menu>
|
||||
<div main>...</div>
|
||||
|
||||
AFTER:
|
||||
<ion-menu contentId="main-content"></ion-menu>
|
||||
<div id="main-content">...</div>
|
||||
`);
|
||||
}
|
||||
const content = this.contentId !== undefined
|
||||
? document.getElementById(this.contentId)
|
||||
: parent && parent.querySelector && parent.querySelector('[main]');
|
||||
: null;
|
||||
|
||||
if (!content || !content.tagName) {
|
||||
// requires content element
|
||||
if (content === null) {
|
||||
console.error('Menu: must have a "content" element to listen for drag events on.');
|
||||
return;
|
||||
}
|
||||
|
||||
59
core/src/components/menu/test/focus-trap/e2e.ts
Normal file
59
core/src/components/menu/test/focus-trap/e2e.ts
Normal 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');
|
||||
});
|
||||
56
core/src/components/menu/test/focus-trap/index.html
Normal file
56
core/src/components/menu/test/focus-trap/index.html
Normal 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>
|
||||
@@ -93,6 +93,14 @@ export const createSheetGesture = (
|
||||
contentEl.scrollY = false;
|
||||
}
|
||||
|
||||
raf(() => {
|
||||
/**
|
||||
* Dismisses the open keyboard when the sheet drag gesture is started.
|
||||
* Sets the focus onto the modal element.
|
||||
*/
|
||||
baseEl.focus();
|
||||
});
|
||||
|
||||
animation.progressStart(true, 1 - currentBreakpoint);
|
||||
};
|
||||
|
||||
@@ -188,11 +196,11 @@ export const createSheetGesture = (
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This must be a one time callback
|
||||
* otherwise a new callback will
|
||||
* be added every time onEnd runs.
|
||||
*/
|
||||
/**
|
||||
* This must be a one time callback
|
||||
* otherwise a new callback will
|
||||
* be added every time onEnd runs.
|
||||
*/
|
||||
}, { oneTimeCallback: true })
|
||||
.progressEnd(1, 0, 500);
|
||||
|
||||
|
||||
@@ -48,13 +48,6 @@
|
||||
outline: none;
|
||||
|
||||
contain: strict;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:host(.modal-interactive) .modal-wrapper,
|
||||
:host(.modal-interactive) ion-backdrop {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
:host(.overlay-hidden) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getIonMode } from '../../global/ionic-global';
|
||||
import { Animation, AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, Gesture, ModalAttributes, OverlayEventDetail, OverlayInterface } from '../../interface';
|
||||
import { CoreDelegate, attachComponent, detachComponent } from '../../utils/framework-delegate';
|
||||
import { raf } from '../../utils/helpers';
|
||||
import { KEYBOARD_DID_OPEN } from '../../utils/keyboard/keyboard';
|
||||
import { BACKDROP, activeAnimations, dismiss, eventMethod, prepareOverlay, present } from '../../utils/overlays';
|
||||
import { getClassMap } from '../../utils/theme';
|
||||
import { deepReady } from '../../utils/transition';
|
||||
@@ -44,6 +45,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
private currentBreakpoint?: number;
|
||||
private wrapperEl?: HTMLElement;
|
||||
private backdropEl?: HTMLIonBackdropElement;
|
||||
private keyboardOpenCallback?: () => void;
|
||||
|
||||
private inline = false;
|
||||
private workingDelegate?: FrameworkDelegate;
|
||||
@@ -216,28 +218,28 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Event({ eventName: 'ionModalDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
/**
|
||||
* Emitted after the modal has presented.
|
||||
* Shorthand for ionModalWillDismiss.
|
||||
*/
|
||||
/**
|
||||
* Emitted after the modal has presented.
|
||||
* Shorthand for ionModalWillDismiss.
|
||||
*/
|
||||
@Event({ eventName: 'didPresent' }) didPresentShorthand!: EventEmitter<void>;
|
||||
|
||||
/**
|
||||
* Emitted before the modal has presented.
|
||||
* Shorthand for ionModalWillPresent.
|
||||
*/
|
||||
/**
|
||||
* Emitted before the modal has presented.
|
||||
* Shorthand for ionModalWillPresent.
|
||||
*/
|
||||
@Event({ eventName: 'willPresent' }) willPresentShorthand!: EventEmitter<void>;
|
||||
|
||||
/**
|
||||
* Emitted before the modal has dismissed.
|
||||
* Shorthand for ionModalWillDismiss.
|
||||
*/
|
||||
/**
|
||||
* Emitted before the modal has dismissed.
|
||||
* Shorthand for ionModalWillDismiss.
|
||||
*/
|
||||
@Event({ eventName: 'willDismiss' }) willDismissShorthand!: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
/**
|
||||
* Emitted after the modal has dismissed.
|
||||
* Shorthand for ionModalDidDismiss.
|
||||
*/
|
||||
/**
|
||||
* Emitted after the modal has dismissed.
|
||||
* Shorthand for ionModalDidDismiss.
|
||||
*/
|
||||
@Event({ eventName: 'didDismiss' }) didDismissShorthand!: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
@Watch('swipeToClose')
|
||||
@@ -290,14 +292,14 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
const triggerEl = (trigger !== undefined) ? document.getElementById(trigger) : null;
|
||||
if (!triggerEl) { return; }
|
||||
|
||||
const configureTriggerInteraction = (triggerEl: HTMLElement, modalEl: HTMLIonModalElement) => {
|
||||
const configureTriggerInteraction = (trigEl: HTMLElement, modalEl: HTMLIonModalElement) => {
|
||||
const openModal = () => {
|
||||
modalEl.present();
|
||||
}
|
||||
triggerEl.addEventListener('click', openModal);
|
||||
trigEl.addEventListener('click', openModal);
|
||||
|
||||
return () => {
|
||||
triggerEl.removeEventListener('click', openModal);
|
||||
trigEl.removeEventListener('click', openModal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,6 +382,30 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
this.initSwipeToClose();
|
||||
}
|
||||
|
||||
/* tslint:disable-next-line */
|
||||
if (typeof window !== 'undefined') {
|
||||
this.keyboardOpenCallback = () => {
|
||||
if (this.gesture) {
|
||||
/**
|
||||
* When the native keyboard is opened and the webview
|
||||
* is resized, the gesture implementation will become unresponsive
|
||||
* and enter a free-scroll mode.
|
||||
*
|
||||
* When the keyboard is opened, we disable the gesture for
|
||||
* a single frame and re-enable once the contents have repositioned
|
||||
* from the keyboard placement.
|
||||
*/
|
||||
this.gesture.enable(false);
|
||||
raf(() => {
|
||||
if (this.gesture) {
|
||||
this.gesture.enable(true)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
window.addEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
|
||||
}
|
||||
|
||||
this.currentTransition = undefined;
|
||||
}
|
||||
|
||||
@@ -474,6 +500,11 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* tslint:disable-next-line */
|
||||
if (typeof window !== 'undefined' && this.keyboardOpenCallback) {
|
||||
window.removeEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* When using an inline modal
|
||||
* and presenting a modal it is possible to
|
||||
@@ -494,19 +525,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
|
||||
if (dismissed) {
|
||||
const { delegate } = this.getDelegate();
|
||||
|
||||
/**
|
||||
* If the modal is presented through a controller, we don't need to detach
|
||||
* since the el was already removed during the `dismiss` call above. Skipping
|
||||
* this step also prevents an issue where rapdily dismissing right after
|
||||
* presenting could cause `detachComponent` to be called after the present
|
||||
* finished, blanking out the newly opened modal.
|
||||
*
|
||||
* TODO(FW-423) try and find a way to resolve the race condition directly
|
||||
*/
|
||||
if (this.inline) {
|
||||
await detachComponent(delegate, this.usersElement);
|
||||
}
|
||||
await detachComponent(delegate, this.usersElement);
|
||||
|
||||
if (this.animation) {
|
||||
this.animation.destroy();
|
||||
@@ -568,7 +587,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
|
||||
const showHandle = handle !== false && isSheetModal;
|
||||
const mode = getIonMode(this);
|
||||
const { presented, modalId } = this;
|
||||
const { modalId } = this;
|
||||
const isCardModal = presentingElement !== undefined && mode === 'ios';
|
||||
|
||||
return (
|
||||
@@ -586,7 +605,6 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
[`modal-card`]: isCardModal,
|
||||
[`modal-sheet`]: isSheetModal,
|
||||
'overlay-hidden': true,
|
||||
'modal-interactive': presented,
|
||||
...getClassMap(this.cssClass)
|
||||
}}
|
||||
id={modalId}
|
||||
|
||||
@@ -12,6 +12,10 @@ test('modal: inline', async () => {
|
||||
expect(modal).not.toBe(null);
|
||||
await modal.waitForVisible();
|
||||
|
||||
const container = await modal.find('.ion-page');
|
||||
|
||||
expect(container).not.toBe(null);
|
||||
|
||||
screenshotCompares.push(await page.compareScreenshot());
|
||||
|
||||
await modal.callMethod('dismiss');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop, State, Watch, h } from '@stencil/core';
|
||||
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h } from '@stencil/core';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import { Color } from '../../interface';
|
||||
@@ -118,7 +118,9 @@ export class PickerColumnInternal implements ComponentInterface {
|
||||
}
|
||||
}
|
||||
|
||||
scrollActiveItemIntoView() {
|
||||
/** @internal */
|
||||
@Method()
|
||||
async scrollActiveItemIntoView() {
|
||||
const activeEl = this.activeItem;
|
||||
|
||||
if (activeEl) {
|
||||
|
||||
@@ -374,6 +374,7 @@ export class PickerInternal implements ComponentInterface {
|
||||
const lastColumn = numericPickers[1];
|
||||
|
||||
let value = inputEl.value;
|
||||
let minuteValue;
|
||||
switch (value.length) {
|
||||
case 1:
|
||||
this.searchColumn(firstColumn, value);
|
||||
@@ -396,7 +397,7 @@ export class PickerInternal implements ComponentInterface {
|
||||
* for a match in the minutes column
|
||||
*/
|
||||
if (value.length === 1) {
|
||||
const minuteValue = inputEl.value.substring(inputEl.value.length - 1);
|
||||
minuteValue = inputEl.value.substring(inputEl.value.length - 1);
|
||||
this.searchColumn(lastColumn, minuteValue, 'end');
|
||||
}
|
||||
break;
|
||||
@@ -417,7 +418,7 @@ export class PickerInternal implements ComponentInterface {
|
||||
* we can check the second value
|
||||
* for a match in the minutes column
|
||||
*/
|
||||
const minuteValue = (value.length === 1) ? inputEl.value.substring(1) : inputEl.value.substring(2);
|
||||
minuteValue = (value.length === 1) ? inputEl.value.substring(1) : inputEl.value.substring(2);
|
||||
|
||||
this.searchColumn(lastColumn, minuteValue, 'end');
|
||||
break;
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface PopoverOptions<T extends ComponentRef = ComponentRef> {
|
||||
dismissOnSelect?: boolean;
|
||||
reference?: PositionReference;
|
||||
side?: PositionSide;
|
||||
align?: PositionAlign;
|
||||
alignment?: PositionAlign;
|
||||
arrow?: boolean;
|
||||
|
||||
trigger?: string;
|
||||
|
||||
@@ -42,13 +42,6 @@
|
||||
color: $popover-text-color;
|
||||
|
||||
z-index: $z-index-overlay;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:host(.popover-interactive) .popover-content,
|
||||
:host(.popover-interactive) ion-backdrop {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
:host(.overlay-hidden) {
|
||||
|
||||
@@ -372,7 +372,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
* was dispatched.
|
||||
*/
|
||||
@Method()
|
||||
async present(event?: MouseEvent | TouchEvent | PointerEvent): Promise<void> {
|
||||
async present(event?: MouseEvent | TouchEvent | PointerEvent | CustomEvent): Promise<void> {
|
||||
if (this.presented) {
|
||||
return;
|
||||
}
|
||||
@@ -565,7 +565,7 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
|
||||
render() {
|
||||
const mode = getIonMode(this);
|
||||
const { onLifecycle, popoverId, parentPopover, dismissOnSelect, presented, side, arrow, htmlAttributes } = this;
|
||||
const { onLifecycle, popoverId, parentPopover, dismissOnSelect, side, arrow, htmlAttributes } = this;
|
||||
const desktop = isPlatform('desktop');
|
||||
const enableArrow = arrow && !parentPopover && !desktop;
|
||||
|
||||
@@ -584,7 +584,6 @@ export class Popover implements ComponentInterface, PopoverInterface {
|
||||
[mode]: true,
|
||||
'popover-translucent': this.translucent,
|
||||
'overlay-hidden': true,
|
||||
'popover-interactive': presented,
|
||||
'popover-desktop': desktop,
|
||||
[`popover-side-${side}`]: true,
|
||||
'popover-nested': !!parentPopover
|
||||
|
||||
@@ -107,7 +107,7 @@ interface PopoverOptions {
|
||||
dismissOnSelect?: boolean;
|
||||
reference?: PositionReference;
|
||||
side?: PositionSide;
|
||||
align?: PositionAlign;
|
||||
alignment?: PositionAlign;
|
||||
arrow?: boolean;
|
||||
}
|
||||
```
|
||||
@@ -1022,7 +1022,7 @@ Type: `Promise<OverlayEventDetail<T>>`
|
||||
|
||||
|
||||
|
||||
### `present(event?: MouseEvent | TouchEvent | PointerEvent | undefined) => Promise<void>`
|
||||
### `present(event?: MouseEvent | TouchEvent | PointerEvent | CustomEvent<any> | undefined) => Promise<void>`
|
||||
|
||||
Present the popover overlay after it has been created.
|
||||
Developers can pass a mouse, touch, or pointer event
|
||||
|
||||
@@ -177,7 +177,7 @@ export class Radio implements ComponentInterface {
|
||||
disabled={disabled}
|
||||
tabindex="-1"
|
||||
id={inputId}
|
||||
ref={el => this.nativeInput = el as HTMLInputElement}
|
||||
ref={nativeEl => this.nativeInput = nativeEl as HTMLInputElement}
|
||||
/>
|
||||
</Host>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import { Color, Gesture, GestureDetail, KnobName, RangeChangeEventDetail, RangeValue, StyleEventDetail } from '../../interface';
|
||||
import { clamp, debounceEvent, getAriaLabel, inheritAttributes, renderHiddenInput } from '../../utils/helpers';
|
||||
import { isRTL } from '../../utils/rtl';
|
||||
import { createColorClasses, hostContext } from '../../utils/theme';
|
||||
|
||||
import { PinFormatter } from './range-interface';
|
||||
@@ -290,13 +291,13 @@ export class Range implements ComponentInterface {
|
||||
|
||||
// figure out which knob they started closer to
|
||||
let ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
|
||||
if (document.dir === 'rtl') {
|
||||
if (isRTL(this.el)) {
|
||||
ratio = 1 - ratio;
|
||||
}
|
||||
|
||||
this.pressedKnob =
|
||||
!this.dualKnobs ||
|
||||
Math.abs(this.ratioA - ratio) < Math.abs(this.ratioB - ratio)
|
||||
Math.abs(this.ratioA - ratio) < Math.abs(this.ratioB - ratio)
|
||||
? 'A'
|
||||
: 'B';
|
||||
|
||||
@@ -320,7 +321,7 @@ export class Range implements ComponentInterface {
|
||||
// update the knob being interacted with
|
||||
const rect = this.rect;
|
||||
let ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
|
||||
if (document.dir === 'rtl') {
|
||||
if (isRTL(this.el)) {
|
||||
ratio = 1 - ratio;
|
||||
}
|
||||
|
||||
@@ -384,9 +385,9 @@ export class Range implements ComponentInterface {
|
||||
this.value = !this.dualKnobs
|
||||
? valA
|
||||
: {
|
||||
lower: Math.min(valA, valB),
|
||||
upper: Math.max(valA, valB)
|
||||
};
|
||||
lower: Math.min(valA, valB),
|
||||
upper: Math.max(valA, valB)
|
||||
};
|
||||
|
||||
this.noUpdate = false;
|
||||
}
|
||||
@@ -432,10 +433,10 @@ export class Range implements ComponentInterface {
|
||||
const barStart = `${ratioLower * 100}%`;
|
||||
const barEnd = `${100 - ratioUpper * 100}%`;
|
||||
|
||||
const doc = document;
|
||||
const isRTL = doc.dir === 'rtl';
|
||||
const start = isRTL ? 'right' : 'left';
|
||||
const end = isRTL ? 'left' : 'right';
|
||||
const rtl = isRTL(this.el);
|
||||
|
||||
const start = rtl ? 'right' : 'left';
|
||||
const end = rtl ? 'left' : 'right';
|
||||
|
||||
const tickStyle = (tick: any) => {
|
||||
return {
|
||||
@@ -502,7 +503,7 @@ export class Range implements ComponentInterface {
|
||||
part="bar-active"
|
||||
/>
|
||||
|
||||
{ renderKnob(isRTL, {
|
||||
{renderKnob(rtl, {
|
||||
knob: 'A',
|
||||
pressed: pressedKnob === 'A',
|
||||
value: this.valA,
|
||||
@@ -516,7 +517,7 @@ export class Range implements ComponentInterface {
|
||||
labelText
|
||||
})}
|
||||
|
||||
{ this.dualKnobs && renderKnob(isRTL, {
|
||||
{this.dualKnobs && renderKnob(rtl, {
|
||||
knob: 'B',
|
||||
pressed: pressedKnob === 'B',
|
||||
value: this.valB,
|
||||
@@ -551,8 +552,8 @@ interface RangeKnob {
|
||||
handleKeyboard: (name: KnobName, isIncrease: boolean) => void;
|
||||
}
|
||||
|
||||
const renderKnob = (isRTL: boolean, { knob, value, ratio, min, max, disabled, pressed, pin, handleKeyboard, labelText, pinFormatter }: RangeKnob) => {
|
||||
const start = isRTL ? 'right' : 'left';
|
||||
const renderKnob = (rtl: boolean, { knob, value, ratio, min, max, disabled, pressed, pin, handleKeyboard, labelText, pinFormatter }: RangeKnob) => {
|
||||
const start = rtl ? 'right' : 'left';
|
||||
|
||||
const knobStyle = () => {
|
||||
const style: any = {};
|
||||
|
||||
10
core/src/components/range/test/rtl/e2e.ts
Normal file
10
core/src/components/range/test/rtl/e2e.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('range: RTL', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/range/test/rtl?ionic:_testing=true'
|
||||
});
|
||||
|
||||
const compare = await page.compareScreenshot();
|
||||
expect(compare).toMatchScreenshot();
|
||||
});
|
||||
43
core/src/components/range/test/rtl/index.html
Normal file
43
core/src/components/range/test/rtl/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Range - RTL</title>
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Range - RTL</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content id="content">
|
||||
|
||||
<ion-list>
|
||||
<ion-list-header>
|
||||
<ion-label>
|
||||
Range
|
||||
</ion-label>
|
||||
</ion-list-header>
|
||||
<ion-item>
|
||||
<ion-range dir="rtl" value="20" aria-label="Default Range"></ion-range>
|
||||
</ion-item>
|
||||
|
||||
</ion-content>
|
||||
|
||||
</ion-app>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, ComponentInterface, Element, Host, Listen, h } from '@stencil/core';
|
||||
import { reorderThreeOutline, reorderTwoSharp } from 'ionicons/icons';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
|
||||
@@ -31,11 +32,11 @@ export class Reorder implements ComponentInterface {
|
||||
|
||||
render() {
|
||||
const mode = getIonMode(this);
|
||||
const reorderIcon = mode === 'ios' ? 'reorder-three-outline' : 'reorder-two-sharp';
|
||||
const reorderIcon = mode === 'ios' ? reorderThreeOutline : reorderTwoSharp;
|
||||
return (
|
||||
<Host class={mode}>
|
||||
<slot>
|
||||
<ion-icon name={reorderIcon} lazy={false} class="reorder-icon" part="icon" />
|
||||
<ion-icon icon={reorderIcon} lazy={false} class="reorder-icon" part="icon" />
|
||||
</slot>
|
||||
</Host>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ion-ripple-effect
|
||||
|
||||
The ripple effect component adds the [Material Design ink ripple interaction effect](https://material.io/develop/web/components/ripples/). This component can only be used inside of an `<ion-app>` and can be added to any component.
|
||||
The ripple effect component adds the [Material Design ink ripple interaction effect](https://material.io/develop/web/supporting/ripple). This component can only be used inside of an `<ion-app>` and can be added to any component.
|
||||
|
||||
It's important to note that the parent should have [relative positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/position) because the ripple effect is absolutely positioned and will cover the closest parent with relative positioning. The parent element should also be given the `ion-activatable` class, which tells the ripple effect that the element is clickable.
|
||||
|
||||
|
||||
@@ -100,9 +100,9 @@ Type: `Promise<void>`
|
||||
|
||||
|
||||
|
||||
### `push(url: string, direction?: RouterDirection, animation?: AnimationBuilder | undefined) => Promise<boolean>`
|
||||
### `push(path: string, direction?: RouterDirection, animation?: AnimationBuilder | undefined) => Promise<boolean>`
|
||||
|
||||
Navigate to the specified URL.
|
||||
Navigate to the specified path.
|
||||
|
||||
#### Returns
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import { NavigationHookResult } from '../route/route-interface';
|
||||
import { ROUTER_INTENT_BACK, ROUTER_INTENT_FORWARD, ROUTER_INTENT_NONE } from './utils/constants';
|
||||
import { printRedirects, printRoutes } from './utils/debug';
|
||||
import { readNavState, waitUntilNavNode, writeNavState } from './utils/dom';
|
||||
import { findRouteRedirect, routerIDsToChain, routerPathToChain } from './utils/matching';
|
||||
import { findChainForIDs, findChainForSegments, findRouteRedirect } from './utils/matching';
|
||||
import { readRedirects, readRoutes } from './utils/parser';
|
||||
import { chainToPath, generatePath, parsePath, readPath, writePath } from './utils/path';
|
||||
import { chainToSegments, generatePath, parsePath, readSegments, writeSegments } from './utils/path';
|
||||
|
||||
@Component({
|
||||
tag: 'ion-router'
|
||||
@@ -59,12 +59,12 @@ export class Router implements ComponentInterface {
|
||||
async componentWillLoad() {
|
||||
await waitUntilNavNode();
|
||||
|
||||
const canProceed = await this.runGuards(this.getPath());
|
||||
const canProceed = await this.runGuards(this.getSegments());
|
||||
if (canProceed !== true) {
|
||||
if (typeof canProceed === 'object') {
|
||||
const { redirect } = canProceed;
|
||||
const path = parsePath(redirect);
|
||||
this.setPath(path.segments, ROUTER_INTENT_NONE, path.queryString);
|
||||
this.setSegments(path.segments, ROUTER_INTENT_NONE, path.queryString);
|
||||
await this.writeNavStateRoot(path.segments, ROUTER_INTENT_NONE);
|
||||
}
|
||||
} else {
|
||||
@@ -80,7 +80,7 @@ export class Router implements ComponentInterface {
|
||||
@Listen('popstate', { target: 'window' })
|
||||
protected async onPopState() {
|
||||
const direction = this.historyDirection();
|
||||
let segments = this.getPath();
|
||||
let segments = this.getSegments();
|
||||
|
||||
const canProceed = await this.runGuards(segments);
|
||||
if (canProceed !== true) {
|
||||
@@ -117,18 +117,21 @@ export class Router implements ComponentInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the specified URL.
|
||||
* Navigate to the specified path.
|
||||
*
|
||||
* @param url The url to navigate to.
|
||||
* @param path The path to navigate to.
|
||||
* @param direction The direction of the animation. Defaults to `"forward"`.
|
||||
*/
|
||||
@Method()
|
||||
async push(url: string, direction: RouterDirection = 'forward', animation?: AnimationBuilder) {
|
||||
if (url.startsWith('.')) {
|
||||
url = (new URL(url, window.location.href)).pathname;
|
||||
async push(path: string, direction: RouterDirection = 'forward', animation?: AnimationBuilder) {
|
||||
if (path.startsWith('.')) {
|
||||
const currentPath = this.previousPath ?? '/';
|
||||
// Convert currentPath to an URL by pre-pending a protocol and a host to resolve the relative path.
|
||||
const url = new URL(path, `https://host/${currentPath}`);
|
||||
path = url.pathname + url.search;
|
||||
}
|
||||
|
||||
let parsedPath = parsePath(url);
|
||||
let parsedPath = parsePath(path);
|
||||
|
||||
const canProceed = await this.runGuards(parsedPath.segments);
|
||||
if (canProceed !== true) {
|
||||
@@ -139,13 +142,11 @@ export class Router implements ComponentInterface {
|
||||
}
|
||||
}
|
||||
|
||||
this.setPath(parsedPath.segments, direction, parsedPath.queryString);
|
||||
this.setSegments(parsedPath.segments, direction, parsedPath.queryString);
|
||||
return this.writeNavStateRoot(parsedPath.segments, direction, animation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Go back to previous page in the window.history.
|
||||
*/
|
||||
/** Go back to previous page in the window.history. */
|
||||
@Method()
|
||||
back(): Promise<void> {
|
||||
window.history.back();
|
||||
@@ -168,35 +169,35 @@ export class Router implements ComponentInterface {
|
||||
}
|
||||
const { ids, outlet } = await readNavState(window.document.body);
|
||||
const routes = readRoutes(this.el);
|
||||
const chain = routerIDsToChain(ids, routes);
|
||||
const chain = findChainForIDs(ids, routes);
|
||||
if (!chain) {
|
||||
console.warn('[ion-router] no matching URL for ', ids.map(i => i.id));
|
||||
return false;
|
||||
}
|
||||
|
||||
const path = chainToPath(chain);
|
||||
if (!path) {
|
||||
const segments = chainToSegments(chain);
|
||||
if (!segments) {
|
||||
console.warn('[ion-router] router could not match path because some required param is missing');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setPath(path, direction);
|
||||
this.setSegments(segments, direction);
|
||||
|
||||
await this.safeWriteNavState(outlet, chain, ROUTER_INTENT_NONE, path, null, ids.length);
|
||||
await this.safeWriteNavState(outlet, chain, ROUTER_INTENT_NONE, segments, null, ids.length);
|
||||
return true;
|
||||
}
|
||||
|
||||
// This handler gets called when a `ion-route-redirect` component is added to the DOM or if the from or to property of such node changes.
|
||||
/** This handler gets called when a `ion-route-redirect` component is added to the DOM or if the from or to property of such node changes. */
|
||||
private onRedirectChanged() {
|
||||
const path = this.getPath();
|
||||
if (path && findRouteRedirect(path, readRedirects(this.el))) {
|
||||
this.writeNavStateRoot(path, ROUTER_INTENT_NONE);
|
||||
const segments = this.getSegments();
|
||||
if (segments && findRouteRedirect(segments, readRedirects(this.el))) {
|
||||
this.writeNavStateRoot(segments, ROUTER_INTENT_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
// This handler gets called when a `ion-route` component is added to the DOM or if the from or to property of such node changes.
|
||||
/** This handler gets called when a `ion-route` component is added to the DOM or if the from or to property of such node changes. */
|
||||
private onRoutesChanged() {
|
||||
return this.writeNavStateRoot(this.getPath(), ROUTER_INTENT_NONE);
|
||||
return this.writeNavStateRoot(this.getSegments(), ROUTER_INTENT_NONE);
|
||||
}
|
||||
|
||||
private historyDirection() {
|
||||
@@ -220,47 +221,47 @@ export class Router implements ComponentInterface {
|
||||
return ROUTER_INTENT_NONE;
|
||||
}
|
||||
|
||||
private async writeNavStateRoot(path: string[] | null, direction: RouterDirection, animation?: AnimationBuilder): Promise<boolean> {
|
||||
if (!path) {
|
||||
private async writeNavStateRoot(segments: string[] | null, direction: RouterDirection, animation?: AnimationBuilder): Promise<boolean> {
|
||||
if (!segments) {
|
||||
console.error('[ion-router] URL is not part of the routing set');
|
||||
return false;
|
||||
}
|
||||
|
||||
// lookup redirect rule
|
||||
const redirects = readRedirects(this.el);
|
||||
const redirect = findRouteRedirect(path, redirects);
|
||||
const redirect = findRouteRedirect(segments, redirects);
|
||||
|
||||
let redirectFrom: string[] | null = null;
|
||||
|
||||
if (redirect) {
|
||||
const { segments, queryString } = redirect.to!;
|
||||
this.setPath(segments, direction, queryString);
|
||||
const { segments: toSegments, queryString } = redirect.to!;
|
||||
this.setSegments(toSegments, direction, queryString);
|
||||
redirectFrom = redirect.from;
|
||||
path = segments;
|
||||
segments = toSegments;
|
||||
}
|
||||
|
||||
// lookup route chain
|
||||
const routes = readRoutes(this.el);
|
||||
const chain = routerPathToChain(path, routes);
|
||||
const chain = findChainForSegments(segments, routes);
|
||||
if (!chain) {
|
||||
console.error('[ion-router] the path does not match any route');
|
||||
return false;
|
||||
}
|
||||
|
||||
// write DOM give
|
||||
return this.safeWriteNavState(document.body, chain, direction, path, redirectFrom, 0, animation);
|
||||
return this.safeWriteNavState(document.body, chain, direction, segments, redirectFrom, 0, animation);
|
||||
}
|
||||
|
||||
private async safeWriteNavState(
|
||||
node: HTMLElement | undefined, chain: RouteChain, direction: RouterDirection,
|
||||
path: string[], redirectFrom: string[] | null,
|
||||
segments: string[], redirectFrom: string[] | null,
|
||||
index = 0,
|
||||
animation?: AnimationBuilder
|
||||
): Promise<boolean> {
|
||||
const unlock = await this.lock();
|
||||
let changed = false;
|
||||
try {
|
||||
changed = await this.writeNavState(node, chain, direction, path, redirectFrom, index, animation);
|
||||
changed = await this.writeNavState(node, chain, direction, segments, redirectFrom, index, animation);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
@@ -279,11 +280,13 @@ export class Router implements ComponentInterface {
|
||||
return resolve;
|
||||
}
|
||||
|
||||
// Executes the beforeLeave hook of the source route and the beforeEnter hook of the target route if they exist.
|
||||
//
|
||||
// When the beforeLeave hook does not return true (to allow navigating) then that value is returned early and the beforeEnter is executed.
|
||||
// Otherwise the beforeEnterHook hook of the target route is executed.
|
||||
private async runGuards(to: string[] | null = this.getPath(), from?: string[] | null): Promise<NavigationHookResult> {
|
||||
/**
|
||||
* Executes the beforeLeave hook of the source route and the beforeEnter hook of the target route if they exist.
|
||||
*
|
||||
* When the beforeLeave hook does not return true (to allow navigating) then that value is returned early and the beforeEnter is executed.
|
||||
* Otherwise the beforeEnterHook hook of the target route is executed.
|
||||
*/
|
||||
private async runGuards(to: string[] | null = this.getSegments(), from?: string[] | null): Promise<NavigationHookResult> {
|
||||
if (from === undefined) {
|
||||
from = parsePath(this.previousPath).segments;
|
||||
}
|
||||
@@ -292,13 +295,13 @@ export class Router implements ComponentInterface {
|
||||
|
||||
const routes = readRoutes(this.el);
|
||||
|
||||
const fromChain = routerPathToChain(from, routes);
|
||||
const fromChain = findChainForSegments(from, routes);
|
||||
const beforeLeaveHook = fromChain && fromChain[fromChain.length - 1].beforeLeave;
|
||||
|
||||
const canLeave = beforeLeaveHook ? await beforeLeaveHook() : true;
|
||||
if (canLeave === false || typeof canLeave === 'object') { return canLeave; }
|
||||
|
||||
const toChain = routerPathToChain(to, routes);
|
||||
const toChain = findChainForSegments(to, routes);
|
||||
const beforeEnterHook = toChain && toChain[toChain.length - 1].beforeEnter;
|
||||
|
||||
return beforeEnterHook ? beforeEnterHook() : true;
|
||||
@@ -306,7 +309,7 @@ export class Router implements ComponentInterface {
|
||||
|
||||
private async writeNavState(
|
||||
node: HTMLElement | undefined, chain: RouteChain, direction: RouterDirection,
|
||||
path: string[], redirectFrom: string[] | null,
|
||||
segments: string[], redirectFrom: string[] | null,
|
||||
index = 0, animation?: AnimationBuilder
|
||||
): Promise<boolean> {
|
||||
if (this.busy) {
|
||||
@@ -316,7 +319,7 @@ export class Router implements ComponentInterface {
|
||||
this.busy = true;
|
||||
|
||||
// generate route event and emit will change
|
||||
const routeEvent = this.routeChangeEvent(path, redirectFrom);
|
||||
const routeEvent = this.routeChangeEvent(segments, redirectFrom);
|
||||
if (routeEvent) {
|
||||
this.ionRouteWillChange.emit(routeEvent);
|
||||
}
|
||||
@@ -331,23 +334,23 @@ export class Router implements ComponentInterface {
|
||||
return changed;
|
||||
}
|
||||
|
||||
private setPath(path: string[], direction: RouterDirection, queryString?: string) {
|
||||
private setSegments(segments: string[], direction: RouterDirection, queryString?: string) {
|
||||
this.state++;
|
||||
writePath(window.history, this.root, this.useHash, path, direction, this.state, queryString);
|
||||
writeSegments(window.history, this.root, this.useHash, segments, direction, this.state, queryString);
|
||||
}
|
||||
|
||||
private getPath(): string[] | null {
|
||||
return readPath(window.location, this.root, this.useHash);
|
||||
private getSegments(): string[] | null {
|
||||
return readSegments(window.location, this.root, this.useHash);
|
||||
}
|
||||
|
||||
private routeChangeEvent(path: string[], redirectFromPath: string[] | null): RouterEventDetail | null {
|
||||
private routeChangeEvent(toSegments: string[], redirectFromSegments: string[] | null): RouterEventDetail | null {
|
||||
const from = this.previousPath;
|
||||
const to = generatePath(path);
|
||||
const to = generatePath(toSegments);
|
||||
this.previousPath = to;
|
||||
if (to === from) {
|
||||
return null;
|
||||
}
|
||||
const redirectedFrom = redirectFromPath ? generatePath(redirectFromPath) : null;
|
||||
const redirectedFrom = redirectFromSegments ? generatePath(redirectFromSegments) : null;
|
||||
return {
|
||||
from,
|
||||
redirectedFrom,
|
||||
|
||||
@@ -57,6 +57,8 @@
|
||||
<p><a href='#/two/three/hello'>Go to page 3 (hello)</a></p>
|
||||
<p><a href='#/two/second-page'>Go to page 2</a></p>
|
||||
<p><a href='#/two/'>Go to page 1</a></p>
|
||||
<ion-button id="btn-rel" href="./relative?param=1">Page 3 (relative)</ion-button>
|
||||
<ion-button id="btn-abs" href="/two/three/absolute">Page 3 (absolute)</ion-button>
|
||||
</ion-content>`;
|
||||
}
|
||||
}
|
||||
|
||||
29
core/src/components/router/test/basic/router-push.e2e.ts
Normal file
29
core/src/components/router/test/basic/router-push.e2e.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('push should support relative path', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/router/test/basic#/two/three/hola?ionic:_testing=true'
|
||||
});
|
||||
await page.waitForChanges();
|
||||
|
||||
const backButton = await page.$('#btn-rel');
|
||||
await backButton.click();
|
||||
await page.waitForChanges();
|
||||
|
||||
const url = await page.url();
|
||||
expect(url).toContain('#/two/three/relative?param=1');
|
||||
});
|
||||
|
||||
test('push should support absolute path', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/router/test/basic#/two/three/hola?ionic:_testing=true'
|
||||
});
|
||||
await page.waitForChanges();
|
||||
|
||||
const backButton = await page.$('#btn-abs');
|
||||
await backButton.click();
|
||||
await page.waitForChanges();
|
||||
|
||||
const url = await page.url();
|
||||
expect(url).toContain('#/two/three/absolute');
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
import { mockWindow } from '@stencil/core/testing';
|
||||
|
||||
import { RouteChain, RouteID } from '../utils/interface';
|
||||
import { routerIDsToChain, routerPathToChain } from '../utils/matching';
|
||||
import { findChainForSegments, findChainForIDs } from '../utils/matching';
|
||||
import { readRoutes } from '../utils/parser';
|
||||
import { chainToPath, generatePath, parsePath } from '../utils/path';
|
||||
import { chainToSegments, generatePath, parsePath } from '../utils/path';
|
||||
|
||||
import { mockRouteElement } from './parser.spec';
|
||||
|
||||
@@ -76,10 +76,9 @@ describe('ionic-conference-app', () => {
|
||||
}
|
||||
});
|
||||
|
||||
export function getRouteIDs(path: string, routes: RouteChain[]): string[] {
|
||||
return routerPathToChain(parsePath(path).segments, routes)!.map(r => r.id);
|
||||
function getRouteIDs(path: string, routes: RouteChain[]): string[] {
|
||||
return findChainForSegments(parsePath(path).segments, routes)!.map(r => r.id);
|
||||
}
|
||||
|
||||
export function getRoutePath(ids: RouteID[], routes: RouteChain[]): string {
|
||||
return generatePath(chainToPath(routerIDsToChain(ids, routes)!)!);
|
||||
function getRoutePath(ids: RouteID[], routes: RouteChain[]): string {
|
||||
return generatePath(chainToSegments(findChainForIDs(ids, routes)!)!);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user