@codecademy/gamut 68.1.3-alpha.bcf87d.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/DatePicker/Calendar/Calendar.d.ts +5 -7
- package/dist/DatePicker/Calendar/Calendar.js +24 -11
- package/dist/DatePicker/Calendar/CalendarBody.js +33 -14
- package/dist/DatePicker/Calendar/CalendarFooter.js +13 -19
- package/dist/DatePicker/Calendar/CalendarHeader.js +52 -29
- package/dist/DatePicker/Calendar/types.d.ts +9 -4
- package/dist/DatePicker/Calendar/utils/dateGrid.js +8 -14
- package/dist/DatePicker/Calendar/utils/format.d.ts +23 -17
- package/dist/DatePicker/Calendar/utils/format.js +64 -71
- package/dist/DatePicker/Calendar/utils/keyHandler.d.ts +1 -1
- package/dist/DatePicker/Calendar/utils/keyHandler.js +17 -8
- package/dist/DatePicker/DatePicker.d.ts +1 -1
- package/dist/DatePicker/DatePicker.js +22 -21
- package/dist/DatePicker/DatePickerCalendar.js +12 -4
- package/dist/DatePicker/DatePickerContext.d.ts +1 -1
- package/dist/DatePicker/DatePickerContext.js +2 -2
- package/dist/DatePicker/DatePickerInput.js +38 -27
- package/dist/DatePicker/translations.d.ts +3 -0
- package/dist/DatePicker/translations.js +8 -0
- package/dist/DatePicker/types.d.ts +18 -1
- package/dist/DatePicker/utils.d.ts +3 -1
- package/dist/DatePicker/utils.js +29 -10
- package/package.json +2 -2
|
@@ -2,66 +2,101 @@
|
|
|
2
2
|
* Date formatting for the calendar using Intl.DateTimeFormat.
|
|
3
3
|
*/
|
|
4
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
|
+
|
|
5
12
|
/**
|
|
6
13
|
* Format month and year for the calendar header (e.g. "February 2026").
|
|
7
14
|
*/
|
|
8
15
|
export const formatMonthYear = (date, locale) => {
|
|
9
|
-
return new Intl.DateTimeFormat(locale
|
|
16
|
+
return new Intl.DateTimeFormat(locale, {
|
|
10
17
|
month: 'long',
|
|
11
18
|
year: 'numeric'
|
|
12
19
|
}).format(date);
|
|
13
20
|
};
|
|
14
21
|
|
|
15
22
|
/**
|
|
16
|
-
* Get
|
|
23
|
+
* Get weekday names for column headers or abbr attributes.
|
|
17
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")
|
|
18
26
|
*/
|
|
19
|
-
export const
|
|
20
|
-
const formatter = new Intl.DateTimeFormat(locale
|
|
21
|
-
weekday:
|
|
27
|
+
export const getWeekdayNames = (format, locale, weekStartsOn = 0) => {
|
|
28
|
+
const formatter = new Intl.DateTimeFormat(locale, {
|
|
29
|
+
weekday: format
|
|
22
30
|
});
|
|
23
31
|
// Jan 7, 2024 is a Sunday; add 0..6 days to get Sun..Sat
|
|
24
32
|
const sunday = new Date(2024, 0, 7);
|
|
25
|
-
const
|
|
33
|
+
const names = Array.from({
|
|
26
34
|
length: 7
|
|
27
35
|
}, (_, i) => {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
return formatter.format(
|
|
36
|
+
const date = new Date(sunday);
|
|
37
|
+
date.setDate(sunday.getDate() + i);
|
|
38
|
+
return formatter.format(date);
|
|
31
39
|
});
|
|
32
40
|
if (weekStartsOn === 1) {
|
|
33
|
-
return [...
|
|
41
|
+
return [...names.slice(1), names[0]];
|
|
34
42
|
}
|
|
35
|
-
return
|
|
43
|
+
return names;
|
|
36
44
|
};
|
|
37
45
|
|
|
38
46
|
/**
|
|
39
|
-
* Get
|
|
40
|
-
*
|
|
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").
|
|
41
49
|
*/
|
|
42
|
-
export const
|
|
43
|
-
const
|
|
44
|
-
|
|
50
|
+
export const getRelativeMonthLabels = locale => {
|
|
51
|
+
const rtf = new Intl.RelativeTimeFormat(locale, {
|
|
52
|
+
numeric: 'auto'
|
|
45
53
|
});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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'
|
|
53
66
|
});
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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('');
|
|
58
93
|
};
|
|
59
94
|
|
|
60
95
|
/**
|
|
61
96
|
* Format a date for display in the date picker input (e.g. "2/15/2026").
|
|
62
97
|
*/
|
|
63
98
|
export const formatDateForInput = (date, locale) => {
|
|
64
|
-
return new Intl.DateTimeFormat(locale
|
|
99
|
+
return new Intl.DateTimeFormat(locale, {
|
|
65
100
|
month: 'numeric',
|
|
66
101
|
day: 'numeric',
|
|
67
102
|
year: 'numeric'
|
|
@@ -79,52 +114,10 @@ export const parseDateFromInput = (value, locale) => {
|
|
|
79
114
|
const trimmed = value.trim();
|
|
80
115
|
if (!trimmed) return null;
|
|
81
116
|
const parsed = new Date(trimmed);
|
|
82
|
-
if (
|
|
117
|
+
if (!isValidDate(parsed)) return null;
|
|
83
118
|
const formatted = formatDateForInput(parsed, locale);
|
|
84
119
|
if (formatted === trimmed) return parsed;
|
|
85
120
|
const parts = trimmed.split(/[/-]/);
|
|
86
121
|
if (parts.length >= 3) return parsed;
|
|
87
122
|
return null;
|
|
88
|
-
};
|
|
89
|
-
const RANGE_SEPARATOR = ' – ';
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Format a date range for the input (e.g. "2/15/2026 – 2/20/2026").
|
|
93
|
-
*/
|
|
94
|
-
export const formatDateRangeForInput = (startDate, endDate, locale) => {
|
|
95
|
-
if (!startDate && !endDate) return '';
|
|
96
|
-
if (!startDate) return formatDateForInput(endDate, locale);
|
|
97
|
-
if (!endDate) return formatDateForInput(startDate, locale);
|
|
98
|
-
return `${formatDateForInput(startDate, locale)}${RANGE_SEPARATOR}${formatDateForInput(endDate, locale)}`;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Parse a range string (e.g. "2/15/2026 – 2/20/2026") into { startDate, endDate }.
|
|
103
|
-
* Returns null if invalid. Single date is allowed and yields startDate = endDate.
|
|
104
|
-
*/
|
|
105
|
-
export const parseDateRangeFromInput = (value, locale) => {
|
|
106
|
-
const trimmed = value.trim();
|
|
107
|
-
if (!trimmed) return null;
|
|
108
|
-
const parts = trimmed.split(RANGE_SEPARATOR).map(s => s.trim());
|
|
109
|
-
if (parts.length === 1) {
|
|
110
|
-
const d = parseDateFromInput(parts[0], locale);
|
|
111
|
-
if (!d) return null;
|
|
112
|
-
return {
|
|
113
|
-
startDate: d,
|
|
114
|
-
endDate: new Date(d)
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
if (parts.length === 2) {
|
|
118
|
-
const start = parseDateFromInput(parts[0], locale);
|
|
119
|
-
const end = parseDateFromInput(parts[1], locale);
|
|
120
|
-
if (!start || !end) return null;
|
|
121
|
-
return start.getTime() <= end.getTime() ? {
|
|
122
|
-
startDate: start,
|
|
123
|
-
endDate: end
|
|
124
|
-
} : {
|
|
125
|
-
startDate: end,
|
|
126
|
-
endDate: start
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
return null;
|
|
130
123
|
};
|
|
@@ -10,4 +10,4 @@ export declare const getDatesWithRow: (weeks: (Date | null)[][]) => {
|
|
|
10
10
|
export declare const keyHandler: (e: React.KeyboardEvent, date: Date, onFocusedDateChange: (date: Date | null) => void, datesWithRow: {
|
|
11
11
|
date: Date;
|
|
12
12
|
rowIndex: number;
|
|
13
|
-
}[], month: number, year: number, disabledDates: Date[], onDateSelect: (date: Date) => void, onEscapeKeyPress?: () => void, onVisibleDateChange?: ((newDate: Date) => void) | undefined) => void;
|
|
13
|
+
}[], month: number, year: number, disabledDates: Date[], onDateSelect: (date: Date) => void, onEscapeKeyPress?: () => void, onVisibleDateChange?: ((newDate: Date) => void) | undefined, hasAdjacentMonthRight?: boolean, hasAdjacentMonthLeft?: boolean) => void;
|
|
@@ -21,14 +21,15 @@ export const getDatesWithRow = weeks => {
|
|
|
21
21
|
});
|
|
22
22
|
return result;
|
|
23
23
|
};
|
|
24
|
-
export const keyHandler = (e, date, onFocusedDateChange, datesWithRow, month, year, disabledDates, onDateSelect, onEscapeKeyPress, onVisibleDateChange) => {
|
|
25
|
-
const key = date.getTime();
|
|
24
|
+
export const keyHandler = (e, date, onFocusedDateChange, datesWithRow, month, year, disabledDates, onDateSelect, onEscapeKeyPress, onVisibleDateChange, hasAdjacentMonthRight, hasAdjacentMonthLeft) => {
|
|
26
25
|
const idx = datesWithRow.findIndex(({
|
|
27
|
-
date:
|
|
28
|
-
}) =>
|
|
26
|
+
date: dateWithRow
|
|
27
|
+
}) => dateWithRow.getTime() === date.getTime());
|
|
29
28
|
if (idx < 0) return;
|
|
30
29
|
const currentRow = datesWithRow[idx].rowIndex;
|
|
31
30
|
const day = date.getDate();
|
|
31
|
+
const hasRight = !!hasAdjacentMonthRight;
|
|
32
|
+
const hasLeft = !!hasAdjacentMonthLeft;
|
|
32
33
|
let newDate = null;
|
|
33
34
|
let newVisibleDate = null;
|
|
34
35
|
switch (e.key) {
|
|
@@ -39,7 +40,9 @@ export const keyHandler = (e, date, onFocusedDateChange, datesWithRow, month, ye
|
|
|
39
40
|
} else {
|
|
40
41
|
const lastDayPrevMonth = new Date(year, month, 0);
|
|
41
42
|
newDate = lastDayPrevMonth;
|
|
42
|
-
|
|
43
|
+
if (!hasLeft) {
|
|
44
|
+
newVisibleDate = new Date(year, month - 1, 1);
|
|
45
|
+
}
|
|
43
46
|
}
|
|
44
47
|
break;
|
|
45
48
|
case 'ArrowRight':
|
|
@@ -48,7 +51,9 @@ export const keyHandler = (e, date, onFocusedDateChange, datesWithRow, month, ye
|
|
|
48
51
|
newDate = datesWithRow[idx + 1].date;
|
|
49
52
|
} else {
|
|
50
53
|
newDate = new Date(year, month + 1, 1);
|
|
51
|
-
|
|
54
|
+
if (!hasRight) {
|
|
55
|
+
newVisibleDate = new Date(year, month + 1, 1);
|
|
56
|
+
}
|
|
52
57
|
}
|
|
53
58
|
break;
|
|
54
59
|
case 'ArrowUp':
|
|
@@ -56,7 +61,9 @@ export const keyHandler = (e, date, onFocusedDateChange, datesWithRow, month, ye
|
|
|
56
61
|
newDate = new Date(date);
|
|
57
62
|
newDate.setDate(newDate.getDate() - 7);
|
|
58
63
|
if (newDate.getMonth() !== month || newDate.getFullYear() !== year) {
|
|
59
|
-
|
|
64
|
+
if (!hasLeft) {
|
|
65
|
+
newVisibleDate = new Date(newDate.getFullYear(), newDate.getMonth(), 1);
|
|
66
|
+
}
|
|
60
67
|
}
|
|
61
68
|
break;
|
|
62
69
|
case 'ArrowDown':
|
|
@@ -64,7 +71,9 @@ export const keyHandler = (e, date, onFocusedDateChange, datesWithRow, month, ye
|
|
|
64
71
|
newDate = new Date(date);
|
|
65
72
|
newDate.setDate(newDate.getDate() + 7);
|
|
66
73
|
if (newDate.getMonth() !== month || newDate.getFullYear() !== year) {
|
|
67
|
-
|
|
74
|
+
if (!hasRight) {
|
|
75
|
+
newVisibleDate = new Date(newDate.getFullYear(), newDate.getMonth(), 1);
|
|
76
|
+
}
|
|
68
77
|
}
|
|
69
78
|
break;
|
|
70
79
|
case 'Home':
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
|
-
import type
|
|
2
|
+
import { type DatePickerProps } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* DatePicker: single-date or range. Holds shared state and provides it via context.
|
|
5
5
|
* Single: selectedDate, setSelectedDate. Range: startDate, endDate, setStartDate, setEndDate.
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { MiniArrowRightIcon } from '@codecademy/gamut-icons';
|
|
1
2
|
import { useCallback, useId, useMemo, useRef, useState } from 'react';
|
|
2
|
-
import { FlexBox } from '../Box';
|
|
3
|
+
import { Box, FlexBox } from '../Box';
|
|
3
4
|
import { PopoverContainer } from '../PopoverContainer';
|
|
4
5
|
import { DatePickerCalendar } from './DatePickerCalendar';
|
|
5
6
|
import { DatePickerProvider } from './DatePickerContext';
|
|
6
7
|
import { DatePickerInput } from './DatePickerInput';
|
|
8
|
+
import { DEFAULT_DATE_PICKER_TRANSLATIONS } from './translations';
|
|
7
9
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
8
10
|
function isRangeProps(props) {
|
|
9
11
|
return props.mode === 'range';
|
|
@@ -16,11 +18,12 @@ function isRangeProps(props) {
|
|
|
16
18
|
*/
|
|
17
19
|
export const DatePicker = props => {
|
|
18
20
|
const {
|
|
19
|
-
locale
|
|
21
|
+
locale,
|
|
20
22
|
disabledDates = [],
|
|
21
23
|
placeholder,
|
|
22
24
|
mode,
|
|
23
|
-
children
|
|
25
|
+
children,
|
|
26
|
+
translations: translationsProp
|
|
24
27
|
} = props;
|
|
25
28
|
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
|
|
26
29
|
const [activeRangePart, setActiveRangePart] = useState(null);
|
|
@@ -44,6 +47,10 @@ export const DatePicker = props => {
|
|
|
44
47
|
}
|
|
45
48
|
}, [props]);
|
|
46
49
|
const contextValue = useMemo(() => {
|
|
50
|
+
const translations = {
|
|
51
|
+
...DEFAULT_DATE_PICKER_TRANSLATIONS,
|
|
52
|
+
...translationsProp
|
|
53
|
+
};
|
|
47
54
|
const base = {
|
|
48
55
|
startOrSelectedDate,
|
|
49
56
|
setSelection,
|
|
@@ -52,7 +59,8 @@ export const DatePicker = props => {
|
|
|
52
59
|
closeCalendar,
|
|
53
60
|
locale,
|
|
54
61
|
disabledDates,
|
|
55
|
-
calendarDialogId
|
|
62
|
+
calendarDialogId,
|
|
63
|
+
translations
|
|
56
64
|
};
|
|
57
65
|
return mode === 'range' ? {
|
|
58
66
|
...base,
|
|
@@ -64,26 +72,21 @@ export const DatePicker = props => {
|
|
|
64
72
|
...base,
|
|
65
73
|
mode: 'single'
|
|
66
74
|
};
|
|
67
|
-
}, [mode, startOrSelectedDate, endDate, setSelection, activeRangePart, setActiveRangePart, isCalendarOpen, openCalendar, closeCalendar, locale, disabledDates, calendarDialogId]);
|
|
68
|
-
|
|
69
|
-
// what is this doing
|
|
70
|
-
// useEffect(() => {
|
|
71
|
-
// if (!isCalendarOpen) return;
|
|
72
|
-
// const id = setTimeout(() => inputRef.current?.focus(), 0);
|
|
73
|
-
// return () => clearTimeout(id);
|
|
74
|
-
// }, [isCalendarOpen]);
|
|
75
|
-
|
|
75
|
+
}, [mode, startOrSelectedDate, endDate, setSelection, activeRangePart, setActiveRangePart, isCalendarOpen, openCalendar, closeCalendar, locale, disabledDates, calendarDialogId, translationsProp]);
|
|
76
76
|
const content = children !== undefined ? children : /*#__PURE__*/_jsxs(_Fragment, {
|
|
77
77
|
children: [/*#__PURE__*/_jsx(FlexBox, {
|
|
78
78
|
gap: 8,
|
|
79
79
|
width: "fit-content",
|
|
80
|
-
wrap: true,
|
|
81
80
|
children: mode === 'range' ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
82
81
|
children: [/*#__PURE__*/_jsx(DatePickerInput, {
|
|
83
82
|
label: props.startLabel,
|
|
84
83
|
placeholder: placeholder,
|
|
85
84
|
rangePart: "start",
|
|
86
85
|
ref: inputRef
|
|
86
|
+
}), /*#__PURE__*/_jsx(Box, {
|
|
87
|
+
alignSelf: "center",
|
|
88
|
+
mt: 32,
|
|
89
|
+
children: /*#__PURE__*/_jsx(MiniArrowRightIcon, {})
|
|
87
90
|
}), /*#__PURE__*/_jsx(DatePickerInput, {
|
|
88
91
|
label: props.endLabel,
|
|
89
92
|
placeholder: placeholder,
|
|
@@ -97,21 +100,19 @@ export const DatePicker = props => {
|
|
|
97
100
|
})
|
|
98
101
|
}), /*#__PURE__*/_jsx(PopoverContainer, {
|
|
99
102
|
alignment: "bottom-left",
|
|
100
|
-
allowPageInteraction: true
|
|
101
|
-
// look into if we can kill this and mess with where we are focusing instead
|
|
102
|
-
,
|
|
103
|
+
allowPageInteraction: true,
|
|
103
104
|
focusOnProps: {
|
|
104
105
|
autoFocus: false,
|
|
105
106
|
focusLock: false
|
|
106
107
|
},
|
|
107
108
|
invertAxis: "x",
|
|
108
109
|
isOpen: isCalendarOpen,
|
|
109
|
-
offset: 10,
|
|
110
110
|
targetRef: inputRef,
|
|
111
|
-
|
|
112
|
-
,
|
|
111
|
+
x: -20,
|
|
112
|
+
y: -16,
|
|
113
|
+
onRequestClose: closeCalendar,
|
|
113
114
|
children: /*#__PURE__*/_jsx("div", {
|
|
114
|
-
"aria-label":
|
|
115
|
+
"aria-label": contextValue.translations.calendarDialogAriaLabel,
|
|
115
116
|
id: calendarDialogId,
|
|
116
117
|
role: "dialog",
|
|
117
118
|
children: /*#__PURE__*/_jsx(DatePickerCalendar, {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { breakpoints } from '@codecademy/gamut-styles';
|
|
1
2
|
import { useEffect, useId, useRef, useState } from 'react';
|
|
3
|
+
import { useMedia } from 'react-use';
|
|
2
4
|
import { Box, FlexBox } from '../Box';
|
|
3
5
|
import { Calendar, CalendarBody, CalendarFooter, CalendarHeader } from './Calendar';
|
|
4
6
|
import { useDatePicker } from './DatePickerContext';
|
|
@@ -27,7 +29,8 @@ export const DatePickerCalendar = ({
|
|
|
27
29
|
disabledDates,
|
|
28
30
|
locale,
|
|
29
31
|
closeCalendar,
|
|
30
|
-
isCalendarOpen
|
|
32
|
+
isCalendarOpen,
|
|
33
|
+
translations
|
|
31
34
|
} = context;
|
|
32
35
|
const isRange = mode === 'range';
|
|
33
36
|
const endDate = isRange ? context.endDate : undefined;
|
|
@@ -53,7 +56,7 @@ export const DatePickerCalendar = ({
|
|
|
53
56
|
handleDateSelectSingle(date, startOrSelectedDate, setSelection);
|
|
54
57
|
} else {
|
|
55
58
|
context.setActiveRangePart(null);
|
|
56
|
-
handleDateSelectRange(date, context.activeRangePart, startOrSelectedDate, context.endDate, setSelection);
|
|
59
|
+
handleDateSelectRange(date, context.activeRangePart, startOrSelectedDate, context.endDate, setSelection, disabledDates);
|
|
57
60
|
}
|
|
58
61
|
};
|
|
59
62
|
const handleClearDate = () => {
|
|
@@ -69,6 +72,7 @@ export const DatePickerCalendar = ({
|
|
|
69
72
|
const focusTarget = focusedDate ?? startOrSelectedDate ?? endDate ?? new Date();
|
|
70
73
|
const addMonths = (date, n) => new Date(date.getFullYear(), date.getMonth() + n, 1);
|
|
71
74
|
const secondMonthDate = addMonths(visibleDate, 1);
|
|
75
|
+
const isTwoMonthsVisible = useMedia(`(min-width: ${breakpoints.xs})`);
|
|
72
76
|
return /*#__PURE__*/_jsxs(Calendar, {
|
|
73
77
|
children: [/*#__PURE__*/_jsxs(Box, {
|
|
74
78
|
p: 24,
|
|
@@ -83,6 +87,7 @@ export const DatePickerCalendar = ({
|
|
|
83
87
|
disabledDates: disabledDates,
|
|
84
88
|
endDate: endDate,
|
|
85
89
|
focusedDate: focusTarget,
|
|
90
|
+
hasAdjacentMonthRight: isTwoMonthsVisible,
|
|
86
91
|
labelledById: headingId,
|
|
87
92
|
locale: locale,
|
|
88
93
|
selectedDate: startOrSelectedDate,
|
|
@@ -105,6 +110,7 @@ export const DatePickerCalendar = ({
|
|
|
105
110
|
disabledDates: disabledDates,
|
|
106
111
|
endDate: endDate,
|
|
107
112
|
focusedDate: focusTarget,
|
|
113
|
+
hasAdjacentMonthLeft: isTwoMonthsVisible,
|
|
108
114
|
labelledById: headingId,
|
|
109
115
|
locale: locale,
|
|
110
116
|
selectedDate: startOrSelectedDate,
|
|
@@ -118,9 +124,11 @@ export const DatePickerCalendar = ({
|
|
|
118
124
|
})]
|
|
119
125
|
})]
|
|
120
126
|
}), /*#__PURE__*/_jsx(CalendarFooter, {
|
|
127
|
+
clearText: translations.clearText,
|
|
128
|
+
disabled: startOrSelectedDate === null && endDate === null,
|
|
129
|
+
locale: locale,
|
|
130
|
+
showClearButton: isRange,
|
|
121
131
|
onClearDate: handleClearDate,
|
|
122
|
-
onCurrentMonthYearChange: setVisibleDate,
|
|
123
|
-
onSelectedDateChange: date => date === null ? handleClearDate() : handleTodayClick(),
|
|
124
132
|
onTodayClick: handleTodayClick
|
|
125
133
|
})]
|
|
126
134
|
});
|
|
@@ -8,4 +8,4 @@ export declare const DatePickerProvider: import("react").Provider<DatePickerCont
|
|
|
8
8
|
* Must be used inside a DatePicker. For composed layouts, use this to get
|
|
9
9
|
* openCalendar, closeCalendar, isCalendarOpen, inputRef, calendarDialogId, etc.
|
|
10
10
|
*/
|
|
11
|
-
export declare
|
|
11
|
+
export declare const useDatePicker: () => DatePickerContextValueType;
|
|
@@ -9,10 +9,10 @@ export const DatePickerProvider = DatePickerContext.Provider;
|
|
|
9
9
|
* Must be used inside a DatePicker. For composed layouts, use this to get
|
|
10
10
|
* openCalendar, closeCalendar, isCalendarOpen, inputRef, calendarDialogId, etc.
|
|
11
11
|
*/
|
|
12
|
-
export
|
|
12
|
+
export const useDatePicker = () => {
|
|
13
13
|
const value = useContext(DatePickerContext);
|
|
14
14
|
if (value == null) {
|
|
15
15
|
throw new Error('useDatePickerContext must be used within a DatePicker.');
|
|
16
16
|
}
|
|
17
17
|
return value;
|
|
18
|
-
}
|
|
18
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MiniCalendarIcon } from '@codecademy/gamut-icons';
|
|
2
2
|
import { forwardRef, useEffect, useId, useRef, useState } from 'react';
|
|
3
|
+
import { FormGroup } from '../Form/elements/FormGroup';
|
|
3
4
|
import { Input } from '../Form/inputs/Input';
|
|
4
|
-
import { formatDateForInput, parseDateFromInput } from './Calendar/utils/format';
|
|
5
|
+
import { formatDateForInput, getDateFormatPattern, parseDateFromInput } from './Calendar/utils/format';
|
|
5
6
|
import { useDatePicker } from './DatePickerContext';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -43,7 +44,8 @@ export const DatePickerInput = /*#__PURE__*/forwardRef(({
|
|
|
43
44
|
openCalendar,
|
|
44
45
|
locale,
|
|
45
46
|
isCalendarOpen,
|
|
46
|
-
calendarDialogId
|
|
47
|
+
calendarDialogId,
|
|
48
|
+
translations
|
|
47
49
|
} = context;
|
|
48
50
|
const isRange = mode === 'range';
|
|
49
51
|
const inputID = useId();
|
|
@@ -97,30 +99,39 @@ export const DatePickerInput = /*#__PURE__*/forwardRef(({
|
|
|
97
99
|
const handleOpenCalendar = () => {
|
|
98
100
|
openCalendar();
|
|
99
101
|
};
|
|
100
|
-
const defaultLabel = isRange
|
|
101
|
-
return /*#__PURE__*/_jsx(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"aria-controls": calendarDialogId,
|
|
105
|
-
"aria-expanded": isCalendarOpen,
|
|
106
|
-
"aria-haspopup": "dialog",
|
|
107
|
-
icon: CalendarIcon // add mini calendar icon and update
|
|
102
|
+
const defaultLabel = !isRange ? translations.dateLabel : rangePart === 'end' ? translations.endDateLabel : translations.startDateLabel;
|
|
103
|
+
return /*#__PURE__*/_jsx(FormGroup, {
|
|
104
|
+
htmlFor: inputId,
|
|
105
|
+
isSoloField: true // should probaly be based on a prop
|
|
108
106
|
,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
107
|
+
label: label ?? defaultLabel,
|
|
108
|
+
mb: 0,
|
|
109
|
+
pb: 0,
|
|
110
|
+
spacing: "tight",
|
|
111
|
+
width: "170px",
|
|
112
|
+
children: /*#__PURE__*/_jsx(Input, {
|
|
113
|
+
...rest,
|
|
114
|
+
"aria-autocomplete": "none",
|
|
115
|
+
"aria-controls": calendarDialogId,
|
|
116
|
+
"aria-expanded": isCalendarOpen,
|
|
117
|
+
"aria-haspopup": "dialog",
|
|
118
|
+
icon: () => /*#__PURE__*/_jsx(MiniCalendarIcon, {
|
|
119
|
+
size: 16
|
|
120
|
+
}),
|
|
121
|
+
id: inputId,
|
|
122
|
+
placeholder: placeholder ?? getDateFormatPattern(locale),
|
|
123
|
+
ref: ref,
|
|
124
|
+
role: "combobox",
|
|
125
|
+
type: "text",
|
|
126
|
+
value: inputValue,
|
|
127
|
+
onBlur: handleBlur,
|
|
128
|
+
onChange: handleChange,
|
|
129
|
+
onClick: handleOpenCalendar,
|
|
130
|
+
onFocus: () => {
|
|
131
|
+
isInputFocusedRef.current = true;
|
|
132
|
+
if (isRange && rangePart) context.setActiveRangePart(rangePart);
|
|
133
|
+
},
|
|
134
|
+
onKeyDown: handleKeyDown
|
|
135
|
+
})
|
|
125
136
|
});
|
|
126
137
|
});
|
|
@@ -17,6 +17,8 @@ export interface DatePickerBaseProps {
|
|
|
17
17
|
children?: React.ReactNode;
|
|
18
18
|
/** Placeholder for the input. */
|
|
19
19
|
placeholder?: string;
|
|
20
|
+
/** Override UI strings (e.g. clear button). Merged with defaults. */
|
|
21
|
+
translations?: DatePickerTranslations;
|
|
20
22
|
}
|
|
21
23
|
/** Props for the DatePicker (single-date mode). */
|
|
22
24
|
export interface DatePickerSingleProps extends DatePickerBaseProps {
|
|
@@ -48,14 +50,29 @@ export interface DatePickerRangeProps extends DatePickerBaseProps {
|
|
|
48
50
|
export type DatePickerProps = DatePickerSingleProps | DatePickerRangeProps;
|
|
49
51
|
/** Which range input is active (focused); null = calendar drives both (selection mode). */
|
|
50
52
|
export type ActiveRangePart = 'start' | 'end' | null;
|
|
53
|
+
/** Optional translations for DatePicker UI strings. Pass to override defaults. */
|
|
54
|
+
export interface DatePickerTranslations {
|
|
55
|
+
/** Label for the clear date button (default: "Clear"). */
|
|
56
|
+
clearText?: string;
|
|
57
|
+
/** Default label for the date input in single mode (default: "Date"). */
|
|
58
|
+
dateLabel?: string;
|
|
59
|
+
/** Default label for the start date input in range mode (default: "Start date"). */
|
|
60
|
+
startDateLabel?: string;
|
|
61
|
+
/** Default label for the end date input in range mode (default: "End date"). */
|
|
62
|
+
endDateLabel?: string;
|
|
63
|
+
/** aria-label for the calendar dialog (default: "Choose date"). */
|
|
64
|
+
calendarDialogAriaLabel?: string;
|
|
65
|
+
}
|
|
51
66
|
/** Shared state provided by DatePicker via context. */
|
|
52
67
|
export interface DatePickerBaseContextValue {
|
|
53
68
|
isCalendarOpen: boolean;
|
|
54
69
|
openCalendar: () => void;
|
|
55
70
|
closeCalendar: () => void;
|
|
56
|
-
locale
|
|
71
|
+
locale?: string;
|
|
57
72
|
disabledDates: Date[];
|
|
58
73
|
calendarDialogId: string;
|
|
74
|
+
/** UI string overrides (e.g. clear button). */
|
|
75
|
+
translations: Required<DatePickerTranslations>;
|
|
59
76
|
/** Start date (range) or selected date (single). */
|
|
60
77
|
startOrSelectedDate: Date | null;
|
|
61
78
|
/** Set selection. Single: (date). Range: (start, end). */
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { ActiveRangePart } from './types';
|
|
2
|
+
/** True if any disabled date falls within [start, end] (inclusive, by calendar day). */
|
|
3
|
+
export declare const rangeContainsDisabled: (start: Date, end: Date, disabledDates: Date[]) => boolean;
|
|
2
4
|
export declare const handleDateSelectSingle: (date: Date, selectedDate: Date | null, setSelection: (date: Date | null) => void) => void;
|
|
3
|
-
export declare const handleDateSelectRange: (date: Date, activeRangePart: ActiveRangePart, startDate: Date | null, endDate: Date | null, setSelection: (startDate: Date | null, endDate?: Date | null) => void) => void;
|
|
5
|
+
export declare const handleDateSelectRange: (date: Date, activeRangePart: ActiveRangePart, startDate: Date | null, endDate: Date | null, setSelection: (startDate: Date | null, endDate?: Date | null) => void, disabledDates: Date[]) => void;
|