# Clicking a booked or blocked slot shows clear unavailable message

- **Pattern:** `ab-000521` (`booking-calendar-availability.calendar-display.unavailable-feedback`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000521
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000521)

## Why it matters

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.

## Severity rationale

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.

## Remediation

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.

```tsx
// 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.

## Detection

- **ID:** `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:

  ```tsx
  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>
    )
  }
  ```

## External references

- wcag 4.1.3

Taxons: user-experience

HTML version: https://auditbuffet.com/patterns/ab-000521
