A booking form that opens when a user clicks a booked slot violates WCAG 2.2 SC 4.1.3 (Status Messages) and creates a silent failure path: the form submits, the server rejects it, and the user has no idea why. Beyond accessibility, silent ignores of non-available slot clicks leave users wondering if the click registered at all — they repeat-click, get confused, and abandon. Assistive technology users cannot detect visually-implied disabled states; they rely on programmatic feedback. Two distinct messages (one for booked, one for blocked) are required because the corrective actions differ.
High because absent click feedback on unavailable slots produces silent failures that leave assistive technology users unable to determine why a booking attempt failed, violating WCAG 2.2 SC 4.1.3.
Add explicit conditional feedback in the slot's click handler — never silently ignore a click on a non-available state. Use a toast library like sonner or an inline message; a console.log does not count.
// src/components/CalendarSlot.tsx
import { toast } from 'sonner'
export function CalendarSlot({ status, time, onSelect }) {
const MESSAGES = {
booked: 'That slot is already booked — pick a different time.',
blocked: 'That time is unavailable — pick a different time.',
past: 'That time has already passed — pick a future slot.',
}
function handleClick() {
if (status === 'available') {
onSelect(time)
} else {
toast.error(MESSAGES[status])
}
}
return (
<button
onClick={handleClick}
aria-disabled={status !== 'available'}
className={status !== 'available' ? 'cursor-not-allowed' : 'cursor-pointer'}
>
{time}
</button>
)
}
Verify that Enter and Space key activation also triggers the feedback, not just mouse clicks.
ID: booking-calendar-availability.calendar-display.unavailable-feedback
Severity: high
What to look for: Count all non-available slot states that a user can click (booked, blocked, past). For each, trace the click handler to verify it produces user-visible feedback. Look for code that handles click events on non-bookable slots — it should show feedback (toast, modal, inline text), not silently ignore the click or navigate to a booking form. Examine the slot component's onClick or onPress handler for conditional branching on status. Examine files matching src/components/*Slot*, src/components/*Calendar*.
Pass criteria: Count all non-available slot states. 100% of non-available slot click handlers must display a feedback message (toast, modal, or inline text) within 500ms explaining why the slot is unavailable. At least 2 distinct messages must exist (one for booked, one for blocked). The feedback mechanism must be visible (not just a console.log). Report: "X of Y non-available states produce visible click feedback."
Fail criteria: Clicking a booked slot shows a booking form anyway, or produces no feedback, or all non-available states silently ignore the click.
Skip (N/A) when: Calendar does not support clicking slots (e.g., display-only, no interactive elements).
Detail on fail: Example: "Clicking a booked slot opens the booking form as if it were available. User must manually check and deselect, or the form submits and errors server-side."
Cross-reference: Check visual-distinction — feedback supplements visual cues; both must be present.
Cross-reference: Check past-date-protection — past dates also need click feedback, not just disabled styling.
Cross-reference: Check keyboard-navigation — feedback should also trigger via Enter/Space key activation, not just mouse click.
Remediation: Add feedback on unavailable slot clicks:
import { toast } from 'sonner'
export function CalendarSlot({ status, time, onAvailableClick }) {
const handleClick = () => {
if (status === 'available') {
onAvailableClick(time)
} else if (status === 'booked') {
toast.error('This slot is booked. Please choose another time.')
} else if (status === 'blocked') {
toast.error('This time is not available. Please choose another time.')
} else if (status === 'past') {
toast.error('This time has passed. Please choose a future slot.')
}
}
return (
<button
onClick={handleClick}
disabled={status !== 'available'}
className={status === 'available' ? 'cursor-pointer' : 'cursor-not-allowed'}
>
{time}
</button>
)
}