From b052d3b2622b795cde102591f0191338e15b14a0 Mon Sep 17 00:00:00 2001 From: Amanda Johnston <90629384+amandaejohnston@users.noreply.github.com> Date: Mon, 3 Oct 2022 15:08:43 -0500 Subject: [PATCH] feat(searchbar): ionChange will only emit from user committed changes (#26026) --- BREAKING.md | 12 +- .../text-value-accessor.ts | 25 +--- angular/src/directives/proxies.ts | 12 +- angular/src/index.ts | 5 +- angular/src/ionic-module.ts | 2 - angular/test/base/e2e/src/searchbar.spec.ts | 18 +++ .../test/base/src/app/app-routing.module.ts | 1 + .../app/searchbar/searchbar-routing.module.ts | 16 +++ .../app/searchbar/searchbar.component.html | 16 +++ .../src/app/searchbar/searchbar.component.ts | 16 +++ .../src/app/searchbar/searchbar.module.ts | 21 +++ core/api.txt | 4 +- core/src/components.d.ts | 12 +- .../test/search/radio-group.e2e.ts | 3 + .../searchbar/searchbar-interface.ts | 2 +- core/src/components/searchbar/searchbar.tsx | 124 ++++++++++++++---- .../searchbar/test/events/searchbar.e2e.ts | 83 ++++++++++++ 17 files changed, 302 insertions(+), 70 deletions(-) create mode 100644 angular/test/base/e2e/src/searchbar.spec.ts create mode 100644 angular/test/base/src/app/searchbar/searchbar-routing.module.ts create mode 100644 angular/test/base/src/app/searchbar/searchbar.component.html create mode 100644 angular/test/base/src/app/searchbar/searchbar.component.ts create mode 100644 angular/test/base/src/app/searchbar/searchbar.module.ts create mode 100644 core/src/components/searchbar/test/events/searchbar.e2e.ts diff --git a/BREAKING.md b/BREAKING.md index 1b6d9a574c..841001c4af 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -20,6 +20,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver - [Modal](#version-7x-modal) - [Overlays](#version-7x-overlays) - [Range](#version-7x-range) + - [Searchbar](#version-7x-searchbar) - [Segment](#version-7x-segment) - [Slides](#version-7x-slides) - [Textarea](#version-7x-textarea) @@ -102,6 +103,16 @@ iOS: |`$range-ios-knob-box-shadow`|`0 3px 1px rgba(0, 0, 0, .1), 0 4px 8px rgba(0, 0, 0, .13), 0 0 0 1px rgba(0, 0, 0, .02)`|`0px 0.5px 4px rgba(0, 0, 0, 0.12), 0px 6px 13px rgba(0, 0, 0, 0.12)`| |`$range-ios-knob-width`|`28px`|`26px`| + + +- `ionChange` is no longer emitted when the `value` of `ion-searchbar` is modified externally. `ionChange` is only emitted from user committed changes, such as typing in the searchbar and the searchbar losing focus. + + - If your application requires immediate feedback based on the user typing actively in the searchbar, 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`. + +- The `debounce` property's default value has changed from 250 to `undefined`. If `debounce` is undefined, the `ionInput` event will fire immediately. +

Segment

- `ionChange` is no longer emitted when the `value` of `ion-segment` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking a segment button or dragging to activate a segment button. @@ -129,7 +140,6 @@ Developers using these components will need to migrate to using Swiper.js direct - `ionInput` dispatches an event detail of `null` when the textarea is cleared as a result of `clear-on-edit="true"`. -

Virtual Scroll

`ion-virtual-scroll` has been removed from Ionic. 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 772bc65eb7..de915aa4b9 100644 --- a/angular/src/directives/control-value-accessors/text-value-accessor.ts +++ b/angular/src/directives/control-value-accessors/text-value-accessor.ts @@ -4,8 +4,7 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { ValueAccessor } from './value-accessor'; @Directive({ - /* tslint:disable-next-line:directive-selector */ - selector: 'ion-searchbar', + selector: 'ion-input:not([type=number]),ion-textarea,ion-searchbar', providers: [ { provide: NG_VALUE_ACCESSOR, @@ -19,28 +18,6 @@ export class TextValueAccessorDirective extends ValueAccessor { super(injector, el); } - @HostListener('ionChange', ['$event.target']) - _handleInputEvent(el: any): void { - this.handleValueChange(el, el.value); - } -} - -@Directive({ - selector: 'ion-input:not([type=number]),ion-textarea', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: InputValueAccessorDirective, - multi: true, - }, - ], -}) -// TODO rename this value accessor to `TextValueAccessorDirective` when search-bar is updated -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/proxies.ts b/angular/src/directives/proxies.ts index e76bf3a868..9f483acba4 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -1518,11 +1518,17 @@ export class IonRow { import type { SearchbarChangeEventDetail as ISearchbarSearchbarChangeEventDetail } from '@ionic/core'; export declare interface IonSearchbar extends Components.IonSearchbar { /** - * Emitted when a keyboard input occurred. + * Emitted when the `value` of the `ion-searchbar` element has changed. */ - ionInput: EventEmitter>; + 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. + +The `ionChange` event is fired when the element loses focus after its value +has been modified. This includes modifications made when clicking the clear +or cancel buttons. */ ionChange: EventEmitter>; /** diff --git a/angular/src/index.ts b/angular/src/index.ts index 833fe68411..38ba6c1c72 100644 --- a/angular/src/index.ts +++ b/angular/src/index.ts @@ -3,10 +3,7 @@ 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, - InputValueAccessorDirective as InputValueAccessor, -} from './directives/control-value-accessors/text-value-accessor'; +export { TextValueAccessorDirective as TextValueAccessor } 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 f292b5ca1f..2db4f26d47 100644 --- a/angular/src/ionic-module.ts +++ b/angular/src/ionic-module.ts @@ -9,7 +9,6 @@ import { 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'; @@ -41,7 +40,6 @@ const DECLARATIONS = [ RadioValueAccessorDirective, SelectValueAccessorDirective, TextValueAccessorDirective, - InputValueAccessorDirective, // navigation IonTabs, diff --git a/angular/test/base/e2e/src/searchbar.spec.ts b/angular/test/base/e2e/src/searchbar.spec.ts new file mode 100644 index 0000000000..e35f4088dd --- /dev/null +++ b/angular/test/base/e2e/src/searchbar.spec.ts @@ -0,0 +1,18 @@ +describe('Searchbar', () => { + beforeEach(() => cy.visit('/searchbar')); + + it('should become valid', () => { + cy.get('#status').should('have.text', 'INVALID'); + + cy.get('ion-searchbar').type('hello'); + + cy.get('#status').should('have.text', 'VALID'); + }); + + it('should update the form control value when typing', () => { + cy.get('#value').contains(`"searchbar": ""`); + cy.get('ion-searchbar').type('hello'); + + cy.get('#value').contains(`"searchbar": "hello"`); + }); +}); \ No newline at end of file diff --git a/angular/test/base/src/app/app-routing.module.ts b/angular/test/base/src/app/app-routing.module.ts index 279d6b3cd7..d2e421c7a5 100644 --- a/angular/test/base/src/app/app-routing.module.ts +++ b/angular/test/base/src/app/app-routing.module.ts @@ -26,6 +26,7 @@ const routes: Routes = [ { path: 'alerts', component: AlertComponent }, { path: 'inputs', component: InputsComponent }, { path: 'textarea', loadChildren: () => import('./textarea/textarea.module').then(m => m.TextareaModule) }, + { path: 'searchbar', loadChildren: () => import('./searchbar/searchbar.module').then(m => m.SearchbarModule) }, { path: 'form', component: FormComponent }, { path: 'modals', component: ModalComponent }, { path: 'modal-inline', loadChildren: () => import('./modal-inline').then(m => m.ModalInlineModule) }, diff --git a/angular/test/base/src/app/searchbar/searchbar-routing.module.ts b/angular/test/base/src/app/searchbar/searchbar-routing.module.ts new file mode 100644 index 0000000000..678a4833fa --- /dev/null +++ b/angular/test/base/src/app/searchbar/searchbar-routing.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from "@angular/core"; +import { RouterModule } from "@angular/router"; +import { SearchbarComponent } from "./searchbar.component"; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: '', + component: SearchbarComponent + } + ]) + ], + exports: [RouterModule] +}) +export class SearchbarRoutingModule { } \ No newline at end of file diff --git a/angular/test/base/src/app/searchbar/searchbar.component.html b/angular/test/base/src/app/searchbar/searchbar.component.html new file mode 100644 index 0000000000..52ce6106d1 --- /dev/null +++ b/angular/test/base/src/app/searchbar/searchbar.component.html @@ -0,0 +1,16 @@ + +
+ + + Searchbar + + + +
+

+ Form status: {{ form.status }} +

+

+ Form value: {{ form.value | json }} +

+
\ No newline at end of file diff --git a/angular/test/base/src/app/searchbar/searchbar.component.ts b/angular/test/base/src/app/searchbar/searchbar.component.ts new file mode 100644 index 0000000000..1715cc9487 --- /dev/null +++ b/angular/test/base/src/app/searchbar/searchbar.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core'; +import { FormBuilder, Validators } from '@angular/forms'; + +@Component({ + selector: 'app-searchbar', + templateUrl: 'searchbar.component.html', +}) +export class SearchbarComponent { + + form = this.fb.group({ + searchbar: ['', Validators.required] + }) + + constructor(private fb: FormBuilder) { } + +} \ No newline at end of file diff --git a/angular/test/base/src/app/searchbar/searchbar.module.ts b/angular/test/base/src/app/searchbar/searchbar.module.ts new file mode 100644 index 0000000000..8c2a618cff --- /dev/null +++ b/angular/test/base/src/app/searchbar/searchbar.module.ts @@ -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 { SearchbarRoutingModule } from "./searchbar-routing.module"; +import { SearchbarComponent } from "./searchbar.component"; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + IonicModule, + SearchbarRoutingModule + ], + declarations: [ + SearchbarComponent + ] +}) +export class SearchbarModule { } \ No newline at end of file diff --git a/core/api.txt b/core/api.txt index 176c1027bb..3b09ac0002 100644 --- a/core/api.txt +++ b/core/api.txt @@ -1094,7 +1094,7 @@ ion-searchbar,prop,cancelButtonIcon,string,config.get('backButtonIcon', arrowBac ion-searchbar,prop,cancelButtonText,string,'Cancel',false,false ion-searchbar,prop,clearIcon,string | undefined,undefined,false,false ion-searchbar,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record | undefined,undefined,false,true -ion-searchbar,prop,debounce,number,250,false,false +ion-searchbar,prop,debounce,number | undefined,undefined,false,false ion-searchbar,prop,disabled,boolean,false,false,false ion-searchbar,prop,enterkeyhint,"done" | "enter" | "go" | "next" | "previous" | "search" | "send" | undefined,undefined,false,false ion-searchbar,prop,inputmode,"decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined,undefined,false,false @@ -1113,7 +1113,7 @@ ion-searchbar,event,ionCancel,void,true ion-searchbar,event,ionChange,SearchbarChangeEventDetail,true ion-searchbar,event,ionClear,void,true ion-searchbar,event,ionFocus,void,true -ion-searchbar,event,ionInput,KeyboardEvent,true +ion-searchbar,event,ionInput,KeyboardEvent | null,true ion-searchbar,css-prop,--background ion-searchbar,css-prop,--border-radius ion-searchbar,css-prop,--box-shadow diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 2bc918823c..b74a2b2a82 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -2366,9 +2366,9 @@ export namespace Components { */ "color"?: Color; /** - * 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; /** * If `true`, the user cannot interact with the input. */ @@ -6108,7 +6108,7 @@ declare namespace LocalJSX { */ "color"?: Color; /** - * 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; /** @@ -6136,7 +6136,7 @@ declare namespace LocalJSX { */ "onIonCancel"?: (event: IonSearchbarCustomEvent) => 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. The `ionChange` event is fired when the element loses focus after its value has been modified. This includes modifications made when clicking the clear or cancel buttons. */ "onIonChange"?: (event: IonSearchbarCustomEvent) => void; /** @@ -6148,9 +6148,9 @@ declare namespace LocalJSX { */ "onIonFocus"?: (event: IonSearchbarCustomEvent) => void; /** - * Emitted when a keyboard input occurred. + * Emitted when the `value` of the `ion-searchbar` element has changed. */ - "onIonInput"?: (event: IonSearchbarCustomEvent) => void; + "onIonInput"?: (event: IonSearchbarCustomEvent) => void; /** * Emitted when the styles change. */ diff --git a/core/src/components/radio-group/test/search/radio-group.e2e.ts b/core/src/components/radio-group/test/search/radio-group.e2e.ts index 9acba673e3..e5c2be6c9f 100644 --- a/core/src/components/radio-group/test/search/radio-group.e2e.ts +++ b/core/src/components/radio-group/test/search/radio-group.e2e.ts @@ -11,6 +11,7 @@ test.describe('radio-group', () => { test('radio should remain checked after being removed/readded to the dom', async ({ page }) => { const radioGroup = page.locator('ion-radio-group'); const radio = page.locator('ion-radio[value=two]'); + const searchbarInput = page.locator('ion-searchbar input'); // select radio await radio.click(); @@ -18,6 +19,7 @@ test.describe('radio-group', () => { // filter radio so it is not in DOM await page.fill('ion-searchbar input', 'zero'); + await searchbarInput.evaluate((el) => el.blur()); await page.waitForChanges(); expect(radio).toBeHidden(); @@ -26,6 +28,7 @@ test.describe('radio-group', () => { // clear the search so the radio appears await page.fill('ion-searchbar input', ''); + await searchbarInput.evaluate((el) => el.blur()); await page.waitForChanges(); // ensure that the new radio instance is still checked diff --git a/core/src/components/searchbar/searchbar-interface.ts b/core/src/components/searchbar/searchbar-interface.ts index d965e9209d..45697096ae 100644 --- a/core/src/components/searchbar/searchbar-interface.ts +++ b/core/src/components/searchbar/searchbar-interface.ts @@ -1,5 +1,5 @@ export interface SearchbarChangeEventDetail { - value?: string; + value?: string | null; } export interface SearchbarCustomEvent extends CustomEvent { diff --git a/core/src/components/searchbar/searchbar.tsx b/core/src/components/searchbar/searchbar.tsx index 8b352c7f18..f7f1f49788 100644 --- a/core/src/components/searchbar/searchbar.tsx +++ b/core/src/components/searchbar/searchbar.tsx @@ -24,6 +24,12 @@ export class Searchbar implements ComponentInterface { private nativeInput?: HTMLInputElement; private isCancelVisible = false; private shouldAlignLeft = true; + private originalIonInput!: EventEmitter; + + /** + * The value of the input when the textarea is focused. + */ + private focusedValue?: string | null; @Element() el!: HTMLIonSearchbarElement; @@ -69,13 +75,19 @@ export class Searchbar implements ComponentInterface { @Prop() clearIcon?: string; /** - * 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 = 250; + @Prop() debounce?: number; @Watch('debounce') protected debounceChanged() { - this.ionChange = debounceEvent(this.ionChange, this.debounce); + const { ionInput, debounce, originalIonInput } = this; + + /** + * If debounce is undefined, we have to manually revert the ionInput emitter in case + * debounce used to be set to a number. Otherwise, the event would stay debounced. + */ + this.ionInput = debounce === undefined ? originalIonInput : debounceEvent(ionInput, debounce); } /** @@ -149,12 +161,18 @@ export class Searchbar implements ComponentInterface { @Prop({ mutable: true }) value?: string | null = ''; /** - * Emitted when a keyboard input occurred. + * Emitted when the `value` of the `ion-searchbar` element has changed. */ - @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. + * + * The `ionChange` event is fired when the element loses focus after its value + * has been modified. This includes modifications made when clicking the clear + * or cancel buttons. */ @Event() ionChange!: EventEmitter; @@ -191,7 +209,6 @@ export class Searchbar implements ComponentInterface { if (inputEl && inputEl.value !== value) { inputEl.value = value; } - this.ionChange.emit({ value }); } @Watch('showCancelButton') @@ -207,6 +224,7 @@ export class Searchbar implements ComponentInterface { } componentDidLoad() { + this.originalIonInput = this.ionInput; this.positionElements(); this.debounceChanged(); @@ -240,31 +258,58 @@ export class Searchbar 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; + // 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 }); + } + /** * Clears the input field and triggers the control change. */ - private onClearInput = (shouldFocus?: boolean) => { + private onClearInput = async (shouldFocus?: boolean) => { this.ionClear.emit(); - // setTimeout() fixes https://github.com/ionic-team/ionic/issues/7527 - // wait for 4 frames - setTimeout(() => { - const value = this.getValue(); - if (value !== '') { - this.value = ''; - this.ionInput.emit(); + return new Promise((resolve) => { + // setTimeout() fixes https://github.com/ionic-team/ionic/issues/7527 + // wait for 4 frames + setTimeout(() => { + const value = this.getValue(); + if (value !== '') { + this.value = ''; + this.ionInput.emit(null); - /** - * When tapping clear button - * ensure input is focused after - * clearing input so users - * can quickly start typing. - */ - if (shouldFocus && !this.focused) { - this.setFocus(); + /** + * When tapping clear button + * ensure input is focused after + * clearing input so users + * can quickly start typing. + */ + if (shouldFocus && !this.focused) { + this.setFocus(); + + /** + * The setFocus call above will clear focusedValue, + * but ionChange will never have gotten a chance to + * fire. Manually revert focusedValue so onBlur can + * compare against what was in the box before the clear. + */ + this.focusedValue = value; + } } - } - }, 16 * 4); + + resolve(); + }, 16 * 4); + }); }; /** @@ -272,13 +317,27 @@ export class Searchbar implements ComponentInterface { * the clearInput function doesn't want the input to blur * then calls the custom cancel function if the user passed one in. */ - private onCancelSearchbar = (ev?: Event) => { + private onCancelSearchbar = async (ev?: Event) => { if (ev) { ev.preventDefault(); ev.stopPropagation(); } this.ionCancel.emit(); - this.onClearInput(); + + // get cached values before clearing the input + const value = this.getValue(); + const focused = this.focused; + + await this.onClearInput(); + + /** + * If there used to be something in the box, and we weren't focused + * beforehand (meaning no blur fired that would already handle this), + * manually fire ionChange. + */ + if (value && !focused) { + this.emitValueChange(); + } if (this.nativeInput) { this.nativeInput.blur(); @@ -296,6 +355,10 @@ export class Searchbar implements ComponentInterface { this.ionInput.emit(ev as KeyboardEvent); }; + private onChange = () => { + this.emitValueChange(); + }; + /** * Sets the Searchbar to not focused and checks if it should align left * based on whether there is a value in the searchbar or not. @@ -304,6 +367,11 @@ export class Searchbar implements ComponentInterface { this.focused = false; this.ionBlur.emit(); this.positionElements(); + + if (this.focusedValue !== this.value) { + this.emitValueChange(); + } + this.focusedValue = undefined; }; /** @@ -311,6 +379,7 @@ export class Searchbar implements ComponentInterface { */ private onFocus = () => { this.focused = true; + this.focusedValue = this.value; this.ionFocus.emit(); this.positionElements(); }; @@ -503,6 +572,7 @@ export class Searchbar implements ComponentInterface { inputMode={this.inputmode} enterKeyHint={this.enterkeyhint} onInput={this.onInput} + onChange={this.onChange} onBlur={this.onBlur} onFocus={this.onFocus} placeholder={this.placeholder} diff --git a/core/src/components/searchbar/test/events/searchbar.e2e.ts b/core/src/components/searchbar/test/events/searchbar.e2e.ts new file mode 100644 index 0000000000..123443f792 --- /dev/null +++ b/core/src/components/searchbar/test/events/searchbar.e2e.ts @@ -0,0 +1,83 @@ +import { expect } from '@playwright/test'; +import { test } from '@utils/test/playwright'; + +test.describe('searchbar: events (ionChange)', () => { + test.beforeEach(({ skip }) => { + skip.rtl(); + }); + + test('should emit when blurred after value change', async ({ page }) => { + await page.setContent(``); + const nativeInput = page.locator('ion-searchbar input'); + const ionChange = await page.spyOnEvent('ionChange'); + + await nativeInput.type('new value', { delay: 100 }); + await nativeInput.evaluate((e) => e.blur()); + + await ionChange.next(); + expect(ionChange).toHaveReceivedEventDetail({ value: 'new value' }); + expect(ionChange).toHaveReceivedEventTimes(1); + }); + + test('should emit when blurred after clicking clear with default value', async ({ page }) => { + await page.setContent(``); + const nativeInput = page.locator('ion-searchbar input'); + const ionChange = await page.spyOnEvent('ionChange'); + + await page.click('.searchbar-clear-button'); + await page.waitForChanges(); + await nativeInput.evaluate((e) => e.blur()); + + await ionChange.next(); + expect(ionChange).toHaveReceivedEventDetail({ value: '' }); + expect(ionChange).toHaveReceivedEventTimes(1); + }); + + test('should emit after clicking cancel with default value', async ({ page }) => { + await page.setContent(``); + const ionChange = await page.spyOnEvent('ionChange'); + + await page.click('.searchbar-cancel-button'); + await page.waitForChanges(); + + await ionChange.next(); + expect(ionChange).toHaveReceivedEventDetail({ value: '' }); + expect(ionChange).toHaveReceivedEventTimes(1); + }); + + test('should not emit if the value is set programmatically', async ({ page }) => { + await page.setContent(``); + const searchbar = page.locator('ion-searchbar'); + const ionChange = await page.spyOnEvent('ionChange'); + + await searchbar.evaluate((el: HTMLIonSearchbarElement) => { + el.value = 'new value'; + }); + + await page.waitForChanges(); + expect(ionChange).toHaveReceivedEventTimes(0); + + // Update the value again to make sure it doesn't emit a second time + await searchbar.evaluate((el: HTMLIonSearchbarElement) => { + el.value = 'new value 2'; + }); + + await page.waitForChanges(); + expect(ionChange).toHaveReceivedEventTimes(0); + }); +}); + +test.describe('searchbar: events (ionInput)', () => { + test.beforeEach(({ skip }) => { + skip.rtl(); + }); + + test('should emit when the user types', async ({ page }) => { + await page.setContent(``); + const nativeInput = page.locator('ion-searchbar input'); + const ionInput = await page.spyOnEvent('ionInput'); + + await nativeInput.type('new value', { delay: 100 }); + expect(ionInput).toHaveReceivedEventTimes(9); + }); +});