Merge branch 'feature-7.0' into 7-sync-09-23-22

This commit is contained in:
Liam DeBeasi
2022-09-23 16:34:53 -05:00
committed by GitHub
15 changed files with 280 additions and 33 deletions

View File

@ -1307,7 +1307,7 @@ ion-textarea,method,setFocus,setFocus() => Promise<void>
ion-textarea,event,ionBlur,FocusEvent,true
ion-textarea,event,ionChange,TextareaChangeEventDetail,true
ion-textarea,event,ionFocus,FocusEvent,true
ion-textarea,event,ionInput,InputEvent,true
ion-textarea,event,ionInput,InputEvent | null,true
ion-textarea,css-prop,--background
ion-textarea,css-prop,--border-radius
ion-textarea,css-prop,--color

View File

@ -2719,7 +2719,7 @@ export namespace Components {
*/
"autofocus": boolean;
/**
* If `true`, the value will be cleared after focus upon edit. Defaults to `true` when `type` is `"password"`, `false` for all other types.
* If `true`, the value will be cleared after focus upon edit.
*/
"clearOnEdit": boolean;
/**
@ -2731,7 +2731,7 @@ export namespace Components {
*/
"cols"?: number;
/**
* Set the amount of time, in milliseconds, to wait to trigger the `ionChange` event after each keystroke. This also impacts form bindings such as `ngModel` or `v-model`.
* Set the amount of time, in milliseconds, to wait to trigger the `ionInput` event after each keystroke.
*/
"debounce": number;
/**
@ -2751,11 +2751,11 @@ export namespace Components {
*/
"inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
/**
* If the value of the type attribute is `text`, `email`, `search`, `password`, `tel`, or `url`, this attribute specifies the maximum number of characters that the user can enter.
* This attribute specifies the maximum number of characters that the user can enter.
*/
"maxlength"?: number;
/**
* If the value of the type attribute is `text`, `email`, `search`, `password`, `tel`, or `url`, this attribute specifies the minimum number of characters that the user can enter.
* This attribute specifies the minimum number of characters that the user can enter.
*/
"minlength"?: number;
/**
@ -6518,7 +6518,7 @@ declare namespace LocalJSX {
*/
"autofocus"?: boolean;
/**
* If `true`, the value will be cleared after focus upon edit. Defaults to `true` when `type` is `"password"`, `false` for all other types.
* If `true`, the value will be cleared after focus upon edit.
*/
"clearOnEdit"?: boolean;
/**
@ -6530,7 +6530,7 @@ declare namespace LocalJSX {
*/
"cols"?: number;
/**
* Set the amount of time, in milliseconds, to wait to trigger the `ionChange` event after each keystroke. This also impacts form bindings such as `ngModel` or `v-model`.
* Set the amount of time, in milliseconds, to wait to trigger the `ionInput` event after each keystroke.
*/
"debounce"?: number;
/**
@ -6546,11 +6546,11 @@ declare namespace LocalJSX {
*/
"inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
/**
* If the value of the type attribute is `text`, `email`, `search`, `password`, `tel`, or `url`, this attribute specifies the maximum number of characters that the user can enter.
* This attribute specifies the maximum number of characters that the user can enter.
*/
"maxlength"?: number;
/**
* If the value of the type attribute is `text`, `email`, `search`, `password`, `tel`, or `url`, this attribute specifies the minimum number of characters that the user can enter.
* This attribute specifies the minimum number of characters that the user can enter.
*/
"minlength"?: number;
/**
@ -6566,7 +6566,7 @@ declare namespace LocalJSX {
*/
"onIonBlur"?: (event: IonTextareaCustomEvent<FocusEvent>) => void;
/**
* Emitted when the input value has changed.
* The `ionChange` event is fired for `<ion-textarea>` elements when the user modifies the element's value. Unlike the `ionInput` event, the `ionChange` event is not necessarily fired for each alteration to an element's value. The `ionChange` event is fired when the element loses focus after its value has been modified.
*/
"onIonChange"?: (event: IonTextareaCustomEvent<TextareaChangeEventDetail>) => void;
/**
@ -6574,9 +6574,9 @@ declare namespace LocalJSX {
*/
"onIonFocus"?: (event: IonTextareaCustomEvent<FocusEvent>) => void;
/**
* Emitted when a keyboard input occurred.
* Ths `ionInput` event fires when the `value` of an `<ion-textarea>` element has been changed.
*/
"onIonInput"?: (event: IonTextareaCustomEvent<InputEvent>) => void;
"onIonInput"?: (event: IonTextareaCustomEvent<InputEvent | null>) => void;
/**
* Emitted when the styles change.
*/

View File

@ -344,6 +344,7 @@ export class Input implements ComponentInterface {
*/
private emitValueChange() {
const { value } = this;
// Checks for both null and undefined values
const newValue = value == null ? value : value.toString();
this.ionChange.emit({ value: newValue });
}
@ -368,7 +369,7 @@ export class Input implements ComponentInterface {
});
}
private onInput = (ev: Event) => {
private onInput = (ev: InputEvent | Event) => {
const input = ev.target as HTMLInputElement | null;
if (input) {
this.value = input.value || '';

View File

@ -0,0 +1,93 @@
import { expect } from '@playwright/test';
import { test } from '@utils/test/playwright';
test.describe('textarea: events: ionChange', () => {
test.beforeEach(({ skip }) => {
skip.rtl();
});
test.describe('when the textarea is blurred', () => {
test('should emit if the value has changed', async ({ page }) => {
await page.setContent(`<ion-textarea></ion-textarea>`);
const nativeTextarea = page.locator('ion-textarea textarea');
const ionChangeSpy = await page.spyOnEvent('ionChange');
await nativeTextarea.type('new value', { delay: 100 });
// Value change is not emitted until the control is blurred.
await nativeTextarea.evaluate((e) => e.blur());
await ionChangeSpy.next();
expect(ionChangeSpy).toHaveReceivedEventDetail({ value: 'new value' });
expect(ionChangeSpy).toHaveReceivedEventTimes(1);
});
test('should emit if the textarea is cleared with an initial value', async ({ page }) => {
await page.setContent(`<ion-textarea clear-on-edit="true" value="123"></ion-textarea>`);
const textarea = page.locator('ion-textarea');
const nativeTextarea = textarea.locator('textarea');
const ionChangeSpy = await page.spyOnEvent('ionChange');
await nativeTextarea.type('new value');
await nativeTextarea.evaluate((e) => e.blur());
await ionChangeSpy.next();
expect(ionChangeSpy).toHaveReceivedEventDetail({ value: 'new value' });
expect(ionChangeSpy).toHaveReceivedEventTimes(1);
});
test('should not emit if the value is set programmatically', async ({ page }) => {
await page.setContent(`<ion-textarea></ion-textarea>`);
const textarea = page.locator('ion-textarea');
const ionChangeSpy = await page.spyOnEvent('ionChange');
await textarea.evaluate((el: HTMLIonTextareaElement) => {
el.value = 'new value';
});
await page.waitForChanges();
expect(ionChangeSpy).toHaveReceivedEventTimes(0);
// Update the value again to make sure it doesn't emit a second time
await textarea.evaluate((el: HTMLIonTextareaElement) => {
el.value = 'new value 2';
});
await page.waitForChanges();
expect(ionChangeSpy).toHaveReceivedEventTimes(0);
});
});
});
test.describe('textarea: events: ionInput', () => {
test('should emit when the user types', async ({ page }) => {
await page.setContent(`<ion-textarea value="some value"></ion-textarea>`);
const ionInputSpy = await page.spyOnEvent('ionInput');
const nativeTextarea = page.locator('ion-textarea textarea');
await nativeTextarea.type('new value', { delay: 100 });
expect(ionInputSpy).toHaveReceivedEventDetail({ isTrusted: true });
});
test('should emit when the textarea is cleared on edit', async ({ page }) => {
await page.setContent(`<ion-textarea clear-on-edit="true" value="some value"></ion-textarea>`);
const ionInputSpy = await page.spyOnEvent('ionInput');
const textarea = page.locator('ion-textarea');
await textarea.click();
await textarea.press('Backspace');
expect(ionInputSpy).toHaveReceivedEventTimes(1);
expect(ionInputSpy).toHaveReceivedEventDetail(null);
});
});

View File

@ -21,9 +21,13 @@ import { createColorClasses } from '../../utils/theme';
export class Textarea implements ComponentInterface {
private nativeInput?: HTMLTextAreaElement;
private inputId = `ion-textarea-${textareaIds++}`;
private didBlurAfterEdit = false;
private didBlurAfterEdit = this.hasValue();
private textareaWrapper?: HTMLElement;
private inheritedAttributes: Attributes = {};
/**
* The value of the input when the textarea is focused.
*/
private focusedValue?: string | null;
@Element() el!: HTMLElement;
@ -48,18 +52,18 @@ export class Textarea implements ComponentInterface {
@Prop() autofocus = false;
/**
* If `true`, the value will be cleared after focus upon edit. Defaults to `true` when `type` is `"password"`, `false` for all other types.
* If `true`, the value will be cleared after focus upon edit.
*/
@Prop({ mutable: true }) clearOnEdit = false;
@Prop() clearOnEdit = false;
/**
* Set the amount of time, in milliseconds, to wait to trigger the `ionChange` event after each keystroke. This also impacts form bindings such as `ngModel` or `v-model`.
* Set the amount of time, in milliseconds, to wait to trigger the `ionInput` event after each keystroke.
*/
@Prop() debounce = 0;
@Watch('debounce')
protected debounceChanged() {
this.ionChange = debounceEvent(this.ionChange, this.debounce);
this.ionInput = debounceEvent(this.ionInput, this.debounce);
}
/**
@ -87,12 +91,12 @@ export class Textarea implements ComponentInterface {
@Prop() enterkeyhint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send';
/**
* If the value of the type attribute is `text`, `email`, `search`, `password`, `tel`, or `url`, this attribute specifies the maximum number of characters that the user can enter.
* This attribute specifies the maximum number of characters that the user can enter.
*/
@Prop() maxlength?: number;
/**
* If the value of the type attribute is `text`, `email`, `search`, `password`, `tel`, or `url`, this attribute specifies the minimum number of characters that the user can enter.
* This attribute specifies the minimum number of characters that the user can enter.
*/
@Prop() minlength?: number;
@ -159,18 +163,23 @@ export class Textarea implements ComponentInterface {
}
this.runAutoGrow();
this.emitStyle();
this.ionChange.emit({ value });
}
/**
* Emitted when the input value has changed.
* The `ionChange` event is fired for `<ion-textarea>` elements when the user
* modifies the element's value. Unlike the `ionInput` event, the `ionChange`
* event is not necessarily fired for each alteration to an element's value.
*
* The `ionChange` event is fired when the element loses focus after its value
* has been modified.
*/
@Event() ionChange!: EventEmitter<TextareaChangeEventDetail>;
/**
* Emitted when a keyboard input occurred.
* Ths `ionInput` event fires when the `value` of an `<ion-textarea>` element
* has been changed.
*/
@Event() ionInput!: EventEmitter<InputEvent>;
@Event() ionInput!: EventEmitter<InputEvent | null>;
/**
* Emitted when the styles change.
@ -252,6 +261,21 @@ export class Textarea implements ComponentInterface {
});
}
/**
* Emits an `ionChange` event.
*
* This API should be called for user committed changes.
* This API should not be used for external value changes.
*/
private emitValueChange() {
const { value } = this;
// Checks for both null and undefined values
const newValue = value == null ? value : value.toString();
// Emitting a value change should update the internal state for tracking the focused value
this.focusedValue = newValue;
this.ionChange.emit({ value: newValue });
}
private runAutoGrow() {
if (this.nativeInput && this.autoGrow) {
writeTask(() => {
@ -278,6 +302,8 @@ export class Textarea implements ComponentInterface {
this.value = '';
}
this.ionInput.emit(null);
// Reset the flag
this.didBlurAfterEdit = false;
}
@ -298,16 +324,26 @@ export class Textarea implements ComponentInterface {
return this.value || '';
}
// `Event` type is used instead of `InputEvent`
// since the types from Stencil are not derived
// from the element (e.g. textarea and input
// should be InputEvent, but all other elements
// should be Event).
private onInput = (ev: Event) => {
if (this.nativeInput) {
this.value = this.nativeInput.value;
const input = ev.target as HTMLTextAreaElement | null;
if (input) {
this.value = input.value || '';
}
this.emitStyle();
this.ionInput.emit(ev as InputEvent);
};
private onChange = () => {
this.emitValueChange();
};
private onFocus = (ev: FocusEvent) => {
this.hasFocus = true;
this.focusedValue = this.value;
this.focusChange();
this.ionFocus.emit(ev);
@ -317,6 +353,14 @@ export class Textarea implements ComponentInterface {
this.hasFocus = false;
this.focusChange();
if (this.focusedValue !== this.value) {
/**
* Emits the `ionChange` event when the textarea value
* is different than the value when the textarea was focused.
*/
this.emitValueChange();
}
this.ionBlur.emit(ev);
};
@ -361,6 +405,7 @@ export class Textarea implements ComponentInterface {
rows={this.rows}
wrap={this.wrap}
onInput={this.onInput}
onChange={this.onChange}
onBlur={this.onBlur}
onFocus={this.onFocus}
onKeyDown={this.onKeyDown}