diff --git a/BREAKING.md b/BREAKING.md index d833b25849..8cebc60411 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -16,6 +16,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver - [Components](#version-7x-components) - [Accordion Group](#version-7x-accordion-group) - [Checkbox](#version-7x-checkbox) + - [Datetime](#version-7x-datetime) - [Input](#version-7x-input) - [Modal](#version-7x-modal) - [Overlays](#version-7x-overlays) @@ -72,6 +73,12 @@ This section details the desktop browser, JavaScript framework, and mobile platf `ionChange` is no longer emitted when the `checked` property of `ion-checkbox` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking or tapping the checkbox. +

Datetime

+ +- `ionChange` is no longer emitted when the `value` property of `ion-datetime` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking or tapping a date. + +- Datetime no longer automatically adjusts the `value` property when passed an array and `multiple="false"`. Developers should update their apps to ensure they are using the API correctly. +

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 or from clicking the clear action within the input. diff --git a/angular/test/base/e2e/src/inputs.spec.ts b/angular/test/base/e2e/src/inputs.spec.ts index 76d0a0d325..f921c98d82 100644 --- a/angular/test/base/e2e/src/inputs.spec.ts +++ b/angular/test/base/e2e/src/inputs.spec.ts @@ -44,7 +44,8 @@ describe('Inputs', () => { cy.get('ion-input').eq(0).type('hola'); cy.get('ion-input input').eq(0).blur(); - cy.get('ion-datetime').invoke('prop', 'value', '1996-03-15'); + // Set date to 1994-03-14 + cy.get('ion-datetime').first().shadow().find('.calendar-day:not([disabled])').first().click(); cy.get('ion-select#game-console').click(); cy.get('ion-alert').should('exist').should('be.visible'); @@ -58,7 +59,7 @@ describe('Inputs', () => { cy.get('#checkbox-note').should('have.text', 'true'); cy.get('#toggle-note').should('have.text', 'true'); cy.get('#input-note').should('have.text', 'hola'); - cy.get('#datetime-note').should('have.text', '1996-03-15'); + cy.get('#datetime-note').should('have.text', '1994-03-14'); cy.get('#select-note').should('have.text', 'ps'); cy.get('#range-note').should('have.text', '20'); }); diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 011be1f9d3..a282dd6f5d 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -845,7 +845,7 @@ export namespace Components { */ "titleSelectedDatesFormatter"?: TitleSelectedDatesFormatter; /** - * The value of the datetime as a valid ISO 8601 datetime string. Should be an array of strings if `multiple="true"`. + * The value of the datetime as a valid ISO 8601 datetime string. This should be an array of strings only when `multiple="true"`. */ "value"?: string | string[] | null; /** @@ -4588,6 +4588,10 @@ declare namespace LocalJSX { * Emitted when the styles change. */ "onIonStyle"?: (event: IonDatetimeCustomEvent) => void; + /** + * Emitted when the value property has changed. This is used to ensure that ion-datetime-button can respond to any value property changes. + */ + "onIonValueChange"?: (event: IonDatetimeCustomEvent) => void; /** * If `true`, a wheel picker will be rendered instead of a calendar grid where possible. If `false`, a calendar grid will be rendered instead of a wheel picker where possible. A wheel picker can be rendered instead of a grid when `presentation` is one of the following values: `'date'`, `'date-time'`, or `'time-date'`. A wheel picker will always be rendered regardless of the `preferWheel` value when `presentation` is one of the following values: `'time'`, `'month'`, `'month-year'`, or `'year'`. */ @@ -4625,7 +4629,7 @@ declare namespace LocalJSX { */ "titleSelectedDatesFormatter"?: TitleSelectedDatesFormatter; /** - * The value of the datetime as a valid ISO 8601 datetime string. Should be an array of strings if `multiple="true"`. + * The value of the datetime as a valid ISO 8601 datetime string. This should be an array of strings only when `multiple="true"`. */ "value"?: string | string[] | null; /** diff --git a/core/src/components/datetime-button/datetime-button.tsx b/core/src/components/datetime-button/datetime-button.tsx index f4d85c17ef..1d80a763bf 100644 --- a/core/src/components/datetime-button/datetime-button.tsx +++ b/core/src/components/datetime-button/datetime-button.tsx @@ -126,7 +126,7 @@ export class DatetimeButton implements ComponentInterface { * text in the buttons. */ this.setDateTimeText(); - addEventListener(datetimeEl, 'ionChange', this.setDateTimeText); + addEventListener(datetimeEl, 'ionValueChange', this.setDateTimeText); /** * Configure the initial selected button diff --git a/core/src/components/datetime-button/test/multiple/datetime-button.e2e.ts b/core/src/components/datetime-button/test/multiple/datetime-button.e2e.ts index 326493b3d5..7f0bb994d1 100644 --- a/core/src/components/datetime-button/test/multiple/datetime-button.e2e.ts +++ b/core/src/components/datetime-button/test/multiple/datetime-button.e2e.ts @@ -73,12 +73,12 @@ test.describe('datetime-button: multiple selection', () => { await page.waitForSelector('.datetime-ready'); const datetime = page.locator('ion-datetime'); - const ionChange = await page.spyOnEvent('ionChange'); + const ionValueChange = await page.spyOnEvent('ionValueChange'); const dateButton = page.locator('#date-button'); await expect(dateButton).toHaveText('2 days'); await datetime.evaluate((el: HTMLIonDatetimeElement) => (el.value = ['2022-06-01', '2022-06-02', '2022-06-03'])); - await ionChange.next(); + await ionValueChange.next(); await expect(dateButton).toHaveText('3 days'); }); diff --git a/core/src/components/datetime/datetime.tsx b/core/src/components/datetime/datetime.tsx index a78bf569b8..57b5dd16e0 100644 --- a/core/src/components/datetime/datetime.tsx +++ b/core/src/components/datetime/datetime.tsx @@ -321,7 +321,7 @@ export class Datetime implements ComponentInterface { /** * The value of the datetime as a valid ISO 8601 datetime string. - * Should be an array of strings if `multiple="true"`. + * This should be an array of strings only when `multiple="true"`. */ @Prop({ mutable: true }) value?: string | string[] | null; @@ -330,13 +330,10 @@ export class Datetime implements ComponentInterface { */ @Watch('value') protected valueChanged() { - const { value, minParts, maxParts, workingParts, multiple } = this; + const { value, minParts, maxParts, workingParts } = this; if (this.hasValue()) { - if (!multiple && Array.isArray(value)) { - this.value = value[0]; - return; // setting this.value will trigger re-run of this function - } + this.warnIfIncorrectValueUsage(); /** * Clones the value of the `activeParts` to the private clone, to update @@ -383,7 +380,7 @@ export class Datetime implements ComponentInterface { } this.emitStyle(); - this.ionChange.emit({ value }); + this.ionValueChange.emit({ value }); } /** @@ -459,6 +456,14 @@ export class Datetime implements ComponentInterface { */ @Event() ionChange!: EventEmitter; + /** + * Emitted when the value property has changed. + * This is used to ensure that ion-datetime-button can respond + * to any value property changes. + * @internal + */ + @Event() ionValueChange!: EventEmitter; + /** * Emitted when the datetime has focus. */ @@ -496,7 +501,7 @@ export class Datetime implements ComponentInterface { if (activeParts !== undefined || !isCalendarPicker) { const activePartsIsArray = Array.isArray(activeParts); if (activePartsIsArray && activeParts.length === 0) { - this.value = undefined; + this.setValue(undefined); } else { /** * Prevent convertDataToISO from doing any @@ -517,7 +522,7 @@ export class Datetime implements ComponentInterface { activeParts.tzOffset = date.getTimezoneOffset() * -1; } - this.value = convertDataToISO(activeParts); + this.setValue(convertDataToISO(activeParts)); } } @@ -551,6 +556,32 @@ export class Datetime implements ComponentInterface { } } + private warnIfIncorrectValueUsage = () => { + const { multiple, value } = this; + if (!multiple && Array.isArray(value)) { + /** + * We do some processing on the `value` array so + * that it looks more like an array when logged to + * the console. + * Example given ['a', 'b'] + * Default toString() behavior: a,b + * Custom behavior: ['a', 'b'] + */ + printIonWarning( + `ion-datetime was passed an array of values, but multiple="false". This is incorrect usage and may result in unexpected behaviors. To dismiss this warning, pass a string to the "value" property when multiple="false". + + Value Passed: [${value.map((v) => `'${v}'`).join(', ')}] +`, + this.el + ); + } + }; + + private setValue = (value?: string | string[] | null) => { + this.value = value; + this.ionChange.emit({ value }); + }; + /** * Returns the DatetimePart interface * to use when rendering an initial set of @@ -1155,13 +1186,11 @@ export class Datetime implements ComponentInterface { private processValue = (value?: string | string[] | null) => { const hasValue = value !== null && value !== undefined; - let valueToProcess = parseDate(value ?? getToday()); + const valueToProcess = parseDate(value ?? getToday()); - const { minParts, maxParts, multiple } = this; - if (!multiple && Array.isArray(value)) { - this.value = value[0]; - valueToProcess = (valueToProcess as DatetimeParts[])[0]; - } + const { minParts, maxParts } = this; + + this.warnIfIncorrectValueUsage(); /** * Datetime should only warn of out of bounds values @@ -1319,7 +1348,7 @@ export class Datetime implements ComponentInterface { const clearButtonClick = () => { this.reset(); - this.value = undefined; + this.setValue(undefined); }; /** diff --git a/core/src/components/datetime/test/basic/datetime.e2e.ts b/core/src/components/datetime/test/basic/datetime.e2e.ts index 87c4b648a3..bf019fd324 100644 --- a/core/src/components/datetime/test/basic/datetime.e2e.ts +++ b/core/src/components/datetime/test/basic/datetime.e2e.ts @@ -279,3 +279,40 @@ test.describe('datetime: visibility', () => { await expect(monthYearInterface).toBeHidden(); }); }); + +test.describe('datetime: ionChange', () => { + test.beforeEach(({ skip }) => { + skip.rtl(); + skip.mode('ios', 'ionChange has consistent behavior across modes'); + }); + + test('should fire ionChange when confirming a value from the calendar grid', async ({ page }) => { + await page.setContent(` + + `); + + await page.waitForSelector('.datetime-ready'); + + const ionChange = await page.spyOnEvent('ionChange'); + const calendarButtons = page.locator('.calendar-day:not([disabled])'); + + await calendarButtons.nth(0).click(); + + await ionChange.next(); + await expect(ionChange).toHaveReceivedEventTimes(1); + }); + + test('should not fire ionChange when programmatically setting a value', async ({ page }) => { + await page.setContent(` + + `); + + await page.waitForSelector('.datetime-ready'); + + const ionChange = await page.spyOnEvent('ionChange'); + const datetime = page.locator('ion-datetime'); + + await datetime.evaluate((el: HTMLIonDatetimeElement) => (el.value = '2022-01-01')); + await expect(ionChange).not.toHaveReceivedEvent(); + }); +}); diff --git a/core/src/components/datetime/test/multiple/datetime.e2e.ts b/core/src/components/datetime/test/multiple/datetime.e2e.ts index e5d65c1756..701ec09aee 100644 --- a/core/src/components/datetime/test/multiple/datetime.e2e.ts +++ b/core/src/components/datetime/test/multiple/datetime.e2e.ts @@ -93,11 +93,6 @@ test.describe('datetime: multiple date selection (functionality)', () => { await expect(monthYear).toHaveText('April 2022'); }); - test('multiple=false and array for defaulut value should switch to first item', async ({ page }) => { - const datetime = await setup(page, 'multipleFalseArrayValue'); - await expect(datetime).toHaveJSProperty('value', '2022-06-01'); - }); - test('with buttons, should only update value when confirm is called', async ({ page }) => { const datetime = await setup(page, 'withButtons'); const june2Button = datetime.locator('[data-month="6"][data-day="2"]'); diff --git a/core/src/components/datetime/test/presentation/datetime.e2e.ts b/core/src/components/datetime/test/presentation/datetime.e2e.ts index 050caa720e..64c5d633f2 100644 --- a/core/src/components/datetime/test/presentation/datetime.e2e.ts +++ b/core/src/components/datetime/test/presentation/datetime.e2e.ts @@ -164,13 +164,10 @@ class TimePickerFixture { } async setValue(value: string) { - const ionChange = await this.page.spyOnEvent('ionChange'); await this.timePicker.evaluate((el: HTMLIonDatetimeElement, newValue: string) => { el.value = newValue; }, value); - await ionChange.next(); - // Changing the value can take longer than the default 100ms to repaint await this.page.waitForChanges(300); } diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 1b876ab4fa..89ab65a6c8 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -297,6 +297,7 @@ export const IonDatetime = /*@__PURE__*/ defineContainer('ion-d 'preferWheel', 'ionCancel', 'ionChange', + 'ionValueChange', 'ionFocus', 'ionBlur', 'ionStyle',