Skip to content

Commit

Permalink
feat(datetime): ionChange will only emit from user committed changes (#…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
liamdebeasi committed Oct 11, 2022
1 parent 21b7c8d commit cc2af20
Show file tree
Hide file tree
Showing 10 changed files with 102 additions and 31 deletions.
7 changes: 7 additions & 0 deletions BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions angular/test/base/e2e/src/inputs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
});
Expand Down
8 changes: 6 additions & 2 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand Down Expand Up @@ -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'`.
*/
Expand Down Expand Up @@ -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;
/**
Expand Down
2 changes: 1 addition & 1 deletion core/src/components/datetime-button/datetime-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down
61 changes: 45 additions & 16 deletions core/src/components/datetime/datetime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand Down Expand Up @@ -383,7 +380,7 @@ export class Datetime implements ComponentInterface {
}

this.emitStyle();
this.ionChange.emit({ value });
this.ionValueChange.emit({ value });
}

/**
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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
Expand All @@ -517,7 +522,7 @@ export class Datetime implements ComponentInterface {
activeParts.tzOffset = date.getTimezoneOffset() * -1;
}

this.value = convertDataToISO(activeParts);
this.setValue(convertDataToISO(activeParts));
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1319,7 +1348,7 @@ export class Datetime implements ComponentInterface {

const clearButtonClick = () => {
this.reset();
this.value = undefined;
this.setValue(undefined);
};

/**
Expand Down
37 changes: 37 additions & 0 deletions core/src/components/datetime/test/basic/datetime.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
5 changes: 0 additions & 5 deletions core/src/components/datetime/test/multiple/datetime.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions packages/vue/src/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ export const IonDatetime = /*@__PURE__*/ defineContainer<JSX.IonDatetime>('ion-d
'preferWheel',
'ionCancel',
'ionChange',
'ionValueChange',
'ionFocus',
'ionBlur',
'ionStyle',
Expand Down

0 comments on commit cc2af20

Please sign in to comment.