@codecademy/gamut 68.1.3-alpha.bcf87d.0 → 68.1.3-alpha.e93e89.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/CalendarFooter.js +13 -19
- package/dist/DatePicker/Calendar/CalendarHeader.js +15 -11
- package/dist/DatePicker/Calendar/types.d.ts +5 -4
- package/dist/DatePicker/Calendar/utils/format.d.ts +22 -0
- package/dist/DatePicker/Calendar/utils/format.js +58 -4
- package/dist/DatePicker/DatePicker.d.ts +1 -1
- package/dist/DatePicker/DatePicker.js +35 -15
- package/dist/DatePicker/DatePickerCalendar.js +6 -3
- package/dist/DatePicker/DatePickerInput.js +37 -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/package.json +2 -2
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import * as React from 'react';
|
|
2
2
|
/**
|
|
3
3
|
* Outer wrapper for the calendar (header + body + footer).
|
|
4
4
|
* Used by DatePickerCalendar to group the calendar content.
|
|
5
|
+
* Renders a CheckerDense pattern shadow at offset left 8, top 8.
|
|
5
6
|
*/
|
|
6
|
-
export declare const Calendar:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} & {
|
|
10
|
-
theme?: import("@emotion/react").Theme | undefined;
|
|
11
|
-
}, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, {}>;
|
|
7
|
+
export declare const Calendar: React.FC<{
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}>;
|
|
@@ -1,15 +1,28 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { CheckerDense } from '@codecademy/gamut-patterns';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { Box } from '../../Box';
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Outer wrapper for the calendar (header + body + footer).
|
|
5
7
|
* Used by DatePickerCalendar to group the calendar content.
|
|
8
|
+
* Renders a CheckerDense pattern shadow at offset left 8, top 8.
|
|
6
9
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
})(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
11
|
+
export const Calendar = ({
|
|
12
|
+
children
|
|
13
|
+
}) => /*#__PURE__*/_jsxs(Box, {
|
|
14
|
+
position: "relative",
|
|
15
|
+
width: "max-content",
|
|
16
|
+
children: [/*#__PURE__*/_jsx(CheckerDense, {
|
|
17
|
+
left: 8,
|
|
18
|
+
position: "absolute",
|
|
19
|
+
top: 8
|
|
20
|
+
}), /*#__PURE__*/_jsx(Box, {
|
|
21
|
+
bg: "background",
|
|
22
|
+
border: 1,
|
|
23
|
+
borderRadius: "sm",
|
|
24
|
+
position: "relative",
|
|
25
|
+
zIndex: 1,
|
|
26
|
+
children: children
|
|
27
|
+
})]
|
|
28
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { FlexBox } from '../../Box';
|
|
3
3
|
import { TextButton } from '../../Button';
|
|
4
|
-
import {
|
|
4
|
+
import { getRelativeTodayLabel } from './utils/format';
|
|
5
|
+
|
|
5
6
|
// function formatQuickActionLabel(action: QuickAction): string {
|
|
6
7
|
// const { num, timePeriod } = action;
|
|
7
8
|
// const period =
|
|
@@ -22,23 +23,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
22
23
|
// : 'years';
|
|
23
24
|
// return `${num} ${period}`;
|
|
24
25
|
// }
|
|
25
|
-
|
|
26
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
26
27
|
export const CalendarFooter = ({
|
|
27
28
|
onClearDate,
|
|
28
29
|
onTodayClick,
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
locale,
|
|
31
|
+
clearText,
|
|
32
|
+
disabled,
|
|
33
|
+
showClearButton
|
|
31
34
|
}) => {
|
|
32
|
-
const handleClearDate = () => {
|
|
33
|
-
onSelectedDateChange(null);
|
|
34
|
-
onClearDate?.();
|
|
35
|
-
};
|
|
36
|
-
const handleTodayClick = () => {
|
|
37
|
-
const today = new Date();
|
|
38
|
-
onSelectedDateChange(today);
|
|
39
|
-
onCurrentMonthYearChange(new Date(today.getFullYear(), today.getMonth(), 1));
|
|
40
|
-
onTodayClick?.();
|
|
41
|
-
};
|
|
42
35
|
// const actions = quickActions.slice(0, 3);
|
|
43
36
|
|
|
44
37
|
return /*#__PURE__*/_jsxs(FlexBox, {
|
|
@@ -46,14 +39,15 @@ export const CalendarFooter = ({
|
|
|
46
39
|
borderTop: 1,
|
|
47
40
|
justifyContent: "space-between",
|
|
48
41
|
p: 12,
|
|
49
|
-
children: [/*#__PURE__*/_jsx(TextButton, {
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
children: [showClearButton && /*#__PURE__*/_jsx(TextButton, {
|
|
43
|
+
disabled: disabled,
|
|
44
|
+
onClick: () => onClearDate?.(),
|
|
45
|
+
children: clearText
|
|
52
46
|
}), /*#__PURE__*/_jsx(FlexBox, {
|
|
53
47
|
gap: 32,
|
|
54
48
|
children: /*#__PURE__*/_jsx(TextButton, {
|
|
55
|
-
onClick:
|
|
56
|
-
children:
|
|
49
|
+
onClick: () => onTodayClick?.(),
|
|
50
|
+
children: getRelativeTodayLabel(locale)
|
|
57
51
|
})
|
|
58
52
|
})]
|
|
59
53
|
});
|
|
@@ -3,21 +3,25 @@ import * as React from 'react';
|
|
|
3
3
|
import { FlexBox } from '../../Box';
|
|
4
4
|
import { IconButton } from '../../Button';
|
|
5
5
|
import { Text } from '../../Typography';
|
|
6
|
-
import { formatMonthYear } from './utils/format';
|
|
6
|
+
import { formatMonthYear, getRelativeMonthLabels } from './utils/format';
|
|
7
7
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
8
8
|
export const CalendarHeader = ({
|
|
9
9
|
currentMonthYear,
|
|
10
10
|
onCurrentMonthYearChange,
|
|
11
11
|
secondMonthYear,
|
|
12
|
-
|
|
12
|
+
onLastMonthClick,
|
|
13
13
|
onNextMonthClick,
|
|
14
14
|
locale,
|
|
15
15
|
headingId
|
|
16
16
|
}) => {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const {
|
|
18
|
+
nextMonth,
|
|
19
|
+
lastMonth
|
|
20
|
+
} = getRelativeMonthLabels(locale);
|
|
21
|
+
const handleLastMonth = () => {
|
|
22
|
+
const lastMonth = new Date(currentMonthYear.getFullYear(), currentMonthYear.getMonth() - 1, 1);
|
|
23
|
+
onCurrentMonthYearChange?.(lastMonth);
|
|
24
|
+
onLastMonthClick?.();
|
|
21
25
|
};
|
|
22
26
|
const handleNextMonth = () => {
|
|
23
27
|
const nextMonth = new Date(currentMonthYear.getFullYear(), currentMonthYear.getMonth() + 1, 1);
|
|
@@ -29,11 +33,11 @@ export const CalendarHeader = ({
|
|
|
29
33
|
justifyContent: "space-between",
|
|
30
34
|
pb: 16,
|
|
31
35
|
children: [/*#__PURE__*/_jsx(IconButton, {
|
|
32
|
-
"aria-label":
|
|
36
|
+
"aria-label": lastMonth,
|
|
33
37
|
icon: MiniChevronLeftIcon,
|
|
34
38
|
size: "small",
|
|
35
|
-
tip:
|
|
36
|
-
onClick:
|
|
39
|
+
tip: lastMonth,
|
|
40
|
+
onClick: handleLastMonth
|
|
37
41
|
}), /*#__PURE__*/_jsx(Text, {
|
|
38
42
|
"aria-live": "polite",
|
|
39
43
|
as: "h2",
|
|
@@ -53,10 +57,10 @@ export const CalendarHeader = ({
|
|
|
53
57
|
id: headingId,
|
|
54
58
|
children: formatMonthYear(secondMonthYear, locale)
|
|
55
59
|
}), /*#__PURE__*/_jsx(IconButton, {
|
|
56
|
-
"aria-label":
|
|
60
|
+
"aria-label": nextMonth,
|
|
57
61
|
icon: MiniChevronRightIcon,
|
|
58
62
|
size: "small",
|
|
59
|
-
tip:
|
|
63
|
+
tip: nextMonth,
|
|
60
64
|
onClick: handleNextMonth
|
|
61
65
|
})]
|
|
62
66
|
});
|
|
@@ -9,7 +9,7 @@ export interface CalendarHeaderProps {
|
|
|
9
9
|
/** Currently displayed second month and year (used for heading and prev/next range) */
|
|
10
10
|
secondMonthYear?: Date;
|
|
11
11
|
/** Called after navigating to previous month; use for click tracking. */
|
|
12
|
-
|
|
12
|
+
onLastMonthClick?: () => void;
|
|
13
13
|
/** Called after navigating to next month; use for click tracking. */
|
|
14
14
|
onNextMonthClick?: () => void;
|
|
15
15
|
/** Locale for month/year formatting (e.g. 'en-US') */
|
|
@@ -49,11 +49,12 @@ export interface QuickAction {
|
|
|
49
49
|
onClick: () => void;
|
|
50
50
|
}
|
|
51
51
|
export interface CalendarFooterProps {
|
|
52
|
+
disabled?: boolean;
|
|
53
|
+
showClearButton?: boolean;
|
|
54
|
+
locale?: string;
|
|
55
|
+
clearText: string;
|
|
52
56
|
onClearDate?: () => void;
|
|
53
57
|
onTodayClick?: () => void;
|
|
54
|
-
/** Called when the user navigates to a different month. Pass the new date (e.g. setVisibleDate) so the calendar updates. */
|
|
55
|
-
onSelectedDateChange: (newDate: Date | null) => void;
|
|
56
|
-
onCurrentMonthYearChange: (newDate: Date) => void;
|
|
57
58
|
/** Max 3 quick actions (e.g. "7 days", "1 month") */
|
|
58
59
|
quickActions?: QuickAction[];
|
|
59
60
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Date formatting for the calendar using Intl.DateTimeFormat.
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Capitalize the first character of a string; rest unchanged (e.g. "next month" → "Next month").
|
|
6
|
+
*/
|
|
7
|
+
export declare const capitalizeFirst: (str: string) => string;
|
|
4
8
|
/**
|
|
5
9
|
* Format month and year for the calendar header (e.g. "February 2026").
|
|
6
10
|
*/
|
|
@@ -15,6 +19,24 @@ export declare const getWeekdayLabels: (locale?: string, weekStartsOn?: 0 | 1) =
|
|
|
15
19
|
* Same order as getWeekdayLabels.
|
|
16
20
|
*/
|
|
17
21
|
export declare const getWeekdayFullNames: (locale?: string, weekStartsOn?: 0 | 1) => string[];
|
|
22
|
+
/**
|
|
23
|
+
* Get localized "next month" and "previous month" labels for calendar nav.
|
|
24
|
+
* Uses Intl.RelativeTimeFormat with numeric: "auto" (e.g. "next month", "last month").
|
|
25
|
+
*/
|
|
26
|
+
export declare const getRelativeMonthLabels: (locale?: string) => {
|
|
27
|
+
nextMonth: string;
|
|
28
|
+
lastMonth: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Get localized "today" label (e.g. "today").
|
|
32
|
+
*/
|
|
33
|
+
export declare const getRelativeTodayLabel: (locale?: string) => string;
|
|
34
|
+
/**
|
|
35
|
+
* Get the locale's short date format pattern (e.g. "MM/DD/YYYY" for en-US,
|
|
36
|
+
* "DD/MM/YYYY" for en-GB). Uses Intl.DateTimeFormat formatToParts to infer
|
|
37
|
+
* order and separators. Useful for parsing or building locale-aware inputs.
|
|
38
|
+
*/
|
|
39
|
+
export declare const getDateFormatPattern: (locale?: string) => string;
|
|
18
40
|
/**
|
|
19
41
|
* Format a date for display in the date picker input (e.g. "2/15/2026").
|
|
20
42
|
*/
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
* Date formatting for the calendar using Intl.DateTimeFormat.
|
|
3
3
|
*/
|
|
4
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
|
+
|
|
5
10
|
/**
|
|
6
11
|
* Format month and year for the calendar header (e.g. "February 2026").
|
|
7
12
|
*/
|
|
8
13
|
export const formatMonthYear = (date, locale) => {
|
|
9
|
-
return new Intl.DateTimeFormat(locale
|
|
14
|
+
return new Intl.DateTimeFormat(locale, {
|
|
10
15
|
month: 'long',
|
|
11
16
|
year: 'numeric'
|
|
12
17
|
}).format(date);
|
|
@@ -17,7 +22,7 @@ export const formatMonthYear = (date, locale) => {
|
|
|
17
22
|
* Order depends on weekStartsOn: 0 = Sunday first, 1 = Monday first.
|
|
18
23
|
*/
|
|
19
24
|
export const getWeekdayLabels = (locale, weekStartsOn = 0) => {
|
|
20
|
-
const formatter = new Intl.DateTimeFormat(locale
|
|
25
|
+
const formatter = new Intl.DateTimeFormat(locale, {
|
|
21
26
|
weekday: 'short'
|
|
22
27
|
});
|
|
23
28
|
// Jan 7, 2024 is a Sunday; add 0..6 days to get Sun..Sat
|
|
@@ -40,7 +45,7 @@ export const getWeekdayLabels = (locale, weekStartsOn = 0) => {
|
|
|
40
45
|
* Same order as getWeekdayLabels.
|
|
41
46
|
*/
|
|
42
47
|
export const getWeekdayFullNames = (locale, weekStartsOn = 0) => {
|
|
43
|
-
const formatter = new Intl.DateTimeFormat(locale
|
|
48
|
+
const formatter = new Intl.DateTimeFormat(locale, {
|
|
44
49
|
weekday: 'long'
|
|
45
50
|
});
|
|
46
51
|
const sunday = new Date(2024, 0, 7);
|
|
@@ -57,11 +62,60 @@ export const getWeekdayFullNames = (locale, weekStartsOn = 0) => {
|
|
|
57
62
|
return names;
|
|
58
63
|
};
|
|
59
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
|
+
|
|
60
114
|
/**
|
|
61
115
|
* Format a date for display in the date picker input (e.g. "2/15/2026").
|
|
62
116
|
*/
|
|
63
117
|
export const formatDateForInput = (date, locale) => {
|
|
64
|
-
return new Intl.DateTimeFormat(locale
|
|
118
|
+
return new Intl.DateTimeFormat(locale, {
|
|
65
119
|
month: 'numeric',
|
|
66
120
|
day: 'numeric',
|
|
67
121
|
year: 'numeric'
|
|
@@ -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 {
|
|
1
|
+
import { MiniArrowRightIcon } from '@codecademy/gamut-icons';
|
|
2
|
+
import { useCallback, useId, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
3
|
import { 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,17 +18,30 @@ 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);
|
|
27
30
|
const inputRef = useRef(null);
|
|
28
31
|
const dialogId = useId();
|
|
29
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]);
|
|
30
45
|
const openCalendar = useCallback(() => setIsCalendarOpen(true), []);
|
|
31
46
|
const closeCalendar = useCallback(() => {
|
|
32
47
|
setIsCalendarOpen(false);
|
|
@@ -44,6 +59,10 @@ export const DatePicker = props => {
|
|
|
44
59
|
}
|
|
45
60
|
}, [props]);
|
|
46
61
|
const contextValue = useMemo(() => {
|
|
62
|
+
const translations = {
|
|
63
|
+
...DEFAULT_DATE_PICKER_TRANSLATIONS,
|
|
64
|
+
...translationsProp
|
|
65
|
+
};
|
|
47
66
|
const base = {
|
|
48
67
|
startOrSelectedDate,
|
|
49
68
|
setSelection,
|
|
@@ -52,7 +71,8 @@ export const DatePicker = props => {
|
|
|
52
71
|
closeCalendar,
|
|
53
72
|
locale,
|
|
54
73
|
disabledDates,
|
|
55
|
-
calendarDialogId
|
|
74
|
+
calendarDialogId,
|
|
75
|
+
translations
|
|
56
76
|
};
|
|
57
77
|
return mode === 'range' ? {
|
|
58
78
|
...base,
|
|
@@ -64,7 +84,7 @@ export const DatePicker = props => {
|
|
|
64
84
|
...base,
|
|
65
85
|
mode: 'single'
|
|
66
86
|
};
|
|
67
|
-
}, [mode, startOrSelectedDate, endDate, setSelection, activeRangePart, setActiveRangePart, isCalendarOpen, openCalendar, closeCalendar, locale, disabledDates, calendarDialogId]);
|
|
87
|
+
}, [mode, startOrSelectedDate, endDate, setSelection, activeRangePart, setActiveRangePart, isCalendarOpen, openCalendar, closeCalendar, locale, disabledDates, calendarDialogId, translationsProp]);
|
|
68
88
|
|
|
69
89
|
// what is this doing
|
|
70
90
|
// useEffect(() => {
|
|
@@ -84,7 +104,9 @@ export const DatePicker = props => {
|
|
|
84
104
|
placeholder: placeholder,
|
|
85
105
|
rangePart: "start",
|
|
86
106
|
ref: inputRef
|
|
87
|
-
}), /*#__PURE__*/_jsx(
|
|
107
|
+
}), /*#__PURE__*/_jsx(MiniArrowRightIcon, {
|
|
108
|
+
alignSelf: "center"
|
|
109
|
+
}), ' ', /*#__PURE__*/_jsx(DatePickerInput, {
|
|
88
110
|
label: props.endLabel,
|
|
89
111
|
placeholder: placeholder,
|
|
90
112
|
rangePart: "end"
|
|
@@ -96,22 +118,20 @@ export const DatePicker = props => {
|
|
|
96
118
|
ref: inputRef
|
|
97
119
|
})
|
|
98
120
|
}), /*#__PURE__*/_jsx(PopoverContainer, {
|
|
99
|
-
alignment: "bottom-
|
|
100
|
-
allowPageInteraction: true
|
|
101
|
-
// look into if we can kill this and mess with where we are focusing instead
|
|
102
|
-
,
|
|
121
|
+
alignment: "bottom-right",
|
|
122
|
+
allowPageInteraction: true,
|
|
103
123
|
focusOnProps: {
|
|
104
124
|
autoFocus: false,
|
|
105
125
|
focusLock: false
|
|
106
126
|
},
|
|
107
|
-
invertAxis: "x",
|
|
108
127
|
isOpen: isCalendarOpen,
|
|
109
|
-
offset:
|
|
128
|
+
offset: popoverOffset,
|
|
110
129
|
targetRef: inputRef,
|
|
111
|
-
|
|
112
|
-
,
|
|
130
|
+
x: popoverX,
|
|
131
|
+
y: 0,
|
|
132
|
+
onRequestClose: closeCalendar,
|
|
113
133
|
children: /*#__PURE__*/_jsx("div", {
|
|
114
|
-
"aria-label":
|
|
134
|
+
"aria-label": contextValue.translations.calendarDialogAriaLabel,
|
|
115
135
|
id: calendarDialogId,
|
|
116
136
|
role: "dialog",
|
|
117
137
|
children: /*#__PURE__*/_jsx(DatePickerCalendar, {
|
|
@@ -27,7 +27,8 @@ export const DatePickerCalendar = ({
|
|
|
27
27
|
disabledDates,
|
|
28
28
|
locale,
|
|
29
29
|
closeCalendar,
|
|
30
|
-
isCalendarOpen
|
|
30
|
+
isCalendarOpen,
|
|
31
|
+
translations
|
|
31
32
|
} = context;
|
|
32
33
|
const isRange = mode === 'range';
|
|
33
34
|
const endDate = isRange ? context.endDate : undefined;
|
|
@@ -118,9 +119,11 @@ export const DatePickerCalendar = ({
|
|
|
118
119
|
})]
|
|
119
120
|
})]
|
|
120
121
|
}), /*#__PURE__*/_jsx(CalendarFooter, {
|
|
122
|
+
clearText: translations.clearText,
|
|
123
|
+
disabled: startOrSelectedDate === null && endDate === null,
|
|
124
|
+
locale: locale,
|
|
125
|
+
showClearButton: isRange,
|
|
121
126
|
onClearDate: handleClearDate,
|
|
122
|
-
onCurrentMonthYearChange: setVisibleDate,
|
|
123
|
-
onSelectedDateChange: date => date === null ? handleClearDate() : handleTodayClick(),
|
|
124
127
|
onTodayClick: handleTodayClick
|
|
125
128
|
})]
|
|
126
129
|
});
|
|
@@ -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,38 @@ 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
|
+
pb: 0,
|
|
109
|
+
spacing: "tight",
|
|
110
|
+
width: "170px",
|
|
111
|
+
children: /*#__PURE__*/_jsx(Input, {
|
|
112
|
+
...rest,
|
|
113
|
+
"aria-autocomplete": "none",
|
|
114
|
+
"aria-controls": calendarDialogId,
|
|
115
|
+
"aria-expanded": isCalendarOpen,
|
|
116
|
+
"aria-haspopup": "dialog",
|
|
117
|
+
icon: () => /*#__PURE__*/_jsx(MiniCalendarIcon, {
|
|
118
|
+
size: 16
|
|
119
|
+
}),
|
|
120
|
+
id: inputId,
|
|
121
|
+
placeholder: placeholder ?? getDateFormatPattern(locale),
|
|
122
|
+
ref: ref,
|
|
123
|
+
role: "combobox",
|
|
124
|
+
type: "text",
|
|
125
|
+
value: inputValue,
|
|
126
|
+
onBlur: handleBlur,
|
|
127
|
+
onChange: handleChange,
|
|
128
|
+
onClick: handleOpenCalendar,
|
|
129
|
+
onFocus: () => {
|
|
130
|
+
isInputFocusedRef.current = true;
|
|
131
|
+
if (isRange && rangePart) context.setActiveRangePart(rangePart);
|
|
132
|
+
},
|
|
133
|
+
onKeyDown: handleKeyDown
|
|
134
|
+
})
|
|
125
135
|
});
|
|
126
136
|
});
|
|
@@ -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). */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codecademy/gamut",
|
|
3
3
|
"description": "Styleguide & Component library for Codecademy",
|
|
4
|
-
"version": "68.1.3-alpha.
|
|
4
|
+
"version": "68.1.3-alpha.e93e89.0",
|
|
5
5
|
"author": "Codecademy Engineering <dev@codecademy.com>",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@codecademy/gamut-icons": "9.57.0",
|
|
@@ -59,5 +59,5 @@
|
|
|
59
59
|
"dist/**/[A-Z]**/[A-Z]*.js",
|
|
60
60
|
"dist/**/[A-Z]**/index.js"
|
|
61
61
|
],
|
|
62
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "597c47b90e8bb7ebae7c00c214c0ef0887bcb8a5"
|
|
63
63
|
}
|