Skip to content

Commit 84c916c

Browse files
Create DateRangePicker component (#575)
1 parent 3f80b9c commit 84c916c

File tree

5 files changed

+788
-261
lines changed

5 files changed

+788
-261
lines changed

src/components/DatePicker/Common.tsx

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
import styled from "styled-components";
2+
import { InputElement, InputWrapper } from "../Input/InputWrapper";
3+
import { ReactNode, useCallback, useId } from "react";
4+
import { Icon } from "../Icon/Icon";
5+
import { Container } from "../Container/Container";
6+
import { useCalendar, UseCalendarOptions } from "@h6s/calendar";
7+
import { IconButton } from "../IconButton/IconButton";
8+
import { Text } from "../Typography/Text/Text";
9+
10+
const locale = "en-US";
11+
const selectedDateFormatter = new Intl.DateTimeFormat(locale, {
12+
day: "2-digit",
13+
month: "short",
14+
year: "numeric",
15+
});
16+
17+
const explicitWidth = "250px";
18+
19+
const HighlightedInputWrapper = styled(InputWrapper)<{ $isActive: boolean }>`
20+
${({ $isActive, theme }) => {
21+
return `border: ${theme.click.datePicker.dateOption.stroke} solid ${
22+
$isActive
23+
? theme.click.datePicker.dateOption.color.stroke.active
24+
: theme.click.field.color.stroke.default
25+
};`;
26+
}}
27+
28+
width: ${explicitWidth};
29+
}`;
30+
31+
interface DatePickerInputProps {
32+
isActive: boolean;
33+
disabled: boolean;
34+
id?: string;
35+
placeholder?: string;
36+
selectedDate?: Date;
37+
}
38+
39+
export const DatePickerInput = ({
40+
isActive,
41+
disabled,
42+
id,
43+
placeholder,
44+
selectedDate,
45+
}: DatePickerInputProps) => {
46+
const defaultId = useId();
47+
const formattedSelectedDate =
48+
selectedDate instanceof Date ? selectedDateFormatter.format(selectedDate) : "";
49+
50+
return (
51+
<HighlightedInputWrapper
52+
$isActive={isActive}
53+
disabled={disabled}
54+
id={id ?? defaultId}
55+
>
56+
<Icon name="calendar" />
57+
<InputElement
58+
data-testid="datepicker-input"
59+
placeholder={placeholder}
60+
readOnly
61+
value={formattedSelectedDate}
62+
/>
63+
</HighlightedInputWrapper>
64+
);
65+
};
66+
67+
interface DateRangePickerInputProps {
68+
isActive: boolean;
69+
disabled: boolean;
70+
id?: string;
71+
placeholder?: string;
72+
selectedEndDate?: Date;
73+
selectedStartDate?: Date;
74+
}
75+
76+
export const DateRangePickerInput = ({
77+
isActive,
78+
disabled,
79+
id,
80+
placeholder,
81+
selectedEndDate,
82+
selectedStartDate,
83+
}: DateRangePickerInputProps) => {
84+
const defaultId = useId();
85+
86+
let formattedValue = (
87+
<Text
88+
color="muted"
89+
component="span"
90+
>
91+
{placeholder ?? ""}
92+
</Text>
93+
);
94+
if (selectedStartDate) {
95+
if (selectedEndDate) {
96+
formattedValue = (
97+
<span>
98+
{selectedDateFormatter.format(selectedStartDate)}{" "}
99+
{selectedDateFormatter.format(selectedEndDate)}
100+
</span>
101+
);
102+
} else {
103+
formattedValue = (
104+
<span>
105+
{selectedDateFormatter.format(selectedStartDate)}{" "}
106+
<Text
107+
color="muted"
108+
component="span"
109+
>
110+
– end date
111+
</Text>
112+
</span>
113+
);
114+
}
115+
}
116+
117+
return (
118+
<HighlightedInputWrapper
119+
$isActive={isActive}
120+
disabled={disabled}
121+
id={id ?? defaultId}
122+
>
123+
<Icon name="calendar" />
124+
<InputElement
125+
as="div"
126+
data-testid="daterangepicker-input"
127+
>
128+
{formattedValue}
129+
</InputElement>
130+
</HighlightedInputWrapper>
131+
);
132+
};
133+
134+
const weekdayFormatter = new Intl.DateTimeFormat(locale, { weekday: "short" });
135+
const headerDateFormatter = new Intl.DateTimeFormat(locale, {
136+
month: "short",
137+
year: "numeric",
138+
});
139+
140+
const DatePickerContainer = styled(Container)`
141+
background: ${({ theme }) =>
142+
theme.click.datePicker.dateOption.color.background.default};
143+
`;
144+
145+
const UnselectableTitle = styled.h2`
146+
${({ theme }) => `
147+
color: ${theme.click.datePicker.color.title.default};
148+
font: ${theme.click.datePicker.typography.title.default};
149+
`}
150+
151+
user-select: none;
152+
`;
153+
154+
const DateTable = styled.table`
155+
border-collapse: separate;
156+
border-spacing: 0;
157+
font: ${({ theme }) => theme.typography.styles.product.text.normal.md};
158+
table-layout: fixed;
159+
user-select: none;
160+
width: ${explicitWidth};
161+
162+
thead tr {
163+
height: ${({ theme }) => theme.click.datePicker.dateOption.size.height};
164+
}
165+
166+
tbody {
167+
cursor: pointer;
168+
}
169+
170+
td,
171+
th {
172+
padding: 4px;
173+
}
174+
`;
175+
176+
const DateTableHeader = styled.th`
177+
${({ theme }) => `
178+
color: ${theme.click.datePicker.color.daytitle.default};
179+
font: ${theme.click.datePicker.typography.daytitle.default};
180+
`}
181+
182+
width: 14%;
183+
`;
184+
185+
export const DateTableCell = styled.td<{
186+
$isCurrentMonth?: boolean;
187+
$isDisabled?: boolean;
188+
$isSelected?: boolean;
189+
$isToday?: boolean;
190+
}>`
191+
${({ theme }) => `
192+
border: ${theme.click.datePicker.dateOption.stroke} solid ${theme.click.datePicker.dateOption.color.stroke.default};
193+
border-radius: ${theme.click.datePicker.dateOption.radii.default};
194+
font: ${theme.click.datePicker.dateOption.typography.label.default};
195+
`}
196+
197+
${({ $isCurrentMonth, $isDisabled, theme }) =>
198+
(!$isCurrentMonth || $isDisabled) &&
199+
`
200+
color: ${theme.click.datePicker.dateOption.color.label.disabled};
201+
font: ${theme.click.datePicker.dateOption.typography.label.disabled};
202+
`}
203+
204+
${({ $isSelected, theme }) =>
205+
$isSelected &&
206+
`
207+
background: ${theme.click.datePicker.dateOption.color.background.active};
208+
color: ${theme.click.datePicker.dateOption.color.label.active};
209+
`}
210+
211+
212+
text-align: center;
213+
214+
${({ $isToday, theme }) =>
215+
$isToday && `font: ${theme.click.datePicker.dateOption.typography.label.active};`}
216+
217+
&:hover {
218+
${({ $isDisabled, theme }) =>
219+
`border: ${theme.click.datePicker.dateOption.stroke} solid ${
220+
$isDisabled
221+
? theme.click.datePicker.dateOption.color.stroke.disabled
222+
: theme.click.datePicker.dateOption.color.stroke.hover
223+
};
224+
225+
226+
border-radius: ${theme.click.datePicker.dateOption.radii.default};`};
227+
}
228+
`;
229+
230+
export type Body = ReturnType<typeof useCalendar>["body"];
231+
232+
interface CalendarRendererProps {
233+
calendarOptions?: UseCalendarOptions;
234+
children: (body: Body) => ReactNode;
235+
}
236+
237+
export const CalendarRenderer = ({
238+
calendarOptions = {},
239+
children,
240+
...props
241+
}: CalendarRendererProps) => {
242+
const { body, headers, month, navigation, year } = useCalendar({
243+
defaultWeekStart: 1,
244+
...calendarOptions,
245+
});
246+
247+
const handleNextClick = useCallback((): void => {
248+
navigation.toNext();
249+
}, [navigation]);
250+
251+
const handlePreviousClick = useCallback((): void => {
252+
navigation.toPrev();
253+
}, [navigation]);
254+
255+
const headerDate = new Date();
256+
headerDate.setMonth(month);
257+
headerDate.setFullYear(year);
258+
259+
return (
260+
<DatePickerContainer
261+
data-testid="datepicker-calendar-container"
262+
isResponsive={false}
263+
fillWidth={false}
264+
orientation="vertical"
265+
padding="sm"
266+
{...props}
267+
>
268+
<Container
269+
isResponsive={false}
270+
justifyContent="space-between"
271+
orientation="horizontal"
272+
>
273+
<IconButton
274+
icon="chevron-left"
275+
onClick={handlePreviousClick}
276+
size="sm"
277+
type="ghost"
278+
/>
279+
<UnselectableTitle>{headerDateFormatter.format(headerDate)}</UnselectableTitle>
280+
<IconButton
281+
icon="chevron-right"
282+
onClick={handleNextClick}
283+
size="sm"
284+
type="ghost"
285+
/>
286+
</Container>
287+
<DateTable>
288+
<thead>
289+
<tr>
290+
{headers.weekDays.map(({ key, value: date }) => {
291+
return (
292+
<DateTableHeader key={key}>
293+
{weekdayFormatter.format(date)}
294+
</DateTableHeader>
295+
);
296+
})}
297+
</tr>
298+
</thead>
299+
<tbody>{children(body)}</tbody>
300+
</DateTable>
301+
</DatePickerContainer>
302+
);
303+
};

0 commit comments

Comments
 (0)