Skip to content

Commit 4a312e8

Browse files
committed
feat(radio): add helperText and errorText properties
1 parent 45705b2 commit 4a312e8

File tree

7 files changed

+136
-4
lines changed

7 files changed

+136
-4
lines changed

core/api.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1323,6 +1323,8 @@ ion-radio,shadow
13231323
ion-radio,prop,alignment,"center" | "start" | undefined,undefined,false,false
13241324
ion-radio,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
13251325
ion-radio,prop,disabled,boolean,false,false,false
1326+
ion-radio,prop,errorText,string | undefined,undefined,false,false
1327+
ion-radio,prop,helperText,string | undefined,undefined,false,false
13261328
ion-radio,prop,justify,"end" | "space-between" | "start" | undefined,undefined,false,false
13271329
ion-radio,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
13281330
ion-radio,prop,mode,"ios" | "md",undefined,false,false
@@ -1339,8 +1341,11 @@ ion-radio,css-prop,--color-checked,md
13391341
ion-radio,css-prop,--inner-border-radius,ios
13401342
ion-radio,css-prop,--inner-border-radius,md
13411343
ion-radio,part,container
1344+
ion-radio,part,error-text
1345+
ion-radio,part,helper-text
13421346
ion-radio,part,label
13431347
ion-radio,part,mark
1348+
ion-radio,part,supporting-text
13441349

13451350
ion-radio-group,none
13461351
ion-radio-group,prop,allowEmptySelection,boolean,false,false,false

core/src/components.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2267,6 +2267,14 @@ export namespace Components {
22672267
* If `true`, the user cannot interact with the radio.
22682268
*/
22692269
"disabled": boolean;
2270+
/**
2271+
* Text that is placed under the radio and displayed when an error is detected.
2272+
*/
2273+
"errorText"?: string;
2274+
/**
2275+
* Text that is placed under the radio and displayed when no error is detected.
2276+
*/
2277+
"helperText"?: string;
22702278
/**
22712279
* How to pack the label and radio within a line. `"start"`: The label and radio will appear on the left in LTR and on the right in RTL. `"end"`: The label and radio will appear on the right in LTR and on the left in RTL. `"space-between"`: The label and radio will appear on opposite ends of the line with space between the two elements. Setting this property will change the radio `display` to `block`.
22722280
*/
@@ -7017,6 +7025,14 @@ declare namespace LocalJSX {
70177025
* If `true`, the user cannot interact with the radio.
70187026
*/
70197027
"disabled"?: boolean;
7028+
/**
7029+
* Text that is placed under the radio and displayed when an error is detected.
7030+
*/
7031+
"errorText"?: string;
7032+
/**
7033+
* Text that is placed under the radio and displayed when no error is detected.
7034+
*/
7035+
"helperText"?: string;
70207036
/**
70217037
* How to pack the label and radio within a line. `"start"`: The label and radio will appear on the left in LTR and on the right in RTL. `"end"`: The label and radio will appear on the right in LTR and on the left in RTL. `"space-between"`: The label and radio will appear on opposite ends of the line with space between the two elements. Setting this property will change the radio `display` to `block`.
70227038
*/

core/src/components/radio/radio.scss

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,53 @@ input {
140140
align-items: center;
141141
}
142142

143+
// Radio Bottom Content
144+
// ----------------------------------------------------------------
145+
146+
.radio-bottom {
147+
@include padding(5px, null, null, null);
148+
149+
display: flex;
150+
151+
justify-content: space-between;
152+
153+
font-size: dynamic-font(12px);
154+
155+
white-space: normal;
156+
}
157+
158+
:host(.radio-label-placement-stacked) .radio-bottom {
159+
font-size: dynamic-font(16px);
160+
}
161+
162+
// Radio Hint Text
163+
// ----------------------------------------------------------------
164+
165+
/**
166+
* Error text should only be shown when .ion-invalid is
167+
* present on the checkbox. Otherwise the helper text should
168+
* be shown.
169+
*/
170+
.radio-bottom .error-text {
171+
display: none;
172+
173+
color: ion-color(danger, base);
174+
}
175+
176+
.radio-bottom .helper-text {
177+
display: block;
178+
179+
color: $text-color-step-300;
180+
}
181+
182+
:host(.ion-touched.ion-invalid) .radio-bottom .error-text {
183+
display: block;
184+
}
185+
186+
:host(.ion-touched.ion-invalid) .radio-bottom .helper-text {
187+
display: none;
188+
}
189+
143190
// Radio Label Placement - Start
144191
// ----------------------------------------------------------------
145192

@@ -213,6 +260,8 @@ input {
213260
*/
214261
:host(.radio-label-placement-stacked) .radio-wrapper {
215262
flex-direction: column;
263+
264+
text-align: center;
216265
}
217266

218267
:host(.radio-label-placement-stacked) .label-text-wrapper {

core/src/components/radio/radio.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import type { Color } from '../../interface';
1515
* @part container - The container for the radio mark.
1616
* @part label - The label text describing the radio.
1717
* @part mark - The checkmark or dot used to indicate the checked state.
18+
* @part supporting-text - Supporting text displayed beneath the radio label.
19+
* @part helper-text - Supporting text displayed beneath the radio label when the radio is valid.
20+
* @part error-text - Supporting text displayed beneath the radio label when the radio is invalid and touched.
1821
*/
1922
@Component({
2023
tag: 'ion-radio',
@@ -26,6 +29,8 @@ import type { Color } from '../../interface';
2629
})
2730
export class Radio implements ComponentInterface {
2831
private inputId = `ion-rb-${radioButtonIds++}`;
32+
private helperTextId = `${this.inputId}-helper-text`;
33+
private errorTextId = `${this.inputId}-error-text`;
2934
private radioGroup: HTMLIonRadioGroupElement | null = null;
3035

3136
@Element() el!: HTMLIonRadioElement;
@@ -58,6 +63,16 @@ export class Radio implements ComponentInterface {
5863
*/
5964
@Prop() disabled = false;
6065

66+
/**
67+
* Text that is placed under the radio and displayed when an error is detected.
68+
*/
69+
@Prop() errorText?: string;
70+
71+
/**
72+
* Text that is placed under the radio and displayed when no error is detected.
73+
*/
74+
@Prop() helperText?: string;
75+
6176
/**
6277
* the value of the radio.
6378
*/
@@ -212,6 +227,48 @@ export class Radio implements ComponentInterface {
212227
);
213228
}
214229

230+
private getHintTextID(): string | undefined {
231+
const { el, helperText, errorText, helperTextId, errorTextId } = this;
232+
233+
if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) {
234+
return errorTextId;
235+
}
236+
237+
if (helperText) {
238+
return helperTextId;
239+
}
240+
241+
return undefined;
242+
}
243+
244+
/**
245+
* Responsible for rendering helper text and error text.
246+
* This element should only be rendered if hint text is set.
247+
*/
248+
private renderHintText() {
249+
const { helperText, errorText, helperTextId, errorTextId } = this;
250+
251+
/**
252+
* undefined and empty string values should
253+
* be treated as not having helper/error text.
254+
*/
255+
const hasHintText = !!helperText || !!errorText;
256+
if (!hasHintText) {
257+
return;
258+
}
259+
260+
return (
261+
<div class="radio-bottom">
262+
<div id={helperTextId} class="helper-text" part="supporting-text helper-text">
263+
{helperText}
264+
</div>
265+
<div id={errorTextId} class="error-text" part="supporting-text error-text">
266+
{errorText}
267+
</div>
268+
</div>
269+
);
270+
}
271+
215272
render() {
216273
const { checked, disabled, color, el, justify, labelPlacement, hasLabel, buttonTabindex, alignment } = this;
217274
const mode = getIonMode(this);
@@ -237,6 +294,8 @@ export class Radio implements ComponentInterface {
237294
role="radio"
238295
aria-checked={checked ? 'true' : 'false'}
239296
aria-disabled={disabled ? 'true' : null}
297+
aria-describedby={this.getHintTextID()}
298+
aria-invalid={this.getHintTextID() === this.errorTextId}
240299
tabindex={buttonTabindex}
241300
>
242301
<label class="radio-wrapper">
@@ -248,6 +307,7 @@ export class Radio implements ComponentInterface {
248307
part="label"
249308
>
250309
<slot></slot>
310+
{this.renderHintText()}
251311
</div>
252312
<div class="native-wrapper">{this.renderRadioControl()}</div>
253313
</label>

packages/angular/src/directives/proxies.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1607,14 +1607,14 @@ export declare interface IonProgressBar extends Components.IonProgressBar {}
16071607

16081608

16091609
@ProxyCmp({
1610-
inputs: ['alignment', 'color', 'disabled', 'justify', 'labelPlacement', 'mode', 'name', 'value']
1610+
inputs: ['alignment', 'color', 'disabled', 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', 'value']
16111611
})
16121612
@Component({
16131613
selector: 'ion-radio',
16141614
changeDetection: ChangeDetectionStrategy.OnPush,
16151615
template: '<ng-content></ng-content>',
16161616
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
1617-
inputs: ['alignment', 'color', 'disabled', 'justify', 'labelPlacement', 'mode', 'name', 'value'],
1617+
inputs: ['alignment', 'color', 'disabled', 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', 'value'],
16181618
})
16191619
export class IonRadio {
16201620
protected el: HTMLElement;

packages/angular/standalone/src/directives/proxies.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,14 +1614,14 @@ export declare interface IonProgressBar extends Components.IonProgressBar {}
16141614

16151615
@ProxyCmp({
16161616
defineCustomElementFn: defineIonRadio,
1617-
inputs: ['alignment', 'color', 'disabled', 'justify', 'labelPlacement', 'mode', 'name', 'value']
1617+
inputs: ['alignment', 'color', 'disabled', 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', 'value']
16181618
})
16191619
@Component({
16201620
selector: 'ion-radio',
16211621
changeDetection: ChangeDetectionStrategy.OnPush,
16221622
template: '<ng-content></ng-content>',
16231623
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
1624-
inputs: ['alignment', 'color', 'disabled', 'justify', 'labelPlacement', 'mode', 'name', 'value'],
1624+
inputs: ['alignment', 'color', 'disabled', 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', 'value'],
16251625
standalone: true
16261626
})
16271627
export class IonRadio {

packages/vue/src/proxies.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,8 @@ export const IonRadio = /*@__PURE__*/ defineContainer<JSX.IonRadio, JSX.IonRadio
612612
'color',
613613
'name',
614614
'disabled',
615+
'errorText',
616+
'helperText',
615617
'value',
616618
'labelPlacement',
617619
'justify',

0 commit comments

Comments
 (0)