diff --git a/src/calendar/CalendarButton.ts b/src/calendar/CalendarButton.ts index c28481a9e..c7689fafb 100644 --- a/src/calendar/CalendarButton.ts +++ b/src/calendar/CalendarButton.ts @@ -27,7 +27,7 @@ export const useCalendarButton = createHook< goto, } = options; - const TYPES = { + const HANDLER_TYPES = { nextMonth: { handler: focusNextMonth, ariaLabel: "Next Month", @@ -47,8 +47,8 @@ export const useCalendarButton = createHook< }; return { - "aria-label": TYPES[goto]?.ariaLabel, - onClick: callAllHandlers(htmlOnClick, TYPES[goto]?.handler), + "aria-label": HANDLER_TYPES[goto]?.ariaLabel, + onClick: callAllHandlers(htmlOnClick, HANDLER_TYPES[goto]?.handler), ...htmlProps, }; }, @@ -60,16 +60,16 @@ export const CalendarButton = createComponent({ useHook: useCalendarButton, }); -export type CalendarButtonOptions = { - goto: CalendarGoto; -} & Pick< - CalendarStateReturn, - | "focusNextMonth" - | "focusPreviousMonth" - | "focusPreviousYear" - | "focusNextYear" -> & - ButtonOptions; +export type CalendarButtonOptions = ButtonOptions & + Pick< + CalendarStateReturn, + | "focusNextMonth" + | "focusPreviousMonth" + | "focusPreviousYear" + | "focusNextYear" + > & { + goto: CalendarGoto; + }; export type CalendarButtonHTMLProps = ButtonHTMLProps; diff --git a/src/calendar/CalendarCellButton.ts b/src/calendar/CalendarCellButton.ts index 79fc93d4b..663af6bff 100644 --- a/src/calendar/CalendarCellButton.ts +++ b/src/calendar/CalendarCellButton.ts @@ -92,39 +92,37 @@ export const useCalendarCellButton = createHook< }); // aria-label should be localize Day of week, Month, Day and Year without Time. - let ariaLabel = dateFormatter.format(date); - if (isToday) { - // If date is today, set appropriate string depending on selected state: - ariaLabel = isSelected - ? `Today, ${ariaLabel} selected` - : `Today, ${ariaLabel}`; - } else if (isSelected) { - // If date is selected but not today: - ariaLabel = `${ariaLabel} selected`; - } - - // When a cell is focused and this is a range calendar, add a prompt to help - // screenreader users know that they are in a range selection mode. - if (options.isRangeCalendar && isFocused && !isDisabled) { - let rangeSelectionPrompt = ""; - - // If selection has started add "click to finish selecting range" - if (anchorDate) { - rangeSelectionPrompt = "click to finish selecting range"; - // Otherwise, add "click to start selecting range" prompt - } else { - rangeSelectionPrompt = "click to start selecting range"; + function getAriaLabel() { + let ariaLabel = dateFormatter.format(date); + const isTodayLabel = isToday ? "Today, " : ""; + const isSelctedLabel = isSelected ? " selected" : ""; + ariaLabel = `${isTodayLabel}${ariaLabel}${isSelctedLabel}`; + + // When a cell is focused and this is a range calendar, add a prompt to help + // screenreader users know that they are in a range selection mode. + if (options.isRangeCalendar && isFocused && !isDisabled) { + let rangeSelectionPrompt = ""; + + // If selection has started add "click to finish selecting range" + if (anchorDate) { + rangeSelectionPrompt = "click to finish selecting range"; + // Otherwise, add "click to start selecting range" prompt + } else { + rangeSelectionPrompt = "click to start selecting range"; + } + + // Append to aria-label + if (rangeSelectionPrompt) { + ariaLabel = `${ariaLabel} (${rangeSelectionPrompt})`; + } } - // Append to aria-label - if (rangeSelectionPrompt) { - ariaLabel = `${ariaLabel} (${rangeSelectionPrompt})`; - } + return ariaLabel; } return { children: useDateFormatter({ day: "numeric" }).format(date), - "aria-label": ariaLabel, + "aria-label": getAriaLabel(), tabIndex: !disabled ? (isSameDay(date, focusedDate) ? 0 : -1) : undefined, ref: useForkRef(ref, htmlRef), onClick: callAllHandlers(htmlOnClick, onClick), @@ -140,23 +138,23 @@ export const CalendarCellButton = createComponent({ useHook: useCalendarCellButton, }); -export type CalendarCellButtonOptions = { - date: Date; -} & Pick< - CalendarStateReturn, - | "focusedDate" - | "selectDate" - | "setFocusedDate" - | "isDisabled" - | "month" - | "minDate" - | "maxDate" - | "dateValue" - | "isFocused" - | "isRangeCalendar" -> & +export type CalendarCellButtonOptions = ButtonOptions & Partial> & - ButtonOptions; + Pick< + CalendarStateReturn, + | "focusedDate" + | "selectDate" + | "setFocusedDate" + | "isDisabled" + | "month" + | "minDate" + | "maxDate" + | "dateValue" + | "isFocused" + | "isRangeCalendar" + > & { + date: Date; + }; export type CalendarCellButtonHTMLProps = ButtonHTMLProps; diff --git a/src/calendar/CalendarState.ts b/src/calendar/CalendarState.ts index e85ca6078..9e1ec1bdc 100644 --- a/src/calendar/CalendarState.ts +++ b/src/calendar/CalendarState.ts @@ -62,13 +62,15 @@ export function useCalendarState( defaultValue: parseDate(defaultValueProp), onChange, }); + const monthFormatter = useDateFormatter({ month: "long", year: "numeric" }); + const initialMonth = value ?? new Date(); const minValue = parseDate(minValueProp); const maxValue = parseDate(maxValueProp); - const initialMonth = value ?? new Date(); const [currentMonth, setCurrentMonth] = React.useState(initialMonth); const [focusedDate, setFocusedDate] = React.useState(initialMonth); + const [isFocused, setFocused] = React.useState(autoFocus); const month = currentMonth.getMonth(); const year = currentMonth.getFullYear(); @@ -128,10 +130,6 @@ export function useCalendarState( } } - const [isFocused, setFocused] = React.useState(autoFocus); - - const monthFormatter = useDateFormatter({ month: "long", year: "numeric" }); - // Announce when the current month changes useUpdateEffect(() => { // announce the new month with a change from the Previous or Next button diff --git a/src/calendar/__tests__/Calendar.test.tsx b/src/calendar/__tests__/Calendar.test.tsx index a30cea9aa..2c040af0a 100644 --- a/src/calendar/__tests__/Calendar.test.tsx +++ b/src/calendar/__tests__/Calendar.test.tsx @@ -226,6 +226,25 @@ describe("Calendar", () => { expect(label(/^saturday, october 31, 2020$/i)).toHaveFocus(); }); + it("should have proper aria-label for calendar cell button", () => { + MockDate.set(new Date(2021, 7, 10)); + render(); + + screen.getByRole("button", { + name: /today, tuesday, august 10, 2021/i, + }); + + repeat(press.Tab, 5); + press.Enter(); + screen.getByRole("button", { + name: /today, tuesday, august 10, 2021 selected/i, + }); + press.ArrowRight(); + expect(screen.getByLabelText(/wednesday, august 11, 2021/i)).toHaveFocus(); + + MockDate.reset(); + }); + test("Calendar renders with no a11y violations", async () => { const { container } = render(); const results = await axe(container); diff --git a/src/calendar/helpers/index.ts b/src/calendar/helpers/index.ts index 997f521eb..9440b2966 100644 --- a/src/calendar/helpers/index.ts +++ b/src/calendar/helpers/index.ts @@ -14,7 +14,7 @@ export function useWeekDays(weekStart: number) { const dateDay = setDay(Date.now(), (index + weekStart) % 7); const day = dayFormatter.format(dateDay); const dayLong = dayFormatterLong.format(dateDay); - return { title: dayLong, abbr: day }; + return { title: dayLong, abbr: day } as const; }); }