@codecademy/gamut
68.2.268.2.3-alpha.93a7da.0
dist/DatePicker/DatePickerCalendar/Calendar/CalendarBody.js+
dist/DatePicker/DatePickerCalendar/Calendar/CalendarBody.jsNew file+186
Index: package/dist/DatePicker/DatePickerCalendar/Calendar/CalendarBody.js
===================================================================
--- package/dist/DatePicker/DatePickerCalendar/Calendar/CalendarBody.js
+++ package/dist/DatePicker/DatePickerCalendar/Calendar/CalendarBody.js
@@ -0,0 +1,186 @@
+import { useCallback, useLayoutEffect, useMemo, useRef } from 'react';
+import * as React from 'react';
+import { useIsoFirstWeekday, useResolvedLocale } from '../../utils/locale';
+import { getDatesWithRow, getMonthGrid, isDateDisabled, isDateInRange, isSameDay, normalizeDate } from './utils/dateGrid';
+import { CalendarTable, DateCell, TableHeader } from './utils/elements';
+import { formatDateForAriaLabel, getWeekdayNames } from './utils/format';
+import { keyHandler } from './utils/keyHandler';
+import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
+export const CalendarBody = ({
+ displayDate,
+ selectedDate,
+ endDate = null,
+ shouldDisableDate,
+ onDateSelect,
+ locale,
+ weekStartsOn,
+ labelledById,
+ focusedDate,
+ onFocusedDateChange,
+ onDisplayDateChange,
+ onEscapeKeyPress,
+ hasAdjacentMonthRight,
+ hasAdjacentMonthLeft,
+ focusGridSync,
+ calendarKeyboardSurfaceRef
+}) => {
+ const resolvedLocale = useResolvedLocale(locale);
+ const firstWeekday = useIsoFirstWeekday(resolvedLocale, weekStartsOn);
+ const year = displayDate.getFullYear();
+ const month = displayDate.getMonth();
+ const weeks = getMonthGrid({
+ year,
+ month,
+ firstWeekday
+ });
+ const weekdayLabels = getWeekdayNames({
+ format: 'short',
+ locale: resolvedLocale,
+ firstWeekday
+ });
+ const weekdayFullNames = getWeekdayNames({
+ format: 'long',
+ locale: resolvedLocale,
+ firstWeekday
+ });
+ const buttonRefs = useRef(new Map());
+ const tableRef = useRef(null);
+ const datesWithRow = useMemo(() => getDatesWithRow(weeks), [weeks]);
+ const focusTarget = focusedDate ?? selectedDate;
+ const isToday = useCallback(date => date !== null && isSameDay(date, new Date()), []);
+ const focusButton = useCallback(date => {
+ if (date === null) return false;
+ const key = normalizeDate(date);
+ const el = buttonRefs.current.get(key);
+ if (!el) return false;
+ el.focus();
+ return true;
+ }, []);
+ useLayoutEffect(() => {
+ // Keep the roving tabindex / focused day aligned with `focusTarget` when it makes sense for a11y.
+ if (focusTarget === null) return;
+
+ // Standalone calendar (e.g. Storybook): always move DOM focus to the active day.
+ if (!focusGridSync) {
+ focusButton(focusTarget);
+ return;
+ }
+ const activeEl = document.activeElement;
+ const inThisGrid = tableRef.current?.contains(activeEl) ?? false;
+ const focusInSharedSurface = calendarKeyboardSurfaceRef?.current?.contains(activeEl) ?? false;
+ const requested = focusGridSync.gridFocusRequested;
+
+ // Month navigation unmounts the active cell; focus often lands on <body>, the dialog shell,
+ // or another non-grid node — not inside calendarKeyboardSurfaceRef, so we must still sync.
+ const surfaceEl = calendarKeyboardSurfaceRef?.current;
+ const focusLostFromCellUnmount = activeEl === document.body || activeEl === document.documentElement || activeEl instanceof HTMLElement && surfaceEl != null && surfaceEl.contains(activeEl) === false && activeEl.contains(surfaceEl);
+
+ // Sync DOM focus when: navigating inside this table; first focus from input (keyboard open);
+ // focus is in the multi-month strip (cross-grid arrows); or focus was lost after the grid updated.
+ // Do not pull focus from the input when the user opened with the mouse and never entered the surface.
+ const shouldSyncFocus = inThisGrid || requested || focusInSharedSurface || focusLostFromCellUnmount && surfaceEl != null;
+ if (!shouldSyncFocus) return;
+ const finish = success => {
+ if (success && requested) {
+ focusGridSync.onGridFocusRequestHandled();
+ }
+ };
+ let success = focusButton(focusTarget);
+ if (success) {
+ finish(true);
+ return;
+ }
+
+ // New cells may not have refs until after this layout pass (e.g. display month just changed).
+ if (shouldSyncFocus) {
+ requestAnimationFrame(() => {
+ success = focusButton(focusTarget);
+ if (success) finish(true);
+ });
+ }
+ }, [focusTarget, focusButton, focusGridSync, calendarKeyboardSurfaceRef, /** Re-run when the month grid remounts so we can re-attach roving focus after displayDate changes. */
+ year, month]);
+ const onKeyDown = useCallback((e, date) => keyHandler({
+ e,
+ date,
+ onFocusedDateChange,
+ datesWithRow,
+ month,
+ year,
+ shouldDisableDate,
+ onDateSelect,
+ onEscapeKeyPress,
+ onDisplayDateChange,
+ hasAdjacentMonthRight,
+ hasAdjacentMonthLeft
+ }), [onFocusedDateChange, datesWithRow, month, year, shouldDisableDate, onDateSelect, onEscapeKeyPress, onDisplayDateChange, hasAdjacentMonthLeft, hasAdjacentMonthRight]);
+ const setButtonRef = useCallback((date, el) => {
+ const normalizedDateTime = normalizeDate(date);
+ if (el) buttonRefs.current.set(normalizedDateTime, el);else buttonRefs.current.delete(normalizedDateTime);
+ }, []);
+ return /*#__PURE__*/_jsxs(CalendarTable, {
+ "aria-labelledby": labelledById,
+ ref: tableRef,
+ role: "grid",
+ children: [/*#__PURE__*/_jsx("thead", {
+ children: /*#__PURE__*/_jsx("tr", {
+ children: weekdayLabels.map((label, i) => /*#__PURE__*/_jsx(TableHeader, {
+ abbr: weekdayFullNames[i],
+ scope: "col",
+ children: label
+ }, label))
+ })
+ }), /*#__PURE__*/_jsx("tbody", {
+ children: weeks.map((week, rowIndex) => /*#__PURE__*/_jsx("tr", {
+ children: week.map((date, colIndex) => {
+ if (date === null) {
+ return (
+ /*#__PURE__*/
+ // eslint-disable-next-line jsx-a11y/control-has-associated-label -- this is a false positive
+ _jsx("td", {
+ role: "gridcell"
+ }, `empty-${rowIndex}-${colIndex}`)
+ );
+ }
+ const selected = isSameDay(date, selectedDate) || isSameDay(date, endDate);
+ const range = !!selectedDate && !!endDate;
+ const inRange = range && isDateInRange({
+ date,
+ startDate: selectedDate,
+ endDate
+ });
+ const disabled = isDateDisabled({
+ date,
+ shouldDisableDate
+ });
+ const today = isToday(date);
+ const isFocused = focusTarget !== null && isSameDay(date, focusTarget);
+ return /*#__PURE__*/_jsx(DateCell, {
+ "aria-current": today ? 'date' : undefined,
+ "aria-disabled": disabled,
+ "aria-label": formatDateForAriaLabel({
+ date,
+ locale: resolvedLocale
+ }),
+ "aria-selected": selected || inRange,
+ isDisabled: disabled,
+ isInRange: inRange,
+ isRangeEnd: range && isSameDay(date, endDate),
+ isRangeStart: range && isSameDay(date, selectedDate),
+ isSelected: selected,
+ isToday: today,
+ ref: el => setButtonRef(date, el),
+ role: "gridcell",
+ tabIndex: isFocused ? 0 : -1,
+ onClick: () => {
+ if (!disabled) onDateSelect(date);
+ },
+ onFocus: () => onFocusedDateChange?.(date),
+ onKeyDown: e => onKeyDown(e, date),
+ children: date.getDate()
+ }, date.getTime());
+ })
+ }, week.join('-')))
+ })]
+ });
+};
\ No newline at end of file