Skip to content

Commit 94557a5

Browse files
authored
Merge pull request #105 from makeswift/andrew/calendar
Calendar updates
2 parents daa6a8a + ae628e8 commit 94557a5

File tree

4 files changed

+380
-124
lines changed

4 files changed

+380
-124
lines changed

.changeset/short-eyes-turn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'commerce-toolkit': minor
3+
---
4+
5+
Added month and year dropdowns to Calendar component

src/components/button/button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export function Button({
5959
<button
6060
aria-busy={loading}
6161
className={cn(
62-
'relative z-0 inline-flex h-fit select-none items-center justify-center overflow-hidden text-center font-semibold leading-normal duration-200 ease-in-out [font-family:var(--button-font-family,var(--font-family-body))] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--button-focus,hsl(var(--primary)))] disabled:pointer-events-none disabled:opacity-30',
62+
'relative z-0 inline-flex h-fit select-none items-center justify-center overflow-hidden text-center font-semibold leading-normal transition-all duration-75 ease-linear [font-family:var(--button-font-family,var(--font-family-body))] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--button-focus,hsl(var(--primary)))] disabled:pointer-events-none disabled:opacity-30',
6363
{
6464
brand:
6565
'bg-[var(--button-brand-background,hsl(var(--primary)))] text-[var(--button-brand-text,hsl(var(--foreground)))] hover:opacity-70',
Lines changed: 238 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,267 @@
1-
import { ChevronLeftIcon } from 'lucide-react';
2-
import type { ComponentProps } from 'react';
3-
import { DayPicker } from 'react-day-picker';
1+
'use client';
42

3+
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
4+
import type { ComponentProps, CSSProperties } from 'react';
5+
import { useEffect, useRef } from 'react';
6+
import { type DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker';
7+
8+
import { type ButtonProps } from '@/components/button';
59
import { cn } from '@/lib';
610

7-
const components = {
8-
Chevron: () => <ChevronLeftIcon className="h-5 w-5" strokeWidth={1} />,
11+
export type CalendarProps = ComponentProps<typeof DayPicker> & {
12+
buttonVariant?: ButtonProps['variant'];
913
};
1014

11-
export type CalendarProps = ComponentProps<typeof DayPicker>;
12-
1315
/**
1416
* This component supports various CSS variables for theming. Here's a comprehensive list, along
1517
* with their default values:
1618
*
1719
* ```css
18-
* :root {
20+
* :root {
1921
* --calendar-font-family: var(--font-family-body);
20-
* --calendar-light-focus: var(--foreground);
21-
* --calendar-light-border: var(--contrast-100);
22-
* --calendar-light-text: var(--foreground);
23-
* --calendar-light-background: var(--background);
24-
* --calendar-light-button-border-hover: var(--contrast-200);
25-
* --calendar-light-selected-button-background: var(--primary);
26-
* --calendar-light-selected-button-text: var(--foreground);
27-
* --calendar-light-selected-middle-button-background: transparent;
28-
* --calendar-light-text-disabled: var(--contrast-300);
29-
* --calendar-light-range-background: var(--primary-highlight);
30-
* --calendar-dark-focus: var(--background);
31-
* --calendar-dark-border: var(--contrast-500);
32-
* --calendar-dark-text: var(--background);
33-
* --calendar-dark-background: var(--foreground);
34-
* --calendar-dark-button-border-hover: var(--contrast-400);
35-
* --calendar-dark-selected-button-background: var(--primary);
36-
* --calendar-dark-selected-button-text: var(--foreground);
37-
* --calendar-dark-selected-middle-button-background: transparent;
38-
* --calendar-dark-text-disabled: var(--contrast-300);
39-
* --calendar-dark-range-background: color-mix(in oklab, var(--primary), white 60%);
40-
* }
22+
* --calendar-focus: var(--foreground);
23+
* --calendar-text: var(--foreground);
24+
* --calendar-background: var(--background);
25+
* --calendar-selected-background: var(--primary);
26+
* --calendar-selected-text: var(--foreground);
27+
* --calendar-text-disabled: var(--contrast-300);
28+
* }
4129
* ```
4230
*/
43-
export function Calendar({ className, classNames, ...props }: CalendarProps) {
31+
export function Calendar({
32+
className,
33+
classNames,
34+
showOutsideDays = true,
35+
captionLayout = 'label',
36+
formatters,
37+
components,
38+
...props
39+
}: ComponentProps<typeof DayPicker> & {
40+
buttonVariant?: ButtonProps['variant'];
41+
}) {
42+
const defaultClassNames = getDefaultClassNames();
43+
44+
const cellSizeStyle: CSSProperties & { '--cell-size': string } = {
45+
'--cell-size': '40px',
46+
};
47+
4448
return (
4549
<DayPicker
50+
captionLayout={captionLayout}
4651
className={cn(
47-
'box-content w-[280px] rounded-lg border border-[var(--calendar-light-border,hsl(var(--contrast-100)))] bg-[var(--calendar-light-background,hsl(var(--background)))] p-3 font-[var(--calendar-font-family,var(--font-family-body))] text-[var(--calendar-light-text,hsl(var(--foreground)))]',
52+
'group/calendar rounded-2xl bg-[var(--calendar-background,hsl(var(--background)))] p-3 font-[var(--calendar-font-family,var(--font-family-body))] text-[var(--calendar-text,hsl(var(--foreground)))] shadow-lg shadow-black/10 ring-1 ring-black/5',
4853
className,
4954
)}
5055
classNames={{
51-
months: 'relative',
52-
month_caption: 'flex justify-center w-full font-medium pb-0.5',
53-
nav: 'absolute flex justify-between w-full',
54-
button_next: cn(
55-
'rotate-180 rounded-full focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--calendar-light-focus,hsl(var(--foreground)))]',
56+
root: cn('w-fit', defaultClassNames.root),
57+
months: cn('relative flex flex-col gap-8 md:flex-row', defaultClassNames.months),
58+
month: cn('flex w-full flex-col gap-1', defaultClassNames.month),
59+
nav: cn(
60+
'absolute -inset-x-0.5 top-0 flex w-full items-center justify-between gap-1 text-foreground',
61+
defaultClassNames.nav,
5662
),
5763
button_previous: cn(
58-
'rounded-full focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--calendar-light-focus,hsl(var(--foreground)))]',
64+
'inline-flex size-[var(--cell-size)] cursor-default items-center justify-center rounded-full transition-colors duration-75 ease-linear hover:bg-contrast-100 focus-visible:ring-[var(--calendar-focus,hsl(var(--primary)))] aria-disabled:cursor-not-allowed aria-disabled:opacity-50',
65+
defaultClassNames.button_previous,
66+
),
67+
button_next: cn(
68+
'inline-flex size-[var(--cell-size)] cursor-default items-center justify-center rounded-full transition-colors duration-75 ease-linear hover:bg-contrast-100 focus-visible:ring-[var(--calendar-focus,hsl(var(--primary)))] aria-disabled:cursor-not-allowed aria-disabled:opacity-25',
69+
defaultClassNames.button_next,
70+
),
71+
month_caption: cn(
72+
'flex h-[var(--cell-size)] w-full items-center justify-center px-[var(--cell-size)]',
73+
defaultClassNames.month_caption,
74+
),
75+
dropdowns: cn(
76+
'flex h-[var(--cell-size)] w-full items-center justify-center gap-1.5 text-sm font-medium',
77+
defaultClassNames.dropdowns,
78+
),
79+
dropdown_root: cn(
80+
'relative rounded-lg border border-contrast-200 transition-colors duration-75 ease-linear hover:bg-foreground/5 focus:border-foreground',
81+
defaultClassNames.dropdown_root,
82+
),
83+
dropdown: cn('absolute inset-0 opacity-0', defaultClassNames.dropdown),
84+
caption_label: cn(
85+
'flex h-8 select-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-sm font-medium',
86+
defaultClassNames.caption_label,
87+
),
88+
table: 'w-full border-collapse',
89+
weekdays: cn('flex', defaultClassNames.weekdays),
90+
weekday: cn(
91+
'inline-flex size-[var(--cell-size)] flex-1 select-none items-center justify-center text-xs font-semibold',
92+
defaultClassNames.weekday,
93+
),
94+
week: cn('mt-1 flex w-full', defaultClassNames.week),
95+
week_number_header: cn(
96+
'size-[var(--cell-size)] select-none',
97+
defaultClassNames.week_number_header,
98+
),
99+
week_number: cn('select-none text-xs', defaultClassNames.week_number),
100+
day: cn(
101+
'group/day relative z-0 size-[var(--cell-size)] select-none p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-full',
102+
props.showWeekNumber === true
103+
? '[&:nth-child(2)[data-selected=true]_button]:rounded-l-full'
104+
: '[&:first-child[data-selected=true]_button]:rounded-l-full',
105+
defaultClassNames.day,
106+
),
107+
range_start: cn(
108+
'rounded-l-full bg-[var(--calendar-selected-background,hsl(var(--primary)/.25))]',
109+
defaultClassNames.range_start,
110+
),
111+
range_middle: cn('rounded-none', defaultClassNames.range_middle),
112+
range_end: cn(
113+
'rounded-r-full bg-[var(--calendar-selected-background,hsl(var(--primary)/.25))]',
114+
defaultClassNames.range_end,
59115
),
60-
month_grid: 'flex flex-col gap-0.5',
61-
weeks: 'flex flex-col gap-0.5',
62-
weekdays: 'flex',
63-
weekday: 'flex h-10 w-10 items-center justify-center text-xs font-medium',
64-
week: 'flex',
65-
day: 'h-10 w-10 flex text-xs font-medium group p-0',
66-
day_button: cn(
67-
'flex h-full w-full items-center justify-center rounded-full hover:border hover:border-[var(--calendar-light-button-border-hover,hsl(var(--contrast-200)))] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--calendar-light-focus,hsl(var(--foreground)))] disabled:hover:border-none group-data-[selected=true]/middle:bg-[var(--calendar-light-selected-middle-button-background,transparent)] group-data-[selected=true]:bg-[var(--calendar-light-selected-button-background,hsl(var(--primary)))] group-data-[selected=true]:text-[var(--calendar-light-selected-button-text,hsl(var(--foreground)))]',
116+
today: cn(
117+
'rounded-full ring-1 ring-inset ring-contrast-200 data-[selected=true]:rounded-full data-[selected=true]:ring-0',
118+
defaultClassNames.today,
68119
),
69-
disabled: 'text-[var(--calendar-light-text-disabled,hsl(var(--contrast-300)))]',
70-
outside: 'text-[var(--calendar-light-text-disabled,hsl(var(--contrast-300)))]',
71-
range_start:
72-
'bg-gradient-to-l from-[var(--calendar-light-range-background,color-mix(in_oklab,hsl(var(--primary)),white_75%))]',
73-
range_middle:
74-
'group/middle bg-[var(--calendar-light-range-background,color-mix(in_oklab,hsl(var(--primary)),white_75%))]',
75-
range_end:
76-
'bg-gradient-to-r from-[var(--calendar-light-range-background,color-mix(in_oklab,hsl(var(--primary)),white_75%))]',
120+
outside: cn(
121+
'text-opacity-50 hover:text-opacity-100 aria-selected:text-opacity-50',
122+
defaultClassNames.outside,
123+
),
124+
disabled: cn('cursor-not-allowed text-opacity-50', defaultClassNames.disabled),
125+
hidden: cn('invisible', defaultClassNames.hidden),
77126
...classNames,
78127
}}
79-
components={components}
128+
components={{
129+
Root: ({ className: rootClassName, rootRef, ...rootProps }) => {
130+
return (
131+
<div className={cn(rootClassName)} data-slot="calendar" ref={rootRef} {...rootProps} />
132+
);
133+
},
134+
Chevron: ({ className: chevronClassName, orientation, ...chevronProps }) => {
135+
if (orientation === 'left') {
136+
return (
137+
<ChevronLeftIcon
138+
absoluteStrokeWidth
139+
className={cn('size-5 -translate-x-px', chevronClassName)}
140+
{...chevronProps}
141+
/>
142+
);
143+
}
144+
145+
if (orientation === 'right') {
146+
return (
147+
<ChevronRightIcon
148+
absoluteStrokeWidth
149+
className={cn('size-5 translate-x-px', chevronClassName)}
150+
{...chevronProps}
151+
/>
152+
);
153+
}
154+
155+
return (
156+
<ChevronDownIcon
157+
absoluteStrokeWidth
158+
className={cn('size-3.5', chevronClassName)}
159+
{...chevronProps}
160+
/>
161+
);
162+
},
163+
DayButton: CalendarDayButton,
164+
WeekNumber: ({ children, ...weekNumberProps }) => {
165+
return (
166+
<td {...weekNumberProps}>
167+
<div className="flex size-[var(--cell-size)] items-center justify-center text-center">
168+
{children}
169+
</div>
170+
</td>
171+
);
172+
},
173+
...components,
174+
}}
175+
formatters={{
176+
formatMonthDropdown: (date) => date.toLocaleString('default', { month: 'short' }),
177+
...formatters,
178+
}}
179+
showOutsideDays={showOutsideDays}
180+
style={cellSizeStyle}
80181
{...props}
81182
/>
82183
);
83184
}
185+
186+
type CalendarDayButtonProps = ComponentProps<typeof DayButton>;
187+
188+
function CalendarDayButton({
189+
className: dayButtonClassName,
190+
day,
191+
modifiers,
192+
...dayButtonProps
193+
}: CalendarDayButtonProps) {
194+
const defaultClassNames = getDefaultClassNames();
195+
196+
const ref = useRef<HTMLButtonElement>(null);
197+
198+
useEffect(() => {
199+
if (modifiers.focused === true) ref.current?.focus();
200+
}, [modifiers.focused]);
201+
202+
return (
203+
<button
204+
className={cn(
205+
// Base styles
206+
'size-[var(--cell-size)] rounded-full p-0 text-xs font-normal',
207+
'transition-colors duration-75 ease-linear',
208+
'hover:bg-contrast-100',
209+
// Range end
210+
'data-[range-end=true]:rounded-full',
211+
'data-[range-end=true]:bg-[var(--calendar-selected-button-background,hsl(var(--primary)))]',
212+
'data-[range-end=true]:text-[var(--calendar-selected-text,hsl(var(--foreground)))]',
213+
// Range middle
214+
'data-[range-middle=true]:rounded-none',
215+
'data-[range-middle=true]:bg-[var(--calendar-selected-background,hsl(var(--primary)/.25))]',
216+
'data-[range-middle=true]:before:content-[""]',
217+
'data-[range-middle=true]:before:absolute',
218+
'data-[range-middle=true]:before:-z-10',
219+
'data-[range-middle=true]:before:inset-0',
220+
'data-[range-middle=true]:before:bg-[var(--calendar-selected-background,hsl(var(--primary)))]',
221+
'data-[range-middle=true]:before:rounded-full',
222+
'data-[range-middle=true]:before:opacity-0',
223+
'data-[range-middle=true]:before:transition-opacity',
224+
'data-[range-middle=true]:before:duration-75',
225+
'data-[range-middle=true]:before:ease-linear',
226+
'data-[range-middle=true]:hover:before:opacity-100',
227+
// Range start
228+
'data-[range-start=true]:rounded-full',
229+
'data-[range-start=true]:bg-[var(--calendar-selected-background,hsl(var(--primary)))]',
230+
'data-[range-start=true]:text-[var(--calendar-selected-text,hsl(var(--foreground)))]',
231+
// Selected single
232+
'data-[selected-single=true]:bg-[var(--calendar-selected-background,hsl(var(--primary)))]',
233+
'data-[selected-single=true]:text-[var(--calendar-selected-text,hsl(var(--foreground)))]',
234+
// Focused day (group)
235+
'group-data-[focused=true]/day:relative',
236+
'group-data-[focused=true]/day:z-10',
237+
'group-data-[focused=true]/day:outline-1',
238+
'group-data-[focused=true]/day:outline-foreground',
239+
'group-data-[focused=true]/day:ring-0',
240+
// Outside day (group)
241+
'group-data-[outside=true]/day:text-[var(--calendar-text,hsl(var(--foreground)/0.4))]',
242+
'group-data-[outside=true]/day:hover:text-[var(--calendar-text,hsl(var(--foreground)/1))]',
243+
// Selected day (group)
244+
'group-data-[selected=true]/day:text-foreground',
245+
'group-data-[selected=true]/day:hover:text-foreground',
246+
// Disabled day (group)
247+
'group-data-[disabled=true]/day:opacity-40',
248+
'group-data-[disabled=true]/day:pointer-events-none',
249+
'group-data-[disabled=true]/day:cursor-not-allowed',
250+
defaultClassNames.day,
251+
dayButtonClassName,
252+
)}
253+
data-day={day.date.toLocaleDateString()}
254+
data-range-end={modifiers.range_end === true}
255+
data-range-middle={modifiers.range_middle === true}
256+
data-range-start={modifiers.range_start === true}
257+
data-selected-single={
258+
modifiers.selected === true &&
259+
modifiers.range_start !== true &&
260+
modifiers.range_end !== true &&
261+
modifiers.range_middle !== true
262+
}
263+
ref={ref}
264+
{...dayButtonProps}
265+
/>
266+
);
267+
}

0 commit comments

Comments
 (0)