feat(radio): add helperText and errorText properties

This commit is contained in:
Brandy Smith
2025-01-28 18:22:26 -05:00
parent 45705b2c3f
commit 4a312e837f
7 changed files with 136 additions and 4 deletions

View File

@ -1323,6 +1323,8 @@ ion-radio,shadow
ion-radio,prop,alignment,"center" | "start" | undefined,undefined,false,false
ion-radio,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
ion-radio,prop,disabled,boolean,false,false,false
ion-radio,prop,errorText,string | undefined,undefined,false,false
ion-radio,prop,helperText,string | undefined,undefined,false,false
ion-radio,prop,justify,"end" | "space-between" | "start" | undefined,undefined,false,false
ion-radio,prop,labelPlacement,"end" | "fixed" | "stacked" | "start",'start',false,false
ion-radio,prop,mode,"ios" | "md",undefined,false,false
@ -1339,8 +1341,11 @@ ion-radio,css-prop,--color-checked,md
ion-radio,css-prop,--inner-border-radius,ios
ion-radio,css-prop,--inner-border-radius,md
ion-radio,part,container
ion-radio,part,error-text
ion-radio,part,helper-text
ion-radio,part,label
ion-radio,part,mark
ion-radio,part,supporting-text
ion-radio-group,none
ion-radio-group,prop,allowEmptySelection,boolean,false,false,false

View File

@ -2267,6 +2267,14 @@ export namespace Components {
* If `true`, the user cannot interact with the radio.
*/
"disabled": boolean;
/**
* Text that is placed under the radio and displayed when an error is detected.
*/
"errorText"?: string;
/**
* Text that is placed under the radio and displayed when no error is detected.
*/
"helperText"?: string;
/**
* 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`.
*/
@ -7017,6 +7025,14 @@ declare namespace LocalJSX {
* If `true`, the user cannot interact with the radio.
*/
"disabled"?: boolean;
/**
* Text that is placed under the radio and displayed when an error is detected.
*/
"errorText"?: string;
/**
* Text that is placed under the radio and displayed when no error is detected.
*/
"helperText"?: string;
/**
* 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`.
*/

View File

@ -140,6 +140,53 @@ input {
align-items: center;
}
// Radio Bottom Content
// ----------------------------------------------------------------
.radio-bottom {
@include padding(5px, null, null, null);
display: flex;
justify-content: space-between;
font-size: dynamic-font(12px);
white-space: normal;
}
:host(.radio-label-placement-stacked) .radio-bottom {
font-size: dynamic-font(16px);
}
// Radio Hint Text
// ----------------------------------------------------------------
/**
* Error text should only be shown when .ion-invalid is
* present on the checkbox. Otherwise the helper text should
* be shown.
*/
.radio-bottom .error-text {
display: none;
color: ion-color(danger, base);
}
.radio-bottom .helper-text {
display: block;
color: $text-color-step-300;
}
:host(.ion-touched.ion-invalid) .radio-bottom .error-text {
display: block;
}
:host(.ion-touched.ion-invalid) .radio-bottom .helper-text {
display: none;
}
// Radio Label Placement - Start
// ----------------------------------------------------------------
@ -213,6 +260,8 @@ input {
*/
:host(.radio-label-placement-stacked) .radio-wrapper {
flex-direction: column;
text-align: center;
}
:host(.radio-label-placement-stacked) .label-text-wrapper {

View File

@ -15,6 +15,9 @@ import type { Color } from '../../interface';
* @part container - The container for the radio mark.
* @part label - The label text describing the radio.
* @part mark - The checkmark or dot used to indicate the checked state.
* @part supporting-text - Supporting text displayed beneath the radio label.
* @part helper-text - Supporting text displayed beneath the radio label when the radio is valid.
* @part error-text - Supporting text displayed beneath the radio label when the radio is invalid and touched.
*/
@Component({
tag: 'ion-radio',
@ -26,6 +29,8 @@ import type { Color } from '../../interface';
})
export class Radio implements ComponentInterface {
private inputId = `ion-rb-${radioButtonIds++}`;
private helperTextId = `${this.inputId}-helper-text`;
private errorTextId = `${this.inputId}-error-text`;
private radioGroup: HTMLIonRadioGroupElement | null = null;
@Element() el!: HTMLIonRadioElement;
@ -58,6 +63,16 @@ export class Radio implements ComponentInterface {
*/
@Prop() disabled = false;
/**
* Text that is placed under the radio and displayed when an error is detected.
*/
@Prop() errorText?: string;
/**
* Text that is placed under the radio and displayed when no error is detected.
*/
@Prop() helperText?: string;
/**
* the value of the radio.
*/
@ -212,6 +227,48 @@ export class Radio implements ComponentInterface {
);
}
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;
}
/**
* Responsible for rendering helper text and error text.
* This element should only be rendered if hint text is set.
*/
private renderHintText() {
const { helperText, errorText, helperTextId, errorTextId } = this;
/**
* undefined and empty string values should
* be treated as not having helper/error text.
*/
const hasHintText = !!helperText || !!errorText;
if (!hasHintText) {
return;
}
return (
<div class="radio-bottom">
<div id={helperTextId} class="helper-text" part="supporting-text helper-text">
{helperText}
</div>
<div id={errorTextId} class="error-text" part="supporting-text error-text">
{errorText}
</div>
</div>
);
}
render() {
const { checked, disabled, color, el, justify, labelPlacement, hasLabel, buttonTabindex, alignment } = this;
const mode = getIonMode(this);
@ -237,6 +294,8 @@ export class Radio implements ComponentInterface {
role="radio"
aria-checked={checked ? 'true' : 'false'}
aria-disabled={disabled ? 'true' : null}
aria-describedby={this.getHintTextID()}
aria-invalid={this.getHintTextID() === this.errorTextId}
tabindex={buttonTabindex}
>
<label class="radio-wrapper">
@ -248,6 +307,7 @@ export class Radio implements ComponentInterface {
part="label"
>
<slot></slot>
{this.renderHintText()}
</div>
<div class="native-wrapper">{this.renderRadioControl()}</div>
</label>

View File

@ -1607,14 +1607,14 @@ export declare interface IonProgressBar extends Components.IonProgressBar {}
@ProxyCmp({
inputs: ['alignment', 'color', 'disabled', 'justify', 'labelPlacement', 'mode', 'name', 'value']
inputs: ['alignment', 'color', 'disabled', 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', 'value']
})
@Component({
selector: 'ion-radio',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['alignment', 'color', 'disabled', 'justify', 'labelPlacement', 'mode', 'name', 'value'],
inputs: ['alignment', 'color', 'disabled', 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', 'value'],
})
export class IonRadio {
protected el: HTMLElement;

View File

@ -1614,14 +1614,14 @@ export declare interface IonProgressBar extends Components.IonProgressBar {}
@ProxyCmp({
defineCustomElementFn: defineIonRadio,
inputs: ['alignment', 'color', 'disabled', 'justify', 'labelPlacement', 'mode', 'name', 'value']
inputs: ['alignment', 'color', 'disabled', 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', 'value']
})
@Component({
selector: 'ion-radio',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['alignment', 'color', 'disabled', 'justify', 'labelPlacement', 'mode', 'name', 'value'],
inputs: ['alignment', 'color', 'disabled', 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', 'value'],
standalone: true
})
export class IonRadio {

View File

@ -612,6 +612,8 @@ export const IonRadio = /*@__PURE__*/ defineContainer<JSX.IonRadio, JSX.IonRadio
'color',
'name',
'disabled',
'errorText',
'helperText',
'value',
'labelPlacement',
'justify',