mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
fix(checkbox, toggle, radio-group): improve screen reader announcement timing for validation errors (#30714)
Issue number: internal
---------
<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->
<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->
## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->
Currently, when an error text is shown, it may not announce itself to
voice assistants. This is because the way error text currently works is
by always existing in the DOM, but being hidden when there is no error.
When the error state changes, the error text is shown, but as far as the
voice assistant can tell it's always been there and nothing has changed.
## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->
- Updated aria attributes
- Added observer with an observer
## Does this introduce a breaking change?
- [ ] Yes
- [x] No
<!--
If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->
## Other information
<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->
[Checkbox
preview](https://ionic-framework-git-fw-6757-ionic1.vercel.app/src/components/checkbox/test/validation/)
[Toggle
preview](https://ionic-framework-git-fw-6757-ionic1.vercel.app/src/components/toggle/test/validation/)
[Radio Group
preview](https://ionic-framework-git-fw-6757-ionic1.vercel.app/src/components/radio-group/test/validation/)
⚠️ Flakiness ⚠️
The flakiness on checkbox and toggle is because when a native input is
present on the page, the browser will have the screen reader to be
really fast when it comes to checking information. This speed ends up
being too fast for `ion-checkbox` to be able to add the error text. This
leads to the error text not being announce consistently. There's no
issue when it comes to ion-input or ion-textarea because Ionic uses the
native inputs so their arias are read. There's also no issue
with ion-select because we don't have a native input. It's only an issue
with checkbox and the others is because it has a [native input that is
being
hidden](8e884bd2cb/core/src/components/checkbox/checkbox.tsx (L368-L369)).
So the browser sees that and speeds up the screen reader.
The flakiness on radio group is because when you tab out of the last
radio button, the ionBlur event is emitted by the child <ion-radio>.
This event bubbles up, but the timing is still too early for the group.
This commit is contained in:
@@ -93,7 +93,7 @@
|
||||
<ion-select-option value="option2">Option 2</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
|
||||
<!-- Display validation state for debugging -->
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
@@ -102,6 +102,80 @@
|
||||
<p>Select Errors: <span id="select-errors">{{selectField.errors | json}}</span></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- Test ion-checkbox with required validation -->
|
||||
<ion-item>
|
||||
<ion-checkbox
|
||||
[(ngModel)]="checkboxValue"
|
||||
name="checkboxField"
|
||||
required
|
||||
#checkboxField="ngModel"
|
||||
id="template-checkbox-test"
|
||||
helper-text="You must agree to continue"
|
||||
error-text="Please accept the terms and conditions"
|
||||
>
|
||||
I agree to the terms and conditions
|
||||
</ion-checkbox>
|
||||
</ion-item>
|
||||
|
||||
<!-- Display validation state for debugging -->
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<p>Checkbox Touched: <span id="checkbox-touched">{{checkboxField.touched}}</span></p>
|
||||
<p>Checkbox Invalid: <span id="checkbox-invalid">{{checkboxField.invalid}}</span></p>
|
||||
<p>Checkbox Errors: <span id="checkbox-errors">{{checkboxField.errors | json}}</span></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- Test ion-toggle with required validation -->
|
||||
<ion-item>
|
||||
<ion-toggle
|
||||
[(ngModel)]="toggleValue"
|
||||
name="toggleField"
|
||||
required
|
||||
#toggleField="ngModel"
|
||||
id="template-toggle-test"
|
||||
helper-text="You must turn on to continue"
|
||||
error-text="Please turn on this toggle"
|
||||
>
|
||||
Tap to turn on
|
||||
</ion-toggle>
|
||||
</ion-item>
|
||||
|
||||
<!-- Display validation state for debugging -->
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<p>Toggle Touched: <span id="toggle-touched">{{toggleField.touched}}</span></p>
|
||||
<p>Toggle Invalid: <span id="toggle-invalid">{{toggleField.invalid}}</span></p>
|
||||
<p>Toggle Errors: <span id="toggle-errors">{{toggleField.errors | json}}</span></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- Test ion-radio-group with required validation -->
|
||||
<ion-item>
|
||||
<ion-radio-group
|
||||
[(ngModel)]="radioGroupValue"
|
||||
name="radioGroupField"
|
||||
required
|
||||
#radioGroupField="ngModel"
|
||||
id="template-radio-group-test"
|
||||
helper-text="You must select one to continue"
|
||||
error-text="Please select a fruit"
|
||||
allow-empty-selection="true"
|
||||
>
|
||||
<ion-radio value="grapes">Grapes</ion-radio><br />
|
||||
<ion-radio value="strawberries">Strawberries</ion-radio>
|
||||
</ion-radio-group>
|
||||
</ion-item>
|
||||
|
||||
<!-- Display validation state for debugging -->
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<p>Radio Group Touched: <span id="radio-group-touched">{{radioGroupField.touched}}</span></p>
|
||||
<p>Radio Group Invalid: <span id="radio-group-invalid">{{radioGroupField.invalid}}</span></p>
|
||||
<p>Radio Group Errors: <span id="radio-group-errors">{{radioGroupField.errors | json}}</span></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<div class="ion-padding">
|
||||
|
||||
@@ -10,6 +10,9 @@ export class TemplateFormComponent {
|
||||
textareaValue = '';
|
||||
minLengthValue = '';
|
||||
selectValue = '';
|
||||
checkboxValue = false;
|
||||
toggleValue = false;
|
||||
radioGroupValue = '';
|
||||
|
||||
// Track if form has been submitted
|
||||
submitted = false;
|
||||
|
||||
@@ -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' }
|
||||
]
|
||||
},
|
||||
|
||||
@@ -121,19 +121,34 @@
|
||||
<ion-list-header>
|
||||
<ion-label>Validation Tests</ion-label>
|
||||
</ion-list-header>
|
||||
<ion-item routerLink="/standalone/validation/checkbox-validation">
|
||||
<ion-label>
|
||||
Checkbox Validation Test
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item routerLink="/standalone/validation/input-validation">
|
||||
<ion-label>
|
||||
Input Validation Test
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item routerLink="/standalone/validation/radio-group-validation">
|
||||
<ion-label>
|
||||
Radio Group Validation Test
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item routerLink="/standalone/validation/select-validation">
|
||||
<ion-label>
|
||||
Select Validation Test
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item routerLink="/standalone/validation/textarea-validation">
|
||||
<ion-label>
|
||||
Textarea Validation Test
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item routerLink="/standalone/validation/select-validation">
|
||||
<ion-item routerLink="/standalone/validation/toggle-validation">
|
||||
<ion-label>
|
||||
Select Validation Test
|
||||
Toggle Validation Test
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Checkbox - Validation Test</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<div class="validation-info">
|
||||
<h2>Screen Reader Testing Instructions:</h2>
|
||||
<ol>
|
||||
<li>Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)</li>
|
||||
<li>Tab through the form fields</li>
|
||||
<li>When you tab away from an empty required field, the error should be announced immediately</li>
|
||||
<li>The error text should be announced BEFORE the next field is announced</li>
|
||||
<li>Test in Chrome, Safari, and Firefox to verify consistent behavior</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<div class="grid">
|
||||
<div>
|
||||
<h2>Required Field</h2>
|
||||
<ion-checkbox
|
||||
#termsCheckbox
|
||||
id="terms-checkbox"
|
||||
[helperText]="fieldMetadata.terms.helperText"
|
||||
[errorText]="fieldMetadata.terms.errorText"
|
||||
formControlName="terms"
|
||||
required
|
||||
>
|
||||
{{ fieldMetadata.terms.label }}
|
||||
</ion-checkbox>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Optional Field (No Validation)</h2>
|
||||
<ion-checkbox
|
||||
#optionalCheckbox
|
||||
id="optional-checkbox"
|
||||
[helperText]="fieldMetadata.optional.helperText"
|
||||
formControlName="optional"
|
||||
>
|
||||
{{ fieldMetadata.optional.label }}
|
||||
</ion-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="ion-padding">
|
||||
<ion-button id="submit-btn" expand="block" [disabled]="form.invalid" (click)="onSubmit()">Submit Form</ion-button>
|
||||
<ion-button id="reset-btn" expand="block" fill="outline" (click)="form.reset()">Reset Form</ion-button>
|
||||
</div>
|
||||
</ion-content>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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!');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Radio Group - Validation Test</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<div class="validation-info">
|
||||
<h2>Screen Reader Testing Instructions:</h2>
|
||||
<ol>
|
||||
<li>Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)</li>
|
||||
<li>Tab through the form fields</li>
|
||||
<li>When you tab away from an empty required field, the error should be announced immediately</li>
|
||||
<li>The error text should be announced BEFORE the next field is announced</li>
|
||||
<li>Test in Chrome, Safari, and Firefox to verify consistent behavior</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<div class="grid">
|
||||
<div>
|
||||
<h2>Required Field</h2>
|
||||
<ion-radio-group
|
||||
#fruitsRadioGroup
|
||||
id="fruits-radio-group"
|
||||
[helperText]="fieldMetadata.fruits.helperText"
|
||||
[errorText]="fieldMetadata.fruits.errorText"
|
||||
formControlName="fruits"
|
||||
allow-empty-selection="true"
|
||||
required
|
||||
>
|
||||
<ion-radio [value]="fieldMetadata.fruits.firstRadio">{{ fieldMetadata.fruits.firstRadio }}</ion-radio><br />
|
||||
<ion-radio [value]="fieldMetadata.fruits.secondRadio">{{ fieldMetadata.fruits.secondRadio }}</ion-radio>
|
||||
</ion-radio-group>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Optional Field (No Validation)</h2>
|
||||
<ion-radio-group
|
||||
#optionalRadioGroup
|
||||
id="optional-radio-group"
|
||||
[helperText]="fieldMetadata.optional.helperText"
|
||||
formControlName="optional"
|
||||
allow-empty-selection="true"
|
||||
>
|
||||
<ion-radio [value]="fieldMetadata.optional.firstRadio">{{ fieldMetadata.optional.firstRadio }}</ion-radio><br />
|
||||
<ion-radio [value]="fieldMetadata.optional.secondRadio">{{ fieldMetadata.optional.secondRadio }}</ion-radio>
|
||||
</ion-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="ion-padding">
|
||||
<ion-button id="submit-btn" expand="block" [disabled]="form.invalid" (click)="onSubmit()">Submit Form</ion-button>
|
||||
<ion-button id="reset-btn" expand="block" fill="outline" (click)="form.reset()">Reset Form</ion-button>
|
||||
</div>
|
||||
</ion-content>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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!');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Toggle - Validation Test</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<div class="validation-info">
|
||||
<h2>Screen Reader Testing Instructions:</h2>
|
||||
<ol>
|
||||
<li>Enable your screen reader (VoiceOver, NVDA, JAWS, etc.)</li>
|
||||
<li>Tab through the form fields</li>
|
||||
<li>When you tab away from an empty required field, the error should be announced immediately</li>
|
||||
<li>The error text should be announced BEFORE the next field is announced</li>
|
||||
<li>Test in Chrome, Safari, and Firefox to verify consistent behavior</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="form">
|
||||
<div class="grid">
|
||||
<div>
|
||||
<h2>Required Field</h2>
|
||||
<ion-toggle
|
||||
#onToggle
|
||||
id="on-toggle"
|
||||
[helperText]="fieldMetadata.on.helperText"
|
||||
[errorText]="fieldMetadata.on.errorText"
|
||||
formControlName="on"
|
||||
required
|
||||
>
|
||||
{{ fieldMetadata.on.label }}
|
||||
</ion-toggle>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Optional Field (No Validation)</h2>
|
||||
<ion-toggle
|
||||
#optionalToggle
|
||||
id="optional-toggle"
|
||||
[helperText]="fieldMetadata.optional.helperText"
|
||||
formControlName="optional"
|
||||
>
|
||||
{{ fieldMetadata.optional.label }}
|
||||
</ion-toggle>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="ion-padding">
|
||||
<ion-button id="submit-btn" expand="block" [disabled]="form.invalid" (click)="onSubmit()">Submit Form</ion-button>
|
||||
<ion-button id="reset-btn" expand="block" fill="outline" (click)="form.reset()">Reset Form</ion-button>
|
||||
</div>
|
||||
</ion-content>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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!');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user