feat(radio-group): add compareWith property (#28452)

This commit is contained in:
Shawn Taylor
2023-11-09 10:21:55 -05:00
committed by GitHub
parent 27c4d194c5
commit 0ae327f0e0
12 changed files with 266 additions and 40 deletions

View File

@ -7,3 +7,5 @@ export interface RadioGroupCustomEvent<T = any> extends CustomEvent {
detail: RadioGroupChangeEventDetail<T>;
target: HTMLIonRadioGroupElement;
}
export type RadioGroupCompareFn = (currentValue: any, compareValue: any) => boolean;

View File

@ -4,7 +4,7 @@ import { renderHiddenInput } from '@utils/helpers';
import { getIonMode } from '../../global/ionic-global';
import type { RadioGroupChangeEventDetail } from './radio-group-interface';
import type { RadioGroupChangeEventDetail, RadioGroupCompareFn } from './radio-group-interface';
@Component({
tag: 'ion-radio-group',
@ -21,6 +21,14 @@ export class RadioGroup implements ComponentInterface {
*/
@Prop() allowEmptySelection = false;
/**
* This property allows developers to specify a custom function or property
* name for comparing objects when determining the selected option in the
* ion-radio-group. When not specified, the default behavior will use strict
* equality (===) for comparison.
*/
@Prop() compareWith?: string | RadioGroupCompareFn | null;
/**
* The name of the control, which is submitted with the form data.
*/

View File

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Radio Group - compareWith</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Radio Group - compareWith</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-list-header>Compare with String</ion-list-header>
<ion-radio-group id="compareWithString" compare-with="value"> </ion-radio-group>
</ion-list>
<ion-list>
<ion-list-header>Compare with Function</ion-list-header>
<ion-radio-group id="compareWithFn"></ion-radio-group>
</ion-list>
</ion-content>
</ion-app>
<script>
const options = [
{
label: 'Red',
value: 'red',
},
{
label: 'Blue',
value: 'blue',
},
{
label: 'Green',
value: 'green',
},
];
const radioGroupWithFn = document.querySelector('#compareWithFn');
radioGroupWithFn.compareWith = (a, b) => a.value === b.value;
document.querySelectorAll('ion-radio-group').forEach((radioGroup) => {
radioGroup.value = options[1];
options.forEach((option) => {
const radio = document.createElement('ion-radio');
radio.value = option;
radio.textContent = option.label;
const item = document.createElement('ion-item');
item.appendChild(radio);
radioGroup.appendChild(item);
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,111 @@
import { h } from '@stencil/core';
import { newSpecPage } from '@stencil/core/testing';
import { Radio } from '../../radio/radio';
import { RadioGroup } from '../radio-group';
describe('ion-radio-group', () => {
it('should correctly set value when using compareWith string', async () => {
const page = await newSpecPage({
components: [Radio, RadioGroup],
template: () => (
<ion-radio-group compareWith="value" value={{ label: 'Blue', value: 'blue' }}>
<ion-radio value={{ label: 'Red', value: 'red' }}>Red</ion-radio>
<ion-radio value={{ label: 'Blue', value: 'blue' }}>Blue</ion-radio>
<ion-radio value={{ label: 'Green', value: 'green' }}>Green</ion-radio>
</ion-radio-group>
),
});
const radioGroup = page.body.querySelector('ion-radio-group')!;
const radios = document.querySelectorAll('ion-radio')!;
await radios[2].click();
await page.waitForChanges();
expect(radios[2].getAttribute('aria-checked')).toBe('true');
expect(radioGroup.value).toEqual({
label: 'Green',
value: 'green',
});
});
it('should correctly set value when using compareWith function', async () => {
const page = await newSpecPage({
components: [Radio, RadioGroup],
template: () => (
<ion-radio-group value={{ label: 'Blue', value: 'blue' }}>
<ion-radio value={{ label: 'Red', value: 'red' }}>Red</ion-radio>
<ion-radio value={{ label: 'Blue', value: 'blue' }}>Blue</ion-radio>
<ion-radio value={{ label: 'Green', value: 'green' }}>Green</ion-radio>
</ion-radio-group>
),
});
const radioGroup = page.body.querySelector('ion-radio-group')!;
const radios = document.querySelectorAll('ion-radio')!;
radioGroup.compareWith = (a, b) => a.value === b.value;
await radios[2].click();
await page.waitForChanges();
expect(radios[2].getAttribute('aria-checked')).toBe('true');
expect(radioGroup.value).toEqual({
label: 'Green',
value: 'green',
});
});
it('should correctly set value when using compareWith null', async () => {
const page = await newSpecPage({
components: [RadioGroup, Radio],
template: () => (
<ion-radio-group compareWith={null} value="blue">
<ion-radio value="red">Red</ion-radio>
<br />
<ion-radio value="blue">Blue</ion-radio>
<br />
<ion-radio value="green">Green</ion-radio>
</ion-radio-group>
),
});
const radioGroup = page.body.querySelector('ion-radio-group')!;
const radios = document.querySelectorAll('ion-radio')!;
await radios[2].click();
await page.waitForChanges();
expect(radios[2].getAttribute('aria-checked')).toBe('true');
expect(radioGroup.value).toEqual('green');
});
it('should work with different parameter types', async () => {
const page = await newSpecPage({
components: [Radio, RadioGroup],
template: () => (
<ion-radio-group value={2}>
<ion-radio value={1}>Option #1</ion-radio>
<ion-radio value={2}>Option #2</ion-radio>
<ion-radio value={3}>Option #3</ion-radio>
</ion-radio-group>
),
});
const radioGroup = page.body.querySelector('ion-radio-group')!;
radioGroup.compareWith = (val1, val2) => {
// convert val1 to a number
return +val1 === val2;
};
const radios = document.querySelectorAll('ion-radio')!;
await expect(radios[1].getAttribute('aria-checked')).toBe('true');
await radios[2].click();
await page.waitForChanges();
expect(radios[2].getAttribute('aria-checked')).toBe('true');
expect(radioGroup.value).toEqual(3);
});
});

View File

@ -1,7 +1,7 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Method, Prop, State, Watch, h } from '@stencil/core';
import type { LegacyFormController } from '@utils/forms';
import { createLegacyFormController } from '@utils/forms';
import { createLegacyFormController, isOptionSelected } from '@utils/forms';
import { addEventListener, getAriaLabel, removeEventListener } from '@utils/helpers';
import { printIonWarning } from '@utils/logging';
import { createColorClasses, hostContext } from '@utils/theme';
@ -196,7 +196,9 @@ export class Radio implements ComponentInterface {
private updateState = () => {
if (this.radioGroup) {
this.checked = this.radioGroup.value === this.value;
const { compareWith, value: radioGroupValue } = this.radioGroup;
this.checked = isOptionSelected(radioGroupValue, this.value, compareWith);
}
};

View File

@ -1,7 +1,7 @@
import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Component, Element, Event, Host, Method, Prop, State, Watch, h, forceUpdate } from '@stencil/core';
import type { LegacyFormController, NotchController } from '@utils/forms';
import { createLegacyFormController, createNotchController } from '@utils/forms';
import { compareOptions, createLegacyFormController, createNotchController, isOptionSelected } from '@utils/forms';
import { findItemLabel, focusElement, getAriaLabel, renderHiddenInput, inheritAttributes } from '@utils/helpers';
import type { Attributes } from '@utils/helpers';
import { printIonWarning } from '@utils/logging';
@ -82,7 +82,10 @@ export class Select implements ComponentInterface {
@Prop({ reflect: true }) color?: Color;
/**
* A property name or function used to compare object values
* This property allows developers to specify a custom function or property
* name for comparing objects when determining the selected option in the
* ion-select. When not specified, the default behavior will use strict
* equality (===) for comparison.
*/
@Prop() compareWith?: string | SelectCompareFn | null;
@ -1076,21 +1079,6 @@ Developers can use the "legacy" property to continue using the legacy form marku
}
}
const isOptionSelected = (
currentValue: any[] | any,
compareValue: any,
compareWith?: string | SelectCompareFn | null
) => {
if (currentValue === undefined) {
return false;
}
if (Array.isArray(currentValue)) {
return currentValue.some((val) => compareOptions(val, compareValue, compareWith));
} else {
return compareOptions(currentValue, compareValue, compareWith);
}
};
const getOptionValue = (el: HTMLIonSelectOptionElement) => {
const value = el.value;
return value === undefined ? el.textContent || '' : value;
@ -1106,20 +1094,6 @@ const parseValue = (value: any) => {
return value.toString();
};
const compareOptions = (
currentValue: any,
compareValue: any,
compareWith?: string | SelectCompareFn | null
): boolean => {
if (typeof compareWith === 'function') {
return compareWith(currentValue, compareValue);
} else if (typeof compareWith === 'string') {
return currentValue[compareWith] === compareValue[compareWith];
} else {
return Array.isArray(compareValue) ? compareValue.includes(currentValue) : currentValue === compareValue;
}
};
const generateText = (
opts: HTMLIonSelectOptionElement[],
value: any | any[],