fix(inputs): keyboard focus improvements (#16838)

fixes #16815
fixes #16872
fixes #13978
fixes #16610
This commit is contained in:
Manu MA
2019-01-11 19:36:02 +01:00
committed by GitHub
parent 275d385c17
commit 6364e4e2a1
38 changed files with 263 additions and 234 deletions

View File

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

View File

@@ -190,6 +190,8 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
hostData() {
return {
'role': 'dialog',
'aria-modal': 'true',
style: {
zIndex: 20000 + this.overlayIndex,
},

View File

@@ -117,11 +117,7 @@
}
.alert-tappable {
display: flex;
height: $alert-ios-tappable-height;
contain: strict;
}

View File

@@ -110,12 +110,10 @@
}
.alert-tappable {
display: flex;
position: relative;
height: $alert-md-tappable-height;
contain: strict;
overflow: hidden;
}

View File

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

View File

@@ -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"
>
<div class="alert-button-inner">
<div class="alert-checkbox-icon">
@@ -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"
>
<div class="alert-button-inner">
@@ -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)
};

View File

@@ -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 (
<a
href={this.href}
onClick={(ev: Event) => openURL(this.win, this.href, ev, this.routerDirection)}
>
<a href={this.href}>
<slot></slot>
</a>
);

View File

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

View File

@@ -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 {
<button
type="button"
class="button-native"
onClick={(ev: Event) => this.onClick(ev)}
>
<span class="button-inner">
{backButtonIcon && <ion-icon icon={backButtonIcon} lazy={false}></ion-icon>}

View File

@@ -83,7 +83,7 @@
}
// Focused/Activated Solid Button with Color
:host(.button-solid.ion-color.focused) .button-native {
:host(.button-solid.ion-color.ion-focused) .button-native {
background: #{current-color(shade)};
}
@@ -107,7 +107,7 @@
color: current-color(base);
}
:host(.button-outline.focused.ion-color) .button-native {
:host(.button-outline.ion-focused.ion-color) .button-native {
background: current-color(base, .1);
color: current-color(base);
}
@@ -130,7 +130,7 @@
}
// Focused Clear Button with Color
:host(.button-clear.focused.ion-color) .button-native {
:host(.button-clear.ion-focused.ion-color) .button-native {
background: current-color(base, .1);
color: current-color(base);
}
@@ -231,7 +231,7 @@
pointer-events: none;
}
:host(.focused) .button-native {
:host(.ion-focused) .button-native {
background: var(--background-focused);
color: var(--color-focused);
}

View File

@@ -1,4 +1,4 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Prop, State } from '@stencil/core';
import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Prop } from '@stencil/core';
import { Color, Mode, RouterDirection } from '../../interface';
import { hasShadowDom } from '../../utils/helpers';
@@ -20,8 +20,6 @@ export class Button implements ComponentInterface {
@Prop({ context: 'window' }) win!: Window;
@State() keyFocus = false;
/**
* The color to use from your application's color palette.
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
@@ -103,20 +101,8 @@ export class Button implements ComponentInterface {
this.inToolbar = !!this.el.closest('ion-buttons');
}
private onFocus = () => {
this.ionFocus.emit();
}
private onKeyUp = () => {
this.keyFocus = true;
}
private onBlur = () => {
this.keyFocus = false;
this.ionBlur.emit();
}
private onClick = (ev: Event) => {
@Listen('click')
onClick(ev: Event) {
if (this.type === 'button') {
return openURL(this.win, this.href, ev, this.routerDirection);
@@ -139,14 +125,22 @@ export class Button implements ComponentInterface {
return Promise.resolve(false);
}
private onFocus = () => {
this.ionFocus.emit();
}
private onBlur = () => {
this.ionBlur.emit();
}
hostData() {
const { buttonType, keyFocus, disabled, color, expand, shape, size, strong } = this;
const { buttonType, disabled, color, expand, shape, size, strong } = this;
let fill = this.fill;
if (fill === undefined) {
fill = this.inToolbar ? 'clear' : 'solid';
}
return {
'aria-disabled': this.disabled ? 'true' : null,
'aria-disabled': disabled ? 'true' : null,
class: {
...createColorClasses(color),
[buttonType]: true,
@@ -156,9 +150,9 @@ export class Button implements ComponentInterface {
[`${buttonType}-${fill}`]: true,
[`${buttonType}-strong`]: strong,
'focused': keyFocus,
'button-disabled': disabled,
'ion-activatable': true,
'ion-focusable': true,
}
};
}
@@ -175,9 +169,7 @@ export class Button implements ComponentInterface {
class="button-native"
disabled={this.disabled}
onFocus={this.onFocus}
onKeyUp={this.onKeyUp}
onBlur={this.onBlur}
onClick={this.onClick}
>
<span class="button-inner">
<slot name="icon-only"></slot>

View File

@@ -31,26 +31,6 @@
}
// iOS Checkbox Keyboard Focus
// -----------------------------------------
:host(.checkbox-key) .checkbox-icon::after {
@include border-radius(50%);
@include position(-9px, null, null, -9px);
display: block;
position: absolute;
width: 36px;
height: 36px;
background: $checkbox-ios-background-color-focused;
content: "";
opacity: .2;
}
// iOS Checkbox Within An Item
// -----------------------------------------

View File

@@ -44,27 +44,6 @@
opacity: $checkbox-md-disabled-opacity;
}
// Material Design Checkbox Keyboard Focus
// -----------------------------------------
:host(.checkbox-key) .checkbox-icon::after {
@include border-radius(50%);
@include position(-12px, null, null, -12px);
display: block;
position: absolute;
width: 36px;
height: 36px;
background: $checkbox-md-background-color-focused;
content: "";
opacity: .2;
}
// Material Design Checkbox Within An Item
// -----------------------------------------

View File

@@ -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 { CheckboxChangeEventDetail, Color, Mode, StyleEventDetail } from '../../interface';
import { findItemLabel, renderHiddenInput } from '../../utils/helpers';
@@ -18,8 +18,6 @@ export class Checkbox implements ComponentInterface {
@Element() el!: HTMLElement;
@State() keyFocus = false;
/**
* The color to use from your application's color palette.
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
@@ -98,40 +96,36 @@ export class Checkbox implements ComponentInterface {
});
}
private onClick = () => {
@Listen('click')
onClick() {
this.checked = !this.checked;
}
private onKeyUp = () => {
this.keyFocus = true;
}
private onFocus = () => {
this.ionFocus.emit();
}
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': 'checkbox',
'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),
'checkbox-checked': this.checked,
'checkbox-disabled': this.disabled,
'checkbox-key': this.keyFocus,
...createColorClasses(color),
'in-item': hostContext('ion-item', el),
'checkbox-checked': checked,
'checkbox-disabled': disabled,
'interactive': true
}
};
@@ -149,10 +143,9 @@ export class Checkbox implements ComponentInterface {
</svg>,
<button
type="button"
onClick={this.onClick}
onKeyUp={this.onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
disabled={this.disabled}
>
</button>
];

View File

@@ -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 {
<div class="datetime-text">{datetimeText}</div>,
<button
type="button"
onClick={this.onClick}
onKeyUp={this.onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
disabled={this.disabled}
ref={el => this.buttonEl = el}
>
</button>
];

View File

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

View File

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

View File

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

View File

@@ -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)}
>
<span class="button-inner">
<slot name="start"></slot>

View File

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

View File

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

View File

@@ -91,6 +91,10 @@
// Activated Item
// --------------------------------------------------
:host(.ion-focused) .item-native {
background: var(--background-focused);
}
:host(.activated) .item-native {
background: var(--background-activated);
}

View File

@@ -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 {
<TagType
{...attrs}
class="item-native"
disabled={this.disabled}
onClick={(ev: Event) => openURL(win, href, ev, routerDirection)}
>
<slot name="start"></slot>

View File

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

View File

@@ -202,6 +202,7 @@ export class Picker implements ComponentInterface, OverlayInterface {
hostData() {
return {
'aria-modal': 'true',
class: {
...createThemedClasses(this.mode, 'picker'),
...getClassMap(this.cssClass)

View File

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

View File

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

View File

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

View File

@@ -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 {
</div>,
<button
type="button"
onClick={this.onClick}
onKeyUp={this.onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
disabled={this.disabled}
>
</button>,
];

View File

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

View File

@@ -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}
>
<slot></slot>
{this.mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}

View File

@@ -32,7 +32,7 @@
pointer-events: none;
}
:host(.select-key) button {
:host(.ion-focused) button {
border: 2px solid #5e9ed6;
}

View File

@@ -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 {
</div>,
<button
type="button"
onClick={this.onClick}
onKeyUp={this.onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
disabled={this.disabled}
ref={(el => this.buttonEl = el)}
>
</button>
];

View File

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

View File

@@ -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 [
<div class="toggle-icon">
<div class="toggle-inner"/>
</div>
);
</div>,
<button
type="button"
onFocus={this.onFocus}
onBlur={this.onBlur}
disabled={this.disabled}
>
</button>
];
}
}

View File

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

View File

@@ -25,6 +25,18 @@ export function createOverlay<T extends HTMLIonOverlayElement>(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';

View File

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