From eaa64fdb7320fb50112d5ecc8c4b74425f921d0f Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 7 Oct 2025 15:30:47 -0700 Subject: [PATCH 01/10] fix(checkbox, select): improve error text accessibility --- core/src/components/checkbox/checkbox.tsx | 46 ++++- .../checkbox/test/validation/index.html | 184 +++++++++++++++++ core/src/components/select/select.tsx | 39 +++- .../select/test/validation/index.html | 190 ++++++++++++++++++ 4 files changed, 443 insertions(+), 16 deletions(-) create mode 100644 core/src/components/checkbox/test/validation/index.html create mode 100644 core/src/components/select/test/validation/index.html diff --git a/core/src/components/checkbox/checkbox.tsx b/core/src/components/checkbox/checkbox.tsx index 00a9de5c7cf..53d1bcd4a03 100644 --- a/core/src/components/checkbox/checkbox.tsx +++ b/core/src/components/checkbox/checkbox.tsx @@ -1,5 +1,5 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Component, Element, Event, Host, Method, Prop, h } from '@stencil/core'; +import { Component, Element, Event, Host, Method, Prop, State, h, forceUpdate } from '@stencil/core'; import type { Attributes } from '@utils/helpers'; import { inheritAriaAttributes, renderHiddenInput } from '@utils/helpers'; import { createColorClasses, hostContext } from '@utils/theme'; @@ -121,6 +121,11 @@ export class Checkbox implements ComponentInterface { */ @Prop() required = false; + /** + * Track validation state for proper aria-live announcements. + */ + @State() isInvalid = false; + /** * Emitted when the checked property has changed as a result of a user action such as a click. * @@ -138,6 +143,11 @@ export class Checkbox implements ComponentInterface { */ @Event() ionBlur!: EventEmitter; + connectedCallback() { + // Always set initial state. + this.isInvalid = this.checkInvalidState(); + } + componentWillLoad() { this.inheritedAttributes = { ...inheritAriaAttributes(this.el), @@ -179,6 +189,13 @@ export class Checkbox implements ComponentInterface { }; private onBlur = () => { + const newIsInvalid = this.checkInvalidState(); + if (this.isInvalid !== newIsInvalid) { + this.isInvalid = newIsInvalid; + // Force a re-render to update aria-describedby immediately. + forceUpdate(this); + } + this.ionBlur.emit(); }; @@ -208,9 +225,9 @@ export class Checkbox implements ComponentInterface { }; private getHintTextID(): string | undefined { - const { el, helperText, errorText, helperTextId, errorTextId } = this; + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; - if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) { + if (isInvalid && errorText) { return errorTextId; } @@ -226,7 +243,7 @@ export class Checkbox implements ComponentInterface { * This element should only be rendered if hint text is set. */ private renderHintText() { - const { helperText, errorText, helperTextId, errorTextId } = this; + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; /** * undefined and empty string values should @@ -239,16 +256,26 @@ export class Checkbox implements ComponentInterface { return (
-
- {helperText} +
+ {!isInvalid ? helperText : null}
-
- {errorText} +
); } + /** + * Checks if the input is in an invalid state based on Ionic validation classes + */ + private checkInvalidState(): boolean { + const hasIonTouched = this.el.classList.contains('ion-touched'); + const hasIonInvalid = this.el.classList.contains('ion-invalid'); + + return hasIonTouched && hasIonInvalid; + } + render() { const { color, @@ -279,10 +306,11 @@ export class Checkbox implements ComponentInterface { role="checkbox" aria-checked={indeterminate ? 'mixed' : `${checked}`} aria-describedby={this.getHintTextID()} - aria-invalid={this.getHintTextID() === this.errorTextId} + aria-invalid={this.isInvalid ? 'true' : undefined} aria-labelledby={hasLabelContent ? this.inputLabelId : null} aria-label={inheritedAttributes['aria-label'] || null} aria-disabled={disabled ? 'true' : null} + aria-required={required ? 'true' : undefined} tabindex={disabled ? undefined : 0} onKeyDown={this.onKeyDown} class={createColorClasses(color, { diff --git a/core/src/components/checkbox/test/validation/index.html b/core/src/components/checkbox/test/validation/index.html new file mode 100644 index 00000000000..8686186c7de --- /dev/null +++ b/core/src/components/checkbox/test/validation/index.html @@ -0,0 +1,184 @@ + + + + + Checkbox - Validation + + + + + + + + + + + + + + Checkbox - Validation Test + + + + +
+

Screen Reader Testing Instructions:

+
    +
  1. Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
  2. +
  3. Tab through the form fields
  4. +
  5. When you tab away from an empty required field, the error should be announced immediately
  6. +
  7. The error text should be announced BEFORE the next field is announced
  8. +
  9. Test in Chrome, Safari, and Firefox to verify consistent behavior
  10. +
+
+ +
+
+

Required Field

+ I agree to the terms and conditions +
+ +
+

Optional Field (No Validation)

+ Optional Checkbox +
+
+ +
+ Submit Form + Reset Form +
+
+
+ + + + diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index 7df8049d13a..19f69fae77a 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -81,6 +81,11 @@ export class Select implements ComponentInterface { */ @State() hasFocus = false; + /** + * Track validation state for proper aria-live announcements. + */ + @State() isInvalid = false; + /** * The text to display on the cancel button. */ @@ -298,6 +303,9 @@ export class Select implements ComponentInterface { */ forceUpdate(this); }); + + // Always set initial state. + this.isInvalid = this.checkInvalidState(); } componentWillLoad() { @@ -868,8 +876,15 @@ export class Select implements ComponentInterface { }; private onBlur = () => { + const newIsInvalid = this.checkInvalidState(); this.hasFocus = false; + if (this.isInvalid !== newIsInvalid) { + this.isInvalid = newIsInvalid; + // Force a re-render to update aria-describedby immediately. + forceUpdate(this); + } + this.ionBlur.emit(); }; @@ -1067,9 +1082,9 @@ export class Select implements ComponentInterface { } private getHintTextID(): string | undefined { - const { el, helperText, errorText, helperTextId, errorTextId } = this; + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; - if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) { + if (isInvalid && errorText) { return errorTextId; } @@ -1084,14 +1099,14 @@ export class Select implements ComponentInterface { * Renders the helper text or error text values */ private renderHintText() { - const { helperText, errorText, helperTextId, errorTextId } = this; + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; return [ -
- {helperText} +
+ {isInvalid ? helperText : null}
, -
- {errorText} + , ]; } @@ -1115,6 +1130,16 @@ export class Select implements ComponentInterface { return
{this.renderHintText()}
; } + /** + * Checks if the input is in an invalid state based on Ionic validation classes + */ + private checkInvalidState(): boolean { + const hasIonTouched = this.el.classList.contains('ion-touched'); + const hasIonInvalid = this.el.classList.contains('ion-invalid'); + + return hasIonTouched && hasIonInvalid; + } + render() { const { disabled, diff --git a/core/src/components/select/test/validation/index.html b/core/src/components/select/test/validation/index.html new file mode 100644 index 00000000000..f955e0067b6 --- /dev/null +++ b/core/src/components/select/test/validation/index.html @@ -0,0 +1,190 @@ + + + + + Select - Validation + + + + + + + + + + + + + + Select - Validation Test + + + + +
+

Screen Reader Testing Instructions:

+
    +
  1. Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
  2. +
  3. Tab through the form fields
  4. +
  5. When you tab away from an empty required field, the error should be announced immediately
  6. +
  7. The error text should be announced BEFORE the next field is announced
  8. +
  9. Test in Chrome, Safari, and Firefox to verify consistent behavior
  10. +
+
+ +
+
+

Required Field

+ + Apples + Oranges + Pears + +
+ +
+

Optional Field (No Validation)

+ Optional Checkbox +
+
+ +
+ Submit Form + Reset Form +
+
+
+ + + + From ec4eb8344ebdac0e11755b37d6e8efa9ecf8da17 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Thu, 30 Oct 2025 16:17:34 -0700 Subject: [PATCH 02/10] refactor(checkbox): use promise --- core/src/components/checkbox/checkbox.tsx | 56 +++++++++++++++++++++-- core/src/utils/forms/validity.ts | 2 +- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/core/src/components/checkbox/checkbox.tsx b/core/src/components/checkbox/checkbox.tsx index c47b69ac81d..67927944b51 100644 --- a/core/src/components/checkbox/checkbox.tsx +++ b/core/src/components/checkbox/checkbox.tsx @@ -1,5 +1,6 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Component, Element, Event, Host, Method, Prop, State, h, forceUpdate } from '@stencil/core'; +import { Component, Element, Event, Host, Method, Prop, State, h, forceUpdate, Build } from '@stencil/core'; +import { checkInvalidState } from '@utils/forms'; import type { Attributes } from '@utils/helpers'; import { inheritAriaAttributes, renderHiddenInput } from '@utils/helpers'; import { createColorClasses, hostContext } from '@utils/theme'; @@ -35,6 +36,7 @@ export class Checkbox implements ComponentInterface { private helperTextId = `${this.inputId}-helper-text`; private errorTextId = `${this.inputId}-error-text`; private inheritedAttributes: Attributes = {}; + private validationObserver?: MutationObserver; @Element() el!: HTMLIonCheckboxElement; @@ -125,6 +127,8 @@ export class Checkbox implements ComponentInterface { */ @State() isInvalid = false; + @State() private hintTextID?: string; + /** * Emitted when the checked property has changed as a result of a user action such as a click. * @@ -143,7 +147,45 @@ export class Checkbox implements ComponentInterface { @Event() ionBlur!: EventEmitter; connectedCallback() { - // Always set initial state. + const { el } = this; + + // Watch for class changes to update validation state. + if (Build.isBrowser && typeof MutationObserver !== 'undefined') { + this.validationObserver = new MutationObserver(() => { + const newIsInvalid = checkInvalidState(el); + if (this.isInvalid !== newIsInvalid) { + this.isInvalid = newIsInvalid; + /** + * Screen readers tend to announce changes + * to `aria-describedby` when the attribute + * is changed during a blur event for a + * native form control. + * However, the announcement can be spotty + * when using a non-native form control + * and `forceUpdate()`. + * This is due to `forceUpdate()` internally + * rescheduling the DOM update to a lower + * priority queue regardless if it's called + * inside a Promise or not, thus causing + * the screen reader to potentially miss the + * change. + * By using a State variable inside a Promise, + * it guarantees a re-render immediately at + * a higher priority. + */ + Promise.resolve().then(() => { + this.hintTextID = this.getHintTextID(); + }); + } + }); + + this.validationObserver.observe(el, { + attributes: true, + attributeFilter: ['class'], + }); + } + + // Always set initial state this.isInvalid = this.checkInvalidState(); } @@ -153,6 +195,14 @@ export class Checkbox implements ComponentInterface { }; } + disconnectedCallback() { + // Clean up validation observer to prevent memory leaks. + if (this.validationObserver) { + this.validationObserver.disconnect(); + this.validationObserver = undefined; + } + } + /** @internal */ @Method() async setFocus() { @@ -301,7 +351,7 @@ export class Checkbox implements ComponentInterface { Date: Mon, 3 Nov 2025 08:57:39 -0800 Subject: [PATCH 03/10] refactor(checkbox): cleanup --- core/src/components/checkbox/checkbox.tsx | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/core/src/components/checkbox/checkbox.tsx b/core/src/components/checkbox/checkbox.tsx index 67927944b51..2059f1e7479 100644 --- a/core/src/components/checkbox/checkbox.tsx +++ b/core/src/components/checkbox/checkbox.tsx @@ -1,5 +1,5 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Component, Element, Event, Host, Method, Prop, State, h, forceUpdate, Build } from '@stencil/core'; +import { Component, Element, Event, Host, Method, Prop, State, h, Build } from '@stencil/core'; import { checkInvalidState } from '@utils/forms'; import type { Attributes } from '@utils/helpers'; import { inheritAriaAttributes, renderHiddenInput } from '@utils/helpers'; @@ -186,7 +186,7 @@ export class Checkbox implements ComponentInterface { } // Always set initial state - this.isInvalid = this.checkInvalidState(); + this.isInvalid = checkInvalidState(el); } componentWillLoad() { @@ -235,13 +235,6 @@ export class Checkbox implements ComponentInterface { }; private onBlur = () => { - const newIsInvalid = this.checkInvalidState(); - if (this.isInvalid !== newIsInvalid) { - this.isInvalid = newIsInvalid; - // Force a re-render to update aria-describedby immediately. - forceUpdate(this); - } - this.ionBlur.emit(); }; @@ -312,16 +305,6 @@ export class Checkbox implements ComponentInterface { ); } - /** - * Checks if the input is in an invalid state based on Ionic validation classes - */ - private checkInvalidState(): boolean { - const hasIonTouched = this.el.classList.contains('ion-touched'); - const hasIonInvalid = this.el.classList.contains('ion-invalid'); - - return hasIonTouched && hasIonInvalid; - } - render() { const { color, From f5088213f1d8cc8c3fac6bdecff44e441ad1c870 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 3 Nov 2025 09:28:49 -0800 Subject: [PATCH 04/10] fix(toggle): improve error text accessibility --- core/src/components/checkbox/checkbox.tsx | 2 +- .../toggle/test/validation/index.html | 184 ++++++++++++++++++ core/src/components/toggle/toggle.tsx | 80 ++++++-- core/src/utils/forms/validity.ts | 7 +- 4 files changed, 259 insertions(+), 14 deletions(-) create mode 100644 core/src/components/toggle/test/validation/index.html diff --git a/core/src/components/checkbox/checkbox.tsx b/core/src/components/checkbox/checkbox.tsx index 2059f1e7479..506f6c8e245 100644 --- a/core/src/components/checkbox/checkbox.tsx +++ b/core/src/components/checkbox/checkbox.tsx @@ -1,5 +1,5 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Component, Element, Event, Host, Method, Prop, State, h, Build } from '@stencil/core'; +import { Build, Component, Element, Event, Host, Method, Prop, State, h } from '@stencil/core'; import { checkInvalidState } from '@utils/forms'; import type { Attributes } from '@utils/helpers'; import { inheritAriaAttributes, renderHiddenInput } from '@utils/helpers'; diff --git a/core/src/components/toggle/test/validation/index.html b/core/src/components/toggle/test/validation/index.html new file mode 100644 index 00000000000..bcc8dbadfc3 --- /dev/null +++ b/core/src/components/toggle/test/validation/index.html @@ -0,0 +1,184 @@ + + + + + Toggle - Validation + + + + + + + + + + + + + + Toggle - Validation Test + + + + +
+

Screen Reader Testing Instructions:

+
    +
  1. Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
  2. +
  3. Tab through the form fields
  4. +
  5. When you tab away from an empty required field, the error should be announced immediately
  6. +
  7. The error text should be announced BEFORE the next field is announced
  8. +
  9. Test in Chrome, Safari, and Firefox to verify consistent behavior
  10. +
+
+ +
+
+

Required Field

+ Tap to turn on +
+ +
+

Optional Field (No Validation)

+ Optional Toggle +
+
+ +
+ Submit Form + Reset Form +
+
+
+ + + + diff --git a/core/src/components/toggle/toggle.tsx b/core/src/components/toggle/toggle.tsx index 18f4b26115c..826da56686b 100644 --- a/core/src/components/toggle/toggle.tsx +++ b/core/src/components/toggle/toggle.tsx @@ -1,5 +1,6 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core'; +import { Build, Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core'; +import { checkInvalidState } from '@utils/forms'; import { renderHiddenInput, inheritAriaAttributes } from '@utils/helpers'; import type { Attributes } from '@utils/helpers'; import { hapticSelection } from '@utils/native/haptic'; @@ -44,11 +45,19 @@ export class Toggle implements ComponentInterface { private inheritedAttributes: Attributes = {}; private toggleTrack?: HTMLElement; private didLoad = false; + private validationObserver?: MutationObserver; @Element() el!: HTMLIonToggleElement; @State() activated = false; + /** + * Track validation state for proper aria-live announcements. + */ + @State() isInvalid = false; + + @State() private hintTextID?: string; + /** * The color to use from your application's color palette. * Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. @@ -168,15 +177,56 @@ export class Toggle implements ComponentInterface { } async connectedCallback() { + const { didLoad, el } = this; + /** * If we have not yet rendered * ion-toggle, then toggleTrack is not defined. * But if we are moving ion-toggle via appendChild, * then toggleTrack will be defined. */ - if (this.didLoad) { + if (didLoad) { this.setupGesture(); } + + // Watch for class changes to update validation state. + if (Build.isBrowser && typeof MutationObserver !== 'undefined') { + this.validationObserver = new MutationObserver(() => { + const newIsInvalid = checkInvalidState(el); + if (this.isInvalid !== newIsInvalid) { + this.isInvalid = newIsInvalid; + /** + * Screen readers tend to announce changes + * to `aria-describedby` when the attribute + * is changed during a blur event for a + * native form control. + * However, the announcement can be spotty + * when using a non-native form control + * and `forceUpdate()`. + * This is due to `forceUpdate()` internally + * rescheduling the DOM update to a lower + * priority queue regardless if it's called + * inside a Promise or not, thus causing + * the screen reader to potentially miss the + * change. + * By using a State variable inside a Promise, + * it guarantees a re-render immediately at + * a higher priority. + */ + Promise.resolve().then(() => { + this.hintTextID = this.getHintTextID(); + }); + } + }); + + this.validationObserver.observe(el, { + attributes: true, + attributeFilter: ['class'], + }); + } + + // Always set initial state + this.isInvalid = checkInvalidState(el); } componentDidLoad() { @@ -207,6 +257,12 @@ export class Toggle implements ComponentInterface { this.gesture.destroy(); this.gesture = undefined; } + + // Clean up validation observer to prevent memory leaks. + if (this.validationObserver) { + this.validationObserver.disconnect(); + this.validationObserver = undefined; + } } componentWillLoad() { @@ -336,9 +392,9 @@ export class Toggle implements ComponentInterface { } private getHintTextID(): string | undefined { - const { el, helperText, errorText, helperTextId, errorTextId } = this; + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; - if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) { + if (isInvalid && errorText) { return errorTextId; } @@ -354,7 +410,7 @@ export class Toggle implements ComponentInterface { * This element should only be rendered if hint text is set. */ private renderHintText() { - const { helperText, errorText, helperTextId, errorTextId } = this; + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; /** * undefined and empty string values should @@ -367,11 +423,11 @@ export class Toggle implements ComponentInterface { return (
-
- {helperText} +
+ {!isInvalid ? helperText : null}
-
- {errorText} +
); @@ -385,7 +441,6 @@ export class Toggle implements ComponentInterface { color, disabled, el, - errorTextId, hasLabel, inheritedAttributes, inputId, @@ -405,12 +460,13 @@ export class Toggle implements ComponentInterface { Date: Mon, 3 Nov 2025 11:57:28 -0800 Subject: [PATCH 05/10] fix(radio-group): improve error text accessibility --- .../components/radio-group/radio-group.tsx | 76 ++++++- .../radio-group/test/validation/index.html | 195 ++++++++++++++++++ core/src/utils/forms/validity.ts | 3 +- 3 files changed, 263 insertions(+), 11 deletions(-) create mode 100644 core/src/components/radio-group/test/validation/index.html diff --git a/core/src/components/radio-group/radio-group.tsx b/core/src/components/radio-group/radio-group.tsx index c3e1e4c0b0e..b9a059acd53 100644 --- a/core/src/components/radio-group/radio-group.tsx +++ b/core/src/components/radio-group/radio-group.tsx @@ -1,5 +1,6 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Component, Element, Event, Host, Listen, Method, Prop, Watch, h } from '@stencil/core'; +import { Build, Component, Element, Event, Host, Listen, Method, Prop, State, Watch, h } from '@stencil/core'; +import { checkInvalidState } from '@utils/forms'; import { renderHiddenInput } from '@utils/helpers'; import { getIonMode } from '../../global/ionic-global'; @@ -19,9 +20,17 @@ export class RadioGroup implements ComponentInterface { private errorTextId = `${this.inputId}-error-text`; private labelId = `${this.inputId}-lbl`; private label?: HTMLIonLabelElement | null; + private validationObserver?: MutationObserver; @Element() el!: HTMLElement; + /** + * Track validation state for proper aria-live announcements. + */ + @State() isInvalid = false; + + @State() private hintTextID?: string; + /** * If `true`, the radios can be deselected. */ @@ -121,6 +130,53 @@ export class RadioGroup implements ComponentInterface { this.labelId = label.id = this.name + '-lbl'; } } + + // Watch for class changes to update validation state. + if (Build.isBrowser && typeof MutationObserver !== 'undefined') { + this.validationObserver = new MutationObserver(() => { + const newIsInvalid = checkInvalidState(this.el); + if (this.isInvalid !== newIsInvalid) { + this.isInvalid = newIsInvalid; + /** + * Screen readers tend to announce changes + * to `aria-describedby` when the attribute + * is changed during a blur event for a + * native form control. + * However, the announcement can be spotty + * when using a non-native form control + * and `forceUpdate()`. + * This is due to `forceUpdate()` internally + * rescheduling the DOM update to a lower + * priority queue regardless if it's called + * inside a Promise or not, thus causing + * the screen reader to potentially miss the + * change. + * By using a State variable inside a Promise, + * it guarantees a re-render immediately at + * a higher priority. + */ + Promise.resolve().then(() => { + this.hintTextID = this.getHintTextID(); + }); + } + }); + + this.validationObserver.observe(this.el, { + attributes: true, + attributeFilter: ['class'], + }); + } + + // Always set initial state + this.isInvalid = checkInvalidState(this.el); + } + + disconnectedCallback() { + // Clean up validation observer to prevent memory leaks. + if (this.validationObserver) { + this.validationObserver.disconnect(); + this.validationObserver = undefined; + } } private getRadios(): HTMLIonRadioElement[] { @@ -244,7 +300,7 @@ export class RadioGroup implements ComponentInterface { * Renders the helper text or error text values */ private renderHintText() { - const { helperText, errorText, helperTextId, errorTextId } = this; + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; const hasHintText = !!helperText || !!errorText; if (!hasHintText) { @@ -253,20 +309,20 @@ export class RadioGroup implements ComponentInterface { return (
-
- {helperText} +
+ {!isInvalid ? helperText : null}
-
- {errorText} +
); } private getHintTextID(): string | undefined { - const { el, helperText, errorText, helperTextId, errorTextId } = this; + const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this; - if (el.classList.contains('ion-touched') && el.classList.contains('ion-invalid') && errorText) { + if (isInvalid && errorText) { return errorTextId; } @@ -287,8 +343,8 @@ export class RadioGroup implements ComponentInterface { diff --git a/core/src/components/radio-group/test/validation/index.html b/core/src/components/radio-group/test/validation/index.html new file mode 100644 index 00000000000..a700e28896b --- /dev/null +++ b/core/src/components/radio-group/test/validation/index.html @@ -0,0 +1,195 @@ + + + + + Radrio Group - Validation + + + + + + + + + + + + + + Radio Group - Validation Test + + + + +
+

Screen Reader Testing Instructions:

+
    +
  1. Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
  2. +
  3. Tab through the form fields
  4. +
  5. When you tab away from an empty required field, the error should be announced immediately
  6. +
  7. The error text should be announced BEFORE the next field is announced
  8. +
  9. Test in Chrome, Safari, and Firefox to verify consistent behavior
  10. +
+
+ +
+
+

Required Field

+ + Grapes
+ Strawberries +
+
+ +
+

Optional Field (No Validation)

+ + Cucumbers
+ Tomatoes +
+
+
+ +
+ Submit Form + Reset Form +
+
+
+ + + + diff --git a/core/src/utils/forms/validity.ts b/core/src/utils/forms/validity.ts index 45e8f3e2310..5c2adabe675 100644 --- a/core/src/utils/forms/validity.ts +++ b/core/src/utils/forms/validity.ts @@ -3,7 +3,8 @@ type FormElement = | HTMLIonTextareaElement | HTMLIonSelectElement | HTMLIonCheckboxElement - | HTMLIonToggleElement; + | HTMLIonToggleElement + | HTMLElement; /** * Checks if the form element is in an invalid state based on From 44c635d0a6afe3dc50c8ae4a244f36436c959508 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 3 Nov 2025 15:36:56 -0800 Subject: [PATCH 06/10] fix(checkbox, toggle, radio-group): add initial value --- core/src/components/checkbox/checkbox.tsx | 3 +++ core/src/components/checkbox/test/validation/index.html | 2 +- core/src/components/radio-group/radio-group.tsx | 6 +++++- core/src/components/radio-group/test/validation/index.html | 2 +- core/src/components/toggle/test/validation/index.html | 2 +- core/src/components/toggle/toggle.tsx | 2 ++ 6 files changed, 13 insertions(+), 4 deletions(-) diff --git a/core/src/components/checkbox/checkbox.tsx b/core/src/components/checkbox/checkbox.tsx index 506f6c8e245..5af4fa7a9d4 100644 --- a/core/src/components/checkbox/checkbox.tsx +++ b/core/src/components/checkbox/checkbox.tsx @@ -174,6 +174,7 @@ export class Checkbox implements ComponentInterface { * a higher priority. */ Promise.resolve().then(() => { + console.log('updating hint text id'); this.hintTextID = this.getHintTextID(); }); } @@ -193,6 +194,8 @@ export class Checkbox implements ComponentInterface { this.inheritedAttributes = { ...inheritAriaAttributes(this.el), }; + + this.hintTextID = this.getHintTextID(); } disconnectedCallback() { diff --git a/core/src/components/checkbox/test/validation/index.html b/core/src/components/checkbox/test/validation/index.html index 8686186c7de..e3f4bce5601 100644 --- a/core/src/components/checkbox/test/validation/index.html +++ b/core/src/components/checkbox/test/validation/index.html @@ -146,7 +146,7 @@

Optional Field (No Validation)

const isInvalid = checkbox.classList.contains('ion-invalid'); if (isInvalid) { - console.log('Field marked invalid:', checkbox.label, checkbox.errorText); + console.log('Field marked invalid:', checkbox.innerText, checkbox.errorText); } }); diff --git a/core/src/components/radio-group/radio-group.tsx b/core/src/components/radio-group/radio-group.tsx index b9a059acd53..62f63b006c0 100644 --- a/core/src/components/radio-group/radio-group.tsx +++ b/core/src/components/radio-group/radio-group.tsx @@ -1,7 +1,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Build, Component, Element, Event, Host, Listen, Method, Prop, State, Watch, h } from '@stencil/core'; import { checkInvalidState } from '@utils/forms'; -import { renderHiddenInput } from '@utils/helpers'; +import { inheritAriaAttributes, renderHiddenInput } from '@utils/helpers'; import { getIonMode } from '../../global/ionic-global'; @@ -171,6 +171,10 @@ export class RadioGroup implements ComponentInterface { this.isInvalid = checkInvalidState(this.el); } + componentWillLoad() { + this.hintTextID = this.getHintTextID(); + } + disconnectedCallback() { // Clean up validation observer to prevent memory leaks. if (this.validationObserver) { diff --git a/core/src/components/radio-group/test/validation/index.html b/core/src/components/radio-group/test/validation/index.html index a700e28896b..66222aa2367 100644 --- a/core/src/components/radio-group/test/validation/index.html +++ b/core/src/components/radio-group/test/validation/index.html @@ -157,7 +157,7 @@

Optional Field (No Validation)

const isInvalid = radioGroup.classList.contains('ion-invalid'); if (isInvalid) { - console.log('Field marked invalid:', radioGroup.label, radioGroup.errorText); + console.log('Field marked invalid:', radioGroup.id, radioGroup.errorText); } }); diff --git a/core/src/components/toggle/test/validation/index.html b/core/src/components/toggle/test/validation/index.html index bcc8dbadfc3..54932edeb08 100644 --- a/core/src/components/toggle/test/validation/index.html +++ b/core/src/components/toggle/test/validation/index.html @@ -146,7 +146,7 @@

Optional Field (No Validation)

const isInvalid = toggle.classList.contains('ion-invalid'); if (isInvalid) { - console.log('Field marked invalid:', toggle.label, toggle.errorText); + console.log('Field marked invalid:', toggle.innerText, toggle.errorText); } }); diff --git a/core/src/components/toggle/toggle.tsx b/core/src/components/toggle/toggle.tsx index 826da56686b..b6b8f349d0b 100644 --- a/core/src/components/toggle/toggle.tsx +++ b/core/src/components/toggle/toggle.tsx @@ -269,6 +269,8 @@ export class Toggle implements ComponentInterface { this.inheritedAttributes = { ...inheritAriaAttributes(this.el), }; + + this.hintTextID = this.getHintTextID(); } private onStart() { From 90d03bf72e5f766d410a1e8c7ad93d5f21948ab2 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 3 Nov 2025 15:45:20 -0800 Subject: [PATCH 07/10] fix(radio-group): remove unused code --- core/src/components/radio-group/radio-group.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/components/radio-group/radio-group.tsx b/core/src/components/radio-group/radio-group.tsx index 62f63b006c0..26c187453a6 100644 --- a/core/src/components/radio-group/radio-group.tsx +++ b/core/src/components/radio-group/radio-group.tsx @@ -1,7 +1,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Build, Component, Element, Event, Host, Listen, Method, Prop, State, Watch, h } from '@stencil/core'; import { checkInvalidState } from '@utils/forms'; -import { inheritAriaAttributes, renderHiddenInput } from '@utils/helpers'; +import { renderHiddenInput } from '@utils/helpers'; import { getIonMode } from '../../global/ionic-global'; From 1ec03fe6cdcc1136790b67e3a13a16ec4a74a140 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 3 Nov 2025 20:06:35 -0800 Subject: [PATCH 08/10] test(angular): add test pages --- .../template-form.component.html | 88 ++++++++++++++++++- .../template-form/template-form.component.ts | 3 + .../standalone/app-standalone/app.routes.ts | 3 + .../home-page/home-page.component.html | 19 +++- .../checkbox-validation.component.html | 53 +++++++++++ .../checkbox-validation.component.scss | 36 ++++++++ .../checkbox-validation.component.ts | 61 +++++++++++++ .../radio-group-validation.component.html | 57 ++++++++++++ .../radio-group-validation.component.scss | 36 ++++++++ .../radio-group-validation.component.ts | 66 ++++++++++++++ .../toggle-validation.component.html | 54 ++++++++++++ .../toggle-validation.component.scss | 36 ++++++++ .../toggle-validation.component.ts | 61 +++++++++++++ 13 files changed, 570 insertions(+), 3 deletions(-) create mode 100644 packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.html create mode 100644 packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.scss create mode 100644 packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.ts create mode 100644 packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.html create mode 100644 packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.scss create mode 100644 packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.ts create mode 100644 packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.html create mode 100644 packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.scss create mode 100644 packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.ts diff --git a/packages/angular/test/base/src/app/lazy/template-form/template-form.component.html b/packages/angular/test/base/src/app/lazy/template-form/template-form.component.html index ccd902f6b95..c756ae7c668 100644 --- a/packages/angular/test/base/src/app/lazy/template-form/template-form.component.html +++ b/packages/angular/test/base/src/app/lazy/template-form/template-form.component.html @@ -93,7 +93,7 @@ Option 2 - + @@ -102,6 +102,92 @@

Select Errors: {{selectField.errors | json}}

+ + + + + I agree to the terms and conditions + + + + + + +

Checkbox Touched: {{checkboxField.touched}}

+

Checkbox Invalid: {{checkboxField.invalid}}

+

Checkbox Errors: {{checkboxField.errors | json}}

+
+
+ + + + + Tap to turn on + + + + + + +

Toggle Touched: {{toggleField.touched}}

+

Toggle Invalid: {{toggleField.invalid}}

+

Toggle Errors: {{toggleField.errors | json}}

+
+
+ + + + + I agree to the terms and conditions + + + + Grapes
+ Strawberries +
+
+ + + + +

Radio Group Touched: {{radioGroupField.touched}}

+

Radio Group Invalid: {{radioGroupField.invalid}}

+

Radio Group Errors: {{radioGroupField.errors | json}}

+
+
diff --git a/packages/angular/test/base/src/app/lazy/template-form/template-form.component.ts b/packages/angular/test/base/src/app/lazy/template-form/template-form.component.ts index 705e104e808..d26e1a6cc4a 100644 --- a/packages/angular/test/base/src/app/lazy/template-form/template-form.component.ts +++ b/packages/angular/test/base/src/app/lazy/template-form/template-form.component.ts @@ -10,6 +10,9 @@ export class TemplateFormComponent { textareaValue = ''; minLengthValue = ''; selectValue = ''; + checkboxValue = false; + toggleValue = false; + radioGroupValue = ''; // Track if form has been submitted submitted = false; diff --git a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts index 93f6284957f..007743f905f 100644 --- a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts +++ b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts @@ -48,6 +48,9 @@ export const routes: Routes = [ { path: 'input-validation', loadComponent: () => import('../validation/input-validation/input-validation.component').then(c => c.InputValidationComponent) }, { path: 'textarea-validation', loadComponent: () => import('../validation/textarea-validation/textarea-validation.component').then(c => c.TextareaValidationComponent) }, { path: 'select-validation', loadComponent: () => import('../validation/select-validation/select-validation.component').then(c => c.SelectValidationComponent) }, + { path: 'checkbox-validation', loadComponent: () => import('../validation/checkbox-validation/checkbox-validation.component').then(c => c.CheckboxValidationComponent) }, + { path: 'toggle-validation', loadComponent: () => import('../validation/toggle-validation/toggle-validation.component').then(c => c.ToggleValidationComponent) }, + { path: 'radio-group-validation', loadComponent: () => import('../validation/radio-group-validation/radio-group-validation.component').then(c => c.RadioGroupValidationComponent) }, { path: '**', redirectTo: 'input-validation' } ] }, diff --git a/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html b/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html index f0eece0ba33..7ac9c619180 100644 --- a/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html +++ b/packages/angular/test/base/src/app/standalone/home-page/home-page.component.html @@ -121,14 +121,19 @@ Validation Tests + + + Checkbox Validation Test + + Input Validation Test - + - Textarea Validation Test + Radio Group Validation Test @@ -136,6 +141,16 @@ Select Validation Test + + + Textarea Validation Test + + + + + Toggle Validation Test + + diff --git a/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.html b/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.html new file mode 100644 index 00000000000..86a8425e0cb --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.html @@ -0,0 +1,53 @@ + + + Checkbox - Validation Test + + + + +
+

Screen Reader Testing Instructions:

+
    +
  1. Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
  2. +
  3. Tab through the form fields
  4. +
  5. When you tab away from an empty required field, the error should be announced immediately
  6. +
  7. The error text should be announced BEFORE the next field is announced
  8. +
  9. Test in Chrome, Safari, and Firefox to verify consistent behavior
  10. +
+
+ +
+
+
+

Required Field

+ + {{ fieldMetadata.terms.label }} + +
+ +
+

Optional Field (No Validation)

+ + {{ fieldMetadata.optional.label }} + +
+
+
+ +
+ Submit Form + Reset Form +
+
diff --git a/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.scss b/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.scss new file mode 100644 index 00000000000..d8b2a267e5a --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.scss @@ -0,0 +1,36 @@ +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + grid-row-gap: 50px; + grid-column-gap: 50px; +} + +h2 { + font-size: 12px; + font-weight: normal; + color: var(--ion-color-step-600); + margin-top: 10px; + margin-bottom: 5px; +} + +.validation-info { + margin: 20px; + padding: 10px; + background: var(--ion-color-light); + border-radius: 4px; +} + +.validation-info h2 { + font-size: 14px; + font-weight: 600; + margin-bottom: 10px; +} + +.validation-info ol { + margin: 0; + padding-left: 20px; +} + +.validation-info li { + margin-bottom: 5px; +} diff --git a/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.ts b/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.ts new file mode 100644 index 00000000000..48bf8a935f1 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/checkbox-validation/checkbox-validation.component.ts @@ -0,0 +1,61 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { + FormBuilder, + ReactiveFormsModule, + Validators +} from '@angular/forms'; +import { + IonButton, + IonContent, + IonHeader, + IonCheckbox, + IonTitle, + IonToolbar +} from '@ionic/angular/standalone'; + +@Component({ + selector: 'app-checkbox-validation', + templateUrl: './checkbox-validation.component.html', + styleUrls: ['./checkbox-validation.component.scss'], + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + IonCheckbox, + IonButton, + IonHeader, + IonToolbar, + IonTitle, + IonContent + ] +}) +export class CheckboxValidationComponent { + // Field metadata for labels and error messages + fieldMetadata = { + terms: { + label: 'I agree to the terms and conditions', + helperText: "You must agree to continue", + errorText: 'This field is required' + }, + optional: { + label: 'Optional Checkbox', + helperText: 'You can skip this field', + errorText: '' + } + }; + + form = this.fb.group({ + terms: [false, Validators.requiredTrue], + optional: [false] + }); + + constructor(private fb: FormBuilder) {} + + // Submit form + onSubmit(): void { + if (this.form.valid) { + alert('Form submitted successfully!'); + } + } +} diff --git a/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.html b/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.html new file mode 100644 index 00000000000..b7c90ba1ce6 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.html @@ -0,0 +1,57 @@ + + + Radio Group - Validation Test + + + + +
+

Screen Reader Testing Instructions:

+
    +
  1. Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
  2. +
  3. Tab through the form fields
  4. +
  5. When you tab away from an empty required field, the error should be announced immediately
  6. +
  7. The error text should be announced BEFORE the next field is announced
  8. +
  9. Test in Chrome, Safari, and Firefox to verify consistent behavior
  10. +
+
+ +
+
+
+

Required Field

+ + {{ fieldMetadata.fruits.firstRadio }}
+ {{ fieldMetadata.fruits.secondRadio }} +
+
+ +
+

Optional Field (No Validation)

+ + {{ fieldMetadata.optional.firstRadio }}
+ {{ fieldMetadata.optional.secondRadio }} +
+
+
+
+ +
+ Submit Form + Reset Form +
+
diff --git a/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.scss b/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.scss new file mode 100644 index 00000000000..add228ccab1 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.scss @@ -0,0 +1,36 @@ +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + grid-row-gap: 20px; + grid-column-gap: 20px; +} + +h2 { + font-size: 12px; + font-weight: normal; + color: var(--ion-color-step-600); + margin-top: 10px; + margin-bottom: 5px; +} + +.validation-info { + margin: 20px; + padding: 10px; + background: var(--ion-color-light); + border-radius: 4px; +} + +.validation-info h2 { + font-size: 14px; + font-weight: 600; + margin-bottom: 10px; +} + +.validation-info ol { + margin: 0; + padding-left: 20px; +} + +.validation-info li { + margin-bottom: 5px; +} diff --git a/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.ts b/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.ts new file mode 100644 index 00000000000..aa4ee109a50 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/radio-group-validation/radio-group-validation.component.ts @@ -0,0 +1,66 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { + FormBuilder, + ReactiveFormsModule, + Validators +} from '@angular/forms'; +import { + IonButton, + IonContent, + IonHeader, + IonRadioGroup, + IonRadio, + IonTitle, + IonToolbar +} from '@ionic/angular/standalone'; + +@Component({ + selector: 'app-radio-group-validation', + templateUrl: './radio-group-validation.component.html', + styleUrls: ['./radio-group-validation.component.scss'], + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + IonRadioGroup, + IonRadio, + IonButton, + IonHeader, + IonToolbar, + IonTitle, + IonContent + ] +}) +export class RadioGroupValidationComponent { + // Field metadata for labels and error messages + fieldMetadata = { + fruits: { + helperText: "You must select one to continue", + errorText: 'This field is required', + firstRadio: "Grapes", + secondRadio: "Strawberries" + }, + optional: { + label: 'Optional Radio', + helperText: 'You can skip this field', + errorText: '', + firstRadio: "Option A", + secondRadio: "Option B" + } + }; + + form = this.fb.group({ + fruits: ['', Validators.required], + optional: [''] + }); + + constructor(private fb: FormBuilder) {} + + // Submit form + onSubmit(): void { + if (this.form.valid) { + alert('Form submitted successfully!'); + } + } +} diff --git a/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.html b/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.html new file mode 100644 index 00000000000..1bf6ac67e75 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.html @@ -0,0 +1,54 @@ + + + Toggle - Validation Test + + + + +
+

Screen Reader Testing Instructions:

+
    +
  1. Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)
  2. +
  3. Tab through the form fields
  4. +
  5. When you tab away from an empty required field, the error should be announced immediately
  6. +
  7. The error text should be announced BEFORE the next field is announced
  8. +
  9. Test in Chrome, Safari, and Firefox to verify consistent behavior
  10. +
+
+ +
+
+
+

Required Field

+ + {{ fieldMetadata.on.label }} + +
+ +
+

Optional Field (No Validation)

+ + {{ fieldMetadata.optional.label }} + + +
+
+
+ +
+ Submit Form + Reset Form +
+
diff --git a/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.scss b/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.scss new file mode 100644 index 00000000000..add228ccab1 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.scss @@ -0,0 +1,36 @@ +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + grid-row-gap: 20px; + grid-column-gap: 20px; +} + +h2 { + font-size: 12px; + font-weight: normal; + color: var(--ion-color-step-600); + margin-top: 10px; + margin-bottom: 5px; +} + +.validation-info { + margin: 20px; + padding: 10px; + background: var(--ion-color-light); + border-radius: 4px; +} + +.validation-info h2 { + font-size: 14px; + font-weight: 600; + margin-bottom: 10px; +} + +.validation-info ol { + margin: 0; + padding-left: 20px; +} + +.validation-info li { + margin-bottom: 5px; +} diff --git a/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.ts b/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.ts new file mode 100644 index 00000000000..d756ac150be --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/validation/toggle-validation/toggle-validation.component.ts @@ -0,0 +1,61 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { + FormBuilder, + ReactiveFormsModule, + Validators +} from '@angular/forms'; +import { + IonButton, + IonContent, + IonHeader, + IonToggle, + IonTitle, + IonToolbar +} from '@ionic/angular/standalone'; + +@Component({ + selector: 'app-toggle-validation', + templateUrl: './toggle-validation.component.html', + styleUrls: ['./toggle-validation.component.scss'], + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + IonToggle, + IonButton, + IonHeader, + IonToolbar, + IonTitle, + IonContent + ] +}) +export class ToggleValidationComponent { + // Field metadata for labels and error messages + fieldMetadata = { + on: { + label: 'Tap to turn on', + helperText: "You must turn on to continue", + errorText: 'This field is required' + }, + optional: { + label: 'Optional Toggle', + helperText: 'You can skip this field', + errorText: '' + } + }; + + form = this.fb.group({ + on: [false, Validators.requiredTrue], + optional: [false] + }); + + constructor(private fb: FormBuilder) {} + + // Submit form + onSubmit(): void { + if (this.form.valid) { + alert('Form submitted successfully!'); + } + } +} From 502437d4f9426b4187fed8b4f302ef4d0fc76679 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 3 Nov 2025 20:27:35 -0800 Subject: [PATCH 09/10] refactor(radio-group, angular): remove issues --- .../radio-group/test/validation/index.html | 2 +- .../lazy/template-form/template-form.component.html | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/core/src/components/radio-group/test/validation/index.html b/core/src/components/radio-group/test/validation/index.html index 66222aa2367..29e05fb5505 100644 --- a/core/src/components/radio-group/test/validation/index.html +++ b/core/src/components/radio-group/test/validation/index.html @@ -2,7 +2,7 @@ - Radrio Group - Validation + Radio Group - Validation - - - I agree to the terms and conditions - - Date: Tue, 4 Nov 2025 08:57:50 -0800 Subject: [PATCH 10/10] fix(angular): add missing ion-item --- .../base/src/app/lazy/template-form/template-form.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/angular/test/base/src/app/lazy/template-form/template-form.component.html b/packages/angular/test/base/src/app/lazy/template-form/template-form.component.html index 158c9eba6e8..870f53872f2 100644 --- a/packages/angular/test/base/src/app/lazy/template-form/template-form.component.html +++ b/packages/angular/test/base/src/app/lazy/template-form/template-form.component.html @@ -152,6 +152,7 @@ +