From 94f06685f9c781766acf08f7893c1b99f01a0bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Vila=C3=A7a=20de=20Jesus?= Date: Sun, 25 Oct 2026 16:27:06 -0300 Subject: [PATCH 1/2] chore: start #623 From 84489c199054e84b5d230fadc75af3d460bcc4fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Vila=C3=A7a=20de=20Jesus?= Date: Sun, 25 Oct 2026 18:26:03 -0300 Subject: [PATCH 2/2] refactor: share the Date field shell and calendar frame (#623) --- .../src/lib/components/Date/Calendar.svelte | 16 +- .../lib/components/Date/CalendarFrame.svelte | 35 ++++ .../src/lib/components/Date/DateField.svelte | 63 ++------ .../lib/components/Date/DateFieldShell.svelte | 151 ++++++++++++++++++ .../src/lib/components/Date/DatePicker.svelte | 115 +++---------- .../components/Date/DateRangePicker.svelte | 134 ++++++---------- .../lib/components/Date/RangeCalendar.svelte | 16 +- .../src/lib/components/Date/TimeField.svelte | 63 ++------ .../ui/src/lib/components/Date/date-utils.ts | 13 ++ 9 files changed, 306 insertions(+), 300 deletions(-) create mode 100644 packages/ui/src/lib/components/Date/CalendarFrame.svelte create mode 100644 packages/ui/src/lib/components/Date/DateFieldShell.svelte diff --git a/packages/ui/src/lib/components/Date/Calendar.svelte b/packages/ui/src/lib/components/Date/Calendar.svelte index 2227bf8a..76cd793a 100644 --- a/packages/ui/src/lib/components/Date/Calendar.svelte +++ b/packages/ui/src/lib/components/Date/Calendar.svelte @@ -3,7 +3,7 @@ import { calendar } from '../../i18n/groups/calendar.js'; import type { DateValue } from '@internationalized/date'; import { announce } from '../../utils/announce.js'; - import CalendarBody from './CalendarBody.svelte'; + import CalendarFrame from './CalendarFrame.svelte'; import { CalendarState } from './calendar-state.svelte.js'; import type { WeekStart } from './date-utils.js'; import './date.css'; @@ -80,16 +80,4 @@ }); - -
-
{cal.fullLabel}
- -
+ diff --git a/packages/ui/src/lib/components/Date/CalendarFrame.svelte b/packages/ui/src/lib/components/Date/CalendarFrame.svelte new file mode 100644 index 00000000..dc301cfc --- /dev/null +++ b/packages/ui/src/lib/components/Date/CalendarFrame.svelte @@ -0,0 +1,35 @@ + + + +
+
{cal.fullLabel}
+ +
diff --git a/packages/ui/src/lib/components/Date/DateField.svelte b/packages/ui/src/lib/components/Date/DateField.svelte index d03f081f..94a46f22 100644 --- a/packages/ui/src/lib/components/Date/DateField.svelte +++ b/packages/ui/src/lib/components/Date/DateField.svelte @@ -2,7 +2,7 @@ import type { DateValue } from '@internationalized/date'; import { announce } from '../../utils/announce.js'; import { uid } from '../../utils/index.js'; - import DateSegments from './DateSegments.svelte'; + import DateFieldShell from './DateFieldShell.svelte'; import { defaultDate } from './date-utils.js'; import { FieldState } from './field-state.svelte.js'; import './date.css'; @@ -53,7 +53,6 @@ const id = uid('df'); - const describedBy = $derived(error ? `${id}-err` : description ? `${id}-hint` : undefined); let wrap = $state(null); // Empty field: the placeholder decides the segment format and the base for typed values. @@ -84,48 +83,18 @@ $effect(() => field.sync(value)); -
- {#if label} - -
!disabled && field.focusFirst()}> - {label}{#if required}{/if} -
- {/if} - -
- -
- {#if name} - - {/if} - - {#if error} - - {:else if description} -

{description}

- {/if} -
+ diff --git a/packages/ui/src/lib/components/Date/DateFieldShell.svelte b/packages/ui/src/lib/components/Date/DateFieldShell.svelte new file mode 100644 index 00000000..7219504c --- /dev/null +++ b/packages/ui/src/lib/components/Date/DateFieldShell.svelte @@ -0,0 +1,151 @@ + + +{#snippet body()} +
+ {#if label} + + + {label}{#if required}{/if} + + {/if} + +
+ {#if segments} + {@render segments()} + {:else} + + {/if} + {#if TriggerIcon} + + {/if} +
+ {#if name} + + {/if} + + {#if error} + + {:else if description} +

{description}

+ {/if} +
+{/snippet} + +{#if range} +
{@render body()}
+{:else} + {@render body()} +{/if} diff --git a/packages/ui/src/lib/components/Date/DatePicker.svelte b/packages/ui/src/lib/components/Date/DatePicker.svelte index f8b0f760..49e12f10 100644 --- a/packages/ui/src/lib/components/Date/DatePicker.svelte +++ b/packages/ui/src/lib/components/Date/DatePicker.svelte @@ -5,10 +5,10 @@ import type { DateValue } from '@internationalized/date'; import { announce } from '../../utils/announce.js'; import { uid } from '../../utils/index.js'; - import CalendarBody from './CalendarBody.svelte'; + import CalendarFrame from './CalendarFrame.svelte'; import { CalendarState } from './calendar-state.svelte.js'; - import DateSegments from './DateSegments.svelte'; - import { defaultDate, type WeekStart } from './date-utils.js'; + import DateFieldShell from './DateFieldShell.svelte'; + import { defaultDate, focusOwnDay, type WeekStart } from './date-utils.js'; import { FieldState } from './field-state.svelte.js'; import PickerPopover from './PickerPopover.svelte'; import './date.css'; @@ -74,7 +74,6 @@ }: Props = $props(); const id = uid('dp'); - const describedBy = $derived(error ? `${id}-err` : description ? `${id}-hint` : undefined); let wrap = $state(null); let triggerEl = $state(null); @@ -134,84 +133,28 @@ open = false; if (restoreFocus) triggerEl?.focus(); } - - /** - * Focus goes to this popover's own active day, without scrolling the page (other calendars may - * sit further down). - */ - function focusOwnDay(content: HTMLElement) { - queueMicrotask(() => { - const day = - content.querySelector('[data-ca-day][data-focused]') ?? - content.querySelector('[data-ca-day]:not([data-disabled])'); - day?.focus({ preventScroll: true }); - }); - } -
- {#if label} - -
!disabled && field.focusFirst()}> - {label}{#if required}{/if} -
- {/if} - -
- - -
- {#if name} - - {/if} - - {#if error} - - {:else if description} -

{description}

- {/if} -
+ (open = !open)} + {name} + formValue={value?.toString()} + bind:wrap +/> close()} onopen={focusOwnDay} > - -
-
{cal.fullLabel}
- -
+
diff --git a/packages/ui/src/lib/components/Date/DateRangePicker.svelte b/packages/ui/src/lib/components/Date/DateRangePicker.svelte index 4a65c0a8..a5bf1c2e 100644 --- a/packages/ui/src/lib/components/Date/DateRangePicker.svelte +++ b/packages/ui/src/lib/components/Date/DateRangePicker.svelte @@ -5,10 +5,11 @@ import type { DateValue } from '@internationalized/date'; import { announce } from '../../utils/announce.js'; import { uid } from '../../utils/index.js'; - import CalendarBody from './CalendarBody.svelte'; + import CalendarFrame from './CalendarFrame.svelte'; import { CalendarState } from './calendar-state.svelte.js'; import DateSegments from './DateSegments.svelte'; - import { defaultDate, type WeekStart } from './date-utils.js'; + import DateFieldShell from './DateFieldShell.svelte'; + import { defaultDate, focusOwnDay, type WeekStart } from './date-utils.js'; import { FieldState } from './field-state.svelte.js'; import PickerPopover from './PickerPopover.svelte'; import type { DateRange } from './types.js'; @@ -69,7 +70,6 @@ }: Props = $props(); const id = uid('drp'); - const describedBy = $derived(error ? `${id}-err` : description ? `${id}-hint` : undefined); let wrap = $state(null); let triggerEl = $state(null); @@ -146,81 +146,51 @@ open = false; if (restoreFocus) triggerEl?.focus(); } - - /** See DatePicker: focus this popover's own day without scrolling the page. */ - function focusOwnDay(content: HTMLElement) { - queueMicrotask(() => { - const day = - content.querySelector('[data-ca-day][data-focused]') ?? - content.querySelector('[data-ca-day]:not([data-disabled])'); - day?.focus({ preventScroll: true }); - }); - } -
-
- {#if label} - - !disabled && startField.focusFirst()}> - {label}{#if required}{/if} - - {/if} - -
- - - - -
- - {#if error} - - {:else if description} -

{description}

- {/if} -
-
+ (open = !open)} + bind:wrap +> + {#snippet segments()} + + + + {/snippet} + close()} onopen={focusOwnDay} > - -
-
{cal.fullLabel}
- -
+
diff --git a/packages/ui/src/lib/components/Date/RangeCalendar.svelte b/packages/ui/src/lib/components/Date/RangeCalendar.svelte index c5997dc6..56647204 100644 --- a/packages/ui/src/lib/components/Date/RangeCalendar.svelte +++ b/packages/ui/src/lib/components/Date/RangeCalendar.svelte @@ -3,7 +3,7 @@ import { calendar } from '../../i18n/groups/calendar.js'; import type { DateValue } from '@internationalized/date'; import { announce } from '../../utils/announce.js'; - import CalendarBody from './CalendarBody.svelte'; + import CalendarFrame from './CalendarFrame.svelte'; import { CalendarState } from './calendar-state.svelte.js'; import type { WeekStart } from './date-utils.js'; import type { DateRange } from './types.js'; @@ -76,16 +76,4 @@ }); - -
-
{cal.fullLabel}
- -
+ diff --git a/packages/ui/src/lib/components/Date/TimeField.svelte b/packages/ui/src/lib/components/Date/TimeField.svelte index 9f827b2e..2d05ac49 100644 --- a/packages/ui/src/lib/components/Date/TimeField.svelte +++ b/packages/ui/src/lib/components/Date/TimeField.svelte @@ -2,7 +2,7 @@ import { Time } from '@internationalized/date'; import { announce } from '../../utils/announce.js'; import { uid } from '../../utils/index.js'; - import DateSegments from './DateSegments.svelte'; + import DateFieldShell from './DateFieldShell.svelte'; import { FieldState } from './field-state.svelte.js'; import type { TimeValue } from './types.js'; import './date.css'; @@ -54,7 +54,6 @@ const id = uid('tmf'); - const describedBy = $derived(error ? `${id}-err` : description ? `${id}-hint` : undefined); let wrap = $state(null); const basePlaceholder = $derived(placeholder ?? new Time(0, 0, 0)); @@ -82,48 +81,18 @@ $effect(() => field.sync(value)); -
- {#if label} - -
!disabled && field.focusFirst()}> - {label}{#if required}{/if} -
- {/if} - -
- -
- {#if name} - - {/if} - - {#if error} - - {:else if description} -

{description}

- {/if} -
+ diff --git a/packages/ui/src/lib/components/Date/date-utils.ts b/packages/ui/src/lib/components/Date/date-utils.ts index 6dd9dd70..14d58b90 100644 --- a/packages/ui/src/lib/components/Date/date-utils.ts +++ b/packages/ui/src/lib/components/Date/date-utils.ts @@ -216,3 +216,16 @@ export function placeholderFor(part: string, locale: string): string { if (part === "hour" || part === "minute" || part === "second") return "––"; return ""; } + +/** + * Moves focus to a picker popover's own active day, without scrolling the page (other calendars + * may sit further down). + */ +export function focusOwnDay(content: HTMLElement): void { + queueMicrotask(() => { + const day = + content.querySelector("[data-ca-day][data-focused]") ?? + content.querySelector("[data-ca-day]:not([data-disabled])"); + day?.focus({ preventScroll: true }); + }); +}