mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
chore(): sync vue branch with master
This commit is contained in:
16
.github/ionic-issue-bot.yml
vendored
16
.github/ionic-issue-bot.yml
vendored
@@ -130,22 +130,6 @@ noReproduction:
|
||||
lock: true
|
||||
dryRun: false
|
||||
|
||||
labelPullRequest:
|
||||
labels:
|
||||
- label: "package: angular"
|
||||
branch: master
|
||||
path: ^angular
|
||||
- label: "package: core"
|
||||
branch: master
|
||||
path: ^core
|
||||
- label: "package: react"
|
||||
branch: master
|
||||
path: ^react
|
||||
- label: "package: vue"
|
||||
branch: master
|
||||
path: ^vue
|
||||
dryRun: false
|
||||
|
||||
wrongRepo:
|
||||
repos:
|
||||
- label: "ionitron: capacitor"
|
||||
|
||||
21
.github/labeler.yml
vendored
Normal file
21
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# This is used with the label workflow which
|
||||
# will triage pull requests and apply a label based on the
|
||||
# paths that are modified in the pull request.
|
||||
#
|
||||
# For more information, see:
|
||||
# https://github.com/actions/labeler
|
||||
|
||||
'package: core':
|
||||
- core/**/*
|
||||
|
||||
'package: angular':
|
||||
- angular/**/*
|
||||
- packages/angular-*/**/*
|
||||
|
||||
'package: react':
|
||||
- packages/react/**/*
|
||||
- packages/react-*/**/*
|
||||
|
||||
'package: vue':
|
||||
- packages/vue/**/*
|
||||
- packages/vue-*/**/*
|
||||
19
.github/workflows/label.yml
vendored
Normal file
19
.github/workflows/label.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# This workflow will triage pull requests and apply a label based on the
|
||||
# paths that are modified in the pull request.
|
||||
#
|
||||
# To use this workflow, you will need to set up a .github/labeler.yml
|
||||
# file with configuration. For more information, see:
|
||||
# https://github.com/actions/labeler
|
||||
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@main
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
sync-labels: true
|
||||
39
CHANGELOG.md
39
CHANGELOG.md
@@ -1,3 +1,36 @@
|
||||
## [5.3.5](https://github.com/ionic-team/ionic/compare/v5.3.4...v5.3.5) (2020-10-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **button:** allow any element type to use the "icon-only" slot ([#22168](https://github.com/ionic-team/ionic/issues/22168)) ([c454c84](https://github.com/ionic-team/ionic/commit/c454c84ef46322143467600334a0263d4e7df6cb))
|
||||
* **datetime:** do not set ampm when the column doesn't exist ([#22220](https://github.com/ionic-team/ionic/issues/22220)) ([18fb885](https://github.com/ionic-team/ionic/commit/18fb8855e0c45fe65843b33811812c51c74de90f)), closes [#22149](https://github.com/ionic-team/ionic/issues/22149)
|
||||
* **datetime:** remove the automatic switching from am to pm ([#22207](https://github.com/ionic-team/ionic/issues/22207)) ([f81d18c](https://github.com/ionic-team/ionic/commit/f81d18c6f9f1bce056afda1cac4cf6d6ace0a7ca)), closes [#18924](https://github.com/ionic-team/ionic/issues/18924) [#22171](https://github.com/ionic-team/ionic/issues/22171) [#22199](https://github.com/ionic-team/ionic/issues/22199)
|
||||
* **item:** properly align datetime and select with fixed or no labels ([#22221](https://github.com/ionic-team/ionic/issues/22221)) ([f42c688](https://github.com/ionic-team/ionic/commit/f42c688f4630e3dc5d10b947e7f2bee9d5967d8c)), closes [#18773](https://github.com/ionic-team/ionic/issues/18773) [#18761](https://github.com/ionic-team/ionic/issues/18761) [#18779](https://github.com/ionic-team/ionic/issues/18779)
|
||||
* **label:** keep color when focused on a floating or stacked label ([#18576](https://github.com/ionic-team/ionic/issues/18576)) ([992580a](https://github.com/ionic-team/ionic/commit/992580a3830321bdf9591681ebe38e823205389d)), closes [#18531](https://github.com/ionic-team/ionic/issues/18531)
|
||||
* **select:** do not close popover or set value when switching with arrow keys ([#22210](https://github.com/ionic-team/ionic/issues/22210)) ([1878c8e](https://github.com/ionic-team/ionic/commit/1878c8e7e01c02f06bdc5f1562af0d45531539cf)), closes [#22179](https://github.com/ionic-team/ionic/issues/22179)
|
||||
|
||||
|
||||
|
||||
## [5.3.4](https://github.com/ionic-team/ionic/compare/v5.3.3...v5.3.4) (2020-09-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **alert:** follow accessibility guidelines outlined by wai-aria ([#22159](https://github.com/ionic-team/ionic/issues/22159)) ([e9b2cc8](https://github.com/ionic-team/ionic/commit/e9b2cc8453f5e1c45d44397df738f60ea5b32efd)), closes [#21744](https://github.com/ionic-team/ionic/issues/21744)
|
||||
* **overlays:** return focus to presenting element after dismissal ([#22167](https://github.com/ionic-team/ionic/issues/22167)) ([cc45ad8](https://github.com/ionic-team/ionic/commit/cc45ad815c002c5d890f2e105c546b4c3b3a58c0)), closes [#21768](https://github.com/ionic-team/ionic/issues/21768)
|
||||
* **picker-column:** add cancelable check to avoid intervention error in chrome ([#22140](https://github.com/ionic-team/ionic/issues/22140)) ([a24a041](https://github.com/ionic-team/ionic/commit/a24a041064fd9ce6ca161d3522083d50e585e9dd)), closes [#22137](https://github.com/ionic-team/ionic/issues/22137)
|
||||
* **radio:** follow accessibility guidelines outlined by wai-aria ([#22113](https://github.com/ionic-team/ionic/issues/22113)) ([ea0e049](https://github.com/ionic-team/ionic/commit/ea0e0499e24865faad3d11f50f7037645f6cdcc8)), closes [#21743](https://github.com/ionic-team/ionic/issues/21743)
|
||||
* **reorder:** allow click event propagation when reorder group is disabled ([#21947](https://github.com/ionic-team/ionic/issues/21947)) ([baafe08](https://github.com/ionic-team/ionic/commit/baafe08927b7b858170496605781e6fa682e0147)), closes [#21017](https://github.com/ionic-team/ionic/issues/21017)
|
||||
* **segment:** do not allow text selection on desktop ([#22158](https://github.com/ionic-team/ionic/issues/22158)) ([1526bdf](https://github.com/ionic-team/ionic/commit/1526bdfb492c1fa8d71f8a1af8cd97abd9e62642))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **segment:** improve scrolling performance on ios when using segment ([#22110](https://github.com/ionic-team/ionic/issues/22110)) ([68afc49](https://github.com/ionic-team/ionic/commit/68afc49e9ed27acffb0b765b7be6b03e8574850d)), closes [#22095](https://github.com/ionic-team/ionic/issues/22095)
|
||||
|
||||
|
||||
|
||||
## [5.3.3](https://github.com/ionic-team/ionic/compare/v5.3.2...v5.3.3) (2020-09-17)
|
||||
|
||||
|
||||
@@ -60,7 +93,7 @@
|
||||
* **card:** expose global card css variables ([#21756](https://github.com/ionic-team/ionic/issues/21756)) ([096eef4](https://github.com/ionic-team/ionic/commit/096eef4a79c2d05c37eb224466c6d7d512d2be20)), closes [#21694](https://github.com/ionic-team/ionic/issues/21694)
|
||||
* **input:** accept datetime-local, month, and week type values ([#21758](https://github.com/ionic-team/ionic/issues/21758)) ([fa93dff](https://github.com/ionic-team/ionic/commit/fa93dffdb4f350e8db8acc7f06b06761974eea8e)), closes [#21757](https://github.com/ionic-team/ionic/issues/21757)
|
||||
* **input, textarea:** expose native events for ionBlur and ionFocus ([#21777](https://github.com/ionic-team/ionic/issues/21777)) ([a625c83](https://github.com/ionic-team/ionic/commit/a625c837a60abc07ad71c696196a89f1a25a4c27)), closes [#17363](https://github.com/ionic-team/ionic/issues/17363)
|
||||
* **react:** add custom history to IonReactRouter ([#21775](https://github.com/ionic-team/ionic/issues/21775)) ([d4a5fbd](https://github.com/ionic-team/ionic/commit/d4a5fbd955e8ecccba8b77491943d81fdf5a5ef4)), closes [#20297](https://github.com/ionic-team/ionic/issues/20297)
|
||||
* **react:** add custom history to IonReactRouter ([#21775](https://github.com/ionic-team/ionic/issues/21775)) ([d4a5fbd](https://github.com/ionic-team/ionic/commit/d4a5fbd955e8ecccba8b77491943d81fdf5a5ef4)), closes [#20297](https://github.com/ionic-team/ionic/issues/20297)
|
||||
* **react:** add new react router ([#21693](https://github.com/ionic-team/ionic/issues/21693)) ([c171ccb](https://github.com/ionic-team/ionic/commit/c171ccbd37c1ee4b4934758a3a759170ff357cb2))
|
||||
* **router:** add navigation hooks ([#21709](https://github.com/ionic-team/ionic/issues/21709)) ([77464ef](https://github.com/ionic-team/ionic/commit/77464ef21aaaa5afa7a02e5417f3ec295b240601))
|
||||
* **segment-button, toast:** expose additional shadow parts ([#21532](https://github.com/ionic-team/ionic/issues/21532)) ([a5e4669](https://github.com/ionic-team/ionic/commit/a5e4669c4bcbcb2cdd605ed17c35e42438bd5596))
|
||||
@@ -341,9 +374,9 @@
|
||||
* **modal:** presenting multiple card-style modals now adds border radius properly ([#20476](https://github.com/ionic-team/ionic/issues/20476)) ([abf594a](https://github.com/ionic-team/ionic/commit/abf594aa611562a76e3baecfa38456d41a1410f3)), closes [#20475](https://github.com/ionic-team/ionic/issues/20475)
|
||||
* **modal:** prevent card style modal styles from being overridden ([#20470](https://github.com/ionic-team/ionic/issues/20470)) ([86ab77a](https://github.com/ionic-team/ionic/commit/86ab77a6e2cb124510c244110fc78a4bc0654cd1)), closes [#20469](https://github.com/ionic-team/ionic/issues/20469)
|
||||
* **react:** do a better job matching up route to sync ([#20446](https://github.com/ionic-team/ionic/issues/20446)) ([c0aadd6](https://github.com/ionic-team/ionic/commit/c0aadd60077e5ba379959d93006e3a9c1418263c)), closes [#20363](https://github.com/ionic-team/ionic/issues/20363)
|
||||
* **react:** do not remove pages when navigating between tabs ([#20431](https://github.com/ionic-team/ionic/issues/20431)) ([b6fbe98](https://github.com/ionic-team/ionic/commit/b6fbe98812fbab5ef9e0723802605c0711581dd2)), closes [#20398](https://github.com/ionic-team/ionic/issues/20398)
|
||||
* **react:** do not remove pages when navigating between tabs ([#20431](https://github.com/ionic-team/ionic/issues/20431)) ([b6fbe98](https://github.com/ionic-team/ionic/commit/b6fbe98812fbab5ef9e0723802605c0711581dd2)), closes [#20398](https://github.com/ionic-team/ionic/issues/20398)
|
||||
* **react:** icons with MD set should work in browser ([#20463](https://github.com/ionic-team/ionic/issues/20463)) ([82670fe](https://github.com/ionic-team/ionic/commit/82670fe4d0592451cbc243b3008beb3f8f483c30))
|
||||
* **react:** update paths of tab buttons when href changes in ion buttons ([#20480](https://github.com/ionic-team/ionic/issues/20480)) ([45d03ba](https://github.com/ionic-team/ionic/commit/45d03baf981d0e10eb1fe689908532adef2ba31d)), closes [#20321](https://github.com/ionic-team/ionic/issues/20321)
|
||||
* **react:** update paths of tab buttons when href changes in ion buttons ([#20480](https://github.com/ionic-team/ionic/issues/20480)) ([45d03ba](https://github.com/ionic-team/ionic/commit/45d03baf981d0e10eb1fe689908532adef2ba31d)), closes [#20321](https://github.com/ionic-team/ionic/issues/20321)
|
||||
* **searchbar:** properly align placeholder ([#20460](https://github.com/ionic-team/ionic/issues/20460)) ([4d6e15a](https://github.com/ionic-team/ionic/commit/4d6e15ab18fc894c3826b89362163256adc227f4)), closes [#20456](https://github.com/ionic-team/ionic/issues/20456)
|
||||
* **segment:** border radius applies to indicator on ios ([#20541](https://github.com/ionic-team/ionic/issues/20541)) ([9b5854d](https://github.com/ionic-team/ionic/commit/9b5854d79712356f8a3772442c0cc412db09d5e0)), closes [#20539](https://github.com/ionic-team/ionic/issues/20539)
|
||||
* **segment:** do not show ripple effect if disabled via config ([#20542](https://github.com/ionic-team/ionic/issues/20542)) ([7a461c5](https://github.com/ionic-team/ionic/commit/7a461c59c5d9a23de0bcdd53397f452d17251fd6)), closes [#20533](https://github.com/ionic-team/ionic/issues/20533)
|
||||
|
||||
14
angular/package-lock.json
generated
14
angular/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/angular",
|
||||
"version": "5.3.3",
|
||||
"version": "5.3.5",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -147,9 +147,9 @@
|
||||
}
|
||||
},
|
||||
"@ionic/core": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.3.2.tgz",
|
||||
"integrity": "sha512-s/SDnS993fnZ3d6EzOlURHBbc2aI2/WsZSsCLgnJz3G+KUO4hY/2RQvdmtl0FZpDHMSyehG6tRgFFXvnFSI9CQ==",
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.3.4.tgz",
|
||||
"integrity": "sha512-4UVzj+Vd7o0VJ06dReG01PvttnLLPSzUVgXSYMBKKR849Pvuh5Q9t5s4GEEQgGoxhv1S6Ai+zphWGFMvviOyfw==",
|
||||
"requires": {
|
||||
"ionicons": "^5.1.2",
|
||||
"tslib": "^1.10.0"
|
||||
@@ -1640,9 +1640,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"ionicons": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-5.1.2.tgz",
|
||||
"integrity": "sha512-zO7ZgbBbXhpA7cXO2rDzTNdcCqErjg1Sprq/ossTvaiV0MriOjRE7JO3EGvYjDTPzF9YALGpvLXqCgsRT0tprA=="
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-5.2.1.tgz",
|
||||
"integrity": "sha512-dtw4rR7Sr2ssGHRrkTMESQ/p4gwovmHgafKvZsEeMjb712xjBOD3YSycy5kofS5RusU/IFVog8693BZiJ0ELzA=="
|
||||
},
|
||||
"is-accessor-descriptor": {
|
||||
"version": "0.1.6",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/angular",
|
||||
"version": "5.3.3",
|
||||
"version": "5.3.5",
|
||||
"description": "Angular specific wrappers for @ionic/core",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -42,7 +42,7 @@
|
||||
"validate": "npm i && npm run lint && npm run test && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ionic/core": "5.3.3",
|
||||
"@ionic/core": "5.3.5",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
"sync:build": "sh scripts/build-ionic.sh",
|
||||
"sync": "sh scripts/sync.sh",
|
||||
"build": "npm run sync && ng build --prod --no-progress",
|
||||
"test": "ng e2e --prod",
|
||||
"pretest": "webdriver-manager update --versions.chrome 85.0.4183.87",
|
||||
"test": "ng e2e --prod --webdriver-update=false",
|
||||
"test.dev": "npm run sync && ng e2e",
|
||||
"lint": "ng lint",
|
||||
"postinstall": "npm run sync && ngcc",
|
||||
|
||||
2
core/package-lock.json
generated
2
core/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "0.5.2",
|
||||
"version": "5.3.5",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "0.5.2",
|
||||
"version": "5.3.5",
|
||||
"description": "Base components for Ionic",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
|
||||
6
core/src/components.d.ts
vendored
6
core/src/components.d.ts
vendored
@@ -1703,6 +1703,8 @@ export namespace Components {
|
||||
* The name of the control, which is submitted with the form data.
|
||||
*/
|
||||
"name": string;
|
||||
"setButtonTabindex": (value: number) => Promise<void>;
|
||||
"setFocus": () => Promise<void>;
|
||||
/**
|
||||
* the value of the radio.
|
||||
*/
|
||||
@@ -4440,6 +4442,10 @@ declare namespace LocalJSX {
|
||||
* The mode determines which platform styles to use.
|
||||
*/
|
||||
"mode"?: "ios" | "md";
|
||||
/**
|
||||
* Emitted when the color changes.
|
||||
*/
|
||||
"onIonColor"?: (event: CustomEvent<StyleEventDetail>) => void;
|
||||
/**
|
||||
* Emitted when the styles change.
|
||||
*/
|
||||
|
||||
@@ -286,6 +286,7 @@ export class ActionSheetExample {
|
||||
<script>
|
||||
import { IonButton, actionSheetController } from '@ionic/vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { caretForwardCircle, close, heart, trash, share } from 'ionicons/icons';
|
||||
|
||||
export default defineComponent({
|
||||
components: { IonButton },
|
||||
@@ -299,35 +300,35 @@ export default defineComponent({
|
||||
{
|
||||
text: 'Delete',
|
||||
role: 'destructive',
|
||||
icon: 'trash',
|
||||
icon: trash,
|
||||
handler: () => {
|
||||
console.log('Delete clicked')
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Share',
|
||||
icon: 'share',
|
||||
icon: share,
|
||||
handler: () => {
|
||||
console.log('Share clicked')
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Play (open modal)',
|
||||
icon: 'caret-forward-circle',
|
||||
icon: caretForwardCircle,
|
||||
handler: () => {
|
||||
console.log('Play clicked')
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Favorite',
|
||||
icon: 'heart',
|
||||
icon: heart,
|
||||
handler: () => {
|
||||
console.log('Favorite clicked')
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Cancel',
|
||||
icon: 'close',
|
||||
icon: close,
|
||||
role: 'cancel',
|
||||
handler: () => {
|
||||
console.log('Cancel clicked')
|
||||
@@ -342,6 +343,77 @@ export default defineComponent({
|
||||
</script>
|
||||
```
|
||||
|
||||
Developers can also use this component directly in their template:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<ion-button @click="setOpen(true)">Show Action Sheet</ion-button>
|
||||
<ion-action-sheet
|
||||
:is-open="isOpenRef"
|
||||
header="Albums"
|
||||
css-class="my-custom-class"
|
||||
:buttons="buttons"
|
||||
@onDidDismiss="setOpen(false)"
|
||||
>
|
||||
</ion-action-sheet>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IonActionSheet, IonButton } from '@ionic/vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import { caretForwardCircle, close, heart, trash, share } from 'ionicons/icons';
|
||||
|
||||
export default defineComponent({
|
||||
components: { IonActionSheet, IonButton },
|
||||
setup() {
|
||||
const isOpenRef = ref(false);
|
||||
const setOpen = (state: boolean) => isOpenRef.value = state;
|
||||
const buttons = [
|
||||
{
|
||||
text: 'Delete',
|
||||
role: 'destructive',
|
||||
icon: trash,
|
||||
handler: () => {
|
||||
console.log('Delete clicked')
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Share',
|
||||
icon: share,
|
||||
handler: () => {
|
||||
console.log('Share clicked')
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Play (open modal)',
|
||||
icon: caretForwardCircle,
|
||||
handler: () => {
|
||||
console.log('Play clicked')
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Favorite',
|
||||
icon: heart,
|
||||
handler: () => {
|
||||
console.log('Favorite clicked')
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Cancel',
|
||||
icon: close,
|
||||
role: 'cancel',
|
||||
handler: () => {
|
||||
console.log('Cancel clicked')
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return { buttons, isOpenRef, setOpen }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<script>
|
||||
import { IonButton, actionSheetController } from '@ionic/vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { caretForwardCircle, close, heart, trash, share } from 'ionicons/icons';
|
||||
|
||||
export default defineComponent({
|
||||
components: { IonButton },
|
||||
@@ -19,35 +20,35 @@ export default defineComponent({
|
||||
{
|
||||
text: 'Delete',
|
||||
role: 'destructive',
|
||||
icon: 'trash',
|
||||
icon: trash,
|
||||
handler: () => {
|
||||
console.log('Delete clicked')
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Share',
|
||||
icon: 'share',
|
||||
icon: share,
|
||||
handler: () => {
|
||||
console.log('Share clicked')
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Play (open modal)',
|
||||
icon: 'caret-forward-circle',
|
||||
icon: caretForwardCircle,
|
||||
handler: () => {
|
||||
console.log('Play clicked')
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Favorite',
|
||||
icon: 'heart',
|
||||
icon: heart,
|
||||
handler: () => {
|
||||
console.log('Favorite clicked')
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Cancel',
|
||||
icon: 'close',
|
||||
icon: close,
|
||||
role: 'cancel',
|
||||
handler: () => {
|
||||
console.log('Cancel clicked')
|
||||
@@ -61,3 +62,74 @@ export default defineComponent({
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
Developers can also use this component directly in their template:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<ion-button @click="setOpen(true)">Show Action Sheet</ion-button>
|
||||
<ion-action-sheet
|
||||
:is-open="isOpenRef"
|
||||
header="Albums"
|
||||
css-class="my-custom-class"
|
||||
:buttons="buttons"
|
||||
@onDidDismiss="setOpen(false)"
|
||||
>
|
||||
</ion-action-sheet>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IonActionSheet, IonButton } from '@ionic/vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import { caretForwardCircle, close, heart, trash, share } from 'ionicons/icons';
|
||||
|
||||
export default defineComponent({
|
||||
components: { IonActionSheet, IonButton },
|
||||
setup() {
|
||||
const isOpenRef = ref(false);
|
||||
const setOpen = (state: boolean) => isOpenRef.value = state;
|
||||
const buttons = [
|
||||
{
|
||||
text: 'Delete',
|
||||
role: 'destructive',
|
||||
icon: trash,
|
||||
handler: () => {
|
||||
console.log('Delete clicked')
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Share',
|
||||
icon: share,
|
||||
handler: () => {
|
||||
console.log('Share clicked')
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Play (open modal)',
|
||||
icon: caretForwardCircle,
|
||||
handler: () => {
|
||||
console.log('Play clicked')
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Favorite',
|
||||
icon: heart,
|
||||
handler: () => {
|
||||
console.log('Favorite clicked')
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Cancel',
|
||||
icon: close,
|
||||
role: 'cancel',
|
||||
handler: () => {
|
||||
console.log('Cancel clicked')
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return { buttons, isOpenRef, setOpen }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface AlertInput {
|
||||
max?: string | number;
|
||||
cssClass?: string | string[];
|
||||
attributes?: AlertInputAttributes | AlertTextareaAttributes;
|
||||
tabindex?: number;
|
||||
}
|
||||
|
||||
export interface AlertTextareaAttributes extends JSXBase.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, Watch, forceUpdate, h } from '@stencil/core';
|
||||
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Listen, Method, Prop, Watch, forceUpdate, h } from '@stencil/core';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import { AlertButton, AlertInput, AlertInputAttributes, AlertTextareaAttributes, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface';
|
||||
@@ -130,6 +130,59 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
*/
|
||||
@Event({ eventName: 'ionAlertDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
@Listen('keydown', { target: 'document' })
|
||||
onKeydown(ev: any) {
|
||||
const inputTypes = new Set(this.processedInputs.map(i => i.type));
|
||||
|
||||
// The only inputs we want to navigate between using arrow keys are the radios
|
||||
// ignore the keydown event if it is not on a radio button
|
||||
if (
|
||||
!inputTypes.has('radio')
|
||||
|| (ev.target && !this.el.contains(ev.target))
|
||||
|| ev.target.classList.contains('alert-button'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all radios inside of the radio group and then
|
||||
// filter out disabled radios since we need to skip those
|
||||
const query = this.el.querySelectorAll('.alert-radio') as NodeListOf<HTMLButtonElement>;
|
||||
const radios = Array.from(query).filter(radio => !radio.disabled);
|
||||
|
||||
// The focused radio is the one that shares the same id as
|
||||
// the event target
|
||||
const index = radios.findIndex(radio => radio.id === ev.target.id);
|
||||
|
||||
// We need to know what the next radio element should
|
||||
// be in order to change the focus
|
||||
let nextEl: HTMLButtonElement | undefined;
|
||||
|
||||
// If hitting arrow down or arrow right, move to the next radio
|
||||
// If we're on the last radio, move to the first radio
|
||||
if (['ArrowDown', 'ArrowRight'].includes(ev.key)) {
|
||||
nextEl = (index === radios.length - 1)
|
||||
? radios[0]
|
||||
: radios[index + 1];
|
||||
}
|
||||
|
||||
// If hitting arrow up or arrow left, move to the previous radio
|
||||
// If we're on the first radio, move to the last radio
|
||||
if (['ArrowUp', 'ArrowLeft'].includes(ev.key)) {
|
||||
nextEl = (index === 0)
|
||||
? radios[radios.length - 1]
|
||||
: radios[index - 1]
|
||||
}
|
||||
|
||||
if (nextEl && radios.includes(nextEl)) {
|
||||
const nextProcessed = this.processedInputs.find(input => input.id === nextEl?.id);
|
||||
|
||||
if (nextProcessed) {
|
||||
this.rbClick(nextProcessed);
|
||||
nextEl.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('buttons')
|
||||
buttonsChanged() {
|
||||
const buttons = this.buttons;
|
||||
@@ -144,12 +197,21 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
inputsChanged() {
|
||||
const inputs = this.inputs;
|
||||
|
||||
// Get the first input that is not disabled and the checked one
|
||||
// If an enabled checked input exists, set it to be the focusable input
|
||||
// otherwise we default to focus the first input
|
||||
// This will only be used when the input is type radio
|
||||
const first = inputs.find(input => !input.disabled);
|
||||
const checked = inputs.find(input => input.checked && !input.disabled);
|
||||
const focusable = checked || first;
|
||||
|
||||
// An alert can be created with several different inputs. Radios,
|
||||
// checkboxes and inputs are all accepted, but they cannot be mixed.
|
||||
const inputTypes = new Set(inputs.map(i => i.type));
|
||||
if (inputTypes.has('checkbox') && inputTypes.has('radio')) {
|
||||
console.warn(`Alert cannot mix input types: ${(Array.from(inputTypes.values()).join('/'))}. Please see alert docs for more info.`);
|
||||
}
|
||||
|
||||
this.inputType = inputTypes.values().next().value;
|
||||
this.processedInputs = inputs.map((i, index) => ({
|
||||
type: i.type || 'text',
|
||||
@@ -165,6 +227,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
max: i.max,
|
||||
cssClass: i.cssClass || '',
|
||||
attributes: i.attributes || {},
|
||||
tabindex: (i.type === 'radio' && i !== focusable) ? -1 : 0
|
||||
}) as AlertInput);
|
||||
}
|
||||
|
||||
@@ -241,6 +304,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
private rbClick(selectedInput: AlertInput) {
|
||||
for (const input of this.processedInputs) {
|
||||
input.checked = input === selectedInput;
|
||||
input.tabindex = input === selectedInput ? 0 : -1;
|
||||
}
|
||||
this.activeId = selectedInput.id;
|
||||
safeCall(selectedInput.handler, selectedInput);
|
||||
@@ -333,7 +397,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
aria-checked={`${i.checked}`}
|
||||
id={i.id}
|
||||
disabled={i.disabled}
|
||||
tabIndex={0}
|
||||
tabIndex={i.tabindex}
|
||||
role="checkbox"
|
||||
class={{
|
||||
...getClassMap(i.cssClass),
|
||||
@@ -373,7 +437,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
aria-checked={`${i.checked}`}
|
||||
disabled={i.disabled}
|
||||
id={i.id}
|
||||
tabIndex={0}
|
||||
tabIndex={i.tabindex}
|
||||
class={{
|
||||
...getClassMap(i.cssClass),
|
||||
'alert-radio-button': true,
|
||||
@@ -411,7 +475,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
placeholder={i.placeholder}
|
||||
value={i.value}
|
||||
id={i.id}
|
||||
tabIndex={0}
|
||||
tabIndex={i.tabindex}
|
||||
{...i.attributes as AlertTextareaAttributes}
|
||||
disabled={i.attributes?.disabled ?? i.disabled}
|
||||
class={inputClass(i)}
|
||||
@@ -432,7 +496,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
|
||||
max={i.max}
|
||||
value={i.value}
|
||||
id={i.id}
|
||||
tabIndex={0}
|
||||
tabIndex={i.tabindex}
|
||||
{...i.attributes as AlertInputAttributes}
|
||||
disabled={i.attributes?.disabled ?? i.disabled}
|
||||
class={inputClass(i)}
|
||||
|
||||
@@ -1419,6 +1419,40 @@ export default defineComponent({
|
||||
</script>
|
||||
```
|
||||
|
||||
Developers can also use this component directly in their template:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<ion-button @click="setOpen(true)">Show Alert</ion-button>
|
||||
<ion-alert
|
||||
:is-open="isOpenRef"
|
||||
header="Alert"
|
||||
sub-header="Subtitle"
|
||||
message="This is an alert message."
|
||||
css-class="my-custom-class"
|
||||
:buttons="buttons"
|
||||
@onDidDismiss="setOpen(false)"
|
||||
>
|
||||
</ion-alert>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IonAlert, IonButton } from '@ionic/vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { IonAlert, IonButton },
|
||||
setup() {
|
||||
const isOpenRef = ref(false);
|
||||
const setOpen = (state: boolean) => isOpenRef.value = state;
|
||||
const buttons = ['Ok'];
|
||||
|
||||
return { buttons, isOpenRef, setOpen }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -206,7 +206,6 @@
|
||||
type: 'radio',
|
||||
label: 'Radio 1',
|
||||
value: 'value1',
|
||||
checked: true
|
||||
},
|
||||
{
|
||||
type: 'radio',
|
||||
@@ -216,7 +215,8 @@
|
||||
{
|
||||
type: 'radio',
|
||||
label: 'Radio 3',
|
||||
value: 'value3'
|
||||
value: 'value3',
|
||||
checked: true
|
||||
},
|
||||
{
|
||||
type: 'radio',
|
||||
@@ -241,12 +241,12 @@
|
||||
role: 'cancel',
|
||||
cssClass: 'secondary',
|
||||
handler: () => {
|
||||
console.log('Confirm Cancel')
|
||||
console.log('Confirm Cancel');
|
||||
}
|
||||
}, {
|
||||
text: 'Ok',
|
||||
handler: () => {
|
||||
console.log('Confirm Ok')
|
||||
handler: (ev) => {
|
||||
console.log('Confirm Ok', ev);
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -263,3 +263,37 @@ export default defineComponent({
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
Developers can also use this component directly in their template:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<ion-button @click="setOpen(true)">Show Alert</ion-button>
|
||||
<ion-alert
|
||||
:is-open="isOpenRef"
|
||||
header="Alert"
|
||||
sub-header="Subtitle"
|
||||
message="This is an alert message."
|
||||
css-class="my-custom-class"
|
||||
:buttons="buttons"
|
||||
@onDidDismiss="setOpen(false)"
|
||||
>
|
||||
</ion-alert>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IonAlert, IonButton } from '@ionic/vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { IonAlert, IonButton },
|
||||
setup() {
|
||||
const isOpenRef = ref(false);
|
||||
const setOpen = (state: boolean) => isOpenRef.value = state;
|
||||
const buttons = ['Ok'];
|
||||
|
||||
return { buttons, isOpenRef, setOpen }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -137,7 +137,7 @@ export class Button implements ComponentInterface, AnchorInterface, ButtonInterf
|
||||
}
|
||||
|
||||
private get hasIconOnly() {
|
||||
return !!this.el.querySelector('ion-icon[slot="icon-only"]');
|
||||
return !!this.el.querySelector('[slot="icon-only"]');
|
||||
}
|
||||
|
||||
private get rippleType() {
|
||||
|
||||
@@ -17,6 +17,14 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Button - Icon</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button>
|
||||
<ion-icon slot="icon-only" name="heart"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button class="avatar-button">
|
||||
<img slot="icon-only" src="/src/components/avatar/test/avatar.svg">
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
@@ -26,8 +34,6 @@
|
||||
<ion-icon name="home" slot="start"></ion-icon>
|
||||
Left Icon
|
||||
</ion-button>
|
||||
</p>
|
||||
<p>
|
||||
<ion-button href="#">
|
||||
<ion-icon name="home" slot="start"></ion-icon>
|
||||
Left Icon
|
||||
@@ -38,8 +44,6 @@
|
||||
<ion-icon name="star" slot="end"></ion-icon>
|
||||
Right Icon
|
||||
</ion-button>
|
||||
</p>
|
||||
<p>
|
||||
<ion-button href="#">
|
||||
<ion-icon name="star" slot="end"></ion-icon>
|
||||
Right Icon
|
||||
@@ -49,8 +53,6 @@
|
||||
<ion-button>
|
||||
<ion-icon name="flag" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</p>
|
||||
<p>
|
||||
<ion-button href="#">
|
||||
<ion-icon name="flag" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
@@ -60,8 +62,6 @@
|
||||
<ion-icon name="help-circle" slot="start"></ion-icon>
|
||||
Left, Large
|
||||
</ion-button>
|
||||
</p>
|
||||
<p>
|
||||
<ion-button size="large" href="#">
|
||||
<ion-icon name="help-circle" slot="start"></ion-icon>
|
||||
Left, Large
|
||||
@@ -72,8 +72,6 @@
|
||||
<ion-icon name="settings" slot="end"></ion-icon>
|
||||
Right, Large
|
||||
</ion-button>
|
||||
</p>
|
||||
<p>
|
||||
<ion-button size="large" href="#">
|
||||
<ion-icon name="settings" slot="end"></ion-icon>
|
||||
Right, Large
|
||||
@@ -83,8 +81,6 @@
|
||||
<ion-button size="large">
|
||||
<ion-icon name="heart" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</p>
|
||||
<p>
|
||||
<ion-button size="large" href="#">
|
||||
<ion-icon name="heart" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
@@ -94,8 +90,6 @@
|
||||
<ion-icon name="checkmark" slot="start"></ion-icon>
|
||||
Left, Small
|
||||
</ion-button>
|
||||
</p>
|
||||
<p>
|
||||
<ion-button size="small" href="#">
|
||||
<ion-icon name="checkmark" slot="start"></ion-icon>
|
||||
Left, Small
|
||||
@@ -106,8 +100,6 @@
|
||||
<ion-icon name="arrow-forward" slot="end"></ion-icon>
|
||||
Right, Small
|
||||
</ion-button>
|
||||
</p>
|
||||
<p>
|
||||
<ion-button size="small" href="#">
|
||||
<ion-icon name="arrow-forward" slot="end"></ion-icon>
|
||||
Right, Small
|
||||
@@ -117,8 +109,6 @@
|
||||
<ion-button size="small">
|
||||
<ion-icon name="search" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</p>
|
||||
<p>
|
||||
<ion-button size="small" href="#">
|
||||
<ion-icon name="search" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
@@ -128,4 +118,15 @@
|
||||
</ion-app>
|
||||
</body>
|
||||
|
||||
<style>
|
||||
.avatar-button img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.ios .avatar-button img {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
</style>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -220,7 +220,7 @@ describe('datetime-util', () => {
|
||||
"second": undefined,
|
||||
"tzOffset": 0,
|
||||
"year": 1000,
|
||||
"ampm": "am"
|
||||
"ampm": undefined
|
||||
});
|
||||
});
|
||||
|
||||
@@ -235,7 +235,7 @@ describe('datetime-util', () => {
|
||||
"second": undefined,
|
||||
"tzOffset": 0,
|
||||
"year": undefined,
|
||||
"ampm": "pm"
|
||||
"ampm": undefined
|
||||
});
|
||||
});
|
||||
|
||||
@@ -250,7 +250,7 @@ describe('datetime-util', () => {
|
||||
"second": 20,
|
||||
"tzOffset": 0,
|
||||
"year": 1994,
|
||||
"ampm": "pm"
|
||||
"ampm": undefined
|
||||
});
|
||||
});
|
||||
|
||||
@@ -265,7 +265,7 @@ describe('datetime-util', () => {
|
||||
"second": undefined,
|
||||
"tzOffset": 0,
|
||||
"year": 2018,
|
||||
"ampm": "am"
|
||||
"ampm": undefined
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,10 +3,16 @@
|
||||
* Defaults to the current date if
|
||||
* no date given
|
||||
*/
|
||||
export const getDateValue = (date: DatetimeData, format: string): number => {
|
||||
export const getDateValue = (date: DatetimeData, format: string): number | string => {
|
||||
const getValue = getValueFromFormat(date, format);
|
||||
|
||||
if (getValue !== undefined) { return getValue; }
|
||||
if (getValue !== undefined) {
|
||||
if (format === FORMAT_A || format === FORMAT_a) {
|
||||
date.ampm = getValue;
|
||||
}
|
||||
|
||||
return getValue;
|
||||
}
|
||||
|
||||
const defaultDate = parseDate(new Date().toISOString());
|
||||
return getValueFromFormat((defaultDate as DatetimeData), format);
|
||||
@@ -238,7 +244,6 @@ export const parseDate = (val: string | undefined | null): DatetimeData | undefi
|
||||
second: parse[6],
|
||||
millisecond: parse[7],
|
||||
tzOffset,
|
||||
ampm: parse[4] >= 12 ? 'pm' : 'am'
|
||||
};
|
||||
};
|
||||
|
||||
@@ -326,24 +331,6 @@ export const updateDate = (existingData: DatetimeData, newData: any, displayTime
|
||||
// newData is from the datetime picker's selected values
|
||||
// update the existing datetimeValue with the new values
|
||||
if (newData.ampm !== undefined && newData.hour !== undefined) {
|
||||
// If the date we came from exists, we need to change the meridiem value when
|
||||
// going to and from 12
|
||||
if (existingData.ampm !== undefined && existingData.hour !== undefined) {
|
||||
// If the existing meridiem is am, we want to switch to pm if it is either
|
||||
// A) coming from 0 (12 am)
|
||||
// B) going to 12 (12 pm)
|
||||
if (existingData.ampm === 'am' && (existingData.hour === 0 || newData.hour.value === 12)) {
|
||||
newData.ampm.value = 'pm';
|
||||
}
|
||||
|
||||
// If the existing meridiem is pm, we want to switch to am if it is either
|
||||
// A) coming from 12 (12 pm)
|
||||
// B) going to 12 (12 am)
|
||||
if (existingData.ampm === 'pm' && (existingData.hour === 12 || newData.hour.value === 12)) {
|
||||
newData.ampm.value = 'am';
|
||||
}
|
||||
}
|
||||
|
||||
// change the value of the hour based on whether or not it is am or pm
|
||||
// if the meridiem is pm and equal to 12, it remains 12
|
||||
// otherwise we add 12 to the hour value
|
||||
|
||||
@@ -7,14 +7,12 @@ const getActiveElementText = async (page) => {
|
||||
|
||||
test('datetime/picker: focus trap', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/datetime/test/basic?ionic:_testing=true' });
|
||||
|
||||
await page.click('#datetime-part');
|
||||
await page.waitForSelector('#datetime-part');
|
||||
|
||||
let datetime = await page.find('ion-datetime');
|
||||
|
||||
expect(datetime).not.toBe(null);
|
||||
await datetime.waitForVisible();
|
||||
|
||||
// TODO fix
|
||||
await page.waitFor(100);
|
||||
|
||||
@@ -113,8 +113,8 @@
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>HH:mm</ion-label>
|
||||
<ion-datetime display-format="HH:mm"></ion-datetime>
|
||||
<ion-label>HH:mm A</ion-label>
|
||||
<ion-datetime display-format="HH:mm A"></ion-datetime>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
@@ -127,6 +127,11 @@
|
||||
<ion-datetime display-format="h:mm a" value="01:47"></ion-datetime>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>h:mm A</ion-label>
|
||||
<ion-datetime display-format="h:mm A" value="14:23"></ion-datetime>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>hh:mm A (15 min steps)</ion-label>
|
||||
<ion-datetime display-format="h:mm A" minute-values="0,15,30,45"></ion-datetime>
|
||||
|
||||
@@ -30,6 +30,38 @@ describe('Datetime', () => {
|
||||
expect(monthvalue).toEqual(date.getMonth() + 1);
|
||||
expect(yearValue).toEqual(date.getFullYear());
|
||||
});
|
||||
|
||||
it('it should return the date value for a given time', () => {
|
||||
const dateTimeData: DatetimeData = {
|
||||
hour: 2,
|
||||
minute: 23,
|
||||
tzOffset: 0
|
||||
};
|
||||
|
||||
const hourValue = getDateValue(dateTimeData, 'hh');
|
||||
const minuteValue = getDateValue(dateTimeData, 'mm');
|
||||
const ampmValue = getDateValue(dateTimeData, 'A');
|
||||
|
||||
expect(hourValue).toEqual(2);
|
||||
expect(minuteValue).toEqual(23);
|
||||
expect(ampmValue).toEqual("am");
|
||||
});
|
||||
|
||||
it('it should return the date value for a given time after 12', () => {
|
||||
const dateTimeData: DatetimeData = {
|
||||
hour: 16,
|
||||
minute: 47,
|
||||
tzOffset: 0
|
||||
};
|
||||
|
||||
const hourValue = getDateValue(dateTimeData, 'hh');
|
||||
const minuteValue = getDateValue(dateTimeData, 'mm');
|
||||
const ampmValue = getDateValue(dateTimeData, 'a');
|
||||
|
||||
expect(hourValue).toEqual(4);
|
||||
expect(minuteValue).toEqual(47);
|
||||
expect(ampmValue).toEqual("pm");
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDateTime()', () => {
|
||||
|
||||
@@ -204,6 +204,15 @@
|
||||
}
|
||||
|
||||
|
||||
// iOS Fixed Labels
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.item-label-fixed) ::slotted(ion-select),
|
||||
:host(.item-label-fixed) ::slotted(ion-datetime) {
|
||||
--padding-start: 0;
|
||||
}
|
||||
|
||||
|
||||
// FROM TEXTAREA
|
||||
// iOS Stacked & Floating Textarea
|
||||
// --------------------------------------------------
|
||||
|
||||
@@ -212,7 +212,6 @@
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Material Design Floating/Stacked Label
|
||||
// --------------------------------------------------
|
||||
|
||||
@@ -222,6 +221,15 @@
|
||||
}
|
||||
|
||||
|
||||
// Material Design Fixed Labels
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.item-label-fixed) ::slotted(ion-select),
|
||||
:host(.item-label-fixed) ::slotted(ion-datetime) {
|
||||
--padding-start: 8px;
|
||||
}
|
||||
|
||||
|
||||
// Material Design Toggle/Radio Item
|
||||
// --------------------------------------------------
|
||||
|
||||
@@ -274,3 +282,14 @@
|
||||
:host(.item-has-focus:not(.ion-color)) ::slotted(.label-floating) {
|
||||
color: $label-md-text-color-focused;
|
||||
}
|
||||
|
||||
// Material Design Inputs: Highlight Color
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.ion-color) {
|
||||
--highlight-color-focused: #{current-color(contrast)};
|
||||
}
|
||||
|
||||
:host(.item-label-color) {
|
||||
--highlight-color-focused: #{current-color(base)};
|
||||
}
|
||||
|
||||
@@ -407,8 +407,16 @@ button, a {
|
||||
// Item Select
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(:not(.item-label)) ::slotted(ion-select) {
|
||||
--padding-start: 0;
|
||||
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
:host(.item-label-stacked) ::slotted(ion-select),
|
||||
:host(.item-label-floating) ::slotted(ion-select) {
|
||||
--padding-top: 8px;
|
||||
--padding-bottom: 8px;
|
||||
--padding-start: 0;
|
||||
|
||||
align-self: stretch;
|
||||
@@ -422,6 +430,10 @@ button, a {
|
||||
// Item Datetime
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(:not(.item-label)) ::slotted(ion-datetime) {
|
||||
--padding-start: 0;
|
||||
}
|
||||
|
||||
:host(.item-label-stacked) ::slotted(ion-datetime),
|
||||
:host(.item-label-floating) ::slotted(ion-datetime) {
|
||||
--padding-start: 0;
|
||||
@@ -463,4 +475,4 @@ button, a {
|
||||
|
||||
ion-ripple-effect {
|
||||
color: var(--ripple-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { createColorClasses, hostContext, openURL } from '../../utils/theme';
|
||||
})
|
||||
export class Item implements ComponentInterface, AnchorInterface, ButtonInterface {
|
||||
|
||||
private labelColorStyles = {};
|
||||
private itemStyles = new Map<string, CssClassMap>();
|
||||
|
||||
@Element() el!: HTMLIonItemElement;
|
||||
@@ -111,6 +112,18 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
|
||||
*/
|
||||
@Prop() type: 'submit' | 'reset' | 'button' = 'button';
|
||||
|
||||
@Listen('ionColor')
|
||||
labelColorChanged(ev: CustomEvent<string>) {
|
||||
const { color } = this;
|
||||
|
||||
// There will be a conflict with item color if
|
||||
// we apply the label color to item, so we ignore
|
||||
// the label color if the user sets a color on item
|
||||
if (color === undefined) {
|
||||
this.labelColorStyles = ev.detail;
|
||||
}
|
||||
}
|
||||
|
||||
@Listen('ionStyle')
|
||||
itemStyle(ev: CustomEvent<StyleEventDetail>) {
|
||||
ev.stopPropagation();
|
||||
@@ -212,7 +225,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
|
||||
}
|
||||
|
||||
render() {
|
||||
const { detail, detailIcon, download, lines, disabled, href, rel, target, routerAnimation, routerDirection } = this;
|
||||
const { detail, detailIcon, download, labelColorStyles, lines, disabled, href, rel, target, routerAnimation, routerDirection } = this;
|
||||
const childStyles = {};
|
||||
const mode = getIonMode(this);
|
||||
const clickable = this.isClickable();
|
||||
@@ -236,6 +249,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
|
||||
aria-disabled={disabled ? 'true' : null}
|
||||
class={{
|
||||
...childStyles,
|
||||
...labelColorStyles,
|
||||
...createColorClasses(this.color, {
|
||||
'item': true,
|
||||
[mode]: true,
|
||||
|
||||
19
core/src/components/item/test/alignment/e2e.ts
Normal file
19
core/src/components/item/test/alignment/e2e.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { newE2EPage } from '@stencil/core/testing';
|
||||
|
||||
test('item: alignment', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/item/test/alignment?ionic:_testing=true'
|
||||
});
|
||||
|
||||
const compare = await page.compareScreenshot();
|
||||
expect(compare).toMatchScreenshot();
|
||||
});
|
||||
|
||||
test('item: alignment-rtl', async () => {
|
||||
const page = await newE2EPage({
|
||||
url: '/src/components/item/test/alignment?ionic:_testing=true&rtl=true'
|
||||
});
|
||||
|
||||
const compare = await page.compareScreenshot();
|
||||
expect(compare).toMatchScreenshot();
|
||||
});
|
||||
135
core/src/components/item/test/alignment/index.html
Normal file
135
core/src/components/item/test/alignment/index.html
Normal file
@@ -0,0 +1,135 @@
|
||||
<!DOCTYPE html>
|
||||
<html dir="ltr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Item - Alignment</title>
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Item - Alignment</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-list-header>Leading Icons</ion-list-header>
|
||||
<ion-item>
|
||||
<ion-icon slot="start" name="time"></ion-icon>
|
||||
<ion-datetime display-format="DDDD MMMM D YYYY hh:mm:ss a" value="2019-10-01T15:43:40.394Z"></ion-datetime>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-icon slot="start" name="radio-button-off"></ion-icon>
|
||||
<ion-input placeholder="Choose Starting Point"></ion-input>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-icon slot="start" name="pin"></ion-icon>
|
||||
<ion-select placeholder="Choose Really Really Long Destination Here">
|
||||
<ion-select-option>Madison, WI</ion-select-option>
|
||||
<ion-select-option>Atlanta, GA</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list>
|
||||
<ion-list-header>Default Labels</ion-list-header>
|
||||
<ion-item>
|
||||
<ion-label>Time</ion-label>
|
||||
<ion-datetime display-format="DDDD MMMM D YYYY hh:mm:ss a" value="2019-10-01T15:43:40.394Z"></ion-datetime>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>From</ion-label>
|
||||
<ion-input placeholder="Choose Starting Point"></ion-input>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label>Destination</ion-label>
|
||||
<ion-select placeholder="Choose Really Really Long Destination Here">
|
||||
<ion-select-option>Madison, WI</ion-select-option>
|
||||
<ion-select-option>Atlanta, GA</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list>
|
||||
<ion-list-header>Fixed Labels</ion-list-header>
|
||||
<ion-item>
|
||||
<ion-label position="fixed">Time</ion-label>
|
||||
<ion-datetime display-format="DDDD MMMM D YYYY hh:mm:ss a" value="2019-10-01T15:43:40.394Z"></ion-datetime>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="fixed">From</ion-label>
|
||||
<ion-input placeholder="Choose Starting Point"></ion-input>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="fixed">Destination</ion-label>
|
||||
<ion-select placeholder="Choose Really Really Long Destination Here">
|
||||
<ion-select-option>Madison, WI</ion-select-option>
|
||||
<ion-select-option>Atlanta, GA</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list>
|
||||
<ion-list-header>Floating Labels</ion-list-header>
|
||||
<ion-item>
|
||||
<ion-label position="floating">Time</ion-label>
|
||||
<ion-datetime display-format="DDDD MMMM D YYYY hh:mm:ss a"></ion-datetime>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="floating">From</ion-label>
|
||||
<ion-input></ion-input>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="floating">Destination</ion-label>
|
||||
<ion-select>
|
||||
<ion-select-option>Madison, WI</ion-select-option>
|
||||
<ion-select-option>Atlanta, GA</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list>
|
||||
<ion-list-header>Stacked Labels</ion-list-header>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">Time</ion-label>
|
||||
<ion-datetime display-format="DDDD MMMM D YYYY hh:mm:ss a" value="2019-10-01T15:43:40.394Z"></ion-datetime>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">From</ion-label>
|
||||
<ion-input placeholder="Choose Starting Point"></ion-input>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">Destination</ion-label>
|
||||
<ion-select placeholder="Choose Really Really Long Destination Here">
|
||||
<ion-select-option>Madison, WI</ion-select-option>
|
||||
<ion-select-option>Atlanta, GA</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
<style>
|
||||
ion-item {
|
||||
--border-width: 0;
|
||||
--inner-border-width: 0;
|
||||
--background: #ddd;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -38,21 +38,22 @@
|
||||
@include margin(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
:host-context(.item-select).label-floating {
|
||||
@include transform(translate3d(0, 130%, 0));
|
||||
}
|
||||
|
||||
:host-context(.item-has-focus).label-floating,
|
||||
:host-context(.item-has-placeholder).label-floating,
|
||||
:host-context(.item-has-value).label-floating {
|
||||
@include transform(translateY(50%), scale(.75));
|
||||
}
|
||||
|
||||
:host-context(.item-has-focus).label-stacked,
|
||||
:host-context(.item-has-focus).label-floating {
|
||||
:host-context(.item-has-focus).label-stacked:not(.ion-color),
|
||||
:host-context(.item-has-focus).label-floating:not(.ion-color) {
|
||||
color: $label-md-text-color-focused;
|
||||
}
|
||||
|
||||
:host-context(.item-has-focus.ion-color).label-stacked:not(.ion-color),
|
||||
:host-context(.item-has-focus.ion-color).label-floating:not(.ion-color) {
|
||||
color: #{current-color(contrast)};
|
||||
}
|
||||
|
||||
|
||||
// MD Typography
|
||||
// --------------------------------------------------
|
||||
|
||||
@@ -32,6 +32,12 @@ export class Label implements ComponentInterface {
|
||||
*/
|
||||
@Prop() position?: 'fixed' | 'stacked' | 'floating';
|
||||
|
||||
/**
|
||||
* Emitted when the color changes.
|
||||
* @internal
|
||||
*/
|
||||
@Event() ionColor!: EventEmitter<StyleEventDetail>;
|
||||
|
||||
/**
|
||||
* Emitted when the styles change.
|
||||
* @internal
|
||||
@@ -44,6 +50,7 @@ export class Label implements ComponentInterface {
|
||||
this.inRange = !!this.el.closest('ion-range');
|
||||
this.noAnimate = (this.position === 'floating');
|
||||
this.emitStyle();
|
||||
this.emitColor();
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
@@ -54,11 +61,25 @@ export class Label implements ComponentInterface {
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('color')
|
||||
colorChanged() {
|
||||
this.emitColor();
|
||||
}
|
||||
|
||||
@Watch('position')
|
||||
positionChanged() {
|
||||
this.emitStyle();
|
||||
}
|
||||
|
||||
private emitColor() {
|
||||
const { color } = this;
|
||||
|
||||
this.ionColor.emit({
|
||||
'item-label-color': color !== undefined,
|
||||
[`ion-color-${color}`]: color !== undefined
|
||||
});
|
||||
}
|
||||
|
||||
private emitStyle() {
|
||||
const { inRange, position } = this;
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Label - Basic</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button onClick="changeColors()">
|
||||
Color Change
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
@@ -58,10 +63,37 @@
|
||||
<ion-label position="floating">Floating</ion-label>
|
||||
<ion-input></ion-input>
|
||||
</ion-item>
|
||||
<ion-item class="floating-color">
|
||||
<ion-label position="floating" color="success">Floating: Success</ion-label>
|
||||
<ion-input></ion-input>
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<ion-label position="stacked">Stacked</ion-label>
|
||||
<ion-input></ion-input>
|
||||
</ion-item>
|
||||
<ion-item class="stacked-color">
|
||||
<ion-label position="stacked" color="danger">Stacked: Danger</ion-label>
|
||||
<ion-input></ion-input>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-list>
|
||||
<ion-item color="tertiary">
|
||||
<ion-label position="floating">(Item: Tertiary) Floating</ion-label>
|
||||
<ion-input></ion-input>
|
||||
</ion-item>
|
||||
<ion-item color="primary">
|
||||
<ion-label position="stacked">(Item: Primary) Stacked</ion-label>
|
||||
<ion-input></ion-input>
|
||||
</ion-item>
|
||||
<ion-item class="floating-color" color="tertiary">
|
||||
<ion-label position="floating" color="success">(Item: Tertiary) Floating: Success</ion-label>
|
||||
<ion-input></ion-input>
|
||||
</ion-item>
|
||||
<ion-item class="stacked-color" color="primary">
|
||||
<ion-label position="stacked" color="danger">(Item: Primary) Stacked: Danger</ion-label>
|
||||
<ion-input></ion-input>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
|
||||
@@ -70,6 +102,17 @@
|
||||
color: lightblue;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const labels = document.querySelectorAll('.floating-color ion-label, .stacked-color ion-label');
|
||||
|
||||
function changeColors() {
|
||||
for (label of labels) {
|
||||
const currentColor = label.color;
|
||||
label.color = currentColor === 'success' ? 'danger' : 'success';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -265,6 +265,40 @@ export default defineComponent({
|
||||
</script>
|
||||
```
|
||||
|
||||
Developers can also use this component directly in their template:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<ion-button @click="setOpen(true)">Show Loading</ion-button>
|
||||
<ion-loading
|
||||
:is-open="isOpenRef"
|
||||
cssClass="my-custom-class"
|
||||
message="Please wait..."
|
||||
:duration="timeout"
|
||||
@onDidDismiss="setOpen(false)"
|
||||
>
|
||||
</ion-loading>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IonLoading, IonButton } from '@ionic/vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
timeout: { type: Number, default: 1000 },
|
||||
},
|
||||
components: { IonLoading, IonButton },
|
||||
setup() {
|
||||
const isOpenRef = ref(false);
|
||||
const setOpen = (state: boolean) => isOpenRef.value = state;
|
||||
|
||||
return { isOpenRef, setOpen }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -52,3 +52,37 @@ export default defineComponent({
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
Developers can also use this component directly in their template:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<ion-button @click="setOpen(true)">Show Loading</ion-button>
|
||||
<ion-loading
|
||||
:is-open="isOpenRef"
|
||||
cssClass="my-custom-class"
|
||||
message="Please wait..."
|
||||
:duration="timeout"
|
||||
@onDidDismiss="setOpen(false)"
|
||||
>
|
||||
</ion-loading>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IonLoading, IonButton } from '@ionic/vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
timeout: { type: Number, default: 1000 },
|
||||
},
|
||||
components: { IonLoading, IonButton },
|
||||
setup() {
|
||||
const isOpenRef = ref(false);
|
||||
const setOpen = (state: boolean) => isOpenRef.value = state;
|
||||
|
||||
return { isOpenRef, setOpen }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -631,12 +631,7 @@ export default {
|
||||
component: Modal,
|
||||
cssClass: 'my-custom-class',
|
||||
componentProps: {
|
||||
data: {
|
||||
content: 'New Content',
|
||||
},
|
||||
propsData: {
|
||||
title: 'New title',
|
||||
},
|
||||
title: 'New Title'
|
||||
},
|
||||
})
|
||||
return modal.present();
|
||||
@@ -646,6 +641,37 @@ export default {
|
||||
</script>
|
||||
```
|
||||
|
||||
Developers can also use this component directly in their template:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<ion-button @click="setOpen(true)">Show Modal</ion-button>
|
||||
<ion-modal
|
||||
:is-open="isOpenRef"
|
||||
css-class="my-custom-class"
|
||||
@onDidDismiss="setOpen(false)"
|
||||
>
|
||||
<Modal :data="data"></Modal>
|
||||
</ion-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IonModal, IonButton } from '@ionic/vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import Modal from './modal.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { IonModal, IonButton, Modal },
|
||||
setup() {
|
||||
const isOpenRef = ref(false);
|
||||
const setOpen = (state: boolean) => isOpenRef.value = state;
|
||||
const data = { content: 'New Content' };
|
||||
return { isOpenRef, setOpen, data }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -9,17 +9,15 @@ const getActiveElementText = async (page) => {
|
||||
|
||||
test('modal: focus trap', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/modal/test/basic?ionic:_testing=true' });
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#basic-modal');
|
||||
await page.waitForSelector('#basic-modal');
|
||||
|
||||
let modal = await page.find('ion-modal');
|
||||
|
||||
expect(modal).not.toBe(null);
|
||||
await modal.waitForVisible();
|
||||
|
||||
// TODO fix
|
||||
await page.waitFor(50);
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
@@ -39,6 +37,33 @@ test('modal: focus trap', async () => {
|
||||
expect(activeElementTextThree).toEqual('Dismiss Modal');
|
||||
});
|
||||
|
||||
test('modal: return focus', async () => {
|
||||
const page = await newE2EPage({ url: '/src/components/modal/test/basic?ionic:_testing=true' });
|
||||
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
await page.click('#basic-modal');
|
||||
await page.waitForSelector('#basic-modal');
|
||||
|
||||
let modal = await page.find('ion-modal');
|
||||
expect(modal).not.toBe(null);
|
||||
|
||||
await ionModalDidPresent.next()
|
||||
|
||||
await Promise.all([
|
||||
await modal.callMethod('dismiss'),
|
||||
await ionModalDidDismiss.next(),
|
||||
await modal.waitForNotVisible(),
|
||||
]);
|
||||
|
||||
modal = await page.find('ion-modal');
|
||||
expect(modal).toBeNull();
|
||||
|
||||
const activeElement = await page.evaluateHandle(() => document.activeElement);
|
||||
const id = await activeElement.evaluate((node) => node.id);
|
||||
expect(id).toEqual('basic-modal');
|
||||
});
|
||||
|
||||
test('modal: basic', async () => {
|
||||
await testModal(DIRECTORY, '#basic-modal');
|
||||
});
|
||||
|
||||
@@ -53,12 +53,7 @@ export default {
|
||||
component: Modal,
|
||||
cssClass: 'my-custom-class',
|
||||
componentProps: {
|
||||
data: {
|
||||
content: 'New Content',
|
||||
},
|
||||
propsData: {
|
||||
title: 'New title',
|
||||
},
|
||||
title: 'New Title'
|
||||
},
|
||||
})
|
||||
return modal.present();
|
||||
@@ -67,3 +62,34 @@ export default {
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Developers can also use this component directly in their template:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<ion-button @click="setOpen(true)">Show Modal</ion-button>
|
||||
<ion-modal
|
||||
:is-open="isOpenRef"
|
||||
css-class="my-custom-class"
|
||||
@onDidDismiss="setOpen(false)"
|
||||
>
|
||||
<Modal :data="data"></Modal>
|
||||
</ion-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IonModal, IonButton } from '@ionic/vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import Modal from './modal.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { IonModal, IonButton, Modal },
|
||||
setup() {
|
||||
const isOpenRef = ref(false);
|
||||
const setOpen = (state: boolean) => isOpenRef.value = state;
|
||||
const data = { content: 'New Content' };
|
||||
return { isOpenRef, setOpen, data }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -250,7 +250,9 @@ export class PickerColumnCmp implements ComponentInterface {
|
||||
// We have to prevent default in order to block scrolling under the picker
|
||||
// but we DO NOT have to stop propagation, since we still want
|
||||
// some "click" events to capture
|
||||
detail.event.preventDefault();
|
||||
if (detail.event.cancelable) {
|
||||
detail.event.preventDefault();
|
||||
}
|
||||
detail.event.stopPropagation();
|
||||
|
||||
hapticSelectionStart();
|
||||
@@ -272,7 +274,9 @@ export class PickerColumnCmp implements ComponentInterface {
|
||||
}
|
||||
|
||||
private onMove(detail: GestureDetail) {
|
||||
detail.event.preventDefault();
|
||||
if (detail.event.cancelable) {
|
||||
detail.event.preventDefault();
|
||||
}
|
||||
detail.event.stopPropagation();
|
||||
|
||||
// update the scroll position relative to pointer start position
|
||||
|
||||
@@ -76,6 +76,28 @@ In Angular, the CSS of a specific page is scoped only to elements of that page.
|
||||
### Javascript
|
||||
|
||||
```javascript
|
||||
class PopoverExamplePage extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.innerHTML = `
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-list-header><ion-label>Ionic</ion-label></ion-list-header>
|
||||
<ion-item button><ion-label>Item 0</ion-label></ion-item>
|
||||
<ion-item button><ion-label>Item 1</ion-label></ion-item>
|
||||
<ion-item button><ion-label>Item 2</ion-label></ion-item>
|
||||
<ion-item button><ion-label>Item 3</ion-label></ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('popover-example-page', PopoverExamplePage);
|
||||
|
||||
function presentPopover(ev) {
|
||||
const popover = Object.assign(document.createElement('ion-popover'), {
|
||||
component: 'popover-example-page',
|
||||
@@ -96,18 +118,26 @@ import React, { useState } from 'react';
|
||||
import { IonPopover, IonButton } from '@ionic/react';
|
||||
|
||||
export const PopoverExample: React.FC = () => {
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
const [popoverState, setShowPopover] = useState({ showPopover: false, event: undefined });
|
||||
|
||||
return (
|
||||
<>
|
||||
<IonPopover
|
||||
isOpen={showPopover}
|
||||
cssClass='my-custom-class'
|
||||
onDidDismiss={e => setShowPopover(false)}
|
||||
event={popoverState.event}
|
||||
isOpen={popoverState.showPopover}
|
||||
onDidDismiss={() => setShowPopover({ showPopover: false, event: undefined })}
|
||||
>
|
||||
<p>This is popover content</p>
|
||||
</IonPopover>
|
||||
<IonButton onClick={() => setShowPopover(true)}>Show Popover</IonButton>
|
||||
<IonButton onClick={
|
||||
(e: any) => {
|
||||
e.persist();
|
||||
setShowPopover({ showPopover: true, event: e })
|
||||
}}
|
||||
>
|
||||
Show Popover
|
||||
</IonButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -224,6 +254,42 @@ export default {
|
||||
</script>
|
||||
```
|
||||
|
||||
Developers can also use this component directly in their template:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<ion-button @click="setOpen(true, $event)">Show Popover</ion-button>
|
||||
<ion-popover
|
||||
:is-open="isOpenRef"
|
||||
css-class="my-custom-class"
|
||||
:event="event"
|
||||
:translucent="true"
|
||||
@onDidDismiss="setOpen(false)"
|
||||
>
|
||||
<Popover></Popover>
|
||||
</ion-popover>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IonButton, IonPopover } from '@ionic/vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import Popver from './popover.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { IonButton, IonPopover, Popover },
|
||||
setup() {
|
||||
const isOpenRef = ref(false);
|
||||
const event = ref();
|
||||
const setOpen = (state: boolean, event?: Event) => {
|
||||
event.value = event;
|
||||
isOpenRef.value = state;
|
||||
}
|
||||
return { isOpenRef, setOpen, event }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -1,4 +1,26 @@
|
||||
```javascript
|
||||
class PopoverExamplePage extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.innerHTML = `
|
||||
<ion-content>
|
||||
<ion-list>
|
||||
<ion-list-header><ion-label>Ionic</ion-label></ion-list-header>
|
||||
<ion-item button><ion-label>Item 0</ion-label></ion-item>
|
||||
<ion-item button><ion-label>Item 1</ion-label></ion-item>
|
||||
<ion-item button><ion-label>Item 2</ion-label></ion-item>
|
||||
<ion-item button><ion-label>Item 3</ion-label></ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('popover-example-page', PopoverExamplePage);
|
||||
|
||||
function presentPopover(ev) {
|
||||
const popover = Object.assign(document.createElement('ion-popover'), {
|
||||
component: 'popover-example-page',
|
||||
|
||||
@@ -3,19 +3,27 @@ import React, { useState } from 'react';
|
||||
import { IonPopover, IonButton } from '@ionic/react';
|
||||
|
||||
export const PopoverExample: React.FC = () => {
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
const [popoverState, setShowPopover] = useState({ showPopover: false, event: undefined });
|
||||
|
||||
return (
|
||||
<>
|
||||
<IonPopover
|
||||
isOpen={showPopover}
|
||||
cssClass='my-custom-class'
|
||||
onDidDismiss={e => setShowPopover(false)}
|
||||
event={popoverState.event}
|
||||
isOpen={popoverState.showPopover}
|
||||
onDidDismiss={() => setShowPopover({ showPopover: false, event: undefined })}
|
||||
>
|
||||
<p>This is popover content</p>
|
||||
</IonPopover>
|
||||
<IonButton onClick={() => setShowPopover(true)}>Show Popover</IonButton>
|
||||
<IonButton onClick={
|
||||
(e: any) => {
|
||||
e.persist();
|
||||
setShowPopover({ showPopover: true, event: e })
|
||||
}}
|
||||
>
|
||||
Show Popover
|
||||
</IonButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
```
|
||||
|
||||
@@ -46,3 +46,39 @@ export default {
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Developers can also use this component directly in their template:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<ion-button @click="setOpen(true, $event)">Show Popover</ion-button>
|
||||
<ion-popover
|
||||
:is-open="isOpenRef"
|
||||
css-class="my-custom-class"
|
||||
:event="event"
|
||||
:translucent="true"
|
||||
@onDidDismiss="setOpen(false)"
|
||||
>
|
||||
<Popover></Popover>
|
||||
</ion-popover>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IonButton, IonPopover } from '@ionic/vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import Popver from './popover.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { IonButton, IonPopover, Popover },
|
||||
setup() {
|
||||
const isOpenRef = ref(false);
|
||||
const event = ref();
|
||||
const setOpen = (state: boolean, event?: Event) => {
|
||||
event.value = event;
|
||||
isOpenRef.value = state;
|
||||
}
|
||||
return { isOpenRef, setOpen, event }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop, Watch, h } from '@stencil/core';
|
||||
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Listen, Prop, Watch, h } from '@stencil/core';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import { RadioGroupChangeEventDetail } from '../../interface';
|
||||
@@ -30,6 +30,8 @@ export class RadioGroup implements ComponentInterface {
|
||||
|
||||
@Watch('value')
|
||||
valueChanged(value: any | undefined) {
|
||||
this.setRadioTabindex(value);
|
||||
|
||||
this.ionChange.emit({ value });
|
||||
}
|
||||
|
||||
@@ -38,6 +40,31 @@ export class RadioGroup implements ComponentInterface {
|
||||
*/
|
||||
@Event() ionChange!: EventEmitter<RadioGroupChangeEventDetail>;
|
||||
|
||||
componentDidLoad() {
|
||||
this.setRadioTabindex(this.value);
|
||||
}
|
||||
|
||||
private setRadioTabindex = (value: any | undefined) => {
|
||||
const radios = this.getRadios();
|
||||
|
||||
// Get the first radio that is not disabled and the checked one
|
||||
const first = radios.find(radio => !radio.disabled);
|
||||
const checked = radios.find(radio => (radio.value === value && !radio.disabled));
|
||||
|
||||
if (!first && !checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If an enabled checked radio exists, set it to be the focusable radio
|
||||
// otherwise we default to focus the first radio
|
||||
const focusable = checked || first;
|
||||
|
||||
for (const radio of radios) {
|
||||
const tabindex = radio === focusable ? 0 : -1;
|
||||
radio.setButtonTabindex(tabindex);
|
||||
}
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
// Get the list header if it exists and set the id
|
||||
// this is used to set aria-labelledby
|
||||
@@ -51,6 +78,10 @@ export class RadioGroup implements ComponentInterface {
|
||||
}
|
||||
}
|
||||
|
||||
private getRadios(): HTMLIonRadioElement[] {
|
||||
return Array.from(this.el.querySelectorAll('ion-radio'));
|
||||
}
|
||||
|
||||
private onClick = (ev: Event) => {
|
||||
const selectedRadio = ev.target && (ev.target as HTMLElement).closest('ion-radio');
|
||||
if (selectedRadio) {
|
||||
@@ -64,6 +95,50 @@ export class RadioGroup implements ComponentInterface {
|
||||
}
|
||||
}
|
||||
|
||||
@Listen('keydown', { target: 'document' })
|
||||
onKeydown(ev: any) {
|
||||
const inSelectPopover = !!this.el.closest('ion-select-popover');
|
||||
|
||||
if (ev.target && !this.el.contains(ev.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all radios inside of the radio group and then
|
||||
// filter out disabled radios since we need to skip those
|
||||
const radios = Array.from(this.el.querySelectorAll('ion-radio')).filter(radio => !radio.disabled);
|
||||
|
||||
// Only move the radio if the current focus is in the radio group
|
||||
if (ev.target && radios.includes(ev.target)) {
|
||||
const index = radios.findIndex(radio => radio === ev.target);
|
||||
|
||||
let next;
|
||||
|
||||
// If hitting arrow down or arrow right, move to the next radio
|
||||
// If we're on the last radio, move to the first radio
|
||||
if (['ArrowDown', 'ArrowRight'].includes(ev.key)) {
|
||||
next = (index === radios.length - 1)
|
||||
? radios[0]
|
||||
: radios[index + 1];
|
||||
}
|
||||
|
||||
// If hitting arrow up or arrow left, move to the previous radio
|
||||
// If we're on the first radio, move to the last radio
|
||||
if (['ArrowUp', 'ArrowLeft'].includes(ev.key)) {
|
||||
next = (index === 0)
|
||||
? radios[radios.length - 1]
|
||||
: radios[index - 1]
|
||||
}
|
||||
|
||||
if (next && radios.includes(next)) {
|
||||
next.setFocus();
|
||||
|
||||
if (!inSelectPopover) {
|
||||
this.value = next.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Host
|
||||
|
||||
@@ -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, StyleEventDetail } from '../../interface';
|
||||
@@ -20,7 +20,7 @@ import { createColorClasses, hostContext } from '../../utils/theme';
|
||||
shadow: true
|
||||
})
|
||||
export class Radio implements ComponentInterface {
|
||||
|
||||
private buttonEl?: HTMLButtonElement;
|
||||
private inputId = `ion-rb-${radioButtonIds++}`;
|
||||
private radioGroup: HTMLIonRadioGroupElement | null = null;
|
||||
|
||||
@@ -31,6 +31,12 @@ export class Radio implements ComponentInterface {
|
||||
*/
|
||||
@State() checked = false;
|
||||
|
||||
/**
|
||||
* The tabindex of the radio button.
|
||||
* @internal
|
||||
*/
|
||||
@State() buttonTabindex = -1;
|
||||
|
||||
/**
|
||||
* The color to use from your application's color palette.
|
||||
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
|
||||
@@ -69,6 +75,20 @@ export class Radio implements ComponentInterface {
|
||||
*/
|
||||
@Event() ionBlur!: EventEmitter<void>;
|
||||
|
||||
/** @internal */
|
||||
@Method()
|
||||
async setFocus() {
|
||||
if (this.buttonEl) {
|
||||
this.buttonEl.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@Method()
|
||||
async setButtonTabindex(value: number) {
|
||||
this.buttonTabindex = value;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (this.value === undefined) {
|
||||
this.value = this.inputId;
|
||||
@@ -117,7 +137,7 @@ export class Radio implements ComponentInterface {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { inputId, disabled, checked, color, el } = this;
|
||||
const { inputId, disabled, checked, color, el, buttonTabindex } = this;
|
||||
const mode = getIonMode(this);
|
||||
const labelId = inputId + '-lbl';
|
||||
const label = findItemLabel(el);
|
||||
@@ -142,10 +162,12 @@ export class Radio implements ComponentInterface {
|
||||
<div class="radio-inner" part="mark" />
|
||||
</div>
|
||||
<button
|
||||
ref={btnEl => this.buttonEl = btnEl}
|
||||
type="button"
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
disabled={disabled}
|
||||
tabindex={buttonTabindex}
|
||||
>
|
||||
</button>
|
||||
</Host>
|
||||
|
||||
@@ -20,6 +20,26 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content id="content" class="radio-test outer-content">
|
||||
<ion-list>
|
||||
<ion-radio-group id="pizzaToppings">
|
||||
<ion-item-divider>
|
||||
<ion-label>Pizza Toppings (Unchecked Group)</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item>
|
||||
<ion-label>Pepperoni</ion-label>
|
||||
<ion-radio slot="start" value="pepperoni"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Sausage</ion-label>
|
||||
<ion-radio slot="start" value="sausage"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Pineapple</ion-label>
|
||||
<ion-radio slot="start" value="pineapple"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
<ion-list>
|
||||
<ion-radio-group id="fruitRadio" value="grape">
|
||||
<ion-item-divider>
|
||||
@@ -77,6 +97,10 @@
|
||||
</ion-radio-group>
|
||||
|
||||
<ion-radio-group allow-empty-selection value="custom">
|
||||
<ion-item-divider>
|
||||
<ion-label>Custom (Group w/ allow empty)</ion-label>
|
||||
</ion-item-divider>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Custom</ion-label>
|
||||
<ion-radio slot="end" color="danger" value="custom" style="--border-radius:2px;--inner-border-radius: 10px 0px 10px 0px;"></ion-radio>
|
||||
@@ -84,6 +108,10 @@
|
||||
</ion-radio-group>
|
||||
|
||||
<ion-radio-group allow-empty-selection value="custom">
|
||||
<ion-item-divider>
|
||||
<ion-label>Part (Group w/ allow empty)</ion-label>
|
||||
</ion-item-divider>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Radio ::part</ion-label>
|
||||
<ion-radio slot="end" value="custom" class="radio-part"></ion-radio>
|
||||
|
||||
@@ -88,6 +88,7 @@ The `detail` property of the `ionItemReorder` event includes all of the relevant
|
||||
```javascript
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { IonReorderGroup } from '@ionic/angular';
|
||||
import { ItemReorderEventDetail } from '@ionic/core';
|
||||
|
||||
@Component({
|
||||
selector: 'reorder-group-example',
|
||||
@@ -99,7 +100,7 @@ export class ReorderGroupExample {
|
||||
|
||||
constructor() {}
|
||||
|
||||
doReorder(ev: any) {
|
||||
doReorder(ev: CustomEvent<ItemReorderEventDetail>) {
|
||||
// The `from` and `to` properties contain the index of the item
|
||||
// when the drag started and ended, respectively
|
||||
console.log('Dragged from index', ev.detail.from, 'to', ev.detail.to);
|
||||
@@ -121,6 +122,7 @@ export class ReorderGroupExample {
|
||||
```javascript
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { IonReorderGroup } from '@ionic/angular';
|
||||
import { ItemReorderEventDetail } from '@ionic/core';
|
||||
|
||||
@Component({
|
||||
selector: 'reorder-group-example',
|
||||
@@ -134,7 +136,7 @@ export class ReorderGroupExample {
|
||||
|
||||
constructor() {}
|
||||
|
||||
doReorder(ev: any) {
|
||||
doReorder(ev: CustomEvent<ItemReorderEventDetail>) {
|
||||
// Before complete is called with the items they will remain in the
|
||||
// order before the drag
|
||||
console.log('Before complete', this.items);
|
||||
|
||||
@@ -115,6 +115,24 @@
|
||||
</ion-item>
|
||||
</ion-reorder>
|
||||
|
||||
<ion-reorder>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
Item 11 (the whole item can be dragged)
|
||||
</ion-label>
|
||||
<ion-toggle></ion-toggle>
|
||||
</ion-item>
|
||||
</ion-reorder>
|
||||
|
||||
<ion-reorder>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
Item 12 (the whole item can be dragged)
|
||||
</ion-label>
|
||||
<ion-checkbox></ion-checkbox>
|
||||
</ion-item>
|
||||
</ion-reorder>
|
||||
|
||||
</ion-reorder-group>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
```javascript
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { IonReorderGroup } from '@ionic/angular';
|
||||
import { ItemReorderEventDetail } from '@ionic/core';
|
||||
|
||||
@Component({
|
||||
selector: 'reorder-group-example',
|
||||
@@ -83,7 +84,7 @@ export class ReorderGroupExample {
|
||||
|
||||
constructor() {}
|
||||
|
||||
doReorder(ev: any) {
|
||||
doReorder(ev: CustomEvent<ItemReorderEventDetail>) {
|
||||
// The `from` and `to` properties contain the index of the item
|
||||
// when the drag started and ended, respectively
|
||||
console.log('Dragged from index', ev.detail.from, 'to', ev.detail.to);
|
||||
@@ -105,6 +106,7 @@ export class ReorderGroupExample {
|
||||
```javascript
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { IonReorderGroup } from '@ionic/angular';
|
||||
import { ItemReorderEventDetail } from '@ionic/core';
|
||||
|
||||
@Component({
|
||||
selector: 'reorder-group-example',
|
||||
@@ -118,7 +120,7 @@ export class ReorderGroupExample {
|
||||
|
||||
constructor() {}
|
||||
|
||||
doReorder(ev: any) {
|
||||
doReorder(ev: CustomEvent<ItemReorderEventDetail>) {
|
||||
// Before complete is called with the items they will remain in the
|
||||
// order before the drag
|
||||
console.log('Before complete', this.items);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ComponentInterface, Host, Listen, h } from '@stencil/core';
|
||||
import { Component, ComponentInterface, Element, Host, Listen, h } from '@stencil/core';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
|
||||
@@ -14,11 +14,19 @@ import { getIonMode } from '../../global/ionic-global';
|
||||
shadow: true
|
||||
})
|
||||
export class Reorder implements ComponentInterface {
|
||||
@Element() el!: HTMLIonReorderElement;
|
||||
|
||||
@Listen('click', { capture: true })
|
||||
onClick(ev: Event) {
|
||||
const reorderGroup = this.el.closest('ion-reorder-group');
|
||||
|
||||
ev.preventDefault();
|
||||
ev.stopImmediatePropagation();
|
||||
|
||||
// Only stop event propagation if the reorder is inside of an enabled
|
||||
// reorder group. This allows interaction with clickable children components.
|
||||
if (!reorderGroup || !reorderGroup.disabled) {
|
||||
ev.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
min-height: #{$segment-button-ios-min-height};
|
||||
|
||||
// Necessary for the z-index to work properly
|
||||
transform: translate3d(0, 0, 0);
|
||||
transform: translate(0, 0);
|
||||
|
||||
font-size: #{$segment-button-ios-font-size};
|
||||
|
||||
@@ -62,8 +62,6 @@
|
||||
|
||||
content: "";
|
||||
opacity: 1;
|
||||
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
:host(:first-of-type)::before {
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
@include text-inherit();
|
||||
@include margin(var(--margin-top), var(--margin-end), var(--margin-bottom), var(--margin-start));
|
||||
@include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start));
|
||||
@include transform(translate3d(0,0,0));
|
||||
@include transform(translate(0,0));
|
||||
|
||||
display: flex;
|
||||
position: relative;
|
||||
@@ -270,8 +270,6 @@ ion-ripple-effect {
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
will-change: transform, opacity;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -306,4 +304,4 @@ ion-ripple-effect {
|
||||
.segment-button-indicator-animated {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
text-align: center;
|
||||
|
||||
contain: paint;
|
||||
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -244,7 +244,7 @@ export class Segment implements ComponentInterface {
|
||||
|
||||
// Scale the indicator width to match the previous indicator width
|
||||
// and translate it on top of the previous indicator
|
||||
const transform = `translate3d(${xPosition}px, 0, 0) scaleX(${widthDelta})`;
|
||||
const transform = `translate(${xPosition}px, 0) scaleX(${widthDelta})`;
|
||||
|
||||
writeTask(() => {
|
||||
// Remove the transition before positioning on top of the previous indicator
|
||||
|
||||
@@ -209,7 +209,7 @@ However, the Select Option does set a class for easier styling and allows for th
|
||||
<ion-item>
|
||||
<ion-label>Users</ion-label>
|
||||
<ion-select [compareWith]="compareWith">
|
||||
<ion-select-option *ngFor="let user of users">{{user.first + ' ' + user.last}}</ion-select-option>
|
||||
<ion-select-option *ngFor="let user of users" [value]="user">{{user.first + ' ' + user.last}}</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
@@ -218,13 +218,19 @@ However, the Select Option does set a class for easier styling and allows for th
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
first: string;
|
||||
last: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'select-example',
|
||||
templateUrl: 'select-example.html',
|
||||
styleUrls: ['./select-example.css'],
|
||||
})
|
||||
export class SelectExample {
|
||||
users: any[] = [
|
||||
users: User[] = [
|
||||
{
|
||||
id: 1,
|
||||
first: 'Alice',
|
||||
@@ -242,11 +248,75 @@ export class SelectExample {
|
||||
}
|
||||
];
|
||||
|
||||
compareWithFn = (o1, o2) => {
|
||||
compareWith(o1: User, o2: User) {
|
||||
return o1 && o2 ? o1.id === o2.id : o1 === o2;
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
compareWith = compareWithFn;
|
||||
### Objects as Values with Multiple Selection
|
||||
|
||||
```html
|
||||
<ion-list>
|
||||
<ion-list-header>
|
||||
<ion-label>
|
||||
Objects as Values (compareWith)
|
||||
</ion-label>
|
||||
</ion-list-header>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Users</ion-label>
|
||||
<ion-select [compareWith]="compareWith" multiple="true">
|
||||
<ion-select-option *ngFor="let user of users" [value]="user">{{user.first + ' ' + user.last}}</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
first: string;
|
||||
last: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'select-example',
|
||||
templateUrl: 'select-example.html',
|
||||
styleUrls: ['./select-example.css'],
|
||||
})
|
||||
export class SelectExample {
|
||||
users: User[] = [
|
||||
{
|
||||
id: 1,
|
||||
first: 'Alice',
|
||||
last: 'Smith',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
first: 'Bob',
|
||||
last: 'Davis',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
first: 'Charlie',
|
||||
last: 'Rosenburg',
|
||||
}
|
||||
];
|
||||
|
||||
compareWith(o1: User, o2: User | User[]) {
|
||||
if (!o1 || !o2) {
|
||||
return o1 === o2;
|
||||
}
|
||||
|
||||
if (Array.isArray(o2)) {
|
||||
return o2.some((u: User) => u.id === o1.id);
|
||||
}
|
||||
|
||||
return o1.id === o2.id;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -15,3 +15,7 @@
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
}
|
||||
|
||||
:host-context(.item-label-floating) .select-icon {
|
||||
@include transform(translate3d(0, -9px, 0));
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<ion-item>
|
||||
<ion-label>Users</ion-label>
|
||||
<ion-select [compareWith]="compareWith">
|
||||
<ion-select-option *ngFor="let user of users">{{user.first + ' ' + user.last}}</ion-select-option>
|
||||
<ion-select-option *ngFor="let user of users" [value]="user">{{user.first + ' ' + user.last}}</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
@@ -89,13 +89,19 @@
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
first: string;
|
||||
last: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'select-example',
|
||||
templateUrl: 'select-example.html',
|
||||
styleUrls: ['./select-example.css'],
|
||||
})
|
||||
export class SelectExample {
|
||||
users: any[] = [
|
||||
users: User[] = [
|
||||
{
|
||||
id: 1,
|
||||
first: 'Alice',
|
||||
@@ -113,11 +119,75 @@ export class SelectExample {
|
||||
}
|
||||
];
|
||||
|
||||
compareWithFn = (o1, o2) => {
|
||||
compareWith(o1: User, o2: User) {
|
||||
return o1 && o2 ? o1.id === o2.id : o1 === o2;
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
compareWith = compareWithFn;
|
||||
### Objects as Values with Multiple Selection
|
||||
|
||||
```html
|
||||
<ion-list>
|
||||
<ion-list-header>
|
||||
<ion-label>
|
||||
Objects as Values (compareWith)
|
||||
</ion-label>
|
||||
</ion-list-header>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Users</ion-label>
|
||||
<ion-select [compareWith]="compareWith" multiple="true">
|
||||
<ion-select-option *ngFor="let user of users" [value]="user">{{user.first + ' ' + user.last}}</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
first: string;
|
||||
last: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'select-example',
|
||||
templateUrl: 'select-example.html',
|
||||
styleUrls: ['./select-example.css'],
|
||||
})
|
||||
export class SelectExample {
|
||||
users: User[] = [
|
||||
{
|
||||
id: 1,
|
||||
first: 'Alice',
|
||||
last: 'Smith',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
first: 'Bob',
|
||||
last: 'Davis',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
first: 'Charlie',
|
||||
last: 'Rosenburg',
|
||||
}
|
||||
];
|
||||
|
||||
compareWith(o1: User, o2: User | User[]) {
|
||||
if (!o1 || !o2) {
|
||||
return o1 === o2;
|
||||
}
|
||||
|
||||
if (Array.isArray(o2)) {
|
||||
return o2.some((u: User) => u.id === o1.id);
|
||||
}
|
||||
|
||||
return o1.id === o2.id;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -198,4 +268,4 @@ export class SelectExample {
|
||||
subHeader: 'Select your favorite color'
|
||||
};
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
@@ -271,6 +271,36 @@ export default {
|
||||
</script>
|
||||
```
|
||||
|
||||
Developers can also use this component directly in their template:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<ion-button @click="setOpen(true)">Show Toast</ion-button>
|
||||
<ion-toast
|
||||
:is-open="isOpenRef"
|
||||
message="Your settings have been saved."
|
||||
:duration="2000"
|
||||
@onDidDismiss="setOpen(false)"
|
||||
>
|
||||
</ion-toast>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IonToast, IonButton } from '@ionic/vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { IonToast, IonButton },
|
||||
setup() {
|
||||
const isOpenRef = ref(false);
|
||||
const setOpen = (state: boolean) => isOpenRef.value = state;
|
||||
|
||||
return { isOpenRef, setOpen }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -51,3 +51,33 @@ export default {
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Developers can also use this component directly in their template:
|
||||
|
||||
```html
|
||||
<template>
|
||||
<ion-button @click="setOpen(true)">Show Toast</ion-button>
|
||||
<ion-toast
|
||||
:is-open="isOpenRef"
|
||||
message="Your settings have been saved."
|
||||
:duration="2000"
|
||||
@onDidDismiss="setOpen(false)"
|
||||
>
|
||||
</ion-toast>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IonToast, IonButton } from '@ionic/vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { IonToast, IonButton },
|
||||
setup() {
|
||||
const isOpenRef = ref(false);
|
||||
const setOpen = (state: boolean) => isOpenRef.value = state;
|
||||
|
||||
return { isOpenRef, setOpen }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -63,7 +63,7 @@ export const createOverlay = <T extends HTMLIonOverlayElement>(tagName: string,
|
||||
return Promise.resolve() as any;
|
||||
};
|
||||
|
||||
const focusableQueryString = '[tabindex]:not([tabindex^="-"]), input:not([type=hidden]), textarea, button, select, .ion-focusable';
|
||||
const focusableQueryString = '[tabindex]:not([tabindex^="-"]), input:not([type=hidden]):not([tabindex^="-"]), textarea:not([tabindex^="-"]), button:not([tabindex^="-"]), select:not([tabindex^="-"]), .ion-focusable:not([tabindex^="-"])';
|
||||
const innerFocusableQueryString = 'input:not([type=hidden]), textarea, button, select';
|
||||
|
||||
const focusFirstDescendant = (ref: Element, overlay: HTMLIonOverlayElement) => {
|
||||
@@ -260,11 +260,47 @@ export const present = async (
|
||||
overlay.didPresent.emit();
|
||||
}
|
||||
|
||||
/**
|
||||
* When an overlay that steals focus
|
||||
* is dismissed, focus should be returned
|
||||
* to the element that was focused
|
||||
* prior to the overlay opening. Toast
|
||||
* does not steal focus and is excluded
|
||||
* from returning focus as a result.
|
||||
*/
|
||||
if (overlay.el.tagName !== 'ION-TOAST') {
|
||||
focusPreviousElementOnDismiss(overlay.el);
|
||||
}
|
||||
|
||||
if (overlay.keyboardClose) {
|
||||
overlay.el.focus();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* When an overlay component is dismissed,
|
||||
* focus should be returned to the element
|
||||
* that presented the overlay. Otherwise
|
||||
* focus will be set on the body which
|
||||
* means that people using screen readers
|
||||
* or tabbing will need to re-navigate
|
||||
* to where they were before they
|
||||
* opened the overlay.
|
||||
*/
|
||||
const focusPreviousElementOnDismiss = async (overlayEl: any) => {
|
||||
let previousElement = document.activeElement as HTMLElement | null;
|
||||
if (!previousElement) { return; }
|
||||
|
||||
const shadowRoot = previousElement && previousElement.shadowRoot;
|
||||
if (shadowRoot) {
|
||||
// If there are no inner focusable elements, just focus the host element.
|
||||
previousElement = shadowRoot.querySelector(innerFocusableQueryString) || previousElement;
|
||||
}
|
||||
|
||||
await overlayEl.onDidDismiss();
|
||||
previousElement.focus();
|
||||
}
|
||||
|
||||
export const dismiss = async (
|
||||
overlay: OverlayInterface,
|
||||
data: any | undefined,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/docs",
|
||||
"version": "5.3.3",
|
||||
"version": "5.3.5",
|
||||
"description": "Pre-packaged API documentation for the Ionic docs.",
|
||||
"main": "core.json",
|
||||
"types": "core.d.ts",
|
||||
|
||||
14
packages/angular-server/package-lock.json
generated
14
packages/angular-server/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/angular-server",
|
||||
"version": "5.3.3",
|
||||
"version": "5.3.5",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -99,9 +99,9 @@
|
||||
}
|
||||
},
|
||||
"@ionic/core": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.3.2.tgz",
|
||||
"integrity": "sha512-s/SDnS993fnZ3d6EzOlURHBbc2aI2/WsZSsCLgnJz3G+KUO4hY/2RQvdmtl0FZpDHMSyehG6tRgFFXvnFSI9CQ==",
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.3.4.tgz",
|
||||
"integrity": "sha512-4UVzj+Vd7o0VJ06dReG01PvttnLLPSzUVgXSYMBKKR849Pvuh5Q9t5s4GEEQgGoxhv1S6Ai+zphWGFMvviOyfw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ionicons": "^5.1.2",
|
||||
@@ -1999,9 +1999,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"ionicons": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-5.1.2.tgz",
|
||||
"integrity": "sha512-zO7ZgbBbXhpA7cXO2rDzTNdcCqErjg1Sprq/ossTvaiV0MriOjRE7JO3EGvYjDTPzF9YALGpvLXqCgsRT0tprA==",
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-5.2.1.tgz",
|
||||
"integrity": "sha512-dtw4rR7Sr2ssGHRrkTMESQ/p4gwovmHgafKvZsEeMjb712xjBOD3YSycy5kofS5RusU/IFVog8693BZiJ0ELzA==",
|
||||
"dev": true
|
||||
},
|
||||
"is-accessor-descriptor": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/angular-server",
|
||||
"version": "5.3.3",
|
||||
"version": "5.3.5",
|
||||
"description": "Angular SSR Module for Ionic",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -49,7 +49,7 @@
|
||||
"@angular/core": "8.2.13",
|
||||
"@angular/platform-browser": "8.2.13",
|
||||
"@angular/platform-server": "8.2.13",
|
||||
"@ionic/core": "5.3.3",
|
||||
"@ionic/core": "5.3.5",
|
||||
"ng-packagr": "5.7.1",
|
||||
"tslint": "^5.12.1",
|
||||
"tslint-ionic-rules": "0.0.21",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/react-router",
|
||||
"version": "5.3.3",
|
||||
"version": "5.3.5",
|
||||
"description": "React Router wrapper for @ionic/react",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -39,16 +39,16 @@
|
||||
"tslib": "*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ionic/core": "5.3.3",
|
||||
"@ionic/react": "5.3.3",
|
||||
"@ionic/core": "5.3.5",
|
||||
"@ionic/react": "5.3.5",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-router": "^5.0.1",
|
||||
"react-router-dom": "^5.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ionic/core": "5.3.3",
|
||||
"@ionic/react": "5.3.3",
|
||||
"@ionic/core": "5.3.5",
|
||||
"@ionic/react": "5.3.5",
|
||||
"@rollup/plugin-node-resolve": "^8.1.0",
|
||||
"@testing-library/jest-dom": "^5.11.0",
|
||||
"@testing-library/react": "^10.4.9",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/react",
|
||||
"version": "5.3.3",
|
||||
"version": "5.3.5",
|
||||
"description": "React specific wrapper for @ionic/core",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -39,7 +39,7 @@
|
||||
"css/"
|
||||
],
|
||||
"dependencies": {
|
||||
"@ionic/core": "5.3.3",
|
||||
"@ionic/core": "5.3.5",
|
||||
"ionicons": "^5.1.2",
|
||||
"tslib": "*"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user