feat(searchbar): ionInput now emits value payload (#26831)

resolves #26828

BREAKING CHANGE:

The `detail` payload for the `ionInput` event now on `ion-searchbar` contains an object with the current `value` as well as the native event that triggered `ionInput`.
This commit is contained in:
Liam DeBeasi
2023-02-23 12:15:43 -05:00
committed by GitHub
parent fcfdd9e9ba
commit 865f8de9dc
11 changed files with 55 additions and 34 deletions

View File

@ -123,7 +123,7 @@ This section details the desktop browser, JavaScript framework, and mobile platf
<h4 id="version-7x-input">Input</h4> <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. - `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, clicking the clear action within the input, or pressing the "Enter" key.
- If your application requires immediate feedback based on the user typing actively in the input, consider migrating your event listeners to using `ionInput` instead. - If your application requires immediate feedback based on the user typing actively in the input, consider migrating your event listeners to using `ionInput` instead.
@ -196,7 +196,7 @@ Ionic now listens on the `keydown` event instead of the `keyup` event when deter
<h4 id="version-7x-searchbar">Searchbar</h4> <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. - `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 or pressing the "Enter" key.
- If your application requires immediate feedback based on the user typing actively in the searchbar, consider migrating your event listeners to using `ionInput` instead. - If your application requires immediate feedback based on the user typing actively in the searchbar, consider migrating your event listeners to using `ionInput` instead.
@ -204,6 +204,8 @@ Ionic now listens on the `keydown` event instead of the `keyup` event when deter
- The `debounce` property's default value has changed from 250 to `undefined`. If `debounce` is undefined, the `ionInput` event will fire immediately. - The `debounce` property's default value has changed from 250 to `undefined`. If `debounce` is undefined, the `ionInput` event will fire immediately.
- The `detail` payload for the `ionInput` event now contains an object with the current `value` as well as the native event that triggered `ionInput`.
**Design tokens** **Design tokens**
| Token | Previous Value | New Value | | Token | Previous Value | New Value |

View File

@ -1019,7 +1019,7 @@ 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` Depending on the way the users interacts with the element, the `ionChange`
event fires at a different moment: event fires at a different moment:
- When the user commits the change explicitly (e.g. by selecting a date - When the user commits the change explicitly (e.g. by selecting a date
from a date picker for `<ion-input type="date">`, etc.). from a date picker for `<ion-input type="date">`, pressing the "Enter" key, etc.).
- When the element loses focus after its value has changed: for elements - When the element loses focus after its value has changed: for elements
where the user's interaction is typing. where the user's interaction is typing.
*/ */
@ -1862,21 +1862,23 @@ export class IonSearchbar {
} }
import type { SearchbarInputEventDetail as IIonSearchbarSearchbarInputEventDetail } from '@ionic/core';
import type { SearchbarChangeEventDetail as IIonSearchbarSearchbarChangeEventDetail } from '@ionic/core'; import type { SearchbarChangeEventDetail as IIonSearchbarSearchbarChangeEventDetail } from '@ionic/core';
export declare interface IonSearchbar extends Components.IonSearchbar { export declare interface IonSearchbar extends Components.IonSearchbar {
/** /**
* Emitted when the `value` of the `ion-searchbar` element has changed. * Emitted when the `value` of the `ion-searchbar` element has changed.
*/ */
ionInput: EventEmitter<CustomEvent<KeyboardEvent | null>>; ionInput: EventEmitter<CustomEvent<IIonSearchbarSearchbarInputEventDetail>>;
/** /**
* The `ionChange` event is fired for `<ion-searchbar>` elements when the user * The `ionChange` event is fired for `<ion-searchbar>` elements when the user
modifies the element's value. Unlike the `ionInput` event, the `ionChange` modifies the element's value. Unlike the `ionInput` event, the `ionChange`
event is not necessarily fired for each alteration to an element's value. 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 The `ionChange` event is fired when the value has been committed
has been modified. This includes modifications made when clicking the clear by the user. This can happen when the element loses focus or
or cancel buttons. when the "Enter" key is pressed. `ionChange` can also fire
when clicking the clear or cancel buttons.
*/ */
ionChange: EventEmitter<CustomEvent<IIonSearchbarSearchbarChangeEventDetail>>; ionChange: EventEmitter<CustomEvent<IIonSearchbarSearchbarChangeEventDetail>>;
/** /**

View File

@ -114,6 +114,7 @@ export {
ScrollCustomEvent, ScrollCustomEvent,
SearchbarCustomEvent, SearchbarCustomEvent,
SearchbarChangeEventDetail, SearchbarChangeEventDetail,
SearchbarInputEventDetail,
SegmentChangeEventDetail, SegmentChangeEventDetail,
SegmentCustomEvent, SegmentCustomEvent,
SelectChangeEventDetail, SelectChangeEventDetail,

View File

@ -1162,7 +1162,7 @@ ion-searchbar,event,ionCancel,void,true
ion-searchbar,event,ionChange,SearchbarChangeEventDetail,true ion-searchbar,event,ionChange,SearchbarChangeEventDetail,true
ion-searchbar,event,ionClear,void,true ion-searchbar,event,ionClear,void,true
ion-searchbar,event,ionFocus,void,true ion-searchbar,event,ionFocus,void,true
ion-searchbar,event,ionInput,KeyboardEvent | null,true ion-searchbar,event,ionInput,SearchbarInputEventDetail,true
ion-searchbar,css-prop,--background ion-searchbar,css-prop,--background
ion-searchbar,css-prop,--border-radius ion-searchbar,css-prop,--border-radius
ion-searchbar,css-prop,--box-shadow ion-searchbar,css-prop,--box-shadow

View File

@ -32,7 +32,7 @@ import { PinFormatter, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, Rang
import { RefresherEventDetail } from "./components/refresher/refresher-interface"; import { RefresherEventDetail } from "./components/refresher/refresher-interface";
import { ItemReorderEventDetail } from "./components/reorder-group/reorder-group-interface"; import { ItemReorderEventDetail } from "./components/reorder-group/reorder-group-interface";
import { NavigationHookCallback } from "./components/route/route-interface"; import { NavigationHookCallback } from "./components/route/route-interface";
import { SearchbarChangeEventDetail } from "./components/searchbar/searchbar-interface"; import { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
import { SegmentChangeEventDetail } from "./components/segment/segment-interface"; import { SegmentChangeEventDetail } from "./components/segment/segment-interface";
import { SegmentButtonLayout } from "./components/segment-button/segment-button-interface"; import { SegmentButtonLayout } from "./components/segment-button/segment-button-interface";
import { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./components/select/select-interface"; import { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./components/select/select-interface";
@ -68,7 +68,7 @@ export { PinFormatter, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, Rang
export { RefresherEventDetail } from "./components/refresher/refresher-interface"; export { RefresherEventDetail } from "./components/refresher/refresher-interface";
export { ItemReorderEventDetail } from "./components/reorder-group/reorder-group-interface"; export { ItemReorderEventDetail } from "./components/reorder-group/reorder-group-interface";
export { NavigationHookCallback } from "./components/route/route-interface"; export { NavigationHookCallback } from "./components/route/route-interface";
export { SearchbarChangeEventDetail } from "./components/searchbar/searchbar-interface"; export { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
export { SegmentChangeEventDetail } from "./components/segment/segment-interface"; export { SegmentChangeEventDetail } from "./components/segment/segment-interface";
export { SegmentButtonLayout } from "./components/segment-button/segment-button-interface"; export { SegmentButtonLayout } from "./components/segment-button/segment-button-interface";
export { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./components/select/select-interface"; export { SelectChangeEventDetail, SelectCompareFn, SelectInterface } from "./components/select/select-interface";
@ -5276,7 +5276,7 @@ declare namespace LocalJSX {
*/ */
"onIonBlur"?: (event: IonInputCustomEvent<FocusEvent>) => void; "onIonBlur"?: (event: IonInputCustomEvent<FocusEvent>) => void;
/** /**
* 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. * 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">`, pressing the "Enter" key, 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;
/** /**
@ -6602,7 +6602,7 @@ declare namespace LocalJSX {
*/ */
"onIonCancel"?: (event: IonSearchbarCustomEvent<void>) => void; "onIonCancel"?: (event: IonSearchbarCustomEvent<void>) => void;
/** /**
* 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. * 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 value has been committed by the user. This can happen when the element loses focus or when the "Enter" key is pressed. `ionChange` can also fire when clicking the clear or cancel buttons.
*/ */
"onIonChange"?: (event: IonSearchbarCustomEvent<SearchbarChangeEventDetail>) => void; "onIonChange"?: (event: IonSearchbarCustomEvent<SearchbarChangeEventDetail>) => void;
/** /**
@ -6616,7 +6616,7 @@ declare namespace LocalJSX {
/** /**
* Emitted when the `value` of the `ion-searchbar` element has changed. * Emitted when the `value` of the `ion-searchbar` element has changed.
*/ */
"onIonInput"?: (event: IonSearchbarCustomEvent<KeyboardEvent | null>) => void; "onIonInput"?: (event: IonSearchbarCustomEvent<SearchbarInputEventDetail>) => void;
/** /**
* Emitted when the styles change. * Emitted when the styles change.
*/ */

View File

@ -291,10 +291,9 @@ export class Input implements ComponentInterface {
* Depending on the way the users interacts with the element, the `ionChange` * Depending on the way the users interacts with the element, the `ionChange`
* event fires at a different moment: * event fires at a different moment:
* - When the user commits the change explicitly (e.g. by selecting a date * - When the user commits the change explicitly (e.g. by selecting a date
* from a date picker for `<ion-input type="date">`, etc.). * from a date picker for `<ion-input type="date">`, pressing the "Enter" key, etc.).
* - When the element loses focus after its value has changed: for elements * - When the element loses focus after its value has changed: for elements
* where the user's interaction is typing. * where the user's interaction is typing.
*
*/ */
@Event() ionChange!: EventEmitter<InputChangeEventDetail>; @Event() ionChange!: EventEmitter<InputChangeEventDetail>;

View File

@ -1,5 +1,11 @@
export interface SearchbarChangeEventDetail { export interface SearchbarChangeEventDetail {
value?: string | null; value?: string | null;
event?: Event;
}
export interface SearchbarInputEventDetail {
value?: string | null;
event?: Event;
} }
export interface SearchbarCustomEvent extends CustomEvent { export interface SearchbarCustomEvent extends CustomEvent {

View File

@ -9,7 +9,7 @@ import { debounceEvent, raf } from '../../utils/helpers';
import { isRTL } from '../../utils/rtl'; import { isRTL } from '../../utils/rtl';
import { createColorClasses } from '../../utils/theme'; import { createColorClasses } from '../../utils/theme';
import type { SearchbarChangeEventDetail } from './searchbar-interface'; import type { SearchbarChangeEventDetail, SearchbarInputEventDetail } from './searchbar-interface';
/** /**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use. * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
@ -26,7 +26,7 @@ export class Searchbar implements ComponentInterface {
private nativeInput?: HTMLInputElement; private nativeInput?: HTMLInputElement;
private isCancelVisible = false; private isCancelVisible = false;
private shouldAlignLeft = true; private shouldAlignLeft = true;
private originalIonInput?: EventEmitter<KeyboardEvent | null>; private originalIonInput?: EventEmitter<SearchbarInputEventDetail>;
/** /**
* The value of the input when the textarea is focused. * The value of the input when the textarea is focused.
@ -165,16 +165,17 @@ export class Searchbar implements ComponentInterface {
/** /**
* Emitted when the `value` of the `ion-searchbar` element has changed. * Emitted when the `value` of the `ion-searchbar` element has changed.
*/ */
@Event() ionInput!: EventEmitter<KeyboardEvent | null>; @Event() ionInput!: EventEmitter<SearchbarInputEventDetail>;
/** /**
* The `ionChange` event is fired for `<ion-searchbar>` elements when the user * The `ionChange` event is fired for `<ion-searchbar>` elements when the user
* modifies the element's value. Unlike the `ionInput` event, the `ionChange` * modifies the element's value. Unlike the `ionInput` event, the `ionChange`
* event is not necessarily fired for each alteration to an element's value. * 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 * The `ionChange` event is fired when the value has been committed
* has been modified. This includes modifications made when clicking the clear * by the user. This can happen when the element loses focus or
* or cancel buttons. * when the "Enter" key is pressed. `ionChange` can also fire
* when clicking the clear or cancel buttons.
*/ */
@Event() ionChange!: EventEmitter<SearchbarChangeEventDetail>; @Event() ionChange!: EventEmitter<SearchbarChangeEventDetail>;
@ -266,13 +267,21 @@ export class Searchbar implements ComponentInterface {
* This API should be called for user committed changes. * This API should be called for user committed changes.
* This API should not be used for external value changes. * This API should not be used for external value changes.
*/ */
private emitValueChange() { private emitValueChange(event?: Event) {
const { value } = this; const { value } = this;
// Checks for both null and undefined values // Checks for both null and undefined values
const newValue = value == null ? value : value.toString(); const newValue = value == null ? value : value.toString();
// Emitting a value change should update the internal state for tracking the focused value // Emitting a value change should update the internal state for tracking the focused value
this.focusedValue = newValue; this.focusedValue = newValue;
this.ionChange.emit({ value: newValue }); this.ionChange.emit({ value: newValue, event });
}
/**
* Emits an `ionInput` event.
*/
private emitInputChange(event?: Event) {
const { value } = this;
this.ionInput.emit({ value, event });
} }
/** /**
@ -288,7 +297,7 @@ export class Searchbar implements ComponentInterface {
const value = this.getValue(); const value = this.getValue();
if (value !== '') { if (value !== '') {
this.value = ''; this.value = '';
this.ionInput.emit(null); this.emitInputChange();
/** /**
* When tapping clear button * When tapping clear button
@ -338,7 +347,7 @@ export class Searchbar implements ComponentInterface {
* manually fire ionChange. * manually fire ionChange.
*/ */
if (value && !focused) { if (value && !focused) {
this.emitValueChange(); this.emitValueChange(ev);
} }
if (this.nativeInput) { if (this.nativeInput) {
@ -349,29 +358,29 @@ export class Searchbar implements ComponentInterface {
/** /**
* Update the Searchbar input value when the input changes * Update the Searchbar input value when the input changes
*/ */
private onInput = (ev: Event) => { private onInput = (ev: InputEvent | Event) => {
const input = ev.target as HTMLInputElement | null; const input = ev.target as HTMLInputElement | null;
if (input) { if (input) {
this.value = input.value; this.value = input.value;
} }
this.ionInput.emit(ev as KeyboardEvent); this.emitInputChange(ev);
}; };
private onChange = () => { private onChange = (ev: Event) => {
this.emitValueChange(); this.emitValueChange(ev);
}; };
/** /**
* Sets the Searchbar to not focused and checks if it should align left * 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. * based on whether there is a value in the searchbar or not.
*/ */
private onBlur = () => { private onBlur = (ev: FocusEvent) => {
this.focused = false; this.focused = false;
this.ionBlur.emit(); this.ionBlur.emit();
this.positionElements(); this.positionElements();
if (this.focusedValue !== this.value) { if (this.focusedValue !== this.value) {
this.emitValueChange(); this.emitValueChange(ev);
} }
this.focusedValue = undefined; this.focusedValue = undefined;
}; };

View File

@ -15,7 +15,7 @@ test.describe('searchbar: events (ionChange)', () => {
await nativeInput.evaluate((e) => e.blur()); await nativeInput.evaluate((e) => e.blur());
await ionChange.next(); await ionChange.next();
expect(ionChange).toHaveReceivedEventDetail({ value: 'new value' }); expect(ionChange).toHaveReceivedEventDetail({ value: 'new value', event: { isTrusted: true } });
expect(ionChange).toHaveReceivedEventTimes(1); expect(ionChange).toHaveReceivedEventTimes(1);
}); });
@ -29,7 +29,7 @@ test.describe('searchbar: events (ionChange)', () => {
await nativeInput.evaluate((e) => e.blur()); await nativeInput.evaluate((e) => e.blur());
await ionChange.next(); await ionChange.next();
expect(ionChange).toHaveReceivedEventDetail({ value: '' }); expect(ionChange).toHaveReceivedEventDetail({ value: '', event: { isTrusted: true } });
expect(ionChange).toHaveReceivedEventTimes(1); expect(ionChange).toHaveReceivedEventTimes(1);
}); });
@ -41,7 +41,7 @@ test.describe('searchbar: events (ionChange)', () => {
await page.waitForChanges(); await page.waitForChanges();
await ionChange.next(); await ionChange.next();
expect(ionChange).toHaveReceivedEventDetail({ value: '' }); expect(ionChange).toHaveReceivedEventDetail({ value: '', event: { isTrusted: true } });
expect(ionChange).toHaveReceivedEventTimes(1); expect(ionChange).toHaveReceivedEventTimes(1);
}); });

View File

@ -69,6 +69,7 @@ export {
ScrollCustomEvent, ScrollCustomEvent,
SearchbarCustomEvent, SearchbarCustomEvent,
SearchbarChangeEventDetail, SearchbarChangeEventDetail,
SearchbarInputEventDetail,
SegmentChangeEventDetail, SegmentChangeEventDetail,
SegmentCustomEvent, SegmentCustomEvent,
SelectChangeEventDetail, SelectChangeEventDetail,

View File

@ -109,6 +109,7 @@ export {
ScrollCustomEvent, ScrollCustomEvent,
SearchbarCustomEvent, SearchbarCustomEvent,
SearchbarChangeEventDetail, SearchbarChangeEventDetail,
SearchbarInputEventDetail,
SegmentChangeEventDetail, SegmentChangeEventDetail,
SegmentCustomEvent, SegmentCustomEvent,
SelectChangeEventDetail, SelectChangeEventDetail,