diff --git a/BREAKING.md b/BREAKING.md index a7ac8778e8..9eb81a1721 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -96,6 +96,8 @@ This section details the desktop browser, JavaScript framework, and mobile platf - 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 `detail` payload for the `ionInput` event now contains an object with the current `value` as well as the native event that triggered `ionInput`. +

Modal

- The `swipeToClose` property has been removed in favor of `canDismiss`. @@ -168,7 +170,7 @@ Developers using these components will need to migrate to using Swiper.js direct - 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`. -- `ionInput` dispatches an event detail of `null` when the textarea is cleared as a result of `clear-on-edit="true"`. +- The `detail` payload for the `ionInput` event now contains an object with the current `value` as well as the native event that triggered `ionInput`.

Toggle

diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts index c72ceeb3a2..19948ca2d9 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -1832,6 +1832,7 @@ export class IonText { } import type { TextareaChangeEventDetail as ITextareaTextareaChangeEventDetail } from '@ionic/core'; +import type { TextareaInputEventDetail as ITextareaTextareaInputEventDetail } from '@ionic/core'; export declare interface IonTextarea extends Components.IonTextarea { /** * The `ionChange` event is fired for `` elements when the user @@ -1849,7 +1850,7 @@ has been changed. When `clearOnEdit` is enabled, the `ionInput` event will be fired when the user clears the textarea by performing a keydown event. */ - ionInput: EventEmitter>; + ionInput: EventEmitter>; /** * Emitted when the input loses focus. */ diff --git a/core/api.txt b/core/api.txt index 17fd18ae3c..d40de6fa3c 100644 --- a/core/api.txt +++ b/core/api.txt @@ -552,7 +552,7 @@ ion-input,method,setFocus,setFocus() => Promise ion-input,event,ionBlur,FocusEvent,true ion-input,event,ionChange,InputChangeEventDetail,true ion-input,event,ionFocus,FocusEvent,true -ion-input,event,ionInput,Event | InputEvent,true +ion-input,event,ionInput,InputInputEventDetail,true ion-input,css-prop,--background ion-input,css-prop,--color ion-input,css-prop,--padding-bottom @@ -1315,7 +1315,7 @@ ion-textarea,method,setFocus,setFocus() => Promise ion-textarea,event,ionBlur,FocusEvent,true ion-textarea,event,ionChange,TextareaChangeEventDetail,true ion-textarea,event,ionFocus,FocusEvent,true -ion-textarea,event,ionInput,InputEvent,true +ion-textarea,event,ionInput,TextareaInputEventDetail,true ion-textarea,css-prop,--background ion-textarea,css-prop,--border-radius ion-textarea,css-prop,--color diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 27d3d7405e..12393dbc93 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -5,7 +5,7 @@ * It contains typing information for all components that exist in this project. */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; -import { AccordionGroupChangeEventDetail, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, BreadcrumbCollapsedClickEventDetail, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DatetimePresentation, FrameworkDelegate, InputChangeEventDetail, InputInputEventDetail, ItemReorderEventDetail, MenuChangeEventDetail, ModalBreakpointChangeEventDetail, ModalHandleBehavior, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerButton, PickerColumn, PopoverSize, PositionAlign, PositionReference, PositionSide, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, TitleSelectedDatesFormatter, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, TriggerAction, ViewController } from "./interface"; +import { AccordionGroupChangeEventDetail, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, BreadcrumbCollapsedClickEventDetail, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DatetimePresentation, FrameworkDelegate, InputChangeEventDetail, InputInputEventDetail, ItemReorderEventDetail, MenuChangeEventDetail, ModalBreakpointChangeEventDetail, ModalHandleBehavior, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerButton, PickerColumn, PopoverSize, PositionAlign, PositionReference, PositionSide, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextareaInputEventDetail, TextFieldTypes, TitleSelectedDatesFormatter, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, TriggerAction, ViewController } from "./interface"; import { IonicSafeString } from "./utils/sanitization"; import { CounterFormatter } from "./components/item/item-interface"; import { PickerColumnItem } from "./components/picker-column-internal/picker-column-internal-interfaces"; @@ -6624,7 +6624,7 @@ declare namespace LocalJSX { /** * The `ionInput` event fires when the `value` of an `` element has been changed. When `clearOnEdit` is enabled, the `ionInput` event will be fired when the user clears the textarea by performing a keydown event. */ - "onIonInput"?: (event: IonTextareaCustomEvent) => void; + "onIonInput"?: (event: IonTextareaCustomEvent) => void; /** * Emitted when the styles change. */ diff --git a/core/src/components/input/input-interface.ts b/core/src/components/input/input-interface.ts index 1e2f865e4b..6b87d102a4 100644 --- a/core/src/components/input/input-interface.ts +++ b/core/src/components/input/input-interface.ts @@ -1,12 +1,16 @@ export interface InputChangeEventDetail { - value: string | undefined | null; + value?: string | number | null; + event?: Event; } // We recognize that InputInput is not an ideal naming pattern for this type. // TODO (FW-2199): Explore renaming this type to something more appropriate. -export type InputInputEventDetail = InputEvent | Event; +export interface InputInputEventDetail { + value?: string | number | null; + event?: Event; +} -export interface InputCustomEvent extends CustomEvent { - detail: InputChangeEventDetail; +export interface InputCustomEvent extends CustomEvent { + detail: T; target: HTMLIonInputElement; } diff --git a/core/src/components/input/input.tsx b/core/src/components/input/input.tsx index 528871a9a5..045b955f6c 100644 --- a/core/src/components/input/input.tsx +++ b/core/src/components/input/input.tsx @@ -342,13 +342,21 @@ export class Input implements ComponentInterface { * This API should be called for user committed changes. * This API should not be used for external value changes. */ - private emitValueChange() { + private emitValueChange(event?: Event) { 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 }); + this.ionChange.emit({ value: newValue, event }); + } + + /** + * Emits an `ionInput` event. + */ + private emitInputChange(event?: Event) { + const { value } = this; + this.ionInput.emit({ value, event }); } private shouldClearOnEdit() { @@ -376,11 +384,11 @@ export class Input implements ComponentInterface { if (input) { this.value = input.value || ''; } - this.ionInput.emit(ev as InputEvent); + this.emitInputChange(ev); }; - private onChange = () => { - this.emitValueChange(); + private onChange = (ev: Event) => { + this.emitValueChange(ev); }; private onBlur = (ev: FocusEvent) => { @@ -392,7 +400,7 @@ export class Input implements ComponentInterface { * Emits the `ionChange` event when the input value * is different than the value when the input was focused. */ - this.emitValueChange(); + this.emitValueChange(ev); } this.didInputClearOnEdit = false; @@ -422,7 +430,7 @@ export class Input implements ComponentInterface { */ if (!this.didInputClearOnEdit && this.hasValue() && ev.key !== 'Enter') { this.value = ''; - this.ionInput.emit(); + this.emitInputChange(ev); } this.didInputClearOnEdit = true; } @@ -443,7 +451,7 @@ export class Input implements ComponentInterface { this.setFocus(); } this.value = ''; - this.ionInput.emit(ev); + this.emitInputChange(ev); }; private hasValue(): boolean { diff --git a/core/src/components/input/test/input-events.e2e.ts b/core/src/components/input/test/input-events.e2e.ts index b56cb8298f..30a37e7e4f 100644 --- a/core/src/components/input/test/input-events.e2e.ts +++ b/core/src/components/input/test/input-events.e2e.ts @@ -20,7 +20,7 @@ test.describe('input: events: ionChange', () => { await ionChangeSpy.next(); - expect(ionChangeSpy).toHaveReceivedEventDetail({ value: 'new value' }); + expect(ionChangeSpy).toHaveReceivedEventDetail({ value: 'new value', event: { isTrusted: true } }); }); }); @@ -71,7 +71,7 @@ test.describe('input: events: ionInput', () => { await page.click('ion-input .input-clear-icon'); - expect(ionInputSpy).toHaveReceivedEventDetail({ isTrusted: true }); + expect(ionInputSpy).toHaveReceivedEventDetail({ value: '', event: { isTrusted: true } }); }); test('should emit when the input is cleared from the keyboard', async ({ page }) => { @@ -86,6 +86,6 @@ test.describe('input: events: ionInput', () => { expect(await input.evaluate((el: HTMLIonInputElement) => el.value)).toBe(''); expect(ionInputSpy).toHaveReceivedEventTimes(1); - expect(ionInputSpy).toHaveReceivedEventDetail(null); + expect(ionInputSpy).toHaveReceivedEventDetail({ value: '', event: { isTrusted: true } }); }); }); diff --git a/core/src/components/textarea/test/textarea-events.e2e.ts b/core/src/components/textarea/test/textarea-events.e2e.ts index fefb472031..801e38c2b2 100644 --- a/core/src/components/textarea/test/textarea-events.e2e.ts +++ b/core/src/components/textarea/test/textarea-events.e2e.ts @@ -19,7 +19,7 @@ test.describe('textarea: events: ionChange', () => { await ionChangeSpy.next(); - expect(ionChangeSpy).toHaveReceivedEventDetail({ value: 'new value' }); + expect(ionChangeSpy).toHaveReceivedEventDetail({ value: 'new value', event: { isTrusted: true } }); expect(ionChangeSpy).toHaveReceivedEventTimes(1); }); @@ -36,7 +36,7 @@ test.describe('textarea: events: ionChange', () => { await ionChangeSpy.next(); - expect(ionChangeSpy).toHaveReceivedEventDetail({ value: 'new value' }); + expect(ionChangeSpy).toHaveReceivedEventDetail({ value: 'new value', event: { isTrusted: true } }); expect(ionChangeSpy).toHaveReceivedEventTimes(1); }); @@ -75,7 +75,7 @@ test.describe('textarea: events: ionInput', () => { const nativeTextarea = page.locator('ion-textarea textarea'); await nativeTextarea.type('new value', { delay: 100 }); - expect(ionInputSpy).toHaveReceivedEventDetail({ isTrusted: true }); + expect(ionInputSpy).toHaveReceivedEventDetail({ value: 'new valuesome value', event: { isTrusted: true } }); }); test('should emit when the textarea is cleared on edit', async ({ page }) => { @@ -88,6 +88,6 @@ test.describe('textarea: events: ionInput', () => { await textarea.press('Backspace'); expect(ionInputSpy).toHaveReceivedEventTimes(1); - expect(ionInputSpy).toHaveReceivedEventDetail(null); + expect(ionInputSpy).toHaveReceivedEventDetail({ value: '', event: { isTrusted: true } }); }); }); diff --git a/core/src/components/textarea/textarea-interface.ts b/core/src/components/textarea/textarea-interface.ts index a3f79c5aa1..2b97999db4 100644 --- a/core/src/components/textarea/textarea-interface.ts +++ b/core/src/components/textarea/textarea-interface.ts @@ -1,8 +1,14 @@ export interface TextareaChangeEventDetail { value?: string | null; + event?: Event; } -export interface TextareaCustomEvent extends CustomEvent { - detail: TextareaChangeEventDetail; +export interface TextareaInputEventDetail { + value?: string | null; + event?: Event; +} + +export interface TextareaCustomEvent extends CustomEvent { + detail: T; target: HTMLIonTextareaElement; } diff --git a/core/src/components/textarea/textarea.tsx b/core/src/components/textarea/textarea.tsx index bb6910db8a..c4350b8da8 100644 --- a/core/src/components/textarea/textarea.tsx +++ b/core/src/components/textarea/textarea.tsx @@ -2,7 +2,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, h, writeTask } from '@stencil/core'; import { getIonMode } from '../../global/ionic-global'; -import type { Color, StyleEventDetail, TextareaChangeEventDetail } from '../../interface'; +import type { Color, StyleEventDetail, TextareaChangeEventDetail, TextareaInputEventDetail } from '../../interface'; import type { Attributes } from '../../utils/helpers'; import { inheritAriaAttributes, debounceEvent, findItemLabel, inheritAttributes } from '../../utils/helpers'; import { createColorClasses } from '../../utils/theme'; @@ -188,7 +188,7 @@ export class Textarea implements ComponentInterface { * When `clearOnEdit` is enabled, the `ionInput` event will be fired when * the user clears the textarea by performing a keydown event. */ - @Event() ionInput!: EventEmitter; + @Event() ionInput!: EventEmitter; /** * Emitted when the styles change. @@ -276,13 +276,21 @@ export class Textarea implements ComponentInterface { * This API should be called for user committed changes. * This API should not be used for external value changes. */ - private emitValueChange() { + private emitValueChange(event?: Event) { 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 }); + this.ionChange.emit({ value: newValue, event }); + } + + /** + * Emits an `ionInput` event. + */ + private emitInputChange(event?: Event) { + const { value } = this; + this.ionInput.emit({ value, event }); } private runAutoGrow() { @@ -300,7 +308,7 @@ export class Textarea implements ComponentInterface { /** * Check if we need to clear the text input if clearOnEdit is enabled */ - private checkClearOnEdit() { + private checkClearOnEdit(ev: Event) { if (!this.clearOnEdit) { return; } @@ -310,7 +318,7 @@ export class Textarea implements ComponentInterface { */ if (!this.didTextareaClearOnEdit && this.hasValue()) { this.value = ''; - this.ionInput.emit(); + this.emitInputChange(ev); } this.didTextareaClearOnEdit = true; } @@ -337,11 +345,11 @@ export class Textarea implements ComponentInterface { if (input) { this.value = input.value || ''; } - this.ionInput.emit(ev as InputEvent); + this.emitInputChange(ev); }; - private onChange = () => { - this.emitValueChange(); + private onChange = (ev: Event) => { + this.emitValueChange(ev); }; private onFocus = (ev: FocusEvent) => { @@ -361,14 +369,14 @@ export class Textarea implements ComponentInterface { * Emits the `ionChange` event when the textarea value * is different than the value when the textarea was focused. */ - this.emitValueChange(); + this.emitValueChange(ev); } this.didTextareaClearOnEdit = false; this.ionBlur.emit(ev); }; - private onKeyDown = () => { - this.checkClearOnEdit(); + private onKeyDown = (ev: Event) => { + this.checkClearOnEdit(ev); }; render() {