11<script lang="ts" setup>
22import type { CalendarRootEmits , CalendarRootProps , DateValue } from " reka-ui"
33import type { HTMLAttributes , Ref } from " vue"
4- import type { LayoutTypes } from " . "
4+ import { ref } from " vue "
55import { getLocalTimeZone , today } from " @internationalized/date"
6- import { createReusableTemplate , reactiveOmit , useVModel } from " @vueuse/core"
6+ import { reactiveOmit , useVModel } from " @vueuse/core"
77import { CalendarRoot , useDateFormatter , useForwardPropsEmits } from " reka-ui"
8- import { createYear , createYearRange , toDate } from " reka-ui/date"
9- import { computed , toRaw } from " vue"
10- import { cn } from " @/lib/utils"
11- import { NativeSelect , NativeSelectOption } from ' @/components/ui/native-select'
12- import { CalendarCell , CalendarCellTrigger , CalendarGrid , CalendarGridBody , CalendarGridHead , CalendarGridRow , CalendarHeadCell , CalendarHeader , CalendarHeading , CalendarNextButton , CalendarPrevButton } from " ."
8+ import { toDate } from " reka-ui/date"
9+ import {
10+ CalendarCell ,
11+ CalendarCellTrigger ,
12+ CalendarGrid ,
13+ CalendarGridBody ,
14+ CalendarGridHead ,
15+ CalendarGridRow ,
16+ CalendarHeadCell ,
17+ CalendarHeader ,
18+ CalendarNextButton ,
19+ CalendarPrevButton ,
20+ } from " ."
21+ import CalendarMonthGrid from " ./CalendarMonthGrid.vue"
22+ import CalendarYearGrid from " ./CalendarYearGrid.vue"
1323
14- const props = withDefaults (defineProps <CalendarRootProps & { class? : HTMLAttributes [" class" ], layout? : LayoutTypes , yearRange? : DateValue [] }>(), {
15- modelValue: undefined ,
16- layout: undefined ,
17- })
24+ const props = withDefaults (
25+ defineProps <CalendarRootProps & { class? : HTMLAttributes [" class" ] }>(),
26+ {
27+ modelValue: undefined ,
28+ },
29+ )
1830const emits = defineEmits <CalendarRootEmits >()
1931
20- const delegatedProps = reactiveOmit (props , " class" , " layout " , " placeholder" )
32+ const delegatedProps = reactiveOmit (props , " class" , " placeholder" )
2133
2234const placeholder = useVModel (props , " placeholder" , emits , {
2335 passive: true ,
@@ -26,137 +38,110 @@ const placeholder = useVModel(props, "placeholder", emits, {
2638
2739const formatter = useDateFormatter (props .locale ?? " en" )
2840
29- const yearRange = computed (() => {
30- return props .yearRange ?? createYearRange ({
31- start: props ?.minValue ?? (toRaw (props .placeholder ) ?? props .defaultPlaceholder ?? today (getLocalTimeZone ()))
32- .cycle (" year" , - 100 ),
41+ type View = " days" | " months" | " years"
42+ const view = ref <View >(" days" )
3343
34- end: props ?. maxValue ?? ( toRaw ( props . placeholder ) ?? props . defaultPlaceholder ?? today ( getLocalTimeZone ()))
35- . cycle ( " year " , 10 ),
36- })
37- })
44+ function drillUp() {
45+ if ( view . value === " days " ) view . value = " months "
46+ else if ( view . value === " months " ) view . value = " years "
47+ }
3848
39- const [DefineMonthTemplate, ReuseMonthTemplate] = createReusableTemplate <{ date: DateValue }>()
40- const [DefineYearTemplate, ReuseYearTemplate] = createReusableTemplate <{ date: DateValue }>()
49+ function step(direction : -1 | 1 ) {
50+ const amount = view .value === " months" ? direction : direction * 10
51+ placeholder .value = placeholder .value .add ({ years: amount })
52+ }
53+
54+ function pickMonth(month : number ) {
55+ placeholder .value = placeholder .value .set ({ month })
56+ view .value = " days"
57+ }
58+
59+ function pickYear(year : number ) {
60+ placeholder .value = placeholder .value .set ({ year })
61+ view .value = " months"
62+ }
4163
4264const forwarded = useForwardPropsEmits (delegatedProps , emits )
4365 </script >
4466
4567<template >
46- <DefineMonthTemplate v-slot =" { date }" >
47- <div class =" **:data-[slot=native-select-icon]:right-1" >
48- <div class =" relative" >
49- <div class =" absolute inset-0 flex h-full items-center text-sm pl-2 pointer-events-none" >
50- {{ formatter.custom(toDate(date), { month: 'short' }) }}
51- </div >
52- <NativeSelect
53- class =" text-xs h-8 pr-6 pl-2 text-transparent relative"
54- :model-value =" date.month"
55- @change =" (e: Event) => {
56- placeholder = placeholder.set({
57- month: Number((e?.target as any)?.value),
58- })
59- }"
68+ <div :class =" props.class" data-slot =" calendar" >
69+ <CalendarRoot
70+ v-show =" view === 'days'"
71+ v-slot =" { grid, weekDays }"
72+ v-bind =" forwarded"
73+ v-model:placeholder =" placeholder"
74+ class =" p-3"
75+ >
76+ <CalendarHeader class =" pt-0" >
77+ <nav class =" flex items-center gap-1 absolute top-0 inset-x-0 justify-between pointer-events-none" >
78+ <CalendarPrevButton class =" pointer-events-auto" >
79+ <slot name =" calendar-prev-icon" />
80+ </CalendarPrevButton >
81+ <CalendarNextButton class =" pointer-events-auto" >
82+ <slot name =" calendar-next-icon" />
83+ </CalendarNextButton >
84+ </nav >
85+ <button
86+ type =" button"
87+ aria-label =" Switch to month view"
88+ class =" block mx-auto h-7 px-2 text-sm font-medium rounded-md hover:bg-accent"
89+ @click =" drillUp"
6090 >
61- <NativeSelectOption v-for =" (month) in createYear({ dateObj: date })" :key =" month.toString()" :value =" month.month" :selected =" date.month === month.month" >
62- {{ formatter.custom(toDate(month), { month: 'short' }) }}
63- </NativeSelectOption >
64- </NativeSelect >
65- </div >
66- </div >
67- </DefineMonthTemplate >
91+ {{ formatter.custom(toDate(placeholder), { month: 'long', year: 'numeric' }) }}
92+ </button >
93+ </CalendarHeader >
6894
69- <DefineYearTemplate v-slot =" { date }" >
70- <div class =" **:data-[slot=native-select-icon]:right-1" >
71- <div class =" relative" >
72- <div class =" absolute inset-0 flex h-full items-center text-sm pl-2 pointer-events-none" >
73- {{ formatter.custom(toDate(date), { year: 'numeric' }) }}
74- </div >
75- <NativeSelect
76- class =" text-xs h-8 pr-6 pl-2 text-transparent relative"
77- :model-value =" date.year"
78- @change =" (e: Event) => {
79- placeholder = placeholder.set({
80- year: Number((e?.target as any)?.value),
81- })
82- }"
83- >
84- <NativeSelectOption v-for =" (year) in yearRange" :key =" year.toString()" :value =" year.year" :selected =" date.year === year.year" >
85- {{ formatter.custom(toDate(year), { year: 'numeric' }) }}
86- </NativeSelectOption >
87- </NativeSelect >
95+ <div class =" flex flex-col gap-y-4 mt-4 sm:flex-row sm:gap-x-4 sm:gap-y-0" >
96+ <CalendarGrid v-for =" month in grid" :key =" month.value.toString()" >
97+ <CalendarGridHead >
98+ <CalendarGridRow >
99+ <CalendarHeadCell v-for =" day in weekDays" :key =" day" >
100+ {{ day }}
101+ </CalendarHeadCell >
102+ </CalendarGridRow >
103+ </CalendarGridHead >
104+ <CalendarGridBody >
105+ <CalendarGridRow
106+ v-for =" (weekDates, index) in month.rows"
107+ :key =" `weekDate-${index}`"
108+ class =" mt-2 w-full"
109+ >
110+ <CalendarCell
111+ v-for =" weekDate in weekDates"
112+ :key =" weekDate.toString()"
113+ :date =" weekDate"
114+ >
115+ <CalendarCellTrigger :day =" weekDate" :month =" month.value" />
116+ </CalendarCell >
117+ </CalendarGridRow >
118+ </CalendarGridBody >
119+ </CalendarGrid >
88120 </div >
89- </div >
90- </DefineYearTemplate >
91-
92- <CalendarRoot
93- v-slot =" { grid, weekDays, date }"
94- v-bind =" forwarded"
95- v-model:placeholder =" placeholder"
96- data-slot =" calendar"
97- :class =" cn('p-3', props.class)"
98- >
99- <CalendarHeader class =" pt-0" >
100- <nav class =" flex items-center gap-1 absolute top-0 inset-x-0 justify-between" >
101- <CalendarPrevButton >
102- <slot name =" calendar-prev-icon" />
103- </CalendarPrevButton >
104- <CalendarNextButton >
105- <slot name =" calendar-next-icon" />
106- </CalendarNextButton >
107- </nav >
121+ </CalendarRoot >
108122
109- <slot name =" calendar-heading" :date =" date" :month =" ReuseMonthTemplate" :year =" ReuseYearTemplate" >
110- <template v-if =" layout === ' month-and-year' " >
111- <div class =" flex items-center justify-center gap-1" >
112- <ReuseMonthTemplate :date =" date" />
113- <ReuseYearTemplate :date =" date" />
114- </div >
115- </template >
116- <template v-else-if =" layout === ' month-only' " >
117- <div class =" flex items-center justify-center gap-1" >
118- <ReuseMonthTemplate :date =" date" />
119- {{ formatter.custom(toDate(date), { year: 'numeric' }) }}
120- </div >
121- </template >
122- <template v-else-if =" layout === ' year-only' " >
123- <div class =" flex items-center justify-center gap-1" >
124- {{ formatter.custom(toDate(date), { month: 'short' }) }}
125- <ReuseYearTemplate :date =" date" />
126- </div >
127- </template >
128- <template v-else >
129- <CalendarHeading />
130- </template >
131- </slot >
132- </CalendarHeader >
123+ <CalendarMonthGrid
124+ v-if =" view === 'months'"
125+ :placeholder =" placeholder"
126+ :model-value =" props.modelValue"
127+ :min-value =" props.minValue"
128+ :max-value =" props.maxValue"
129+ :locale =" props.locale"
130+ @prev =" step(-1)"
131+ @next =" step(1)"
132+ @drill-up =" drillUp"
133+ @pick =" pickMonth"
134+ />
133135
134- <div class =" flex flex-col gap-y-4 mt-4 sm:flex-row sm:gap-x-4 sm:gap-y-0" >
135- <CalendarGrid v-for =" month in grid" :key =" month.value.toString()" >
136- <CalendarGridHead >
137- <CalendarGridRow >
138- <CalendarHeadCell
139- v-for =" day in weekDays" :key =" day"
140- >
141- {{ day }}
142- </CalendarHeadCell >
143- </CalendarGridRow >
144- </CalendarGridHead >
145- <CalendarGridBody >
146- <CalendarGridRow v-for =" (weekDates, index) in month.rows" :key =" `weekDate-${index}`" class =" mt-2 w-full" >
147- <CalendarCell
148- v-for =" weekDate in weekDates"
149- :key =" weekDate.toString()"
150- :date =" weekDate"
151- >
152- <CalendarCellTrigger
153- :day =" weekDate"
154- :month =" month.value"
155- />
156- </CalendarCell >
157- </CalendarGridRow >
158- </CalendarGridBody >
159- </CalendarGrid >
160- </div >
161- </CalendarRoot >
136+ <CalendarYearGrid
137+ v-if =" view === 'years'"
138+ :placeholder =" placeholder"
139+ :model-value =" props.modelValue"
140+ :min-value =" props.minValue"
141+ :max-value =" props.maxValue"
142+ @prev =" step(-1)"
143+ @next =" step(1)"
144+ @pick =" pickYear"
145+ />
146+ </div >
162147</template >
0 commit comments