Skip to content

Commit 29a5a3e

Browse files
authored
Merge pull request #988 from buildo/date-input
Expose `DateInput` component
2 parents 4df6883 + 2441cef commit 29a5a3e

File tree

14 files changed

+610
-177
lines changed

14 files changed

+610
-177
lines changed

packages/bento-design-system/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@
127127
"react-select": "5.7.4",
128128
"react-table": "7.8.0",
129129
"recharts": "2.8.0",
130-
"ts-pattern": "3.3.5"
130+
"ts-pattern": "5.7.0"
131131
},
132132
"devDependencies": {
133133
"@babel/core": "7.22.20",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { DatePickerAria, DateRangePickerAria } from "@react-aria/datepicker";
2+
import { DatePickerState, DateRangePickerState } from "@react-stately/datepicker";
3+
import React from "react";
4+
import { match } from "ts-pattern";
5+
import { Input } from "./Input";
6+
import { Calendar } from "./Calendar";
7+
import { Box } from "../Box/Box";
8+
import { Inline } from "../Layout/Inline";
9+
import { Button, FieldProps } from "..";
10+
import { SingleDateProps, RangeDateProps } from "./types";
11+
12+
type BaseSingleDateProps = SingleDateProps & {
13+
datePickerAria: DatePickerAria;
14+
datePickerState: DatePickerState;
15+
} & Pick<FieldProps<Date | null>, "onChange" | "value">;
16+
17+
type BaseRangeDateProps = RangeDateProps & {
18+
dateRangePickerAria: DateRangePickerAria;
19+
dateRangePickerState: DateRangePickerState;
20+
} & Pick<FieldProps<[Date, Date] | null>, "onChange" | "value">;
21+
22+
type Props = (BaseSingleDateProps | BaseRangeDateProps) & {
23+
inputRef: React.RefObject<HTMLInputElement>;
24+
};
25+
26+
function BaseSingleDateInput(props: Extract<Props, { type?: "single" }>) {
27+
const shortcuts = props.shortcuts && (
28+
<Inline space={4}>
29+
{props.shortcuts.map((shortcut) => (
30+
<Button
31+
key={shortcut.label}
32+
kind="transparent"
33+
hierarchy="secondary"
34+
size="small"
35+
label={shortcut.label}
36+
onPress={() => {
37+
props.onChange(shortcut.value);
38+
props.datePickerState.close();
39+
}}
40+
/>
41+
))}
42+
</Inline>
43+
);
44+
45+
return (
46+
<>
47+
<Box {...props.datePickerAria.groupProps} ref={props.inputRef}>
48+
<Input
49+
type="single"
50+
fieldProps={props.datePickerAria.fieldProps}
51+
buttonProps={props.datePickerAria.buttonProps}
52+
isCalendarOpen={props.datePickerState.isOpen}
53+
/>
54+
</Box>
55+
{props.datePickerState.isOpen && (
56+
<Calendar
57+
type="single"
58+
{...props.datePickerAria.calendarProps}
59+
inputRef={props.inputRef}
60+
onClose={props.datePickerState.close}
61+
shortcuts={shortcuts}
62+
/>
63+
)}
64+
</>
65+
);
66+
}
67+
68+
function BaseRangeDateInput(props: Extract<Props, { type: "range" }>) {
69+
const shortcuts = props.shortcuts && (
70+
<Inline space={4}>
71+
{props.shortcuts.map((shortcut) => (
72+
<Button
73+
key={shortcut.label}
74+
kind="transparent"
75+
hierarchy="secondary"
76+
size="small"
77+
label={shortcut.label}
78+
onPress={() => {
79+
props.onChange(shortcut.value);
80+
props.dateRangePickerState.close();
81+
}}
82+
/>
83+
))}
84+
</Inline>
85+
);
86+
87+
return (
88+
<>
89+
<Box {...props.dateRangePickerAria.groupProps} ref={props.inputRef}>
90+
<Input
91+
type="range"
92+
fieldProps={{
93+
start: props.dateRangePickerAria.startFieldProps,
94+
end: props.dateRangePickerAria.endFieldProps,
95+
}}
96+
buttonProps={props.dateRangePickerAria.buttonProps}
97+
isCalendarOpen={props.dateRangePickerState.isOpen}
98+
/>
99+
</Box>
100+
{props.dateRangePickerState.isOpen && (
101+
<Calendar
102+
type="range"
103+
{...props.dateRangePickerAria.calendarProps}
104+
inputRef={props.inputRef}
105+
onClose={props.dateRangePickerState.close}
106+
shortcuts={shortcuts}
107+
/>
108+
)}
109+
</>
110+
);
111+
}
112+
113+
export function BaseDateInput(props: Props) {
114+
return match(props)
115+
.with({ type: "single" }, { type: undefined }, (props) => <BaseSingleDateInput {...props} />)
116+
.with({ type: "range" }, (props) => <BaseRangeDateInput {...props} />)
117+
.exhaustive();
118+
}
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,71 @@
11
import { useDatePicker, useDateRangePicker } from "@react-aria/datepicker";
22
import { useDatePickerState, useDateRangePickerState } from "@react-stately/datepicker";
33
import { useRef } from "react";
4+
import { match } from "ts-pattern";
45
import { FieldProps } from "../Field/FieldProps";
56
import { CalendarDate, DateValue, getLocalTimeZone } from "@internationalized/date";
6-
import { Input } from "./Input";
7-
import { Calendar } from "./Calendar";
8-
import { Box } from "../Box/Box";
7+
import { BaseDateInput } from "./BaseDateInput";
98
import { Field } from "../Field/Field";
10-
import { LocalizedString } from "../util/ConfigurableTypes";
119
import { RangeValue } from "@react-types/shared";
12-
import { Inline } from "../Layout/Inline";
13-
import { Button } from "..";
10+
import { DateProps, SingleDateProps, RangeDateProps } from "./types";
11+
import { dateToCalendarDate } from "./utils";
1412

15-
export type ShortcutProps<Value> = {
16-
label: LocalizedString;
17-
value: Value;
18-
};
19-
type SingleDateFieldProps = {
20-
type?: "single";
21-
shortcuts?: ShortcutProps<Date | null>[];
22-
} & FieldProps<Date | null>;
23-
type RangeDateFieldProps = {
24-
type: "range";
25-
shortcuts?: ShortcutProps<[Date, Date] | null>[];
26-
} & FieldProps<[Date, Date] | null>;
27-
type Props = (SingleDateFieldProps | RangeDateFieldProps) & {
28-
minDate?: Date;
29-
maxDate?: Date;
30-
shouldDisableDate?: (date: Date) => boolean;
31-
readOnly?: boolean;
32-
};
13+
type SingleDateFieldProps = SingleDateProps & FieldProps<Date | null> & DateProps;
14+
type RangeDateFieldProps = RangeDateProps & FieldProps<[Date, Date] | null> & DateProps;
3315

34-
function dateToCalendarDate(date: Date): CalendarDate {
35-
return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());
36-
}
37-
38-
function SingleDateField({ disabled, readOnly, ...props }: Extract<Props, { type?: "single" }>) {
16+
function SingleDateField(props: SingleDateFieldProps) {
3917
const localTimeZone = getLocalTimeZone();
18+
const ref = useRef(null);
4019

4120
const internalProps = {
4221
...props,
4322
value: props.value ? dateToCalendarDate(props.value) : props.value,
4423
onChange: (date: CalendarDate | null) => {
4524
props.onChange(date?.toDate(localTimeZone) ?? null);
4625
},
47-
isDisabled: disabled,
48-
isReadOnly: readOnly,
26+
isDisabled: props.disabled,
27+
isReadOnly: props.isReadOnly ?? props.readOnly,
4928
validationState: props.issues ? "invalid" : "valid",
5029
minValue: props.minDate ? dateToCalendarDate(props.minDate) : undefined,
5130
maxValue: props.maxDate ? dateToCalendarDate(props.maxDate) : undefined,
5231
isDateUnavailable: props.shouldDisableDate
5332
? (date: DateValue) => props.shouldDisableDate!(date.toDate(localTimeZone))
5433
: undefined,
5534
shouldForceLeadingZeros: true,
35+
onBlur: props.onBlur,
36+
autoFocus: props.autoFocus,
5637
} as const;
57-
const state = useDatePickerState(internalProps);
58-
const ref = useRef(null);
59-
const {
60-
groupProps,
61-
labelProps,
62-
fieldProps,
63-
buttonProps,
64-
descriptionProps,
65-
errorMessageProps,
66-
calendarProps,
67-
} = useDatePicker(internalProps, state, ref);
6838

69-
const shortcuts = props.shortcuts && (
70-
<Inline space={4}>
71-
{props.shortcuts.map((shortcut) => (
72-
<Button
73-
key={shortcut.label}
74-
kind="transparent"
75-
hierarchy="secondary"
76-
size="small"
77-
label={shortcut.label}
78-
onPress={() => {
79-
props.onChange(shortcut.value);
80-
state.close();
81-
}}
82-
/>
83-
))}
84-
</Inline>
85-
);
39+
const datePickerState = useDatePickerState(internalProps);
40+
const datePickerAria = useDatePicker(internalProps, datePickerState, ref);
8641

8742
return (
8843
<Field
8944
{...props}
90-
disabled={disabled}
91-
labelProps={labelProps}
92-
assistiveTextProps={descriptionProps}
93-
errorMessageProps={errorMessageProps}
45+
disabled={props.disabled}
46+
labelProps={datePickerAria.labelProps}
47+
assistiveTextProps={datePickerAria.descriptionProps}
48+
errorMessageProps={datePickerAria.errorMessageProps}
9449
>
95-
<Box {...groupProps} ref={ref}>
96-
<Input
97-
type="single"
98-
fieldProps={fieldProps}
99-
buttonProps={buttonProps}
100-
isCalendarOpen={state.isOpen}
101-
/>
102-
</Box>
103-
{state.isOpen && (
104-
<Calendar
105-
type="single"
106-
{...calendarProps}
107-
inputRef={ref}
108-
onClose={state.close}
109-
shortcuts={shortcuts}
110-
/>
111-
)}
50+
<BaseDateInput
51+
type="single"
52+
value={props.value}
53+
onChange={props.onChange}
54+
shortcuts={props.shortcuts}
55+
datePickerAria={datePickerAria}
56+
datePickerState={datePickerState}
57+
inputRef={ref}
58+
/>
11259
</Field>
11360
);
11461
}
11562

116-
function RangeDateField({ disabled, readOnly, ...props }: Extract<Props, { type: "range" }>) {
63+
function RangeDateField(props: RangeDateFieldProps) {
11764
const localTimeZone = getLocalTimeZone();
65+
const ref = useRef(null);
66+
11867
const internalProps = {
11968
...props,
120-
isDisabled: disabled,
121-
isReadOnly: readOnly,
122-
validationState: props.issues ? "invalid" : "valid",
123-
minValue: props.minDate ? dateToCalendarDate(props.minDate) : undefined,
124-
maxValue: props.maxDate ? dateToCalendarDate(props.maxDate) : undefined,
125-
isDateUnavailable: props.shouldDisableDate
126-
? (date: DateValue) => props.shouldDisableDate!(date.toDate(localTimeZone))
127-
: undefined,
12869
value: props.value
12970
? {
13071
start: dateToCalendarDate(props.value[0]),
@@ -138,70 +79,52 @@ function RangeDateField({ disabled, readOnly, ...props }: Extract<Props, { type:
13879
props.onChange([range.start.toDate(localTimeZone), range.end.toDate(localTimeZone)]);
13980
}
14081
},
82+
isDisabled: props.disabled,
83+
isReadOnly: props.isReadOnly ?? props.readOnly,
84+
validationState: props.issues ? "invalid" : "valid",
85+
minValue: props.minDate ? dateToCalendarDate(props.minDate) : undefined,
86+
maxValue: props.maxDate ? dateToCalendarDate(props.maxDate) : undefined,
87+
isDateUnavailable: props.shouldDisableDate
88+
? (date: DateValue) => props.shouldDisableDate!(date.toDate(localTimeZone))
89+
: undefined,
14190
shouldForceLeadingZeros: true,
91+
onBlur: props.onBlur,
92+
autoFocus: props.autoFocus,
14293
} as const;
143-
const state = useDateRangePickerState(internalProps);
144-
const ref = useRef(null);
145-
const {
146-
groupProps,
147-
labelProps,
148-
buttonProps,
149-
descriptionProps,
150-
errorMessageProps,
151-
calendarProps,
152-
startFieldProps,
153-
endFieldProps,
154-
} = useDateRangePicker(internalProps, state, ref);
15594

156-
const shortcuts = props.shortcuts && (
157-
<Inline space={4}>
158-
{props.shortcuts.map((shortcut) => (
159-
<Button
160-
key={shortcut.label}
161-
kind="transparent"
162-
hierarchy="secondary"
163-
size="small"
164-
label={shortcut.label}
165-
onPress={() => {
166-
props.onChange(shortcut.value);
167-
state.close();
168-
}}
169-
/>
170-
))}
171-
</Inline>
172-
);
95+
const rangeDatePickerState = useDateRangePickerState(internalProps);
96+
const rangeDatePickerAria = useDateRangePicker(internalProps, rangeDatePickerState, ref);
17397

17498
return (
17599
<Field
176100
{...props}
177-
disabled={disabled}
178-
labelProps={labelProps}
179-
assistiveTextProps={descriptionProps}
180-
errorMessageProps={errorMessageProps}
101+
disabled={props.disabled}
102+
labelProps={rangeDatePickerAria.labelProps}
103+
assistiveTextProps={rangeDatePickerAria.descriptionProps}
104+
errorMessageProps={rangeDatePickerAria.errorMessageProps}
181105
>
182-
<Box {...groupProps} ref={ref}>
183-
<Input
184-
type="range"
185-
fieldProps={{ start: startFieldProps, end: endFieldProps }}
186-
buttonProps={buttonProps}
187-
isCalendarOpen={state.isOpen}
188-
/>
189-
</Box>
190-
{state.isOpen && (
191-
<Calendar
192-
type="range"
193-
{...calendarProps}
194-
inputRef={ref}
195-
onClose={state.close}
196-
shortcuts={shortcuts}
197-
/>
198-
)}
106+
<BaseDateInput
107+
type="range"
108+
value={props.value}
109+
onChange={props.onChange}
110+
shortcuts={props.shortcuts}
111+
dateRangePickerAria={rangeDatePickerAria}
112+
dateRangePickerState={rangeDatePickerState}
113+
inputRef={ref}
114+
/>
199115
</Field>
200116
);
201117
}
202118

203-
export function DateField(props: Props) {
204-
return props.type === "range" ? <RangeDateField {...props} /> : <SingleDateField {...props} />;
205-
}
119+
export type DateFieldProps = SingleDateFieldProps | RangeDateFieldProps;
206120

207-
export type { Props as DateFieldProps };
121+
export function DateField(props: DateFieldProps) {
122+
// Note: checking this case in the pattern matching below doesn't work for some reason
123+
if (props.type == null) {
124+
return <SingleDateField {...props} />;
125+
}
126+
return match(props)
127+
.with({ type: "single" }, (props) => <SingleDateField {...props} />)
128+
.with({ type: "range" }, (props) => <RangeDateField {...props} />)
129+
.exhaustive();
130+
}

0 commit comments

Comments
 (0)