mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-17 18:54:11 +08:00
feat(datetime): ionChange will only emit from user committed changes (#26083)
resolves #20873 resolves #24452 BREAKING CHANGE - `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.
This commit is contained in:
@ -16,6 +16,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver
|
|||||||
- [Components](#version-7x-components)
|
- [Components](#version-7x-components)
|
||||||
- [Accordion Group](#version-7x-accordion-group)
|
- [Accordion Group](#version-7x-accordion-group)
|
||||||
- [Checkbox](#version-7x-checkbox)
|
- [Checkbox](#version-7x-checkbox)
|
||||||
|
- [Datetime](#version-7x-datetime)
|
||||||
- [Input](#version-7x-input)
|
- [Input](#version-7x-input)
|
||||||
- [Modal](#version-7x-modal)
|
- [Modal](#version-7x-modal)
|
||||||
- [Overlays](#version-7x-overlays)
|
- [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.
|
`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.
|
||||||
|
|
||||||
|
<h4 id="version-7x-datetime">Datetime</h4>
|
||||||
|
|
||||||
|
- `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.
|
||||||
|
|
||||||
<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 or from clicking the clear action within the input.
|
||||||
|
@ -44,7 +44,8 @@ describe('Inputs', () => {
|
|||||||
cy.get('ion-input').eq(0).type('hola');
|
cy.get('ion-input').eq(0).type('hola');
|
||||||
cy.get('ion-input input').eq(0).blur();
|
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-select#game-console').click();
|
||||||
cy.get('ion-alert').should('exist').should('be.visible');
|
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('#checkbox-note').should('have.text', 'true');
|
||||||
cy.get('#toggle-note').should('have.text', 'true');
|
cy.get('#toggle-note').should('have.text', 'true');
|
||||||
cy.get('#input-note').should('have.text', 'hola');
|
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('#select-note').should('have.text', 'ps');
|
||||||
cy.get('#range-note').should('have.text', '20');
|
cy.get('#range-note').should('have.text', '20');
|
||||||
});
|
});
|
||||||
|
8
core/src/components.d.ts
vendored
8
core/src/components.d.ts
vendored
@ -845,7 +845,7 @@ export namespace Components {
|
|||||||
*/
|
*/
|
||||||
"titleSelectedDatesFormatter"?: TitleSelectedDatesFormatter;
|
"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;
|
"value"?: string | string[] | null;
|
||||||
/**
|
/**
|
||||||
@ -4588,6 +4588,10 @@ declare namespace LocalJSX {
|
|||||||
* Emitted when the styles change.
|
* Emitted when the styles change.
|
||||||
*/
|
*/
|
||||||
"onIonStyle"?: (event: IonDatetimeCustomEvent<StyleEventDetail>) => void;
|
"onIonStyle"?: (event: IonDatetimeCustomEvent<StyleEventDetail>) => 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<DatetimeChangeEventDetail>) => 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'`.
|
* 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;
|
"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;
|
"value"?: string | string[] | null;
|
||||||
/**
|
/**
|
||||||
|
@ -126,7 +126,7 @@ export class DatetimeButton implements ComponentInterface {
|
|||||||
* text in the buttons.
|
* text in the buttons.
|
||||||
*/
|
*/
|
||||||
this.setDateTimeText();
|
this.setDateTimeText();
|
||||||
addEventListener(datetimeEl, 'ionChange', this.setDateTimeText);
|
addEventListener(datetimeEl, 'ionValueChange', this.setDateTimeText);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure the initial selected button
|
* Configure the initial selected button
|
||||||
|
@ -73,12 +73,12 @@ test.describe('datetime-button: multiple selection', () => {
|
|||||||
await page.waitForSelector('.datetime-ready');
|
await page.waitForSelector('.datetime-ready');
|
||||||
|
|
||||||
const datetime = page.locator('ion-datetime');
|
const datetime = page.locator('ion-datetime');
|
||||||
const ionChange = await page.spyOnEvent('ionChange');
|
const ionValueChange = await page.spyOnEvent('ionValueChange');
|
||||||
const dateButton = page.locator('#date-button');
|
const dateButton = page.locator('#date-button');
|
||||||
await expect(dateButton).toHaveText('2 days');
|
await expect(dateButton).toHaveText('2 days');
|
||||||
|
|
||||||
await datetime.evaluate((el: HTMLIonDatetimeElement) => (el.value = ['2022-06-01', '2022-06-02', '2022-06-03']));
|
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');
|
await expect(dateButton).toHaveText('3 days');
|
||||||
});
|
});
|
||||||
|
@ -321,7 +321,7 @@ export class Datetime implements ComponentInterface {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The value of the datetime as a valid ISO 8601 datetime string.
|
* 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;
|
@Prop({ mutable: true }) value?: string | string[] | null;
|
||||||
|
|
||||||
@ -330,13 +330,10 @@ export class Datetime implements ComponentInterface {
|
|||||||
*/
|
*/
|
||||||
@Watch('value')
|
@Watch('value')
|
||||||
protected valueChanged() {
|
protected valueChanged() {
|
||||||
const { value, minParts, maxParts, workingParts, multiple } = this;
|
const { value, minParts, maxParts, workingParts } = this;
|
||||||
|
|
||||||
if (this.hasValue()) {
|
if (this.hasValue()) {
|
||||||
if (!multiple && Array.isArray(value)) {
|
this.warnIfIncorrectValueUsage();
|
||||||
this.value = value[0];
|
|
||||||
return; // setting this.value will trigger re-run of this function
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clones the value of the `activeParts` to the private clone, to update
|
* Clones the value of the `activeParts` to the private clone, to update
|
||||||
@ -383,7 +380,7 @@ export class Datetime implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.emitStyle();
|
this.emitStyle();
|
||||||
this.ionChange.emit({ value });
|
this.ionValueChange.emit({ value });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -459,6 +456,14 @@ export class Datetime implements ComponentInterface {
|
|||||||
*/
|
*/
|
||||||
@Event() ionChange!: EventEmitter<DatetimeChangeEventDetail>;
|
@Event() ionChange!: EventEmitter<DatetimeChangeEventDetail>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<DatetimeChangeEventDetail>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitted when the datetime has focus.
|
* Emitted when the datetime has focus.
|
||||||
*/
|
*/
|
||||||
@ -496,7 +501,7 @@ export class Datetime implements ComponentInterface {
|
|||||||
if (activeParts !== undefined || !isCalendarPicker) {
|
if (activeParts !== undefined || !isCalendarPicker) {
|
||||||
const activePartsIsArray = Array.isArray(activeParts);
|
const activePartsIsArray = Array.isArray(activeParts);
|
||||||
if (activePartsIsArray && activeParts.length === 0) {
|
if (activePartsIsArray && activeParts.length === 0) {
|
||||||
this.value = undefined;
|
this.setValue(undefined);
|
||||||
} else {
|
} else {
|
||||||
/**
|
/**
|
||||||
* Prevent convertDataToISO from doing any
|
* Prevent convertDataToISO from doing any
|
||||||
@ -517,7 +522,7 @@ export class Datetime implements ComponentInterface {
|
|||||||
activeParts.tzOffset = date.getTimezoneOffset() * -1;
|
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
|
* Returns the DatetimePart interface
|
||||||
* to use when rendering an initial set of
|
* to use when rendering an initial set of
|
||||||
@ -1155,13 +1186,11 @@ export class Datetime implements ComponentInterface {
|
|||||||
|
|
||||||
private processValue = (value?: string | string[] | null) => {
|
private processValue = (value?: string | string[] | null) => {
|
||||||
const hasValue = value !== null && value !== undefined;
|
const hasValue = value !== null && value !== undefined;
|
||||||
let valueToProcess = parseDate(value ?? getToday());
|
const valueToProcess = parseDate(value ?? getToday());
|
||||||
|
|
||||||
const { minParts, maxParts, multiple } = this;
|
const { minParts, maxParts } = this;
|
||||||
if (!multiple && Array.isArray(value)) {
|
|
||||||
this.value = value[0];
|
this.warnIfIncorrectValueUsage();
|
||||||
valueToProcess = (valueToProcess as DatetimeParts[])[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Datetime should only warn of out of bounds values
|
* Datetime should only warn of out of bounds values
|
||||||
@ -1319,7 +1348,7 @@ export class Datetime implements ComponentInterface {
|
|||||||
|
|
||||||
const clearButtonClick = () => {
|
const clearButtonClick = () => {
|
||||||
this.reset();
|
this.reset();
|
||||||
this.value = undefined;
|
this.setValue(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -279,3 +279,40 @@ test.describe('datetime: visibility', () => {
|
|||||||
await expect(monthYearInterface).toBeHidden();
|
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(`
|
||||||
|
<ion-datetime presentation="date" value="2022-01-02"></ion-datetime>
|
||||||
|
`);
|
||||||
|
|
||||||
|
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(`
|
||||||
|
<ion-datetime presentation="date" value="2022-01-02"></ion-datetime>
|
||||||
|
`);
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -93,11 +93,6 @@ test.describe('datetime: multiple date selection (functionality)', () => {
|
|||||||
await expect(monthYear).toHaveText('April 2022');
|
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 }) => {
|
test('with buttons, should only update value when confirm is called', async ({ page }) => {
|
||||||
const datetime = await setup(page, 'withButtons');
|
const datetime = await setup(page, 'withButtons');
|
||||||
const june2Button = datetime.locator('[data-month="6"][data-day="2"]');
|
const june2Button = datetime.locator('[data-month="6"][data-day="2"]');
|
||||||
|
@ -164,13 +164,10 @@ class TimePickerFixture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setValue(value: string) {
|
async setValue(value: string) {
|
||||||
const ionChange = await this.page.spyOnEvent('ionChange');
|
|
||||||
await this.timePicker.evaluate((el: HTMLIonDatetimeElement, newValue: string) => {
|
await this.timePicker.evaluate((el: HTMLIonDatetimeElement, newValue: string) => {
|
||||||
el.value = newValue;
|
el.value = newValue;
|
||||||
}, value);
|
}, value);
|
||||||
|
|
||||||
await ionChange.next();
|
|
||||||
|
|
||||||
// Changing the value can take longer than the default 100ms to repaint
|
// Changing the value can take longer than the default 100ms to repaint
|
||||||
await this.page.waitForChanges(300);
|
await this.page.waitForChanges(300);
|
||||||
}
|
}
|
||||||
|
@ -297,6 +297,7 @@ export const IonDatetime = /*@__PURE__*/ defineContainer<JSX.IonDatetime>('ion-d
|
|||||||
'preferWheel',
|
'preferWheel',
|
||||||
'ionCancel',
|
'ionCancel',
|
||||||
'ionChange',
|
'ionChange',
|
||||||
|
'ionValueChange',
|
||||||
'ionFocus',
|
'ionFocus',
|
||||||
'ionBlur',
|
'ionBlur',
|
||||||
'ionStyle',
|
'ionStyle',
|
||||||
|
Reference in New Issue
Block a user