Skip to content

Commit 1f0e17c

Browse files
authored
Merge pull request #131 from buildo/3692685-implement_datepicker
#3692685: Implement DatePicker
2 parents 2966cb4 + 67c2d59 commit 1f0e17c

34 files changed

+1300
-44
lines changed

packages/bento-design-system/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@
3636
"@farzadsh/floating-ui-*": "NOTE(gabro): we are using a fork of Floating UI due to an issue with the build of the published package. See https://github.com/floating-ui/floating-ui/pull/1529"
3737
},
3838
"dependencies": {
39+
"@datepicker-react/hooks": "^2.8.4",
3940
"@dessert-box/react": "^0.2.0",
4041
"@farzadsh/floating-ui-core": "^0.4.1",
4142
"@farzadsh/floating-ui-dom": "^0.2.1",
4243
"@farzadsh/floating-ui-react-dom": "^0.4.4",
4344
"@fontsource/ibm-plex-sans": "^4.5.7",
45+
"@internationalized/date": "^3.0.0-alpha.4",
4446
"@react-aria/breadcrumbs": "^3.1.5",
4547
"@react-aria/button": "^3.3.4",
4648
"@react-aria/checkbox": "^3.2.3",
@@ -73,10 +75,12 @@
7375
"@react-types/overlays": "^3.5.4",
7476
"@react-types/radio": "^3.1.2",
7577
"@react-types/shared": "^3.11.2",
78+
"@vanilla-extract/dynamic": "^2.0.2",
7679
"@vanilla-extract/recipes": "^0.2.4",
7780
"@vanilla-extract/sprinkles": "^1.4.0",
7881
"clsx": "^1.1.1",
7982
"react-cool-dimensions": "^2.0.7",
83+
"react-input-mask": "^2.0.4",
8084
"react-keyed-flatten-children": "^1.3.0",
8185
"react-select": "^5.2.2",
8286
"react-table": "^7.7.0",
@@ -95,6 +99,7 @@
9599
"@testing-library/user-event": "13.5.0",
96100
"@types/jest": "27.4.1",
97101
"@types/react": "17.0.44",
102+
"@types/react-input-mask": "2.0.5",
98103
"@types/react-table": "7.7.10",
99104
"@vanilla-extract/babel-plugin": "1.1.5",
100105
"@vanilla-extract/css": "1.6.8",

packages/bento-design-system/src/BentoConfig.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { BreadcrumbConfig } from "./Breadcrumb/Config";
66
import { ButtonConfig } from "./Button/Config";
77
import { CardConfig } from "./Card/Config";
88
import { ChipConfig } from "./Chip/Config";
9+
import { DateFieldConfig } from "./DateField/Config";
910
import { DisclosureConfig } from "./Disclosure/Config";
1011
import { DisclosureGroupConfig } from "./DisclosureGroup/Config";
1112
import { DecorativeDividerConfig } from "./Divider/Config";
@@ -46,6 +47,7 @@ export type BentoConfig<
4647
button: ButtonConfig;
4748
card: CardConfig;
4849
chip: ChipConfig<AtomsFn, ChipCustomColor>;
50+
dateField: DateFieldConfig;
4951
decorativeDivider: DecorativeDividerConfig<AtomsFn>;
5052
disclosure: DisclosureConfig;
5153
disclosureGroup: DisclosureGroupConfig;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { MonthType, useMonth } from "@datepicker-react/hooks";
2+
import { useDateFormatter } from "@react-aria/i18n";
3+
import { useOverlay, useOverlayPosition } from "@react-aria/overlays";
4+
import { mergeProps } from "@react-aria/utils";
5+
import { FunctionComponent, RefObject, useRef } from "react";
6+
import { IconButtonProps } from "../IconButton/createIconButton";
7+
import { Box, Stack, Tiles } from "../internal";
8+
import { MenuProps } from "../Menu/createMenu";
9+
import { Label } from "../Typography/Label/Label";
10+
import { Children } from "../util/Children";
11+
import { createPortal } from "../util/createPortal";
12+
import { unsafeLocalizedString } from "../util/LocalizedString";
13+
import { createCalendarHeader } from "./CalendarHeader";
14+
import { DateFieldConfig } from "./Config";
15+
import { calendar, weekDay } from "./DateField.css";
16+
import { createDay } from "./Day";
17+
18+
export type CommonCalendarProps = {
19+
inputRef: RefObject<HTMLInputElement>;
20+
focusedDate: Date | null;
21+
onDateFocus(date: Date): void;
22+
onDateSelect(date: Date): void;
23+
onDateHover(date: Date): void;
24+
isStartDate(date: Date): boolean;
25+
isEndDate(date: Date): boolean;
26+
isDateFocused(date: Date): boolean;
27+
isDateSelected(date: Date): boolean;
28+
isDateHovered(date: Date): boolean;
29+
isDateBlocked(date: Date): boolean;
30+
isFirstOrLastSelectedDate(date: Date): boolean;
31+
};
32+
33+
type Props = CommonCalendarProps & {
34+
type: "single" | "range";
35+
activeDate: MonthType;
36+
goToPreviousMonth: () => void;
37+
goToNextMonth: () => void;
38+
selectActiveDate: (date: Date) => void;
39+
onClose: () => void;
40+
shortcuts?: Children;
41+
};
42+
43+
function boxShadowFromElevation(config: "none" | "small" | "medium" | "large") {
44+
switch (config) {
45+
case "none":
46+
return "none";
47+
case "small":
48+
return "elevationSmall";
49+
case "medium":
50+
return "elevationMedium";
51+
case "large":
52+
return "elevationLarge";
53+
}
54+
}
55+
56+
export function createCalendar(
57+
config: DateFieldConfig,
58+
{
59+
Menu,
60+
IconButton,
61+
}: {
62+
Menu: FunctionComponent<MenuProps>;
63+
IconButton: FunctionComponent<IconButtonProps>;
64+
}
65+
) {
66+
const CalendarHeader = createCalendarHeader(config, { Menu, IconButton });
67+
const Day = createDay(config);
68+
69+
return function Calendar(props: Props) {
70+
const weekdayFormatter = useDateFormatter({
71+
weekday: "narrow",
72+
});
73+
const overlayRef = useRef(null);
74+
75+
const { days, weekdayLabels } = useMonth({
76+
year: props.activeDate.year,
77+
month: props.activeDate.month,
78+
weekdayLabelFormat: (date) => weekdayFormatter.format(date),
79+
});
80+
81+
const { overlayProps } = useOverlay(
82+
{
83+
isOpen: true,
84+
isDismissable: true,
85+
onClose: props.onClose,
86+
},
87+
overlayRef
88+
);
89+
const { overlayProps: positionProps } = useOverlayPosition({
90+
targetRef: props.inputRef,
91+
overlayRef,
92+
placement: "bottom start",
93+
offset: 35,
94+
isOpen: true,
95+
shouldFlip: true,
96+
});
97+
98+
return createPortal(
99+
<Box
100+
className={calendar}
101+
borderRadius={config.radius}
102+
padding={config.padding}
103+
boxShadow={boxShadowFromElevation(config.elevation)}
104+
{...mergeProps(overlayProps, positionProps)}
105+
color={undefined}
106+
ref={overlayRef}
107+
>
108+
<Stack space={16} align="center">
109+
<CalendarHeader {...props} />
110+
<Tiles columns={7} space={0}>
111+
{weekdayLabels.map((d, index) => (
112+
<Box
113+
className={weekDay}
114+
width={config.dayWidth}
115+
height={config.dayHeight}
116+
key={`${d}-${index}`}
117+
>
118+
<Label size={config.dayOfWeekLabelSize}>{unsafeLocalizedString(d)}</Label>
119+
</Box>
120+
))}
121+
{days.map((day, index) => {
122+
if (typeof day === "object") {
123+
return <Day key={day.dayLabel} {...props} date={day.date} label={day.dayLabel} />;
124+
} else {
125+
return <Box key={`empty-${index}`} width={40} height={40} />;
126+
}
127+
})}
128+
</Tiles>
129+
{props.shortcuts}
130+
</Stack>
131+
</Box>
132+
);
133+
};
134+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { MonthType } from "@datepicker-react/hooks";
2+
import { FunctionComponent } from "react";
3+
import { IconButtonProps } from "../IconButton/createIconButton";
4+
import { IconChevronLeft, IconChevronRight } from "../Icons";
5+
import { Box, Column, Columns } from "../internal";
6+
import { MenuProps } from "../Menu/createMenu";
7+
import { useDefaultMessages } from "../util/useDefaultMessages";
8+
import { DateFieldConfig } from "./Config";
9+
import { createSelector } from "./Selector";
10+
11+
type Props = {
12+
activeDate: MonthType;
13+
goToPreviousMonth: () => void;
14+
goToNextMonth: () => void;
15+
selectActiveDate: (date: Date) => void;
16+
};
17+
18+
export function createCalendarHeader(
19+
config: DateFieldConfig,
20+
{
21+
IconButton,
22+
Menu,
23+
}: {
24+
IconButton: FunctionComponent<IconButtonProps>;
25+
Menu: FunctionComponent<MenuProps>;
26+
}
27+
) {
28+
const Selector = createSelector(config, { Menu });
29+
return function CalendarHeader({
30+
goToPreviousMonth,
31+
goToNextMonth,
32+
selectActiveDate,
33+
activeDate,
34+
}: Props) {
35+
const { defaultMessages } = useDefaultMessages();
36+
return (
37+
<Box paddingBottom={16} style={{ paddingLeft: 12, paddingRight: 12 }} width="full">
38+
<Columns space={4} alignY="center">
39+
<Column width="content">
40+
<IconButton
41+
label={defaultMessages.DateField.previousMonthLabel}
42+
size={16}
43+
kind="transparent"
44+
hierarchy="secondary"
45+
icon={IconChevronLeft}
46+
onPress={goToPreviousMonth}
47+
/>
48+
</Column>
49+
<Selector datePart="month" activeMonth={activeDate} onSelect={selectActiveDate} />
50+
<Selector datePart="year" activeMonth={activeDate} onSelect={selectActiveDate} />
51+
<Column width="content">
52+
<IconButton
53+
label={defaultMessages.DateField.nextMonthLabel}
54+
size={16}
55+
kind="transparent"
56+
hierarchy="secondary"
57+
icon={IconChevronRight}
58+
onPress={goToNextMonth}
59+
/>
60+
</Column>
61+
</Columns>
62+
</Box>
63+
);
64+
};
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { IconProps } from "../Icons";
2+
import { BentoSprinkles } from "../internal";
3+
import { BodyProps } from "../Typography/Body/Body";
4+
import { LabelProps } from "../Typography/Label/Label";
5+
import { Children } from "../util/Children";
6+
7+
export type DateFieldConfig = {
8+
radius: BentoSprinkles["borderRadius"];
9+
padding: BentoSprinkles["padding"];
10+
elevation: "none" | "small" | "medium" | "large";
11+
monthYearLabelSize: LabelProps["size"];
12+
dayOfWeekLabelSize: LabelProps["size"];
13+
previousMonthIcon: (props: IconProps) => Children;
14+
nextMonthIcon: (props: IconProps) => Children;
15+
monthYearSelectIcons: {
16+
open: (props: IconProps) => Children;
17+
close: (props: IconProps) => Children;
18+
};
19+
dayWidth: BentoSprinkles["width"];
20+
dayHeight: BentoSprinkles["height"];
21+
dayRadius: BentoSprinkles["borderRadius"];
22+
daySize: BodyProps["size"];
23+
};

0 commit comments

Comments
 (0)