@codecademy/gamut 68.1.2 → 68.1.3-alpha.77d8dc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/ConnectedForm/utils.d.ts +1 -1
  2. package/dist/DatePicker/Calendar/Calendar.d.ts +9 -0
  3. package/dist/DatePicker/Calendar/Calendar.js +28 -0
  4. package/dist/DatePicker/Calendar/CalendarBody.d.ts +3 -0
  5. package/dist/DatePicker/Calendar/CalendarBody.js +155 -0
  6. package/dist/DatePicker/Calendar/CalendarFooter.d.ts +3 -0
  7. package/dist/DatePicker/Calendar/CalendarFooter.js +54 -0
  8. package/dist/DatePicker/Calendar/CalendarHeader.d.ts +3 -0
  9. package/dist/DatePicker/Calendar/CalendarHeader.js +67 -0
  10. package/dist/DatePicker/Calendar/index.d.ts +6 -0
  11. package/dist/DatePicker/Calendar/index.js +5 -0
  12. package/dist/DatePicker/Calendar/types.d.ts +60 -0
  13. package/dist/DatePicker/Calendar/types.js +1 -0
  14. package/dist/DatePicker/Calendar/utils/dateGrid.d.ts +30 -0
  15. package/dist/DatePicker/Calendar/utils/dateGrid.js +93 -0
  16. package/dist/DatePicker/Calendar/utils/format.d.ts +61 -0
  17. package/dist/DatePicker/Calendar/utils/format.js +184 -0
  18. package/dist/DatePicker/Calendar/utils/index.d.ts +3 -0
  19. package/dist/DatePicker/Calendar/utils/index.js +3 -0
  20. package/dist/DatePicker/Calendar/utils/keyHandler.d.ts +13 -0
  21. package/dist/DatePicker/Calendar/utils/keyHandler.js +116 -0
  22. package/dist/DatePicker/Calendar/utils/validation.d.ts +13 -0
  23. package/dist/DatePicker/Calendar/utils/validation.js +23 -0
  24. package/dist/DatePicker/DatePicker.d.ts +8 -0
  25. package/dist/DatePicker/DatePicker.js +147 -0
  26. package/dist/DatePicker/DatePickerCalendar.d.ts +13 -0
  27. package/dist/DatePicker/DatePickerCalendar.js +130 -0
  28. package/dist/DatePicker/DatePickerContext.d.ts +11 -0
  29. package/dist/DatePicker/DatePickerContext.js +18 -0
  30. package/dist/DatePicker/DatePickerInput.d.ts +16 -0
  31. package/dist/DatePicker/DatePickerInput.js +135 -0
  32. package/dist/DatePicker/index.d.ts +13 -0
  33. package/dist/DatePicker/index.js +10 -0
  34. package/dist/DatePicker/translations.d.ts +3 -0
  35. package/dist/DatePicker/translations.js +4 -0
  36. package/dist/DatePicker/types.d.ts +85 -0
  37. package/dist/DatePicker/types.js +1 -0
  38. package/dist/DatePicker/utils.d.ts +3 -0
  39. package/dist/DatePicker/utils.js +71 -0
  40. package/dist/FocusTrap/index.d.ts +2 -2
  41. package/dist/List/elements.d.ts +1 -1
  42. package/dist/PopoverContainer/PopoverContainer.js +3 -1
  43. package/dist/PopoverContainer/types.d.ts +5 -0
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +1 -0
  46. package/package.json +2 -2
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Date formatting for the calendar using Intl.DateTimeFormat.
3
+ */
4
+
5
+ /**
6
+ * Capitalize the first character of a string; rest unchanged (e.g. "next month" → "Next month").
7
+ */
8
+ export const capitalizeFirst = str => str.length === 0 ? str : str[0].toUpperCase() + str.slice(1);
9
+
10
+ /**
11
+ * Format month and year for the calendar header (e.g. "February 2026").
12
+ */
13
+ export const formatMonthYear = (date, locale) => {
14
+ return new Intl.DateTimeFormat(locale, {
15
+ month: 'long',
16
+ year: 'numeric'
17
+ }).format(date);
18
+ };
19
+
20
+ /**
21
+ * Get short weekday labels for column headers (e.g. ["Su", "Mo", ...]).
22
+ * Order depends on weekStartsOn: 0 = Sunday first, 1 = Monday first.
23
+ */
24
+ export const getWeekdayLabels = (locale, weekStartsOn = 0) => {
25
+ const formatter = new Intl.DateTimeFormat(locale, {
26
+ weekday: 'short'
27
+ });
28
+ // Jan 7, 2024 is a Sunday; add 0..6 days to get Sun..Sat
29
+ const sunday = new Date(2024, 0, 7);
30
+ const labels = Array.from({
31
+ length: 7
32
+ }, (_, i) => {
33
+ const d = new Date(sunday);
34
+ d.setDate(sunday.getDate() + i);
35
+ return formatter.format(d);
36
+ });
37
+ if (weekStartsOn === 1) {
38
+ return [...labels.slice(1), labels[0]];
39
+ }
40
+ return labels;
41
+ };
42
+
43
+ /**
44
+ * Get full weekday names for abbr attributes (e.g. "Sunday", "Monday").
45
+ * Same order as getWeekdayLabels.
46
+ */
47
+ export const getWeekdayFullNames = (locale, weekStartsOn = 0) => {
48
+ const formatter = new Intl.DateTimeFormat(locale, {
49
+ weekday: 'long'
50
+ });
51
+ const sunday = new Date(2024, 0, 7);
52
+ const names = Array.from({
53
+ length: 7
54
+ }, (_, i) => {
55
+ const d = new Date(sunday);
56
+ d.setDate(sunday.getDate() + i);
57
+ return formatter.format(d);
58
+ });
59
+ if (weekStartsOn === 1) {
60
+ return [...names.slice(1), names[0]];
61
+ }
62
+ return names;
63
+ };
64
+
65
+ /**
66
+ * Get localized "next month" and "previous month" labels for calendar nav.
67
+ * Uses Intl.RelativeTimeFormat with numeric: "auto" (e.g. "next month", "last month").
68
+ */
69
+ export const getRelativeMonthLabels = locale => {
70
+ const rtf = new Intl.RelativeTimeFormat(locale, {
71
+ numeric: 'auto'
72
+ });
73
+ return {
74
+ nextMonth: capitalizeFirst(rtf.format(1, 'month')),
75
+ lastMonth: capitalizeFirst(rtf.format(-1, 'month'))
76
+ };
77
+ };
78
+
79
+ /**
80
+ * Get localized "today" label (e.g. "today").
81
+ */
82
+ export const getRelativeTodayLabel = locale => {
83
+ const rtf = new Intl.RelativeTimeFormat(locale, {
84
+ numeric: 'auto'
85
+ });
86
+ return capitalizeFirst(rtf.format(0, 'day'));
87
+ };
88
+
89
+ /**
90
+ * Get the locale's short date format pattern (e.g. "MM/DD/YYYY" for en-US,
91
+ * "DD/MM/YYYY" for en-GB). Uses Intl.DateTimeFormat formatToParts to infer
92
+ * order and separators. Useful for parsing or building locale-aware inputs.
93
+ */
94
+ export const getDateFormatPattern = locale => {
95
+ const parts = new Intl.DateTimeFormat(locale, {
96
+ year: 'numeric',
97
+ month: '2-digit',
98
+ day: '2-digit'
99
+ }).formatToParts(new Date(2025, 0, 15));
100
+ return parts.map(p => {
101
+ switch (p.type) {
102
+ case 'day':
103
+ return 'DD';
104
+ case 'month':
105
+ return 'MM';
106
+ case 'year':
107
+ return 'YYYY';
108
+ default:
109
+ return p.value;
110
+ }
111
+ }).join('');
112
+ };
113
+
114
+ /**
115
+ * Format a date for display in the date picker input (e.g. "2/15/2026").
116
+ */
117
+ export const formatDateForInput = (date, locale) => {
118
+ return new Intl.DateTimeFormat(locale, {
119
+ month: 'numeric',
120
+ day: 'numeric',
121
+ year: 'numeric'
122
+ }).format(date);
123
+ };
124
+
125
+ /**
126
+ * Parse a string from the date input into a Date, or null if invalid.
127
+ * Only returns a date when the input is a complete valid date (e.g. "2/15/2026").
128
+ * Partial input like "1" or "2/15" returns null even though Date("1") would parse.
129
+ */
130
+
131
+ // this logic needs some work
132
+ export const parseDateFromInput = (value, locale) => {
133
+ const trimmed = value.trim();
134
+ if (!trimmed) return null;
135
+ const parsed = new Date(trimmed);
136
+ if (Number.isNaN(parsed.getTime())) return null;
137
+ const formatted = formatDateForInput(parsed, locale);
138
+ if (formatted === trimmed) return parsed;
139
+ const parts = trimmed.split(/[/-]/);
140
+ if (parts.length >= 3) return parsed;
141
+ return null;
142
+ };
143
+ const RANGE_SEPARATOR = ' – ';
144
+
145
+ /**
146
+ * Format a date range for the input (e.g. "2/15/2026 – 2/20/2026").
147
+ */
148
+ export const formatDateRangeForInput = (startDate, endDate, locale) => {
149
+ if (!startDate && !endDate) return '';
150
+ if (!startDate) return formatDateForInput(endDate, locale);
151
+ if (!endDate) return formatDateForInput(startDate, locale);
152
+ return `${formatDateForInput(startDate, locale)}${RANGE_SEPARATOR}${formatDateForInput(endDate, locale)}`;
153
+ };
154
+
155
+ /**
156
+ * Parse a range string (e.g. "2/15/2026 – 2/20/2026") into { startDate, endDate }.
157
+ * Returns null if invalid. Single date is allowed and yields startDate = endDate.
158
+ */
159
+ export const parseDateRangeFromInput = (value, locale) => {
160
+ const trimmed = value.trim();
161
+ if (!trimmed) return null;
162
+ const parts = trimmed.split(RANGE_SEPARATOR).map(s => s.trim());
163
+ if (parts.length === 1) {
164
+ const d = parseDateFromInput(parts[0], locale);
165
+ if (!d) return null;
166
+ return {
167
+ startDate: d,
168
+ endDate: new Date(d)
169
+ };
170
+ }
171
+ if (parts.length === 2) {
172
+ const start = parseDateFromInput(parts[0], locale);
173
+ const end = parseDateFromInput(parts[1], locale);
174
+ if (!start || !end) return null;
175
+ return start.getTime() <= end.getTime() ? {
176
+ startDate: start,
177
+ endDate: end
178
+ } : {
179
+ startDate: end,
180
+ endDate: start
181
+ };
182
+ }
183
+ return null;
184
+ };
@@ -0,0 +1,3 @@
1
+ export * from './dateGrid';
2
+ export * from './format';
3
+ export * from './validation';
@@ -0,0 +1,3 @@
1
+ export * from './dateGrid';
2
+ export * from './format';
3
+ export * from './validation';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Clamp a day to the last day of the given month (e.g. Jan 31 -> Feb 28).
3
+ */
4
+ export declare const clampToMonth: (year: number, month: number, day: number) => Date;
5
+ /** Flat list of dates in grid order (row-major, non-null only) with row index for Home/End */
6
+ export declare const getDatesWithRow: (weeks: (Date | null)[][]) => {
7
+ date: Date;
8
+ rowIndex: number;
9
+ }[];
10
+ export declare const keyHandler: (e: React.KeyboardEvent, date: Date, onFocusedDateChange: (date: Date | null) => void, datesWithRow: {
11
+ date: Date;
12
+ rowIndex: number;
13
+ }[], month: number, year: number, disabledDates: Date[], onDateSelect: (date: Date) => void, onEscapeKeyPress?: () => void, onVisibleDateChange?: ((newDate: Date) => void) | undefined) => void;
@@ -0,0 +1,116 @@
1
+ import { isDateDisabled } from './dateGrid';
2
+
3
+ /**
4
+ * Clamp a day to the last day of the given month (e.g. Jan 31 -> Feb 28).
5
+ */
6
+ export const clampToMonth = (year, month, day) => {
7
+ const last = new Date(year, month + 1, 0).getDate();
8
+ return new Date(year, month, Math.min(day, last));
9
+ };
10
+
11
+ /** Flat list of dates in grid order (row-major, non-null only) with row index for Home/End */
12
+ export const getDatesWithRow = weeks => {
13
+ const result = [];
14
+ weeks.forEach((week, rowIndex) => {
15
+ week.forEach(date => {
16
+ if (date !== null) result.push({
17
+ date,
18
+ rowIndex
19
+ });
20
+ });
21
+ });
22
+ return result;
23
+ };
24
+ export const keyHandler = (e, date, onFocusedDateChange, datesWithRow, month, year, disabledDates, onDateSelect, onEscapeKeyPress, onVisibleDateChange) => {
25
+ const key = date.getTime();
26
+ const idx = datesWithRow.findIndex(({
27
+ date: d
28
+ }) => d.getTime() === key);
29
+ if (idx < 0) return;
30
+ const currentRow = datesWithRow[idx].rowIndex;
31
+ const day = date.getDate();
32
+ let newDate = null;
33
+ let newVisibleDate = null;
34
+ switch (e.key) {
35
+ case 'ArrowLeft':
36
+ e.preventDefault();
37
+ if (idx > 0) {
38
+ newDate = datesWithRow[idx - 1].date;
39
+ } else {
40
+ const lastDayPrevMonth = new Date(year, month, 0);
41
+ newDate = lastDayPrevMonth;
42
+ newVisibleDate = new Date(year, month - 1, 1);
43
+ }
44
+ break;
45
+ case 'ArrowRight':
46
+ e.preventDefault();
47
+ if (idx < datesWithRow.length - 1) {
48
+ newDate = datesWithRow[idx + 1].date;
49
+ } else {
50
+ newDate = new Date(year, month + 1, 1);
51
+ newVisibleDate = new Date(year, month + 1, 1);
52
+ }
53
+ break;
54
+ case 'ArrowUp':
55
+ e.preventDefault();
56
+ newDate = new Date(date);
57
+ newDate.setDate(newDate.getDate() - 7);
58
+ if (newDate.getMonth() !== month || newDate.getFullYear() !== year) {
59
+ newVisibleDate = new Date(newDate.getFullYear(), newDate.getMonth(), 1);
60
+ }
61
+ break;
62
+ case 'ArrowDown':
63
+ e.preventDefault();
64
+ newDate = new Date(date);
65
+ newDate.setDate(newDate.getDate() + 7);
66
+ if (newDate.getMonth() !== month || newDate.getFullYear() !== year) {
67
+ newVisibleDate = new Date(newDate.getFullYear(), newDate.getMonth(), 1);
68
+ }
69
+ break;
70
+ case 'Home':
71
+ e.preventDefault();
72
+ newDate = datesWithRow.find(({
73
+ rowIndex
74
+ }) => rowIndex === currentRow)?.date ?? date;
75
+ break;
76
+ case 'End':
77
+ e.preventDefault();
78
+ newDate = [...datesWithRow].reverse().find(({
79
+ rowIndex
80
+ }) => rowIndex === currentRow)?.date ?? date;
81
+ break;
82
+ case 'PageDown':
83
+ e.preventDefault();
84
+ if (e.shiftKey) {
85
+ newDate = clampToMonth(year + 1, month, day);
86
+ } else {
87
+ newDate = clampToMonth(year, month + 1, day);
88
+ }
89
+ newVisibleDate = new Date(newDate.getFullYear(), newDate.getMonth(), 1);
90
+ break;
91
+ case 'PageUp':
92
+ e.preventDefault();
93
+ if (e.shiftKey) {
94
+ newDate = clampToMonth(year - 1, month, day);
95
+ } else {
96
+ newDate = clampToMonth(year, month - 1, day);
97
+ }
98
+ newVisibleDate = new Date(newDate.getFullYear(), newDate.getMonth(), 1);
99
+ break;
100
+ case 'Enter':
101
+ case ' ':
102
+ e.preventDefault();
103
+ if (!isDateDisabled(date, disabledDates)) onDateSelect(date);
104
+ return;
105
+ case 'Escape':
106
+ e.preventDefault();
107
+ onEscapeKeyPress?.();
108
+ return;
109
+ default:
110
+ return;
111
+ }
112
+ if (newDate !== null) {
113
+ onFocusedDateChange(newDate);
114
+ if (newVisibleDate !== null) onVisibleDateChange?.(newVisibleDate);
115
+ }
116
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Validation helpers for DatePicker (single-date).
3
+ * Used to mark invalid dates as unselectable and for manual entry validation.
4
+ */
5
+ /**
6
+ * Check if a date is in the past (before today at start of day).
7
+ * Useful for disabling past dates in the calendar.
8
+ */
9
+ export declare const isPastDate: (date: Date) => boolean;
10
+ /**
11
+ * Check if a date is valid (finite and not NaN).
12
+ */
13
+ export declare const isValidDate: (date: Date) => boolean;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Validation helpers for DatePicker (single-date).
3
+ * Used to mark invalid dates as unselectable and for manual entry validation.
4
+ */
5
+
6
+ /**
7
+ * Check if a date is in the past (before today at start of day).
8
+ * Useful for disabling past dates in the calendar.
9
+ */
10
+ export const isPastDate = date => {
11
+ const today = new Date();
12
+ today.setHours(0, 0, 0, 0);
13
+ const normalizedDate = new Date(date);
14
+ normalizedDate.setHours(0, 0, 0, 0);
15
+ return normalizedDate.getTime() < today.getTime();
16
+ };
17
+
18
+ /**
19
+ * Check if a date is valid (finite and not NaN).
20
+ */
21
+ export const isValidDate = date => {
22
+ return date instanceof Date && !Number.isNaN(date.getTime());
23
+ };
@@ -0,0 +1,8 @@
1
+ /// <reference types="react" />
2
+ import { type DatePickerProps } from './types';
3
+ /**
4
+ * DatePicker: single-date or range. Holds shared state and provides it via context.
5
+ * Single: selectedDate, setSelectedDate. Range: startDate, endDate, setStartDate, setEndDate.
6
+ * With no children, renders default layout (input + calendar popover).
7
+ */
8
+ export declare const DatePicker: React.FC<DatePickerProps>;
@@ -0,0 +1,147 @@
1
+ import { MiniArrowRightIcon } from '@codecademy/gamut-icons';
2
+ import { useCallback, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
+ import { FlexBox } from '../Box';
4
+ import { PopoverContainer } from '../PopoverContainer';
5
+ import { DatePickerCalendar } from './DatePickerCalendar';
6
+ import { DatePickerProvider } from './DatePickerContext';
7
+ import { DatePickerInput } from './DatePickerInput';
8
+ import { DEFAULT_DATE_PICKER_TRANSLATIONS } from './translations';
9
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
10
+ function isRangeProps(props) {
11
+ return props.mode === 'range';
12
+ }
13
+
14
+ /**
15
+ * DatePicker: single-date or range. Holds shared state and provides it via context.
16
+ * Single: selectedDate, setSelectedDate. Range: startDate, endDate, setStartDate, setEndDate.
17
+ * With no children, renders default layout (input + calendar popover).
18
+ */
19
+ export const DatePicker = props => {
20
+ const {
21
+ locale,
22
+ disabledDates = [],
23
+ placeholder,
24
+ mode,
25
+ children,
26
+ translations: translationsProp
27
+ } = props;
28
+ const [isCalendarOpen, setIsCalendarOpen] = useState(false);
29
+ const [activeRangePart, setActiveRangePart] = useState(null);
30
+ const inputRef = useRef(null);
31
+ const dialogId = useId();
32
+ const calendarDialogId = `datepicker-dialog-${dialogId.replace(/:/g, '')}`;
33
+ const popoverOffset = 4;
34
+
35
+ // Align popover left edge with input left edge. PopoverContainer's "bottom-right"
36
+ // sets popover left = target left + (target width + offset + x), so we pass
37
+ // x = -(target width + offset) to get popover left = target left.
38
+ const [popoverX, setPopoverX] = useState(0);
39
+ useLayoutEffect(() => {
40
+ if (isCalendarOpen && inputRef.current) {
41
+ const width = inputRef.current.offsetWidth;
42
+ setPopoverX(-(width + popoverOffset));
43
+ }
44
+ }, [isCalendarOpen, popoverOffset]);
45
+ const openCalendar = useCallback(() => setIsCalendarOpen(true), []);
46
+ const closeCalendar = useCallback(() => {
47
+ setIsCalendarOpen(false);
48
+ setActiveRangePart(null);
49
+ inputRef.current?.focus();
50
+ }, []);
51
+ const startOrSelectedDate = isRangeProps(props) ? props.startDate : props.selectedDate;
52
+ const endDate = isRangeProps(props) ? props.endDate : null;
53
+ const setSelection = useCallback((start, end) => {
54
+ if (isRangeProps(props)) {
55
+ props.setStartDate(start);
56
+ props.setEndDate(end ?? null);
57
+ } else {
58
+ props.setSelectedDate(start);
59
+ }
60
+ }, [props]);
61
+ const contextValue = useMemo(() => {
62
+ const translations = {
63
+ ...DEFAULT_DATE_PICKER_TRANSLATIONS,
64
+ ...translationsProp
65
+ };
66
+ const base = {
67
+ startOrSelectedDate,
68
+ setSelection,
69
+ isCalendarOpen,
70
+ openCalendar,
71
+ closeCalendar,
72
+ locale,
73
+ disabledDates,
74
+ calendarDialogId,
75
+ translations
76
+ };
77
+ return mode === 'range' ? {
78
+ ...base,
79
+ mode: 'range',
80
+ endDate,
81
+ activeRangePart,
82
+ setActiveRangePart
83
+ } : {
84
+ ...base,
85
+ mode: 'single'
86
+ };
87
+ }, [mode, startOrSelectedDate, endDate, setSelection, activeRangePart, setActiveRangePart, isCalendarOpen, openCalendar, closeCalendar, locale, disabledDates, calendarDialogId, translationsProp]);
88
+
89
+ // what is this doing
90
+ // useEffect(() => {
91
+ // if (!isCalendarOpen) return;
92
+ // const id = setTimeout(() => inputRef.current?.focus(), 0);
93
+ // return () => clearTimeout(id);
94
+ // }, [isCalendarOpen]);
95
+
96
+ const content = children !== undefined ? children : /*#__PURE__*/_jsxs(_Fragment, {
97
+ children: [/*#__PURE__*/_jsx(FlexBox, {
98
+ gap: 8,
99
+ width: "fit-content",
100
+ wrap: true,
101
+ children: mode === 'range' ? /*#__PURE__*/_jsxs(_Fragment, {
102
+ children: [/*#__PURE__*/_jsx(DatePickerInput, {
103
+ label: props.startLabel,
104
+ placeholder: placeholder,
105
+ rangePart: "start",
106
+ ref: inputRef
107
+ }), /*#__PURE__*/_jsx(MiniArrowRightIcon, {
108
+ alignSelf: "center"
109
+ }), ' ', /*#__PURE__*/_jsx(DatePickerInput, {
110
+ label: props.endLabel,
111
+ placeholder: placeholder,
112
+ rangePart: "end"
113
+ // does this need a ref?
114
+ })]
115
+ }) : /*#__PURE__*/_jsx(DatePickerInput, {
116
+ label: props.label,
117
+ placeholder: placeholder,
118
+ ref: inputRef
119
+ })
120
+ }), /*#__PURE__*/_jsx(PopoverContainer, {
121
+ alignment: "bottom-right",
122
+ allowPageInteraction: true,
123
+ focusOnProps: {
124
+ autoFocus: false,
125
+ focusLock: false
126
+ },
127
+ isOpen: isCalendarOpen,
128
+ offset: popoverOffset,
129
+ targetRef: inputRef,
130
+ x: popoverX,
131
+ y: 0,
132
+ onRequestClose: closeCalendar,
133
+ children: /*#__PURE__*/_jsx("div", {
134
+ "aria-label": "Choose date",
135
+ id: calendarDialogId,
136
+ role: "dialog",
137
+ children: /*#__PURE__*/_jsx(DatePickerCalendar, {
138
+ dialogId: calendarDialogId
139
+ })
140
+ })
141
+ })]
142
+ });
143
+ return /*#__PURE__*/_jsx(DatePickerProvider, {
144
+ value: contextValue,
145
+ children: content
146
+ });
147
+ };
@@ -0,0 +1,13 @@
1
+ /// <reference types="react" />
2
+ export type DatePickerCalendarProps = {
3
+ /** id for the dialog (for aria-controls from input). */
4
+ dialogId: string;
5
+ /** Whether to start the calendar on Sunday (0) or Monday (1). Default is Sunday. */
6
+ weekStartsOn?: 0 | 1;
7
+ };
8
+ /**
9
+ * Calendar that composes Calendar, CalendarHeader, CalendarBody, CalendarFooter.
10
+ * When inside DatePicker: owns local visibleDate and focusedDate; updates shared
11
+ * state via context. Supports single-date and range modes.
12
+ */
13
+ export declare const DatePickerCalendar: React.FC<DatePickerCalendarProps>;
@@ -0,0 +1,130 @@
1
+ import { useEffect, useId, useRef, useState } from 'react';
2
+ import { Box, FlexBox } from '../Box';
3
+ import { Calendar, CalendarBody, CalendarFooter, CalendarHeader } from './Calendar';
4
+ import { useDatePicker } from './DatePickerContext';
5
+ import { handleDateSelectRange, handleDateSelectSingle } from './utils';
6
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
+ /**
8
+ * Calendar that composes Calendar, CalendarHeader, CalendarBody, CalendarFooter.
9
+ * When inside DatePicker: owns local visibleDate and focusedDate; updates shared
10
+ * state via context. Supports single-date and range modes.
11
+ */
12
+ export const DatePickerCalendar = ({
13
+ dialogId,
14
+ weekStartsOn = 0
15
+ }) => {
16
+ const context = useDatePicker();
17
+ const generatedId = useId();
18
+ const fallbackDialogId = `datepicker-calendar-${generatedId.replace(/:/g, '')}`;
19
+ const headingId = dialogId ?? context?.calendarDialogId ?? fallbackDialogId;
20
+ if (context == null) {
21
+ throw new Error('DatePickerCalendar must be used inside a DatePicker (it reads shared state from context).');
22
+ }
23
+ const {
24
+ mode,
25
+ startOrSelectedDate,
26
+ setSelection,
27
+ disabledDates,
28
+ locale,
29
+ closeCalendar,
30
+ isCalendarOpen,
31
+ translations
32
+ } = context;
33
+ const isRange = mode === 'range';
34
+ const endDate = isRange ? context.endDate : undefined;
35
+ const firstOfMonth = date => new Date(date.getFullYear(), date.getMonth(), 1);
36
+ const [visibleDate, setVisibleDate] = useState(() => firstOfMonth(startOrSelectedDate ?? new Date()));
37
+ const [focusedDate, setFocusedDate] = useState(() => startOrSelectedDate ?? endDate ?? new Date());
38
+ const wasOpenRef = useRef(false);
39
+
40
+ // Sync visible month to selection only when the calendar opens, not on every
41
+ // date click. Otherwise clicking a date in the second month would jump the view.
42
+ useEffect(() => {
43
+ const justOpened = isCalendarOpen && !wasOpenRef.current;
44
+ wasOpenRef.current = isCalendarOpen;
45
+ if (!justOpened) return;
46
+ const anchor = startOrSelectedDate ?? endDate;
47
+ if (anchor) {
48
+ setVisibleDate(firstOfMonth(anchor));
49
+ setFocusedDate(startOrSelectedDate ?? endDate ?? new Date());
50
+ }
51
+ }, [isCalendarOpen, startOrSelectedDate, endDate]);
52
+ const onDateSelect = date => {
53
+ if (!isRange) {
54
+ handleDateSelectSingle(date, startOrSelectedDate, setSelection);
55
+ } else {
56
+ context.setActiveRangePart(null);
57
+ handleDateSelectRange(date, context.activeRangePart, startOrSelectedDate, context.endDate, setSelection);
58
+ }
59
+ };
60
+ const handleClearDate = () => {
61
+ setSelection(null);
62
+ setFocusedDate(visibleDate);
63
+ };
64
+ const handleTodayClick = () => {
65
+ const today = new Date();
66
+ setSelection(today);
67
+ setVisibleDate(firstOfMonth(today));
68
+ setFocusedDate(today);
69
+ };
70
+ const focusTarget = focusedDate ?? startOrSelectedDate ?? endDate ?? new Date();
71
+ const addMonths = (date, n) => new Date(date.getFullYear(), date.getMonth() + n, 1);
72
+ const secondMonthDate = addMonths(visibleDate, 1);
73
+ return /*#__PURE__*/_jsxs(Calendar, {
74
+ children: [/*#__PURE__*/_jsxs(Box, {
75
+ p: 24,
76
+ children: [/*#__PURE__*/_jsx(CalendarHeader, {
77
+ currentMonthYear: visibleDate,
78
+ headingId: headingId,
79
+ locale: locale,
80
+ secondMonthYear: secondMonthDate,
81
+ onCurrentMonthYearChange: setVisibleDate
82
+ }), /*#__PURE__*/_jsxs(FlexBox, {
83
+ children: [/*#__PURE__*/_jsx(CalendarBody, {
84
+ disabledDates: disabledDates,
85
+ endDate: endDate,
86
+ focusedDate: focusTarget,
87
+ labelledById: headingId,
88
+ locale: locale,
89
+ selectedDate: startOrSelectedDate,
90
+ visibleDate: visibleDate,
91
+ weekStartsOn: weekStartsOn,
92
+ onDateSelect: onDateSelect,
93
+ onEscapeKeyPress: closeCalendar,
94
+ onFocusedDateChange: setFocusedDate,
95
+ onVisibleDateChange: setVisibleDate
96
+ }), /*#__PURE__*/_jsx(Box, {
97
+ display: {
98
+ _: 'none',
99
+ xs: 'initial'
100
+ },
101
+ pl: {
102
+ _: 0,
103
+ xs: 32
104
+ },
105
+ children: /*#__PURE__*/_jsx(CalendarBody, {
106
+ disabledDates: disabledDates,
107
+ endDate: endDate,
108
+ focusedDate: focusTarget,
109
+ labelledById: headingId,
110
+ locale: locale,
111
+ selectedDate: startOrSelectedDate,
112
+ visibleDate: secondMonthDate,
113
+ weekStartsOn: weekStartsOn,
114
+ onDateSelect: onDateSelect,
115
+ onEscapeKeyPress: closeCalendar,
116
+ onFocusedDateChange: setFocusedDate,
117
+ onVisibleDateChange: setVisibleDate
118
+ })
119
+ })]
120
+ })]
121
+ }), /*#__PURE__*/_jsx(CalendarFooter, {
122
+ clearText: translations.clear,
123
+ disabled: startOrSelectedDate === null && endDate === null,
124
+ locale: locale,
125
+ showClearButton: isRange,
126
+ onClearDate: handleClearDate,
127
+ onTodayClick: handleTodayClick
128
+ })]
129
+ });
130
+ };
@@ -0,0 +1,11 @@
1
+ /// <reference types="react" />
2
+ import type { DatePickerContextValue as DatePickerContextValueType } from './types';
3
+ export declare const DatePickerContext: import("react").Context<DatePickerContextValueType | null>;
4
+ /** Provider component; DatePicker uses this to set the context value. */
5
+ export declare const DatePickerProvider: import("react").Provider<DatePickerContextValueType | null>;
6
+ /**
7
+ * Returns the DatePicker context value (shared state and callbacks).
8
+ * Must be used inside a DatePicker. For composed layouts, use this to get
9
+ * openCalendar, closeCalendar, isCalendarOpen, inputRef, calendarDialogId, etc.
10
+ */
11
+ export declare function useDatePicker(): DatePickerContextValueType;