Skip to content

Commit

Permalink
Merge pull request #696 from buildo/datepicker-react-aria
Browse files Browse the repository at this point in the history
Reimplement Datepicker with react-aria
  • Loading branch information
veej authored Sep 27, 2023
2 parents f1d055a + 83b69d9 commit 4fe1bbd
Show file tree
Hide file tree
Showing 15 changed files with 775 additions and 601 deletions.
3 changes: 3 additions & 0 deletions packages/bento-design-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"@internationalized/date": "3.4.0",
"@react-aria/breadcrumbs": "3.5.4",
"@react-aria/button": "3.8.1",
"@react-aria/calendar": "3.4.0",
"@react-aria/checkbox": "3.10.0",
"@react-aria/datepicker": "3.6.0",
"@react-aria/dialog": "3.5.4",
Expand All @@ -96,6 +97,7 @@
"@react-aria/tooltip": "3.6.1",
"@react-aria/utils": "3.19.0",
"@react-aria/visually-hidden": "3.8.3",
"@react-stately/calendar": "3.3.0",
"@react-stately/checkbox": "3.4.4",
"@react-stately/datepicker": "3.6.0",
"@react-stately/menu": "3.5.4",
Expand All @@ -105,6 +107,7 @@
"@react-stately/slider": "3.4.1",
"@react-stately/toggle": "3.6.1",
"@react-stately/tooltip": "3.4.3",
"@react-types/calendar": "3.3.0",
"@react-types/overlays": "3.8.1",
"@react-types/radio": "3.5.0",
"@react-types/shared": "3.19.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/bento-design-system/src/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { AriaButtonProps } from "@react-types/button";
import { useButton } from "@react-aria/button";
import { Label } from "../Typography/Label/Label";
import { Column, Columns } from "../Layout/Columns";
import { IconProps } from "..";
import { Children, IconProps } from "..";
import { useBentoConfig } from "../BentoConfigContext";
import pick from "lodash.pick";
import { getRadiusPropsFromConfig } from "../util/BorderRadiusConfig";
Expand All @@ -31,7 +31,7 @@ type Props = {
hierarchy: "primary" | "secondary" | "danger";
isDisabled?: boolean;
size?: ButtonSize;
icon?: (props: IconProps) => JSX.Element;
icon?: (props: IconProps) => Children;
iconPosition?: "leading" | "trailing";
} & Omit<AriaButtonProps<"button">, "onPress"> &
Pick<React.HTMLProps<HTMLButtonElement>, OtherButtonKeys>;
Expand Down
240 changes: 169 additions & 71 deletions packages/bento-design-system/src/DateField/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,38 @@
import { MonthType, useMonth } from "@datepicker-react/hooks";
import { useDateFormatter } from "@react-aria/i18n";
import { useOverlay, useOverlayPosition } from "@react-aria/overlays";
import { mergeProps } from "@react-aria/utils";
import { RefObject, useRef } from "react";
import { Box, Stack, Tiles } from "..";
import { Label } from "../Typography/Label/Label";
import { Children } from "../util/Children";
import { useCreatePortal } from "../util/useCreatePortal";
import { CalendarHeader } from "./CalendarHeader";
import { calendar, weekDay } from "./DateField.css";
import { useCalendar, useCalendarGrid, useRangeCalendar } from "@react-aria/calendar";
import {
CalendarState,
RangeCalendarState,
useCalendarState,
useRangeCalendarState,
} from "@react-stately/calendar";
import { createCalendar, getWeeksInMonth } from "@internationalized/date";
import { Day } from "./Day";
import { useLocale } from "@react-aria/i18n";
import { Box } from "../Box/Box";
import { calendar, calendarGrid, weekDay } from "../DateField/DateField.css";
import { useBentoConfig } from "../BentoConfigContext";
import { Children, Label, Stack } from "..";
import { getRadiusPropsFromConfig } from "../util/BorderRadiusConfig";
import { CalendarHeader } from "./CalendarHeader";
import { useCreatePortal } from "../util/useCreatePortal";
import { useOverlay, useOverlayPosition } from "@react-aria/overlays";
import { DOMAttributes, useRef } from "react";
import { mergeProps } from "@react-aria/utils";
import { AriaCalendarProps, AriaRangeCalendarProps, DateValue } from "@react-types/calendar";
import { AriaButtonProps } from "@react-types/button";
import { FocusableElement } from "@react-types/shared";

export type CommonCalendarProps = {
inputRef: RefObject<HTMLInputElement>;
focusedDate: Date | null;
onDateFocus(date: Date): void;
onDateSelect(date: Date): void;
onDateHover(date: Date): void;
isStartDate(date: Date): boolean;
isEndDate(date: Date): boolean;
isDateFocused(date: Date): boolean;
isDateSelected(date: Date): boolean;
isDateHovered(date: Date): boolean;
isDateBlocked(date: Date): boolean;
isFirstOrLastSelectedDate(date: Date): boolean;
};

type Props = CommonCalendarProps & {
type: "single" | "range";
activeDate: MonthType;
goToPreviousMonth: () => void;
goToNextMonth: () => void;
selectActiveDate: (date: Date) => void;
type Props = (
| ({
type: "single";
} & AriaCalendarProps<DateValue>)
| ({
type: "range";
} & AriaRangeCalendarProps<DateValue>)
) & {
onClose: () => void;
shortcuts?: Children;
minDate?: Date;
maxDate?: Date;
inputRef: React.RefObject<HTMLInputElement>;
shortcuts: Children;
};

function boxShadowFromElevation(config: "none" | "small" | "medium" | "large") {
Expand All @@ -53,71 +48,174 @@ function boxShadowFromElevation(config: "none" | "small" | "medium" | "large") {
}
}

export function Calendar(props: Props) {
function CalendarGrid(
props:
| {
type: "single";
state: CalendarState;
}
| { type: "range"; state: RangeCalendarState }
) {
const { locale } = useLocale();
const { gridProps, weekDays } = useCalendarGrid({}, props.state);
const config = useBentoConfig().dateField;
const weekdayFormatter = useDateFormatter({
weekday: "narrow",
});
const overlayRef = useRef(null);
const createPortal = useCreatePortal();

const { days, weekdayLabels } = useMonth({
year: props.activeDate.year,
month: props.activeDate.month,
weekdayLabelFormat: (date) => weekdayFormatter.format(date),
});
const weeksInMonth = getWeeksInMonth(props.state.visibleRange.start, locale);

return (
<Box className={calendarGrid} {...gridProps}>
{weekDays.map((day, index) => (
<Box
key={index}
className={weekDay}
style={{ width: config.dayWidth, height: config.dayHeight }}
>
<Label color="secondary" size={config.dayOfWeekLabelSize}>
{day}
</Label>
</Box>
))}
{[...new Array(weeksInMonth).keys()].map((weekIndex) =>
props.state
.getDatesInWeek(weekIndex)
.map((date, i) => (date ? <Day {...props} key={i} date={date} /> : <td key={i} />))
)}
</Box>
);
}

export function CalendarPopover(
props: (
| { type: "single"; state: CalendarState }
| { type: "range"; state: RangeCalendarState }
) & {
prevButtonProps: AriaButtonProps<"button">;
nextButtonProps: AriaButtonProps<"button">;
onClose: () => void;
calendarRef: React.RefObject<HTMLElement>;
inputRef: React.RefObject<HTMLInputElement>;
state: CalendarState | RangeCalendarState;
shortcuts?: Children;
} & DOMAttributes<FocusableElement>
) {
const {
prevButtonProps,
nextButtonProps,
onClose,
inputRef,
state,
calendarRef,
...calendarProps
} = props;
const config = useBentoConfig().dateField;

const createPortal = useCreatePortal();

const { overlayProps } = useOverlay(
{
isOpen: true,
isDismissable: true,
onClose: props.onClose,
},
overlayRef
calendarRef
);
const { overlayProps: positionProps } = useOverlayPosition({
targetRef: props.inputRef,
overlayRef,
overlayRef: calendarRef,
placement: "bottom start",
offset: 35,
isOpen: true,
shouldFlip: true,
});

const gridProps =
props.type === "single"
? {
type: "single" as const,
state: state as CalendarState,
}
: { type: "range" as const, state: state as RangeCalendarState };

return createPortal(
<Box
className={calendar}
{...getRadiusPropsFromConfig(config.radius)}
padding={config.padding}
boxShadow={boxShadowFromElevation(config.elevation)}
{...calendarProps}
{...mergeProps(overlayProps, positionProps)}
ref={overlayRef}
ref={calendarRef}
>
<Stack space={16} align="center">
<CalendarHeader {...props} />
<Tiles columns={7} space={0}>
{weekdayLabels.map((d, index) => (
<Box
className={weekDay}
width={config.dayWidth}
height={config.dayHeight}
key={`${d}-${index}`}
>
<Label size={config.dayOfWeekLabelSize}>{d}</Label>
</Box>
))}
{days.map((day, index) => {
if (typeof day === "object") {
return <Day key={day.dayLabel} {...props} date={day.date} label={day.dayLabel} />;
} else {
return (
<Box key={`empty-${index}`} width={config.dayWidth} height={config.dayHeight} />
);
}
})}
</Tiles>
<CalendarHeader
prevButtonProps={prevButtonProps}
nextButtonProps={nextButtonProps}
focusedDate={state.focusedDate}
onChange={state.setFocusedDate}
minDate={state.minValue}
maxDate={state.maxValue}
/>
<CalendarGrid {...gridProps} />
{props.shortcuts && <Box style={{ maxWidth: config.dayWidth * 7 }}>{props.shortcuts}</Box>}
</Stack>
</Box>
);
}

function SingleCalendar(props: Extract<Props, { type: "single" }>) {
const { locale } = useLocale();
const state = useCalendarState({
...props,
locale,
createCalendar,
});
const { calendarProps, prevButtonProps, nextButtonProps } = useCalendar(props, state);
const ref = useRef(null);

return (
<CalendarPopover
type="single"
{...calendarProps}
prevButtonProps={prevButtonProps}
nextButtonProps={nextButtonProps}
state={state}
onClose={props.onClose}
inputRef={props.inputRef}
calendarRef={ref}
shortcuts={props.shortcuts}
/>
);
}

function RangeCalendar(props: Extract<Props, { type: "range" }>) {
const { locale } = useLocale();
const state = useRangeCalendarState({
...props,
locale,
createCalendar,
});
const ref = useRef(null);
const { calendarProps, prevButtonProps, nextButtonProps } = useRangeCalendar(props, state, ref);

return (
<CalendarPopover
type="range"
{...calendarProps}
prevButtonProps={prevButtonProps}
nextButtonProps={nextButtonProps}
state={state}
onClose={props.onClose}
inputRef={props.inputRef}
calendarRef={ref}
shortcuts={props.shortcuts}
/>
);
}

export function Calendar(props: Props) {
if (props.type === "single") {
return <SingleCalendar {...props} />;
} else {
return <RangeCalendar {...props} />;
}
}
Loading

0 comments on commit 4fe1bbd

Please sign in to comment.