mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-06 22:29:44 +08:00
fix(checkbox, select): improve error text accessibility
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
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 type { Attributes } from '@utils/helpers';
|
||||||
import { inheritAriaAttributes, renderHiddenInput } from '@utils/helpers';
|
import { inheritAriaAttributes, renderHiddenInput } from '@utils/helpers';
|
||||||
import { createColorClasses, hostContext } from '@utils/theme';
|
import { createColorClasses, hostContext } from '@utils/theme';
|
||||||
@ -121,6 +121,11 @@ export class Checkbox implements ComponentInterface {
|
|||||||
*/
|
*/
|
||||||
@Prop() required = false;
|
@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.
|
* 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<void>;
|
@Event() ionBlur!: EventEmitter<void>;
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
// Always set initial state.
|
||||||
|
this.isInvalid = this.checkInvalidState();
|
||||||
|
}
|
||||||
|
|
||||||
componentWillLoad() {
|
componentWillLoad() {
|
||||||
this.inheritedAttributes = {
|
this.inheritedAttributes = {
|
||||||
...inheritAriaAttributes(this.el),
|
...inheritAriaAttributes(this.el),
|
||||||
@ -179,6 +189,13 @@ export class Checkbox implements ComponentInterface {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private onBlur = () => {
|
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();
|
this.ionBlur.emit();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -208,9 +225,9 @@ export class Checkbox implements ComponentInterface {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private getHintTextID(): string | undefined {
|
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;
|
return errorTextId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,7 +243,7 @@ export class Checkbox implements ComponentInterface {
|
|||||||
* This element should only be rendered if hint text is set.
|
* This element should only be rendered if hint text is set.
|
||||||
*/
|
*/
|
||||||
private renderHintText() {
|
private renderHintText() {
|
||||||
const { helperText, errorText, helperTextId, errorTextId } = this;
|
const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* undefined and empty string values should
|
* undefined and empty string values should
|
||||||
@ -239,16 +256,26 @@ export class Checkbox implements ComponentInterface {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="checkbox-bottom">
|
<div class="checkbox-bottom">
|
||||||
<div id={helperTextId} class="helper-text" part="supporting-text helper-text">
|
<div id={helperTextId} class="helper-text" part="supporting-text helper-text" aria-live="polite">
|
||||||
{helperText}
|
{!isInvalid ? helperText : null}
|
||||||
</div>
|
</div>
|
||||||
<div id={errorTextId} class="error-text" part="supporting-text error-text">
|
<div id={errorTextId} class="error-text" part="supporting-text error-text" role="alert">
|
||||||
{errorText}
|
{isInvalid ? errorText : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
render() {
|
||||||
const {
|
const {
|
||||||
color,
|
color,
|
||||||
@ -279,10 +306,11 @@ export class Checkbox implements ComponentInterface {
|
|||||||
role="checkbox"
|
role="checkbox"
|
||||||
aria-checked={indeterminate ? 'mixed' : `${checked}`}
|
aria-checked={indeterminate ? 'mixed' : `${checked}`}
|
||||||
aria-describedby={this.getHintTextID()}
|
aria-describedby={this.getHintTextID()}
|
||||||
aria-invalid={this.getHintTextID() === this.errorTextId}
|
aria-invalid={this.isInvalid ? 'true' : undefined}
|
||||||
aria-labelledby={hasLabelContent ? this.inputLabelId : null}
|
aria-labelledby={hasLabelContent ? this.inputLabelId : null}
|
||||||
aria-label={inheritedAttributes['aria-label'] || null}
|
aria-label={inheritedAttributes['aria-label'] || null}
|
||||||
aria-disabled={disabled ? 'true' : null}
|
aria-disabled={disabled ? 'true' : null}
|
||||||
|
aria-required={required ? 'true' : undefined}
|
||||||
tabindex={disabled ? undefined : 0}
|
tabindex={disabled ? undefined : 0}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
class={createColorClasses(color, {
|
class={createColorClasses(color, {
|
||||||
|
|||||||
184
core/src/components/checkbox/test/validation/index.html
Normal file
184
core/src/components/checkbox/test/validation/index.html
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Checkbox - Validation</title>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
|
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||||
|
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||||
|
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||||
|
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||||
|
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||||
|
<style>
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
grid-row-gap: 30px;
|
||||||
|
grid-column-gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<ion-app>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div>
|
||||||
|
<h2>Required Field</h2>
|
||||||
|
<ion-checkbox
|
||||||
|
id="terms-checkbox"
|
||||||
|
helper-text="You must agree to continue"
|
||||||
|
error-text="Please accept the terms and conditions"
|
||||||
|
required
|
||||||
|
>I agree to the terms and conditions</ion-checkbox
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Optional Field (No Validation)</h2>
|
||||||
|
<ion-checkbox id="optional-checkbox" helper-text="You can skip this field">Optional Checkbox</ion-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ion-padding">
|
||||||
|
<ion-button id="submit-btn" expand="block" disabled>Submit Form</ion-button>
|
||||||
|
<ion-button id="reset-btn" expand="block" fill="outline">Reset Form</ion-button>
|
||||||
|
</div>
|
||||||
|
</ion-content>
|
||||||
|
</ion-app>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Simple validation logic
|
||||||
|
const checkboxes = document.querySelectorAll('ion-checkbox');
|
||||||
|
const submitBtn = document.getElementById('submit-btn');
|
||||||
|
const resetBtn = document.getElementById('reset-btn');
|
||||||
|
|
||||||
|
// Track which fields have been touched
|
||||||
|
const touchedFields = new Set();
|
||||||
|
|
||||||
|
// Validation functions
|
||||||
|
const validators = {
|
||||||
|
'terms-checkbox': (checked) => {
|
||||||
|
return checked === true;
|
||||||
|
},
|
||||||
|
'optional-checkbox': () => true, // Always valid
|
||||||
|
};
|
||||||
|
|
||||||
|
function validateField(checkbox) {
|
||||||
|
const checkboxId = checkbox.id;
|
||||||
|
const checked = checkbox.checked;
|
||||||
|
const isValid = validators[checkboxId] ? validators[checkboxId](checked) : true;
|
||||||
|
|
||||||
|
// Only show validation state if field has been touched
|
||||||
|
if (touchedFields.has(checkboxId)) {
|
||||||
|
if (isValid) {
|
||||||
|
checkbox.classList.remove('ion-invalid');
|
||||||
|
checkbox.classList.add('ion-valid');
|
||||||
|
} else {
|
||||||
|
checkbox.classList.remove('ion-valid');
|
||||||
|
checkbox.classList.add('ion-invalid');
|
||||||
|
}
|
||||||
|
checkbox.classList.add('ion-touched');
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateForm() {
|
||||||
|
let allValid = true;
|
||||||
|
checkboxes.forEach((checkbox) => {
|
||||||
|
if (checkbox.id !== 'optional-checkbox') {
|
||||||
|
const isValid = validateField(checkbox);
|
||||||
|
if (!isValid) {
|
||||||
|
allValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
submitBtn.disabled = !allValid;
|
||||||
|
return allValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
checkboxes.forEach((checkbox) => {
|
||||||
|
// Mark as touched on blur
|
||||||
|
checkbox.addEventListener('ionBlur', (e) => {
|
||||||
|
console.log('Blur event on:', checkbox.id);
|
||||||
|
touchedFields.add(checkbox.id);
|
||||||
|
validateField(checkbox);
|
||||||
|
validateForm();
|
||||||
|
|
||||||
|
const isInvalid = checkbox.classList.contains('ion-invalid');
|
||||||
|
if (isInvalid) {
|
||||||
|
console.log('Field marked invalid:', checkbox.label, checkbox.errorText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate on change
|
||||||
|
checkbox.addEventListener('ionChange', (e) => {
|
||||||
|
console.log('Change event on:', checkbox.id);
|
||||||
|
if (touchedFields.has(checkbox.id)) {
|
||||||
|
validateField(checkbox);
|
||||||
|
validateForm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset button
|
||||||
|
resetBtn.addEventListener('click', () => {
|
||||||
|
checkboxes.forEach((checkbox) => {
|
||||||
|
checkbox.checked = false;
|
||||||
|
checkbox.classList.remove('ion-valid', 'ion-invalid', 'ion-touched');
|
||||||
|
});
|
||||||
|
touchedFields.clear();
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Submit button
|
||||||
|
submitBtn.addEventListener('click', () => {
|
||||||
|
if (validateForm()) {
|
||||||
|
alert('Form submitted successfully!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial setup
|
||||||
|
validateForm();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -81,6 +81,11 @@ export class Select implements ComponentInterface {
|
|||||||
*/
|
*/
|
||||||
@State() hasFocus = false;
|
@State() hasFocus = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track validation state for proper aria-live announcements.
|
||||||
|
*/
|
||||||
|
@State() isInvalid = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The text to display on the cancel button.
|
* The text to display on the cancel button.
|
||||||
*/
|
*/
|
||||||
@ -298,6 +303,9 @@ export class Select implements ComponentInterface {
|
|||||||
*/
|
*/
|
||||||
forceUpdate(this);
|
forceUpdate(this);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Always set initial state.
|
||||||
|
this.isInvalid = this.checkInvalidState();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillLoad() {
|
componentWillLoad() {
|
||||||
@ -868,8 +876,15 @@ export class Select implements ComponentInterface {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private onBlur = () => {
|
private onBlur = () => {
|
||||||
|
const newIsInvalid = this.checkInvalidState();
|
||||||
this.hasFocus = false;
|
this.hasFocus = false;
|
||||||
|
|
||||||
|
if (this.isInvalid !== newIsInvalid) {
|
||||||
|
this.isInvalid = newIsInvalid;
|
||||||
|
// Force a re-render to update aria-describedby immediately.
|
||||||
|
forceUpdate(this);
|
||||||
|
}
|
||||||
|
|
||||||
this.ionBlur.emit();
|
this.ionBlur.emit();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1067,9 +1082,9 @@ export class Select implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getHintTextID(): string | undefined {
|
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;
|
return errorTextId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1084,14 +1099,14 @@ export class Select implements ComponentInterface {
|
|||||||
* Renders the helper text or error text values
|
* Renders the helper text or error text values
|
||||||
*/
|
*/
|
||||||
private renderHintText() {
|
private renderHintText() {
|
||||||
const { helperText, errorText, helperTextId, errorTextId } = this;
|
const { helperText, errorText, helperTextId, errorTextId, isInvalid } = this;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<div id={helperTextId} class="helper-text" part="supporting-text helper-text">
|
<div id={helperTextId} class="helper-text" part="supporting-text helper-text" aria-live="polite">
|
||||||
{helperText}
|
{isInvalid ? helperText : null}
|
||||||
</div>,
|
</div>,
|
||||||
<div id={errorTextId} class="error-text" part="supporting-text error-text">
|
<div id={errorTextId} class="error-text" part="supporting-text error-text" role="alert">
|
||||||
{errorText}
|
{isInvalid ? errorText : null}
|
||||||
</div>,
|
</div>,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -1115,6 +1130,16 @@ export class Select implements ComponentInterface {
|
|||||||
return <div class="select-bottom">{this.renderHintText()}</div>;
|
return <div class="select-bottom">{this.renderHintText()}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
render() {
|
||||||
const {
|
const {
|
||||||
disabled,
|
disabled,
|
||||||
|
|||||||
190
core/src/components/select/test/validation/index.html
Normal file
190
core/src/components/select/test/validation/index.html
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Select - Validation</title>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
|
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||||
|
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||||
|
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||||
|
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||||
|
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||||
|
<style>
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
grid-row-gap: 30px;
|
||||||
|
grid-column-gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<ion-app>
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>Select - 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>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div>
|
||||||
|
<h2>Required Field</h2>
|
||||||
|
<ion-select
|
||||||
|
id="alert-select"
|
||||||
|
label="Alert"
|
||||||
|
placeholder="Select one"
|
||||||
|
interface="alert"
|
||||||
|
helper-text="You must select an option to continue"
|
||||||
|
error-text="This field is required"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<ion-select-option value="apples">Apples</ion-select-option>
|
||||||
|
<ion-select-option value="oranges">Oranges</ion-select-option>
|
||||||
|
<ion-select-option value="pears">Pears</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Optional Field (No Validation)</h2>
|
||||||
|
<ion-checkbox id="optional-checkbox" helper-text="You can skip this field">Optional Checkbox</ion-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ion-padding">
|
||||||
|
<ion-button id="submit-btn" expand="block" disabled>Submit Form</ion-button>
|
||||||
|
<ion-button id="reset-btn" expand="block" fill="outline">Reset Form</ion-button>
|
||||||
|
</div>
|
||||||
|
</ion-content>
|
||||||
|
</ion-app>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Simple validation logic
|
||||||
|
const checkboxes = document.querySelectorAll('ion-checkbox');
|
||||||
|
const submitBtn = document.getElementById('submit-btn');
|
||||||
|
const resetBtn = document.getElementById('reset-btn');
|
||||||
|
|
||||||
|
// Track which fields have been touched
|
||||||
|
const touchedFields = new Set();
|
||||||
|
|
||||||
|
// Validation functions
|
||||||
|
const validators = {
|
||||||
|
'terms-checkbox': (checked) => {
|
||||||
|
return checked === true;
|
||||||
|
},
|
||||||
|
'optional-checkbox': () => true, // Always valid
|
||||||
|
};
|
||||||
|
|
||||||
|
function validateField(checkbox) {
|
||||||
|
const checkboxId = checkbox.id;
|
||||||
|
const checked = checkbox.checked;
|
||||||
|
const isValid = validators[checkboxId] ? validators[checkboxId](checked) : true;
|
||||||
|
|
||||||
|
// Only show validation state if field has been touched
|
||||||
|
if (touchedFields.has(checkboxId)) {
|
||||||
|
if (isValid) {
|
||||||
|
checkbox.classList.remove('ion-invalid');
|
||||||
|
checkbox.classList.add('ion-valid');
|
||||||
|
} else {
|
||||||
|
checkbox.classList.remove('ion-valid');
|
||||||
|
checkbox.classList.add('ion-invalid');
|
||||||
|
}
|
||||||
|
checkbox.classList.add('ion-touched');
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateForm() {
|
||||||
|
let allValid = true;
|
||||||
|
checkboxes.forEach((checkbox) => {
|
||||||
|
if (checkbox.id !== 'optional-checkbox') {
|
||||||
|
const isValid = validateField(checkbox);
|
||||||
|
if (!isValid) {
|
||||||
|
allValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
submitBtn.disabled = !allValid;
|
||||||
|
return allValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
checkboxes.forEach((checkbox) => {
|
||||||
|
// Mark as touched on blur
|
||||||
|
checkbox.addEventListener('ionBlur', (e) => {
|
||||||
|
console.log('Blur event on:', checkbox.id);
|
||||||
|
touchedFields.add(checkbox.id);
|
||||||
|
validateField(checkbox);
|
||||||
|
validateForm();
|
||||||
|
|
||||||
|
const isInvalid = checkbox.classList.contains('ion-invalid');
|
||||||
|
if (isInvalid) {
|
||||||
|
console.log('Field marked invalid:', checkbox.label, checkbox.errorText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate on change
|
||||||
|
checkbox.addEventListener('ionChange', (e) => {
|
||||||
|
console.log('Change event on:', checkbox.id);
|
||||||
|
if (touchedFields.has(checkbox.id)) {
|
||||||
|
validateField(checkbox);
|
||||||
|
validateForm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset button
|
||||||
|
resetBtn.addEventListener('click', () => {
|
||||||
|
checkboxes.forEach((checkbox) => {
|
||||||
|
checkbox.checked = false;
|
||||||
|
checkbox.classList.remove('ion-valid', 'ion-invalid', 'ion-touched');
|
||||||
|
});
|
||||||
|
touchedFields.clear();
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Submit button
|
||||||
|
submitBtn.addEventListener('click', () => {
|
||||||
|
if (validateForm()) {
|
||||||
|
alert('Form submitted successfully!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial setup
|
||||||
|
validateForm();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user