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`|
+
Searchbar
+
+- `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"`.
-
`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 @@
+
+
+
+ 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);
+ });
+});