fix: add back month and year selection for date picker

This commit is contained in:
Elias Schneider
2025-05-23 11:56:08 +02:00
parent 869c4c5871
commit 6c35570e78
4 changed files with 110 additions and 39 deletions

View File

@@ -68,18 +68,23 @@
<div class="w-full" {...restProps}> <div class="w-full" {...restProps}>
<Popover.Root bind:open> <Popover.Root bind:open>
<Popover.Trigger class="w-full"> <Popover.Trigger {id} class="w-full" >
<Button {#snippet child({ props })}
{id} <Button
variant="outline" {...props}
class={cn('w-full justify-start text-left font-normal', !value && 'text-muted-foreground')} variant="outline"
aria-label={m.select_a_date()} class={cn(
> 'w-full justify-start text-left font-normal',
<CalendarIcon class="mr-2 size-4" /> !value && 'text-muted-foreground'
{calendarDisplayDate )}
? df.format(calendarDisplayDate.toDate(getLocalTimeZone())) aria-label={m.select_a_date()}
: m.select_a_date()} >
</Button> <CalendarIcon class="mr-2 size-4" />
{calendarDisplayDate
? df.format(calendarDisplayDate.toDate(getLocalTimeZone()))
: m.select_a_date()}
</Button>
{/snippet}
</Popover.Trigger> </Popover.Trigger>
<Popover.Content class="w-auto p-0" align="start"> <Popover.Content class="w-auto p-0" align="start">
<Calendar <Calendar

View File

@@ -1,35 +1,99 @@
<script lang="ts"> <script lang="ts">
import { Calendar as CalendarPrimitive } from 'bits-ui'; import * as Calendar from '$lib/components/ui/calendar/index.js';
import * as Calendar from './index.js'; import * as Select from '$lib/components/ui/select/index.js';
import { cn, type WithoutChildrenOrChild } from '$lib/utils/style.js'; import { cn } from '$lib/utils/style';
import { CalendarDate, DateFormatter, getLocalTimeZone, today } from '@internationalized/date';
import { Calendar as CalendarPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
let { let {
ref = $bindable(null),
value = $bindable(), value = $bindable(),
placeholder = $bindable(), placeholder = $bindable()
class: className, }: WithoutChildrenOrChild<CalendarPrimitive.RootProps> & {
weekdayFormat = 'short', value: CalendarDate | undefined;
...restProps } = $props();
}: WithoutChildrenOrChild<CalendarPrimitive.RootProps> = $props();
const currentDate = today(getLocalTimeZone());
const monthFmt = new DateFormatter('en-US', {
month: 'long'
});
const monthOptions = Array.from({ length: 12 }, (_, i) => {
const month = currentDate.set({ month: i + 1 });
return {
value: month.month,
label: monthFmt.format(month.toDate(getLocalTimeZone()))
};
});
const yearOptions = Array.from({ length: 100 }, (_, i) => ({
label: String(new Date().getFullYear() + i),
value: new Date().getFullYear() + i
}));
const defaultYear = $derived(
placeholder ? { value: placeholder.year, label: String(placeholder.year) } : undefined
);
const defaultMonth = $derived(
placeholder
? {
value: placeholder.month,
label: monthFmt.format(placeholder.toDate(getLocalTimeZone()))
}
: undefined
);
const monthLabel = $derived(
monthOptions.find((m) => m.value === defaultMonth?.value)?.label ?? 'Select a month'
);
</script> </script>
<!--
Discriminated Unions + Destructing (required for bindable) do not
get along, so we shut typescript up by casting `value` to `never`.
-->
<CalendarPrimitive.Root <CalendarPrimitive.Root
bind:value={value as never} type="single"
bind:ref weekdayFormat="short"
class={cn('rounded-md border p-3')}
bind:value
bind:placeholder bind:placeholder
{weekdayFormat}
class={cn('p-3', className)}
{...restProps}
> >
{#snippet children({ months, weekdays })} {#snippet children({ months, weekdays })}
<Calendar.Header> <Calendar.Header class="flex w-full items-center justify-between gap-2">
<Calendar.PrevButton /> <Select.Root
<Calendar.Heading /> type="single"
<Calendar.NextButton /> value={`${defaultMonth?.value}`}
onValueChange={(v) => {
if (!placeholder) return;
if (v === `${placeholder.month}`) return;
placeholder = placeholder.set({ month: Number.parseInt(v) });
}}
>
<Select.Trigger aria-label="Select month" class="w-[60%]">
{monthLabel}
</Select.Trigger>
<Select.Content class="max-h-[200px] overflow-y-auto">
{#each monthOptions as { value, label } (value)}
<Select.Item value={`${value}`} {label} />
{/each}
</Select.Content>
</Select.Root>
<Select.Root
type="single"
value={`${defaultYear?.value}`}
onValueChange={(v) => {
if (!v || !placeholder) return;
if (v === `${placeholder?.year}`) return;
placeholder = placeholder.set({ year: Number.parseInt(v) });
}}
>
<Select.Trigger aria-label="Select year" class="w-[40%]">
{defaultYear?.label ?? 'Select year'}
</Select.Trigger>
<Select.Content class="max-h-[200px] overflow-y-auto">
{#each yearOptions as { value, label } (value)}
<Select.Item value={`${value}`} {label} />
{/each}
</Select.Content>
</Select.Root>
</Calendar.Header> </Calendar.Header>
<Calendar.Months> <Calendar.Months>
{#each months as month (month)} {#each months as month (month)}
@@ -47,7 +111,7 @@ get along, so we shut typescript up by casting `value` to `never`.
{#each month.weeks as weekDates (weekDates)} {#each month.weeks as weekDates (weekDates)}
<Calendar.GridRow class="mt-2 w-full"> <Calendar.GridRow class="mt-2 w-full">
{#each weekDates as date (date)} {#each weekDates as date (date)}
<Calendar.Cell {date} month={month.value}> <Calendar.Cell class="select-none" {date} month={month.value}>
<Calendar.Day /> <Calendar.Day />
</Calendar.Cell> </Calendar.Cell>
{/each} {/each}

View File

@@ -19,7 +19,7 @@
</script> </script>
<Dialog.Root open={!!apiKeyResponse} {onOpenChange}> <Dialog.Root open={!!apiKeyResponse} {onOpenChange}>
<Dialog.Content class="max-w-md"> <Dialog.Content class="max-w-md" onOpenAutoFocus={(e) => e.preventDefault()}>
<Dialog.Header> <Dialog.Header>
<Dialog.Title>{m.api_key_created()}</Dialog.Title> <Dialog.Title>{m.api_key_created()}</Dialog.Title>
<Dialog.Description> <Dialog.Description>
@@ -30,7 +30,6 @@
<div> <div>
<div class="mb-2 font-medium">{m.name()}</div> <div class="mb-2 font-medium">{m.name()}</div>
<p class="text-muted-foreground">{apiKeyResponse.apiKey.name}</p> <p class="text-muted-foreground">{apiKeyResponse.apiKey.name}</p>
{#if apiKeyResponse.apiKey.description} {#if apiKeyResponse.apiKey.description}
<div class="mt-4 mb-2 font-medium">{m.description()}</div> <div class="mt-4 mb-2 font-medium">{m.description()}</div>
<p class="text-muted-foreground">{apiKeyResponse.apiKey.description}</p> <p class="text-muted-foreground">{apiKeyResponse.apiKey.description}</p>

View File

@@ -19,7 +19,7 @@ test.describe("API Key Management", () => {
// Choose the date // Choose the date
const currentDate = new Date(); const currentDate = new Date();
await page.getByLabel("Expires At").click(); await page.getByRole("button", { name: "Select a date" }).click();
await page.getByLabel("Select year").click(); await page.getByLabel("Select year").click();
// Select the next year // Select the next year
await page.getByText((currentDate.getFullYear() + 1).toString()).click(); await page.getByText((currentDate.getFullYear() + 1).toString()).click();
@@ -45,7 +45,10 @@ test.describe("API Key Management", () => {
expect(token?.length).toBe(32); expect(token?.length).toBe(32);
// Close the dialog // Close the dialog
await page.getByRole("button", { name: "Close" }).click(); await page
.getByRole("button", { name: "Close", exact: true })
.nth(1)
.click();
await page.reload(); await page.reload();