Skip to content

fix(ui): add ARIA labels to booking calendar date cells, weekday headers, and time slot buttons#28776

Open
LubaKaper wants to merge 5 commits intocalcom:mainfrom
LubaKaper:fix/calendar-aria-labels
Open

fix(ui): add ARIA labels to booking calendar date cells, weekday headers, and time slot buttons#28776
LubaKaper wants to merge 5 commits intocalcom:mainfrom
LubaKaper:fix/calendar-aria-labels

Conversation

@LubaKaper
Copy link
Copy Markdown

@LubaKaper LubaKaper commented Apr 7, 2026

What does this PR do?

Screen readers announced no meaningful context when navigating Cal.com's booking calendar. Date buttons read only a
number ("4"), and time slot buttons read only the time ("1:30pm") — giving users no month, year, or booking context.

This PR adds descriptive aria-label attributes to both elements so screen reader users can navigate the booking flow
independently.

Visual Demo

Image Demo:

Date button — Before: Name: "4", no aria-label, no context

before_available_date

Date button — After: Name: "April 9, 2026" / Name: "April 11, 2026, unavailable" for disabled dates

after_available_date after_disabled_date

Time slot button — Before: Name: "1:30pm", no aria-label, no booking context

before_timeslot

Time slot button — After: Name: "Book 9:45am"

after_timeslot

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change.
    N/A
  • I confirm automated tests are in place that prove my fix is effective or that my feature works. Note: ARIA label
    behavior is validated manually via VoiceOver and DevTools Accessibility tab. No automated test framework currently
    covers screen reader output in this codebase.

How should this be tested?

  1. Navigate to any booking page (e.g. localhost:3000/[username]/15min)
  2. Open DevTools → Accessibility tab
  3. Inspect a date button → Name should show "April 9, 2026"
  4. Inspect a disabled date → Name should show "April 9, 2026, unavailable"
  5. Inspect a time slot button → Name should show "Book 9:45am"

Alternatively enable VoiceOver (Mac: Cmd+F5) and tab through the calendar to hear the labels read aloud.

No environment variables required. Any event type with available slots works.

Checklist

  • My code follows the style guidelines of this project
  • My changes generate no new errors (pre-existing Biome warnings in files, none introduced by this PR)
  • PR is small and focused (3 files, ~30 lines changed)

@github-actions github-actions bot added the 🐛 bug Something isn't working label Apr 7, 2026
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 7, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
2 out of 3 committers have signed the CLA.

✅ m1lestones
✅ LubaKaper
❌ Luba Kaper


Luba Kaper seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

…context

Fixes the accessibility bug where time slot buttons only announced the
time string (e.g. "1:30pm") with no booking context for screen readers.

Buttons now announce "Book 1:30 PM" for available slots and
"1:30 PM, unavailable" for full or taken slots.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@pull-request-size pull-request-size bot added size/L and removed size/S labels Apr 7, 2026
@LubaKaper LubaKaper marked this pull request as ready for review April 8, 2026 00:38
@LubaKaper LubaKaper requested review from a team as code owners April 8, 2026 00:38
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

Adds accessibility labels and tests for booking and calendar UI. Introduces apps/web/modules/bookings/components/AvailableTimes.test.tsx. Updates AvailableTimes.tsx to set dynamic aria-label on time slot buttons based on availability. Updates DatePicker.tsx to add aria-label on day buttons and reorganize imports. Updates calendar Calendar.tsx to provide labelDay/labelWeekday values that include formatted dates and unavailable suffixes. Adds two i18n keys (book_time_slot, time_slot_unavailable_label) to English locale.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary change: adding ARIA labels to calendar date cells, weekday headers, and time slot buttons for improved accessibility.
Description check ✅ Passed The description clearly explains what the PR does, why it matters for screen reader users, provides visual before/after demos, testing steps, and references the linked issue.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from issue #28772: aria-labels added to weekday headers with full weekday names, date cells with full date context, disabled dates marked unavailable, and time slots with booking context.
Out of Scope Changes check ✅ Passed All changes are directly scoped to accessibility improvements for calendar elements. The addition of i18n strings and test coverage are supporting changes directly related to the stated objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/ui/components/form/date-range-picker/Calendar.tsx (1)

62-65: Hardcoded weekday names bypass localization.

The labelWeekday function uses hardcoded English weekday names, which won't translate for non-English users. Consider using date-fns locale-aware formatting or the Intl.DateTimeFormat API for consistency with the rest of the codebase.

♻️ Proposed fix using Intl.DateTimeFormat
       labelWeekday: (day) => {
-        const fullNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
-        return fullNames[day.getDay()];
+        return new Intl.DateTimeFormat("en-US", { weekday: "long" }).format(day);
       },

Note: To support the user's locale, you'd need to pass a locale prop to Calendar and use it here. For now, this at least uses a standard API that can be extended.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/components/form/date-range-picker/Calendar.tsx` around lines 62 -
65, The labelWeekday implementation in Calendar currently returns hardcoded
English names; update the labelWeekday function to produce locale-aware weekday
labels (e.g., via Intl.DateTimeFormat or date-fns format) and accept a locale
parameter for the Calendar component so it can format using the user's locale;
replace the hardcoded fullNames logic in labelWeekday with a call to new
Intl.DateTimeFormat(locale, { weekday: 'long' }).format(day) (or the equivalent
date-fns call) and ensure the Calendar component accepts and forwards the locale
prop to this formatter.
apps/web/modules/bookings/components/AvailableTimes.test.tsx (1)

9-12: The framer-motion mock appears unnecessary.

This mock imports the original module and re-exports it unchanged, which has no effect. If no mocking is needed, consider removing it. If the intent was to mock specific exports like AnimatePresence or m, update accordingly.

♻️ Option 1: Remove the mock if not needed
-vi.mock("framer-motion", async (importOriginal) => {
-  const actual = (await importOriginal()) as typeof import("framer-motion");
-  return { ...actual };
-});
-
♻️ Option 2: If you need to suppress animations for test stability
 vi.mock("framer-motion", async (importOriginal) => {
   const actual = (await importOriginal()) as typeof import("framer-motion");
-  return { ...actual };
+  return {
+    ...actual,
+    AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
+    m: {
+      div: ({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) => <div {...props}>{children}</div>,
+    },
+  };
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/modules/bookings/components/AvailableTimes.test.tsx` around lines 9
- 12, The vi.mock call in AvailableTimes.test.tsx currently re-imports and
re-exports the original framer-motion module (vi.mock("framer-motion", ...))
which is a no-op; either remove this vi.mock block entirely if no mocking is
needed, or replace it with a targeted mock that stubs the specific exports used
in the tests (e.g., mock AnimatePresence to render children directly and mock
the motion component alias like m to a simple passthrough) so tests are stable
without recreating the whole module.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/modules/bookings/components/AvailableTimes.test.tsx`:
- Around line 9-12: The vi.mock call in AvailableTimes.test.tsx currently
re-imports and re-exports the original framer-motion module
(vi.mock("framer-motion", ...)) which is a no-op; either remove this vi.mock
block entirely if no mocking is needed, or replace it with a targeted mock that
stubs the specific exports used in the tests (e.g., mock AnimatePresence to
render children directly and mock the motion component alias like m to a simple
passthrough) so tests are stable without recreating the whole module.

In `@packages/ui/components/form/date-range-picker/Calendar.tsx`:
- Around line 62-65: The labelWeekday implementation in Calendar currently
returns hardcoded English names; update the labelWeekday function to produce
locale-aware weekday labels (e.g., via Intl.DateTimeFormat or date-fns format)
and accept a locale parameter for the Calendar component so it can format using
the user's locale; replace the hardcoded fullNames logic in labelWeekday with a
call to new Intl.DateTimeFormat(locale, { weekday: 'long' }).format(day) (or the
equivalent date-fns call) and ensure the Calendar component accepts and forwards
the locale prop to this formatter.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4c564069-bdba-4cca-90eb-f3a9ea2f247c

📥 Commits

Reviewing files that changed from the base of the PR and between 3c52f57 and 1a90b4b.

📒 Files selected for processing (5)
  • apps/web/modules/bookings/components/AvailableTimes.test.tsx
  • apps/web/modules/bookings/components/AvailableTimes.tsx
  • packages/features/calendars/components/DatePicker.tsx
  • packages/i18n/locales/en/common.json
  • packages/ui/components/form/date-range-picker/Calendar.tsx

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/web/modules/bookings/components/AvailableTimes.test.tsx (1)

70-81: Consider more precise assertions for regression safety.

The regex-based assertions (/^Book /i, /, unavailable$/i) are valid but loose. Using exact expected labels would catch subtle formatting regressions (e.g., wrong time zone, missing space).

♻️ Suggested more precise assertions
   it("renders time slot button with Book aria-label for an available slot", () => {
     render(
       <AvailableTimes
         slots={[mockSlot]}
         event={mockEvent}
         unavailableTimeSlots={[]}
         skipConfirmStep={false}
       />
     );
 
-    expect(screen.getByRole("button", { name: /^Book /i })).toBeInTheDocument();
+    expect(screen.getByRole("button", { name: "Book 1:30pm" })).toBeInTheDocument();
   });
 
   it("renders time slot button with unavailable aria-label when slot is in unavailableTimeSlots", () => {
     render(
       <AvailableTimes
         slots={[mockSlot]}
         event={mockEvent}
         unavailableTimeSlots={[mockSlot.time]}
         skipConfirmStep={false}
       />
     );
 
-    expect(screen.getByRole("button", { name: /, unavailable$/i })).toBeInTheDocument();
+    expect(screen.getByRole("button", { name: "1:30pm, unavailable" })).toBeInTheDocument();
   });

Also applies to: 83-94

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/modules/bookings/components/AvailableTimes.test.tsx` around lines 70
- 81, The test uses loose regex assertions for the button aria-label; update the
assertions in AvailableTimes.test.tsx to assert the exact expected aria-label
text for the rendered buttons (use the exact string that includes the full
label, time and timezone) instead of /^Book /i and /, unavailable$/i so
regressions in formatting are caught; locate the tests that render
<AvailableTimes slots={[mockSlot]} event={mockEvent} ... /> (references:
AvailableTimes component, mockSlot, mockEvent) and replace the regex-based
getByRole name matchers with exact name strings for both the available-slot
assertion and the unavailable-slot assertion (also update the second test around
the other block noted at lines 83-94).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/modules/bookings/components/AvailableTimes.test.tsx`:
- Around line 70-81: The test uses loose regex assertions for the button
aria-label; update the assertions in AvailableTimes.test.tsx to assert the exact
expected aria-label text for the rendered buttons (use the exact string that
includes the full label, time and timezone) instead of /^Book /i and /,
unavailable$/i so regressions in formatting are caught; locate the tests that
render <AvailableTimes slots={[mockSlot]} event={mockEvent} ... /> (references:
AvailableTimes component, mockSlot, mockEvent) and replace the regex-based
getByRole name matchers with exact name strings for both the available-slot
assertion and the unavailable-slot assertion (also update the second test around
the other block noted at lines 83-94).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5ecc7bd6-f8ea-4d84-8107-5107cb44000b

📥 Commits

Reviewing files that changed from the base of the PR and between 1a90b4b and 466632d.

📒 Files selected for processing (2)
  • apps/web/modules/bookings/components/AvailableTimes.test.tsx
  • packages/ui/components/form/date-range-picker/Calendar.tsx
✅ Files skipped from review due to trivial changes (1)
  • packages/ui/components/form/date-range-picker/Calendar.tsx

@LubaKaper
Copy link
Copy Markdown
Author

CLA signed as LubaKaper. Some commits were made with local machine emails — I've added them to my GitHub account.
If the CLA bot still shows as pending, please manually verify — the CLA has been signed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bug Something isn't working size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(ui): add ARIA labels to booking calendar date cells, weekday headers, and time slot buttons

3 participants