mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
feat(searchbar): ionChange will only emit from user committed changes (#26026)
This commit is contained in:
12
BREAKING.md
12
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`|
|
||||
|
||||
<h4 id="version-7x-searchbar">Searchbar</h4>
|
||||
|
||||
- `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.
|
||||
|
||||
<h4 id="version-7x-segment">Segment</h4>
|
||||
|
||||
- `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"`.
|
||||
|
||||
|
||||
<h4 id="version-7x-virtual-scroll">Virtual Scroll</h4>
|
||||
|
||||
`ion-virtual-scroll` has been removed from Ionic.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<CustomEvent<KeyboardEvent>>;
|
||||
ionInput: EventEmitter<CustomEvent<KeyboardEvent | null>>;
|
||||
/**
|
||||
* Emitted when the value has changed.
|
||||
* The `ionChange` event is fired for `<ion-searchbar>` 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<CustomEvent<ISearchbarSearchbarChangeEventDetail>>;
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
18
angular/test/base/e2e/src/searchbar.spec.ts
Normal file
18
angular/test/base/e2e/src/searchbar.spec.ts
Normal file
@@ -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"`);
|
||||
});
|
||||
});
|
||||
@@ -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) },
|
||||
|
||||
@@ -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 { }
|
||||
16
angular/test/base/src/app/searchbar/searchbar.component.html
Normal file
16
angular/test/base/src/app/searchbar/searchbar.component.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<ion-content>
|
||||
<form [formGroup]="form">
|
||||
<ion-list>
|
||||
<ion-item>
|
||||
<ion-label>Searchbar</ion-label>
|
||||
<ion-searchbar formControlName="searchbar"></ion-searchbar>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</form>
|
||||
<p>
|
||||
Form status: <span id="status">{{ form.status }}</span>
|
||||
</p>
|
||||
<p>
|
||||
Form value: <span id="value">{{ form.value | json }}</span>
|
||||
</p>
|
||||
</ion-content>
|
||||
16
angular/test/base/src/app/searchbar/searchbar.component.ts
Normal file
16
angular/test/base/src/app/searchbar/searchbar.component.ts
Normal file
@@ -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) { }
|
||||
|
||||
}
|
||||
21
angular/test/base/src/app/searchbar/searchbar.module.ts
Normal file
21
angular/test/base/src/app/searchbar/searchbar.module.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { NgModule } from "@angular/core";
|
||||
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
|
||||
import { IonicModule } from "@ionic/angular";
|
||||
|
||||
import { SearchbarRoutingModule } from "./searchbar-routing.module";
|
||||
import { SearchbarComponent } from "./searchbar.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
IonicModule,
|
||||
SearchbarRoutingModule
|
||||
],
|
||||
declarations: [
|
||||
SearchbarComponent
|
||||
]
|
||||
})
|
||||
export class SearchbarModule { }
|
||||
@@ -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<never, never> | 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
|
||||
|
||||
12
core/src/components.d.ts
vendored
12
core/src/components.d.ts
vendored
@@ -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>) => void;
|
||||
/**
|
||||
* Emitted when the value has changed.
|
||||
* The `ionChange` event is fired for `<ion-searchbar>` 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<SearchbarChangeEventDetail>) => void;
|
||||
/**
|
||||
@@ -6148,9 +6148,9 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"onIonFocus"?: (event: IonSearchbarCustomEvent<void>) => void;
|
||||
/**
|
||||
* Emitted when a keyboard input occurred.
|
||||
* Emitted when the `value` of the `ion-searchbar` element has changed.
|
||||
*/
|
||||
"onIonInput"?: (event: IonSearchbarCustomEvent<KeyboardEvent>) => void;
|
||||
"onIonInput"?: (event: IonSearchbarCustomEvent<KeyboardEvent | null>) => void;
|
||||
/**
|
||||
* Emitted when the styles change.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface SearchbarChangeEventDetail {
|
||||
value?: string;
|
||||
value?: string | null;
|
||||
}
|
||||
|
||||
export interface SearchbarCustomEvent extends CustomEvent {
|
||||
|
||||
@@ -24,6 +24,12 @@ export class Searchbar implements ComponentInterface {
|
||||
private nativeInput?: HTMLInputElement;
|
||||
private isCancelVisible = false;
|
||||
private shouldAlignLeft = true;
|
||||
private originalIonInput!: EventEmitter<KeyboardEvent | null>;
|
||||
|
||||
/**
|
||||
* 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<KeyboardEvent>;
|
||||
@Event() ionInput!: EventEmitter<KeyboardEvent | null>;
|
||||
|
||||
/**
|
||||
* Emitted when the value has changed.
|
||||
* The `ionChange` event is fired for `<ion-searchbar>` 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<SearchbarChangeEventDetail>;
|
||||
|
||||
@@ -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<void>((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}
|
||||
|
||||
83
core/src/components/searchbar/test/events/searchbar.e2e.ts
Normal file
83
core/src/components/searchbar/test/events/searchbar.e2e.ts
Normal file
@@ -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(`<ion-searchbar></ion-searchbar>`);
|
||||
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(`<ion-searchbar show-clear-button="always" value="default value"></ion-searchbar>`);
|
||||
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(`<ion-searchbar show-cancel-button="always" value="default value"></ion-searchbar>`);
|
||||
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(`<ion-searchbar></ion-searchbar>`);
|
||||
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(`<ion-searchbar debounce="0"></ion-searchbar>`);
|
||||
const nativeInput = page.locator('ion-searchbar input');
|
||||
const ionInput = await page.spyOnEvent('ionInput');
|
||||
|
||||
await nativeInput.type('new value', { delay: 100 });
|
||||
expect(ionInput).toHaveReceivedEventTimes(9);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user