Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
77d4ca9
Add a "today" shortcut for the date picker
jaygeorge May 11, 2026
2787812
Add a "today" shortcut for the date picker
jaygeorge May 11, 2026
a500beb
Add an "apply button" to replace "today" one clicked so you can use "…
jaygeorge May 11, 2026
13a8b65
Compress button a bit
jaygeorge May 11, 2026
069efb8
Date field - improve alignment of inline calendar
jaygeorge May 11, 2026
03c21cb
Date field - improve alignment of calendar
jaygeorge May 11, 2026
bb9af2c
Apply changes to Date Range picker too
jaygeorge May 11, 2026
29e2807
Add a behavior the for the date range picker - when you click today >…
jaygeorge May 11, 2026
ca72db0
Fix "select" behaviour when the date is _not_ inline
jaygeorge May 12, 2026
46ca8f0
Fix clicking "today" when the date is not inline, so it forwards the …
jaygeorge May 12, 2026
fc3d36c
Apply should still show when there is a date _range_
jaygeorge May 12, 2026
ca0945c
For the date range picker, if you click "today", then _don't_ click s…
jaygeorge May 12, 2026
139687f
If there is an "earliest date" or "latest date" set before today then…
jaygeorge May 12, 2026
63ee500
Reset all changes
jaygeorge May 14, 2026
d0497cb
Merge branch '6.x' into date-picker-today-shortcut
jaygeorge May 14, 2026
ca64636
Just apply the alignment fixes
jaygeorge May 14, 2026
9f1e02d
Greatly simplify and improve implementation
jaygeorge May 14, 2026
0720ac5
Tighten up the buttons a bit
jaygeorge May 14, 2026
44b97bb
Avoid stale "today" across midnight
jasonvarga May 14, 2026
af733ec
Remove redundant dark variant on disabled opacity
jasonvarga May 14, 2026
5df5178
Extract today shortcut into its own component
jasonvarga May 14, 2026
dddffce
nitpick diff
jasonvarga May 14, 2026
5e124e7
Fix stale today state when calendar stays open past midnight
jasonvarga May 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions resources/js/components/ui/Calendar/Calendar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from 'reka-ui';
import { parseAbsolute } from '@internationalized/date';
import Icon from '../Icon/Icon.vue';
import CalendarToday from './CalendarToday.vue';

defineOptions({ name: 'Calendar' });

Expand All @@ -39,6 +40,7 @@ const components = computed(() => ({
CalendarHeading: props.components.Heading || CalendarHeading,
CalendarPrev: props.components.Prev || CalendarPrev,
CalendarNext: props.components.Next || CalendarNext,
CalendarToday: props.components.Today || CalendarToday,
CalendarGrid: props.components.Grid || CalendarGrid,
CalendarGridHead: props.components.GridHead || CalendarGridHead,
CalendarGridBody: props.components.GridBody || CalendarGridBody,
Expand Down Expand Up @@ -74,6 +76,20 @@ const gridStyle = computed(() => {
'grid-template-rows': 'auto'
};
});

/** Popover uses slight negative inset to align with card; inline skips that inside padded layouts. */
const calendarHeaderClass = computed(() =>
props.inline
? 'flex items-center justify-between pb-3.5 ms-1 -me-1.5 -mt-1'
: 'flex items-center justify-between ps-3.5 pe-1 pb-3.5 -mt-1.5',
);

/** Month grid wrapper: popover matches narrow card; inline allows shrink in tight form layouts. */
const calendarGridClass = computed(() =>
props.inline
? 'w-full border-collapse space-y-1 select-none -ms-2'
: 'w-full border-collapse space-y-1 select-none',
);
</script>

<template>
Expand All @@ -88,18 +104,19 @@ const gridStyle = computed(() => {
:number-of-months="numberOfMonths"
@update:model-value="emit('update:modelValue', $event)"
>
<Component :is="components.CalendarHeader" class="flex items-center justify-between ps-3 pe-1 pb-3.5 -mt-1">
<Component :is="components.CalendarHeader" :class="calendarHeaderClass">
<Component :is="components.CalendarHeading" class="text-sm font-medium text-gray-925 dark:text-white" />
<div>
<div class="inline-flex items-center">
<Component
:is="components.CalendarPrev"
class="inline-flex size-8 cursor-pointer items-center justify-center rounded-md hover:bg-gray-50 active:scale-90 dark:hover:bg-gray-925"
class="inline-flex size-7.5 cursor-pointer items-center justify-center rounded-md hover:bg-gray-50 active:scale-90 dark:hover:bg-gray-925"
>
<Icon name="chevron-left" class="size-4" />
</Component>
<Component :is="components.CalendarToday" />
<Component
:is="components.CalendarNext"
class="inline-flex size-8 cursor-pointer items-center justify-center rounded-md hover:bg-gray-50 active:scale-90 dark:hover:bg-gray-925"
class="inline-flex size-7.5 cursor-pointer items-center justify-center rounded-md hover:bg-gray-50 active:scale-90 dark:hover:bg-gray-925"
>
<Icon name="chevron-right" class="size-4" />
</Component>
Expand All @@ -111,7 +128,7 @@ const gridStyle = computed(() => {
:is="components.CalendarGrid"
v-for="month in grid"
:key="month.value.toString()"
class="w-full border-collapse space-y-1 select-none"
:class="calendarGridClass"
>
<Component :is="components.CalendarGridHead">
<ui-badge class="mb-2" v-if="numberOfMonths > 1">
Expand Down
53 changes: 53 additions & 0 deletions resources/js/components/ui/Calendar/CalendarToday.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script setup>
import { computed, onMounted, onUnmounted, ref, unref } from 'vue';
import { fromDate, getLocalTimeZone, isSameMonth, startOfMonth, toCalendarDate } from '@internationalized/date';
import { injectCalendarRootContext, injectRangeCalendarRootContext } from 'reka-ui';
import Icon from '../Icon/Icon.vue';

defineOptions({ name: 'CalendarToday' });

const calendarRoot = injectCalendarRootContext(null) ?? injectRangeCalendarRootContext(null);

const today = ref(new Date());
let timer;

// Re-evaluates "today" at midnight so the disabled state doesn't go stale if the calendar stays open overnight.
function scheduleUpdate() {
const now = new Date();
const msUntilMidnight = +new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1) - +now;
timer = setTimeout(() => { today.value = new Date(); scheduleUpdate(); }, msUntilMidnight);
}

onMounted(scheduleUpdate);
onUnmounted(() => clearTimeout(timer));

function currentMonth() {
return startOfMonth(toCalendarDate(fromDate(today.value, getLocalTimeZone())));
}

const disabled = computed(() => {
if (!calendarRoot) return true;
if (unref(calendarRoot.disabled) || unref(calendarRoot.readonly)) return true;
const placeholder = unref(calendarRoot.placeholder);
if (!placeholder) return false;
return isSameMonth(toCalendarDate(placeholder), currentMonth());
});

function goToToday() {
if (!calendarRoot || disabled.value) return;
calendarRoot.onPlaceholderChange(currentMonth());
}
</script>

<template>
<button
type="button"
class="inline-flex size-7.5 shrink-0 cursor-pointer items-center justify-center rounded-md text-gray-925 hover:bg-gray-50 active:scale-90 disabled:pointer-events-none disabled:opacity-40 dark:text-white dark:hover:bg-gray-925"
:disabled="disabled"
:aria-label="__('This month')"
v-tooltip="__('This month')"
@click="goToToday"
>
<Icon name="calendar" class="size-3.5!" />
</button>
</template>
13 changes: 7 additions & 6 deletions resources/js/components/ui/DateRangePicker/DateRangePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const calendarBindings = computed(() => ({
modelValue: props.modelValue ?? [],
min: props.min,
max: props.max,
inline: props.inline,
components: {
Root: DateRangePickerCalendar,
Header: DateRangePickerHeader,
Expand All @@ -72,11 +73,11 @@ const calendarBindings = computed(() => ({
},
}));

// The placeholder defines the month to show when there's no value. Additionally,
// by setting it to an absolute value, it ensures that the emitted event value
// will be the appropriate format (e.g. a full date with time with timezone,
// rather than just a day).
const placeholder = parseAbsoluteToLocal(new Date().toISOString());
// Initial month when there is no value. Use defaultPlaceholder (not placeholder) so
// the range root stays uncontrolled: month navigation can update the visible month,
// and shared calendar UI (e.g. Today) can read that state from inject context.
// Still an absolute local ZonedDateTime so defaults match full date/time shape.
const defaultPlaceholder = parseAbsoluteToLocal(new Date().toISOString());

const calendarEvents = computed(() => ({
'update:model-value': (event) => {
Expand Down Expand Up @@ -131,7 +132,7 @@ const hoverCardDate = computed(() => {
v-bind="$attrs"
prevent-deselect
hide-time-zone
:placeholder="placeholder"
:default-placeholder="defaultPlaceholder"
close-on-select
>
<DateRangePickerField v-slot="{ segments }" class="w-full">
Expand Down
Loading