diff --git a/core/api.txt b/core/api.txt index 6679bd89aa..6a5f7813d3 100644 --- a/core/api.txt +++ b/core/api.txt @@ -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 | 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 diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 1bdfaa8854..7c26acbc2c 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -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`. */ diff --git a/core/src/components/radio/radio.scss b/core/src/components/radio/radio.scss index 1457b8331c..1eeaf9f568 100644 --- a/core/src/components/radio/radio.scss +++ b/core/src/components/radio/radio.scss @@ -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 { diff --git a/core/src/components/radio/radio.tsx b/core/src/components/radio/radio.tsx index bb343c8c85..910b33c0b9 100644 --- a/core/src/components/radio/radio.tsx +++ b/core/src/components/radio/radio.tsx @@ -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 ( +
+
+ {helperText} +
+
+ {errorText} +
+
+ ); + } + 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} > diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index 675c37bd1c..1bab4a5208 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -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: '', // 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; diff --git a/packages/angular/standalone/src/directives/proxies.ts b/packages/angular/standalone/src/directives/proxies.ts index a30385ead5..28b5ff22f1 100644 --- a/packages/angular/standalone/src/directives/proxies.ts +++ b/packages/angular/standalone/src/directives/proxies.ts @@ -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: '', // 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 { diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 7dd5812ebb..a2dc731d14 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -612,6 +612,8 @@ export const IonRadio = /*@__PURE__*/ defineContainer