feat(input): ionChange will only emit from user committed changes (#25858)

Resolves #20106, #20061
This commit is contained in:
Sean Perkins
2022-09-12 15:35:43 -04:00
committed by GitHub
parent ba6b539675
commit 8732b7bdb7
18 changed files with 225 additions and 30 deletions

View File

@ -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) - [Browser and Platform Support](#version-7x-browser-platform-support)
- [Components](#version-7x-components) - [Components](#version-7x-components)
- [Input](#version-7x-input)
- [Overlays](#version-7x-overlays) - [Overlays](#version-7x-overlays)
- [Range](#version-7x-range) - [Range](#version-7x-range)
- [Slides](#version-7x-slides) - [Slides](#version-7x-slides)
@ -50,6 +51,12 @@ This section details the desktop browser, JavaScript framework, and mobile platf
<h2 id="version-7x-components">Components</h2> <h2 id="version-7x-components">Components</h2>
<h4 id="version-7x-input">Input</h4>
`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.
<h4 id="version-7x-overlays">Overlays</h4> <h4 id="version-7x-overlays">Overlays</h4>
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. 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.

View File

@ -25,6 +25,6 @@ export class BooleanValueAccessorDirective extends ValueAccessor {
@HostListener('ionChange', ['$event.target']) @HostListener('ionChange', ['$event.target'])
_handleIonChange(el: any): void { _handleIonChange(el: any): void {
this.handleChangeEvent(el, el.checked); this.handleValueChange(el, el.checked);
} }
} }

View File

@ -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';

View File

@ -18,13 +18,13 @@ export class NumericValueAccessorDirective extends ValueAccessor {
super(injector, el); super(injector, el);
} }
@HostListener('ionChange', ['$event.target']) @HostListener('ionInput', ['$event.target'])
_handleIonChange(el: any): void { handleInputEvent(el: HTMLIonInputElement): void {
this.handleChangeEvent(el, el.value); this.handleValueChange(el, el.value);
} }
registerOnChange(fn: (_: number | null) => void): void { registerOnChange(fn: (_: number | null) => void): void {
super.registerOnChange((value) => { super.registerOnChange((value: string) => {
fn(value === '' ? null : parseFloat(value)); fn(value === '' ? null : parseFloat(value));
}); });
} }

View File

@ -21,6 +21,6 @@ export class RadioValueAccessorDirective extends ValueAccessor {
@HostListener('ionSelect', ['$event.target']) @HostListener('ionSelect', ['$event.target'])
_handleIonSelect(el: any): void { _handleIonSelect(el: any): void {
this.handleChangeEvent(el, el.checked); this.handleValueChange(el, el.checked);
} }
} }

View File

@ -21,6 +21,6 @@ export class SelectValueAccessorDirective extends ValueAccessor {
@HostListener('ionChange', ['$event.target']) @HostListener('ionChange', ['$event.target'])
_handleChangeEvent(el: any): void { _handleChangeEvent(el: any): void {
this.handleChangeEvent(el, el.value); this.handleValueChange(el, el.value);
} }
} }

View File

@ -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-input:not([type=number]),ion-textarea,ion-searchbar', selector: 'ion-textarea,ion-searchbar',
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
@ -21,6 +21,27 @@ export class TextValueAccessorDirective extends ValueAccessor {
@HostListener('ionChange', ['$event.target']) @HostListener('ionChange', ['$event.target'])
_handleInputEvent(el: any): void { _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);
} }
} }

View File

@ -29,7 +29,20 @@ export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDes
setIonicClasses(this.el); 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 (el === this.el.nativeElement) {
if (value !== this.lastValue) { if (value !== this.lastValue) {
this.lastValue = value; this.lastValue = value;

View File

@ -3,7 +3,10 @@ export { BooleanValueAccessorDirective as BooleanValueAccessor } from './directi
export { NumericValueAccessorDirective as NumericValueAccessor } from './directives/control-value-accessors/numeric-value-accessor'; 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 { RadioValueAccessorDirective as RadioValueAccessor } from './directives/control-value-accessors/radio-value-accessor';
export { SelectValueAccessorDirective as SelectValueAccessor } from './directives/control-value-accessors/select-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 { IonTabs } from './directives/navigation/ion-tabs';
export { IonBackButtonDelegateDirective as IonBackButtonDelegate } from './directives/navigation/ion-back-button'; export { IonBackButtonDelegateDirective as IonBackButtonDelegate } from './directives/navigation/ion-back-button';
export { NavDelegate } from './directives/navigation/nav-delegate'; export { NavDelegate } from './directives/navigation/nav-delegate';

View File

@ -3,11 +3,14 @@ import { ModuleWithProviders, APP_INITIALIZER, NgModule, NgZone } from '@angular
import { IonicConfig } from '@ionic/core'; import { IonicConfig } from '@ionic/core';
import { appInitialize } from './app-initialize'; import { appInitialize } from './app-initialize';
import { BooleanValueAccessorDirective } from './directives/control-value-accessors/boolean-value-accessor'; import {
import { NumericValueAccessorDirective } from './directives/control-value-accessors/numeric-value-accessor'; BooleanValueAccessorDirective,
import { RadioValueAccessorDirective } from './directives/control-value-accessors/radio-value-accessor'; NumericValueAccessorDirective,
import { SelectValueAccessorDirective } from './directives/control-value-accessors/select-value-accessor'; RadioValueAccessorDirective,
import { TextValueAccessorDirective } from './directives/control-value-accessors/text-value-accessor'; SelectValueAccessorDirective,
TextValueAccessorDirective,
InputValueAccessorDirective,
} from './directives/control-value-accessors';
import { IonBackButtonDelegateDirective } from './directives/navigation/ion-back-button'; import { IonBackButtonDelegateDirective } from './directives/navigation/ion-back-button';
import { IonRouterOutlet } from './directives/navigation/ion-router-outlet'; import { IonRouterOutlet } from './directives/navigation/ion-router-outlet';
import { IonTabs } from './directives/navigation/ion-tabs'; import { IonTabs } from './directives/navigation/ion-tabs';
@ -38,6 +41,7 @@ const DECLARATIONS = [
RadioValueAccessorDirective, RadioValueAccessorDirective,
SelectValueAccessorDirective, SelectValueAccessorDirective,
TextValueAccessorDirective, TextValueAccessorDirective,
InputValueAccessorDirective,
// navigation // navigation
IonTabs, IonTabs,

View File

@ -36,7 +36,9 @@ describe('Form', () => {
}); });
it('should become valid', () => { 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'); testStatus('INVALID');
// TODO: FW-1160 - Remove when v7 is released // TODO: FW-1160 - Remove when v7 is released

View File

@ -40,7 +40,10 @@ describe('Inputs', () => {
cy.get('ion-checkbox').invoke('prop', 'checked', true); cy.get('ion-checkbox').invoke('prop', 'checked', true);
cy.get('ion-toggle').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-datetime').invoke('prop', 'value', '1996-03-15');
cy.get('ion-select').invoke('prop', 'value', 'playstation'); cy.get('ion-select').invoke('prop', 'value', 'playstation');
cy.get('ion-range').invoke('prop', 'value', 20); cy.get('ion-range').invoke('prop', 'value', 20);

View File

@ -5,7 +5,7 @@
* It contains typing information for all components that exist in this project. * It contains typing information for all components that exist in this project.
*/ */
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; 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 { IonicSafeString } from "./utils/sanitization";
import { AlertAttributes } from "./components/alert/alert-interface"; import { AlertAttributes } from "./components/alert/alert-interface";
import { CounterFormatter } from "./components/item/item-interface"; import { CounterFormatter } from "./components/item/item-interface";
@ -4898,7 +4898,7 @@ declare namespace LocalJSX {
*/ */
"onIonBlur"?: (event: IonInputCustomEvent<FocusEvent>) => void; "onIonBlur"?: (event: IonInputCustomEvent<FocusEvent>) => void;
/** /**
* Emitted when the value has changed. * The `ionChange` event is fired for `<ion-input>` 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 `<ion-input type="date">`, etc.). - When the element loses focus after its value has changed: for elements where the user's interaction is typing.
*/ */
"onIonChange"?: (event: IonInputCustomEvent<InputChangeEventDetail>) => void; "onIonChange"?: (event: IonInputCustomEvent<InputChangeEventDetail>) => void;
/** /**
@ -4906,9 +4906,9 @@ declare namespace LocalJSX {
*/ */
"onIonFocus"?: (event: IonInputCustomEvent<FocusEvent>) => void; "onIonFocus"?: (event: IonInputCustomEvent<FocusEvent>) => void;
/** /**
* Emitted when a keyboard input occurred. * The `ionInput` event fires when the `value` of an `<ion-input>` 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<InputEvent>) => void; "onIonInput"?: (event: IonInputCustomEvent<InputInputEventDetail>) => void;
/** /**
* Emitted when the styles change. * Emitted when the styles change.
*/ */

View File

@ -2,6 +2,10 @@ export interface InputChangeEventDetail {
value: string | undefined | null; 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 { export interface InputCustomEvent extends CustomEvent {
detail: InputChangeEventDetail; detail: InputChangeEventDetail;
target: HTMLIonInputElement; target: HTMLIonInputElement;

View File

@ -6,6 +6,7 @@ import type {
AutocompleteTypes, AutocompleteTypes,
Color, Color,
InputChangeEventDetail, InputChangeEventDetail,
InputInputEventDetail,
StyleEventDetail, StyleEventDetail,
TextFieldTypes, TextFieldTypes,
} from '../../interface'; } from '../../interface';
@ -30,6 +31,17 @@ export class Input implements ComponentInterface {
private didBlurAfterEdit = false; private didBlurAfterEdit = false;
private inheritedAttributes: Attributes = {}; private inheritedAttributes: Attributes = {};
private isComposing = false; 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; @State() hasFocus = false;
@ -192,12 +204,27 @@ export class Input implements ComponentInterface {
@Prop({ mutable: true }) value?: string | number | null = ''; @Prop({ mutable: true }) value?: string | number | null = '';
/** /**
* Emitted when a keyboard input occurred. * The `ionInput` event fires when the `value` of an `<ion-input>` 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<InputEvent>; @Event() ionInput!: EventEmitter<InputInputEventDetail>;
/** /**
* Emitted when the value has changed. * The `ionChange` event is fired for `<ion-input>` 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 `<ion-input type="date">`, etc.).
* - When the element loses focus after its value has changed: for elements
* where the user's interaction is typing.
*
*/ */
@Event() ionChange!: EventEmitter<InputChangeEventDetail>; @Event() ionChange!: EventEmitter<InputChangeEventDetail>;
@ -244,7 +271,6 @@ export class Input implements ComponentInterface {
nativeInput.value = value; nativeInput.value = value;
} }
this.emitStyle(); this.emitStyle();
this.ionChange.emit({ value: this.value == null ? this.value : this.value.toString() });
} }
componentWillLoad() { componentWillLoad() {
@ -310,6 +336,18 @@ export class Input implements ComponentInterface {
return Promise.resolve(this.nativeInput!); 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() { private shouldClearOnEdit() {
const { type, clearOnEdit } = this; const { type, clearOnEdit } = this;
return clearOnEdit === undefined ? type === 'password' : clearOnEdit; return clearOnEdit === undefined ? type === 'password' : clearOnEdit;
@ -338,16 +376,33 @@ export class Input implements ComponentInterface {
this.ionInput.emit(ev as InputEvent); this.ionInput.emit(ev as InputEvent);
}; };
private onChange = () => {
this.emitValueChange();
};
private onBlur = (ev: FocusEvent) => { private onBlur = (ev: FocusEvent) => {
this.hasFocus = false; this.hasFocus = false;
this.focusChanged(); this.focusChanged();
this.emitStyle(); 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); this.ionBlur.emit(ev);
}; };
private onFocus = (ev: FocusEvent) => { private onFocus = (ev: FocusEvent) => {
this.hasFocus = true; this.hasFocus = true;
this.focusedValue = this.value;
this.focusChanged(); this.focusChanged();
this.emitStyle(); this.emitStyle();
@ -386,6 +441,9 @@ export class Input implements ComponentInterface {
} }
this.value = ''; this.value = '';
this.inputCleared = true;
this.ionInput.emit(ev);
/** /**
* This is needed for clearOnEdit * This is needed for clearOnEdit
@ -454,6 +512,7 @@ export class Input implements ComponentInterface {
type={this.type} type={this.type}
value={value} value={value}
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}

View File

@ -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(`<ion-input></ion-input>`);
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(`<ion-input value="" clear-input="true"></ion-input>`);
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(`<ion-input></ion-input>`);
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(`<ion-input value="some value" clear-input="true"></ion-input>`);
const ionInputSpy = await page.spyOnEvent('ionInput');
await page.click('ion-input .input-clear-icon');
expect(ionInputSpy).toHaveReceivedEventDetail({ isTrusted: true });
});
});
});

View File

@ -8,7 +8,7 @@ import type { AnchorInterface, ButtonInterface } from '../../utils/element-inter
import { raf } from '../../utils/helpers'; import { raf } from '../../utils/helpers';
import { printIonError } from '../../utils/logging'; import { printIonError } from '../../utils/logging';
import { createColorClasses, hostContext, openURL } from '../../utils/theme'; 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'; import type { CounterFormatter } from './item-interface';
@ -149,8 +149,8 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
this.updateCounterOutput(this.getFirstInput()); this.updateCounterOutput(this.getFirstInput());
} }
@Listen('ionChange') @Listen('ionInput')
handleIonChange(ev: CustomEvent<InputChangeEventDetail>) { handleIonInput(ev: CustomEvent<InputInputEventDetail>) {
if (this.counter && ev.target === this.getFirstInput()) { if (this.counter && ev.target === this.getFirstInput()) {
this.updateCounterOutput(ev.target as HTMLIonInputElement | HTMLIonTextareaElement); this.updateCounterOutput(ev.target as HTMLIonInputElement | HTMLIonTextareaElement);
} }

View File

@ -24,7 +24,7 @@ test.describe('item: counter', () => {
test('should format on input', async ({ page }) => { test('should format on input', async ({ page }) => {
const input = page.locator('#customFormatter ion-input'); const input = page.locator('#customFormatter ion-input');
await input.click(); await page.click('#customFormatter ion-input input');
await input.type('abcde'); await input.type('abcde');
await page.waitForChanges(); await page.waitForChanges();