diff --git a/angular/package.json b/angular/package.json index 3f45b23b13..6c3763cefb 100644 --- a/angular/package.json +++ b/angular/package.json @@ -38,6 +38,7 @@ "validate": "npm i && npm run lint && npm run test && npm run build" }, "module": "dist/fesm5.js", + "main": "dist/fesm5.js", "types": "dist/core.d.ts", "files": [ "dist/", diff --git a/core/src/components/action-sheet/action-sheet.tsx b/core/src/components/action-sheet/action-sheet.tsx index 31027e6b6e..0353ba39d7 100644 --- a/core/src/components/action-sheet/action-sheet.tsx +++ b/core/src/components/action-sheet/action-sheet.tsx @@ -190,6 +190,8 @@ export class ActionSheet implements ComponentInterface, OverlayInterface { hostData() { return { + 'role': 'dialog', + 'aria-modal': 'true', style: { zIndex: 20000 + this.overlayIndex, }, diff --git a/core/src/components/alert/alert.ios.scss b/core/src/components/alert/alert.ios.scss index 072ba7114e..2caa4d5ff3 100644 --- a/core/src/components/alert/alert.ios.scss +++ b/core/src/components/alert/alert.ios.scss @@ -117,11 +117,7 @@ } .alert-tappable { - display: flex; - height: $alert-ios-tappable-height; - - contain: strict; } diff --git a/core/src/components/alert/alert.md.scss b/core/src/components/alert/alert.md.scss index 5f2ee1a296..db9aedfb3c 100644 --- a/core/src/components/alert/alert.md.scss +++ b/core/src/components/alert/alert.md.scss @@ -110,12 +110,10 @@ } .alert-tappable { - display: flex; position: relative; height: $alert-md-tappable-height; - contain: strict; overflow: hidden; } diff --git a/core/src/components/alert/alert.scss b/core/src/components/alert/alert.scss index 3844317d78..c4148236c4 100644 --- a/core/src/components/alert/alert.scss +++ b/core/src/components/alert/alert.scss @@ -131,6 +131,11 @@ z-index: 0; } +.alert-button.ion-focused, +.alert-tappable.ion-focused { + background: $background-color-step-100; +} + .alert-button-inner { display: flex; @@ -147,6 +152,8 @@ @include margin(0); @include padding(0); + display: flex; + width: 100%; border: 0; @@ -159,16 +166,15 @@ text-align: start; appearance: none; + + contain: strict; } .alert-button, .alert-checkbox, .alert-input, .alert-radio { - &:active, - &:focus { - outline: none; - } + outline: none; } .alert-radio-icon, diff --git a/core/src/components/alert/alert.tsx b/core/src/components/alert/alert.tsx index 0f13c677fd..75a54ea8ce 100644 --- a/core/src/components/alert/alert.tsx +++ b/core/src/components/alert/alert.tsx @@ -309,7 +309,7 @@ export class Alert implements ComponentInterface, OverlayInterface { disabled={i.disabled} tabIndex={0} role="checkbox" - class="alert-tappable alert-checkbox alert-checkbox-button" + class="alert-tappable alert-checkbox alert-checkbox-button ion-focusable" >
@@ -341,7 +341,7 @@ export class Alert implements ComponentInterface, OverlayInterface { disabled={i.disabled} id={i.id} tabIndex={0} - class="alert-radio-button alert-tappable alert-radio" + class="alert-radio-button alert-tappable alert-radio ion-focusable" role="radio" >
@@ -385,7 +385,8 @@ export class Alert implements ComponentInterface, OverlayInterface { hostData() { return { - role: 'alertdialog', + 'role': 'dialog', + 'aria-modal': 'true', style: { zIndex: 20000 + this.overlayIndex, }, @@ -451,6 +452,7 @@ export class Alert implements ComponentInterface, OverlayInterface { function buttonClass(button: AlertButton): CssClassMap { return { 'alert-button': true, + 'ion-focusable': true, 'ion-activatable': true, ...getClassMap(button.cssClass) }; diff --git a/core/src/components/anchor/anchor.tsx b/core/src/components/anchor/anchor.tsx index 1eaf574db8..8bc8f149e6 100644 --- a/core/src/components/anchor/anchor.tsx +++ b/core/src/components/anchor/anchor.tsx @@ -1,4 +1,4 @@ -import { Component, ComponentInterface, Prop } from '@stencil/core'; +import { Component, ComponentInterface, Listen, Prop } from '@stencil/core'; import { Color, RouterDirection } from '../../interface'; import { createColorClasses, openURL } from '../../utils/theme'; @@ -31,6 +31,11 @@ export class Anchor implements ComponentInterface { */ @Prop() routerDirection: RouterDirection = 'forward'; + @Listen('click') + onClick(ev: Event) { + openURL(this.win, this.href, ev, this.routerDirection); + } + hostData() { return { class: { @@ -42,10 +47,7 @@ export class Anchor implements ComponentInterface { render() { return ( - openURL(this.win, this.href, ev, this.routerDirection)} - > + ); diff --git a/core/src/components/app/app.tsx b/core/src/components/app/app.tsx index 702f329f8e..22f18660fa 100644 --- a/core/src/components/app/app.tsx +++ b/core/src/components/app/app.tsx @@ -27,6 +27,7 @@ export class App implements ComponentInterface { importInputShims(win, config); importStatusTap(win, config, queue); importHardwareBackButton(win, config); + importFocusVisible(win); }); } @@ -54,6 +55,10 @@ function importStatusTap(win: Window, config: Config, queue: QueueApi) { } } +function importFocusVisible(win: Window) { + import('../../utils/focus-visible').then(module => module.startFocusVisible(win.document)); +} + function importTapClick(win: Window, config: Config) { import('../../utils/tap-click').then(module => module.startTapClick(win.document, config)); } diff --git a/core/src/components/back-button/back-button.tsx b/core/src/components/back-button/back-button.tsx index 51d1180a86..5f55176344 100644 --- a/core/src/components/back-button/back-button.tsx +++ b/core/src/components/back-button/back-button.tsx @@ -1,4 +1,4 @@ -import { Component, ComponentInterface, Element, Prop } from '@stencil/core'; +import { Component, ComponentInterface, Element, Listen, Prop } from '@stencil/core'; import { Color, Config, Mode } from '../../interface'; import { createColorClasses, openURL } from '../../utils/theme'; @@ -45,6 +45,7 @@ export class BackButton implements ComponentInterface { */ @Prop() text?: string | null; + @Listen('click') async onClick(ev: Event) { const nav = this.el.closest('ion-nav'); ev.preventDefault(); @@ -78,7 +79,6 @@ export class BackButton implements ComponentInterface { ]; diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 3fa50566a5..94f5945fc5 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -1,4 +1,4 @@ -import { Component, ComponentInterface, Element, Event, EventEmitter, Method, Prop, State, Watch } from '@stencil/core'; +import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Method, Prop, State, Watch } from '@stencil/core'; import { DatetimeChangeEventDetail, DatetimeOptions, Mode, PickerColumn, PickerColumnOption, PickerOptions, StyleEventDetail } from '../../interface'; import { clamp, findItemLabel, renderHiddenInput } from '../../utils/helpers'; @@ -15,16 +15,17 @@ import { DatetimeData, LocaleData, convertDataToISO, convertFormatToKey, convert shadow: true }) export class Datetime implements ComponentInterface { + private inputId = `ion-dt-${datetimeIds++}`; private locale: LocaleData = {}; private datetimeMin: DatetimeData = {}; private datetimeMax: DatetimeData = {}; private datetimeValue: DatetimeData = {}; + private buttonEl?: HTMLButtonElement; @Element() el!: HTMLIonDatetimeElement; @State() isExpanded = false; - @State() keyFocus = false; @Prop({ connect: 'ion-picker-controller' }) pickerCtrl!: HTMLIonPickerControllerElement; @@ -238,6 +239,11 @@ export class Datetime implements ComponentInterface { this.emitStyle(); } + @Listen('click') + onClick() { + this.open(); + } + /** * Opens the datetime overlay. */ @@ -252,6 +258,7 @@ export class Datetime implements ComponentInterface { this.isExpanded = true; picker.onDidDismiss().then(() => { this.isExpanded = false; + this.setFocus(); }); await this.validate(picker); await picker.present(); @@ -522,12 +529,10 @@ export class Datetime implements ComponentInterface { return Object.keys(val).length > 0; } - private onClick = () => { - this.open(); - } - - private onKeyUp = () => { - this.keyFocus = true; + private setFocus() { + if (this.buttonEl) { + this.buttonEl.focus(); + } } private onFocus = () => { @@ -535,30 +540,31 @@ export class Datetime implements ComponentInterface { } private onBlur = () => { - this.keyFocus = false; this.ionBlur.emit(); } hostData() { - const addPlaceholderClass = - (this.getText() === undefined && this.placeholder != null) ? true : false; + const { inputId, disabled, isExpanded, el, placeholder } = this; - const labelId = this.inputId + '-lbl'; - const label = findItemLabel(this.el); + const addPlaceholderClass = + (this.getText() === undefined && placeholder != null) ? true : false; + + const labelId = inputId + '-lbl'; + const label = findItemLabel(el); if (label) { label.id = labelId; } return { 'role': 'combobox', - 'aria-disabled': this.disabled ? 'true' : null, - 'aria-expanded': `${this.isExpanded}`, + 'aria-disabled': disabled ? 'true' : null, + 'aria-expanded': `${isExpanded}`, 'aria-haspopup': 'true', 'aria-labelledby': labelId, class: { - 'datetime-disabled': this.disabled, + 'datetime-disabled': disabled, 'datetime-placeholder': addPlaceholderClass, - 'in-item': hostContext('ion-item', this.el) + 'in-item': hostContext('ion-item', el) } }; } @@ -576,10 +582,10 @@ export class Datetime implements ComponentInterface {
{datetimeText}
, ]; diff --git a/core/src/components/fab-button/fab-button.ios.scss b/core/src/components/fab-button/fab-button.ios.scss index 424a039762..31388a8f52 100755 --- a/core/src/components/fab-button/fab-button.ios.scss +++ b/core/src/components/fab-button/fab-button.ios.scss @@ -64,7 +64,7 @@ background: #{current-color(base, $fab-ios-translucent-background-color-alpha)}; } -:host(.ion-color.focused.fab-button-translucent) .button-native, +:host(.ion-color.ion-focused.fab-button-translucent) .button-native, :host(.ion-color.activated.fab-button-translucent) .button-native { background: #{current-color(base)}; } diff --git a/core/src/components/fab-button/fab-button.scss b/core/src/components/fab-button/fab-button.scss index e085567b3a..ba507ae66a 100755 --- a/core/src/components/fab-button/fab-button.scss +++ b/core/src/components/fab-button/fab-button.scss @@ -67,7 +67,7 @@ } // Focused/Activated Button with Color -:host(.ion-color.focused) .button-native, +:host(.ion-color.ion-focused) .button-native, :host(.ion-color.activated) .button-native { background: #{current-color(shade)}; color: #{current-color(contrast)}; @@ -147,7 +147,7 @@ } // Focused Button -:host(.focused) .button-native { +:host(.ion-focused) .button-native { background: var(--background-focused); color: var(--color-focused); } diff --git a/core/src/components/fab-button/fab-button.tsx b/core/src/components/fab-button/fab-button.tsx index ac5e64d1de..aa88f239ee 100755 --- a/core/src/components/fab-button/fab-button.tsx +++ b/core/src/components/fab-button/fab-button.tsx @@ -1,4 +1,4 @@ -import { Component, ComponentInterface, Element, Event, EventEmitter, Prop, State } from '@stencil/core'; +import { Component, ComponentInterface, Element, Event, EventEmitter, Prop } from '@stencil/core'; import { Color, Mode, RouterDirection } from '../../interface'; import { createColorClasses, hostContext, openURL } from '../../utils/theme'; @@ -14,8 +14,6 @@ import { createColorClasses, hostContext, openURL } from '../../utils/theme'; export class FabButton implements ComponentInterface { @Element() el!: HTMLElement; - @State() keyFocus = false; - @Prop({ context: 'window' }) win!: Window; /** @@ -86,17 +84,12 @@ export class FabButton implements ComponentInterface { this.ionFocus.emit(); } - private onKeyUp = () => { - this.keyFocus = true; - } - private onBlur = () => { - this.keyFocus = false; this.ionBlur.emit(); } hostData() { - const { el, disabled, color, activated, show, translucent, size, keyFocus } = this; + const { el, disabled, color, activated, show, translucent, size } = this; const inList = hostContext('ion-fab-list', el); return { 'aria-disabled': disabled ? 'true' : null, @@ -109,7 +102,7 @@ export class FabButton implements ComponentInterface { 'fab-button-disabled': disabled, 'fab-button-translucent': translucent, 'ion-activatable': true, - 'focused': keyFocus, + 'ion-focusable': true, [`fab-button-${size}`]: size !== undefined, } }; @@ -127,7 +120,6 @@ export class FabButton implements ComponentInterface { class="button-native" disabled={this.disabled} onFocus={this.onFocus} - onKeyUp={this.onKeyUp} onBlur={this.onBlur} onClick={(ev: Event) => openURL(this.win, this.href, ev, this.routerDirection)} > diff --git a/core/src/components/item-option/item-option.tsx b/core/src/components/item-option/item-option.tsx index 8fefb13f1a..ef82eabc21 100644 --- a/core/src/components/item-option/item-option.tsx +++ b/core/src/components/item-option/item-option.tsx @@ -1,4 +1,4 @@ -import { Component, ComponentInterface, Element, Prop } from '@stencil/core'; +import { Component, ComponentInterface, Element, Listen, Prop } from '@stencil/core'; import { Color, Mode } from '../../interface'; import { createColorClasses } from '../../utils/theme'; @@ -43,9 +43,12 @@ export class ItemOption implements ComponentInterface { */ @Prop() href?: string; - private clickedOptionButton(ev: Event): boolean { + @Listen('click') + onClick(ev: Event) { const el = (ev.target as HTMLElement).closest('ion-item-option'); - return !!el; + if (el) { + ev.preventDefault(); + } } hostData() { @@ -67,7 +70,6 @@ export class ItemOption implements ComponentInterface { class="button-native" disabled={this.disabled} href={this.href} - onClick={this.clickedOptionButton.bind(this)} > diff --git a/core/src/components/item/item.ios.scss b/core/src/components/item/item.ios.scss index 5a5047f4c9..d32229e1cf 100644 --- a/core/src/components/item/item.ios.scss +++ b/core/src/components/item/item.ios.scss @@ -12,6 +12,7 @@ --inner-border-width: #{0px 0px $item-ios-border-bottom-width 0px}; --background: #{$item-ios-background}; --background-activated: #{$item-ios-background-activated}; + --background-focused: #{$item-ios-background-activated}; --border-color: #{$item-ios-border-bottom-color}; --color: #{$item-ios-color}; --highlight-height: 0; diff --git a/core/src/components/item/item.md.scss b/core/src/components/item/item.md.scss index 1b4acb17d7..63f25d014d 100644 --- a/core/src/components/item/item.md.scss +++ b/core/src/components/item/item.md.scss @@ -9,6 +9,7 @@ --min-height: #{$item-md-min-height}; --background: #{$item-md-background}; --background-activated: var(--background); + --background-focused: #{$item-md-background-activated}; --border-color: #{$item-md-border-bottom-color}; --color: #{$item-md-color}; --transition: background-color 300ms cubic-bezier(.4, 0, .2, 1); diff --git a/core/src/components/item/item.scss b/core/src/components/item/item.scss index cfd4495972..6b57612eda 100644 --- a/core/src/components/item/item.scss +++ b/core/src/components/item/item.scss @@ -91,6 +91,10 @@ // Activated Item // -------------------------------------------------- +:host(.ion-focused) .item-native { + background: var(--background-focused); +} + :host(.activated) .item-native { background: var(--background-activated); } diff --git a/core/src/components/item/item.tsx b/core/src/components/item/item.tsx index df31a67df4..2c00b13ca2 100644 --- a/core/src/components/item/item.tsx +++ b/core/src/components/item/item.tsx @@ -137,6 +137,7 @@ export class Item implements ComponentInterface { 'item': true, 'item-multiple-inputs': this.multipleInputs, 'ion-activatable': this.isClickable(), + 'ion-focusable': true, } }; } @@ -158,6 +159,7 @@ export class Item implements ComponentInterface { openURL(win, href, ev, routerDirection)} > diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx index 9bfb2ee8ce..7afded4304 100644 --- a/core/src/components/modal/modal.tsx +++ b/core/src/components/modal/modal.tsx @@ -191,6 +191,7 @@ export class Modal implements ComponentInterface, OverlayInterface { hostData() { return { 'no-router': true, + 'aria-modal': 'true', class: { ...createThemedClasses(this.mode, 'modal'), ...getClassMap(this.cssClass) diff --git a/core/src/components/picker/picker.tsx b/core/src/components/picker/picker.tsx index 1c18deee40..68279289fe 100644 --- a/core/src/components/picker/picker.tsx +++ b/core/src/components/picker/picker.tsx @@ -202,6 +202,7 @@ export class Picker implements ComponentInterface, OverlayInterface { hostData() { return { + 'aria-modal': 'true', class: { ...createThemedClasses(this.mode, 'picker'), ...getClassMap(this.cssClass) diff --git a/core/src/components/popover/popover.tsx b/core/src/components/popover/popover.tsx index 7115e34e8d..7f0af453ae 100644 --- a/core/src/components/popover/popover.tsx +++ b/core/src/components/popover/popover.tsx @@ -198,10 +198,11 @@ export class Popover implements ComponentInterface, OverlayInterface { hostData() { return { + 'aria-modal': 'true', + 'no-router': true, style: { zIndex: 20000 + this.overlayIndex, }, - 'no-router': true, class: { ...getClassMap(this.cssClass), 'popover-translucent': this.translucent diff --git a/core/src/components/radio/radio.ios.scss b/core/src/components/radio/radio.ios.scss index bd54979a4c..6617a491ce 100644 --- a/core/src/components/radio/radio.ios.scss +++ b/core/src/components/radio/radio.ios.scss @@ -49,7 +49,7 @@ // iOS Radio: Keyboard Focus // ----------------------------------------- -:host(.radio-key) .radio-icon::after { +:host(.ion-focused) .radio-icon::after { @include border-radius(50%); @include position(-8px, null, null, -9px); diff --git a/core/src/components/radio/radio.md.scss b/core/src/components/radio/radio.md.scss index 4cca02bc2f..c64ced772c 100644 --- a/core/src/components/radio/radio.md.scss +++ b/core/src/components/radio/radio.md.scss @@ -80,7 +80,7 @@ // Material Design Radio: Keyboard Focus // ----------------------------------------- -:host(.radio-key) .radio-icon::after { +:host(.ion-focused) .radio-icon::after { @include border-radius(50%); @include position(-12px, null, null, -12px); diff --git a/core/src/components/radio/radio.tsx b/core/src/components/radio/radio.tsx index 893e76a650..7096415770 100644 --- a/core/src/components/radio/radio.tsx +++ b/core/src/components/radio/radio.tsx @@ -1,4 +1,4 @@ -import { Component, ComponentInterface, Element, Event, EventEmitter, Prop, State, Watch } from '@stencil/core'; +import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Prop, Watch } from '@stencil/core'; import { Color, Mode, RadioChangeEventDetail, StyleEventDetail } from '../../interface'; import { findItemLabel } from '../../utils/helpers'; @@ -16,8 +16,6 @@ export class Radio implements ComponentInterface { private inputId = `ion-rb-${radioButtonIds++}`; - @State() keyFocus = false; - @Element() el!: HTMLElement; /** @@ -127,14 +125,8 @@ export class Radio implements ComponentInterface { this.ionRadioDidUnload.emit(); } - private emitStyle() { - this.ionStyle.emit({ - 'radio-checked': this.checked, - 'interactive-disabled': this.disabled, - }); - } - - private onClick = () => { + @Listen('click') + onClick() { if (this.checked) { this.ionDeselect.emit(); } else { @@ -142,8 +134,11 @@ export class Radio implements ComponentInterface { } } - private onKeyUp = () => { - this.keyFocus = true; + private emitStyle() { + this.ionStyle.emit({ + 'radio-checked': this.checked, + 'interactive-disabled': this.disabled, + }); } private onFocus = () => { @@ -151,28 +146,27 @@ export class Radio implements ComponentInterface { } private onBlur = () => { - this.keyFocus = false; this.ionBlur.emit(); } hostData() { - const labelId = this.inputId + '-lbl'; - const label = findItemLabel(this.el); + const { inputId, disabled, checked, color, el } = this; + const labelId = inputId + '-lbl'; + const label = findItemLabel(el); if (label) { label.id = labelId; } return { 'role': 'radio', - 'aria-disabled': this.disabled ? 'true' : null, - 'aria-checked': `${this.checked}`, + 'aria-disabled': disabled ? 'true' : null, + 'aria-checked': `${checked}`, 'aria-labelledby': labelId, class: { - ...createColorClasses(this.color), - 'in-item': hostContext('ion-item', this.el), + ...createColorClasses(color), + 'in-item': hostContext('ion-item', el), 'interactive': true, - 'radio-checked': this.checked, - 'radio-disabled': this.disabled, - 'radio-key': this.keyFocus + 'radio-checked': checked, + 'radio-disabled': disabled, } }; } @@ -184,10 +178,9 @@ export class Radio implements ComponentInterface {
, , ]; diff --git a/core/src/components/range/range.tsx b/core/src/components/range/range.tsx index aa21c46a0e..ff1c446717 100644 --- a/core/src/components/range/range.tsx +++ b/core/src/components/range/range.tsx @@ -165,7 +165,7 @@ export class Range implements ComponentInterface { this.gesture.setDisabled(this.disabled); } - private handleKeyboard(knob: string, isIncrease: boolean) { + private handleKeyboard = (knob: string, isIncrease: boolean) => { let step = this.step; step = step > 0 ? step : 1; step = step / (this.max - this.min); @@ -173,9 +173,9 @@ export class Range implements ComponentInterface { step *= -1; } if (knob === 'A') { - this.ratioA += step; + this.ratioA = clamp(0, this.ratioA + step, 1); } else { - this.ratioB += step; + this.ratioB = clamp(0, this.ratioB + step, 1); } this.updateValue(); } @@ -378,7 +378,7 @@ export class Range implements ComponentInterface { ratio: this.ratioA, pin: this.pin, disabled: this.disabled, - handleKeyboard: this.handleKeyboard.bind(this), + handleKeyboard: this.handleKeyboard, min, max })} @@ -390,7 +390,7 @@ export class Range implements ComponentInterface { ratio: this.ratioB, pin: this.pin, disabled: this.disabled, - handleKeyboard: this.handleKeyboard.bind(this), + handleKeyboard: this.handleKeyboard, min, max })} diff --git a/core/src/components/segment-button/segment-button.tsx b/core/src/components/segment-button/segment-button.tsx index 379b660c68..271423fb22 100644 --- a/core/src/components/segment-button/segment-button.tsx +++ b/core/src/components/segment-button/segment-button.tsx @@ -1,4 +1,4 @@ -import { Component, ComponentInterface, Element, Event, EventEmitter, Prop, Watch } from '@stencil/core'; +import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Prop, Watch } from '@stencil/core'; import { Mode, SegmentButtonLayout } from '../../interface'; @@ -53,7 +53,8 @@ export class SegmentButton implements ComponentInterface { } } - private onClick = () => { + @Listen('click') + onClick() { this.checked = true; } @@ -90,7 +91,6 @@ export class SegmentButton implements ComponentInterface { aria-pressed={this.checked ? 'true' : null} class="button-native" disabled={this.disabled} - onClick={this.onClick} > {this.mode === 'md' && } diff --git a/core/src/components/select/select.scss b/core/src/components/select/select.scss index b87dd180b7..166515a28b 100644 --- a/core/src/components/select/select.scss +++ b/core/src/components/select/select.scss @@ -32,7 +32,7 @@ pointer-events: none; } -:host(.select-key) button { +:host(.ion-focused) button { border: 2px solid #5e9ed6; } diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index 6bd9f579f4..ef3795c028 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -18,6 +18,7 @@ export class Select implements ComponentInterface { private inputId = `ion-sel-${selectIds++}`; private overlay?: OverlaySelect; private didInit = false; + private buttonEl?: HTMLButtonElement; @Element() el!: HTMLIonSelectElement; @@ -26,7 +27,6 @@ export class Select implements ComponentInterface { @Prop({ connect: 'ion-popover-controller' }) popoverCtrl!: HTMLIonPopoverControllerElement; @State() isExpanded = false; - @State() keyFocus = false; /** * The mode determines which platform styles to use. @@ -138,6 +138,11 @@ export class Select implements ComponentInterface { } } + @Listen('click') + onClick() { + this.open(); + } + async componentDidLoad() { await this.loadOptions(); @@ -174,6 +179,7 @@ export class Select implements ComponentInterface { overlay.onDidDismiss().then(() => { this.overlay = undefined; this.isExpanded = false; + this.setFocus(); }); await overlay.present(); return overlay; @@ -353,6 +359,12 @@ export class Select implements ComponentInterface { return generateText(this.childOpts, this.value); } + private setFocus() { + if (this.buttonEl) { + this.buttonEl.focus(); + } + } + private emitStyle() { this.ionStyle.emit({ 'interactive': true, @@ -364,20 +376,11 @@ export class Select implements ComponentInterface { }); } - private onClick = (ev: UIEvent) => { - this.open(ev); - } - - private onKeyUp = () => { - this.keyFocus = true; - } - private onFocus = () => { this.ionFocus.emit(); } private onBlur = () => { - this.keyFocus = false; this.ionBlur.emit(); } @@ -397,7 +400,6 @@ export class Select implements ComponentInterface { class: { 'in-item': hostContext('ion-item', this.el), 'select-disabled': this.disabled, - 'select-key': this.keyFocus } }; } @@ -432,10 +434,10 @@ export class Select implements ComponentInterface {
, ]; diff --git a/core/src/components/toggle/toggle.scss b/core/src/components/toggle/toggle.scss index eb4bb6d47a..5f868b6a13 100644 --- a/core/src/components/toggle/toggle.scss +++ b/core/src/components/toggle/toggle.scss @@ -25,7 +25,7 @@ z-index: $z-index-item-input; } -:host(.toggle-key) input { +:host(.ion-focused) input { border: 2px solid #5e9ed6; } @@ -33,8 +33,7 @@ pointer-events: none; } -input { +button { @include input-cover(); - - pointer-events: none; } + diff --git a/core/src/components/toggle/toggle.tsx b/core/src/components/toggle/toggle.tsx index b5ebcef1aa..bdca2a1daa 100644 --- a/core/src/components/toggle/toggle.tsx +++ b/core/src/components/toggle/toggle.tsx @@ -24,7 +24,6 @@ export class Toggle implements ComponentInterface { @Prop({ context: 'queue' }) queue!: QueueApi; @State() activated = false; - @State() keyFocus = false; /** * The mode determines which platform styles to use. @@ -99,27 +98,6 @@ export class Toggle implements ComponentInterface { } } - @Listen('click') - onClick() { - this.checked = !this.checked; - } - - @Listen('keyup') - onKeyUp() { - this.keyFocus = true; - } - - @Listen('focus') - onFocus() { - this.ionFocus.emit(); - } - - @Listen('blur') - onBlur() { - this.keyFocus = false; - this.ionBlur.emit(); - } - componentWillLoad() { this.emitStyle(); } @@ -138,6 +116,11 @@ export class Toggle implements ComponentInterface { this.disabledChanged(); } + @Listen('click') + onClick() { + this.checked = !this.checked; + } + private emitStyle() { this.ionStyle.emit({ 'interactive-disabled': this.disabled, @@ -176,27 +159,34 @@ export class Toggle implements ComponentInterface { return this.value || ''; } + private onFocus = () => { + this.ionFocus.emit(); + } + + private onBlur = () => { + this.ionBlur.emit(); + } + hostData() { - const labelId = this.inputId + '-lbl'; - const label = findItemLabel(this.el); + const { inputId, disabled, checked, activated, color, el } = this; + const labelId = inputId + '-lbl'; + const label = findItemLabel(el); if (label) { label.id = labelId; } return { 'role': 'checkbox', - 'tabindex': '0', - 'aria-disabled': this.disabled ? 'true' : null, - 'aria-checked': `${this.checked}`, + 'aria-disabled': disabled ? 'true' : null, + 'aria-checked': `${checked}`, 'aria-labelledby': labelId, class: { - ...createColorClasses(this.color), - 'in-item': hostContext('ion-item', this.el), - 'toggle-activated': this.activated, - 'toggle-checked': this.checked, - 'toggle-disabled': this.disabled, - 'toggle-key': this.keyFocus, + ...createColorClasses(color), + 'in-item': hostContext('ion-item', el), + 'toggle-activated': activated, + 'toggle-checked': checked, + 'toggle-disabled': disabled, 'interactive': true } }; @@ -206,11 +196,18 @@ export class Toggle implements ComponentInterface { const value = this.getValue(); renderHiddenInput(true, this.el, this.name, (this.checked ? value : ''), this.disabled); - return ( + return [
-
- ); +
, + + ]; } } diff --git a/core/src/utils/focus-visible.ts b/core/src/utils/focus-visible.ts new file mode 100644 index 0000000000..7ac64767b6 --- /dev/null +++ b/core/src/utils/focus-visible.ts @@ -0,0 +1,46 @@ + +const ION_FOCUSED = 'ion-focused'; +const ION_FOCUSABLE = 'ion-focusable'; +const FOCUS_KEYS = ['Tab', 'ArrowDown', 'Space', 'Escape', ' ', 'Shift', 'Enter', 'ArrowLeft', 'ArrowRight', 'ArrowUp']; + +export function startFocusVisible(doc: Document) { + + let currentFocus: Element[] = []; + let keyboardMode = true; + + function setFocus(elements: Element[]) { + currentFocus.forEach(el => el.classList.remove(ION_FOCUSED)); + elements.forEach(el => el.classList.add(ION_FOCUSED)); + currentFocus = elements; + } + + doc.addEventListener('keydown', ev => { + keyboardMode = FOCUS_KEYS.includes(ev.key); + if (!keyboardMode) { + setFocus([]); + } + }); + + const pointerDown = () => { + keyboardMode = false; + setFocus([]); + }; + doc.addEventListener('focusin', ev => { + if (keyboardMode && ev.composedPath) { + const toFocus = ev.composedPath().filter((el: any) => { + if (el.classList) { + return el.classList.contains(ION_FOCUSABLE); + } + return false; + }) as Element[]; + setFocus(toFocus); + } + }); + doc.addEventListener('focusout', () => { + if (doc.activeElement === doc.body) { + setFocus([]); + } + }); + doc.addEventListener('touchstart', pointerDown); + doc.addEventListener('mousedown', pointerDown); +} diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index 857dbf33b9..487a5d2fa6 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -25,6 +25,18 @@ export function createOverlay(element: T, opts: export function connectListeners(doc: Document) { if (lastId === 0) { lastId = 1; + // trap focus inside overlays + doc.addEventListener('focusin', ev => { + const lastOverlay = getOverlay(doc); + if (lastOverlay && lastOverlay.backdropDismiss && !isDescendant(lastOverlay, ev.target as HTMLElement)) { + const firstInput = lastOverlay.querySelector('input,button') as HTMLElement | null; + if (firstInput) { + firstInput.focus(); + } + } + }); + + // handle back-button click doc.addEventListener('ionBackButton', ev => { const lastOverlay = getOverlay(doc); if (lastOverlay && lastOverlay.backdropDismiss) { @@ -34,6 +46,7 @@ export function connectListeners(doc: Document) { } }); + // handle ESC to close overlay doc.addEventListener('keyup', ev => { if (ev.key === 'Escape') { const lastOverlay = getOverlay(doc); @@ -195,4 +208,14 @@ export function isCancel(role: string | undefined): boolean { return role === 'cancel' || role === BACKDROP; } +function isDescendant(parent: HTMLElement, child: HTMLElement | null) { + while (child) { + if (child === parent) { + return true; + } + child = child.parentElement; + } + return false; +} + export const BACKDROP = 'backdrop'; diff --git a/core/src/utils/tap-click.ts b/core/src/utils/tap-click.ts index 1b43f30296..9dc5cdc287 100644 --- a/core/src/utils/tap-click.ts +++ b/core/src/utils/tap-click.ts @@ -19,6 +19,7 @@ export function startTapClick(doc: Document, config: Config) { if (cancelled || scrolling) { ev.preventDefault(); ev.stopPropagation(); + cancelled = false; } } @@ -48,6 +49,7 @@ export function startTapClick(doc: Document, config: Config) { } function cancelActive() { + console.log('cancelActive()'); clearTimeout(activeDefer); activeDefer = undefined; if (activatableEle) {