Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver
- [Components](#version-9x-components)
- [Legacy Picker](#version-9x-legacy-picker)
- [Router Outlet](#version-9x-router-outlet)
- [Select](#version-9x-select)
- [Framework Specific](#version-9x-framework-specific)
- [React](#version-9x-react)

Expand Down Expand Up @@ -71,6 +72,12 @@ To disable the gesture on a specific outlet, set `swipeGesture` to `false`:

The `swipeBackEnabled` config option is still respected as the initial default and does not need to change for apps that set it once at startup.

<h4 id="version-9x-select">Select</h4>

The `ionChange` event on `ion-select` now only fires when the selected value actually changes. Previously, the `alert` and `action-sheet` interfaces emitted `ionChange` every time the overlay was confirmed, even when the user chose the option that was already selected. This aligns the `alert` and `action-sheet` interfaces with the existing behavior of the `popover` and `modal` interfaces, and with the documented contract of `ionChange`.

Apps that relied on `ionChange` firing on every confirmation (for example, to detect overlay dismissal without a value change) should listen for `ionDismiss` instead, or use the `didDismiss` event on the underlying alert or action sheet.

<h2 id="version-9x-framework-specific">Framework Specific</h2>

<h4 id="version-9x-react">React</h4>
Expand Down
33 changes: 33 additions & 0 deletions core/src/components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,10 +282,43 @@ export class Select implements ComponentInterface {
}

private setValue(value?: any | null) {
if (this.isValueEqual(this.value, value)) {
return;
}
this.value = value;
this.ionChange.emit({ value });
}

private isValueEqual(currentValue: any, newValue: any): boolean {
if (this.multiple) {
const currentArr = Array.isArray(currentValue) ? currentValue : [];
const newArr = Array.isArray(newValue) ? newValue : [];
if (currentArr.length !== newArr.length) {
return false;
}
// Multiset compare: each new value must match a distinct current value.
// A plain `every(isOptionSelected)` would accept ['a','a'] as equal to
// ['a','b'] when both 'a' and 'b' map to options whose values overlap.
const remaining = currentArr.slice();
return newArr.every((val: any) => {
const idx = remaining.findIndex((c: any) => compareOptions(c, val, this.compareWith));
if (idx === -1) {
return false;
}
remaining.splice(idx, 1);
return true;
});
}

if (currentValue == null && newValue == null) {
return true;
}
if (currentValue == null || newValue == null) {
return false;
}
return compareOptions(currentValue, newValue, this.compareWith);
}

async connectedCallback() {
const { el } = this;

Expand Down
107 changes: 107 additions & 0 deletions core/src/components/select/test/basic/select.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,113 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
expect(ionChange).toHaveReceivedEventTimes(1);
});

test('should not fire ionChange when confirming the already-selected alert option', async ({ page }, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/26789',
});

await page.setContent(
`
<ion-select aria-label="Fruit" interface="alert" value="apple">
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
`,
config
);

const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
const ionAlertDidDismiss = await page.spyOnEvent('ionAlertDidDismiss');
const select = page.locator('ion-select') as E2ELocator;
const ionChange = await select.spyOnEvent('ionChange');

await select.click();
await ionAlertDidPresent.next();

const alert = page.locator('ion-alert');
const confirmButton = alert.locator('.alert-button:not(.alert-button-role-cancel)');

await confirmButton.click();
await ionAlertDidDismiss.next();

expect(ionChange).toHaveReceivedEventTimes(0);
await expect(select).toHaveJSProperty('value', 'apple');
});

test('should not fire ionChange when confirming the already-selected alert options (multiple)', async ({
page,
}, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/26789',
});

await page.setContent(
`
<ion-select aria-label="Fruit" interface="alert" multiple="true">
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
`,
config
);

const select = page.locator('ion-select') as E2ELocator;
await select.evaluate((el: HTMLIonSelectElement) => (el.value = ['apple', 'banana']));

const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
const ionAlertDidDismiss = await page.spyOnEvent('ionAlertDidDismiss');
const ionChange = await select.spyOnEvent('ionChange');

await select.click();
await ionAlertDidPresent.next();

const alert = page.locator('ion-alert');
const confirmButton = alert.locator('.alert-button:not(.alert-button-role-cancel)');

await confirmButton.click();
await ionAlertDidDismiss.next();

expect(ionChange).toHaveReceivedEventTimes(0);
});

test('should not fire ionChange when tapping the already-selected action-sheet option', async ({
page,
}, testInfo) => {
testInfo.annotations.push({
type: 'issue',
description: 'https://github.com/ionic-team/ionic-framework/issues/26789',
});

await page.setContent(
`
<ion-select aria-label="Fruit" interface="action-sheet" value="apple">
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
`,
config
);

const ionActionSheetDidPresent = await page.spyOnEvent('ionActionSheetDidPresent');
const ionActionSheetDidDismiss = await page.spyOnEvent('ionActionSheetDidDismiss');
const select = page.locator('ion-select') as E2ELocator;
const ionChange = await select.spyOnEvent('ionChange');

await select.click();
await ionActionSheetDidPresent.next();

const actionSheet = page.locator('ion-action-sheet');
const selectedButton = actionSheet.locator('.action-sheet-button[aria-checked="true"]');

await selectedButton.click();
await ionActionSheetDidDismiss.next();

expect(ionChange).toHaveReceivedEventTimes(0);
await expect(select).toHaveJSProperty('value', 'apple');
});

test('should not fire when programmatically setting a valid value', async ({ page }) => {
await page.setContent(
`
Expand Down
Loading