Skip to content

Commit

Permalink
feat(checkbox): ionChange fires on user interaction (#25923)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
liamdebeasi committed Sep 13, 2022
1 parent c76de0c commit a6b2629
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 17 deletions.
5 changes: 5 additions & 0 deletions BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.

<h4 id="version-7x-checkbox">Checkbox</h4>

`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-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: 4 additions & 1 deletion angular/src/directives/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CustomEvent<ICheckboxCheckboxChangeEventDetail>>;
/**
Expand Down
2 changes: 1 addition & 1 deletion angular/test/base/e2e/src/inputs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion angular/test/base/src/app/inputs/inputs.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@

<ion-item>
<ion-label>Checkbox</ion-label>
<ion-checkbox [(ngModel)]="checkbox" slot="start"></ion-checkbox>
<ion-checkbox [(ngModel)]="checkbox" slot="start" id="first-checkbox"></ion-checkbox>
<ion-note slot="end" id="checkbox-note">{{checkbox}}</ion-note>
</ion-item>

Expand Down
2 changes: 1 addition & 1 deletion core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4314,7 +4314,7 @@ declare namespace LocalJSX {
*/
"onIonBlur"?: (event: IonCheckboxCustomEvent<void>) => 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<CheckboxChangeEventDetail>) => void;
/**
Expand Down
31 changes: 22 additions & 9 deletions core/src/components/checkbox/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<CheckboxChangeEventDetail>;

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

Expand All @@ -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;
};

Expand Down Expand Up @@ -153,7 +166,6 @@ export class Checkbox implements ComponentInterface {

return (
<Host
onClick={this.onClick}
aria-labelledby={label ? labelId : null}
aria-checked={`${checked}`}
aria-hidden={disabled ? 'true' : null}
Expand All @@ -176,6 +188,7 @@ export class Checkbox implements ComponentInterface {
aria-checked={`${checked}`}
disabled={disabled}
id={inputId}
onChange={this.toggleChecked}
onFocus={() => this.onFocus()}
onBlur={() => this.onBlur()}
ref={(focusEl) => (this.focusEl = focusEl)}
Expand Down
49 changes: 49 additions & 0 deletions core/src/components/checkbox/test/basic/checkbox.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(`
<ion-checkbox value="my-checkbox"></ion-checkbox>
`);

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(`
<ion-item>
<ion-checkbox value="my-checkbox"></ion-checkbox>
</ion-item>
`);

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(`
<ion-checkbox value="my-checkbox"></ion-checkbox>
`);

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();
});
});
7 changes: 3 additions & 4 deletions core/src/components/datetime/test/color/datetime.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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();

Expand All @@ -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(
Expand Down

0 comments on commit a6b2629

Please sign in to comment.