Skip to content

Commit d158df1

Browse files
feat(DatePicker): New DatePicker component (#3286)
* calendar phase 1 * add focus management * add input * datepicker * clean up * refactor datepicker * clean up hooks * update exports * add missing validation file * clean up inputref * wip fix for typing date * cleaning up * update range logic * range logic when specific input is focused * deselect logic * fix lint * fix story * fix lint again * update snapshot * fix lint * show 2 months on larger screens * style updates * PR feedback * fix today in range color * clean up context * update placeholder text to be locale based * update next/last month text to be locale based * capitalize * translations * add formgroup * fix calendar alignment and add shadow * clear button logic * more translations * fix disabled hover * make disabledDates optional and add to story * disabled date in range logic * fix keyboard nav now that showing 2 months * PR feedback * toLocaleUpperCase * small fixes * fix range styling * fix alignments * rename props * move around helpers * fix calendar story * support small input size * initial focus a11y updates * PR feedback * clean up utils * add full date aria label to date cells * move around files and clean up exports * update locale * week starts on from locale + polyfill * remove button from DateCell * refactor CalendarHeader * fix errors * fix ci errors * input segments * fix segments typing * dedupe and format * version plan * fix sb import * tests round 1 * CalendarBody tests * style updates * utils tests * segmentUtils tests * segment tests * fix segment refs * close on date select * fix focus & active range part * move DatePicker to organism * WIP mdx file * quick actions * evan pr feedback * dont render empty border in footer * first passthrough of PR feedback * reorg files * update disabled date logic * fix icon button tip sticking around * discriminated union context * change to object syntax * PR feedback * fix date keyboard nav * more tests & tweaks * more tests * sync segment input and calendar month shown * pr feedback * fix bugs * id clean up * always close calendar on escape * form tests * remove robo comments * prop comments and clean up * rename to disableDate * update SB docs * chore(DatePicker): Address Left/Right arrows in calendar for RTL (#3335) address left/right buttons in calendar for RTL * fix failing test * rtl for arrow icon * PR feedback * add placement * change breakpoint * add code example to docs * add height to stories * more doc clean ups * change more to floating and use decorator * Update packages/styleguide/src/lib/Organisms/DatePicker/DatePicker.mdx Co-authored-by: Kenny Lin <kenny.lin.91@gmail.com> * Update packages/styleguide/src/lib/Organisms/DatePicker/DatePicker.mdx Co-authored-by: Kenny Lin <kenny.lin.91@gmail.com> * PR feedback --------- Co-authored-by: Kenny Lin <kenny.lin.91@gmail.com>
1 parent 625b3ae commit d158df1

58 files changed

Lines changed: 8014 additions & 31 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
gamut: minor
3+
---
4+
5+
New DatePicker component

packages/gamut/__tests__/__snapshots__/gamut.test.ts.snap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ exports[`Gamut Exported Keys 1`] = `
3333
"CTAButton",
3434
"DataList",
3535
"DataTable",
36+
"DatePicker",
37+
"DatePickerCalendar",
38+
"DatePickerInput",
39+
"DatePickerProvider",
3640
"DelayedRenderWrapper",
3741
"Dialog",
3842
"Disclosure",
@@ -69,6 +73,7 @@ exports[`Gamut Exported Keys 1`] = `
6973
"ListCol",
7074
"ListRow",
7175
"Markdown",
76+
"matchDisabledDates",
7277
"Menu",
7378
"MenuItem",
7479
"MenuSeparator",
@@ -113,6 +118,7 @@ exports[`Gamut Exported Keys 1`] = `
113118
"ToolTip",
114119
"USE_DEBOUNCED_FIELD_DIRTY_KEY",
115120
"useConnectedForm",
121+
"useDatePicker",
116122
"useDebouncedField",
117123
"useField",
118124
"useFormState",

packages/gamut/jest.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ export default base('gamut', {
88
setupFiles: ['<rootDir>/../../script/jest/base-setup.js'],
99
setupFilesAfterEnv: ['<rootDir>/../../script/jest/rtl-setup.js'],
1010
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
11-
transformIgnorePatterns: ['node_modules/(?!(@vidstack/react)/)'],
11+
transformIgnorePatterns: ['node_modules/(?!(@vidstack/react|@formatjs)/)'],
1212
});

packages/gamut/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"@codecademy/gamut-patterns": "0.10.29",
1010
"@codecademy/gamut-styles": "17.14.0",
1111
"@codecademy/variance": "0.26.1",
12+
"@formatjs/intl-locale": "^5.3.1",
1213
"@react-aria/interactions": "3.25.0",
1314
"@types/marked": "^4.0.8",
1415
"@vidstack/react": "^1.12.12",
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { MiniArrowLeftIcon, MiniArrowRightIcon } from '@codecademy/gamut-icons';
2+
import { useElementDir } from '@codecademy/gamut-styles';
3+
import {
4+
useCallback,
5+
useEffect,
6+
useId,
7+
useMemo,
8+
useRef,
9+
useState,
10+
} from 'react';
11+
12+
import { Box, FlexBox } from '../Box';
13+
import { PopoverContainer } from '../PopoverContainer';
14+
import { DatePickerCalendar } from './DatePickerCalendar';
15+
import {
16+
getDefaultRangeQuickActions,
17+
getDefaultSingleQuickActions,
18+
} from './DatePickerCalendar/utils/quickActions';
19+
import { DatePickerProvider } from './DatePickerContext';
20+
import type {
21+
DatePickerContextValue,
22+
DatePickerRangeContextValue,
23+
} from './DatePickerContext/types';
24+
import { DatePickerInput } from './DatePickerInput';
25+
import type { DatePickerProps } from './types';
26+
import { useResolvedLocale } from './utils/locale';
27+
import { DEFAULT_DATE_PICKER_TRANSLATIONS } from './utils/translations';
28+
29+
export const DatePicker: React.FC<DatePickerProps> = (props) => {
30+
const {
31+
locale,
32+
disableDate,
33+
children,
34+
mode,
35+
translations: translationsProp,
36+
inputSize,
37+
quickActions,
38+
placement = 'inline',
39+
} = props;
40+
const [isCalendarOpen, setIsCalendarOpen] = useState(false);
41+
const [focusGridSignal, setFocusGridSignal] = useState(false);
42+
const [gridFocusRequested, setGridFocusRequested] = useState(false);
43+
const [activeRangePart, setActiveRangePart] =
44+
useState<DatePickerRangeContextValue['activeRangePart']>(null);
45+
const inputRef = useRef<HTMLDivElement | null>(null);
46+
const dialogId = useId();
47+
const calendarDialogId = `datepicker-dialog-${dialogId.replace(/:/g, '')}`;
48+
const isRtl = useElementDir() === 'rtl';
49+
50+
const clearGridFocusRequest = useCallback(() => {
51+
setGridFocusRequested(false);
52+
}, []);
53+
54+
const resolvedLocale = useResolvedLocale(locale);
55+
56+
const openCalendar = useCallback(() => {
57+
setIsCalendarOpen(true);
58+
}, []);
59+
60+
const focusCalendar = useCallback(() => {
61+
setGridFocusRequested(true);
62+
setFocusGridSignal((signal) => !signal);
63+
}, []);
64+
65+
const closeCalendar = useCallback(() => {
66+
setIsCalendarOpen(false);
67+
setActiveRangePart(null);
68+
setGridFocusRequested(false);
69+
const shell = inputRef.current;
70+
const toFocus =
71+
shell?.querySelector<HTMLElement>('[role="spinbutton"]') ?? shell;
72+
toFocus?.focus();
73+
}, []);
74+
75+
useEffect(() => {
76+
if (!isCalendarOpen) return;
77+
const onKeyDown = (e: KeyboardEvent) => {
78+
if (e.key !== 'Escape') return;
79+
e.preventDefault();
80+
e.stopPropagation();
81+
closeCalendar();
82+
};
83+
document.addEventListener('keydown', onKeyDown, true);
84+
return () => document.removeEventListener('keydown', onKeyDown, true);
85+
}, [isCalendarOpen, closeCalendar]);
86+
87+
const contextValue = useMemo<DatePickerContextValue>(() => {
88+
const translations = {
89+
...DEFAULT_DATE_PICKER_TRANSLATIONS,
90+
...translationsProp,
91+
};
92+
const resolvedQuickActions =
93+
quickActions ??
94+
(mode === 'range'
95+
? getDefaultRangeQuickActions(translations)
96+
: getDefaultSingleQuickActions(resolvedLocale));
97+
const base = {
98+
isCalendarOpen,
99+
openCalendar,
100+
focusCalendar,
101+
focusGridSignal,
102+
gridFocusRequested,
103+
clearGridFocusRequest,
104+
closeCalendar,
105+
locale: resolvedLocale,
106+
disableDate,
107+
translations,
108+
quickActions: quickActions === null ? [] : resolvedQuickActions,
109+
};
110+
return mode === 'range'
111+
? {
112+
...base,
113+
mode: 'range',
114+
startDate: props.startDate,
115+
endDate: props.endDate,
116+
activeRangePart,
117+
setActiveRangePart,
118+
onRangeSelection: (startDate: Date | null, endDate: Date | null) => {
119+
props.onStartSelected(startDate);
120+
props.onEndSelected(endDate);
121+
},
122+
}
123+
: {
124+
...base,
125+
mode: 'single',
126+
selectedDate: props.selectedDate,
127+
onSelection: props.onSelected,
128+
};
129+
}, [
130+
translationsProp,
131+
quickActions,
132+
mode,
133+
resolvedLocale,
134+
isCalendarOpen,
135+
openCalendar,
136+
focusCalendar,
137+
focusGridSignal,
138+
gridFocusRequested,
139+
clearGridFocusRequest,
140+
closeCalendar,
141+
disableDate,
142+
props,
143+
activeRangePart,
144+
]);
145+
146+
const content =
147+
children !== undefined ? (
148+
children
149+
) : (
150+
<>
151+
<FlexBox
152+
gap={inputSize === 'small' ? 4 : 8}
153+
ref={inputRef}
154+
width="fit-content"
155+
>
156+
{mode === 'range' ? (
157+
<>
158+
<DatePickerInput
159+
name="datePickerInputStart"
160+
rangePart="start"
161+
size={inputSize}
162+
/>
163+
<Box alignSelf="center" mt={32}>
164+
{isRtl ? <MiniArrowLeftIcon /> : <MiniArrowRightIcon />}
165+
</Box>
166+
<DatePickerInput
167+
name="datePickerInputEnd"
168+
rangePart="end"
169+
size={inputSize}
170+
/>
171+
</>
172+
) : (
173+
<DatePickerInput size={inputSize} />
174+
)}
175+
</FlexBox>
176+
<PopoverContainer
177+
alignment="bottom-left"
178+
allowPageInteraction
179+
focusOnProps={{ autoFocus: false, focusLock: false }}
180+
inline={placement === 'inline'}
181+
invertAxis="x"
182+
isOpen={isCalendarOpen}
183+
targetRef={inputRef}
184+
x={-20}
185+
y={-16}
186+
onRequestClose={closeCalendar}
187+
>
188+
<div
189+
aria-label={contextValue.translations.calendarDialogAriaLabel}
190+
id={calendarDialogId}
191+
role="dialog"
192+
>
193+
<DatePickerCalendar dialogId={calendarDialogId} />
194+
</div>
195+
</PopoverContainer>
196+
</>
197+
);
198+
199+
return (
200+
<DatePickerProvider value={contextValue}>{content}</DatePickerProvider>
201+
);
202+
};

0 commit comments

Comments
 (0)