@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