Skip to content

Commit

Permalink
refactor: upgrade react-day-picker to v9 (#398)
Browse files Browse the repository at this point in the history
* feat: upgrade react-day-picker to v9

* refactor: make the changes v9 migration changes

* fix: styling regressions with the v9 changes
  • Loading branch information
SeanCassiere authored Aug 1, 2024
1 parent b91e493 commit 639d0c5
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 125 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"oidc-client-ts": "^3.0.1",
"react": "^18.3.1",
"react-compiler-runtime": "file:./libs/react-compiler-runtime",
"react-day-picker": "^8.10.1",
"react-day-picker": "^9.0.6",
"react-dom": "^18.3.1",
"react-hook-form": "^7.52.1",
"react-i18next": "^15.0.0",
Expand Down
13 changes: 6 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

234 changes: 122 additions & 112 deletions src/components/ui/calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import * as React from "react";
import {
DayPicker,
useDayPicker,
useNavigation,
type CaptionLayout,
type DropdownProps,
type Matcher,
} from "react-day-picker";

import { buttonVariants } from "@/components/ui/button";
Expand All @@ -17,111 +16,118 @@ import {
SelectTrigger,
} from "@/components/ui/select";

import { setMonth } from "@/lib/config/date-fns";
import { addMonths } from "@/lib/config/date-fns";

import { cn } from "@/lib/utils";

export type CalendarProps = React.ComponentProps<typeof DayPicker>;

const DEFAULT_FROM_YEAR = 1900;

function Calendar({
className,
classNames,
showOutsideDays = true,
fromYear,
toYear,
startMonth,
endMonth,
hidden,
captionLayout,
...props
}: CalendarProps) {
const layout: CaptionLayout = captionLayout || "dropdown-buttons";
const layout: CalendarProps["captionLayout"] = captionLayout || "dropdown";

const calendarStartMonth = startMonth ?? new Date(1900, 0, 0);
const calendarEndMonth =
endMonth ?? new Date(new Date().getFullYear() + 15, 11);

const currentYear = new Date().getFullYear();
const hiddenIsArray = Array.isArray(hidden);

const oldestYear = fromYear ?? DEFAULT_FROM_YEAR;
const newestYear = toYear ?? currentYear + 15;
const calendarHiddenOptions: Matcher[] = [
...(startMonth ? [{ before: startMonth }] : []),
...(endMonth ? [{ after: endMonth }] : []),
...(hiddenIsArray ? hidden : []),
hidden && !hiddenIsArray ? hidden : [],
];

return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
className={cn("inline-flex justify-start", className)}
captionLayout={layout}
fromYear={oldestYear}
toYear={newestYear}
startMonth={calendarStartMonth}
endMonth={calendarEndMonth}
hidden={calendarHiddenOptions}
classNames={{
months: cn(
"flex flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0"
),
month: "space-y-4",
caption: cn(
"relative",
layout === "buttons" && "flex items-center justify-between"
months: "relative",
month: cn(layout === "label" ? "space-y-4" : "space-y-2"),
month_caption: cn(
layout === "label" ? "translate-y-1" : "",
layout === "dropdown" && "flex w-full justify-center"
),
caption_label: cn(
"font-medium",
layout === "dropdown" && "hidden",
layout === "dropdown-buttons" && "hidden"
),
caption_dropdowns: cn(
"flex items-center justify-center",
layout === "dropdown" && "-translate-x-1 gap-1.5",
layout === "dropdown-buttons" && "-translate-x-0.5 gap-1"
"pl-2 font-medium",
layout === "dropdown" && "hidden"
),
dropdowns: cn(
layout === "dropdown"
? "flex items-center justify-center gap-1.5"
: "",
layout === "dropdown-years" || layout === "dropdown-months"
? "flex w-8/12 items-center justify-between [&>button]:translate-x-3 [&>span]:pl-2 [&>span]:font-medium"
: "",
layout === "dropdown-months" ? "flex-row-reverse" : ""
),
nav: cn(
layout === "buttons" && "flex items-center justify-between gap-1"
layout === "label" ||
layout === "dropdown-years" ||
layout === "dropdown-months"
? "absolute right-1 top-0 flex gap-1.5"
: ""
),
nav_button: cn(
button_previous: cn(
buttonVariants({ variant: "outline" }),
"h-8 w-8 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: cn(
layout === "dropdown-buttons" && "absolute left-1 top-0"
),
nav_button_next: cn(
layout === "dropdown-buttons" && "absolute right-1 top-0"
),
dropdown_month: cn("h-8", layout === "dropdown-buttons" && "w-20"),
dropdown_year: cn("h-8", layout === "dropdown-buttons" && "w-20"),
table: cn("w-full border-collapse space-y-1"),
head_row: "flex",
head_cell: cn(
"w-9 select-none rounded-md text-[0.8rem] font-normal text-muted-foreground"
),
row: cn("mt-2 flex w-full"),
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md"
"size-8 bg-transparent p-0 opacity-50 hover:opacity-100",
layout === "dropdown" && "absolute left-1 top-0"
),
button_next: cn(
buttonVariants({ variant: "outline" }),
"size-8 bg-transparent p-0 opacity-50 hover:opacity-100",
layout === "dropdown" && "absolute right-1 top-0"
),
table: "w-full border-collapse",
weekdays: "flex",
weekday:
"m-0.5 h-7 w-9 select-none rounded-md text-[0.8rem] font-normal text-muted-foreground",
row: "mt-2 flex w-full",
cell: "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
),
day_selected: cn(
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground"
),
day_today: cn("bg-accent text-accent-foreground"),
day_outside: cn(
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30"
),
day_disabled: cn("text-muted-foreground opacity-50"),
day_range_middle: cn(
"aria-selected:bg-accent aria-selected:text-accent-foreground"
),
day_hidden: "invisible",
"m-0.5 size-9 p-0 font-normal aria-selected:opacity-100 [&>button]:h-full [&>button]:w-full"
),
selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
today: "bg-accent text-accent-foreground",
outside:
"text-muted-foreground opacity-50 transition-all aria-selected:bg-primary aria-selected:text-primary-foreground aria-selected:opacity-90 aria-selected:hover:opacity-100",
disabled: "text-muted-foreground opacity-50",
range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
hidden: "invisible",
...classNames,
}}
components={{
IconLeft: (props) => (
<icons.ChevronLeft
style={props.style}
className={cn("h-5 w-5", props.className)}
/>
),
IconRight: (props) => (
<icons.ChevronRight
style={props.style}
className={cn("h-5 w-5", props.className)}
/>
),
Chevron(props) {
switch (props.orientation) {
case "left":
return (
<icons.ChevronLeft className={cn("size-5", props.className)} />
);
case "right":
return (
<icons.ChevronRight className={cn("size-5", props.className)} />
);
default:
return <span>·</span>;
}
},
Dropdown: CalendarDropdown,
}}
{...props}
Expand All @@ -130,37 +136,26 @@ function Calendar({
}
Calendar.displayName = "Calendar";

const numberRegex = /^\d+$/;

function CalendarDropdown(props: DropdownProps) {
const { id, fromYear, fromMonth, fromDate, toYear, toMonth, toDate } =
useDayPicker();
const { currentMonth, goToMonth } = useNavigation();

const months = Array.from({ length: 12 }, (_, i) => {
const value = i.toString();
const label = setMonth(new Date(), i).toLocaleString("default", {
month: "long",
});
return { value, label };
});

const years = React.useMemo(() => {
const items: { value: string; label: string }[] = [];

const oldestYear =
fromYear || fromMonth?.getFullYear() || fromDate?.getFullYear();
const newestYear =
toYear || toMonth?.getFullYear() || toDate?.getFullYear();

if (oldestYear && newestYear) {
const range = newestYear - oldestYear + 1;
for (let i = 0; i < range; i++) {
const value = (oldestYear + i).toString();
items.push({ value, label: value });
}
const dropdownType: "unknown" | "months" | "years" = React.useMemo(() => {
const option = props.options?.[0];
if (!option) {
return "unknown";
}
const label = option.label;

if (!numberRegex.test(label)) {
return "months";
}

return items;
}, [fromYear, fromMonth, fromDate, toYear, toMonth, toDate]);
return "years";
}, [props.options]);
const { goToMonth, previousMonth } = useDayPicker();

const currentMonth = addMonths(previousMonth ?? new Date(), 1);
const options = props.options ?? [];

const handleMonthChange = (value: string) => {
const date = new Date(currentMonth);
Expand All @@ -174,16 +169,23 @@ function CalendarDropdown(props: DropdownProps) {
goToMonth(date);
};

if (props.name === "months") {
if (dropdownType === "months") {
return (
<Select onValueChange={handleMonthChange} value={props.value?.toString()}>
<SelectTrigger className={props.className} style={props.style}>
<SelectTrigger
className={cn("m-0 h-8 w-24", props.className)}
style={props.style}
>
{currentMonth.toLocaleString("default", { month: "long" })}
</SelectTrigger>
<SelectContent>
<ScrollArea className="overflow-auto" style={{ maxHeight: "400px" }}>
{months.map(({ value, label }) => (
<SelectItem key={`${id}_month_${value}`} value={value}>
{options.map(({ value, label, disabled }) => (
<SelectItem
key={`${"id"}_month_${value}`}
value={value.toString()}
disabled={disabled}
>
{label}
</SelectItem>
))}
Expand All @@ -193,16 +195,23 @@ function CalendarDropdown(props: DropdownProps) {
);
}

if (props.name === "years") {
if (dropdownType === "years") {
return (
<Select onValueChange={handleYearChange} value={props.value?.toString()}>
<SelectTrigger className={props.className} style={props.style}>
<SelectTrigger
className={cn("m-0 h-8 w-24", props.className)}
style={props.style}
>
{currentMonth.getFullYear()}
</SelectTrigger>
<SelectContent>
<ScrollArea className="overflow-auto" style={{ maxHeight: "400px" }}>
{years.map(({ value, label }) => (
<SelectItem key={`${id}_year_${value}`} value={value}>
{options.map(({ value, label, disabled }) => (
<SelectItem
key={`${"id"}_year_${value}`}
value={value.toString()}
disabled={disabled}
>
{label}
</SelectItem>
))}
Expand All @@ -212,8 +221,9 @@ function CalendarDropdown(props: DropdownProps) {
);
}

return null;
return <div>dropdown</div>;
}

CalendarDropdown.displayName = "CalendarDropdown";

export { Calendar };
6 changes: 3 additions & 3 deletions src/components/ui/input-datepicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,13 @@ function InputDatePicker({
{children}
<PopoverContent
align={align}
className="max-w-[300px] px-0 pb-1.5 pt-1"
className="w-full max-w-[400px] px-0 pb-1.5 pt-1"
>
{mode === "date" || mode === "datetime" ? (
<Calendar
mode="single"
initialFocus
className="pb-1"
autoFocus
className="p-3"
selected={value}
onSelect={(date) => {
if (!onChange) return;
Expand Down
Loading

0 comments on commit 639d0c5

Please sign in to comment.