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.
+
- `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',