diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts index 010759cc5a..3b245d1bd1 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -210,6 +210,8 @@ export declare interface Datetime extends StencilComponents<'IonDatetime'> {} export class Datetime { ionCancel: EventEmitter; ionChange: EventEmitter; + ionFocus: EventEmitter; + ionBlur: EventEmitter; ionStyle: EventEmitter; 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: '', 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: '', 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; ionChange: EventEmitter; - ionStyle: EventEmitter; ionBlur: EventEmitter; ionFocus: EventEmitter; ionInputDidLoad: EventEmitter; ionInputDidUnload: EventEmitter; + ionStyle: EventEmitter; 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']); } } diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 2c1fe952ae..7055fab623 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -1288,6 +1288,10 @@ export namespace Components { */ 'name'?: string; /** + * Emitted when the datetime loses focus. + */ + 'onIonBlur'?: (event: CustomEvent) => void; + /** * Emitted when the datetime selection was cancelled. */ 'onIonCancel'?: (event: CustomEvent) => void; @@ -1296,6 +1300,10 @@ export namespace Components { */ 'onIonChange'?: (event: CustomEvent) => void; /** + * Emitted when the datetime has focus. + */ + 'onIonFocus'?: (event: CustomEvent) => void; + /** * Emitted when the styles change. */ 'onIonStyle'?: (event: CustomEvent) => 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; + 'open': (ev?: UIEvent | undefined) => Promise; /** * The text to display when the select is empty. */ diff --git a/core/src/components/alert/alert.tsx b/core/src/components/alert/alert.tsx index 6bac0364f8..e8b0e3e116 100644 --- a/core/src/components/alert/alert.tsx +++ b/core/src/components/alert/alert.tsx @@ -323,7 +323,7 @@ export class Alert implements ComponentInterface, OverlayInterface { ]; } } diff --git a/core/src/components/checkbox/readme.md b/core/src/components/checkbox/readme.md index 66c872bba2..b55f7b2bf9 100644 --- a/core/src/components/checkbox/readme.md +++ b/core/src/components/checkbox/readme.md @@ -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 diff --git a/core/src/components/datetime/datetime.scss b/core/src/components/datetime/datetime.scss index 66b5ae8a9d..280038ab4a 100644 --- a/core/src/components/datetime/datetime.scss +++ b/core/src/components/datetime/datetime.scss @@ -40,9 +40,10 @@ :host(.datetime-disabled) { opacity: .3; + pointer-events: none; } -.datetime-cover { +button { @include input-cover(); } diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index 7f0a2d8240..3c77f1014c 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -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; + /** + * Emitted when the datetime has focus. + */ + @Event() ionFocus!: EventEmitter; + + /** + * Emitted when the datetime loses focus. + */ + @Event() ionBlur!: EventEmitter; + /** * Emitted when the styles change. + * @internal */ @Event() ionStyle!: EventEmitter; @@ -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 { - 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 { + 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 [
{datetimeText}
, , - + ]; } } diff --git a/core/src/components/datetime/readme.md b/core/src/components/datetime/readme.md index 040fe5b3ca..abc9717fcb 100644 --- a/core/src/components/datetime/readme.md +++ b/core/src/components/datetime/readme.md @@ -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 diff --git a/core/src/components/fab-button/fab-button.tsx b/core/src/components/fab-button/fab-button.tsx index fef9b825b6..c954be83fe 100755 --- a/core/src/components/fab-button/fab-button.tsx +++ b/core/src/components/fab-button/fab-button.tsx @@ -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, diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx index cfb1a2f350..c83903410d 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -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; - /** - * Emitted when the styles change. - */ - @Event() ionStyle!: EventEmitter; - /** * Emitted when the input loses focus. */ @@ -225,6 +210,12 @@ export class Input implements ComponentInterface { */ @Event() ionInputDidUnload!: EventEmitter; + /** + * Emitted when the styles change. + * @internal + */ + @Event() ionStyle!: EventEmitter; + 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 [ 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} />, - , (this.clearInput && !this.readonly && !this.disabled) && , ]; } } diff --git a/core/src/components/radio/readme.md b/core/src/components/radio/readme.md index 6a9a127402..3e2cc0d0e3 100644 --- a/core/src/components/radio/readme.md +++ b/core/src/components/radio/readme.md @@ -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 diff --git a/core/src/components/radio/test/basic/index.html b/core/src/components/radio/test/basic/index.html index 8d7292bb2e..24dd6d7207 100644 --- a/core/src/components/radio/test/basic/index.html +++ b/core/src/components/radio/test/basic/index.html @@ -47,7 +47,7 @@ Pepperoni - + @@ -143,6 +143,12 @@