From c0083af8d9464c9d162b8c6704faea2d241b25c5 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 13 Nov 2026 14:45:19 +0000 Subject: [PATCH] add month/year picker to calendar heading Amp-Thread-ID: https://ampcode.com/threads/T-01a1d550-1311-76f1-becc-cabba2f6801c Co-authored-by: Duncan McClean --- .../js/components/ui/Calendar/Calendar.vue | 5 +- .../ui/Calendar/CalendarMonthYearPicker.vue | 160 ++++++++++++++++++ 2 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 resources/js/components/ui/Calendar/CalendarMonthYearPicker.vue diff --git a/resources/js/components/ui/Calendar/Calendar.vue b/resources/js/components/ui/Calendar/Calendar.vue index 9f9bb5943be..71770a32b2e 100644 --- a/resources/js/components/ui/Calendar/Calendar.vue +++ b/resources/js/components/ui/Calendar/Calendar.vue @@ -17,6 +17,7 @@ import { import { parseAbsolute } from '@internationalized/date'; import Icon from '../Icon/Icon.vue'; import CalendarToday from './CalendarToday.vue'; +import CalendarMonthYearPicker from './CalendarMonthYearPicker.vue'; defineOptions({ name: 'Calendar' }); @@ -105,7 +106,9 @@ const calendarGridClass = computed(() => @update:model-value="emit('update:modelValue', $event)" > - + + +
+import { computed, ref, unref } from 'vue'; +import { + injectCalendarRootContext, + injectRangeCalendarRootContext, + MonthPickerCell, + MonthPickerCellTrigger, + MonthPickerGrid, + MonthPickerGridBody, + MonthPickerGridRow, + MonthPickerHeader, + MonthPickerHeading, + MonthPickerNext, + MonthPickerPrev, + MonthPickerRoot, + YearPickerCell, + YearPickerCellTrigger, + YearPickerGrid, + YearPickerGridBody, + YearPickerGridRow, + YearPickerHeader, + YearPickerHeading, + YearPickerNext, + YearPickerPrev, + YearPickerRoot, +} from 'reka-ui'; +import Icon from '../Icon/Icon.vue'; +import Popover from '../Popover.vue'; + +defineOptions({ name: 'CalendarMonthYearPicker' }); + +const calendarRoot = injectCalendarRootContext(null) ?? injectRangeCalendarRootContext(null); + +const open = ref(false); +const view = ref('month'); +const placeholder = ref(null); + +const disabled = computed(() => !calendarRoot || unref(calendarRoot.disabled) || unref(calendarRoot.readonly)); + +const pickerBindings = computed(() => ({ + minValue: unref(calendarRoot.minValue), + maxValue: unref(calendarRoot.maxValue), + locale: unref(calendarRoot.locale), + preventDeselect: true, + initialFocus: true, +})); + +const navigationButtonClass = + 'inline-flex size-7.5 cursor-pointer items-center justify-center rounded-md hover:bg-gray-50 active:scale-90 disabled:pointer-events-none disabled:opacity-40 dark:hover:bg-gray-925'; + +const cellTriggerClass = [ + 'relative flex h-9 w-full cursor-pointer items-center justify-center rounded-lg text-sm whitespace-nowrap text-gray-925 outline-hidden dark:text-white', + 'data-selected:bg-gray-800! data-selected:text-white dark:data-selected:bg-gray-200! dark:data-selected:text-gray-925', + 'hover:bg-gray-100 focus-visible:bg-gray-200 dark:hover:bg-black dark:focus-visible:bg-black', + 'data-disabled:pointer-events-none data-disabled:text-gray-400 dark:data-disabled:text-gray-600', + 'before:absolute before:top-[3px] before:hidden before:h-1 before:w-1 before:rounded-lg', + 'data-today:before:block data-today:before:bg-green-500', +]; + +function updateOpen(isOpen) { + open.value = isOpen; + + if (isOpen) { + view.value = 'month'; + placeholder.value = unref(calendarRoot.placeholder); + } +} + +function selectYear(date) { + placeholder.value = date; + view.value = 'month'; +} + +function selectMonth(date) { + calendarRoot.onPlaceholderChange(date); + open.value = false; +} + + +