feat(accordion): ionChange will only emit from user committed changes (#25922)

BREAKING CHANGE:

`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.
This commit is contained in:
Liam DeBeasi
2022-09-13 13:33:49 -05:00
committed by GitHub
parent 89b880e91d
commit 4eea9fa5c0
7 changed files with 154 additions and 13 deletions

View File

@@ -68,6 +68,9 @@ export namespace Components {
* If `true`, the accordion group cannot be interacted with, but does not alter the opacity.
*/
"readonly": boolean;
/**
* This method is used to ensure that the value of ion-accordion-group is being set in a valid way. This method should only be called in response to a user generated action.
*/
"requestAccordionToggle": (accordionValue: string | undefined, accordionExpand: boolean) => Promise<void>;
/**
* The value of the accordion group.
@@ -3804,9 +3807,13 @@ declare namespace LocalJSX {
*/
"multiple"?: boolean;
/**
* Emitted when the value property has changed.
* Emitted when the value property has changed as a result of a user action such as a click. This event will not emit when programmatically setting the value property.
*/
"onIonChange"?: (event: IonAccordionGroupCustomEvent<AccordionGroupChangeEventDetail>) => void;
/**
* Emitted when the value property has changed. This is used to ensure that ion-accordion can respond to any value property changes.
*/
"onIonValueChange"?: (event: IonAccordionGroupCustomEvent<AccordionGroupChangeEventDetail>) => void;
/**
* If `true`, the accordion group cannot be interacted with, but does not alter the opacity.
*/

View File

@@ -55,10 +55,21 @@ export class AccordionGroup implements ComponentInterface {
@Prop() expand: 'compact' | 'inset' = 'compact';
/**
* Emitted when the value property has changed.
* Emitted when the value property has changed
* as a result of a user action such as a click.
* This event will not emit when programmatically setting
* the value property.
*/
@Event() ionChange!: EventEmitter<AccordionGroupChangeEventDetail>;
/**
* Emitted when the value property has changed.
* This is used to ensure that ion-accordion can respond
* to any value property changes.
* @internal
*/
@Event() ionValueChange!: EventEmitter<AccordionGroupChangeEventDetail>;
@Watch('value')
valueChanged() {
const { value, multiple } = this;
@@ -68,12 +79,18 @@ export class AccordionGroup implements ComponentInterface {
* let multiple accordions be open
* at once, but user passes an array
* just grab the first value.
* This should emit ionChange because
* we are updating the value internally.
*/
if (!multiple && Array.isArray(value)) {
this.value = value[0];
} else {
this.ionChange.emit({ value: this.value });
this.setValue(value[0]);
}
/**
* Do not use `value` here as that will be
* not account for the adjustment we make above.
*/
this.ionValueChange.emit({ value: this.value });
}
@Watch('disabled')
@@ -156,6 +173,23 @@ export class AccordionGroup implements ComponentInterface {
}
/**
* Sets the value property and emits ionChange.
* This should only be called when the user interacts
* with the accordion and not for any update
* to the value property. The exception is when
* the app sets the value of a single-select
* accordion group to an array.
*/
private setValue(accordionValue: string | string[] | null | undefined) {
const value = (this.value = accordionValue);
this.ionChange.emit({ value });
}
/**
* This method is used to ensure that the value
* of ion-accordion-group is being set in a valid
* way. This method should only be called in
* response to a user generated action.
* @internal
*/
@Method()
@@ -177,10 +211,10 @@ export class AccordionGroup implements ComponentInterface {
const processedValue = Array.isArray(groupValue) ? groupValue : [groupValue];
const valueExists = processedValue.find((v) => v === accordionValue);
if (valueExists === undefined && accordionValue !== undefined) {
this.value = [...processedValue, accordionValue];
this.setValue([...processedValue, accordionValue]);
}
} else {
this.value = accordionValue;
this.setValue(accordionValue);
}
} else {
/**
@@ -190,9 +224,9 @@ export class AccordionGroup implements ComponentInterface {
if (multiple) {
const groupValue = value || [];
const processedValue = Array.isArray(groupValue) ? groupValue : [groupValue];
this.value = processedValue.filter((v) => v !== accordionValue);
this.setValue(processedValue.filter((v) => v !== accordionValue));
} else {
this.value = undefined;
this.setValue(undefined);
}
}
}

View File

@@ -85,14 +85,14 @@ export class Accordion implements ComponentInterface {
const accordionGroupEl = (this.accordionGroupEl = this.el?.closest('ion-accordion-group'));
if (accordionGroupEl) {
this.updateState(true);
addEventListener(accordionGroupEl, 'ionChange', this.updateListener);
addEventListener(accordionGroupEl, 'ionValueChange', this.updateListener);
}
}
disconnectedCallback() {
const accordionGroupEl = this.accordionGroupEl;
if (accordionGroupEl) {
removeEventListener(accordionGroupEl, 'ionChange', this.updateListener);
removeEventListener(accordionGroupEl, 'ionValueChange', this.updateListener);
}
}

View File

@@ -10,3 +10,94 @@ test.describe('accordion: basic', () => {
expect(await page.screenshot()).toMatchSnapshot(`accordion-basic-${page.getSnapshotSettings()}.png`);
});
});
test.describe('accordion: ionChange', () => {
test.beforeEach(({ skip }) => {
skip.rtl();
});
test('should fire ionChange when interacting with accordions', async ({ page }) => {
await page.setContent(`
<ion-accordion-group value="second">
<ion-accordion value="first">
<button slot="header">Header</button>
<div slot="content">First Content</div>
</ion-accordion>
<ion-accordion value="second">
<button slot="header">Header</button>
<div slot="content">Second Content</div>
</ion-accordion>
<ion-accordion value="third">
<button slot="header">Header</button>
<div slot="content">Third Content</div>
</ion-accordion>
</ion-accordion-group>
`);
const ionChange = await page.spyOnEvent('ionChange');
const accordionHeaders = page.locator('button[slot="header"]');
await accordionHeaders.nth(0).click();
await expect(ionChange).toHaveReceivedEventDetail({ value: 'first' });
await accordionHeaders.nth(1).click();
await expect(ionChange).toHaveReceivedEventDetail({ value: 'second' });
await accordionHeaders.nth(1).click();
await expect(ionChange).toHaveReceivedEventDetail({ value: undefined });
await accordionHeaders.nth(2).click();
await expect(ionChange).toHaveReceivedEventDetail({ value: 'third' });
});
test('should not fire when programmatically setting a valid value', async ({ page }) => {
await page.setContent(`
<ion-accordion-group>
<ion-accordion value="first">
<button slot="header">Header</button>
<div slot="content">First Content</div>
</ion-accordion>
<ion-accordion value="second">
<button slot="header">Header</button>
<div slot="content">Second Content</div>
</ion-accordion>
<ion-accordion value="third">
<button slot="header">Header</button>
<div slot="content">Third Content</div>
</ion-accordion>
</ion-accordion-group>
`);
const ionChange = await page.spyOnEvent('ionChange');
const accordionGroup = page.locator('ion-accordion-group');
await accordionGroup.evaluate((el: HTMLIonAccordionGroupElement) => (el.value = 'second'));
await expect(ionChange).not.toHaveReceivedEvent();
});
test('should fire ionChange setting array of values on a single selection accordion', async ({ page }) => {
await page.setContent(`
<ion-accordion-group>
<ion-accordion value="first">
<button slot="header">Header</button>
<div slot="content">First Content</div>
</ion-accordion>
<ion-accordion value="second">
<button slot="header">Header</button>
<div slot="content">Second Content</div>
</ion-accordion>
<ion-accordion value="third">
<button slot="header">Header</button>
<div slot="content">Third Content</div>
</ion-accordion>
</ion-accordion-group>
`);
const ionChange = await page.spyOnEvent('ionChange');
const accordionGroup = page.locator('ion-accordion-group');
await accordionGroup.evaluate((el: HTMLIonAccordionGroupElement) => (el.value = ['second', 'third']));
await expect(ionChange).toHaveReceivedEventDetail({ value: 'second' });
});
});