@codecademy/gamut

68.2.268.2.3-alpha.794432.0
dist/DatePicker/DatePickerCalendar.js
+dist/DatePicker/DatePickerCalendar.jsNew file
+163
Index: package/dist/DatePicker/DatePickerCalendar.js
===================================================================
--- package/dist/DatePicker/DatePickerCalendar.js
+++ package/dist/DatePicker/DatePickerCalendar.js
@@ -0,0 +1,163 @@
+import { breakpoints } from '@codecademy/gamut-styles';
+import { useEffect, useId, useMemo, useRef, useState } from 'react';
+import { useMedia } from 'react-use';
+import { Box, FlexBox } from '../Box';
+import { CalendarBody, CalendarFooter, CalendarHeader, CalendarWrapper } from './Calendar';
+import { useDatePicker } from './DatePickerContext';
+import { handleDateSelectRange, handleDateSelectSingle } from './utils/dateSelect';
+import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
+/**
+ * Calendar that composes Calendar, CalendarHeader, CalendarBody, CalendarFooter.
+ * When inside DatePicker: owns local visibleDate and focusedDate; updates shared
+ * state via context. Supports single-date and range modes.
+ */
+export const DatePickerCalendar = ({
+  dialogId,
+  weekStartsOn
+}) => {
+  const context = useDatePicker();
+  const generatedId = useId();
+  const fallbackDialogId = `datepicker-calendar-${generatedId.replace(/:/g, '')}`;
+  const headingId = dialogId ?? context?.calendarDialogId ?? fallbackDialogId;
+  if (context == null) {
+    throw new Error('DatePickerCalendar must be used inside a DatePicker (it reads shared state from context).');
+  }
+  const {
+    mode,
+    startOrSelectedDate,
+    setSelection,
+    disabledDates,
+    locale,
+    closeCalendar,
+    isCalendarOpen,
+    translations,
+    focusGridSignal,
+    gridFocusRequested,
+    clearGridFocusRequest
+  } = context;
+  const focusGridSync = useMemo(() => ({
+    gridFocusRequested,
+    signal: focusGridSignal,
+    onGridFocusRequestHandled: clearGridFocusRequest
+  }), [gridFocusRequested, focusGridSignal, clearGridFocusRequest]);
+  const isRange = mode === 'range';
+  const endDate = isRange ? context.endDate : undefined;
+  const firstOfMonth = date => new Date(date.getFullYear(), date.getMonth(), 1);
+  const [displayDate, setDisplayDate] = useState(() => firstOfMonth(startOrSelectedDate ?? new Date()));
+  const [focusedDate, setFocusedDate] = useState(() => startOrSelectedDate ?? endDate ?? new Date());
+  const wasOpenRef = useRef(false);
+
+  // Sync visible month to selection only when the calendar opens, not on every
+  // date click. Otherwise clicking a date in the second month would jump the view.
+  useEffect(() => {
+    const justOpened = isCalendarOpen && !wasOpenRef.current;
+    wasOpenRef.current = isCalendarOpen;
+    if (!justOpened) return;
+    const anchor = startOrSelectedDate ?? endDate;
+    if (anchor) {
+      setDisplayDate(firstOfMonth(anchor));
+      setFocusedDate(startOrSelectedDate ?? endDate ?? new Date());
+    }
+  }, [isCalendarOpen, startOrSelectedDate, endDate]);
+  const onDateSelect = date => {
+    if (!isRange) {
+      handleDateSelectSingle({
+        date,
+        selectedDate: startOrSelectedDate,
+        setSelection
+      });
+    } else {
+      context.setActiveRangePart(null);
+      handleDateSelectRange({
+        date,
+        activeRangePart: context.activeRangePart,
+        startDate: startOrSelectedDate,
+        endDate: context.endDate,
+        setSelection,
+        disabledDates
+      });
+    }
+  };
+  const handleClearDate = () => {
+    setSelection(null);
+    setFocusedDate(displayDate);
+  };
+  const handleTodayClick = () => {
+    const today = new Date();
+    setSelection(today);
+    setDisplayDate(firstOfMonth(today));
+    setFocusedDate(today);
+  };
+  const focusTarget = focusedDate ?? startOrSelectedDate ?? endDate ?? new Date();
+  const addMonths = (date, n) => new Date(date.getFullYear(), date.getMonth() + n, 1);
+  const secondMonthDate = addMonths(displayDate, 1);
+  const isTwoMonthsVisible = useMedia(`(min-width: ${breakpoints.xs})`);
+  return /*#__PURE__*/_jsxs(CalendarWrapper, {
+    children: [/*#__PURE__*/_jsxs(FlexBox, {
+      p: 24,
+      pb: 16,
+      children: [/*#__PURE__*/_jsxs(Box, {
+        children: [/*#__PURE__*/_jsx(CalendarHeader, {
+          displayDate: displayDate,
+          headingId: headingId,
+          hideNextNav: isTwoMonthsVisible,
+          locale: locale,
+          onDisplayDateChange: setDisplayDate
+        }), /*#__PURE__*/_jsx(CalendarBody, {
+          disabledDates: disabledDates,
+          displayDate: displayDate,
+          endDate: endDate,
+          focusGridSync: focusGridSync,
+          focusedDate: focusTarget,
+          hasAdjacentMonthRight: isTwoMonthsVisible,
+          labelledById: headingId,
+          locale: locale,
+          selectedDate: startOrSelectedDate,
+          weekStartsOn: weekStartsOn,
+          onDateSelect: onDateSelect,
+          onDisplayDateChange: setDisplayDate,
+          onEscapeKeyPress: closeCalendar,
+          onFocusedDateChange: setFocusedDate
+        })]
+      }), /*#__PURE__*/_jsxs(Box, {
+        display: {
+          _: 'none',
+          xs: 'initial'
+        },
+        pl: {
+          _: 0,
+          xs: 32
+        },
+        children: [/*#__PURE__*/_jsx(CalendarHeader, {
+          displayDate: secondMonthDate,
+          headingId: headingId,
+          hideLastNav: true,
+          locale: locale,
+          onDisplayDateChange: setDisplayDate
+        }), /*#__PURE__*/_jsx(CalendarBody, {
+          disabledDates: disabledDates,
+          displayDate: secondMonthDate,
+          endDate: endDate,
+          focusGridSync: focusGridSync,
+          focusedDate: focusTarget,
+          hasAdjacentMonthLeft: isTwoMonthsVisible,
+          labelledById: headingId,
+          locale: locale,
+          selectedDate: startOrSelectedDate,
+          weekStartsOn: weekStartsOn,
+          onDateSelect: onDateSelect,
+          onDisplayDateChange: setDisplayDate,
+          onEscapeKeyPress: closeCalendar,
+          onFocusedDateChange: setFocusedDate
+        })]
+      })]
+    }), /*#__PURE__*/_jsx(CalendarFooter, {
+      clearText: translations.clearText,
+      disabled: startOrSelectedDate === null && endDate === null,
+      locale: locale,
+      showClearButton: isRange,
+      onClearDate: handleClearDate,
+      onTodayClick: handleTodayClick
+    })]
+  });
+};
\ No newline at end of file