Skip to content

Commit d09205e

Browse files
feat: upgrade to latest shadcn/ui Calendar component with advanced features
- Used official shadcn CLI to get the latest Calendar component - Upgraded to new react-day-picker API with DayButton and getDefaultClassNames - Added CalendarDayButton component with advanced styling and focus management - Includes RTL support, dropdown navigation, and improved accessibility - Fixed import paths to match forms repo structure - Maintains backward compatibility with existing DatePicker usage Key new features: - Custom DayButton implementation with data attributes - Advanced classNames handling with getDefaultClassNames - Better focus management and keyboard navigation - Enhanced styling system with CSS custom properties - Improved range selection and multi-month support
1 parent d2e4159 commit d09205e

4 files changed

Lines changed: 268 additions & 85 deletions

File tree

packages/components/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"@radix-ui/react-radio-group": "^1.2.2",
5252
"@radix-ui/react-scroll-area": "^1.2.2",
5353
"@radix-ui/react-separator": "^1.1.2",
54-
"@radix-ui/react-slot": "^1.1.2",
54+
"@radix-ui/react-slot": "^1.2.3",
5555
"@radix-ui/react-switch": "^1.1.2",
5656
"@radix-ui/react-tooltip": "^1.1.6",
5757
"@tanstack/react-table": "^8.21.2",
@@ -62,7 +62,7 @@
6262
"input-otp": "^1.4.1",
6363
"lucide-react": "^0.468.0",
6464
"next-themes": "^0.4.4",
65-
"react-day-picker": "8.10.1",
65+
"react-day-picker": "^9.7.0",
6666
"react-hook-form": "^7.53.1",
6767
"react-router": "^7.6.1",
6868
"remix-hook-form": "7.0.1",
Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,59 @@
1-
import { Slot } from '@radix-ui/react-slot';
2-
import { type VariantProps, cva } from 'class-variance-authority';
3-
import type * as React from 'react';
4-
import { cn } from './utils';
1+
import * as React from "react"
2+
import { Slot } from "@radix-ui/react-slot"
3+
import { cva, type VariantProps } from "class-variance-authority"
4+
5+
import { cn } from "./utils"
56

67
const buttonVariants = cva(
7-
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
8+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
89
{
910
variants: {
1011
variant: {
11-
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
12-
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
13-
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
14-
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
15-
ghost: 'hover:bg-accent hover:text-accent-foreground',
16-
link: 'text-primary underline-offset-4 hover:underline',
12+
default:
13+
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14+
destructive:
15+
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16+
outline:
17+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18+
secondary:
19+
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20+
ghost:
21+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22+
link: "text-primary underline-offset-4 hover:underline",
1723
},
1824
size: {
19-
default: 'h-10 px-4 py-2',
20-
sm: 'h-9 rounded-md px-3',
21-
lg: 'h-11 rounded-md px-8',
22-
icon: 'h-10 w-10',
25+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
26+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28+
icon: "size-9",
2329
},
2430
},
2531
defaultVariants: {
26-
variant: 'default',
27-
size: 'default',
32+
variant: "default",
33+
size: "default",
2834
},
29-
},
30-
);
35+
}
36+
)
3137

32-
export interface ButtonProps
33-
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
34-
VariantProps<typeof buttonVariants> {
35-
asChild?: boolean;
36-
}
38+
function Button({
39+
className,
40+
variant,
41+
size,
42+
asChild = false,
43+
...props
44+
}: React.ComponentProps<"button"> &
45+
VariantProps<typeof buttonVariants> & {
46+
asChild?: boolean
47+
}) {
48+
const Comp = asChild ? Slot : "button"
3749

38-
export function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) {
39-
const Comp = asChild ? Slot : 'button';
40-
return <Comp className={cn(buttonVariants({ variant, size, className }))} data-slot="button" {...props} />;
50+
return (
51+
<Comp
52+
data-slot="button"
53+
className={cn(buttonVariants({ variant, size, className }))}
54+
{...props}
55+
/>
56+
)
4157
}
4258

43-
Button.displayName = 'Button';
44-
45-
export { buttonVariants };
59+
export { Button, buttonVariants }
Lines changed: 195 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,208 @@
1-
import { ChevronLeft, ChevronRight } from 'lucide-react';
2-
import type * as React from 'react';
3-
import { type CustomComponents, DayPicker } from 'react-day-picker';
4-
import { buttonVariants } from './button';
5-
import { cn } from './utils';
1+
import * as React from "react"
2+
import {
3+
ChevronDownIcon,
4+
ChevronLeftIcon,
5+
ChevronRightIcon,
6+
} from "lucide-react"
7+
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
68

7-
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
9+
import { cn } from "./utils"
10+
import { Button, buttonVariants } from "./button"
11+
12+
function Calendar({
13+
className,
14+
classNames,
15+
showOutsideDays = true,
16+
captionLayout = "label",
17+
buttonVariant = "ghost",
18+
formatters,
19+
components,
20+
...props
21+
}: React.ComponentProps<typeof DayPicker> & {
22+
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
23+
}) {
24+
const defaultClassNames = getDefaultClassNames()
825

9-
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
1026
return (
1127
<DayPicker
1228
showOutsideDays={showOutsideDays}
13-
className={cn('p-3', className)}
14-
data-slot="calendar"
29+
className={cn(
30+
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
31+
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
32+
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
33+
className
34+
)}
35+
captionLayout={captionLayout}
36+
formatters={{
37+
formatMonthDropdown: (date) =>
38+
date.toLocaleString("default", { month: "short" }),
39+
...formatters,
40+
}}
1541
classNames={{
16-
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
17-
month: 'space-y-4',
18-
caption: 'flex justify-center pt-1 relative items-center',
19-
caption_label: 'text-sm font-medium',
20-
nav: 'space-x-1 flex items-center',
21-
nav_button: cn(
22-
buttonVariants({ variant: 'outline' }),
23-
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
24-
),
25-
nav_button_previous: 'absolute left-1',
26-
nav_button_next: 'absolute right-1',
27-
table: 'w-full border-collapse space-y-1',
28-
head_row: 'flex',
29-
head_cell: 'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]',
30-
row: 'flex w-full mt-2',
31-
cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
32-
day: cn(buttonVariants({ variant: 'ghost' }), 'h-9 w-9 p-0 font-normal aria-selected:opacity-100'),
33-
day_range_end: 'day-range-end',
34-
day_selected:
35-
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
36-
day_today: 'bg-accent text-accent-foreground',
37-
day_outside:
38-
'day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground',
39-
day_disabled: 'text-muted-foreground opacity-50',
40-
day_range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground',
41-
day_hidden: 'invisible',
42+
root: cn("w-fit", defaultClassNames.root),
43+
months: cn(
44+
"flex gap-4 flex-col md:flex-row relative",
45+
defaultClassNames.months
46+
),
47+
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
48+
nav: cn(
49+
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
50+
defaultClassNames.nav
51+
),
52+
button_previous: cn(
53+
buttonVariants({ variant: buttonVariant }),
54+
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
55+
defaultClassNames.button_previous
56+
),
57+
button_next: cn(
58+
buttonVariants({ variant: buttonVariant }),
59+
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
60+
defaultClassNames.button_next
61+
),
62+
month_caption: cn(
63+
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
64+
defaultClassNames.month_caption
65+
),
66+
dropdowns: cn(
67+
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
68+
defaultClassNames.dropdowns
69+
),
70+
dropdown_root: cn(
71+
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
72+
defaultClassNames.dropdown_root
73+
),
74+
dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown),
75+
caption_label: cn(
76+
"select-none font-medium",
77+
captionLayout === "label"
78+
? "text-sm"
79+
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
80+
defaultClassNames.caption_label
81+
),
82+
table: "w-full border-collapse",
83+
weekdays: cn("flex", defaultClassNames.weekdays),
84+
weekday: cn(
85+
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
86+
defaultClassNames.weekday
87+
),
88+
week: cn("flex w-full mt-2", defaultClassNames.week),
89+
week_number_header: cn(
90+
"select-none w-(--cell-size)",
91+
defaultClassNames.week_number_header
92+
),
93+
week_number: cn(
94+
"text-[0.8rem] select-none text-muted-foreground",
95+
defaultClassNames.week_number
96+
),
97+
day: cn(
98+
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
99+
defaultClassNames.day
100+
),
101+
range_start: cn(
102+
"rounded-l-md bg-accent",
103+
defaultClassNames.range_start
104+
),
105+
range_middle: cn("rounded-none", defaultClassNames.range_middle),
106+
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
107+
today: cn(
108+
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
109+
defaultClassNames.today
110+
),
111+
outside: cn(
112+
"text-muted-foreground aria-selected:text-muted-foreground",
113+
defaultClassNames.outside
114+
),
115+
disabled: cn(
116+
"text-muted-foreground opacity-50",
117+
defaultClassNames.disabled
118+
),
119+
hidden: cn("invisible", defaultClassNames.hidden),
42120
...classNames,
43121
}}
44-
components={
45-
{
46-
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" {...props} />,
47-
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" {...props} />,
48-
} as Partial<CustomComponents>
49-
}
122+
components={{
123+
Root: ({ className, rootRef, ...props }) => {
124+
return (
125+
<div
126+
data-slot="calendar"
127+
ref={rootRef}
128+
className={cn(className)}
129+
{...props}
130+
/>
131+
)
132+
},
133+
Chevron: ({ className, orientation, ...props }) => {
134+
if (orientation === "left") {
135+
return (
136+
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
137+
)
138+
}
139+
140+
if (orientation === "right") {
141+
return (
142+
<ChevronRightIcon
143+
className={cn("size-4", className)}
144+
{...props}
145+
/>
146+
)
147+
}
148+
149+
return (
150+
<ChevronDownIcon className={cn("size-4", className)} {...props} />
151+
)
152+
},
153+
DayButton: CalendarDayButton,
154+
WeekNumber: ({ children, ...props }) => {
155+
return (
156+
<td {...props}>
157+
<div className="flex size-(--cell-size) items-center justify-center text-center">
158+
{children}
159+
</div>
160+
</td>
161+
)
162+
},
163+
...components,
164+
}}
50165
{...props}
51166
/>
52-
);
167+
)
53168
}
54169

55-
export { Calendar };
170+
function CalendarDayButton({
171+
className,
172+
day,
173+
modifiers,
174+
...props
175+
}: React.ComponentProps<typeof DayButton>) {
176+
const defaultClassNames = getDefaultClassNames()
177+
178+
const ref = React.useRef<HTMLButtonElement>(null)
179+
React.useEffect(() => {
180+
if (modifiers.focused) ref.current?.focus()
181+
}, [modifiers.focused])
182+
183+
return (
184+
<Button
185+
ref={ref}
186+
variant="ghost"
187+
size="icon"
188+
data-day={day.date.toLocaleDateString()}
189+
data-selected-single={
190+
modifiers.selected &&
191+
!modifiers.range_start &&
192+
!modifiers.range_end &&
193+
!modifiers.range_middle
194+
}
195+
data-range-start={modifiers.range_start}
196+
data-range-end={modifiers.range_end}
197+
data-range-middle={modifiers.range_middle}
198+
className={cn(
199+
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
200+
defaultClassNames.day,
201+
className
202+
)}
203+
{...props}
204+
/>
205+
)
206+
}
56207

208+
export { Calendar, CalendarDayButton }

0 commit comments

Comments
 (0)