fix(inputs): fix aria with shadow-dom (#16329)

This commit is contained in:
Manu MA
2018-11-16 19:26:55 +01:00
committed by GitHub
parent 10971cc3ca
commit fd79b57748
41 changed files with 399 additions and 327 deletions

View File

@ -210,6 +210,8 @@ export declare interface Datetime extends StencilComponents<'IonDatetime'> {}
export class Datetime {
ionCancel: EventEmitter<CustomEvent>;
ionChange: EventEmitter<CustomEvent>;
ionFocus: EventEmitter<CustomEvent>;
ionBlur: EventEmitter<CustomEvent>;
ionStyle: EventEmitter<CustomEvent>;
constructor(c: ChangeDetectorRef, r: ElementRef) {
@ -217,7 +219,7 @@ export class Datetime {
const el = r.nativeElement;
proxyMethods(this, el, ['open']);
proxyInputs(this, el, ['mode', 'name', 'disabled', 'min', 'max', 'displayFormat', 'pickerFormat', 'cancelText', 'doneText', 'yearValues', 'monthValues', 'dayValues', 'hourValues', 'minuteValues', 'monthNames', 'monthShortNames', 'dayNames', 'dayShortNames', 'pickerOptions', 'placeholder', 'value']);
proxyOutputs(this, el, ['ionCancel', 'ionChange', 'ionStyle']);
proxyOutputs(this, el, ['ionCancel', 'ionChange', 'ionFocus', 'ionBlur', 'ionStyle']);
}
}
@ -341,22 +343,22 @@ export class InfiniteScrollContent {
}
export declare interface Input extends StencilComponents<'IonInput'> {}
@Component({ selector: 'ion-input', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '<ng-content></ng-content>', inputs: ['color', 'mode', 'accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'debounce', 'disabled', 'inputmode', 'max', 'maxlength', 'min', 'minlength', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'results', 'spellcheck', 'step', 'size', 'type', 'value'] })
@Component({ selector: 'ion-input', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: '<ng-content></ng-content>', inputs: ['color', 'mode', 'accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'debounce', 'disabled', 'inputmode', 'max', 'maxlength', 'min', 'minlength', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'spellcheck', 'step', 'size', 'type', 'value'] })
export class Input {
ionInput: EventEmitter<CustomEvent>;
ionChange: EventEmitter<CustomEvent>;
ionStyle: EventEmitter<CustomEvent>;
ionBlur: EventEmitter<CustomEvent>;
ionFocus: EventEmitter<CustomEvent>;
ionInputDidLoad: EventEmitter<CustomEvent>;
ionInputDidUnload: EventEmitter<CustomEvent>;
ionStyle: EventEmitter<CustomEvent>;
constructor(c: ChangeDetectorRef, r: ElementRef) {
c.detach();
const el = r.nativeElement;
proxyMethods(this, el, ['setFocus']);
proxyInputs(this, el, ['color', 'mode', 'accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'debounce', 'disabled', 'inputmode', 'max', 'maxlength', 'min', 'minlength', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'results', 'spellcheck', 'step', 'size', 'type', 'value']);
proxyOutputs(this, el, ['ionInput', 'ionChange', 'ionStyle', 'ionBlur', 'ionFocus', 'ionInputDidLoad', 'ionInputDidUnload']);
proxyInputs(this, el, ['color', 'mode', 'accept', 'autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearOnEdit', 'debounce', 'disabled', 'inputmode', 'max', 'maxlength', 'min', 'minlength', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'spellcheck', 'step', 'size', 'type', 'value']);
proxyOutputs(this, el, ['ionInput', 'ionChange', 'ionBlur', 'ionFocus', 'ionInputDidLoad', 'ionInputDidUnload', 'ionStyle']);
}
}

View File

@ -1288,6 +1288,10 @@ export namespace Components {
*/
'name'?: string;
/**
* Emitted when the datetime loses focus.
*/
'onIonBlur'?: (event: CustomEvent<void>) => void;
/**
* Emitted when the datetime selection was cancelled.
*/
'onIonCancel'?: (event: CustomEvent<void>) => void;
@ -1296,6 +1300,10 @@ export namespace Components {
*/
'onIonChange'?: (event: CustomEvent<InputChangeEvent>) => void;
/**
* Emitted when the datetime has focus.
*/
'onIonFocus'?: (event: CustomEvent<void>) => void;
/**
* Emitted when the styles change.
*/
'onIonStyle'?: (event: CustomEvent<StyleEvent>) => void;
@ -1696,10 +1704,6 @@ export namespace Components {
*/
'required': boolean;
/**
* This is a nonstandard attribute supported by Safari that only applies when the type is `"search"`. Its value should be a nonnegative decimal integer.
*/
'results'?: number;
/**
* Sets focus on the specified `ion-input`. Use this method instead of the global `input.focus()`.
*/
'setFocus': () => void;
@ -1842,10 +1846,6 @@ export namespace Components {
*/
'required'?: boolean;
/**
* This is a nonstandard attribute supported by Safari that only applies when the type is `"search"`. Its value should be a nonnegative decimal integer.
*/
'results'?: number;
/**
* The initial size of the control. This value is in pixels unless the value of the type attribute is `"text"` or `"password"`, in which case it is an integer number of characters. This attribute applies only when the `type` attribute is set to `"text"`, `"search"`, `"tel"`, `"url"`, `"email"`, or `"password"`, otherwise it is ignored.
*/
'size'?: number;
@ -4052,7 +4052,7 @@ export namespace Components {
/**
* Opens the select overlay, it could be an alert, action-sheet or popover, based in `ion-select` settings.
*/
'open': (ev?: UIEvent | undefined) => Promise<OverlaySelect>;
'open': (ev?: UIEvent | undefined) => Promise<HTMLIonActionSheetElement | HTMLIonAlertElement | HTMLIonPopoverElement | undefined>;
/**
* The text to display when the select is empty.
*/

View File

@ -323,7 +323,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
<button
type="button"
onClick={() => this.cbClick(i)}
aria-checked={i.checked ? 'true' : null}
aria-checked={`${i.checked}`}
id={i.id}
disabled={i.disabled}
tabIndex={0}
@ -356,7 +356,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
<button
type="button"
onClick={() => this.rbClick(i)}
aria-checked={i.checked ? 'true' : null}
aria-checked={`${i.checked}`}
disabled={i.disabled}
id={i.id}
tabIndex={0}

View File

@ -143,6 +143,7 @@ export class Button implements ComponentInterface {
return {
'ion-activatable': true,
'aria-disabled': this.disabled ? 'true' : null,
class: {
...createColorClasses(color),
[buttonType]: true,

View File

@ -35,7 +35,7 @@
--checkmark-color: #{current-color(contrast)};
}
input {
button {
@include input-cover();
}

View File

@ -1,7 +1,7 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Prop, State, Watch } from '@stencil/core';
import { CheckedInputChangeEvent, Color, Mode, StyleEvent } from '../../interface';
import { renderHiddenInput } from '../../utils/helpers';
import { findItemLabel, renderHiddenInput } from '../../utils/helpers';
import { createColorClasses, hostContext } from '../../utils/theme';
@Component({
@ -15,7 +15,6 @@ import { createColorClasses, hostContext } from '../../utils/theme';
export class Checkbox implements ComponentInterface {
private inputId = `ion-cb-${checkboxIds++}`;
private labelId = `${this.inputId}-lbl`;
@Element() el!: HTMLElement;
@ -74,6 +73,7 @@ export class Checkbox implements ComponentInterface {
/**
* Emitted when the styles change.
* @internal
*/
@Event() ionStyle!: EventEmitter<StyleEvent>;
@ -98,7 +98,7 @@ export class Checkbox implements ComponentInterface {
});
}
private onChange = () => {
private onClick = () => {
this.checked = !this.checked;
}
@ -116,7 +116,16 @@ export class Checkbox implements ComponentInterface {
}
hostData() {
const labelId = this.inputId + '-lbl';
const label = findItemLabel(this.el);
if (label) {
label.id = labelId;
}
return {
'role': 'checkbox',
'aria-disabled': this.disabled ? 'true' : null,
'aria-checked': `${this.checked}`,
'aria-labelledby': labelId,
class: {
...createColorClasses(this.color),
'in-item': hostContext('ion-item', this.el),
@ -129,7 +138,7 @@ export class Checkbox implements ComponentInterface {
}
render() {
renderHiddenInput(this.el, this.name, this.value, this.disabled);
renderHiddenInput(true, this.el, this.name, (this.checked ? this.value : ''), this.disabled);
return [
<svg class="checkbox-icon" viewBox="0 0 24 24">
@ -138,19 +147,14 @@ export class Checkbox implements ComponentInterface {
: <path d="M5.9,12.5l3.8,3.8l8.8-8.8"/>
}
</svg>,
<input
type="checkbox"
id={this.inputId}
aria-labelledby={this.labelId}
onChange={this.onChange}
<button
type="button"
onClick={this.onClick}
onKeyUp={this.onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
onKeyUp={this.onKeyUp}
checked={this.checked}
name={this.name}
value={this.value}
disabled={this.disabled}
/>
>
</button>
];
}
}

View File

@ -27,7 +27,6 @@ Checkboxes allow the selection of multiple options from a set of options. They a
| `ionBlur` | Emitted when the toggle loses focus. | void |
| `ionChange` | Emitted when the checked property has changed. | CheckedInputChangeEvent |
| `ionFocus` | Emitted when the toggle has focus. | void |
| `ionStyle` | Emitted when the styles change. | StyleEvent |
## CSS Custom Properties

View File

@ -40,9 +40,10 @@
:host(.datetime-disabled) {
opacity: .3;
pointer-events: none;
}
.datetime-cover {
button {
@include input-cover();
}

View File

@ -1,7 +1,7 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Method, Prop, State, Watch } from '@stencil/core';
import { DatetimeOptions, InputChangeEvent, Mode, PickerColumn, PickerColumnOption, PickerOptions, StyleEvent } from '../../interface';
import { clamp, renderHiddenInput } from '../../utils/helpers';
import { clamp, findItemLabel, renderHiddenInput } from '../../utils/helpers';
import { hostContext } from '../../utils/theme';
import { DatetimeData, LocaleData, convertDataToISO, convertFormatToKey, convertToArrayOfNumbers, convertToArrayOfStrings, dateDataSortValue, dateSortValue, dateValueRange, daysInMonth, getValueFromFormat, parseDate, parseTemplate, renderDatetime, renderTextFormat, updateDate } from './datetime-util';
@ -16,8 +16,6 @@ import { DatetimeData, LocaleData, convertDataToISO, convertFormatToKey, convert
})
export class Datetime implements ComponentInterface {
private inputId = `ion-dt-${datetimeIds++}`;
private labelId = `${this.inputId}-lbl`;
private picker?: HTMLIonPickerElement;
private locale: LocaleData = {};
private datetimeMin: DatetimeData = {};
private datetimeMax: DatetimeData = {};
@ -25,7 +23,8 @@ export class Datetime implements ComponentInterface {
@Element() el!: HTMLIonDatetimeElement;
@State() text?: string | null;
@State() isExpanded = false;
@State() keyFocus = false;
@Prop({ connect: 'ion-picker-controller' }) pickerCtrl!: HTMLIonPickerControllerElement;
@ -207,8 +206,19 @@ export class Datetime implements ComponentInterface {
*/
@Event() ionChange!: EventEmitter<InputChangeEvent>;
/**
* Emitted when the datetime has focus.
*/
@Event() ionFocus!: EventEmitter<void>;
/**
* Emitted when the datetime loses focus.
*/
@Event() ionBlur!: EventEmitter<void>;
/**
* Emitted when the styles change.
* @internal
*/
@Event() ionStyle!: EventEmitter<StyleEvent>;
@ -233,13 +243,17 @@ export class Datetime implements ComponentInterface {
*/
@Method()
async open() {
if (this.disabled) {
if (this.disabled || this.isExpanded) {
return;
}
const pickerOptions = this.generatePickerOptions();
const picker = this.picker = await this.pickerCtrl.create(pickerOptions);
await this.validate();
const picker = await this.pickerCtrl.create(pickerOptions);
this.isExpanded = true;
picker.onDidDismiss().then(() => {
this.isExpanded = false;
});
await this.validate(picker);
await picker.present();
}
@ -255,7 +269,6 @@ export class Datetime implements ComponentInterface {
private updateDatetimeValue(value: any) {
updateDate(this.datetimeValue, value);
this.updateText();
}
private generatePickerOptions(): PickerOptions {
@ -355,11 +368,11 @@ export class Datetime implements ComponentInterface {
return divyColumns(columns);
}
private async validate() {
private async validate(picker: HTMLIonPickerElement) {
const today = new Date();
const minCompareVal = dateDataSortValue(this.datetimeMin);
const maxCompareVal = dateDataSortValue(this.datetimeMax);
const yearCol = await this.picker!.getColumn('year');
const yearCol = await picker.getColumn('year');
let selectedYear: number = today.getFullYear();
if (yearCol) {
@ -378,7 +391,7 @@ export class Datetime implements ComponentInterface {
}
}
const selectedMonth = await this.validateColumn(
const selectedMonth = await this.validateColumn(picker,
'month', 1,
minCompareVal, maxCompareVal,
[selectedYear, 0, 0, 0, 0],
@ -386,21 +399,21 @@ export class Datetime implements ComponentInterface {
);
const numDaysInMonth = daysInMonth(selectedMonth, selectedYear);
const selectedDay = await this.validateColumn(
const selectedDay = await this.validateColumn(picker,
'day', 2,
minCompareVal, maxCompareVal,
[selectedYear, selectedMonth, 0, 0, 0],
[selectedYear, selectedMonth, numDaysInMonth, 23, 59]
);
const selectedHour = await this.validateColumn(
const selectedHour = await this.validateColumn(picker,
'hour', 3,
minCompareVal, maxCompareVal,
[selectedYear, selectedMonth, selectedDay, 0, 0],
[selectedYear, selectedMonth, selectedDay, 23, 59]
);
await this.validateColumn(
await this.validateColumn(picker,
'minute', 4,
minCompareVal, maxCompareVal,
[selectedYear, selectedMonth, selectedDay, selectedHour, 0],
@ -444,7 +457,7 @@ export class Datetime implements ComponentInterface {
min.second = min.second || 0;
max.second = max.second || 59;
// Ensure min/max constraits
// Ensure min/max constraints
if (min.year > max.year) {
console.error('min.year > max.year');
min.year = max.year - 100;
@ -460,8 +473,8 @@ export class Datetime implements ComponentInterface {
}
}
private async validateColumn(name: string, index: number, min: number, max: number, lowerBounds: number[], upperBounds: number[]): Promise<number> {
const column = await this.picker!.getColumn(name);
private async validateColumn(picker: HTMLIonPickerElement, name: string, index: number, min: number, max: number, lowerBounds: number[], upperBounds: number[]): Promise<number> {
const column = await picker.getColumn(name);
if (!column) {
return 0;
}
@ -497,10 +510,10 @@ export class Datetime implements ComponentInterface {
return 0;
}
private updateText() {
private getText() {
// create the text of the formatted data
const template = this.displayFormat || this.pickerFormat || DEFAULT_FORMAT;
this.text = renderDatetime(template, this.datetimeValue, this.locale);
return renderDatetime(template, this.datetimeValue, this.locale);
}
private hasValue(): boolean {
@ -508,11 +521,39 @@ export class Datetime implements ComponentInterface {
return Object.keys(val).length > 0;
}
private onClick = () => {
this.open();
}
private onKeyUp = () => {
this.keyFocus = true;
}
private onFocus = () => {
this.ionFocus.emit();
}
private onBlur = () => {
this.keyFocus = false;
this.ionBlur.emit();
}
hostData() {
const addPlaceholderClass =
(this.text == null && this.placeholder != null) ? true : false;
(this.getText() === undefined && this.placeholder != null) ? true : false;
const labelId = this.inputId + '-lbl';
const label = findItemLabel(this.el);
if (label) {
label.id = labelId;
}
return {
'role': 'combobox',
'aria-disabled': this.disabled ? 'true' : null,
'aria-expanded': `${this.isExpanded}`,
'aria-haspopup': 'true',
'aria-labelledby': labelId,
class: {
'datetime-disabled': this.disabled,
'datetime-placeholder': addPlaceholderClass,
@ -524,24 +565,22 @@ export class Datetime implements ComponentInterface {
render() {
// If selected text has been passed in, use that first
// otherwise use the placeholder
let datetimeText = this.text;
if (datetimeText == null) {
let datetimeText = this.getText();
if (datetimeText === undefined) {
datetimeText = this.placeholder != null ? this.placeholder : '';
}
renderHiddenInput(this.el, this.name, this.value, this.disabled);
renderHiddenInput(true, this.el, this.name, this.value, this.disabled);
return [
<div class="datetime-text">{datetimeText}</div>,
<button
type="button"
aria-haspopup="true"
aria-labelledby={this.labelId}
aria-disabled={this.disabled ? 'true' : null}
onClick={this.open.bind(this)}
class="datetime-cover"
onClick={this.onClick}
onKeyUp={this.onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
>
</button>,
<slot></slot>
</button>
];
}
}

View File

@ -233,9 +233,10 @@ dates in JavaScript.
| Event | Description | Detail |
| ----------- | --------------------------------------------------- | ---------------- |
| `ionBlur` | Emitted when the datetime loses focus. | void |
| `ionCancel` | Emitted when the datetime selection was cancelled. | void |
| `ionChange` | Emitted when the value (selected date) has changed. | InputChangeEvent |
| `ionStyle` | Emitted when the styles change. | StyleEvent |
| `ionFocus` | Emitted when the datetime has focus. | void |
## Methods

View File

@ -94,6 +94,7 @@ export class FabButton implements ComponentInterface {
const inList = hostContext('ion-fab-list', this.el);
return {
'ion-activatable': true,
'aria-disabled': this.disabled ? 'true' : null,
class: {
...createColorClasses(this.color),
'fab-button-in-list': inList,

View File

@ -1,7 +1,7 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Method, Prop, State, Watch } from '@stencil/core';
import { Color, Mode, StyleEvent, TextFieldTypes, TextInputChangeEvent } from '../../interface';
import { debounceEvent, renderHiddenInput } from '../../utils/helpers';
import { debounceEvent, findItemLabel, renderHiddenInput } from '../../utils/helpers';
import { createColorClasses, hostContext } from '../../utils/theme';
@Component({
@ -145,11 +145,6 @@ export class Input implements ComponentInterface {
*/
@Prop() required = false;
/**
* This is a nonstandard attribute supported by Safari that only applies when the type is `"search"`. Its value should be a nonnegative decimal integer.
*/
@Prop() results?: number;
/**
* If `true`, the element will have its spelling and grammar checked.
*/
@ -181,13 +176,8 @@ export class Input implements ComponentInterface {
*/
@Watch('value')
protected valueChanged() {
const inputEl = this.nativeInput;
const value = this.getValue();
if (inputEl && inputEl.value !== value) {
inputEl.value = value;
}
this.emitStyle();
this.ionChange.emit({ value });
this.ionChange.emit({ value: this.value });
}
/**
@ -200,11 +190,6 @@ export class Input implements ComponentInterface {
*/
@Event() ionChange!: EventEmitter<TextInputChangeEvent>;
/**
* Emitted when the styles change.
*/
@Event() ionStyle!: EventEmitter<StyleEvent>;
/**
* Emitted when the input loses focus.
*/
@ -225,6 +210,12 @@ export class Input implements ComponentInterface {
*/
@Event() ionInputDidUnload!: EventEmitter<void>;
/**
* Emitted when the styles change.
* @internal
*/
@Event() ionStyle!: EventEmitter<StyleEvent>;
componentWillLoad() {
// By default, password inputs clear after focus when they have content
if (this.clearOnEdit === undefined && this.type === 'password') {
@ -240,7 +231,6 @@ export class Input implements ComponentInterface {
}
componentDidUnload() {
this.nativeInput = undefined;
this.ionInputDidUnload.emit();
}
@ -324,6 +314,7 @@ export class Input implements ComponentInterface {
hostData() {
return {
'aria-disabled': this.disabled ? 'true' : null,
class: {
...createColorClasses(this.color),
'in-item': hostContext('ion-item', this.el),
@ -335,19 +326,25 @@ export class Input implements ComponentInterface {
render() {
const value = this.getValue();
renderHiddenInput(this.el, this.name, value, this.disabled);
renderHiddenInput(false, this.el, this.name, value, this.disabled);
const labelId = this.inputId + '-lbl';
const label = findItemLabel(this.el);
if (label) {
label.id = labelId;
}
return [
<input
ref={input => this.nativeInput = input as any}
aria-disabled={this.disabled ? 'true' : null}
class="native-input"
ref={input => this.nativeInput = input}
aria-labelledby={labelId}
disabled={this.disabled}
accept={this.accept}
autoCapitalize={this.autocapitalize}
autoComplete={this.autocomplete}
autoCorrect={this.autocorrect}
autoFocus={this.autofocus}
class="native-input"
disabled={this.disabled}
inputMode={this.inputmode}
min={this.min}
max={this.max}
@ -357,7 +354,6 @@ export class Input implements ComponentInterface {
name={this.name}
pattern={this.pattern}
placeholder={this.placeholder || ''}
results={this.results}
readOnly={this.readonly}
required={this.required}
spellCheck={this.spellcheck}
@ -370,7 +366,6 @@ export class Input implements ComponentInterface {
onFocus={this.onFocus}
onKeyDown={this.onKeydown}
/>,
<slot></slot>,
(this.clearInput && !this.readonly && !this.disabled) && <button
type="button"
class="input-clear-icon"

View File

@ -34,7 +34,6 @@ It is meant for text `type` inputs only, such as `"text"`, `"password"`, `"email
| `placeholder` | `placeholder` | Instructional text that shows before the input has a value. | `null \| string \| undefined` | `undefined` |
| `readonly` | `readonly` | If `true`, the user cannot modify the value. | `boolean` | `false` |
| `required` | `required` | If `true`, the user must fill in a value before submitting a form. | `boolean` | `false` |
| `results` | `results` | This is a nonstandard attribute supported by Safari that only applies when the type is `"search"`. Its value should be a nonnegative decimal integer. | `number \| undefined` | `undefined` |
| `size` | `size` | The initial size of the control. This value is in pixels unless the value of the type attribute is `"text"` or `"password"`, in which case it is an integer number of characters. This attribute applies only when the `type` attribute is set to `"text"`, `"search"`, `"tel"`, `"url"`, `"email"`, or `"password"`, otherwise it is ignored. | `number \| undefined` | `undefined` |
| `spellcheck` | `spellcheck` | If `true`, the element will have its spelling and grammar checked. | `boolean` | `false` |
| `step` | `step` | Works with the min and max attributes to limit the increments at which a value can be set. Possible values are: `"any"` or a positive floating point number. | `string \| undefined` | `undefined` |
@ -52,7 +51,6 @@ It is meant for text `type` inputs only, such as `"text"`, `"password"`, `"email
| `ionInput` | Emitted when a keyboard input ocurred. | KeyboardEvent |
| `ionInputDidLoad` | Emitted when the input has been created. | void |
| `ionInputDidUnload` | Emitted when the input has been removed. | void |
| `ionStyle` | Emitted when the styles change. | StyleEvent |
## Methods

View File

@ -16,6 +16,7 @@ export class ItemGroup implements ComponentInterface {
hostData() {
return {
'role': 'group',
class: {
...createThemedClasses(this.mode, 'item-group'),
'item': true,

View File

@ -169,8 +169,6 @@ button, a {
display: flex;
position: relative;
flex: 1;
flex-direction: inherit;
align-items: inherit;

View File

@ -128,6 +128,7 @@ export class Item implements ComponentInterface {
return {
'ion-activatable': this.isClickable(),
'aria-disabled': this.disabled ? 'true' : null,
class: {
...childStyles,
...createColorClasses(this.color),

View File

@ -1,10 +1,14 @@
import { newE2EPage } from '@stencil/core/testing';
import { E2EPage, newE2EPage } from '@stencil/core/testing';
test('item: inputs', async () => {
const page = await newE2EPage({
url: '/src/components/item/test/inputs?ionic:_testing=true'
});
// check form
await page.click('#submit');
await checkFormResult(page, '{"date":"","select":"n64","toggle":"","input":"","input2":"","checkbox":""}');
// Default case, enabled and no value
let compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
@ -13,13 +17,22 @@ test('item: inputs', async () => {
await page.click('#btnDisabled');
await page.waitFor(250);
// check form
await page.click('#submit');
await checkFormResult(page, '{}');
// screenshot
compare = await page.compareScreenshot('should disable all');
expect(compare).toMatchScreenshot();
// Reenable and set some value
await page.click('#btnDisabled');
await page.click('#btnSomeValue');
await page.waitFor(250);
await page.waitFor(100);
// check form
await page.click('#submit');
await checkFormResult(page, '{"date":"2016-12-09","select":"nes","toggle":"on","input":"Some text","input2":"Some text","checkbox":"on"}');
compare = await page.compareScreenshot('should reenable and set value');
expect(compare).toMatchScreenshot();
@ -45,3 +58,8 @@ test('item: inputs', async () => {
compare = await page.compareScreenshot('should set empty');
expect(compare).toMatchScreenshot();
});
async function checkFormResult(page: E2EPage, content: string) {
const div = await page.find('#form-result');
expect(div.textContent).toEqual(content);
}

View File

@ -20,6 +20,7 @@
</ion-header>
<ion-content padding-vertical>
<form onsubmit="return onSubmit(event)">
<ion-list>
<ion-item>
<ion-label>Simple item</ion-label>
@ -31,12 +32,12 @@
<ion-item>
<ion-label>DateTime</ion-label>
<ion-datetime id="datetime" min="1994-03-14" max="2017-12-09" display-format="MM/DD/YYYY"></ion-datetime>
<ion-datetime name="date" id="datetime" min="1994-03-14" max="2017-12-09" display-format="MM/DD/YYYY"></ion-datetime>
</ion-item>
<ion-item>
<ion-label>Select</ion-label>
<ion-select id="select">
<ion-select name="select" id="select">
<ion-select-option value="">No Game Console</ion-select-option>
<ion-select-option value="nes">NES</ion-select-option>
<ion-select-option value="n64" selected>Nintendo64</ion-select-option>
@ -49,30 +50,34 @@
<ion-item>
<ion-label>Toggle</ion-label>
<ion-toggle id="toggle" name="Actually" slot="end"></ion-toggle>
<ion-toggle name="toggle" id="toggle" name="Actually" slot="end"></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Input (text)</ion-label>
<ion-input id="text"></ion-input>
<ion-input name="input" id="text"></ion-input>
</ion-item>
<ion-item>
<ion-label>Input (placeholder)</ion-label>
<ion-input id="placeholder" placeholder="Placeholder"></ion-input>
<ion-input name="input2" id="placeholder" placeholder="Placeholder"></ion-input>
</ion-item>
<ion-item>
<ion-label>Checkbox</ion-label>
<ion-checkbox id="checkbox" slot="start"></ion-checkbox>
<ion-checkbox name="checkbox" id="checkbox" slot="start"></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Range</ion-label>
<ion-range id="range" value="10"></ion-range>
<ion-range name="range" id="range" value="10"></ion-range>
</ion-item>
</ion-list>
<ion-button id="submit" type="submit">Submit</ion-button>
<p id="form-result"></p>
</form>
<ion-list>
<ion-list-header>Controls</ion-list-header>
<ion-item-divider>Value Controls</ion-item-divider>
@ -181,6 +186,16 @@
console.log('CLICK!', ev.target.tagName, ev.target.textContent.trim());
}
function onSubmit(ev) {
const data = new FormData(ev.target);
const json = {};
data.forEach((value, key) => {
json[key] = value;
})
document.getElementById('form-result').textContent = JSON.stringify(json);
return false;
}
</script>
</html>

View File

@ -33,6 +33,7 @@ export class Label implements ComponentInterface {
/**
* Emitted when the styles change.
* @internal
*/
@Event() ionStyle!: EventEmitter<StyleEvent>;

View File

@ -15,13 +15,6 @@ Label is a wrapper element that can be used in combination with `ion-item`, `ion
| `position` | `position` | The position determines where and how the label behaves inside an item. | `"fixed" \| "floating" \| "stacked" \| undefined` | `undefined` |
## Events
| Event | Description | Detail |
| ---------- | ------------------------------- | ---------- |
| `ionStyle` | Emitted when the styles change. | StyleEvent |
## CSS Custom Properties
| Name | Description |

View File

@ -14,6 +14,8 @@
box-sizing: border-box;
user-select: none;
z-index: $z-index-item-input;
}
:host(.radio-disabled) {
@ -38,7 +40,7 @@
height: var(--inner-height);
}
input {
button {
@include input-cover();
}

View File

@ -1,6 +1,7 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Prop, State, Watch } from '@stencil/core';
import { CheckedInputChangeEvent, Color, Mode, StyleEvent } from '../../interface';
import { findItemLabel } from '../../utils/helpers';
import { createColorClasses, hostContext } from '../../utils/theme';
@Component({
@ -14,7 +15,6 @@ import { createColorClasses, hostContext } from '../../utils/theme';
export class Radio implements ComponentInterface {
private inputId = `ion-rb-${radioButtonIds++}`;
private nativeInput!: HTMLInputElement;
@State() keyFocus = false;
@ -64,6 +64,7 @@ export class Radio implements ComponentInterface {
/**
* Emitted when the styles change.
* @internal
*/
@Event() ionStyle!: EventEmitter<StyleEvent>;
@ -82,31 +83,6 @@ export class Radio implements ComponentInterface {
*/
@Event() ionBlur!: EventEmitter<void>;
componentWillLoad() {
if (this.value == null) {
this.value = this.inputId;
}
this.emitStyle();
}
componentDidLoad() {
this.ionRadioDidLoad.emit();
this.nativeInput.checked = this.checked;
const parentItem = this.nativeInput.closest('ion-item');
if (parentItem) {
const itemLabel = parentItem.querySelector('ion-label');
if (itemLabel) {
itemLabel.id = this.inputId + '-lbl';
this.nativeInput.setAttribute('aria-labelledby', itemLabel.id);
}
}
}
componentDidUnload() {
this.ionRadioDidUnload.emit();
}
@Watch('color')
colorChanged() {
this.emitStyle();
@ -114,11 +90,6 @@ export class Radio implements ComponentInterface {
@Watch('checked')
checkedChanged(isChecked: boolean) {
if (this.nativeInput.checked !== isChecked) {
// keep the checked value and native input `nync
this.nativeInput.checked = isChecked;
}
if (isChecked) {
this.ionSelect.emit({
checked: true,
@ -129,11 +100,25 @@ export class Radio implements ComponentInterface {
}
@Watch('disabled')
disabledChanged(isDisabled: boolean) {
this.nativeInput.disabled = isDisabled;
disabledChanged() {
this.emitStyle();
}
componentWillLoad() {
if (this.value == null) {
this.value = this.inputId;
}
this.emitStyle();
}
componentDidLoad() {
this.ionRadioDidLoad.emit();
}
componentDidUnload() {
this.ionRadioDidUnload.emit();
}
private emitStyle() {
this.ionStyle.emit({
'radio-checked': this.checked,
@ -142,12 +127,7 @@ export class Radio implements ComponentInterface {
}
private onClick = () => {
this.checkedChanged(true);
}
private onChange = () => {
this.checked = true;
this.nativeInput.focus();
}
private onKeyUp = () => {
@ -164,7 +144,16 @@ export class Radio implements ComponentInterface {
}
hostData() {
const labelId = this.inputId + '-lbl';
const label = findItemLabel(this.el);
if (label) {
label.id = labelId;
}
return {
'role': 'radio',
'aria-disabled': this.disabled ? 'true' : null,
'aria-checked': `${this.checked}`,
'aria-labelledby': labelId,
class: {
...createColorClasses(this.color),
'in-item': hostContext('ion-item', this.el),
@ -181,19 +170,14 @@ export class Radio implements ComponentInterface {
<div class="radio-icon">
<div class="radio-inner"/>
</div>,
<input
type="radio"
<button
type="button"
onClick={this.onClick}
onChange={this.onChange}
onKeyUp={this.onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
onKeyUp={this.onKeyUp}
id={this.inputId}
name={this.name}
value={this.value}
disabled={this.disabled}
ref={r => this.nativeInput = (r as any)}
/>
>
</button>,
];
}
}

View File

@ -31,7 +31,6 @@ An `ion-radio-group` can be used to group a set of radios. When radios are insid
| `ionRadioDidLoad` | Emitted when the radio loads. | void |
| `ionRadioDidUnload` | Emitted when the radio unloads. | void |
| `ionSelect` | Emitted when the radio button is selected. | CheckedInputChangeEvent |
| `ionStyle` | Emitted when the styles change. | StyleEvent |
## CSS Custom Properties

View File

@ -47,7 +47,7 @@
</ion-item-divider>
<ion-item>
<ion-label>Pepperoni</ion-label>
<ion-radio slot="end" name="pepperoni" checked></ion-radio>
<ion-radio slot="end" name="pepperoni" checked id="pepperoni-radio"></ion-radio>
</ion-item>
<ion-item>
@ -143,6 +143,12 @@
</ion-content>
<script>
const radio = document.getElementById('pepperoni-radio');
if (radio) {
radio.addEventListener('ionSelect', (ev) => {
console.log(ev.detail);
});
}
var radioValues = ['fruitRadio', 'pizzaRadio', 'veggiesRadio'];
printRadioValues();

View File

@ -28,6 +28,10 @@
z-index: $z-index-item-input;
}
:host(.range-disabled) {
pointer-events: none;
}
::slotted(ion-label) {
flex: initial;
}
@ -36,9 +40,6 @@
font-size: 24px;
}
.range-slider {
position: relative;

View File

@ -133,6 +133,7 @@ export class Range implements ComponentInterface {
/**
* Emitted when the styles change.
* @internal
*/
@Event() ionStyle!: EventEmitter<StyleEvent>;
@ -417,6 +418,7 @@ export class Range implements ComponentInterface {
];
}
}
interface RangeKnob {
knob: string;
value: number;

View File

@ -40,7 +40,6 @@ left or right of the range.
| `ionBlur` | Emitted when the range loses focus. | void |
| `ionChange` | Emitted when the value property has changed. | InputChangeEvent |
| `ionFocus` | Emitted when the range has focus. | void |
| `ionStyle` | Emitted when the styles change. | StyleEvent |
## CSS Custom Properties

View File

@ -66,6 +66,12 @@ export class RippleEffect implements ComponentInterface {
});
});
}
hostData() {
return {
role: 'presentation'
};
}
}
function removeRipple(ripple: HTMLElement) {

View File

@ -20,6 +20,17 @@
margin: 1rem;
}
.block {
position: relative;
display: block;
background: #262626;
color: white;
width: 300px;
height: 300px;
border-radius: 20px;
margin: 1rem;
}
</style>
</head>
@ -45,23 +56,23 @@
<p>
<ion-button size="large" fill="clear">Large</ion-button>
</p>
<div class="my-block">
<div class="my-block" ion-activatable>
<ion-ripple-effect></ion-ripple-effect>
This is just a div + effect behind
<ion-button onclick="buttonClicked()">Nested button</ion-button>
</div>
<div class="my-block">
<div class="my-block" ion-activatable>
This is just a div + effect on top
<ion-button onclick="buttonClicked()">Nested button</ion-button>
<ion-ripple-effect></ion-ripple-effect>
</div>
<div class="my-block">
<div class="my-block" ion-activatable>
This is just a div + effect
<ion-ripple-effect></ion-ripple-effect>
</div>
<a class="my-block">
<a class="my-block" ion-activatable>
This is just a a + effect on top
<ion-button onclick="buttonClicked()">Nested button</ion-button>
<ion-ripple-effect></ion-ripple-effect>
@ -72,6 +83,10 @@
<ion-button onclick="buttonClicked()">Nested button</ion-button>
<ion-ripple-effect></ion-ripple-effect>
</button>
<a class="block" ion-activatable>
<ion-ripple-effect></ion-ripple-effect>
</a>
</ion-content>
</ion-app>

View File

@ -376,7 +376,7 @@ export class Searchbar implements ComponentInterface {
return [
<div class="searchbar-input-container">
<input
ref={el => this.nativeInput = el as HTMLInputElement}
ref={el => this.nativeInput = el}
class="searchbar-input"
onInput={this.onInput}
onBlur={this.onBlur}

View File

@ -69,6 +69,8 @@ export class SegmentButton implements ComponentInterface {
const { disabled, checked, color } = this;
return {
'ion-activatable': 'instant',
'aria-disabled': this.disabled ? 'true' : null,
class: {
...createColorClasses(color),
'segment-button-disabled': disabled,

View File

@ -64,12 +64,11 @@ Since select uses the alert, action sheet and popover interfaces, options can be
| `ionCancel` | Emitted when the selection is cancelled. | void |
| `ionChange` | Emitted when the value has changed. | SelectInputChangeEvent |
| `ionFocus` | Emitted when the select has focus. | void |
| `ionStyle` | Emitted when the styles change. | StyleEvent |
## Methods
### `open(ev?: UIEvent | undefined) => Promise<OverlaySelect>`
### `open(ev?: UIEvent | undefined) => Promise<HTMLIonActionSheetElement | HTMLIonAlertElement | HTMLIonPo...`
Opens the select overlay, it could be an alert, action-sheet or popover,
based in `ion-select` settings.
@ -82,7 +81,7 @@ based in `ion-select` settings.
#### Returns
Type: `Promise<OverlaySelect>`
Type: `Promise<HTMLIonActionSheetElement | HTMLIonAlertElement | HTMLIonPopoverElement | undefined>`

View File

@ -46,7 +46,7 @@
color: var(--placeholder-color);
}
.select-cover {
button {
@include input-cover();
}

View File

@ -1,7 +1,7 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Method, Prop, State, Watch } from '@stencil/core';
import { ActionSheetButton, ActionSheetOptions, AlertOptions, CssClassMap, Mode, OverlaySelect, PopoverOptions, SelectInputChangeEvent, SelectInterface, SelectPopoverOption, StyleEvent } from '../../interface';
import { renderHiddenInput } from '../../utils/helpers';
import { findItemLabel, renderHiddenInput } from '../../utils/helpers';
import { hostContext } from '../../utils/theme';
@Component({
@ -16,7 +16,6 @@ export class Select implements ComponentInterface {
private childOpts: HTMLIonSelectOptionElement[] = [];
private inputId = `ion-sel-${selectIds++}`;
private labelId?: string;
private overlay?: OverlaySelect;
private didInit = false;
@ -110,6 +109,7 @@ export class Select implements ComponentInterface {
/**
* Emitted when the styles change.
* @internal
*/
@Event() ionStyle!: EventEmitter<StyleEvent>;
@ -141,11 +141,6 @@ export class Select implements ComponentInterface {
async componentDidLoad() {
await this.loadOptions();
const label = this.getLabel();
if (label) {
this.labelId = label.id = this.name + '-lbl';
}
if (this.value === undefined) {
if (this.multiple) {
// there are no values set at this point
@ -170,9 +165,22 @@ export class Select implements ComponentInterface {
* based in `ion-select` settings.
*/
@Method()
open(ev?: UIEvent): Promise<OverlaySelect> {
let selectInterface = this.interface;
async open(ev?: UIEvent): Promise<OverlaySelect | undefined> {
if (this.disabled || this.isExpanded) {
return undefined;
}
const overlay = this.overlay = await this.createOverlay(ev);
this.isExpanded = true;
overlay.onDidDismiss().then(() => {
this.overlay = undefined;
this.isExpanded = false;
});
await overlay.present();
return overlay;
}
private createOverlay(ev?: UIEvent): Promise<OverlaySelect> {
let selectInterface = this.interface;
if ((selectInterface === 'action-sheet' || selectInterface === 'popover') && this.multiple) {
console.warn(`Select interface cannot be "${selectInterface}" with a multi-value select. Using the "alert" interface instead.`);
selectInterface = 'alert';
@ -186,11 +194,9 @@ export class Select implements ComponentInterface {
if (selectInterface === 'popover') {
return this.openPopover(ev!);
}
if (selectInterface === 'action-sheet') {
return this.openActionSheet();
}
return this.openAlert();
}
@ -222,11 +228,7 @@ export class Select implements ComponentInterface {
})
}
};
const popover = this.overlay = await this.popoverCtrl.create(popoverOpts);
await popover.present();
this.isExpanded = true;
return popover;
return this.popoverCtrl.create(popoverOpts);
}
private async openActionSheet() {
@ -256,12 +258,7 @@ export class Select implements ComponentInterface {
buttons: actionSheetButtons,
cssClass: ['select-action-sheet', interfaceOptions.cssClass]
};
const actionSheet = this.overlay = await this.actionSheetCtrl.create(actionSheetOpts);
await actionSheet.present();
this.isExpanded = true;
return actionSheet;
return this.actionSheetCtrl.create(actionSheetOpts);
}
private async openAlert() {
@ -303,12 +300,7 @@ export class Select implements ComponentInterface {
cssClass: ['select-alert', interfaceOptions.cssClass,
(this.multiple ? 'multiple-select-alert' : 'single-select-alert')]
};
const alert = this.overlay = await this.alertCtrl.create(alertOpts);
await alert.present();
this.isExpanded = true;
return alert;
return this.alertCtrl.create(alertOpts);
}
/**
@ -319,12 +311,7 @@ export class Select implements ComponentInterface {
if (!this.overlay) {
return Promise.resolve(false);
}
const overlay = this.overlay;
this.overlay = undefined;
this.isExpanded = false;
return overlay.dismiss();
return this.overlay.dismiss();
}
private async loadOptions() {
@ -349,11 +336,7 @@ export class Select implements ComponentInterface {
}
private getLabel() {
const item = this.el.closest('ion-item');
if (item) {
return item.querySelector('ion-label');
}
return null;
return findItemLabel(this.el);
}
private hasValue(): boolean {
@ -379,6 +362,10 @@ export class Select implements ComponentInterface {
});
}
private onClick = (ev: UIEvent) => {
this.open(ev);
}
private onKeyUp = () => {
this.keyFocus = true;
}
@ -393,7 +380,18 @@ export class Select implements ComponentInterface {
}
hostData() {
const labelId = this.inputId + '-lbl';
const label = findItemLabel(this.el);
if (label) {
label.id = labelId;
}
return {
'role': 'combobox',
'aria-disabled': this.disabled ? 'true' : null,
'aria-expanded': `${this.isExpanded}`,
'aria-haspopup': 'dialog',
'aria-labelledby': labelId,
class: {
'in-item': hostContext('ion-item', this.el),
'select-disabled': this.disabled,
@ -403,7 +401,13 @@ export class Select implements ComponentInterface {
}
render() {
renderHiddenInput(this.el, this.name, parseValue(this.value), this.disabled);
renderHiddenInput(true, this.el, this.name, parseValue(this.value), this.disabled);
const labelId = this.inputId + '-lbl';
const label = findItemLabel(this.el);
if (label) {
label.id = labelId;
}
let addPlaceholderClass = false;
let selectText = this.getText();
@ -418,11 +422,7 @@ export class Select implements ComponentInterface {
};
return [
<div
role="textbox"
aria-multiline="false"
class={selectTextClasses}
>
<div class={selectTextClasses}>
{selectText}
</div>,
<div class="select-icon" role="presentation">
@ -430,19 +430,11 @@ export class Select implements ComponentInterface {
</div>,
<button
type="button"
role="combobox"
aria-haspopup="dialog"
aria-labelledby={this.labelId}
aria-expanded={this.isExpanded ? 'true' : null}
aria-disabled={this.disabled ? 'true' : null}
onClick={this.open.bind(this)}
onClick={this.onClick}
onKeyUp={this.onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
class="select-cover"
>
<slot></slot>
{this.mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
</button>
];
}

View File

@ -43,7 +43,6 @@ The textarea component accepts the [native textarea attributes](https://develope
| `ionChange` | Emitted when the input value has changed. | TextInputChangeEvent |
| `ionFocus` | Emitted when the input has focus. | void |
| `ionInput` | Emitted when a keyboard input ocurred. | KeyboardEvent |
| `ionStyle` | Emitted when the styles change. | StyleEvent |
## Methods

View File

@ -1,7 +1,7 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Method, Prop, State, Watch } from '@stencil/core';
import { Color, Mode, StyleEvent, TextInputChangeEvent } from '../../interface';
import { debounceEvent, renderHiddenInput } from '../../utils/helpers';
import { debounceEvent, findItemLabel, renderHiddenInput } from '../../utils/helpers';
import { createColorClasses } from '../../utils/theme';
@Component({
@ -131,8 +131,8 @@ export class Textarea implements ComponentInterface {
protected valueChanged() {
const nativeInput = this.nativeInput;
const value = this.getValue();
if (nativeInput!.value !== value) {
nativeInput!.value = value;
if (nativeInput && nativeInput.value !== value) {
nativeInput.value = value;
}
this.ionChange.emit({ value });
}
@ -149,6 +149,7 @@ export class Textarea implements ComponentInterface {
/**
* Emitted when the styles change.
* @internal
*/
@Event() ionStyle!: EventEmitter<StyleEvent>;
@ -193,30 +194,6 @@ export class Textarea implements ComponentInterface {
});
}
private onInput = (ev: Event) => {
this.value = this.nativeInput!.value;
this.emitStyle();
this.ionInput.emit(ev as KeyboardEvent);
}
private onFocus = () => {
this.hasFocus = true;
this.focusChange();
this.ionFocus.emit();
}
private onBlur = () => {
this.hasFocus = false;
this.focusChange();
this.ionBlur.emit();
}
private onKeyDown = () => {
this.checkClearOnEdit();
}
/**
* Check if we need to clear the text input if clearOnEdit is enabled
*/
@ -251,22 +228,53 @@ export class Textarea implements ComponentInterface {
return this.value || '';
}
private onInput = (ev: Event) => {
if (this.nativeInput) {
this.value = this.nativeInput.value;
}
this.emitStyle();
this.ionInput.emit(ev as KeyboardEvent);
}
private onFocus = () => {
this.hasFocus = true;
this.focusChange();
this.ionFocus.emit();
}
private onBlur = () => {
this.hasFocus = false;
this.focusChange();
this.ionBlur.emit();
}
private onKeyDown = () => {
this.checkClearOnEdit();
}
hostData() {
return {
class: {
...createColorClasses(this.color)
}
'aria-disabled': this.disabled ? 'true' : null,
class: createColorClasses(this.color)
};
}
render() {
const value = this.getValue();
renderHiddenInput(this.el, this.name, value, this.disabled);
renderHiddenInput(false, this.el, this.name, value, this.disabled);
const labelId = this.inputId + '-lbl';
const label = findItemLabel(this.el);
if (label) {
label.id = labelId;
}
return (
<textarea
class="native-textarea"
ref={el => this.nativeInput = el as HTMLTextAreaElement}
ref={el => this.nativeInput = el}
autoCapitalize={this.autocapitalize}
autoFocus={this.autofocus}
disabled={this.disabled}

View File

@ -26,7 +26,6 @@ Toggles change the state of a single option. Toggles can be switched on or off b
| `ionBlur` | Emitted when the toggle loses focus. | void |
| `ionChange` | Emitted when the value property has changed. | CheckedInputChangeEvent |
| `ionFocus` | Emitted when the toggle has focus. | void |
| `ionStyle` | Emitted when the styles change. | StyleEvent |
## CSS Custom Properties

View File

@ -77,6 +77,9 @@ describe('toggle', () => {
// spy on the ionChange event
const ionChange = await page.spyOnEvent('ionChange');
// check aria
expect(toggle).toEqualAttribute('aria-checked', 'true');
// find the hidden input in the light dom
const hiddenInput = await page.find('ion-toggle input[type=hidden]');
@ -86,16 +89,6 @@ describe('toggle', () => {
// hidden in put should have aux-input class
expect(hiddenInput).toHaveClass('aux-input');
// find the checkbox input in the shadow dom
const checkboxInput = await page.find('ion-toggle >>> input[type=checkbox]');
// checkbox input should have value on
expect(checkboxInput).toEqualAttribute('value', 'on');
// checkbox input should have checked property true
const checkedValue = await checkboxInput.getProperty('checked');
expect(checkedValue).toBe(true);
// set checked true again, no actual change
toggle.setProperty('checked', true);
@ -116,7 +109,7 @@ describe('toggle', () => {
expect(checkedValue2).toBe(false);
// hidden input property should no value
expect(hiddenInput).toEqualAttribute('value', '');
expect(toggle).toEqualAttribute('aria-checked', 'false');
expect(ionChange).toHaveReceivedEventTimes(1);

View File

@ -17,6 +17,8 @@
display: inline-block;
outline: none;
contain: content;
cursor: pointer;
touch-action: none;
@ -28,10 +30,6 @@
border: 2px solid #5e9ed6;
}
:host(:focus) {
outline: none;
}
:host(.toggle-disabled) {
pointer-events: none;
}

View File

@ -1,8 +1,8 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Prop, QueueApi, State, Watch } from '@stencil/core';
import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Prop, QueueApi, State, Watch } from '@stencil/core';
import { CheckedInputChangeEvent, Color, Gesture, GestureDetail, Mode, StyleEvent } from '../../interface';
import { hapticSelection } from '../../utils/haptic';
import { renderHiddenInput } from '../../utils/helpers';
import { findItemLabel, renderHiddenInput } from '../../utils/helpers';
import { createColorClasses, hostContext } from '../../utils/theme';
@Component({
@ -16,7 +16,6 @@ import { createColorClasses, hostContext } from '../../utils/theme';
export class Toggle implements ComponentInterface {
private inputId = `ion-tg-${toggleIds++}`;
private nativeInput!: HTMLInputElement;
private pivotX = 0;
private gesture?: Gesture;
@ -80,6 +79,7 @@ export class Toggle implements ComponentInterface {
/**
* Emitted when the styles change.
* @internal
*/
@Event() ionStyle!: EventEmitter<StyleEvent>;
@ -99,20 +99,32 @@ 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();
}
async componentDidLoad() {
const parentItem = this.nativeInput.closest('ion-item');
if (parentItem) {
const itemLabel = parentItem.querySelector('ion-label');
if (itemLabel) {
itemLabel.id = this.inputId + '-lbl';
this.nativeInput.setAttribute('aria-labelledby', itemLabel.id);
}
}
this.gesture = (await import('../../utils/gesture/gesture')).createGesture({
el: this.el,
queue: this.queue,
@ -158,24 +170,6 @@ export class Toggle implements ComponentInterface {
}
this.activated = false;
this.nativeInput.focus();
}
private onChange = () => {
this.checked = !this.checked;
}
private onKeyUp = () => {
this.keyFocus = true;
}
private onFocus = () => {
this.ionFocus.emit();
}
private onBlur = () => {
this.keyFocus = false;
this.ionBlur.emit();
}
private getValue() {
@ -183,7 +177,19 @@ export class Toggle implements ComponentInterface {
}
hostData() {
const labelId = this.inputId + '-lbl';
const label = findItemLabel(this.el);
if (label) {
label.id = labelId;
}
return {
'role': 'checkbox',
'tabindex': '0',
'aria-disabled': this.disabled ? 'true' : null,
'aria-checked': `${this.checked}`,
'aria-labelledby': labelId,
class: {
...createColorClasses(this.color),
'in-item': hostContext('ion-item', this.el),
@ -198,27 +204,13 @@ export class Toggle implements ComponentInterface {
render() {
const value = this.getValue();
renderHiddenInput(this.el, this.name, (this.checked ? value : ''), this.disabled);
renderHiddenInput(true, this.el, this.name, (this.checked ? value : ''), this.disabled);
return [
return (
<div class="toggle-icon">
<div class="toggle-inner"/>
</div>,
<input
type="checkbox"
onChange={this.onChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
onKeyUp={this.onKeyUp}
checked={this.checked}
id={this.inputId}
name={this.name}
value={value}
disabled={this.disabled}
ref={r => this.nativeInput = (r as any)}
/>,
<slot></slot>
];
</div>
);
}
}

View File

@ -21,8 +21,16 @@ export function hasShadowDom(el: HTMLElement) {
return !!el.shadowRoot && !!(el as any).attachShadow;
}
export function renderHiddenInput(container: HTMLElement, name: string, value: string | undefined | null, disabled: boolean) {
if (hasShadowDom(container)) {
export function findItemLabel(componentEl: HTMLElement) {
const itemEl = componentEl.closest('ion-item');
if (itemEl) {
return itemEl.querySelector('ion-label');
}
return null;
}
export function renderHiddenInput(always: boolean, container: HTMLElement, name: string, value: string | undefined | null, disabled: boolean) {
if (always || hasShadowDom(container)) {
let input = container.querySelector('input.aux-input') as HTMLInputElement | null;
if (!input) {
input = container.ownerDocument!.createElement('input');