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:
@@ -414,11 +414,16 @@ function FeedingTrackPage() {
|
||||
{children.length > 1 && (
|
||||
<Paper sx={{ p: 2, mb: 3 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>{t('common.selectChild')}</InputLabel>
|
||||
<InputLabel id="child-select-label">{t('common.selectChild')}</InputLabel>
|
||||
<Select
|
||||
labelId="child-select-label"
|
||||
value={selectedChild}
|
||||
onChange={(e) => setSelectedChild(e.target.value)}
|
||||
label={t('common.selectChild')}
|
||||
required
|
||||
inputProps={{
|
||||
'aria-required': 'true',
|
||||
}}
|
||||
>
|
||||
{children.map((child) => (
|
||||
<MenuItem key={child.id} value={child.id}>
|
||||
@@ -486,11 +491,16 @@ function FeedingTrackPage() {
|
||||
|
||||
{/* Side Selector */}
|
||||
<FormControl fullWidth sx={{ mb: 3 }}>
|
||||
<InputLabel>{t('feeding.side')}</InputLabel>
|
||||
<InputLabel id="side-select-label">{t('feeding.side')}</InputLabel>
|
||||
<Select
|
||||
labelId="side-select-label"
|
||||
value={side}
|
||||
onChange={(e) => setSide(e.target.value as 'left' | 'right' | 'both')}
|
||||
label={t('feeding.side')}
|
||||
required
|
||||
inputProps={{
|
||||
'aria-required': 'true',
|
||||
}}
|
||||
>
|
||||
<MenuItem value="left">{t('feeding.sides.left')}</MenuItem>
|
||||
<MenuItem value="right">{t('feeding.sides.right')}</MenuItem>
|
||||
@@ -507,6 +517,13 @@ function FeedingTrackPage() {
|
||||
onChange={(e) => setDuration(parseInt(e.target.value) || 0)}
|
||||
sx={{ mb: 3 }}
|
||||
helperText={t('feeding.placeholders.duration')}
|
||||
inputProps={{
|
||||
'aria-describedby': 'duration-helper',
|
||||
min: 0,
|
||||
}}
|
||||
FormHelperTextProps={{
|
||||
id: 'duration-helper',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
@@ -524,11 +541,16 @@ function FeedingTrackPage() {
|
||||
/>
|
||||
|
||||
<FormControl fullWidth sx={{ mb: 3 }}>
|
||||
<InputLabel>{t('feeding.bottleType')}</InputLabel>
|
||||
<InputLabel id="bottle-type-label">{t('feeding.bottleType')}</InputLabel>
|
||||
<Select
|
||||
labelId="bottle-type-label"
|
||||
value={bottleType}
|
||||
onChange={(e) => setBottleType(e.target.value as 'formula' | 'breastmilk' | 'other')}
|
||||
label={t('feeding.bottleType')}
|
||||
required
|
||||
inputProps={{
|
||||
'aria-required': 'true',
|
||||
}}
|
||||
>
|
||||
<MenuItem value="formula">{t('feeding.bottleTypes.formula')}</MenuItem>
|
||||
<MenuItem value="breastmilk">{t('feeding.bottleTypes.breastmilk')}</MenuItem>
|
||||
@@ -548,6 +570,11 @@ function FeedingTrackPage() {
|
||||
onChange={(e) => setFoodDescription(e.target.value)}
|
||||
sx={{ mb: 3 }}
|
||||
placeholder={t('feeding.placeholders.foodDescription')}
|
||||
required
|
||||
inputProps={{
|
||||
'aria-required': 'true',
|
||||
'aria-invalid': !foodDescription && error ? 'true' : 'false',
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
@@ -557,6 +584,12 @@ function FeedingTrackPage() {
|
||||
onChange={(e) => setAmountDescription(e.target.value)}
|
||||
sx={{ mb: 3 }}
|
||||
placeholder={t('feeding.placeholders.amountDescription')}
|
||||
inputProps={{
|
||||
'aria-describedby': 'amount-description-helper',
|
||||
}}
|
||||
FormHelperTextProps={{
|
||||
id: 'amount-description-helper',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
@@ -571,6 +604,12 @@ function FeedingTrackPage() {
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
sx={{ mb: 3 }}
|
||||
placeholder={t('feeding.placeholders.notes')}
|
||||
inputProps={{
|
||||
'aria-describedby': 'notes-helper',
|
||||
}}
|
||||
FormHelperTextProps={{
|
||||
id: 'notes-helper',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Submit Button */}
|
||||
|
||||
@@ -396,11 +396,16 @@ export default function SleepTrackPage() {
|
||||
{children.length > 1 && (
|
||||
<Paper sx={{ p: 2, mb: 3 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>{t('common.selectChild')}</InputLabel>
|
||||
<InputLabel id="sleep-child-select-label">{t('common.selectChild')}</InputLabel>
|
||||
<Select
|
||||
labelId="sleep-child-select-label"
|
||||
value={selectedChild}
|
||||
onChange={(e) => setSelectedChild(e.target.value)}
|
||||
label={t('common.selectChild')}
|
||||
required
|
||||
inputProps={{
|
||||
'aria-required': 'true',
|
||||
}}
|
||||
>
|
||||
{children.map((child) => (
|
||||
<MenuItem key={child.id} value={child.id}>
|
||||
@@ -416,7 +421,7 @@ export default function SleepTrackPage() {
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
{/* Start Time */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle1" fontWeight="600" sx={{ mb: 1 }}>
|
||||
<Typography variant="subtitle1" fontWeight="600" sx={{ mb: 1 }} id="start-time-label">
|
||||
{t('sleep.startTime')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-start' }}>
|
||||
@@ -426,6 +431,11 @@ export default function SleepTrackPage() {
|
||||
value={startTime}
|
||||
onChange={(e) => setStartTime(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
required
|
||||
inputProps={{
|
||||
'aria-required': 'true',
|
||||
'aria-labelledby': 'start-time-label',
|
||||
}}
|
||||
/>
|
||||
<Button variant="outlined" onClick={setStartNow} sx={{ minWidth: 100 }}>
|
||||
{t('sleep.now')}
|
||||
@@ -436,11 +446,16 @@ export default function SleepTrackPage() {
|
||||
{/* Ongoing Checkbox */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>{t('sleep.status.title')}</InputLabel>
|
||||
<InputLabel id="sleep-status-label">{t('sleep.status.title')}</InputLabel>
|
||||
<Select
|
||||
labelId="sleep-status-label"
|
||||
value={isOngoing ? 'ongoing' : 'completed'}
|
||||
onChange={(e) => setIsOngoing(e.target.value === 'ongoing')}
|
||||
label={t('sleep.status.title')}
|
||||
required
|
||||
inputProps={{
|
||||
'aria-required': 'true',
|
||||
}}
|
||||
>
|
||||
<MenuItem value="completed">{t('sleep.status.completed')}</MenuItem>
|
||||
<MenuItem value="ongoing">{t('sleep.status.ongoing')}</MenuItem>
|
||||
@@ -451,7 +466,7 @@ export default function SleepTrackPage() {
|
||||
{/* End Time */}
|
||||
{!isOngoing && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle1" fontWeight="600" sx={{ mb: 1 }}>
|
||||
<Typography variant="subtitle1" fontWeight="600" sx={{ mb: 1 }} id="end-time-label">
|
||||
{t('sleep.endTime')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-start' }}>
|
||||
@@ -461,6 +476,11 @@ export default function SleepTrackPage() {
|
||||
value={endTime}
|
||||
onChange={(e) => setEndTime(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
required
|
||||
inputProps={{
|
||||
'aria-required': 'true',
|
||||
'aria-labelledby': 'end-time-label',
|
||||
}}
|
||||
/>
|
||||
<Button variant="outlined" onClick={setEndNow} sx={{ minWidth: 100 }}>
|
||||
{t('sleep.now')}
|
||||
@@ -482,8 +502,9 @@ export default function SleepTrackPage() {
|
||||
|
||||
{/* Sleep Quality */}
|
||||
<FormControl fullWidth sx={{ mb: 3 }}>
|
||||
<InputLabel>{t('sleep.quality')}</InputLabel>
|
||||
<InputLabel id="sleep-quality-label">{t('sleep.quality')}</InputLabel>
|
||||
<Select
|
||||
labelId="sleep-quality-label"
|
||||
value={quality}
|
||||
onChange={(e) => setQuality(e.target.value as 'excellent' | 'good' | 'fair' | 'poor')}
|
||||
label={t('sleep.quality')}
|
||||
@@ -497,8 +518,9 @@ export default function SleepTrackPage() {
|
||||
|
||||
{/* Location */}
|
||||
<FormControl fullWidth sx={{ mb: 3 }}>
|
||||
<InputLabel>{t('sleep.location')}</InputLabel>
|
||||
<InputLabel id="sleep-location-label">{t('sleep.location')}</InputLabel>
|
||||
<Select
|
||||
labelId="sleep-location-label"
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
label={t('sleep.location')}
|
||||
@@ -521,6 +543,12 @@ export default function SleepTrackPage() {
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
sx={{ mb: 3 }}
|
||||
placeholder={t('sleep.placeholders.notes')}
|
||||
inputProps={{
|
||||
'aria-describedby': 'sleep-notes-helper',
|
||||
}}
|
||||
FormHelperTextProps={{
|
||||
id: 'sleep-notes-helper',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Submit Button */}
|
||||
|
||||
Reference in New Issue
Block a user