feat: Complete form accessibility enhancement (WCAG 2.1 AA)
Implemented comprehensive form accessibility improvements across all critical forms:
**Accessibility Attributes Added:**
- aria-required="true" on all required form fields
- aria-invalid on fields with validation errors
- aria-describedby linking error messages to inputs
- Unique id attributes on FormHelperText for error association
- role="alert" on error messages for screen reader announcements
- labelId on Select components for proper label association
- noValidate on forms to use custom validation
**Forms Updated:**
1. Login Form (app/(auth)/login/page.tsx)
- Email and password fields with full ARIA support
- Error message association with aria-describedby
2. Registration Form (app/(auth)/register/page.tsx)
- All text fields: name, email, password, DOB, parental email
- Checkbox fields: Terms, Privacy, COPPA consent
- Conditional required fields for minors
3. Child Dialog (components/children/ChildDialog.tsx)
- Name, birthdate, gender fields
- Dynamic aria-invalid based on validation state
4. Tracking Forms:
- Feeding form (app/track/feeding/page.tsx)
- Child selector, side selector, bottle type
- Food description with required validation
- Sleep form (app/track/sleep/page.tsx)
- Child selector, start/end time fields
- Quality and location selectors
**WCAG 2.1 Compliance:**
- ✅ 3.3.2 Labels or Instructions (AA)
- ✅ 4.1.3 Status Messages (AA)
- ✅ 1.3.1 Info and Relationships (A)
- ✅ 3.3.1 Error Identification (A)
**Documentation:**
- Updated REMAINING_FEATURES.md
- Marked Form Accessibility Enhancement as complete
- Status: 79 features completed (57%)
🎉 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -191,7 +191,7 @@ export default function RegisterPage() {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Box component="form" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Box component="form" onSubmit={handleSubmit(onSubmit)} noValidate>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Full Name"
|
||||
@@ -200,9 +200,19 @@ export default function RegisterPage() {
|
||||
helperText={errors.name?.message}
|
||||
{...register('name')}
|
||||
disabled={isLoading}
|
||||
required
|
||||
inputProps={{
|
||||
'aria-required': 'true',
|
||||
'aria-invalid': !!errors.name,
|
||||
'aria-describedby': errors.name ? 'name-error' : undefined,
|
||||
}}
|
||||
InputProps={{
|
||||
sx: { borderRadius: 3 },
|
||||
}}
|
||||
FormHelperTextProps={{
|
||||
id: errors.name ? 'name-error' : undefined,
|
||||
role: errors.name ? 'alert' : undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
@@ -214,10 +224,20 @@ export default function RegisterPage() {
|
||||
helperText={errors.email?.message}
|
||||
{...register('email')}
|
||||
disabled={isLoading}
|
||||
inputProps={{ autoComplete: 'username' }}
|
||||
required
|
||||
inputProps={{
|
||||
autoComplete: 'username',
|
||||
'aria-required': 'true',
|
||||
'aria-invalid': !!errors.email,
|
||||
'aria-describedby': errors.email ? 'email-error' : undefined,
|
||||
}}
|
||||
InputProps={{
|
||||
sx: { borderRadius: 3 },
|
||||
}}
|
||||
FormHelperTextProps={{
|
||||
id: errors.email ? 'email-error' : undefined,
|
||||
role: errors.email ? 'alert' : undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
@@ -229,7 +249,13 @@ export default function RegisterPage() {
|
||||
helperText={errors.password?.message}
|
||||
{...register('password')}
|
||||
disabled={isLoading}
|
||||
inputProps={{ autoComplete: 'new-password' }}
|
||||
required
|
||||
inputProps={{
|
||||
autoComplete: 'new-password',
|
||||
'aria-required': 'true',
|
||||
'aria-invalid': !!errors.password,
|
||||
'aria-describedby': errors.password ? 'password-error' : undefined,
|
||||
}}
|
||||
InputProps={{
|
||||
sx: { borderRadius: 3 },
|
||||
endAdornment: (
|
||||
@@ -238,12 +264,17 @@ export default function RegisterPage() {
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
edge="end"
|
||||
disabled={isLoading}
|
||||
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
FormHelperTextProps={{
|
||||
id: errors.password ? 'password-error' : undefined,
|
||||
role: errors.password ? 'alert' : undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
@@ -255,7 +286,13 @@ export default function RegisterPage() {
|
||||
helperText={errors.confirmPassword?.message}
|
||||
{...register('confirmPassword')}
|
||||
disabled={isLoading}
|
||||
inputProps={{ autoComplete: 'new-password' }}
|
||||
required
|
||||
inputProps={{
|
||||
autoComplete: 'new-password',
|
||||
'aria-required': 'true',
|
||||
'aria-invalid': !!errors.confirmPassword,
|
||||
'aria-describedby': errors.confirmPassword ? 'confirm-password-error' : undefined,
|
||||
}}
|
||||
InputProps={{
|
||||
sx: { borderRadius: 3 },
|
||||
endAdornment: (
|
||||
@@ -264,12 +301,17 @@ export default function RegisterPage() {
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
edge="end"
|
||||
disabled={isLoading}
|
||||
aria-label={showConfirmPassword ? 'Hide password' : 'Show password'}
|
||||
>
|
||||
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
FormHelperTextProps={{
|
||||
id: errors.confirmPassword ? 'confirm-password-error' : undefined,
|
||||
role: errors.confirmPassword ? 'alert' : undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
@@ -281,12 +323,22 @@ export default function RegisterPage() {
|
||||
helperText={errors.dateOfBirth?.message || 'Required for COPPA compliance (users under 13 cannot register)'}
|
||||
{...register('dateOfBirth')}
|
||||
disabled={isLoading}
|
||||
required
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
inputProps={{
|
||||
'aria-required': 'true',
|
||||
'aria-invalid': !!errors.dateOfBirth,
|
||||
'aria-describedby': errors.dateOfBirth ? 'dob-error' : 'dob-helper',
|
||||
}}
|
||||
InputProps={{
|
||||
sx: { borderRadius: 3 },
|
||||
}}
|
||||
FormHelperTextProps={{
|
||||
id: errors.dateOfBirth ? 'dob-error' : 'dob-helper',
|
||||
role: errors.dateOfBirth ? 'alert' : undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
{userAge !== null && userAge < 13 && (
|
||||
@@ -309,15 +361,31 @@ export default function RegisterPage() {
|
||||
helperText={errors.parentalEmail?.message || 'We will send a consent email to your parent/guardian'}
|
||||
{...register('parentalEmail')}
|
||||
disabled={isLoading}
|
||||
required
|
||||
inputProps={{
|
||||
'aria-required': 'true',
|
||||
'aria-invalid': !!errors.parentalEmail,
|
||||
'aria-describedby': errors.parentalEmail ? 'parental-email-error' : 'parental-email-helper',
|
||||
}}
|
||||
InputProps={{
|
||||
sx: { borderRadius: 3 },
|
||||
}}
|
||||
FormHelperTextProps={{
|
||||
id: errors.parentalEmail ? 'parental-email-error' : 'parental-email-helper',
|
||||
role: errors.parentalEmail ? 'alert' : undefined,
|
||||
}}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
{...register('coppaConsent')}
|
||||
disabled={isLoading}
|
||||
required
|
||||
inputProps={{
|
||||
'aria-required': 'true',
|
||||
'aria-invalid': !!errors.coppaConsent,
|
||||
'aria-describedby': errors.coppaConsent ? 'coppa-consent-error' : undefined,
|
||||
} as any}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
@@ -328,7 +396,7 @@ export default function RegisterPage() {
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
{errors.coppaConsent && (
|
||||
<Typography variant="caption" color="error" sx={{ display: 'block', mt: 1 }}>
|
||||
<Typography variant="caption" color="error" sx={{ display: 'block', mt: 1 }} id="coppa-consent-error" role="alert">
|
||||
Parental consent is required for users under 18
|
||||
</Typography>
|
||||
)}
|
||||
@@ -341,6 +409,12 @@ export default function RegisterPage() {
|
||||
<Checkbox
|
||||
{...register('agreeToTerms')}
|
||||
disabled={isLoading}
|
||||
required
|
||||
inputProps={{
|
||||
'aria-required': 'true',
|
||||
'aria-invalid': !!errors.agreeToTerms,
|
||||
'aria-describedby': errors.agreeToTerms ? 'terms-error' : undefined,
|
||||
} as any}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
@@ -353,7 +427,7 @@ export default function RegisterPage() {
|
||||
}
|
||||
/>
|
||||
{errors.agreeToTerms && (
|
||||
<Typography variant="caption" color="error" sx={{ display: 'block', mt: 0.5, ml: 4 }}>
|
||||
<Typography variant="caption" color="error" sx={{ display: 'block', mt: 0.5, ml: 4 }} id="terms-error" role="alert">
|
||||
{errors.agreeToTerms.message}
|
||||
</Typography>
|
||||
)}
|
||||
@@ -363,6 +437,12 @@ export default function RegisterPage() {
|
||||
<Checkbox
|
||||
{...register('agreeToPrivacy')}
|
||||
disabled={isLoading}
|
||||
required
|
||||
inputProps={{
|
||||
'aria-required': 'true',
|
||||
'aria-invalid': !!errors.agreeToPrivacy,
|
||||
'aria-describedby': errors.agreeToPrivacy ? 'privacy-error' : undefined,
|
||||
} as any}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
@@ -375,7 +455,7 @@ export default function RegisterPage() {
|
||||
}
|
||||
/>
|
||||
{errors.agreeToPrivacy && (
|
||||
<Typography variant="caption" color="error" sx={{ display: 'block', mt: 0.5, ml: 4 }}>
|
||||
<Typography variant="caption" color="error" sx={{ display: 'block', mt: 0.5, ml: 4 }} id="privacy-error" role="alert">
|
||||
{errors.agreeToPrivacy.message}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user