diff --git a/.github/ionic-issue-bot.yml b/.github/ionic-issue-bot.yml index d3f7cbef25..9bf011ce4d 100644 --- a/.github/ionic-issue-bot.yml +++ b/.github/ionic-issue-bot.yml @@ -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" diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000000..c39479e679 --- /dev/null +++ b/.github/labeler.yml @@ -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-*/**/* diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml new file mode 100644 index 0000000000..0241c7fa5b --- /dev/null +++ b/.github/workflows/label.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5717554304..72f2049b05 100644 --- a/CHANGELOG.md +++ b/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) diff --git a/angular/package-lock.json b/angular/package-lock.json index 1f3d9478d3..f97916ccea 100644 --- a/angular/package-lock.json +++ b/angular/package-lock.json @@ -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", diff --git a/angular/package.json b/angular/package.json index 6112c65a53..7e2a827b6b 100644 --- a/angular/package.json +++ b/angular/package.json @@ -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": { diff --git a/angular/test/test-app/package.json b/angular/test/test-app/package.json index 6c6f704aec..eaf1979d31 100644 --- a/angular/test/test-app/package.json +++ b/angular/test/test-app/package.json @@ -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", diff --git a/core/package-lock.json b/core/package-lock.json index f3d8567aca..7661deed56 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -1,6 +1,6 @@ { "name": "@ionic/core", - "version": "0.5.2", + "version": "5.3.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/core/package.json b/core/package.json index df522096f8..3f270a3e15 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "@ionic/core", - "version": "0.5.2", + "version": "5.3.5", "description": "Base components for Ionic", "keywords": [ "ionic", diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 6c378ebb70..559bf6bfb0 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -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; + "setFocus": () => Promise; /** * 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) => void; /** * Emitted when the styles change. */ diff --git a/core/src/components/action-sheet/readme.md b/core/src/components/action-sheet/readme.md index 4a70de38e3..83690632c5 100644 --- a/core/src/components/action-sheet/readme.md +++ b/core/src/components/action-sheet/readme.md @@ -286,6 +286,7 @@ export class ActionSheetExample { ``` +Developers can also use this component directly in their template: + +```html + + + +``` + ## Properties diff --git a/core/src/components/action-sheet/usage/vue.md b/core/src/components/action-sheet/usage/vue.md index b2e3658ecf..8e9d4169e5 100644 --- a/core/src/components/action-sheet/usage/vue.md +++ b/core/src/components/action-sheet/usage/vue.md @@ -6,6 +6,7 @@ ``` + +Developers can also use this component directly in their template: + +```html + + + +``` diff --git a/core/src/components/alert/alert-interface.ts b/core/src/components/alert/alert-interface.ts index 5704b35e45..898ca322e8 100644 --- a/core/src/components/alert/alert-interface.ts +++ b/core/src/components/alert/alert-interface.ts @@ -36,6 +36,7 @@ export interface AlertInput { max?: string | number; cssClass?: string | string[]; attributes?: AlertInputAttributes | AlertTextareaAttributes; + tabindex?: number; } export interface AlertTextareaAttributes extends JSXBase.TextareaHTMLAttributes {} diff --git a/core/src/components/alert/alert.tsx b/core/src/components/alert/alert.tsx index 42fbca8098..89e1468477 100644 --- a/core/src/components/alert/alert.tsx +++ b/core/src/components/alert/alert.tsx @@ -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; + @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; + 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)} diff --git a/core/src/components/alert/readme.md b/core/src/components/alert/readme.md index 5ed7034655..ab7fa95b9b 100644 --- a/core/src/components/alert/readme.md +++ b/core/src/components/alert/readme.md @@ -1419,6 +1419,40 @@ export default defineComponent({ ``` +Developers can also use this component directly in their template: + +```html + + + +``` + ## Properties diff --git a/core/src/components/alert/test/basic/index.html b/core/src/components/alert/test/basic/index.html index a7d4847b91..df7afb9a31 100644 --- a/core/src/components/alert/test/basic/index.html +++ b/core/src/components/alert/test/basic/index.html @@ -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); } } ] diff --git a/core/src/components/alert/usage/vue.md b/core/src/components/alert/usage/vue.md index cf90273088..c1cb7990cf 100644 --- a/core/src/components/alert/usage/vue.md +++ b/core/src/components/alert/usage/vue.md @@ -263,3 +263,37 @@ export default defineComponent({ }); ``` + +Developers can also use this component directly in their template: + +```html + + + +``` diff --git a/core/src/components/button/button.tsx b/core/src/components/button/button.tsx index 64fa387d5d..ba10f9db2b 100644 --- a/core/src/components/button/button.tsx +++ b/core/src/components/button/button.tsx @@ -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() { diff --git a/core/src/components/button/test/icon/index.html b/core/src/components/button/test/icon/index.html index deb5afdf52..535d62aed1 100644 --- a/core/src/components/button/test/icon/index.html +++ b/core/src/components/button/test/icon/index.html @@ -17,6 +17,14 @@ Button - Icon + + + + + + + + @@ -26,8 +34,6 @@ Left Icon -

-

Left Icon @@ -38,8 +44,6 @@ Right Icon -

-

Right Icon @@ -49,8 +53,6 @@ -

-

@@ -60,8 +62,6 @@ Left, Large -

-

Left, Large @@ -72,8 +72,6 @@ Right, Large -

-

Right, Large @@ -83,8 +81,6 @@ -

-

@@ -94,8 +90,6 @@ Left, Small -

-

Left, Small @@ -106,8 +100,6 @@ Right, Small -

-

Right, Small @@ -117,8 +109,6 @@ -

-

@@ -128,4 +118,15 @@ + + diff --git a/core/src/components/datetime/datetime-util.spec.ts b/core/src/components/datetime/datetime-util.spec.ts index db6dfd25ff..589b1b0bc9 100644 --- a/core/src/components/datetime/datetime-util.spec.ts +++ b/core/src/components/datetime/datetime-util.spec.ts @@ -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 }); }); diff --git a/core/src/components/datetime/datetime-util.ts b/core/src/components/datetime/datetime-util.ts index 466c863e60..bce769f58e 100644 --- a/core/src/components/datetime/datetime-util.ts +++ b/core/src/components/datetime/datetime-util.ts @@ -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 diff --git a/core/src/components/datetime/test/basic/e2e.ts b/core/src/components/datetime/test/basic/e2e.ts index b9a902dd94..7a3ea3347d 100644 --- a/core/src/components/datetime/test/basic/e2e.ts +++ b/core/src/components/datetime/test/basic/e2e.ts @@ -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); diff --git a/core/src/components/datetime/test/basic/index.html b/core/src/components/datetime/test/basic/index.html index 9f3deabfe5..8d627ee78c 100644 --- a/core/src/components/datetime/test/basic/index.html +++ b/core/src/components/datetime/test/basic/index.html @@ -113,8 +113,8 @@ - HH:mm - + HH:mm A + @@ -127,6 +127,11 @@ + + h:mm A + + + hh:mm A (15 min steps) diff --git a/core/src/components/datetime/test/datetime.spec.ts b/core/src/components/datetime/test/datetime.spec.ts index 3f55504a37..84a2ce49c3 100644 --- a/core/src/components/datetime/test/datetime.spec.ts +++ b/core/src/components/datetime/test/datetime.spec.ts @@ -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()', () => { diff --git a/core/src/components/item/item.ios.scss b/core/src/components/item/item.ios.scss index 87235cbddf..30f2c00062 100644 --- a/core/src/components/item/item.ios.scss +++ b/core/src/components/item/item.ios.scss @@ -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 // -------------------------------------------------- diff --git a/core/src/components/item/item.md.scss b/core/src/components/item/item.md.scss index 643001a9bb..af1b4d5e11 100644 --- a/core/src/components/item/item.md.scss +++ b/core/src/components/item/item.md.scss @@ -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)}; +} diff --git a/core/src/components/item/item.scss b/core/src/components/item/item.scss index ecbfcae447..882cce3023 100644 --- a/core/src/components/item/item.scss +++ b/core/src/components/item/item.scss @@ -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); -} \ No newline at end of file +} diff --git a/core/src/components/item/item.tsx b/core/src/components/item/item.tsx index 124cc8edfc..179c34d845 100644 --- a/core/src/components/item/item.tsx +++ b/core/src/components/item/item.tsx @@ -28,6 +28,7 @@ import { createColorClasses, hostContext, openURL } from '../../utils/theme'; }) export class Item implements ComponentInterface, AnchorInterface, ButtonInterface { + private labelColorStyles = {}; private itemStyles = new Map(); @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) { + 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) { 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, diff --git a/core/src/components/item/test/alignment/e2e.ts b/core/src/components/item/test/alignment/e2e.ts new file mode 100644 index 0000000000..2b3633538d --- /dev/null +++ b/core/src/components/item/test/alignment/e2e.ts @@ -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(); +}); diff --git a/core/src/components/item/test/alignment/index.html b/core/src/components/item/test/alignment/index.html new file mode 100644 index 0000000000..0fe63d492e --- /dev/null +++ b/core/src/components/item/test/alignment/index.html @@ -0,0 +1,135 @@ + + + + + + Item - Alignment + + + + + + + + + + + + + + Item - Alignment + + + + + + Leading Icons + + + + + + + + + + + + Madison, WI + Atlanta, GA + + + + + + Default Labels + + Time + + + + From + + + + Destination + + Madison, WI + Atlanta, GA + + + + + + Fixed Labels + + Time + + + + From + + + + Destination + + Madison, WI + Atlanta, GA + + + + + + Floating Labels + + Time + + + + From + + + + Destination + + Madison, WI + Atlanta, GA + + + + + + Stacked Labels + + Time + + + + From + + + + Destination + + Madison, WI + Atlanta, GA + + + + + + + + + + + diff --git a/core/src/components/label/label.md.scss b/core/src/components/label/label.md.scss index d1739c80f4..5a773408d2 100644 --- a/core/src/components/label/label.md.scss +++ b/core/src/components/label/label.md.scss @@ -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 // -------------------------------------------------- diff --git a/core/src/components/label/label.tsx b/core/src/components/label/label.tsx index 2bf557004f..5acef0a20d 100644 --- a/core/src/components/label/label.tsx +++ b/core/src/components/label/label.tsx @@ -32,6 +32,12 @@ export class Label implements ComponentInterface { */ @Prop() position?: 'fixed' | 'stacked' | 'floating'; + /** + * Emitted when the color changes. + * @internal + */ + @Event() ionColor!: EventEmitter; + /** * 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; diff --git a/core/src/components/label/test/basic/index.html b/core/src/components/label/test/basic/index.html index 60ff708164..3f0954d145 100644 --- a/core/src/components/label/test/basic/index.html +++ b/core/src/components/label/test/basic/index.html @@ -17,6 +17,11 @@ Label - Basic + + + Color Change + + @@ -58,10 +63,37 @@ Floating + + Floating: Success + + Stacked + + Stacked: Danger + + + + + + + (Item: Tertiary) Floating + + + + (Item: Primary) Stacked + + + + (Item: Tertiary) Floating: Success + + + + (Item: Primary) Stacked: Danger + + @@ -70,6 +102,17 @@ color: lightblue; } + + diff --git a/core/src/components/loading/readme.md b/core/src/components/loading/readme.md index 622ea29f6b..84f29b9a4d 100644 --- a/core/src/components/loading/readme.md +++ b/core/src/components/loading/readme.md @@ -265,6 +265,40 @@ export default defineComponent({ ``` +Developers can also use this component directly in their template: + +```html + + + +``` + ## Properties diff --git a/core/src/components/loading/usage/vue.md b/core/src/components/loading/usage/vue.md index 266a3326b4..8d92512536 100644 --- a/core/src/components/loading/usage/vue.md +++ b/core/src/components/loading/usage/vue.md @@ -52,3 +52,37 @@ export default defineComponent({ }); ``` + +Developers can also use this component directly in their template: + +```html + + + +``` diff --git a/core/src/components/modal/readme.md b/core/src/components/modal/readme.md index 6d3771b35a..7df06e6982 100644 --- a/core/src/components/modal/readme.md +++ b/core/src/components/modal/readme.md @@ -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 { ``` +Developers can also use this component directly in their template: + +```html + + + +``` + ## Properties diff --git a/core/src/components/modal/test/basic/e2e.ts b/core/src/components/modal/test/basic/e2e.ts index ff10d74118..06ce04f06b 100644 --- a/core/src/components/modal/test/basic/e2e.ts +++ b/core/src/components/modal/test/basic/e2e.ts @@ -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'); }); diff --git a/core/src/components/modal/usage/vue.md b/core/src/components/modal/usage/vue.md index 8f9233315b..429086a808 100644 --- a/core/src/components/modal/usage/vue.md +++ b/core/src/components/modal/usage/vue.md @@ -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 { } ``` + +Developers can also use this component directly in their template: + +```html + + + +``` diff --git a/core/src/components/picker-column/picker-column.tsx b/core/src/components/picker-column/picker-column.tsx index f884d4205e..82202a3c1f 100644 --- a/core/src/components/picker-column/picker-column.tsx +++ b/core/src/components/picker-column/picker-column.tsx @@ -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 diff --git a/core/src/components/popover/readme.md b/core/src/components/popover/readme.md index b4c493fede..b8fbd9b1b1 100644 --- a/core/src/components/popover/readme.md +++ b/core/src/components/popover/readme.md @@ -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 = ` + + + Ionic + Item 0 + Item 1 + Item 2 + Item 3 + + + `; + } +} + +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 ( <> setShowPopover(false)} + event={popoverState.event} + isOpen={popoverState.showPopover} + onDidDismiss={() => setShowPopover({ showPopover: false, event: undefined })} >

This is popover content

- setShowPopover(true)}>Show Popover + { + e.persist(); + setShowPopover({ showPopover: true, event: e }) + }} + > + Show Popover + ); }; @@ -224,6 +254,42 @@ export default { ``` +Developers can also use this component directly in their template: + +```html + + + +``` + ## Properties diff --git a/core/src/components/popover/usage/javascript.md b/core/src/components/popover/usage/javascript.md index 548a236399..6141a1ec94 100644 --- a/core/src/components/popover/usage/javascript.md +++ b/core/src/components/popover/usage/javascript.md @@ -1,4 +1,26 @@ ```javascript +class PopoverExamplePage extends HTMLElement { + constructor() { + super(); + } + + connectedCallback() { + this.innerHTML = ` + + + Ionic + Item 0 + Item 1 + Item 2 + Item 3 + + + `; + } +} + +customElements.define('popover-example-page', PopoverExamplePage); + function presentPopover(ev) { const popover = Object.assign(document.createElement('ion-popover'), { component: 'popover-example-page', diff --git a/core/src/components/popover/usage/react.md b/core/src/components/popover/usage/react.md index 9cde980381..f55b109f52 100644 --- a/core/src/components/popover/usage/react.md +++ b/core/src/components/popover/usage/react.md @@ -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 ( <> setShowPopover(false)} + event={popoverState.event} + isOpen={popoverState.showPopover} + onDidDismiss={() => setShowPopover({ showPopover: false, event: undefined })} >

This is popover content

- setShowPopover(true)}>Show Popover + { + e.persist(); + setShowPopover({ showPopover: true, event: e }) + }} + > + Show Popover + ); }; -``` \ No newline at end of file +``` diff --git a/core/src/components/popover/usage/vue.md b/core/src/components/popover/usage/vue.md index a46dc65d37..c83f6b6d58 100644 --- a/core/src/components/popover/usage/vue.md +++ b/core/src/components/popover/usage/vue.md @@ -46,3 +46,39 @@ export default { } ``` + +Developers can also use this component directly in their template: + +```html + + + +``` diff --git a/core/src/components/radio-group/radio-group.tsx b/core/src/components/radio-group/radio-group.tsx index c4e58a79f1..17e2980662 100644 --- a/core/src/components/radio-group/radio-group.tsx +++ b/core/src/components/radio-group/radio-group.tsx @@ -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; + 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 ( ; + /** @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 {
diff --git a/core/src/components/radio/test/basic/index.html b/core/src/components/radio/test/basic/index.html index 412d88283a..74dcf63a26 100644 --- a/core/src/components/radio/test/basic/index.html +++ b/core/src/components/radio/test/basic/index.html @@ -20,6 +20,26 @@ + + + + Pizza Toppings (Unchecked Group) + + + Pepperoni + + + + + Sausage + + + + + Pineapple + + + @@ -77,6 +97,10 @@ + + Custom (Group w/ allow empty) + + Custom @@ -84,6 +108,10 @@ + + Part (Group w/ allow empty) + + Radio ::part diff --git a/core/src/components/reorder-group/readme.md b/core/src/components/reorder-group/readme.md index e03b854193..5905c1f810 100644 --- a/core/src/components/reorder-group/readme.md +++ b/core/src/components/reorder-group/readme.md @@ -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) { // 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) { // Before complete is called with the items they will remain in the // order before the drag console.log('Before complete', this.items); diff --git a/core/src/components/reorder-group/test/basic/index.html b/core/src/components/reorder-group/test/basic/index.html index 1f9aaa8311..36f1017e17 100644 --- a/core/src/components/reorder-group/test/basic/index.html +++ b/core/src/components/reorder-group/test/basic/index.html @@ -115,6 +115,24 @@ + + + + Item 11 (the whole item can be dragged) + + + + + + + + + Item 12 (the whole item can be dragged) + + + + + diff --git a/core/src/components/reorder-group/usage/angular.md b/core/src/components/reorder-group/usage/angular.md index 43111202e6..f5773e44fd 100644 --- a/core/src/components/reorder-group/usage/angular.md +++ b/core/src/components/reorder-group/usage/angular.md @@ -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) { // 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) { // Before complete is called with the items they will remain in the // order before the drag console.log('Before complete', this.items); diff --git a/core/src/components/reorder/reorder.tsx b/core/src/components/reorder/reorder.tsx index 18b4d997ba..a013f20aa5 100644 --- a/core/src/components/reorder/reorder.tsx +++ b/core/src/components/reorder/reorder.tsx @@ -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() { diff --git a/core/src/components/segment-button/segment-button.ios.scss b/core/src/components/segment-button/segment-button.ios.scss index 57f4da6f29..2624c3d296 100644 --- a/core/src/components/segment-button/segment-button.ios.scss +++ b/core/src/components/segment-button/segment-button.ios.scss @@ -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 { diff --git a/core/src/components/segment-button/segment-button.scss b/core/src/components/segment-button/segment-button.scss index 2c6d1a1ef2..e9c492c055 100644 --- a/core/src/components/segment-button/segment-button.scss +++ b/core/src/components/segment-button/segment-button.scss @@ -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; } -} \ No newline at end of file +} diff --git a/core/src/components/segment/segment.scss b/core/src/components/segment/segment.scss index 47b231a05a..42bb468692 100644 --- a/core/src/components/segment/segment.scss +++ b/core/src/components/segment/segment.scss @@ -27,6 +27,8 @@ text-align: center; contain: paint; + + user-select: none; } diff --git a/core/src/components/segment/segment.tsx b/core/src/components/segment/segment.tsx index 21ab3d034b..17b1e0d47d 100644 --- a/core/src/components/segment/segment.tsx +++ b/core/src/components/segment/segment.tsx @@ -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 diff --git a/core/src/components/select/readme.md b/core/src/components/select/readme.md index 8d8825a3de..d1888ff1dd 100644 --- a/core/src/components/select/readme.md +++ b/core/src/components/select/readme.md @@ -209,7 +209,7 @@ However, the Select Option does set a class for easier styling and allows for th Users - {{user.first + ' ' + user.last}} + {{user.first + ' ' + user.last}} @@ -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 + + + + Objects as Values (compareWith) + + + + + Users + + {{user.first + ' ' + user.last}} + + + +``` + +```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; + } } ``` diff --git a/core/src/components/select/select.md.scss b/core/src/components/select/select.md.scss index 5f087e47aa..cd12859302 100644 --- a/core/src/components/select/select.md.scss +++ b/core/src/components/select/select.md.scss @@ -15,3 +15,7 @@ width: 19px; height: 19px; } + +:host-context(.item-label-floating) .select-icon { + @include transform(translate3d(0, -9px, 0)); +} diff --git a/core/src/components/select/usage/angular.md b/core/src/components/select/usage/angular.md index bff14eda67..0bfd985904 100644 --- a/core/src/components/select/usage/angular.md +++ b/core/src/components/select/usage/angular.md @@ -80,7 +80,7 @@ Users - {{user.first + ' ' + user.last}} + {{user.first + ' ' + user.last}} @@ -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 + + + + Objects as Values (compareWith) + + + + + Users + + {{user.first + ' ' + user.last}} + + + +``` + +```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' }; } -``` \ No newline at end of file +``` diff --git a/core/src/components/toast/readme.md b/core/src/components/toast/readme.md index e60e5cabea..f29b1c03d8 100644 --- a/core/src/components/toast/readme.md +++ b/core/src/components/toast/readme.md @@ -271,6 +271,36 @@ export default { ``` +Developers can also use this component directly in their template: + +```html + + + +``` + ## Properties diff --git a/core/src/components/toast/usage/vue.md b/core/src/components/toast/usage/vue.md index f0c5134151..9d8bea3ee2 100644 --- a/core/src/components/toast/usage/vue.md +++ b/core/src/components/toast/usage/vue.md @@ -51,3 +51,33 @@ export default { } ``` + +Developers can also use this component directly in their template: + +```html + + + +``` diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index f757b6dc28..ef1b737bc9 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -63,7 +63,7 @@ export const createOverlay = (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, diff --git a/docs/package.json b/docs/package.json index f501a9fda1..b543621f7c 100644 --- a/docs/package.json +++ b/docs/package.json @@ -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", diff --git a/packages/angular-server/package-lock.json b/packages/angular-server/package-lock.json index 51ef0c4898..05cf8e9d98 100644 --- a/packages/angular-server/package-lock.json +++ b/packages/angular-server/package-lock.json @@ -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": { diff --git a/packages/angular-server/package.json b/packages/angular-server/package.json index fe4d466e22..bc379ec6fb 100644 --- a/packages/angular-server/package.json +++ b/packages/angular-server/package.json @@ -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", diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 5c9b838900..fb75afb186 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -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", diff --git a/packages/react/package.json b/packages/react/package.json index bd66b3a54d..166befdf92 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -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": "*" },