diff --git a/BREAKING.md b/BREAKING.md index 9288951b47..3768cb46ec 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -14,6 +14,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver - [Browser and Platform Support](#version-7x-browser-platform-support) - [Components](#version-7x-components) + - [Input](#version-7x-input) - [Overlays](#version-7x-overlays) - [Range](#version-7x-range) - [Slides](#version-7x-slides) @@ -50,6 +51,12 @@ This section details the desktop browser, JavaScript framework, and mobile platf

Components

+

Input

+ +`ionChange` is no longer emitted when the `value` of `ion-input` is modified externally. `ionChange` is only emitted from user committed changes, such as typing in the input and the input losing focus or from clicking the clear action within the input. + +If your application requires immediate feedback based on the user typing actively in the input, consider migrating your event listeners to using `ionInput` instead. +

Overlays

Ionic now listens on the `keydown` event instead of the `keyup` event when determining when to dismiss overlays via the "Escape" key. Any applications that were listening on `keyup` to suppress this behavior should listen on `keydown` instead. diff --git a/angular/src/directives/control-value-accessors/boolean-value-accessor.ts b/angular/src/directives/control-value-accessors/boolean-value-accessor.ts index ac15f5b6b6..b8484516e4 100644 --- a/angular/src/directives/control-value-accessors/boolean-value-accessor.ts +++ b/angular/src/directives/control-value-accessors/boolean-value-accessor.ts @@ -25,6 +25,6 @@ export class BooleanValueAccessorDirective extends ValueAccessor { @HostListener('ionChange', ['$event.target']) _handleIonChange(el: any): void { - this.handleChangeEvent(el, el.checked); + this.handleValueChange(el, el.checked); } } diff --git a/angular/src/directives/control-value-accessors/index.ts b/angular/src/directives/control-value-accessors/index.ts new file mode 100644 index 0000000000..f18d651097 --- /dev/null +++ b/angular/src/directives/control-value-accessors/index.ts @@ -0,0 +1,5 @@ +export * from './boolean-value-accessor'; +export * from './numeric-value-accessor'; +export * from './radio-value-accessor'; +export * from './select-value-accessor'; +export * from './text-value-accessor'; diff --git a/angular/src/directives/control-value-accessors/numeric-value-accessor.ts b/angular/src/directives/control-value-accessors/numeric-value-accessor.ts index e7ce54f671..33401ee0e1 100644 --- a/angular/src/directives/control-value-accessors/numeric-value-accessor.ts +++ b/angular/src/directives/control-value-accessors/numeric-value-accessor.ts @@ -18,13 +18,13 @@ export class NumericValueAccessorDirective extends ValueAccessor { super(injector, el); } - @HostListener('ionChange', ['$event.target']) - _handleIonChange(el: any): void { - this.handleChangeEvent(el, el.value); + @HostListener('ionInput', ['$event.target']) + handleInputEvent(el: HTMLIonInputElement): void { + this.handleValueChange(el, el.value); } registerOnChange(fn: (_: number | null) => void): void { - super.registerOnChange((value) => { + super.registerOnChange((value: string) => { fn(value === '' ? null : parseFloat(value)); }); } diff --git a/angular/src/directives/control-value-accessors/radio-value-accessor.ts b/angular/src/directives/control-value-accessors/radio-value-accessor.ts index 192f10b58c..a3781e9247 100644 --- a/angular/src/directives/control-value-accessors/radio-value-accessor.ts +++ b/angular/src/directives/control-value-accessors/radio-value-accessor.ts @@ -21,6 +21,6 @@ export class RadioValueAccessorDirective extends ValueAccessor { @HostListener('ionSelect', ['$event.target']) _handleIonSelect(el: any): void { - this.handleChangeEvent(el, el.checked); + this.handleValueChange(el, el.checked); } } diff --git a/angular/src/directives/control-value-accessors/select-value-accessor.ts b/angular/src/directives/control-value-accessors/select-value-accessor.ts index 36c36f218a..d46c73ca43 100644 --- a/angular/src/directives/control-value-accessors/select-value-accessor.ts +++ b/angular/src/directives/control-value-accessors/select-value-accessor.ts @@ -21,6 +21,6 @@ export class SelectValueAccessorDirective extends ValueAccessor { @HostListener('ionChange', ['$event.target']) _handleChangeEvent(el: any): void { - this.handleChangeEvent(el, el.value); + this.handleValueChange(el, el.value); } } diff --git a/angular/src/directives/control-value-accessors/text-value-accessor.ts b/angular/src/directives/control-value-accessors/text-value-accessor.ts index b59d475fcc..abcc581dea 100644 --- a/angular/src/directives/control-value-accessors/text-value-accessor.ts +++ b/angular/src/directives/control-value-accessors/text-value-accessor.ts @@ -5,7 +5,7 @@ import { ValueAccessor } from './value-accessor'; @Directive({ /* tslint:disable-next-line:directive-selector */ - selector: 'ion-input:not([type=number]),ion-textarea,ion-searchbar', + selector: 'ion-textarea,ion-searchbar', providers: [ { provide: NG_VALUE_ACCESSOR, @@ -21,6 +21,27 @@ export class TextValueAccessorDirective extends ValueAccessor { @HostListener('ionChange', ['$event.target']) _handleInputEvent(el: any): void { - this.handleChangeEvent(el, el.value); + this.handleValueChange(el, el.value); + } +} + +@Directive({ + selector: 'ion-input:not([type=number])', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: InputValueAccessorDirective, + multi: true, + }, + ], +}) +export class InputValueAccessorDirective extends ValueAccessor { + constructor(injector: Injector, el: ElementRef) { + super(injector, el); + } + + @HostListener('ionInput', ['$event.target']) + _handleInputEvent(el: any): void { + this.handleValueChange(el, el.value); } } diff --git a/angular/src/directives/control-value-accessors/value-accessor.ts b/angular/src/directives/control-value-accessors/value-accessor.ts index 615d736828..65ac7a4e7e 100644 --- a/angular/src/directives/control-value-accessors/value-accessor.ts +++ b/angular/src/directives/control-value-accessors/value-accessor.ts @@ -29,7 +29,20 @@ export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDes setIonicClasses(this.el); } - handleChangeEvent(el: HTMLElement, value: any): void { + /** + * Notifies the ControlValueAccessor of a change in the value of the control. + * + * This is called by each of the ValueAccessor directives when we want to update + * the status and validity of the form control. For example with text components this + * is called when the ionInput event is fired. For select components this is called + * when the ionChange event is fired. + * + * This also updates the Ionic form status classes on the element. + * + * @param el The component element. + * @param value The new value of the control. + */ + handleValueChange(el: HTMLElement, value: any): void { if (el === this.el.nativeElement) { if (value !== this.lastValue) { this.lastValue = value; diff --git a/angular/src/index.ts b/angular/src/index.ts index 38ba6c1c72..833fe68411 100644 --- a/angular/src/index.ts +++ b/angular/src/index.ts @@ -3,7 +3,10 @@ export { BooleanValueAccessorDirective as BooleanValueAccessor } from './directi export { NumericValueAccessorDirective as NumericValueAccessor } from './directives/control-value-accessors/numeric-value-accessor'; export { RadioValueAccessorDirective as RadioValueAccessor } from './directives/control-value-accessors/radio-value-accessor'; export { SelectValueAccessorDirective as SelectValueAccessor } from './directives/control-value-accessors/select-value-accessor'; -export { TextValueAccessorDirective as TextValueAccessor } from './directives/control-value-accessors/text-value-accessor'; +export { + TextValueAccessorDirective as TextValueAccessor, + InputValueAccessorDirective as InputValueAccessor, +} from './directives/control-value-accessors/text-value-accessor'; export { IonTabs } from './directives/navigation/ion-tabs'; export { IonBackButtonDelegateDirective as IonBackButtonDelegate } from './directives/navigation/ion-back-button'; export { NavDelegate } from './directives/navigation/nav-delegate'; diff --git a/angular/src/ionic-module.ts b/angular/src/ionic-module.ts index 34afe3f08e..f292b5ca1f 100644 --- a/angular/src/ionic-module.ts +++ b/angular/src/ionic-module.ts @@ -3,11 +3,14 @@ import { ModuleWithProviders, APP_INITIALIZER, NgModule, NgZone } from '@angular import { IonicConfig } from '@ionic/core'; import { appInitialize } from './app-initialize'; -import { BooleanValueAccessorDirective } from './directives/control-value-accessors/boolean-value-accessor'; -import { NumericValueAccessorDirective } from './directives/control-value-accessors/numeric-value-accessor'; -import { RadioValueAccessorDirective } from './directives/control-value-accessors/radio-value-accessor'; -import { SelectValueAccessorDirective } from './directives/control-value-accessors/select-value-accessor'; -import { TextValueAccessorDirective } from './directives/control-value-accessors/text-value-accessor'; +import { + BooleanValueAccessorDirective, + NumericValueAccessorDirective, + RadioValueAccessorDirective, + SelectValueAccessorDirective, + TextValueAccessorDirective, + InputValueAccessorDirective, +} from './directives/control-value-accessors'; import { IonBackButtonDelegateDirective } from './directives/navigation/ion-back-button'; import { IonRouterOutlet } from './directives/navigation/ion-router-outlet'; import { IonTabs } from './directives/navigation/ion-tabs'; @@ -38,6 +41,7 @@ const DECLARATIONS = [ RadioValueAccessorDirective, SelectValueAccessorDirective, TextValueAccessorDirective, + InputValueAccessorDirective, // navigation IonTabs, diff --git a/angular/test/base/e2e/src/form.spec.ts b/angular/test/base/e2e/src/form.spec.ts index 165ae68b10..37cad16281 100644 --- a/angular/test/base/e2e/src/form.spec.ts +++ b/angular/test/base/e2e/src/form.spec.ts @@ -36,7 +36,9 @@ describe('Form', () => { }); it('should become valid', () => { - cy.get('ion-input.required').invoke('prop', 'value', 'Some value'); + cy.get('ion-input.required').type('Some value'); + cy.get('ion-input.required input').blur(); + testStatus('INVALID'); // TODO: FW-1160 - Remove when v7 is released diff --git a/angular/test/base/e2e/src/inputs.spec.ts b/angular/test/base/e2e/src/inputs.spec.ts index 9cc4f4450e..c04296c567 100644 --- a/angular/test/base/e2e/src/inputs.spec.ts +++ b/angular/test/base/e2e/src/inputs.spec.ts @@ -40,7 +40,10 @@ describe('Inputs', () => { cy.get('ion-checkbox').invoke('prop', 'checked', true); cy.get('ion-toggle').invoke('prop', 'checked', true); - cy.get('ion-input').invoke('prop', 'value', 'hola'); + + cy.get('ion-input').eq(0).type('hola'); + cy.get('ion-input input').eq(0).blur(); + cy.get('ion-datetime').invoke('prop', 'value', '1996-03-15'); cy.get('ion-select').invoke('prop', 'value', 'playstation'); cy.get('ion-range').invoke('prop', 'value', 20); diff --git a/core/src/components.d.ts b/core/src/components.d.ts index ace6c43037..56bf4cb934 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -5,7 +5,7 @@ * It contains typing information for all components that exist in this project. */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; -import { AccordionGroupChangeEventDetail, ActionSheetAttributes, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, BreadcrumbCollapsedClickEventDetail, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DatetimePresentation, FrameworkDelegate, InputChangeEventDetail, ItemReorderEventDetail, LoadingAttributes, MenuChangeEventDetail, ModalAttributes, ModalBreakpointChangeEventDetail, ModalHandleBehavior, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerAttributes, PickerButton, PickerColumn, PopoverAttributes, PopoverSize, PositionAlign, PositionReference, PositionSide, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, TriggerAction, ViewController } from "./interface"; +import { AccordionGroupChangeEventDetail, ActionSheetAttributes, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, BreadcrumbCollapsedClickEventDetail, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DatetimePresentation, FrameworkDelegate, InputChangeEventDetail, InputInputEventDetail, ItemReorderEventDetail, LoadingAttributes, MenuChangeEventDetail, ModalAttributes, ModalBreakpointChangeEventDetail, ModalHandleBehavior, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerAttributes, PickerButton, PickerColumn, PopoverAttributes, PopoverSize, PositionAlign, PositionReference, PositionSide, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, TriggerAction, ViewController } from "./interface"; import { IonicSafeString } from "./utils/sanitization"; import { AlertAttributes } from "./components/alert/alert-interface"; import { CounterFormatter } from "./components/item/item-interface"; @@ -4898,7 +4898,7 @@ declare namespace LocalJSX { */ "onIonBlur"?: (event: IonInputCustomEvent) => void; /** - * Emitted when the value has changed. + * The `ionChange` event is fired for `` 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. Depending on the way the users interacts with the element, the `ionChange` event fires at a different moment: - When the user commits the change explicitly (e.g. by selecting a date from a date picker for ``, etc.). - When the element loses focus after its value has changed: for elements where the user's interaction is typing. */ "onIonChange"?: (event: IonInputCustomEvent) => void; /** @@ -4906,9 +4906,9 @@ declare namespace LocalJSX { */ "onIonFocus"?: (event: IonInputCustomEvent) => void; /** - * Emitted when a keyboard input occurred. + * The `ionInput` event fires when the `value` of an `` element has been changed. For elements that accept text input (`type=text`, `type=tel`, etc.), the interface is [`InputEvent`](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent); for others, the interface is [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event). */ - "onIonInput"?: (event: IonInputCustomEvent) => void; + "onIonInput"?: (event: IonInputCustomEvent) => void; /** * Emitted when the styles change. */ diff --git a/core/src/components/input/input-interface.ts b/core/src/components/input/input-interface.ts index ce33636eb8..1e2f865e4b 100644 --- a/core/src/components/input/input-interface.ts +++ b/core/src/components/input/input-interface.ts @@ -2,6 +2,10 @@ export interface InputChangeEventDetail { value: string | undefined | null; } +// We recognize that InputInput is not an ideal naming pattern for this type. +// TODO (FW-2199): Explore renaming this type to something more appropriate. +export type InputInputEventDetail = InputEvent | Event; + export interface InputCustomEvent extends CustomEvent { detail: InputChangeEventDetail; target: HTMLIonInputElement; diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx index 40d4dfdae7..488c100d68 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -6,6 +6,7 @@ import type { AutocompleteTypes, Color, InputChangeEventDetail, + InputInputEventDetail, StyleEventDetail, TextFieldTypes, } from '../../interface'; @@ -30,6 +31,17 @@ export class Input implements ComponentInterface { private didBlurAfterEdit = false; private inheritedAttributes: Attributes = {}; private isComposing = false; + /** + * If `true`, the user cleared the input by pressing the clear icon, + * within the session of the input being focused. + * + * This property is reset to `false` when the input is blurred. + */ + private inputCleared = false; + /** + * The value of the input when the input is focused. + */ + private focusedValue: string | number | null | undefined; @State() hasFocus = false; @@ -192,12 +204,27 @@ export class Input implements ComponentInterface { @Prop({ mutable: true }) value?: string | number | null = ''; /** - * Emitted when a keyboard input occurred. + * The `ionInput` event fires when the `value` of an `` element + * has been changed. + * + * For elements that accept text input (`type=text`, `type=tel`, etc.), the interface + * is [`InputEvent`](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent); for others, + * the interface is [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event). */ - @Event() ionInput!: EventEmitter; + @Event() ionInput!: EventEmitter; /** - * Emitted when the value has changed. + * The `ionChange` event is fired for `` 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. + * + * Depending on the way the users interacts with the element, the `ionChange` + * event fires at a different moment: + * - When the user commits the change explicitly (e.g. by selecting a date + * from a date picker for ``, etc.). + * - When the element loses focus after its value has changed: for elements + * where the user's interaction is typing. + * */ @Event() ionChange!: EventEmitter; @@ -244,7 +271,6 @@ export class Input implements ComponentInterface { nativeInput.value = value; } this.emitStyle(); - this.ionChange.emit({ value: this.value == null ? this.value : this.value.toString() }); } componentWillLoad() { @@ -310,6 +336,18 @@ export class Input implements ComponentInterface { return Promise.resolve(this.nativeInput!); } + /** + * 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; + const newValue = value == null ? value : value.toString(); + this.ionChange.emit({ value: newValue }); + } + private shouldClearOnEdit() { const { type, clearOnEdit } = this; return clearOnEdit === undefined ? type === 'password' : clearOnEdit; @@ -338,16 +376,33 @@ export class Input implements ComponentInterface { this.ionInput.emit(ev as InputEvent); }; + private onChange = () => { + this.emitValueChange(); + }; + private onBlur = (ev: FocusEvent) => { this.hasFocus = false; this.focusChanged(); this.emitStyle(); + if (this.inputCleared) { + if (this.focusedValue !== this.value) { + /** + * Emits the `ionChange` event when the input value + * is different than the value when the input was focused. + */ + this.emitValueChange(); + } + this.focusedValue = undefined; + this.inputCleared = false; + } + this.ionBlur.emit(ev); }; private onFocus = (ev: FocusEvent) => { this.hasFocus = true; + this.focusedValue = this.value; this.focusChanged(); this.emitStyle(); @@ -386,6 +441,9 @@ export class Input implements ComponentInterface { } this.value = ''; + this.inputCleared = true; + + this.ionInput.emit(ev); /** * This is needed for clearOnEdit @@ -454,6 +512,7 @@ export class Input implements ComponentInterface { type={this.type} value={value} onInput={this.onInput} + onChange={this.onChange} onBlur={this.onBlur} onFocus={this.onFocus} onKeyDown={this.onKeydown} diff --git a/core/src/components/input/test/input-events.e2e.ts b/core/src/components/input/test/input-events.e2e.ts new file mode 100644 index 0000000000..528e46610f --- /dev/null +++ b/core/src/components/input/test/input-events.e2e.ts @@ -0,0 +1,74 @@ +import { expect } from '@playwright/test'; +import { test } from '@utils/test/playwright'; + +test.describe('input: events: ionChange', () => { + test.describe('when the input is blurred', () => { + test.describe('should emit', () => { + test('if the value has changed', async ({ page }) => { + await page.setContent(``); + + const nativeInput = page.locator('ion-input input'); + const ionChangeSpy = await page.spyOnEvent('ionChange'); + + await nativeInput.type('new value', { delay: 100 }); + // Value change is not emitted until the control is blurred. + await nativeInput.evaluate((e) => e.blur()); + + await ionChangeSpy.next(); + + expect(ionChangeSpy).toHaveReceivedEventDetail({ value: 'new value' }); + }); + }); + + test.describe('should not emit', () => { + test('if the value has not changed', async ({ page }) => { + await page.setContent(``); + + const ionChangeSpy = await page.spyOnEvent('ionChange'); + const nativeInput = page.locator('ion-input input'); + + await nativeInput.type('new value', { delay: 100 }); + + await page.click('ion-input .input-clear-icon'); + + await nativeInput.evaluate((e) => e.blur()); + + expect(ionChangeSpy.events.length).toBe(0); + }); + + test('if the value is set programmatically', async ({ page }) => { + await page.setContent(``); + + const input = page.locator('ion-input'); + const ionChangeSpy = await page.spyOnEvent('ionChange'); + + await input.evaluate((el: HTMLIonInputElement) => { + el.value = 'new value'; + }); + + expect(ionChangeSpy.events.length).toBe(0); + + // Update the value again to make sure it doesn't emit a second time + await input.evaluate((el: HTMLIonInputElement) => { + el.value = 'new value 2'; + }); + + expect(ionChangeSpy.events.length).toBe(0); + }); + }); + }); +}); + +test.describe('input: events: ionInput', () => { + test.describe('should emit', () => { + test('when the input is cleared', async ({ page }) => { + await page.setContent(``); + + const ionInputSpy = await page.spyOnEvent('ionInput'); + + await page.click('ion-input .input-clear-icon'); + + expect(ionInputSpy).toHaveReceivedEventDetail({ isTrusted: true }); + }); + }); +}); diff --git a/core/src/components/item/item.tsx b/core/src/components/item/item.tsx index ee70493042..197b766f34 100644 --- a/core/src/components/item/item.tsx +++ b/core/src/components/item/item.tsx @@ -8,7 +8,7 @@ import type { AnchorInterface, ButtonInterface } from '../../utils/element-inter import { raf } from '../../utils/helpers'; import { printIonError } from '../../utils/logging'; import { createColorClasses, hostContext, openURL } from '../../utils/theme'; -import type { InputChangeEventDetail } from '../input/input-interface'; +import type { InputInputEventDetail } from '../input/input-interface'; import type { CounterFormatter } from './item-interface'; @@ -149,8 +149,8 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac this.updateCounterOutput(this.getFirstInput()); } - @Listen('ionChange') - handleIonChange(ev: CustomEvent) { + @Listen('ionInput') + handleIonInput(ev: CustomEvent) { if (this.counter && ev.target === this.getFirstInput()) { this.updateCounterOutput(ev.target as HTMLIonInputElement | HTMLIonTextareaElement); } diff --git a/core/src/components/item/test/counter/item.e2e.ts b/core/src/components/item/test/counter/item.e2e.ts index 7600d0fe2c..06135a659c 100644 --- a/core/src/components/item/test/counter/item.e2e.ts +++ b/core/src/components/item/test/counter/item.e2e.ts @@ -24,7 +24,7 @@ test.describe('item: counter', () => { test('should format on input', async ({ page }) => { const input = page.locator('#customFormatter ion-input'); - await input.click(); + await page.click('#customFormatter ion-input input'); await input.type('abcde'); await page.waitForChanges();