mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 10:01:59 +08:00
Merge branch 'feature-7.0' into 7-sync-09-23-22
This commit is contained in:
12
BREAKING.md
12
BREAKING.md
@ -21,6 +21,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver
|
|||||||
- [Range](#version-7x-range)
|
- [Range](#version-7x-range)
|
||||||
- [Segment](#version-7x-segment)
|
- [Segment](#version-7x-segment)
|
||||||
- [Slides](#version-7x-slides)
|
- [Slides](#version-7x-slides)
|
||||||
|
- [Textarea](#version-7x-textarea)
|
||||||
- [Virtual Scroll](#version-7x-virtual-scroll)
|
- [Virtual Scroll](#version-7x-virtual-scroll)
|
||||||
- [Utilities](#version-7x-utilities)
|
- [Utilities](#version-7x-utilities)
|
||||||
- [hidden attribute](#version-7x-hidden-attribute)
|
- [hidden attribute](#version-7x-hidden-attribute)
|
||||||
@ -109,6 +110,17 @@ Developers using these components will need to migrate to using Swiper.js direct
|
|||||||
- [React](https://ionicframework.com/docs/react/slides)
|
- [React](https://ionicframework.com/docs/react/slides)
|
||||||
- [Vue](https://ionicframework.com/docs/vue/slides)
|
- [Vue](https://ionicframework.com/docs/vue/slides)
|
||||||
|
|
||||||
|
<h4 id="version-7x-textarea">Textarea</h4>
|
||||||
|
|
||||||
|
- `ionChange` is no longer emitted when the `value` of `ion-textarea` is modified externally. `ionChange` is only emitted from user committed changes, such as typing in the textarea and the textarea losing focus.
|
||||||
|
|
||||||
|
- If your application requires immediate feedback based on the user typing actively in the textarea, consider migrating your event listeners to using `ionInput` instead.
|
||||||
|
|
||||||
|
- The `debounce` property has been updated to control the timing in milliseconds to delay the event emission of the `ionInput` event after each keystroke. Previously it would delay the event emission of `ionChange`.
|
||||||
|
|
||||||
|
- `ionInput` dispatches an event detail of `null` when the textarea is cleared as a result of `clear-on-edit="true"`.
|
||||||
|
|
||||||
|
|
||||||
<h4 id="version-7x-virtual-scroll">Virtual Scroll</h4>
|
<h4 id="version-7x-virtual-scroll">Virtual Scroll</h4>
|
||||||
|
|
||||||
`ion-virtual-scroll` has been removed from Ionic.
|
`ion-virtual-scroll` has been removed from Ionic.
|
||||||
|
@ -5,7 +5,7 @@ import { ValueAccessor } from './value-accessor';
|
|||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
/* tslint:disable-next-line:directive-selector */
|
/* tslint:disable-next-line:directive-selector */
|
||||||
selector: 'ion-textarea,ion-searchbar',
|
selector: 'ion-searchbar',
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: NG_VALUE_ACCESSOR,
|
provide: NG_VALUE_ACCESSOR,
|
||||||
@ -26,7 +26,7 @@ export class TextValueAccessorDirective extends ValueAccessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: 'ion-input:not([type=number])',
|
selector: 'ion-input:not([type=number]),ion-textarea',
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: NG_VALUE_ACCESSOR,
|
provide: NG_VALUE_ACCESSOR,
|
||||||
@ -35,6 +35,7 @@ export class TextValueAccessorDirective extends ValueAccessor {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
// TODO rename this value accessor to `TextValueAccessorDirective` when search-bar is updated
|
||||||
export class InputValueAccessorDirective extends ValueAccessor {
|
export class InputValueAccessorDirective extends ValueAccessor {
|
||||||
constructor(injector: Injector, el: ElementRef) {
|
constructor(injector: Injector, el: ElementRef) {
|
||||||
super(injector, el);
|
super(injector, el);
|
||||||
|
@ -1816,13 +1816,19 @@ export class IonText {
|
|||||||
import type { TextareaChangeEventDetail as ITextareaTextareaChangeEventDetail } from '@ionic/core';
|
import type { TextareaChangeEventDetail as ITextareaTextareaChangeEventDetail } from '@ionic/core';
|
||||||
export declare interface IonTextarea extends Components.IonTextarea {
|
export declare interface IonTextarea extends Components.IonTextarea {
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
*/
|
*/
|
||||||
ionChange: EventEmitter<CustomEvent<ITextareaTextareaChangeEventDetail>>;
|
ionChange: EventEmitter<CustomEvent<ITextareaTextareaChangeEventDetail>>;
|
||||||
/**
|
/**
|
||||||
* Emitted when a keyboard input occurred.
|
* Ths `ionInput` event fires when the `value` of an `<ion-textarea>` element
|
||||||
|
has been changed.
|
||||||
*/
|
*/
|
||||||
ionInput: EventEmitter<CustomEvent<InputEvent>>;
|
ionInput: EventEmitter<CustomEvent<InputEvent | null>>;
|
||||||
/**
|
/**
|
||||||
* Emitted when the input loses focus.
|
* Emitted when the input loses focus.
|
||||||
*/
|
*/
|
||||||
|
18
angular/test/base/e2e/src/textarea.spec.ts
Normal file
18
angular/test/base/e2e/src/textarea.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
describe('Textarea', () => {
|
||||||
|
beforeEach(() => cy.visit('/textarea'));
|
||||||
|
|
||||||
|
it('should become valid', () => {
|
||||||
|
cy.get('#status').should('have.text', 'INVALID');
|
||||||
|
|
||||||
|
cy.get('ion-textarea').type('hello');
|
||||||
|
|
||||||
|
cy.get('#status').should('have.text', 'VALID');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the form control value when typing', () => {
|
||||||
|
cy.get('#value').contains(`"textarea": ""`);
|
||||||
|
cy.get('ion-textarea').type('hello');
|
||||||
|
|
||||||
|
cy.get('#value').contains(`"textarea": "hello"`);
|
||||||
|
});
|
||||||
|
});
|
@ -25,6 +25,7 @@ const routes: Routes = [
|
|||||||
{ path: 'accordions', component: AccordionComponent },
|
{ path: 'accordions', component: AccordionComponent },
|
||||||
{ path: 'alerts', component: AlertComponent },
|
{ path: 'alerts', component: AlertComponent },
|
||||||
{ path: 'inputs', component: InputsComponent },
|
{ path: 'inputs', component: InputsComponent },
|
||||||
|
{ path: 'textarea', loadChildren: () => import('./textarea/textarea.module').then(m => m.TextareaModule) },
|
||||||
{ path: 'form', component: FormComponent },
|
{ path: 'form', component: FormComponent },
|
||||||
{ path: 'modals', component: ModalComponent },
|
{ path: 'modals', component: ModalComponent },
|
||||||
{ path: 'modal-inline', loadChildren: () => import('./modal-inline').then(m => m.ModalInlineModule) },
|
{ path: 'modal-inline', loadChildren: () => import('./modal-inline').then(m => m.ModalInlineModule) },
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
Form Status: <span id="status">{{ profileForm.status }}</span>
|
Form Status: <span id="status">{{ profileForm.status }}</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Form Status: <span id="data">{{ profileForm.value | json }}</span>
|
Form value: <span id="data">{{ profileForm.value | json }}</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Form Submit: <span id="submit">{{submitted}}</span>
|
Form Submit: <span id="submit">{{submitted}}</span>
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
import { RouterModule } from "@angular/router";
|
||||||
|
import { TextareaComponent } from "./textarea.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: TextareaComponent
|
||||||
|
}
|
||||||
|
])
|
||||||
|
],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class TextareaRoutingModule { }
|
16
angular/test/base/src/app/textarea/textarea.component.html
Normal file
16
angular/test/base/src/app/textarea/textarea.component.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<ion-content>
|
||||||
|
<form [formGroup]="form">
|
||||||
|
<ion-list>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Textarea</ion-label>
|
||||||
|
<ion-textarea formControlName="textarea"></ion-textarea>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
||||||
|
</form>
|
||||||
|
<p>
|
||||||
|
Form status: <span id="status">{{ form.status }}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Form value: <span id="value">{{ form.value | json }}</span>
|
||||||
|
</p>
|
||||||
|
</ion-content>
|
17
angular/test/base/src/app/textarea/textarea.component.ts
Normal file
17
angular/test/base/src/app/textarea/textarea.component.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { FormBuilder, Validators } from '@angular/forms';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-textarea',
|
||||||
|
templateUrl: 'textarea.component.html',
|
||||||
|
})
|
||||||
|
export class TextareaComponent {
|
||||||
|
|
||||||
|
form = this.fb.group({
|
||||||
|
textarea: ['', Validators.required]
|
||||||
|
})
|
||||||
|
|
||||||
|
constructor(private fb: FormBuilder) { }
|
||||||
|
|
||||||
|
}
|
21
angular/test/base/src/app/textarea/textarea.module.ts
Normal file
21
angular/test/base/src/app/textarea/textarea.module.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||||
|
import { IonicModule } from "@ionic/angular";
|
||||||
|
|
||||||
|
import { TextareaRoutingModule } from "./textarea-routing.module";
|
||||||
|
import { TextareaComponent } from "./textarea.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
IonicModule,
|
||||||
|
TextareaRoutingModule
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
TextareaComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class TextareaModule { }
|
@ -1307,7 +1307,7 @@ ion-textarea,method,setFocus,setFocus() => Promise<void>
|
|||||||
ion-textarea,event,ionBlur,FocusEvent,true
|
ion-textarea,event,ionBlur,FocusEvent,true
|
||||||
ion-textarea,event,ionChange,TextareaChangeEventDetail,true
|
ion-textarea,event,ionChange,TextareaChangeEventDetail,true
|
||||||
ion-textarea,event,ionFocus,FocusEvent,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,--background
|
||||||
ion-textarea,css-prop,--border-radius
|
ion-textarea,css-prop,--border-radius
|
||||||
ion-textarea,css-prop,--color
|
ion-textarea,css-prop,--color
|
||||||
|
22
core/src/components.d.ts
vendored
22
core/src/components.d.ts
vendored
@ -2719,7 +2719,7 @@ export namespace Components {
|
|||||||
*/
|
*/
|
||||||
"autofocus": boolean;
|
"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;
|
"clearOnEdit": boolean;
|
||||||
/**
|
/**
|
||||||
@ -2731,7 +2731,7 @@ export namespace Components {
|
|||||||
*/
|
*/
|
||||||
"cols"?: number;
|
"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;
|
"debounce": number;
|
||||||
/**
|
/**
|
||||||
@ -2751,11 +2751,11 @@ export namespace Components {
|
|||||||
*/
|
*/
|
||||||
"inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
|
"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;
|
"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;
|
"minlength"?: number;
|
||||||
/**
|
/**
|
||||||
@ -6518,7 +6518,7 @@ declare namespace LocalJSX {
|
|||||||
*/
|
*/
|
||||||
"autofocus"?: boolean;
|
"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;
|
"clearOnEdit"?: boolean;
|
||||||
/**
|
/**
|
||||||
@ -6530,7 +6530,7 @@ declare namespace LocalJSX {
|
|||||||
*/
|
*/
|
||||||
"cols"?: number;
|
"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;
|
"debounce"?: number;
|
||||||
/**
|
/**
|
||||||
@ -6546,11 +6546,11 @@ declare namespace LocalJSX {
|
|||||||
*/
|
*/
|
||||||
"inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
|
"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;
|
"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;
|
"minlength"?: number;
|
||||||
/**
|
/**
|
||||||
@ -6566,7 +6566,7 @@ declare namespace LocalJSX {
|
|||||||
*/
|
*/
|
||||||
"onIonBlur"?: (event: IonTextareaCustomEvent<FocusEvent>) => void;
|
"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;
|
"onIonChange"?: (event: IonTextareaCustomEvent<TextareaChangeEventDetail>) => void;
|
||||||
/**
|
/**
|
||||||
@ -6574,9 +6574,9 @@ declare namespace LocalJSX {
|
|||||||
*/
|
*/
|
||||||
"onIonFocus"?: (event: IonTextareaCustomEvent<FocusEvent>) => void;
|
"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.
|
* Emitted when the styles change.
|
||||||
*/
|
*/
|
||||||
|
@ -344,6 +344,7 @@ export class Input implements ComponentInterface {
|
|||||||
*/
|
*/
|
||||||
private emitValueChange() {
|
private emitValueChange() {
|
||||||
const { value } = this;
|
const { value } = this;
|
||||||
|
// Checks for both null and undefined values
|
||||||
const newValue = value == null ? value : value.toString();
|
const newValue = value == null ? value : value.toString();
|
||||||
this.ionChange.emit({ value: newValue });
|
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;
|
const input = ev.target as HTMLInputElement | null;
|
||||||
if (input) {
|
if (input) {
|
||||||
this.value = input.value || '';
|
this.value = input.value || '';
|
||||||
|
93
core/src/components/textarea/test/textarea-events.e2e.ts
Normal file
93
core/src/components/textarea/test/textarea-events.e2e.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
@ -21,9 +21,13 @@ import { createColorClasses } from '../../utils/theme';
|
|||||||
export class Textarea implements ComponentInterface {
|
export class Textarea implements ComponentInterface {
|
||||||
private nativeInput?: HTMLTextAreaElement;
|
private nativeInput?: HTMLTextAreaElement;
|
||||||
private inputId = `ion-textarea-${textareaIds++}`;
|
private inputId = `ion-textarea-${textareaIds++}`;
|
||||||
private didBlurAfterEdit = false;
|
private didBlurAfterEdit = this.hasValue();
|
||||||
private textareaWrapper?: HTMLElement;
|
private textareaWrapper?: HTMLElement;
|
||||||
private inheritedAttributes: Attributes = {};
|
private inheritedAttributes: Attributes = {};
|
||||||
|
/**
|
||||||
|
* The value of the input when the textarea is focused.
|
||||||
|
*/
|
||||||
|
private focusedValue?: string | null;
|
||||||
|
|
||||||
@Element() el!: HTMLElement;
|
@Element() el!: HTMLElement;
|
||||||
|
|
||||||
@ -48,18 +52,18 @@ export class Textarea implements ComponentInterface {
|
|||||||
@Prop() autofocus = false;
|
@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;
|
@Prop() debounce = 0;
|
||||||
|
|
||||||
@Watch('debounce')
|
@Watch('debounce')
|
||||||
protected debounceChanged() {
|
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';
|
@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;
|
@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;
|
@Prop() minlength?: number;
|
||||||
|
|
||||||
@ -159,18 +163,23 @@ export class Textarea implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
this.runAutoGrow();
|
this.runAutoGrow();
|
||||||
this.emitStyle();
|
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>;
|
@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.
|
* 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() {
|
private runAutoGrow() {
|
||||||
if (this.nativeInput && this.autoGrow) {
|
if (this.nativeInput && this.autoGrow) {
|
||||||
writeTask(() => {
|
writeTask(() => {
|
||||||
@ -278,6 +302,8 @@ export class Textarea implements ComponentInterface {
|
|||||||
this.value = '';
|
this.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.ionInput.emit(null);
|
||||||
|
|
||||||
// Reset the flag
|
// Reset the flag
|
||||||
this.didBlurAfterEdit = false;
|
this.didBlurAfterEdit = false;
|
||||||
}
|
}
|
||||||
@ -298,16 +324,26 @@ export class Textarea implements ComponentInterface {
|
|||||||
return this.value || '';
|
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) => {
|
private onInput = (ev: Event) => {
|
||||||
if (this.nativeInput) {
|
const input = ev.target as HTMLTextAreaElement | null;
|
||||||
this.value = this.nativeInput.value;
|
if (input) {
|
||||||
|
this.value = input.value || '';
|
||||||
}
|
}
|
||||||
this.emitStyle();
|
|
||||||
this.ionInput.emit(ev as InputEvent);
|
this.ionInput.emit(ev as InputEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onChange = () => {
|
||||||
|
this.emitValueChange();
|
||||||
|
};
|
||||||
|
|
||||||
private onFocus = (ev: FocusEvent) => {
|
private onFocus = (ev: FocusEvent) => {
|
||||||
this.hasFocus = true;
|
this.hasFocus = true;
|
||||||
|
this.focusedValue = this.value;
|
||||||
this.focusChange();
|
this.focusChange();
|
||||||
|
|
||||||
this.ionFocus.emit(ev);
|
this.ionFocus.emit(ev);
|
||||||
@ -317,6 +353,14 @@ export class Textarea implements ComponentInterface {
|
|||||||
this.hasFocus = false;
|
this.hasFocus = false;
|
||||||
this.focusChange();
|
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);
|
this.ionBlur.emit(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -361,6 +405,7 @@ export class Textarea implements ComponentInterface {
|
|||||||
rows={this.rows}
|
rows={this.rows}
|
||||||
wrap={this.wrap}
|
wrap={this.wrap}
|
||||||
onInput={this.onInput}
|
onInput={this.onInput}
|
||||||
|
onChange={this.onChange}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
|
Reference in New Issue
Block a user