@codecademy/gamut
68.2.268.2.3-alpha.93a7da.0
dist/DatePicker/DatePickerCalendar/index.js+
dist/DatePicker/DatePickerCalendar/index.jsNew file+241
Index: package/dist/DatePicker/DatePickerCalendar/index.js
===================================================================
--- package/dist/DatePicker/DatePickerCalendar/index.js
+++ package/dist/DatePicker/DatePickerCalendar/index.js
@@ -0,0 +1,241 @@
+import { breakpoints } from '@codecademy/gamut-styles';
+import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
+import { useMedia } from 'react-use';
+import { Box, FlexBox } from '../../Box';
+import { useDatePicker } from '../DatePickerContext';
+import { CalendarBody, CalendarFooter, CalendarHeader, CalendarWrapper } from './Calendar';
+import { addMonths, getFirstOfMonth, isDateWithinVisibleMonths } from './Calendar/utils/dateGrid';
+import { applyRangeOrNewStart, handleDateSelectRange, handleDateSelectSingle, rangeContainsDisabled } from './utils/dateSelect';
+import { computeQuickAction } from './utils/quickActions';
+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,
+ shouldDisableDate,
+ locale,
+ closeCalendar,
+ isCalendarOpen,
+ translations,
+ focusGridSignal,
+ gridFocusRequested,
+ clearGridFocusRequest,
+ quickActions
+ } = context;
+ const focusGridSync = useMemo(() => ({
+ gridFocusRequested,
+ signal: focusGridSignal,
+ onGridFocusRequestHandled: clearGridFocusRequest
+ }), [gridFocusRequested, focusGridSignal, clearGridFocusRequest]);
+ const isRange = mode === 'range';
+ const selectedDate = isRange ? context.startDate : context.selectedDate;
+ const endDate = isRange ? context.endDate : undefined;
+ const setActiveRangePart = isRange ? context.setActiveRangePart : undefined;
+ const activeRangePart = isRange ? context.activeRangePart : null;
+
+ /** Committed value that should drive the visible month when it changes (input, grid, or quick actions). */
+ const anchorDate = useMemo(() => {
+ if (!isRange) return selectedDate ?? null;
+ if (activeRangePart === 'end') return endDate ?? selectedDate ?? null;
+ return selectedDate ?? endDate ?? null;
+ }, [isRange, selectedDate, endDate, activeRangePart]);
+ const [displayDate, setDisplayDate] = useState(() => getFirstOfMonth(anchorDate ?? selectedDate ?? endDate ?? new Date()));
+ const [focusedDate, setFocusedDate] = useState(() => selectedDate ?? endDate ?? new Date());
+ const onFocusedDateChange = useCallback(date => {
+ setFocusedDate(date);
+ }, [setFocusedDate]);
+ const focusTarget = focusedDate ?? selectedDate ?? endDate ?? new Date();
+ const secondMonthDate = addMonths({
+ date: displayDate,
+ n: 1
+ });
+ const isTwoMonthsVisible = useMedia(`(min-width: ${breakpoints.xs})`);
+ /** Current left-column month; read in the anchor sync effect without listing `displayDate` in deps (month nav would retrigger and snap back). */
+ const startOfLeftVisibleMonthRef = useRef(displayDate);
+ startOfLeftVisibleMonthRef.current = displayDate;
+ /** Wraps both month grids so keyboard focus can move between them without treating it as “outside” the calendar. */
+ const calendarKeyboardSurfaceRef = useRef(null);
+
+ // When the committed anchor changes while the popover is open (typed input, grid, quick action),
+ // move focus to that day. Shift the visible month pair only if the anchor is not already shown
+ // (including the second column in a two-month layout), so picking in the right-hand month does
+ // not jump the view.
+ useEffect(() => {
+ if (!isCalendarOpen) {
+ return;
+ }
+ const anchor = anchorDate;
+ if (!anchor) {
+ return;
+ }
+ const alreadyVisible = isDateWithinVisibleMonths({
+ date: anchor,
+ startOfLeftVisibleMonth: startOfLeftVisibleMonthRef.current,
+ showSecondMonth: isTwoMonthsVisible
+ });
+ if (!alreadyVisible) {
+ setDisplayDate(getFirstOfMonth(anchor));
+ }
+ setFocusedDate(anchor);
+ }, [isCalendarOpen, anchorDate, isTwoMonthsVisible]);
+ const onDateSelect = useCallback(date => {
+ if (!isRange) {
+ handleDateSelectSingle({
+ date,
+ selectedDate: context.selectedDate,
+ onSelection: context.onSelection
+ });
+ // Defer close so React can commit the new date and the input can sync segments
+ // before closeCalendar focuses the spinbutton (which blocks segment sync while "focused").
+ queueMicrotask(closeCalendar);
+ return;
+ }
+ setActiveRangePart?.(null);
+ const shouldClose = handleDateSelectRange({
+ date,
+ activeRangePart: context.activeRangePart,
+ startDate: context.startDate,
+ endDate: context.endDate,
+ onRangeSelection: context.onRangeSelection,
+ shouldDisableDate
+ });
+ if (shouldClose) queueMicrotask(closeCalendar);
+ }, [isRange, setActiveRangePart, context, shouldDisableDate, closeCalendar]);
+ const clearDate = useCallback(() => {
+ if (isRange) context.onRangeSelection(null, null);else context.onSelection(null);
+ setFocusedDate(displayDate);
+ }, [isRange, context, setFocusedDate, displayDate]);
+ const computedQuickActions = useMemo(() => {
+ return quickActions.slice(0, 3).map(action => ({
+ ...action,
+ onClick: () => {
+ action.onClick?.();
+ setActiveRangePart?.(null);
+ const {
+ startDate,
+ endDate
+ } = computeQuickAction({
+ num: action.num,
+ timePeriod: action.timePeriod,
+ isRange
+ });
+ if (isRange) {
+ if (rangeContainsDisabled({
+ startDate,
+ endDate,
+ shouldDisableDate
+ })) {
+ applyRangeOrNewStart({
+ startDate,
+ endDate,
+ clickedDate: endDate,
+ shouldDisableDate,
+ onRangeSelection: context.onRangeSelection
+ });
+ } else {
+ context.onRangeSelection(startDate, endDate);
+ }
+ setDisplayDate(getFirstOfMonth(endDate));
+ setFocusedDate(endDate);
+ queueMicrotask(closeCalendar);
+ } else {
+ context.onSelection(startDate);
+ setDisplayDate(getFirstOfMonth(startDate));
+ setFocusedDate(startDate);
+ queueMicrotask(closeCalendar);
+ }
+ }
+ }));
+ }, [closeCalendar, shouldDisableDate, isRange, quickActions, setActiveRangePart, context]);
+ return /*#__PURE__*/_jsxs(CalendarWrapper, {
+ children: [/*#__PURE__*/_jsxs(FlexBox, {
+ p: 24,
+ pb: 16,
+ ref: calendarKeyboardSurfaceRef,
+ children: [/*#__PURE__*/_jsxs(Box, {
+ children: [/*#__PURE__*/_jsx(CalendarHeader, {
+ displayDate: displayDate,
+ headingId: headingId,
+ hideNextNav: isTwoMonthsVisible,
+ locale: locale,
+ onDisplayDateChange: setDisplayDate
+ }), /*#__PURE__*/_jsx(CalendarBody, {
+ calendarKeyboardSurfaceRef: calendarKeyboardSurfaceRef,
+ displayDate: displayDate,
+ endDate: endDate,
+ focusGridSync: focusGridSync,
+ focusedDate: focusTarget,
+ hasAdjacentMonthRight: isTwoMonthsVisible,
+ labelledById: headingId,
+ locale: locale,
+ selectedDate: selectedDate,
+ shouldDisableDate: shouldDisableDate,
+ 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(prev => addMonths({
+ date: prev,
+ n: 1
+ }))
+ }), /*#__PURE__*/_jsx(CalendarBody, {
+ calendarKeyboardSurfaceRef: calendarKeyboardSurfaceRef,
+ displayDate: secondMonthDate,
+ endDate: endDate,
+ focusGridSync: focusGridSync,
+ focusedDate: focusTarget,
+ hasAdjacentMonthLeft: isTwoMonthsVisible,
+ labelledById: headingId,
+ locale: locale,
+ selectedDate: selectedDate,
+ shouldDisableDate: shouldDisableDate,
+ weekStartsOn: weekStartsOn,
+ onDateSelect: onDateSelect,
+ onDisplayDateChange: () => setDisplayDate(prev => addMonths({
+ date: prev,
+ n: 1
+ })),
+ onEscapeKeyPress: closeCalendar,
+ onFocusedDateChange: onFocusedDateChange
+ })]
+ })]
+ }), /*#__PURE__*/_jsx(CalendarFooter, {
+ clearButton: isRange ? {
+ disabled: context.startDate === null && endDate === null,
+ onClick: clearDate,
+ text: translations.clearButtonText
+ } : undefined,
+ quickActions: computedQuickActions
+ })]
+ });
+};
\ No newline at end of file