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:
Liam DeBeasi
2022-10-11 16:17:52 -05:00
committed by GitHub
parent 21b7c8d297
commit cc2af202a9
10 changed files with 102 additions and 31 deletions

View File

@ -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.
<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>
- `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.

View File

@ -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');
});

View File

@ -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<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'`.
*/
@ -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;
/**

View File

@ -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

View File

@ -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');
});

View File

@ -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<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.
*/
@ -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);
};
/**

View File

@ -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(`
<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();
});
});

View File

@ -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"]');

View File

@ -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);
}

View File

@ -297,6 +297,7 @@ export const IonDatetime = /*@__PURE__*/ defineContainer<JSX.IonDatetime>('ion-d
'preferWheel',
'ionCancel',
'ionChange',
'ionValueChange',
'ionFocus',
'ionBlur',
'ionStyle',