@codecademy/gamut 68.1.3-alpha.ce69cb.0 → 68.1.3-alpha.da9068.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.
- package/dist/Alert/elements.d.ts +3 -3
- package/dist/Anchor/index.d.ts +9 -18
- package/dist/Anchor/index.js +6 -9
- package/dist/Box/GridBox.d.ts +0 -1
- package/dist/Box/GridBox.js +1 -1
- package/dist/Box/props.d.ts +1 -1
- package/dist/Button/CTAButton.d.ts +2 -2
- package/dist/Button/FillButton.d.ts +4 -4
- package/dist/Button/IconButton.d.ts +4 -4
- package/dist/Button/StrokeButton.d.ts +4 -4
- package/dist/Button/TextButton.d.ts +4 -4
- package/dist/Button/shared/InlineIconButton.d.ts +2 -2
- package/dist/Button/shared/styles.d.ts +3 -3
- package/dist/Button/shared/types.d.ts +1 -1
- package/dist/ButtonBase/ButtonBase.d.ts +4 -9
- package/dist/ButtonBase/ButtonBase.js +4 -11
- package/dist/Card/elements.d.ts +103 -109
- package/dist/Card/styles.d.ts +8 -8
- package/dist/Coachmark/index.d.ts +1 -1
- package/dist/ConnectedForm/utils.d.ts +1 -1
- package/dist/DatePicker/Calendar/Calendar.d.ts +9 -0
- package/dist/DatePicker/Calendar/Calendar.js +28 -0
- package/dist/DatePicker/Calendar/CalendarBody.d.ts +3 -0
- package/dist/DatePicker/Calendar/CalendarBody.js +174 -0
- package/dist/DatePicker/Calendar/CalendarFooter.d.ts +3 -0
- package/dist/DatePicker/Calendar/CalendarFooter.js +54 -0
- package/dist/DatePicker/Calendar/CalendarHeader.d.ts +3 -0
- package/dist/DatePicker/Calendar/CalendarHeader.js +86 -0
- package/dist/DatePicker/Calendar/index.d.ts +6 -0
- package/dist/DatePicker/Calendar/index.js +5 -0
- package/dist/DatePicker/Calendar/types.d.ts +64 -0
- package/dist/DatePicker/Calendar/types.js +1 -0
- package/dist/DatePicker/Calendar/utils/dateGrid.d.ts +30 -0
- package/dist/DatePicker/Calendar/utils/dateGrid.js +87 -0
- package/dist/DatePicker/Calendar/utils/format.d.ts +45 -0
- package/dist/DatePicker/Calendar/utils/format.js +123 -0
- package/dist/DatePicker/Calendar/utils/index.d.ts +3 -0
- package/dist/DatePicker/Calendar/utils/index.js +3 -0
- package/dist/DatePicker/Calendar/utils/keyHandler.d.ts +13 -0
- package/dist/DatePicker/Calendar/utils/keyHandler.js +125 -0
- package/dist/DatePicker/Calendar/utils/validation.d.ts +13 -0
- package/dist/DatePicker/Calendar/utils/validation.js +23 -0
- package/dist/DatePicker/DatePicker.d.ts +8 -0
- package/dist/DatePicker/DatePicker.js +128 -0
- package/dist/DatePicker/DatePickerCalendar.d.ts +13 -0
- package/dist/DatePicker/DatePickerCalendar.js +135 -0
- package/dist/DatePicker/DatePickerContext.d.ts +11 -0
- package/dist/DatePicker/DatePickerContext.js +18 -0
- package/dist/DatePicker/DatePickerInput.d.ts +16 -0
- package/dist/DatePicker/DatePickerInput.js +137 -0
- package/dist/DatePicker/index.d.ts +13 -0
- package/dist/DatePicker/index.js +10 -0
- package/dist/DatePicker/translations.d.ts +3 -0
- package/dist/DatePicker/translations.js +8 -0
- package/dist/DatePicker/types.d.ts +93 -0
- package/dist/DatePicker/types.js +1 -0
- package/dist/DatePicker/utils.d.ts +5 -0
- package/dist/DatePicker/utils.js +90 -0
- package/dist/Disclosure/elements.d.ts +12 -18
- package/dist/FeatureShimmer/index.js +1 -1
- package/dist/FocusTrap/index.d.ts +2 -2
- package/dist/Form/SelectDropdown/SelectDropdown.js +1 -1
- package/dist/Form/SelectDropdown/elements/controls.js +2 -2
- package/dist/Form/SelectDropdown/elements/multi-value.js +2 -2
- package/dist/Form/SelectDropdown/types/internal.d.ts +2 -2
- package/dist/Form/elements/Form.d.ts +15 -15
- package/dist/Form/elements/FormGroup.d.ts +1 -1
- package/dist/GridForm/GridFormButtons/index.d.ts +4 -4
- package/dist/List/ListProvider.d.ts +1 -1
- package/dist/List/elements.d.ts +43 -45
- package/dist/Menu/MenuItem.js +6 -10
- package/dist/Menu/elements.d.ts +2 -2
- package/dist/Modals/elements.d.ts +1 -1
- package/dist/Pagination/AnimatedPaginationButtons.d.ts +32 -34
- package/dist/Pagination/EllipsisButton.d.ts +4 -4
- package/dist/Pagination/PaginationButton.d.ts +6 -6
- package/dist/Pagination/utils.d.ts +30 -32
- package/dist/Pagination/utils.js +11 -14
- package/dist/Popover/Popover.js +4 -4
- package/dist/Popover/types.d.ts +2 -3
- package/dist/PopoverContainer/PopoverContainer.js +13 -9
- package/dist/PopoverContainer/hooks.d.ts +4 -16
- package/dist/PopoverContainer/hooks.js +24 -31
- package/dist/PopoverContainer/types.d.ts +7 -3
- package/dist/Tabs/TabButton.d.ts +2 -2
- package/dist/Tabs/TabNavLink.d.ts +2 -2
- package/dist/Tag/elements.d.ts +8 -14
- package/dist/Tip/InfoTip/InfoTipButton.d.ts +4 -4
- package/dist/Tip/PreviewTip/elements.d.ts +6 -12
- package/dist/Tip/__tests__/helpers.d.ts +1 -1
- package/dist/Tip/shared/FloatingTip.js +2 -2
- package/dist/Tip/shared/types.d.ts +2 -2
- package/dist/Typography/Text.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/react.js +1 -2
- package/package.json +11 -11
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a grid of days for a calendar month using native Date and Intl.
|
|
3
|
+
* Each row has 7 cells; leading/trailing cells may be null (padding from adjacent months).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DAYS_PER_WEEK = 7;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Normalize to start of day in local time for comparison.
|
|
10
|
+
*/
|
|
11
|
+
const normalizeDate = date => {
|
|
12
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the weekday for a date (0 = Sunday, 6 = Saturday).
|
|
17
|
+
* Optionally use weekStartsOn to compute "offset" for display (e.g. Monday = 0).
|
|
18
|
+
*/
|
|
19
|
+
export const getDayOfWeek = (date, weekStartsOn = 0) => {
|
|
20
|
+
const sundayBased = date.getDay();
|
|
21
|
+
if (weekStartsOn === 0) return sundayBased;
|
|
22
|
+
return (sundayBased + 6) % 7; // Monday = 0
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns an array of weeks for the given month. Each week is an array of 7 items:
|
|
27
|
+
* each item is either a Date (that day) or null (padding from previous/next month).
|
|
28
|
+
*
|
|
29
|
+
* @param year - Full year (e.g. 2026)
|
|
30
|
+
* @param month - Month 0-11 (0 = January)
|
|
31
|
+
* @param weekStartsOn - 0 = Sunday, 1 = Monday
|
|
32
|
+
*/
|
|
33
|
+
export const getMonthGrid = (year, month, weekStartsOn = 0) => {
|
|
34
|
+
const first = new Date(year, month, 1);
|
|
35
|
+
const last = new Date(year, month + 1, 0);
|
|
36
|
+
const firstDayOfWeek = getDayOfWeek(first, weekStartsOn);
|
|
37
|
+
const daysInMonth = last.getDate();
|
|
38
|
+
const weeks = [];
|
|
39
|
+
let currentWeek = [];
|
|
40
|
+
for (let i = 0; i < firstDayOfWeek; i += 1) {
|
|
41
|
+
currentWeek.push(null);
|
|
42
|
+
}
|
|
43
|
+
for (let day = 1; day <= daysInMonth; day += 1) {
|
|
44
|
+
currentWeek.push(new Date(year, month, day));
|
|
45
|
+
if (currentWeek.length === DAYS_PER_WEEK) {
|
|
46
|
+
weeks.push(currentWeek);
|
|
47
|
+
currentWeek = [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Pad end of last week with nulls
|
|
52
|
+
if (currentWeek.length > 0) {
|
|
53
|
+
while (currentWeek.length < DAYS_PER_WEEK) {
|
|
54
|
+
currentWeek.push(null);
|
|
55
|
+
}
|
|
56
|
+
weeks.push(currentWeek);
|
|
57
|
+
}
|
|
58
|
+
return weeks;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if two dates are the same calendar day (ignoring time).
|
|
63
|
+
*/
|
|
64
|
+
export const isSameDay = (a, b) => {
|
|
65
|
+
if (a === null || b === null) return false;
|
|
66
|
+
return normalizeDate(a) === normalizeDate(b);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if `date` is between `start` and `end` (exclusive), ignoring time.
|
|
71
|
+
*/
|
|
72
|
+
export const isDateInRange = (date, start, end) => {
|
|
73
|
+
if (start === null) return false;
|
|
74
|
+
const normalizedDateTime = normalizeDate(date);
|
|
75
|
+
const normalizedStartDateTime = normalizeDate(start);
|
|
76
|
+
const normalizedEndDateTime = end !== null ? normalizeDate(end) : normalizedStartDateTime;
|
|
77
|
+
const low = Math.min(normalizedStartDateTime, normalizedEndDateTime);
|
|
78
|
+
const high = Math.max(normalizedStartDateTime, normalizedEndDateTime);
|
|
79
|
+
return normalizedDateTime > low && normalizedDateTime < high;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if `date` is in the `disabledDates` list (by calendar day).
|
|
84
|
+
*/
|
|
85
|
+
export const isDateDisabled = (date, disabledDates = []) => {
|
|
86
|
+
return disabledDates.some(d => isSameDay(date, d));
|
|
87
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date formatting for the calendar using Intl.DateTimeFormat.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Capitalize the first character of a string using the locale; rest unchanged (e.g. "next month" → "Next month").
|
|
6
|
+
*/
|
|
7
|
+
export declare const capitalizeFirst: (str: string, locale?: string) => string;
|
|
8
|
+
/**
|
|
9
|
+
* Format month and year for the calendar header (e.g. "February 2026").
|
|
10
|
+
*/
|
|
11
|
+
export declare const formatMonthYear: (date: Date, locale?: string) => string;
|
|
12
|
+
/**
|
|
13
|
+
* Get weekday names for column headers or abbr attributes.
|
|
14
|
+
* Order depends on weekStartsOn: 0 = Sunday first, 1 = Monday first.
|
|
15
|
+
* @param format - 'short' for abbreviated (e.g. "Su", "Mo"), 'long' for full (e.g. "Sunday", "Monday")
|
|
16
|
+
*/
|
|
17
|
+
export declare const getWeekdayNames: (format: 'short' | 'long', locale?: string, weekStartsOn?: 0 | 1) => string[];
|
|
18
|
+
/**
|
|
19
|
+
* Get localized "next month" and "previous month" labels for calendar nav.
|
|
20
|
+
* Uses Intl.RelativeTimeFormat with numeric: "auto" (e.g. "next month", "last month").
|
|
21
|
+
*/
|
|
22
|
+
export declare const getRelativeMonthLabels: (locale?: string) => {
|
|
23
|
+
nextMonth: string;
|
|
24
|
+
lastMonth: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Get localized "today" label (e.g. "today").
|
|
28
|
+
*/
|
|
29
|
+
export declare const getRelativeTodayLabel: (locale?: string) => string;
|
|
30
|
+
/**
|
|
31
|
+
* Get the locale's short date format pattern (e.g. "MM/DD/YYYY" for en-US,
|
|
32
|
+
* "DD/MM/YYYY" for en-GB). Uses Intl.DateTimeFormat formatToParts to infer
|
|
33
|
+
* order and separators. Useful for parsing or building locale-aware inputs.
|
|
34
|
+
*/
|
|
35
|
+
export declare const getDateFormatPattern: (locale?: string) => string;
|
|
36
|
+
/**
|
|
37
|
+
* Format a date for display in the date picker input (e.g. "2/15/2026").
|
|
38
|
+
*/
|
|
39
|
+
export declare const formatDateForInput: (date: Date, locale?: string) => string;
|
|
40
|
+
/**
|
|
41
|
+
* Parse a string from the date input into a Date, or null if invalid.
|
|
42
|
+
* Only returns a date when the input is a complete valid date (e.g. "2/15/2026").
|
|
43
|
+
* Partial input like "1" or "2/15" returns null even though Date("1") would parse.
|
|
44
|
+
*/
|
|
45
|
+
export declare const parseDateFromInput: (value: string, locale?: string) => Date | null;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date formatting for the calendar using Intl.DateTimeFormat.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { isValidDate } from './validation';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Capitalize the first character of a string using the locale; rest unchanged (e.g. "next month" → "Next month").
|
|
9
|
+
*/
|
|
10
|
+
export const capitalizeFirst = (str, locale) => str.length === 0 ? str : str[0].toLocaleUpperCase(locale) + str.slice(1);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Format month and year for the calendar header (e.g. "February 2026").
|
|
14
|
+
*/
|
|
15
|
+
export const formatMonthYear = (date, locale) => {
|
|
16
|
+
return new Intl.DateTimeFormat(locale, {
|
|
17
|
+
month: 'long',
|
|
18
|
+
year: 'numeric'
|
|
19
|
+
}).format(date);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get weekday names for column headers or abbr attributes.
|
|
24
|
+
* Order depends on weekStartsOn: 0 = Sunday first, 1 = Monday first.
|
|
25
|
+
* @param format - 'short' for abbreviated (e.g. "Su", "Mo"), 'long' for full (e.g. "Sunday", "Monday")
|
|
26
|
+
*/
|
|
27
|
+
export const getWeekdayNames = (format, locale, weekStartsOn = 0) => {
|
|
28
|
+
const formatter = new Intl.DateTimeFormat(locale, {
|
|
29
|
+
weekday: format
|
|
30
|
+
});
|
|
31
|
+
// Jan 7, 2024 is a Sunday; add 0..6 days to get Sun..Sat
|
|
32
|
+
const sunday = new Date(2024, 0, 7);
|
|
33
|
+
const names = Array.from({
|
|
34
|
+
length: 7
|
|
35
|
+
}, (_, i) => {
|
|
36
|
+
const date = new Date(sunday);
|
|
37
|
+
date.setDate(sunday.getDate() + i);
|
|
38
|
+
return formatter.format(date);
|
|
39
|
+
});
|
|
40
|
+
if (weekStartsOn === 1) {
|
|
41
|
+
return [...names.slice(1), names[0]];
|
|
42
|
+
}
|
|
43
|
+
return names;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get localized "next month" and "previous month" labels for calendar nav.
|
|
48
|
+
* Uses Intl.RelativeTimeFormat with numeric: "auto" (e.g. "next month", "last month").
|
|
49
|
+
*/
|
|
50
|
+
export const getRelativeMonthLabels = locale => {
|
|
51
|
+
const rtf = new Intl.RelativeTimeFormat(locale, {
|
|
52
|
+
numeric: 'auto'
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
nextMonth: capitalizeFirst(rtf.format(1, 'month'), locale),
|
|
56
|
+
lastMonth: capitalizeFirst(rtf.format(-1, 'month'), locale)
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get localized "today" label (e.g. "today").
|
|
62
|
+
*/
|
|
63
|
+
export const getRelativeTodayLabel = locale => {
|
|
64
|
+
const rtf = new Intl.RelativeTimeFormat(locale, {
|
|
65
|
+
numeric: 'auto'
|
|
66
|
+
});
|
|
67
|
+
return capitalizeFirst(rtf.format(0, 'day'), locale);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get the locale's short date format pattern (e.g. "MM/DD/YYYY" for en-US,
|
|
72
|
+
* "DD/MM/YYYY" for en-GB). Uses Intl.DateTimeFormat formatToParts to infer
|
|
73
|
+
* order and separators. Useful for parsing or building locale-aware inputs.
|
|
74
|
+
*/
|
|
75
|
+
export const getDateFormatPattern = locale => {
|
|
76
|
+
const parts = new Intl.DateTimeFormat(locale, {
|
|
77
|
+
year: 'numeric',
|
|
78
|
+
month: '2-digit',
|
|
79
|
+
day: '2-digit'
|
|
80
|
+
}).formatToParts(new Date(2025, 0, 15));
|
|
81
|
+
return parts.map(part => {
|
|
82
|
+
switch (part.type) {
|
|
83
|
+
case 'day':
|
|
84
|
+
return 'DD';
|
|
85
|
+
case 'month':
|
|
86
|
+
return 'MM';
|
|
87
|
+
case 'year':
|
|
88
|
+
return 'YYYY';
|
|
89
|
+
default:
|
|
90
|
+
return part.value;
|
|
91
|
+
}
|
|
92
|
+
}).join('');
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Format a date for display in the date picker input (e.g. "2/15/2026").
|
|
97
|
+
*/
|
|
98
|
+
export const formatDateForInput = (date, locale) => {
|
|
99
|
+
return new Intl.DateTimeFormat(locale, {
|
|
100
|
+
month: 'numeric',
|
|
101
|
+
day: 'numeric',
|
|
102
|
+
year: 'numeric'
|
|
103
|
+
}).format(date);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Parse a string from the date input into a Date, or null if invalid.
|
|
108
|
+
* Only returns a date when the input is a complete valid date (e.g. "2/15/2026").
|
|
109
|
+
* Partial input like "1" or "2/15" returns null even though Date("1") would parse.
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
// this logic needs some work
|
|
113
|
+
export const parseDateFromInput = (value, locale) => {
|
|
114
|
+
const trimmed = value.trim();
|
|
115
|
+
if (!trimmed) return null;
|
|
116
|
+
const parsed = new Date(trimmed);
|
|
117
|
+
if (!isValidDate(parsed)) return null;
|
|
118
|
+
const formatted = formatDateForInput(parsed, locale);
|
|
119
|
+
if (formatted === trimmed) return parsed;
|
|
120
|
+
const parts = trimmed.split(/[/-]/);
|
|
121
|
+
if (parts.length >= 3) return parsed;
|
|
122
|
+
return null;
|
|
123
|
+
};
|
|
@@ -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, hasAdjacentMonthRight?: boolean, hasAdjacentMonthLeft?: boolean) => void;
|
|
@@ -0,0 +1,125 @@
|
|
|
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, hasAdjacentMonthRight, hasAdjacentMonthLeft) => {
|
|
25
|
+
const idx = datesWithRow.findIndex(({
|
|
26
|
+
date: dateWithRow
|
|
27
|
+
}) => dateWithRow.getTime() === date.getTime());
|
|
28
|
+
if (idx < 0) return;
|
|
29
|
+
const currentRow = datesWithRow[idx].rowIndex;
|
|
30
|
+
const day = date.getDate();
|
|
31
|
+
const hasRight = !!hasAdjacentMonthRight;
|
|
32
|
+
const hasLeft = !!hasAdjacentMonthLeft;
|
|
33
|
+
let newDate = null;
|
|
34
|
+
let newVisibleDate = null;
|
|
35
|
+
switch (e.key) {
|
|
36
|
+
case 'ArrowLeft':
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
if (idx > 0) {
|
|
39
|
+
newDate = datesWithRow[idx - 1].date;
|
|
40
|
+
} else {
|
|
41
|
+
const lastDayPrevMonth = new Date(year, month, 0);
|
|
42
|
+
newDate = lastDayPrevMonth;
|
|
43
|
+
if (!hasLeft) {
|
|
44
|
+
newVisibleDate = new Date(year, month - 1, 1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
case 'ArrowRight':
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
if (idx < datesWithRow.length - 1) {
|
|
51
|
+
newDate = datesWithRow[idx + 1].date;
|
|
52
|
+
} else {
|
|
53
|
+
newDate = new Date(year, month + 1, 1);
|
|
54
|
+
if (!hasRight) {
|
|
55
|
+
newVisibleDate = new Date(year, month + 1, 1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
case 'ArrowUp':
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
newDate = new Date(date);
|
|
62
|
+
newDate.setDate(newDate.getDate() - 7);
|
|
63
|
+
if (newDate.getMonth() !== month || newDate.getFullYear() !== year) {
|
|
64
|
+
if (!hasLeft) {
|
|
65
|
+
newVisibleDate = new Date(newDate.getFullYear(), newDate.getMonth(), 1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
case 'ArrowDown':
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
newDate = new Date(date);
|
|
72
|
+
newDate.setDate(newDate.getDate() + 7);
|
|
73
|
+
if (newDate.getMonth() !== month || newDate.getFullYear() !== year) {
|
|
74
|
+
if (!hasRight) {
|
|
75
|
+
newVisibleDate = new Date(newDate.getFullYear(), newDate.getMonth(), 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
case 'Home':
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
newDate = datesWithRow.find(({
|
|
82
|
+
rowIndex
|
|
83
|
+
}) => rowIndex === currentRow)?.date ?? date;
|
|
84
|
+
break;
|
|
85
|
+
case 'End':
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
newDate = [...datesWithRow].reverse().find(({
|
|
88
|
+
rowIndex
|
|
89
|
+
}) => rowIndex === currentRow)?.date ?? date;
|
|
90
|
+
break;
|
|
91
|
+
case 'PageDown':
|
|
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 'PageUp':
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
if (e.shiftKey) {
|
|
103
|
+
newDate = clampToMonth(year - 1, month, day);
|
|
104
|
+
} else {
|
|
105
|
+
newDate = clampToMonth(year, month - 1, day);
|
|
106
|
+
}
|
|
107
|
+
newVisibleDate = new Date(newDate.getFullYear(), newDate.getMonth(), 1);
|
|
108
|
+
break;
|
|
109
|
+
case 'Enter':
|
|
110
|
+
case ' ':
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
if (!isDateDisabled(date, disabledDates)) onDateSelect(date);
|
|
113
|
+
return;
|
|
114
|
+
case 'Escape':
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
onEscapeKeyPress?.();
|
|
117
|
+
return;
|
|
118
|
+
default:
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (newDate !== null) {
|
|
122
|
+
onFocusedDateChange(newDate);
|
|
123
|
+
if (newVisibleDate !== null) onVisibleDateChange?.(newVisibleDate);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
@@ -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,128 @@
|
|
|
1
|
+
import { MiniArrowRightIcon } from '@codecademy/gamut-icons';
|
|
2
|
+
import { useCallback, useId, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { Box, 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 openCalendar = useCallback(() => setIsCalendarOpen(true), []);
|
|
34
|
+
const closeCalendar = useCallback(() => {
|
|
35
|
+
setIsCalendarOpen(false);
|
|
36
|
+
setActiveRangePart(null);
|
|
37
|
+
inputRef.current?.focus();
|
|
38
|
+
}, []);
|
|
39
|
+
const startOrSelectedDate = isRangeProps(props) ? props.startDate : props.selectedDate;
|
|
40
|
+
const endDate = isRangeProps(props) ? props.endDate : null;
|
|
41
|
+
const setSelection = useCallback((start, end) => {
|
|
42
|
+
if (isRangeProps(props)) {
|
|
43
|
+
props.setStartDate(start);
|
|
44
|
+
props.setEndDate(end ?? null);
|
|
45
|
+
} else {
|
|
46
|
+
props.setSelectedDate(start);
|
|
47
|
+
}
|
|
48
|
+
}, [props]);
|
|
49
|
+
const contextValue = useMemo(() => {
|
|
50
|
+
const translations = {
|
|
51
|
+
...DEFAULT_DATE_PICKER_TRANSLATIONS,
|
|
52
|
+
...translationsProp
|
|
53
|
+
};
|
|
54
|
+
const base = {
|
|
55
|
+
startOrSelectedDate,
|
|
56
|
+
setSelection,
|
|
57
|
+
isCalendarOpen,
|
|
58
|
+
openCalendar,
|
|
59
|
+
closeCalendar,
|
|
60
|
+
locale,
|
|
61
|
+
disabledDates,
|
|
62
|
+
calendarDialogId,
|
|
63
|
+
translations
|
|
64
|
+
};
|
|
65
|
+
return mode === 'range' ? {
|
|
66
|
+
...base,
|
|
67
|
+
mode: 'range',
|
|
68
|
+
endDate,
|
|
69
|
+
activeRangePart,
|
|
70
|
+
setActiveRangePart
|
|
71
|
+
} : {
|
|
72
|
+
...base,
|
|
73
|
+
mode: 'single'
|
|
74
|
+
};
|
|
75
|
+
}, [mode, startOrSelectedDate, endDate, setSelection, activeRangePart, setActiveRangePart, isCalendarOpen, openCalendar, closeCalendar, locale, disabledDates, calendarDialogId, translationsProp]);
|
|
76
|
+
const content = children !== undefined ? children : /*#__PURE__*/_jsxs(_Fragment, {
|
|
77
|
+
children: [/*#__PURE__*/_jsx(FlexBox, {
|
|
78
|
+
gap: 8,
|
|
79
|
+
width: "fit-content",
|
|
80
|
+
children: mode === 'range' ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
81
|
+
children: [/*#__PURE__*/_jsx(DatePickerInput, {
|
|
82
|
+
label: props.startLabel,
|
|
83
|
+
placeholder: placeholder,
|
|
84
|
+
rangePart: "start",
|
|
85
|
+
ref: inputRef
|
|
86
|
+
}), /*#__PURE__*/_jsx(Box, {
|
|
87
|
+
alignSelf: "center",
|
|
88
|
+
mt: 32,
|
|
89
|
+
children: /*#__PURE__*/_jsx(MiniArrowRightIcon, {})
|
|
90
|
+
}), /*#__PURE__*/_jsx(DatePickerInput, {
|
|
91
|
+
label: props.endLabel,
|
|
92
|
+
placeholder: placeholder,
|
|
93
|
+
rangePart: "end"
|
|
94
|
+
// does this need a ref?
|
|
95
|
+
})]
|
|
96
|
+
}) : /*#__PURE__*/_jsx(DatePickerInput, {
|
|
97
|
+
label: props.label,
|
|
98
|
+
placeholder: placeholder,
|
|
99
|
+
ref: inputRef
|
|
100
|
+
})
|
|
101
|
+
}), /*#__PURE__*/_jsx(PopoverContainer, {
|
|
102
|
+
alignment: "bottom-left",
|
|
103
|
+
allowPageInteraction: true,
|
|
104
|
+
focusOnProps: {
|
|
105
|
+
autoFocus: false,
|
|
106
|
+
focusLock: false
|
|
107
|
+
},
|
|
108
|
+
invertAxis: "x",
|
|
109
|
+
isOpen: isCalendarOpen,
|
|
110
|
+
targetRef: inputRef,
|
|
111
|
+
x: -20,
|
|
112
|
+
y: -16,
|
|
113
|
+
onRequestClose: closeCalendar,
|
|
114
|
+
children: /*#__PURE__*/_jsx("div", {
|
|
115
|
+
"aria-label": contextValue.translations.calendarDialogAriaLabel,
|
|
116
|
+
id: calendarDialogId,
|
|
117
|
+
role: "dialog",
|
|
118
|
+
children: /*#__PURE__*/_jsx(DatePickerCalendar, {
|
|
119
|
+
dialogId: calendarDialogId
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
})]
|
|
123
|
+
});
|
|
124
|
+
return /*#__PURE__*/_jsx(DatePickerProvider, {
|
|
125
|
+
value: contextValue,
|
|
126
|
+
children: content
|
|
127
|
+
});
|
|
128
|
+
};
|
|
@@ -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>;
|