feat(radio-group): add helperText and errorText properties (#30222)
Issue number: N/A --------- ## What is the current behavior? Radio group does not support helper and error text. ## What is the new behavior? Adds support for helper and error text. ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information - [Supporting Text: Preview](https://ionic-framework-git-rou-11554-ionic1.vercel.app/src/components/radio-group/test/supporting-text) --------- Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
@ -1352,6 +1352,8 @@ ion-radio,part,mark
|
|||||||
ion-radio-group,none
|
ion-radio-group,none
|
||||||
ion-radio-group,prop,allowEmptySelection,boolean,false,false,false
|
ion-radio-group,prop,allowEmptySelection,boolean,false,false,false
|
||||||
ion-radio-group,prop,compareWith,((currentValue: any, compareValue: any) => boolean) | null | string | undefined,undefined,false,false
|
ion-radio-group,prop,compareWith,((currentValue: any, compareValue: any) => boolean) | null | string | undefined,undefined,false,false
|
||||||
|
ion-radio-group,prop,errorText,string | undefined,undefined,false,false
|
||||||
|
ion-radio-group,prop,helperText,string | undefined,undefined,false,false
|
||||||
ion-radio-group,prop,name,string,this.inputId,false,false
|
ion-radio-group,prop,name,string,this.inputId,false,false
|
||||||
ion-radio-group,prop,value,any,undefined,false,false
|
ion-radio-group,prop,value,any,undefined,false,false
|
||||||
ion-radio-group,event,ionChange,RadioGroupChangeEventDetail<any>,true
|
ion-radio-group,event,ionChange,RadioGroupChangeEventDetail<any>,true
|
||||||
|
16
core/src/components.d.ts
vendored
@ -2315,6 +2315,14 @@ export namespace Components {
|
|||||||
* 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.
|
* 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.
|
||||||
*/
|
*/
|
||||||
"compareWith"?: string | RadioGroupCompareFn | null;
|
"compareWith"?: string | RadioGroupCompareFn | null;
|
||||||
|
/**
|
||||||
|
* The error text to display at the top of the radio group.
|
||||||
|
*/
|
||||||
|
"errorText"?: string;
|
||||||
|
/**
|
||||||
|
* The helper text to display at the top of the radio group.
|
||||||
|
*/
|
||||||
|
"helperText"?: string;
|
||||||
/**
|
/**
|
||||||
* The name of the control, which is submitted with the form data.
|
* The name of the control, which is submitted with the form data.
|
||||||
*/
|
*/
|
||||||
@ -7111,6 +7119,14 @@ declare namespace LocalJSX {
|
|||||||
* 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.
|
* 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.
|
||||||
*/
|
*/
|
||||||
"compareWith"?: string | RadioGroupCompareFn | null;
|
"compareWith"?: string | RadioGroupCompareFn | null;
|
||||||
|
/**
|
||||||
|
* The error text to display at the top of the radio group.
|
||||||
|
*/
|
||||||
|
"errorText"?: string;
|
||||||
|
/**
|
||||||
|
* The helper text to display at the top of the radio group.
|
||||||
|
*/
|
||||||
|
"helperText"?: string;
|
||||||
/**
|
/**
|
||||||
* The name of the control, which is submitted with the form data.
|
* The name of the control, which is submitted with the form data.
|
||||||
*/
|
*/
|
||||||
|
12
core/src/components/radio-group/radio-group.ios.scss
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
@import "../../themes/ionic.globals.ios";
|
||||||
|
@import "./radio-group";
|
||||||
|
@import "../item/item.ios.vars";
|
||||||
|
|
||||||
|
// iOS Radio Group Top in List
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
// Add padding to the error and helper text when used in a
|
||||||
|
// list to align them with the list header and item text.
|
||||||
|
ion-list .radio-group-top {
|
||||||
|
@include padding-horizontal($item-ios-padding-start, $item-ios-padding-end);
|
||||||
|
}
|
12
core/src/components/radio-group/radio-group.md.scss
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
@import "../../themes/ionic.globals.md";
|
||||||
|
@import "./radio-group";
|
||||||
|
@import "../item/item.md.vars";
|
||||||
|
|
||||||
|
// Material Design Radio Group Top in List
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
// Add padding to the error and helper text when used in a
|
||||||
|
// list to align them with the list header and item text.
|
||||||
|
ion-list .radio-group-top {
|
||||||
|
@include padding-horizontal($item-md-padding-start, $item-md-padding-end);
|
||||||
|
}
|
44
core/src/components/radio-group/radio-group.scss
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
@import "../../themes/ionic.globals";
|
||||||
|
|
||||||
|
// Radio Group
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
ion-radio-group {
|
||||||
|
// Prevents additional pixels from being rendered on top
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group-wrapper {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Radio Group: Top
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
.radio-group-top {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error text should only be shown when .ion-invalid is present
|
||||||
|
* on the radio group. Otherwise the helper text should be shown.
|
||||||
|
*/
|
||||||
|
.radio-group-top .error-text {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
color: ion-color(danger, base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group-top .helper-text {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
color: $text-color-step-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ion-touched.ion-invalid .radio-group-top .error-text {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ion-touched.ion-invalid .radio-group-top .helper-text {
|
||||||
|
display: none;
|
||||||
|
}
|
@ -8,9 +8,15 @@ import type { RadioGroupChangeEventDetail, RadioGroupCompareFn } from './radio-g
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
tag: 'ion-radio-group',
|
tag: 'ion-radio-group',
|
||||||
|
styleUrls: {
|
||||||
|
ios: 'radio-group.ios.scss',
|
||||||
|
md: 'radio-group.md.scss',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export class RadioGroup implements ComponentInterface {
|
export class RadioGroup implements ComponentInterface {
|
||||||
private inputId = `ion-rg-${radioGroupIds++}`;
|
private inputId = `ion-rg-${radioGroupIds++}`;
|
||||||
|
private helperTextId = `${this.inputId}-helper-text`;
|
||||||
|
private errorTextId = `${this.inputId}-error-text`;
|
||||||
private labelId = `${this.inputId}-lbl`;
|
private labelId = `${this.inputId}-lbl`;
|
||||||
private label?: HTMLIonLabelElement | null;
|
private label?: HTMLIonLabelElement | null;
|
||||||
|
|
||||||
@ -39,6 +45,16 @@ export class RadioGroup implements ComponentInterface {
|
|||||||
*/
|
*/
|
||||||
@Prop({ mutable: true }) value?: any | null;
|
@Prop({ mutable: true }) value?: any | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The helper text to display at the top of the radio group.
|
||||||
|
*/
|
||||||
|
@Prop() helperText?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The error text to display at the top of the radio group.
|
||||||
|
*/
|
||||||
|
@Prop() errorText?: string;
|
||||||
|
|
||||||
@Watch('value')
|
@Watch('value')
|
||||||
valueChanged(value: any | undefined) {
|
valueChanged(value: any | undefined) {
|
||||||
this.setRadioTabindex(value);
|
this.setRadioTabindex(value);
|
||||||
@ -224,13 +240,69 @@ export class RadioGroup implements ComponentInterface {
|
|||||||
radioToFocus?.setFocus();
|
radioToFocus?.setFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the helper text or error text values
|
||||||
|
*/
|
||||||
|
private renderHintText() {
|
||||||
|
const { helperText, errorText, helperTextId, errorTextId } = this;
|
||||||
|
|
||||||
|
const hasHintText = !!helperText || !!errorText;
|
||||||
|
if (!hasHintText) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="radio-group-top">
|
||||||
|
<div id={helperTextId} class="helper-text">
|
||||||
|
{helperText}
|
||||||
|
</div>
|
||||||
|
<div id={errorTextId} class="error-text">
|
||||||
|
{errorText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHintTextID(): string | undefined {
|
||||||
|
const { el, helperText, errorText, helperTextId, errorTextId } = this;
|
||||||
|
|
||||||
|
if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
|
||||||
|
return errorTextId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (helperText) {
|
||||||
|
return helperTextId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { label, labelId, el, name, value } = this;
|
const { label, labelId, el, name, value } = this;
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
|
|
||||||
renderHiddenInput(true, el, name, value, false);
|
renderHiddenInput(true, el, name, value, false);
|
||||||
|
|
||||||
return <Host role="radiogroup" aria-labelledby={label ? labelId : null} onClick={this.onClick} class={mode}></Host>;
|
return (
|
||||||
|
<Host
|
||||||
|
role="radiogroup"
|
||||||
|
aria-labelledby={label ? labelId : null}
|
||||||
|
aria-describedby={this.getHintTextID()}
|
||||||
|
aria-invalid={this.getHintTextID() === this.errorTextId}
|
||||||
|
onClick={this.onClick}
|
||||||
|
class={mode}
|
||||||
|
>
|
||||||
|
{this.renderHintText()}
|
||||||
|
{/*
|
||||||
|
TODO(FW-6279): Wrapping the slot in a div is a workaround due to a
|
||||||
|
Stencil issue. Without the wrapper, the children radio will fire the
|
||||||
|
blur event on focus, instead of waiting for them to be blurred.
|
||||||
|
*/}
|
||||||
|
<div class="radio-group-wrapper">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</Host>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
207
core/src/components/radio-group/test/supporting-text/index.html
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Radio Group - Supporting Text</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>
|
||||||
|
<style>
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
grid-gap: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
|
||||||
|
color: #6f7378;
|
||||||
|
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
ion-radio {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.custom .helper-text {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
.custom .error-text {
|
||||||
|
color: purple;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<ion-app>
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>Radio Group - Supporting Text</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
|
||||||
|
<ion-content id="content" class="ion-padding">
|
||||||
|
<div class="grid">
|
||||||
|
<div class="grid-item">
|
||||||
|
<h2>No Supporting Text</h2>
|
||||||
|
<ion-radio-group value="1">
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
<ion-radio value="4">Label</ion-radio>
|
||||||
|
<ion-radio value="5">Label</ion-radio>
|
||||||
|
</ion-radio-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-item">
|
||||||
|
<h2>Helper Text</h2>
|
||||||
|
<ion-radio-group value="1" helper-text="Helper text" error-text="Error text">
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
<ion-radio value="4">Label</ion-radio>
|
||||||
|
<ion-radio value="5">Label</ion-radio>
|
||||||
|
</ion-radio-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-item">
|
||||||
|
<h2>Error Text</h2>
|
||||||
|
<ion-radio-group
|
||||||
|
value="1"
|
||||||
|
helper-text="Helper text"
|
||||||
|
error-text="Error text"
|
||||||
|
class="ion-invalid ion-touched"
|
||||||
|
>
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
<ion-radio value="4">Label</ion-radio>
|
||||||
|
<ion-radio value="5">Label</ion-radio>
|
||||||
|
</ion-radio-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-item">
|
||||||
|
<ion-list>
|
||||||
|
<ion-list-header>List</ion-list-header>
|
||||||
|
<ion-radio-group value="1">
|
||||||
|
<ion-item>
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-radio value="4">Label</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-radio value="5">Label</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
</ion-list>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-item">
|
||||||
|
<ion-list>
|
||||||
|
<ion-list-header>Supporting Text: List</ion-list-header>
|
||||||
|
<ion-radio-group value="1" helper-text="Helper text" error-text="Error text">
|
||||||
|
<ion-item>
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-radio value="4">Label</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-radio value="5">Label</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
</ion-list>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-item">
|
||||||
|
<ion-list>
|
||||||
|
<ion-list-header>Error Text: List</ion-list-header>
|
||||||
|
<ion-radio-group
|
||||||
|
value="1"
|
||||||
|
helper-text="Helper text"
|
||||||
|
error-text="Error text"
|
||||||
|
class="ion-invalid ion-touched"
|
||||||
|
>
|
||||||
|
<ion-item>
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-radio value="4">Label</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-radio value="5">Label</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
</ion-list>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-item">
|
||||||
|
<h2>Helper Text: Custom</h2>
|
||||||
|
<ion-radio-group value="1" helper-text="Helper text" error-text="Error text" class="custom">
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
<ion-radio value="4">Label</ion-radio>
|
||||||
|
<ion-radio value="5">Label</ion-radio>
|
||||||
|
</ion-radio-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-item">
|
||||||
|
<h2>Error Text: Custom</h2>
|
||||||
|
<ion-radio-group
|
||||||
|
value="1"
|
||||||
|
helper-text="Helper text"
|
||||||
|
error-text="Error text"
|
||||||
|
class="custom ion-invalid ion-touched"
|
||||||
|
>
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
<ion-radio value="4">Label</ion-radio>
|
||||||
|
<ion-radio value="5">Label</ion-radio>
|
||||||
|
</ion-radio-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onclick="toggleValid()" class="expand">Toggle error</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const radioGroups = document.querySelectorAll('ion-radio-group[helper-text]');
|
||||||
|
|
||||||
|
function toggleValid() {
|
||||||
|
radioGroups.forEach((radioGroup) => {
|
||||||
|
radioGroup.classList.toggle('ion-invalid');
|
||||||
|
radioGroup.classList.toggle('ion-touched');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</ion-content>
|
||||||
|
</ion-app>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,247 @@
|
|||||||
|
import { expect } from '@playwright/test';
|
||||||
|
import { configs, test } from '@utils/test/playwright';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functionality is the same across modes & directions
|
||||||
|
*/
|
||||||
|
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||||
|
test.describe(title('radio group: supporting text functionality'), () => {
|
||||||
|
test('should not render top content if no hint is enabled', async ({ page }) => {
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<ion-radio-group value="1">
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
</ion-radio-group>
|
||||||
|
`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const bottomEl = page.locator('ion-radio-group .radio-group-top');
|
||||||
|
await expect(bottomEl).toHaveCount(0);
|
||||||
|
});
|
||||||
|
test('helper text should be visible initially', async ({ page }) => {
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<ion-radio-group value="1" helper-text="Helper text" error-text="Error text">
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
</ion-radio-group>
|
||||||
|
`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const helperText = page.locator('ion-radio-group .helper-text');
|
||||||
|
const errorText = page.locator('ion-radio-group .error-text');
|
||||||
|
await expect(helperText).toBeVisible();
|
||||||
|
await expect(helperText).toHaveText('Helper text');
|
||||||
|
await expect(errorText).toBeHidden();
|
||||||
|
});
|
||||||
|
test('radio group should have an aria-describedby attribute when helper text is present', async ({ page }) => {
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<ion-radio-group value="1" helper-text="Helper text" error-text="Error text">
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
</ion-radio-group>
|
||||||
|
`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const radioGroup = page.locator('ion-radio-group');
|
||||||
|
const helperText = page.locator('ion-radio-group .helper-text');
|
||||||
|
const helperTextId = await helperText.getAttribute('id');
|
||||||
|
const ariaDescribedBy = await radioGroup.getAttribute('aria-describedby');
|
||||||
|
|
||||||
|
expect(ariaDescribedBy).toBe(helperTextId);
|
||||||
|
});
|
||||||
|
test('error text should be visible when radio group is invalid', async ({ page }) => {
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<ion-radio-group value="1" class="ion-invalid ion-touched" helper-text="Helper text" error-text="Error text">
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
</ion-radio-group>
|
||||||
|
`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const helperText = page.locator('ion-radio-group .helper-text');
|
||||||
|
const errorText = page.locator('ion-radio-group .error-text');
|
||||||
|
await expect(helperText).toBeHidden();
|
||||||
|
await expect(errorText).toBeVisible();
|
||||||
|
await expect(errorText).toHaveText('Error text');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('radio group should have an aria-describedby attribute when error text is present', async ({ page }) => {
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<ion-radio-group value="1" class="ion-invalid ion-touched" helper-text="Helper text" error-text="Error text">
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
</ion-radio-group>
|
||||||
|
`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const radioGroup = page.locator('ion-radio-group');
|
||||||
|
const errorText = page.locator('ion-radio-group .error-text');
|
||||||
|
const errorTextId = await errorText.getAttribute('id');
|
||||||
|
const ariaDescribedBy = await radioGroup.getAttribute('aria-describedby');
|
||||||
|
|
||||||
|
expect(ariaDescribedBy).toBe(errorTextId);
|
||||||
|
});
|
||||||
|
test('radio group should have aria-invalid attribute when radio group is invalid', async ({ page }) => {
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<ion-radio-group value="1" class="ion-invalid ion-touched" helper-text="Helper text" error-text="Error text">
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
</ion-radio-group>
|
||||||
|
`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const radioGroup = page.locator('ion-radio-group');
|
||||||
|
|
||||||
|
await expect(radioGroup).toHaveAttribute('aria-invalid');
|
||||||
|
});
|
||||||
|
test('radio group should not have aria-invalid attribute when radio group is valid', async ({ page }) => {
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<ion-radio-group value="1" helper-text="Helper text" error-text="Error text">
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
</ion-radio-group>
|
||||||
|
`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const radioGroup = page.locator('ion-radio-group');
|
||||||
|
|
||||||
|
await expect(radioGroup).not.toHaveAttribute('aria-invalid');
|
||||||
|
});
|
||||||
|
test('radio group should not have aria-describedby attribute when no hint or error text is present', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<ion-radio-group value="1">
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
</ion-radio-group>
|
||||||
|
`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const radioGroup = page.locator('ion-radio-group');
|
||||||
|
|
||||||
|
await expect(radioGroup).not.toHaveAttribute('aria-describedby');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendering is different across modes
|
||||||
|
*/
|
||||||
|
configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||||
|
test.describe(title('radio-group: supporting text rendering'), () => {
|
||||||
|
test('should not have visual regressions when rendering helper text', async ({ page }) => {
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<ion-radio-group value="1" helper-text="Helper text">
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
</ion-radio-group>
|
||||||
|
`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const radioGroup = page.locator('ion-radio-group');
|
||||||
|
await expect(radioGroup).toHaveScreenshot(screenshot(`radio-group-helper-text`));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not have visual regressions when rendering error text', async ({ page }) => {
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<ion-radio-group value="1" class="ion-invalid ion-touched" error-text="Error text">
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
</ion-radio-group>
|
||||||
|
`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const radioGroup = page.locator('ion-radio-group');
|
||||||
|
await expect(radioGroup).toHaveScreenshot(screenshot(`radio-group-error-text`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customizing supporting text is the same across modes and directions
|
||||||
|
*/
|
||||||
|
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||||
|
test.describe(title('radio group: supporting text customization'), () => {
|
||||||
|
test('should not have visual regressions when rendering helper text with custom css', async ({ page }) => {
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<style>
|
||||||
|
.radio-group-top {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group-top .helper-text {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<ion-radio-group value="1" helper-text="Helper text">
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
</ion-radio-group>
|
||||||
|
`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const radioGroup = page.locator('ion-radio-group');
|
||||||
|
await expect(radioGroup).toHaveScreenshot(screenshot(`radio-group-helper-text-custom-css`));
|
||||||
|
});
|
||||||
|
test('should not have visual regressions when rendering error text with custom css', async ({ page }) => {
|
||||||
|
await page.setContent(
|
||||||
|
`
|
||||||
|
<style>
|
||||||
|
.radio-group-top {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group-top .error-text {
|
||||||
|
color: purple;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<ion-radio-group value="1" class="ion-invalid ion-touched" error-text="Error text">
|
||||||
|
<ion-radio value="1">Label</ion-radio>
|
||||||
|
<ion-radio value="2">Label</ion-radio>
|
||||||
|
<ion-radio value="3">Label</ion-radio>
|
||||||
|
</ion-radio-group>
|
||||||
|
`,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
|
||||||
|
const radioGroup = page.locator('ion-radio-group');
|
||||||
|
await expect(radioGroup).toHaveScreenshot(screenshot(`radio-group-error-text-custom-css`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5.8 KiB |
@ -52,6 +52,13 @@ export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDes
|
|||||||
if (el === this.elementRef.nativeElement) {
|
if (el === this.elementRef.nativeElement) {
|
||||||
this.onTouched();
|
this.onTouched();
|
||||||
setIonicClasses(this.elementRef);
|
setIonicClasses(this.elementRef);
|
||||||
|
|
||||||
|
// When ion-radio is blurred, el and this.elementRef.nativeElement are
|
||||||
|
// different so we need to check if the closest ion-radio-group is the same
|
||||||
|
// as this.elementRef.nativeElement and if so, we need to mark the radio group
|
||||||
|
// as touched
|
||||||
|
} else if (el.closest('ion-radio-group') === this.elementRef.nativeElement) {
|
||||||
|
this.onTouched();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1639,14 +1639,14 @@ export declare interface IonRadio extends Components.IonRadio {
|
|||||||
|
|
||||||
|
|
||||||
@ProxyCmp({
|
@ProxyCmp({
|
||||||
inputs: ['allowEmptySelection', 'compareWith', 'name', 'value']
|
inputs: ['allowEmptySelection', 'compareWith', 'errorText', 'helperText', 'name', 'value']
|
||||||
})
|
})
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ion-radio-group',
|
selector: 'ion-radio-group',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: '<ng-content></ng-content>',
|
template: '<ng-content></ng-content>',
|
||||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||||
inputs: ['allowEmptySelection', 'compareWith', 'name', 'value'],
|
inputs: ['allowEmptySelection', 'compareWith', 'errorText', 'helperText', 'name', 'value'],
|
||||||
})
|
})
|
||||||
export class IonRadioGroup {
|
export class IonRadioGroup {
|
||||||
protected el: HTMLIonRadioGroupElement;
|
protected el: HTMLIonRadioGroupElement;
|
||||||
|
@ -16,7 +16,7 @@ import { defineCustomElement } from '@ionic/core/components/ion-radio-group.js';
|
|||||||
|
|
||||||
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';
|
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';
|
||||||
|
|
||||||
const RADIO_GROUP_INPUTS = ['allowEmptySelection', 'compareWith', 'name', 'value'];
|
const RADIO_GROUP_INPUTS = ['allowEmptySelection', 'compareWith', 'errorText', 'helperText', 'name', 'value'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pulling the provider into an object and using PURE works
|
* Pulling the provider into an object and using PURE works
|
||||||
|
@ -698,6 +698,8 @@ export const IonRadioGroup = /*@__PURE__*/ defineContainer<JSX.IonRadioGroup, JS
|
|||||||
'compareWith',
|
'compareWith',
|
||||||
'name',
|
'name',
|
||||||
'value',
|
'value',
|
||||||
|
'helperText',
|
||||||
|
'errorText',
|
||||||
'ionChange',
|
'ionChange',
|
||||||
'ionValueChange'
|
'ionValueChange'
|
||||||
], [
|
], [
|
||||||
|