chore(): sync vue branch with master

This commit is contained in:
Liam DeBeasi
2020-10-08 11:38:58 -04:00
65 changed files with 1493 additions and 182 deletions

View File

@@ -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
View 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
View 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

View File

@@ -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)

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "0.5.2",
"version": "5.3.5",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "0.5.2",
"version": "5.3.5",
"description": "Base components for Ionic",
"keywords": [
"ionic",

View File

@@ -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.
*/

View File

@@ -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

View File

@@ -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>
```

View File

@@ -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> {}

View File

@@ -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)}

View File

@@ -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

View File

@@ -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);
}
}
]

View File

@@ -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>
```

View File

@@ -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() {

View File

@@ -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>

View File

@@ -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
});
});

View File

@@ -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

View File

@@ -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);

View File

@@ -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>

View File

@@ -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()', () => {

View File

@@ -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
// --------------------------------------------------

View File

@@ -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)};
}

View File

@@ -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);
}
}

View File

@@ -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,

View 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();
});

View 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>

View File

@@ -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
// --------------------------------------------------

View File

@@ -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;

View File

@@ -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>

View File

@@ -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

View File

@@ -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>
```

View File

@@ -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

View File

@@ -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');
});

View File

@@ -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>
```

View File

@@ -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

View File

@@ -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

View File

@@ -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',

View File

@@ -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>
</>
);
};
```
```

View File

@@ -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>
```

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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);

View File

@@ -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() {

View File

@@ -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 {

View File

@@ -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;
}
}
}

View File

@@ -27,6 +27,8 @@
text-align: center;
contain: paint;
user-select: none;
}

View File

@@ -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

View File

@@ -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;
}
}
```

View File

@@ -15,3 +15,7 @@
width: 19px;
height: 19px;
}
:host-context(.item-label-floating) .select-icon {
@include transform(translate3d(0, -9px, 0));
}

View File

@@ -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'
};
}
```
```

View File

@@ -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

View File

@@ -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>
```

View File

@@ -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,

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "*"
},