From a6b2629ede9f2b0e16343b9afabf68eb53cacc17 Mon Sep 17 00:00:00 2001 From: Liam DeBeasi Date: Tue, 13 Sep 2022 15:27:34 -0500 Subject: [PATCH] feat(checkbox): ionChange fires on user interaction (#25923) BREAKING CHANGE: `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. --- BREAKING.md | 5 ++ angular/src/directives/proxies.ts | 5 +- angular/test/base/e2e/src/inputs.spec.ts | 2 +- .../base/src/app/inputs/inputs.component.html | 2 +- core/src/components.d.ts | 2 +- core/src/components/checkbox/checkbox.tsx | 31 ++++++++---- .../checkbox/test/basic/checkbox.e2e.ts | 49 +++++++++++++++++++ .../datetime/test/color/datetime.e2e.ts | 7 ++- 8 files changed, 86 insertions(+), 17 deletions(-) diff --git a/BREAKING.md b/BREAKING.md index 9583abf975e..7d3233f1f46 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -15,6 +15,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver - [Browser and Platform Support](#version-7x-browser-platform-support) - [Components](#version-7x-components) - [Accordion Group](#version-7x-accordion-group) + - [Checkbox](#version-7x-checkbox) - [Input](#version-7x-input) - [Overlays](#version-7x-overlays) - [Range](#version-7x-range) @@ -56,6 +57,10 @@ This section details the desktop browser, JavaScript framework, and mobile platf `ionChange` is no longer emitted when the `value` of `ion-accordion-group` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking or tapping the accordion header. +

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. +

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/src/directives/proxies.ts b/angular/src/directives/proxies.ts index c30a4d98fdc..cb4e3c71c1d 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -388,7 +388,10 @@ export class IonCardTitle { import type { CheckboxChangeEventDetail as ICheckboxCheckboxChangeEventDetail } from '@ionic/core'; export declare interface IonCheckbox extends Components.IonCheckbox { /** - * Emitted when the checked property has changed. + * Emitted when the checked property has changed +as a result of a user action such as a click. +This event will not emit when programmatically +setting the checked property. */ ionChange: EventEmitter>; /** diff --git a/angular/test/base/e2e/src/inputs.spec.ts b/angular/test/base/e2e/src/inputs.spec.ts index c04296c567d..9991ad234e8 100644 --- a/angular/test/base/e2e/src/inputs.spec.ts +++ b/angular/test/base/e2e/src/inputs.spec.ts @@ -38,7 +38,7 @@ describe('Inputs', () => { it('change values should update angular', () => { cy.get('#reset-button').click(); - cy.get('ion-checkbox').invoke('prop', 'checked', true); + cy.get('ion-checkbox#first-checkbox').click(); cy.get('ion-toggle').invoke('prop', 'checked', true); cy.get('ion-input').eq(0).type('hola'); diff --git a/angular/test/base/src/app/inputs/inputs.component.html b/angular/test/base/src/app/inputs/inputs.component.html index 5e6ff4a9c4a..fa65a16dcb0 100644 --- a/angular/test/base/src/app/inputs/inputs.component.html +++ b/angular/test/base/src/app/inputs/inputs.component.html @@ -75,7 +75,7 @@ Checkbox - + {{checkbox}} diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 00fee7860e0..c9a38ccdb30 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -4314,7 +4314,7 @@ declare namespace LocalJSX { */ "onIonBlur"?: (event: IonCheckboxCustomEvent) => void; /** - * Emitted when the checked property has changed. + * Emitted when the checked property has changed as a result of a user action such as a click. This event will not emit when programmatically setting the checked property. */ "onIonChange"?: (event: IonCheckboxCustomEvent) => void; /** diff --git a/core/src/components/checkbox/checkbox.tsx b/core/src/components/checkbox/checkbox.tsx index 5d8e482b785..239494d0b19 100644 --- a/core/src/components/checkbox/checkbox.tsx +++ b/core/src/components/checkbox/checkbox.tsx @@ -63,7 +63,10 @@ export class Checkbox implements ComponentInterface { @Prop() value: any | null = 'on'; /** - * Emitted when the checked property has changed. + * Emitted when the checked property has changed + * as a result of a user action such as a click. + * This event will not emit when programmatically + * setting the checked property. */ @Event() ionChange!: EventEmitter; @@ -88,11 +91,7 @@ export class Checkbox implements ComponentInterface { } @Watch('checked') - checkedChanged(isChecked: boolean) { - this.ionChange.emit({ - checked: isChecked, - value: this.value, - }); + checkedChanged() { this.emitStyle(); } @@ -114,11 +113,25 @@ export class Checkbox implements ComponentInterface { } } - private onClick = (ev: any) => { + /** + * Sets the checked property and emits + * the ionChange event. Use this to update the + * checked state in response to user-generated + * actions such as a click. + */ + private setChecked = (state: boolean) => { + const isChecked = (this.checked = state); + this.ionChange.emit({ + checked: isChecked, + value: this.value, + }); + }; + + private toggleChecked = (ev: any) => { ev.preventDefault(); this.setFocus(); - this.checked = !this.checked; + this.setChecked(!this.checked); this.indeterminate = false; }; @@ -153,7 +166,6 @@ export class Checkbox implements ComponentInterface { return ( this.onFocus()} onBlur={() => this.onBlur()} ref={(focusEl) => (this.focusEl = focusEl)} diff --git a/core/src/components/checkbox/test/basic/checkbox.e2e.ts b/core/src/components/checkbox/test/basic/checkbox.e2e.ts index ac8efcfd2a0..6181e944084 100644 --- a/core/src/components/checkbox/test/basic/checkbox.e2e.ts +++ b/core/src/components/checkbox/test/basic/checkbox.e2e.ts @@ -10,3 +10,52 @@ test.describe('checkbox: basic', () => { expect(await page.screenshot()).toMatchSnapshot(`checkbox-basic-${page.getSnapshotSettings()}.png`); }); }); + +test.describe('checkbox: ionChange', () => { + test.beforeEach(({ skip }) => { + skip.rtl(); + }); + test('should fire ionChange when interacting with checkbox', async ({ page }) => { + await page.setContent(` + + `); + + const ionChange = await page.spyOnEvent('ionChange'); + const checkbox = page.locator('ion-checkbox'); + + await checkbox.click(); + await expect(ionChange).toHaveReceivedEventDetail({ value: 'my-checkbox', checked: true }); + + await checkbox.click(); + await expect(ionChange).toHaveReceivedEventDetail({ value: 'my-checkbox', checked: false }); + }); + + test('should fire ionChange when interacting with checkbox in item', async ({ page }) => { + await page.setContent(` + + + + `); + + const ionChange = await page.spyOnEvent('ionChange'); + const item = page.locator('ion-item'); + + await item.click(); + await expect(ionChange).toHaveReceivedEventDetail({ value: 'my-checkbox', checked: true }); + + await item.click(); + await expect(ionChange).toHaveReceivedEventDetail({ value: 'my-checkbox', checked: false }); + }); + + test('should not fire when programmatically setting a value', async ({ page }) => { + await page.setContent(` + + `); + + const ionChange = await page.spyOnEvent('ionChange'); + const checkbox = page.locator('ion-checkbox'); + + await checkbox.evaluate((el: HTMLIonCheckboxElement) => (el.checked = true)); + await expect(ionChange).not.toHaveReceivedEvent(); + }); +}); diff --git a/core/src/components/datetime/test/color/datetime.e2e.ts b/core/src/components/datetime/test/color/datetime.e2e.ts index 35543ece3e9..c7c6c668b61 100644 --- a/core/src/components/datetime/test/color/datetime.e2e.ts +++ b/core/src/components/datetime/test/color/datetime.e2e.ts @@ -6,10 +6,9 @@ test.describe('datetime: color', () => { await page.goto('/src/components/datetime/test/color'); const colorSelect = page.locator('ion-select'); - const darkModeToggle = page.locator('ion-checkbox'); const datetime = page.locator('ion-datetime'); - await darkModeToggle.evaluate((el: HTMLIonCheckboxElement) => (el.checked = true)); + await page.evaluate(() => document.body.classList.toggle('dark')); await page.waitForChanges(); expect(await datetime.first().screenshot()).toMatchSnapshot( @@ -19,7 +18,7 @@ test.describe('datetime: color', () => { `datetime-color-custom-dark-${page.getSnapshotSettings()}.png` ); - await darkModeToggle.evaluate((el: HTMLIonCheckboxElement) => (el.checked = false)); + await page.evaluate(() => document.body.classList.toggle('dark')); await colorSelect.evaluate((el: HTMLIonSelectElement) => (el.value = 'danger')); await page.waitForChanges(); @@ -30,7 +29,7 @@ test.describe('datetime: color', () => { `datetime-color-custom-light-color-${page.getSnapshotSettings()}.png` ); - await darkModeToggle.evaluate((el: HTMLIonCheckboxElement) => (el.checked = true)); + await page.evaluate(() => document.body.classList.toggle('dark')); await page.waitForChanges(); expect(await datetime.first().screenshot()).toMatchSnapshot(