Skip to content

Commit c860cf8

Browse files
authored
🐛 Fix: prevent multiple datepickers from rendering simultaneously (#233)
* Fix(web): Ensure only one date picker is open at a time in DateTimeSection * Chore(tests): Add mock for tabbable library to improve focus-trap testing * Chore(tests): Add GlobalStyle to custom render for consistent styling in tests * Chore(web): Add role and aria-label to DatePicker for improved accessibility * Test(web): Add EventForm test for date picker open/close functionality * Chore(web): Refactor DateTimeSection import path and remove obsolete file * Fix(EventForm): Change selectedEndDate and selectedStartDate props to required * Fix(EventForm): Initialize endTime and startTime state with default values * Fix(EventForm): Use priorityColor for consistent styling based on event priority * Fix(EventForm): Use globalThis for KeyboardEvent type in key event handlers * Fix(EventForm): Ensure isAllDay defaults to false if not provided * Fix(EventForm): Use optional chaining for onDelete callback * Fix(DateTimeSection): Change date picker view from 'picker' to 'grid' for improved layout * Fix(DateTimeSection): Set isAllDay to false for event creation * Refactor(DateTimeSection): extract pickers to separate component
1 parent af0b6f7 commit c860cf8

File tree

8 files changed

+348
-187
lines changed

8 files changed

+348
-187
lines changed

packages/web/src/__tests__/__mocks__/mock.render.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { sagaMiddleware } from "@web/common/store/middlewares";
1111
import { reducers } from "@web/store/reducers";
1212
import { ThemeProvider } from "styled-components";
1313
import { theme } from "@web/common/styles/theme";
14+
import { GlobalStyle } from "@web/components/GlobalStyle";
1415

1516
const customRender = (
1617
ui: ReactElement,
@@ -32,6 +33,7 @@ const customRender = (
3233
return (
3334
<DndProvider backend={HTML5Backend}>
3435
<GoogleOAuthProvider clientId="anyClientId">
36+
<GlobalStyle />
3537
<ThemeProvider theme={theme}>
3638
<BrowserRouter>
3739
<Provider store={store}>{children}</Provider>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const lib = jest.requireActual("tabbable");
2+
3+
/*
4+
Mocks the 'tabbable' library, used by focus-trap
5+
6+
https://github.com/focus-trap/tabbable
7+
8+
https://stackoverflow.com/questions/72762696/jest-error-your-focus-trap-must-have-at-least-one-container-with-at-least-one
9+
*/
10+
const tabbable = {
11+
...lib,
12+
tabbable: (node, options) =>
13+
lib.tabbable(node, {
14+
...options,
15+
displayCheck: "none",
16+
}),
17+
focusable: (node, options) =>
18+
lib.focusable(node, {
19+
...options,
20+
displayCheck: "none",
21+
}),
22+
isFocusable: (node, options) =>
23+
lib.isFocusable(node, {
24+
...options,
25+
displayCheck: "none",
26+
}),
27+
isTabbable: (node, options) =>
28+
lib.isTabbable(node, {
29+
...options,
30+
displayCheck: "none",
31+
}),
32+
};
33+
34+
module.exports = tabbable;

packages/web/src/components/DatePicker/DatePicker.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ export const DatePicker: React.FC<Props> = ({
7575
{...containerProps}
7676
bgColor={bgColor}
7777
selectedColor={theme.color.text.accent}
78+
role="combobox"
79+
aria-label="datepicker"
7880
view={view}
7981
/>
8082
)}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import React, { FC } from "react";
2+
import dayjs from "dayjs";
3+
import { Key } from "ts-key-enum";
4+
import { darken } from "@core/util/color.utils";
5+
import { MONTH_DAY_YEAR } from "@core/constants/date.constants";
6+
import { DatePicker } from "@web/components/DatePicker";
7+
import { AlignItems } from "@web/components/Flex/styled";
8+
import {
9+
dateIsValid,
10+
shouldAdjustComplimentDate,
11+
} from "@web/common/utils/web.date.util";
12+
13+
import { StyledDateFlex } from "./styled";
14+
15+
const stopPropagation = (e: React.MouseEvent<HTMLDivElement>) => {
16+
e.stopPropagation();
17+
};
18+
19+
interface Props {
20+
bgColor: string;
21+
inputColor?: string;
22+
isEndDatePickerOpen: boolean;
23+
isStartDatePickerOpen: boolean;
24+
selectedEndDate: Date;
25+
selectedStartDate: Date;
26+
setSelectedEndDate: (value: Date) => void;
27+
setSelectedStartDate: (value: Date) => void;
28+
setIsStartDatePickerOpen: (arg0: boolean) => void;
29+
setIsEndDatePickerOpen: (arg0: boolean) => void;
30+
}
31+
32+
export const DatePickers: FC<Props> = ({
33+
bgColor,
34+
inputColor,
35+
isEndDatePickerOpen,
36+
isStartDatePickerOpen,
37+
selectedEndDate,
38+
selectedStartDate,
39+
setIsEndDatePickerOpen,
40+
setIsStartDatePickerOpen,
41+
setSelectedEndDate,
42+
setSelectedStartDate,
43+
}) => {
44+
const adjustComplimentDateIfNeeded = (
45+
changed: "start" | "end",
46+
value: Date
47+
) => {
48+
const start = changed === "start" ? value : selectedStartDate;
49+
const end = changed === "end" ? value : selectedEndDate;
50+
51+
const { shouldAdjust, compliment } = shouldAdjustComplimentDate(changed, {
52+
start,
53+
end,
54+
});
55+
56+
if (shouldAdjust) {
57+
if (changed === "start") {
58+
setSelectedEndDate(compliment);
59+
return;
60+
}
61+
62+
if (changed === "end") {
63+
setSelectedStartDate(compliment);
64+
}
65+
}
66+
};
67+
const closeEndDatePicker = () => {
68+
setIsEndDatePickerOpen(false);
69+
};
70+
71+
const closeStartDatePicker = () => {
72+
setIsStartDatePickerOpen(false);
73+
};
74+
const getDateFromInput = (val: string) => {
75+
const date = dayjs(val, MONTH_DAY_YEAR).toDate();
76+
return date;
77+
};
78+
const onPickerKeyDown = (
79+
picker: "start" | "end",
80+
e: React.KeyboardEvent<HTMLDivElement>
81+
) => {
82+
switch (e.key) {
83+
case Key.Backspace: {
84+
e.stopPropagation();
85+
break;
86+
}
87+
case Key.Enter: {
88+
e.stopPropagation();
89+
const input = e.target as HTMLInputElement;
90+
const val = input.value;
91+
const isInvalid = val !== undefined && !dateIsValid(val);
92+
93+
if (isInvalid) {
94+
alert(`Sorry, IDK what to do with a ${picker} date of '${val}'
95+
Make sure it's in '${MONTH_DAY_YEAR}' and try again`);
96+
return;
97+
}
98+
99+
const date = getDateFromInput(val);
100+
101+
if (picker === "start") {
102+
onSelectStartDate(date);
103+
}
104+
105+
if (picker === "end") {
106+
onSelectEndDate(date);
107+
}
108+
109+
break;
110+
}
111+
case Key.Escape: {
112+
if (isStartDatePickerOpen) {
113+
e.stopPropagation();
114+
closeStartDatePicker();
115+
}
116+
if (isEndDatePickerOpen) {
117+
e.stopPropagation();
118+
closeEndDatePicker();
119+
}
120+
break;
121+
}
122+
case Key.Tab: {
123+
if (isStartDatePickerOpen) {
124+
setIsStartDatePickerOpen(false);
125+
}
126+
if (isEndDatePickerOpen) {
127+
setIsEndDatePickerOpen(false);
128+
}
129+
break;
130+
}
131+
default: {
132+
return;
133+
}
134+
}
135+
};
136+
const onSelectStartDate = (start: Date) => {
137+
setSelectedStartDate(start);
138+
setIsStartDatePickerOpen(false);
139+
adjustComplimentDateIfNeeded("start", start);
140+
};
141+
142+
const onSelectEndDate = (end: Date) => {
143+
setSelectedEndDate(end);
144+
setIsEndDatePickerOpen(false);
145+
adjustComplimentDateIfNeeded("end", end);
146+
};
147+
148+
return (
149+
<>
150+
<StyledDateFlex alignItems={AlignItems.CENTER}>
151+
<div onMouseUp={stopPropagation} onMouseDown={stopPropagation}>
152+
<DatePicker
153+
bgColor={darken(bgColor, 15)}
154+
calendarClassName="startDatePicker"
155+
inputColor={inputColor}
156+
isOpen={isStartDatePickerOpen}
157+
onCalendarClose={closeStartDatePicker}
158+
onCalendarOpen={() => {
159+
setIsStartDatePickerOpen(true);
160+
}}
161+
onChange={() => null}
162+
onInputClick={() => {
163+
isEndDatePickerOpen && setIsEndDatePickerOpen(false);
164+
setIsStartDatePickerOpen(true);
165+
}}
166+
onKeyDown={(e) => onPickerKeyDown("start", e)}
167+
onSelect={onSelectStartDate}
168+
selected={selectedStartDate}
169+
title="Pick Start Date"
170+
view="grid"
171+
/>
172+
</div>
173+
</StyledDateFlex>
174+
175+
<StyledDateFlex alignItems={AlignItems.CENTER}>
176+
<div onMouseUp={stopPropagation} onMouseDown={stopPropagation}>
177+
<DatePicker
178+
bgColor={darken(bgColor, 15)}
179+
calendarClassName="endDatePicker"
180+
inputColor={inputColor}
181+
isOpen={isEndDatePickerOpen}
182+
onCalendarClose={closeEndDatePicker}
183+
onCalendarOpen={() => setIsEndDatePickerOpen(true)}
184+
onChange={() => null}
185+
onInputClick={() => {
186+
isStartDatePickerOpen && setIsStartDatePickerOpen(false);
187+
setIsEndDatePickerOpen(true);
188+
}}
189+
onKeyDown={(e) => onPickerKeyDown("end", e)}
190+
onSelect={onSelectEndDate}
191+
selected={selectedEndDate}
192+
title="Pick End Date"
193+
view="grid"
194+
/>
195+
</div>
196+
</StyledDateFlex>
197+
</>
198+
);
199+
};

0 commit comments

Comments
 (0)