@marianmeres/stuic 3.170.0 → 3.172.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +10 -8
- package/API.md +90 -0
- package/README.md +1 -1
- package/dist/components/AssetsPreview/AssetsPreview.fixture.svelte +1 -1
- package/dist/components/Calendar/Calendar.svelte +799 -0
- package/dist/components/Calendar/Calendar.svelte.d.ts +135 -0
- package/dist/components/Calendar/README.md +294 -0
- package/dist/components/Calendar/calendar-i18n-sk.d.ts +20 -0
- package/dist/components/Calendar/calendar-i18n-sk.js +46 -0
- package/dist/components/Calendar/calendar-i18n.d.ts +61 -0
- package/dist/components/Calendar/calendar-i18n.js +73 -0
- package/dist/components/Calendar/index.css +307 -0
- package/dist/components/Calendar/index.d.ts +5 -0
- package/dist/components/Calendar/index.js +5 -0
- package/dist/components/Calendar/iso-date.d.ts +107 -0
- package/dist/components/Calendar/iso-date.js +247 -0
- package/dist/components/CommandMenu/CommandMenu.fixture.svelte +1 -1
- package/dist/components/ContextMenu/ContextMenu.svelte +1 -1
- package/dist/components/ContextMenu/ContextMenu.svelte.d.ts +1 -1
- package/dist/components/ContextMenu/README.md +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.svelte +57 -3
- package/dist/components/DropdownMenu/DropdownMenu.svelte.d.ts +4 -2
- package/dist/components/DropdownMenu/README.md +1 -0
- package/dist/components/Input/FieldDate.svelte +349 -0
- package/dist/components/Input/FieldDate.svelte.d.ts +79 -0
- package/dist/components/Input/FieldDateRange.svelte +373 -0
- package/dist/components/Input/FieldDateRange.svelte.d.ts +90 -0
- package/dist/components/Input/README.md +149 -16
- package/dist/components/Input/_internal/FieldDateShell.svelte +327 -0
- package/dist/components/Input/_internal/FieldDateShell.svelte.d.ts +66 -0
- package/dist/components/Input/index.css +119 -0
- package/dist/components/Input/index.d.ts +2 -0
- package/dist/components/Input/index.js +2 -0
- package/dist/components/ModalDialog/ModalDialog.fixture.svelte +1 -1
- package/dist/components/SlidingPanels/SlidingPanels.fixture.svelte +1 -1
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/docs/_archive/README.md +10 -0
- package/docs/{component-testing → _archive/component-testing}/00-overview-and-roadmap.md +8 -6
- package/docs/{component-testing → _archive/component-testing}/01-framework-setup.md +4 -2
- package/docs/{component-testing → _archive/component-testing}/03-component-coverage-roadmap.md +3 -1
- package/docs/{component-testing → _archive/component-testing}/04-hard-cases-and-e2e.md +5 -3
- package/docs/{component-testing → _archive/component-testing}/05-ci.md +2 -0
- package/docs/{component-testing → _archive/component-testing}/PROGRESS.md +3 -1
- package/docs/_archive/component-testing/README.md +28 -0
- package/docs/{upgrading.md → _archive/upgrading.md} +2 -0
- package/docs/architecture.md +19 -11
- package/docs/domains/actions.md +4 -3
- package/docs/domains/components.md +22 -19
- package/docs/domains/utils.md +2 -1
- package/docs/maybe-todo.md +6 -6
- package/docs/tasks.md +19 -9
- package/docs/{component-testing/02-test-conventions.md → testing-components.md} +30 -31
- package/docs/testing.md +3 -3
- package/package.json +2 -1
- package/docs/component-testing/README.md +0 -38
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
search?: boolean | ContextMenuSearchConfig;
|
|
48
48
|
/** Show backdrop in fallback mode (default: true) */
|
|
49
49
|
showBackdrop?: boolean;
|
|
50
|
-
/** Reserve scrollbar space
|
|
50
|
+
/** Reserve scrollbar space; auto-detected once the menu scrolls (see DropdownMenu) */
|
|
51
51
|
scrollbarGutter?: boolean;
|
|
52
52
|
/** Skip the body scroll lock in fallback mode */
|
|
53
53
|
noScrollLock?: boolean;
|
|
@@ -39,7 +39,7 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children">
|
|
|
39
39
|
search?: boolean | ContextMenuSearchConfig;
|
|
40
40
|
/** Show backdrop in fallback mode (default: true) */
|
|
41
41
|
showBackdrop?: boolean;
|
|
42
|
-
/** Reserve scrollbar space
|
|
42
|
+
/** Reserve scrollbar space; auto-detected once the menu scrolls (see DropdownMenu) */
|
|
43
43
|
scrollbarGutter?: boolean;
|
|
44
44
|
/** Skip the body scroll lock in fallback mode */
|
|
45
45
|
noScrollLock?: boolean;
|
|
@@ -29,7 +29,7 @@ trigger semantics and the cursor anchoring.
|
|
|
29
29
|
| `closeOnEscape` | `boolean` | `true` | Close on Escape |
|
|
30
30
|
| `search` | `boolean \| ContextMenuSearchConfig` | - | Search/filter input inside the menu (see DropdownMenu) |
|
|
31
31
|
| `showBackdrop` | `boolean` | `true` | Backdrop in fallback (centered modal) mode |
|
|
32
|
-
| `scrollbarGutter` | `boolean` | auto | Reserve scrollbar space (auto
|
|
32
|
+
| `scrollbarGutter` | `boolean` | auto | Reserve scrollbar space (auto: once the menu actually scrolls, kept until close) |
|
|
33
33
|
| `noScrollLock` | `boolean` | - | Skip the body scroll lock in fallback mode |
|
|
34
34
|
| `forceFallback` | `boolean` | `false` | Force the centered-modal fallback (testing) |
|
|
35
35
|
| `onOpen` / `onClose` | `() => void` | - | Open/close callbacks |
|
|
@@ -194,8 +194,10 @@
|
|
|
194
194
|
onClose?: () => void;
|
|
195
195
|
/** Called when any action item is selected (fallback if item has no onSelect) */
|
|
196
196
|
onSelect?: (item: DropdownMenuActionItem) => void | boolean | Promise<void | boolean>;
|
|
197
|
-
/** Reserve scrollbar space
|
|
198
|
-
*
|
|
197
|
+
/** Reserve scrollbar space (`scrollbar-gutter: stable`) so the menu width does not
|
|
198
|
+
* jump when the scrollbar comes and goes while open (search filtering, expandable
|
|
199
|
+
* sections). `true`/`false` force it. When undefined, the gutter is reserved only
|
|
200
|
+
* once the content actually scrolls, and kept until the menu closes. */
|
|
199
201
|
scrollbarGutter?: boolean;
|
|
200
202
|
/** Reference to trigger element */
|
|
201
203
|
triggerEl?: HTMLButtonElement;
|
|
@@ -684,9 +686,58 @@
|
|
|
684
686
|
// Computed transition duration
|
|
685
687
|
let transitionDuration = $derived(reducedMotion.current ? 0 : 100);
|
|
686
688
|
|
|
689
|
+
// Scrollbar gutter.
|
|
690
|
+
// `scrollbar-gutter: stable` keeps the dropdown width from jumping when the vertical
|
|
691
|
+
// scrollbar comes and goes while the menu is open (search filtering, expanding or
|
|
692
|
+
// collapsing sections). Reserving it up front for a menu that never scrolls leaves a
|
|
693
|
+
// dead strip along the right edge (with classic, space-taking scrollbars), so unless
|
|
694
|
+
// the `scrollbarGutter` prop decides, the gutter is reserved only once a space-taking
|
|
695
|
+
// scrollbar is actually present — and then kept until the menu closes.
|
|
696
|
+
let hasScrollbar = $state(false);
|
|
697
|
+
|
|
698
|
+
/** Whether `el` currently renders a vertical scrollbar that takes up layout space.
|
|
699
|
+
* Overlay scrollbars take none (and `scrollbar-gutter` reserves nothing for them).
|
|
700
|
+
* Both halves are needed: an already reserved gutter takes the same space as a
|
|
701
|
+
* scrollbar (so width alone is fooled by a stale gutter from the previous session),
|
|
702
|
+
* and a mid-transition frame (`overflow: hidden`, reduced height) "overflows"
|
|
703
|
+
* without any scrollbar (so height alone is fooled by the slide animation). */
|
|
704
|
+
function hasVerticalScrollbar(el: HTMLElement): boolean {
|
|
705
|
+
const cs = getComputedStyle(el);
|
|
706
|
+
const borders =
|
|
707
|
+
(parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.borderRightWidth) || 0);
|
|
708
|
+
// offsetWidth - clientWidth = horizontal borders + vertical scrollbar (or gutter)
|
|
709
|
+
const takesSpace = el.offsetWidth - el.clientWidth - borders > 1;
|
|
710
|
+
return takesSpace && el.scrollHeight > el.clientHeight;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function measureScrollbar() {
|
|
714
|
+
if (hasScrollbar || !dropdownEl) return;
|
|
715
|
+
if (hasVerticalScrollbar(dropdownEl)) hasScrollbar = true;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Each open session measures afresh. Reset on open rather than on close, so the
|
|
719
|
+
// gutter survives the closing slide-out (no width jump mid-outro).
|
|
720
|
+
$effect(() => {
|
|
721
|
+
if (isOpen) hasScrollbar = false;
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// Measure on open and whenever the rendered content can grow (search results,
|
|
725
|
+
// expanded sections, items, positioning mode). Effects run before the slide intro
|
|
726
|
+
// applies its first zero-height frame, so this reads the settled layout; the
|
|
727
|
+
// `onintrostart` / `onintroend` handlers in the template re-check around transitions
|
|
728
|
+
// (a re-open that interrupts the closing slide-out is measured mid-animation here,
|
|
729
|
+
// and only settles by `introend`).
|
|
730
|
+
$effect(() => {
|
|
731
|
+
if (!isOpen || !dropdownEl) return;
|
|
732
|
+
void filteredItems;
|
|
733
|
+
void expandedSections;
|
|
734
|
+
void isSupported;
|
|
735
|
+
untrack(measureScrollbar);
|
|
736
|
+
});
|
|
737
|
+
|
|
687
738
|
// Position styles for CSS Anchor Positioning
|
|
688
739
|
let dropdownStyle = $derived.by(() => {
|
|
689
|
-
const useGutter = scrollbarGutter ??
|
|
740
|
+
const useGutter = scrollbarGutter ?? hasScrollbar;
|
|
690
741
|
const gutterStyle = useGutter ? "scrollbar-gutter: stable;" : "";
|
|
691
742
|
if (isSupported) {
|
|
692
743
|
// Use fixed height when search is enabled AND position is a "top" variant
|
|
@@ -878,6 +929,8 @@
|
|
|
878
929
|
)}
|
|
879
930
|
style={dropdownStyle}
|
|
880
931
|
transition:slide={{ duration: transitionDuration }}
|
|
932
|
+
onintrostart={measureScrollbar}
|
|
933
|
+
onintroend={measureScrollbar}
|
|
881
934
|
>
|
|
882
935
|
<!-- Close button (fallback mode only) -->
|
|
883
936
|
{#if !isSupported}
|
|
@@ -1017,6 +1070,7 @@
|
|
|
1017
1070
|
classExpandableContent
|
|
1018
1071
|
)}
|
|
1019
1072
|
transition:slide={{ duration: transitionDuration }}
|
|
1073
|
+
onintroend={measureScrollbar}
|
|
1020
1074
|
>
|
|
1021
1075
|
{#each item.items as childItem}
|
|
1022
1076
|
{#if childItem.type === "action"}
|
|
@@ -163,8 +163,10 @@ export interface Props extends Omit<HTMLButtonAttributes, "children"> {
|
|
|
163
163
|
onClose?: () => void;
|
|
164
164
|
/** Called when any action item is selected (fallback if item has no onSelect) */
|
|
165
165
|
onSelect?: (item: DropdownMenuActionItem) => void | boolean | Promise<void | boolean>;
|
|
166
|
-
/** Reserve scrollbar space
|
|
167
|
-
*
|
|
166
|
+
/** Reserve scrollbar space (`scrollbar-gutter: stable`) so the menu width does not
|
|
167
|
+
* jump when the scrollbar comes and goes while open (search filtering, expandable
|
|
168
|
+
* sections). `true`/`false` force it. When undefined, the gutter is reserved only
|
|
169
|
+
* once the content actually scrolls, and kept until the menu closes. */
|
|
168
170
|
scrollbarGutter?: boolean;
|
|
169
171
|
/** Reference to trigger element */
|
|
170
172
|
triggerEl?: HTMLButtonElement;
|
|
@@ -11,6 +11,7 @@ A feature-rich dropdown menu component with CSS Anchor Positioning (with fallbac
|
|
|
11
11
|
| `position` | `DropdownMenuPosition` | `"bottom-span-right"` | Popover position relative to trigger |
|
|
12
12
|
| `offset` | `string` | `"0.25rem"` | Offset from trigger element (CSS value) |
|
|
13
13
|
| `maxHeight` | `string` | `"300px"` | Max height of dropdown |
|
|
14
|
+
| `scrollbarGutter` | `boolean` | auto | Reserve scrollbar space (auto: once it scrolls) |
|
|
14
15
|
| `closeOnSelect` | `boolean` | `true` | Close menu when action item is selected |
|
|
15
16
|
| `closeOnClickOutside` | `boolean` | `true` | Close on click outside |
|
|
16
17
|
| `closeOnEscape` | `boolean` | `true` | Close on Escape key |
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
FieldDate — pick a single calendar date.
|
|
3
|
+
|
|
4
|
+
The bound `value` is an ISO `YYYY-MM-DD` string (or null). The user picks it in
|
|
5
|
+
a `Calendar`, either in a dialog opened from a trigger button that shows the
|
|
6
|
+
formatted date (default), or inline inside the field (`embedded`). The `name`
|
|
7
|
+
goes on a hidden input carrying the ISO value, so a <form> submits
|
|
8
|
+
`2026-09-02` regardless of how the date is displayed.
|
|
9
|
+
|
|
10
|
+
Loose input (`"2026-09-02T10:00:00Z"`, a `Date`) is normalized to its calendar
|
|
11
|
+
date for display and submission; garbage is submitted as-is so the validator
|
|
12
|
+
can flag it. `required` / `min` / `max` / `isDateDisabled` are enforced in the
|
|
13
|
+
validator because a hidden input never validates natively.
|
|
14
|
+
-->
|
|
15
|
+
<script lang="ts" module>
|
|
16
|
+
import type { Snippet } from "svelte";
|
|
17
|
+
import type { ValidateOptions } from "../../actions/validate.svelte.js";
|
|
18
|
+
import type { TranslateFn } from "../../types.js";
|
|
19
|
+
import type { THC } from "../Thc/Thc.svelte";
|
|
20
|
+
import type { Props as CalendarProps } from "../Calendar/Calendar.svelte";
|
|
21
|
+
import type { IsoDate } from "../Calendar/iso-date.js";
|
|
22
|
+
import type { InputWrapClassProps } from "./types.js";
|
|
23
|
+
|
|
24
|
+
type SnippetWithId = Snippet<[{ id: string }]>;
|
|
25
|
+
|
|
26
|
+
/** The `Calendar` props a date field lifts to its own top level. */
|
|
27
|
+
export type FieldDateCalendarProps = Pick<
|
|
28
|
+
CalendarProps,
|
|
29
|
+
| "min"
|
|
30
|
+
| "max"
|
|
31
|
+
| "isDateDisabled"
|
|
32
|
+
| "weekStartsOn"
|
|
33
|
+
| "weekendDays"
|
|
34
|
+
| "zone"
|
|
35
|
+
| "captionLayout"
|
|
36
|
+
| "yearRange"
|
|
37
|
+
| "showWeekNumbers"
|
|
38
|
+
| "showOutsideDays"
|
|
39
|
+
| "fixedWeeks"
|
|
40
|
+
| "showToday"
|
|
41
|
+
| "renderDay"
|
|
42
|
+
>;
|
|
43
|
+
|
|
44
|
+
export interface Props extends InputWrapClassProps, FieldDateCalendarProps {
|
|
45
|
+
/** The selected date as `YYYY-MM-DD` (bindable). `null` / `""` = empty. */
|
|
46
|
+
value?: IsoDate | null;
|
|
47
|
+
/** Form field name — applied to the hidden input carrying the ISO date. */
|
|
48
|
+
name?: string;
|
|
49
|
+
/** Render the calendar inline inside the field instead of a trigger + dialog. */
|
|
50
|
+
embedded?: boolean;
|
|
51
|
+
/** Close the dialog right after a date is picked. Default `true`. */
|
|
52
|
+
closeOnSelect?: boolean;
|
|
53
|
+
/** Trigger text while empty. Defaults to `t("placeholder_date")`. */
|
|
54
|
+
placeholder?: string;
|
|
55
|
+
/** Offer clearing (trailing × on the trigger / "Clear" in the embedded footer). Default `true`. */
|
|
56
|
+
clearable?: boolean;
|
|
57
|
+
/** BCP 47 locale of the display text and the calendar names. Browser default when omitted. */
|
|
58
|
+
locale?: string;
|
|
59
|
+
/** `Intl.DateTimeFormat` options of the display text. Default `{ dateStyle: "medium" }`. */
|
|
60
|
+
formatOptions?: Intl.DateTimeFormatOptions;
|
|
61
|
+
/** Custom display formatter (wins over `formatOptions`). */
|
|
62
|
+
format?: (iso: IsoDate) => string;
|
|
63
|
+
/** Months the calendar shows side by side. Default `1`. */
|
|
64
|
+
months?: number;
|
|
65
|
+
/** Anything else for the inner `Calendar` (class hooks, `view`, …). Applied last. */
|
|
66
|
+
calendarProps?: Partial<CalendarProps>;
|
|
67
|
+
/** The hidden input (bindable). */
|
|
68
|
+
input?: HTMLInputElement;
|
|
69
|
+
label?: SnippetWithId | THC;
|
|
70
|
+
description?: SnippetWithId | THC;
|
|
71
|
+
labelAfter?: SnippetWithId | THC;
|
|
72
|
+
below?: SnippetWithId | THC;
|
|
73
|
+
class?: string;
|
|
74
|
+
id?: string;
|
|
75
|
+
tabindex?: number;
|
|
76
|
+
renderSize?: "sm" | "md" | "lg" | string;
|
|
77
|
+
required?: boolean;
|
|
78
|
+
disabled?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Validation. `false` disables the built-in guard (required / valid ISO / min /
|
|
81
|
+
* max / disabled dates); an options object is merged with it (your
|
|
82
|
+
* `customValidator` runs after the built-in checks pass). Default: the guard.
|
|
83
|
+
*/
|
|
84
|
+
validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
|
|
85
|
+
labelLeft?: boolean;
|
|
86
|
+
labelLeftWidth?: "normal" | "wide";
|
|
87
|
+
labelLeftBreakpoint?: number;
|
|
88
|
+
/** Classes for the trigger button */
|
|
89
|
+
classInput?: string;
|
|
90
|
+
/** Classes for the dialog box */
|
|
91
|
+
classDialog?: string;
|
|
92
|
+
style?: string;
|
|
93
|
+
noScrollLock?: boolean;
|
|
94
|
+
/** i18n — see `createCalendarT`. */
|
|
95
|
+
t?: TranslateFn;
|
|
96
|
+
/** Called after the value changed through the picker or the × (not on external writes). */
|
|
97
|
+
onChange?: (value: IsoDate | null) => void;
|
|
98
|
+
}
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<script lang="ts">
|
|
102
|
+
import { tick } from "svelte";
|
|
103
|
+
import type { ValidationResult } from "../../actions/validate.svelte.js";
|
|
104
|
+
import { getId } from "../../utils/get-id.js";
|
|
105
|
+
import Button from "../Button/Button.svelte";
|
|
106
|
+
import Calendar from "../Calendar/Calendar.svelte";
|
|
107
|
+
import { t_default } from "../Calendar/calendar-i18n.js";
|
|
108
|
+
import {
|
|
109
|
+
compareIso,
|
|
110
|
+
dayCellOf,
|
|
111
|
+
DEFAULT_DATE_FORMAT,
|
|
112
|
+
formatIsoDate,
|
|
113
|
+
normalizeIsoDate,
|
|
114
|
+
} from "../Calendar/iso-date.js";
|
|
115
|
+
import FieldDateShell from "./_internal/FieldDateShell.svelte";
|
|
116
|
+
|
|
117
|
+
let {
|
|
118
|
+
value = $bindable(null),
|
|
119
|
+
name,
|
|
120
|
+
embedded = false,
|
|
121
|
+
closeOnSelect = true,
|
|
122
|
+
placeholder,
|
|
123
|
+
clearable = true,
|
|
124
|
+
locale,
|
|
125
|
+
formatOptions,
|
|
126
|
+
format,
|
|
127
|
+
months = 1,
|
|
128
|
+
calendarProps,
|
|
129
|
+
input = $bindable(),
|
|
130
|
+
label = "",
|
|
131
|
+
description,
|
|
132
|
+
labelAfter,
|
|
133
|
+
below,
|
|
134
|
+
class: classProp,
|
|
135
|
+
id = getId(),
|
|
136
|
+
tabindex = 0,
|
|
137
|
+
renderSize = "md",
|
|
138
|
+
required = false,
|
|
139
|
+
disabled = false,
|
|
140
|
+
// Renamed local binding to avoid collision with `export function validate()` below.
|
|
141
|
+
validate: validateProp,
|
|
142
|
+
labelLeft = false,
|
|
143
|
+
labelLeftWidth = "normal",
|
|
144
|
+
labelLeftBreakpoint = 480,
|
|
145
|
+
classInput,
|
|
146
|
+
classDialog,
|
|
147
|
+
style,
|
|
148
|
+
noScrollLock = false,
|
|
149
|
+
t = t_default,
|
|
150
|
+
onChange,
|
|
151
|
+
// calendar
|
|
152
|
+
min,
|
|
153
|
+
max,
|
|
154
|
+
isDateDisabled,
|
|
155
|
+
weekStartsOn,
|
|
156
|
+
weekendDays,
|
|
157
|
+
zone,
|
|
158
|
+
captionLayout,
|
|
159
|
+
yearRange,
|
|
160
|
+
showWeekNumbers,
|
|
161
|
+
showOutsideDays,
|
|
162
|
+
fixedWeeks,
|
|
163
|
+
showToday,
|
|
164
|
+
renderDay,
|
|
165
|
+
// wrapper class props
|
|
166
|
+
classLabel,
|
|
167
|
+
classLabelBox,
|
|
168
|
+
classInputBox,
|
|
169
|
+
classInputBoxWrap,
|
|
170
|
+
classInputBoxWrapInvalid,
|
|
171
|
+
classDescBox,
|
|
172
|
+
classDescBoxToggle,
|
|
173
|
+
classBelowBox,
|
|
174
|
+
classValidationBox,
|
|
175
|
+
}: Props = $props();
|
|
176
|
+
|
|
177
|
+
let shell: FieldDateShell | undefined = $state();
|
|
178
|
+
|
|
179
|
+
let normalized = $derived(normalizeIsoDate(value));
|
|
180
|
+
let _min = $derived(normalizeIsoDate(min));
|
|
181
|
+
let _max = $derived(normalizeIsoDate(max));
|
|
182
|
+
|
|
183
|
+
// What the form submits: the normalized date — or the raw (unparseable) value,
|
|
184
|
+
// so the validator can flag it instead of silently submitting "".
|
|
185
|
+
let submitValue = $derived(normalized ?? (value == null ? "" : `${value}`));
|
|
186
|
+
|
|
187
|
+
const fmt = (iso: IsoDate): string =>
|
|
188
|
+
format
|
|
189
|
+
? format(iso)
|
|
190
|
+
: formatIsoDate(iso, locale, formatOptions ?? DEFAULT_DATE_FORMAT);
|
|
191
|
+
|
|
192
|
+
let display = $derived(normalized ? fmt(normalized) : "");
|
|
193
|
+
|
|
194
|
+
function commit(next: IsoDate | null) {
|
|
195
|
+
value = next;
|
|
196
|
+
onChange?.(next);
|
|
197
|
+
// let the hidden input's value flush before the validate action reads it
|
|
198
|
+
tick().then(() => shell?.dispatchChange());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function onSelect(next: IsoDate | null) {
|
|
202
|
+
commit(next);
|
|
203
|
+
if (!embedded && closeOnSelect && next) shell?.close();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Built-in guard (a hidden input never validates natively): required, a valid
|
|
207
|
+
// ISO date, min / max, disabled dates. Consumer opts are honored: `false`
|
|
208
|
+
// disables; an object is merged (its customValidator runs after the guard).
|
|
209
|
+
const validateOpts = $derived.by<
|
|
210
|
+
boolean | Omit<ValidateOptions, "setValidationResult">
|
|
211
|
+
>(() => {
|
|
212
|
+
if (validateProp === false) return false;
|
|
213
|
+
const custom = validateProp && typeof validateProp === "object" ? validateProp : {};
|
|
214
|
+
return {
|
|
215
|
+
...custom,
|
|
216
|
+
customValidator: (val, ctx, el) => {
|
|
217
|
+
const str = `${val ?? ""}`.trim();
|
|
218
|
+
if (!str) {
|
|
219
|
+
if (required) return t("field_req_att");
|
|
220
|
+
return custom.customValidator?.(val, ctx, el) || "";
|
|
221
|
+
}
|
|
222
|
+
const iso = normalizeIsoDate(str);
|
|
223
|
+
if (!iso) return t("date_invalid");
|
|
224
|
+
if (_min && compareIso(iso, _min) < 0) {
|
|
225
|
+
return t("date_before_min", { value: fmt(_min) });
|
|
226
|
+
}
|
|
227
|
+
if (_max && compareIso(iso, _max) > 0) {
|
|
228
|
+
return t("date_after_max", { value: fmt(_max) });
|
|
229
|
+
}
|
|
230
|
+
if (isDateDisabled?.(iso, dayCellOf(iso, { zone, weekendDays }))) {
|
|
231
|
+
return t("date_disabled");
|
|
232
|
+
}
|
|
233
|
+
return custom.customValidator?.(val, ctx, el) || "";
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ---- Imperative API (satisfies ValidatableField), forwarded to the shell ----
|
|
239
|
+
|
|
240
|
+
/** Trigger validation now. Renders the inline message if invalid. */
|
|
241
|
+
export function validate(): ValidationResult | undefined {
|
|
242
|
+
return shell?.validate();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Clear the inline validation message and reset `setCustomValidity`. */
|
|
246
|
+
export function clearValidation(): void {
|
|
247
|
+
shell?.clearValidation();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Current validation state, or undefined if the validator has never run. */
|
|
251
|
+
export function getValidation(): ValidationResult | undefined {
|
|
252
|
+
return shell?.getValidation();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Focus the trigger (or the embedded calendar's tabbable day). */
|
|
256
|
+
export function focus(): void {
|
|
257
|
+
shell?.focus();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Scroll the field into view. Defaults to smooth + center. */
|
|
261
|
+
export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
|
|
262
|
+
shell?.scrollIntoView(opts);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Open the picker dialog (no-op when `embedded`). */
|
|
266
|
+
export function open(): void {
|
|
267
|
+
shell?.open();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Close the picker dialog. */
|
|
271
|
+
export function close(): void {
|
|
272
|
+
shell?.close();
|
|
273
|
+
}
|
|
274
|
+
</script>
|
|
275
|
+
|
|
276
|
+
{#snippet doneFooter()}
|
|
277
|
+
<Button type="button" size="sm" intent="primary" onclick={() => shell?.close()}>
|
|
278
|
+
{t("done")}
|
|
279
|
+
</Button>
|
|
280
|
+
{/snippet}
|
|
281
|
+
|
|
282
|
+
<FieldDateShell
|
|
283
|
+
bind:this={shell}
|
|
284
|
+
bind:input
|
|
285
|
+
{id}
|
|
286
|
+
{label}
|
|
287
|
+
{description}
|
|
288
|
+
{labelAfter}
|
|
289
|
+
{below}
|
|
290
|
+
class={classProp}
|
|
291
|
+
{style}
|
|
292
|
+
{renderSize}
|
|
293
|
+
{required}
|
|
294
|
+
{disabled}
|
|
295
|
+
{tabindex}
|
|
296
|
+
{labelLeft}
|
|
297
|
+
{labelLeftWidth}
|
|
298
|
+
{labelLeftBreakpoint}
|
|
299
|
+
{classLabel}
|
|
300
|
+
{classLabelBox}
|
|
301
|
+
{classInputBox}
|
|
302
|
+
{classInputBoxWrap}
|
|
303
|
+
{classInputBoxWrapInvalid}
|
|
304
|
+
{classDescBox}
|
|
305
|
+
{classDescBoxToggle}
|
|
306
|
+
{classBelowBox}
|
|
307
|
+
{classValidationBox}
|
|
308
|
+
{display}
|
|
309
|
+
placeholder={placeholder ?? t("placeholder_date")}
|
|
310
|
+
{clearable}
|
|
311
|
+
clearLabel={t("clear_value")}
|
|
312
|
+
{embedded}
|
|
313
|
+
inputs={[{ name, value: submitValue }]}
|
|
314
|
+
validate={validateOpts}
|
|
315
|
+
dialogTitle={t("dialog_title_date")}
|
|
316
|
+
closeLabel={t("close")}
|
|
317
|
+
{noScrollLock}
|
|
318
|
+
{classInput}
|
|
319
|
+
{classDialog}
|
|
320
|
+
onClear={() => commit(null)}
|
|
321
|
+
>
|
|
322
|
+
<Calendar
|
|
323
|
+
mode="single"
|
|
324
|
+
value={normalized}
|
|
325
|
+
{months}
|
|
326
|
+
{min}
|
|
327
|
+
{max}
|
|
328
|
+
{isDateDisabled}
|
|
329
|
+
{weekStartsOn}
|
|
330
|
+
{weekendDays}
|
|
331
|
+
{zone}
|
|
332
|
+
{locale}
|
|
333
|
+
{captionLayout}
|
|
334
|
+
{yearRange}
|
|
335
|
+
{showWeekNumbers}
|
|
336
|
+
{showOutsideDays}
|
|
337
|
+
{fixedWeeks}
|
|
338
|
+
{showToday}
|
|
339
|
+
showClear={embedded && clearable}
|
|
340
|
+
{renderDay}
|
|
341
|
+
{disabled}
|
|
342
|
+
{t}
|
|
343
|
+
focusOnMount={!embedded}
|
|
344
|
+
aria-labelledby={embedded && label ? `${id}-label` : undefined}
|
|
345
|
+
footer={!embedded && !closeOnSelect ? doneFooter : undefined}
|
|
346
|
+
{onSelect}
|
|
347
|
+
{...calendarProps}
|
|
348
|
+
/>
|
|
349
|
+
</FieldDateShell>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { ValidateOptions } from "../../actions/validate.svelte.js";
|
|
3
|
+
import type { TranslateFn } from "../../types.js";
|
|
4
|
+
import type { THC } from "../Thc/Thc.svelte";
|
|
5
|
+
import type { Props as CalendarProps } from "../Calendar/Calendar.svelte";
|
|
6
|
+
import type { IsoDate } from "../Calendar/iso-date.js";
|
|
7
|
+
import type { InputWrapClassProps } from "./types.js";
|
|
8
|
+
type SnippetWithId = Snippet<[{
|
|
9
|
+
id: string;
|
|
10
|
+
}]>;
|
|
11
|
+
/** The `Calendar` props a date field lifts to its own top level. */
|
|
12
|
+
export type FieldDateCalendarProps = Pick<CalendarProps, "min" | "max" | "isDateDisabled" | "weekStartsOn" | "weekendDays" | "zone" | "captionLayout" | "yearRange" | "showWeekNumbers" | "showOutsideDays" | "fixedWeeks" | "showToday" | "renderDay">;
|
|
13
|
+
export interface Props extends InputWrapClassProps, FieldDateCalendarProps {
|
|
14
|
+
/** The selected date as `YYYY-MM-DD` (bindable). `null` / `""` = empty. */
|
|
15
|
+
value?: IsoDate | null;
|
|
16
|
+
/** Form field name — applied to the hidden input carrying the ISO date. */
|
|
17
|
+
name?: string;
|
|
18
|
+
/** Render the calendar inline inside the field instead of a trigger + dialog. */
|
|
19
|
+
embedded?: boolean;
|
|
20
|
+
/** Close the dialog right after a date is picked. Default `true`. */
|
|
21
|
+
closeOnSelect?: boolean;
|
|
22
|
+
/** Trigger text while empty. Defaults to `t("placeholder_date")`. */
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
/** Offer clearing (trailing × on the trigger / "Clear" in the embedded footer). Default `true`. */
|
|
25
|
+
clearable?: boolean;
|
|
26
|
+
/** BCP 47 locale of the display text and the calendar names. Browser default when omitted. */
|
|
27
|
+
locale?: string;
|
|
28
|
+
/** `Intl.DateTimeFormat` options of the display text. Default `{ dateStyle: "medium" }`. */
|
|
29
|
+
formatOptions?: Intl.DateTimeFormatOptions;
|
|
30
|
+
/** Custom display formatter (wins over `formatOptions`). */
|
|
31
|
+
format?: (iso: IsoDate) => string;
|
|
32
|
+
/** Months the calendar shows side by side. Default `1`. */
|
|
33
|
+
months?: number;
|
|
34
|
+
/** Anything else for the inner `Calendar` (class hooks, `view`, …). Applied last. */
|
|
35
|
+
calendarProps?: Partial<CalendarProps>;
|
|
36
|
+
/** The hidden input (bindable). */
|
|
37
|
+
input?: HTMLInputElement;
|
|
38
|
+
label?: SnippetWithId | THC;
|
|
39
|
+
description?: SnippetWithId | THC;
|
|
40
|
+
labelAfter?: SnippetWithId | THC;
|
|
41
|
+
below?: SnippetWithId | THC;
|
|
42
|
+
class?: string;
|
|
43
|
+
id?: string;
|
|
44
|
+
tabindex?: number;
|
|
45
|
+
renderSize?: "sm" | "md" | "lg" | string;
|
|
46
|
+
required?: boolean;
|
|
47
|
+
disabled?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Validation. `false` disables the built-in guard (required / valid ISO / min /
|
|
50
|
+
* max / disabled dates); an options object is merged with it (your
|
|
51
|
+
* `customValidator` runs after the built-in checks pass). Default: the guard.
|
|
52
|
+
*/
|
|
53
|
+
validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
|
|
54
|
+
labelLeft?: boolean;
|
|
55
|
+
labelLeftWidth?: "normal" | "wide";
|
|
56
|
+
labelLeftBreakpoint?: number;
|
|
57
|
+
/** Classes for the trigger button */
|
|
58
|
+
classInput?: string;
|
|
59
|
+
/** Classes for the dialog box */
|
|
60
|
+
classDialog?: string;
|
|
61
|
+
style?: string;
|
|
62
|
+
noScrollLock?: boolean;
|
|
63
|
+
/** i18n — see `createCalendarT`. */
|
|
64
|
+
t?: TranslateFn;
|
|
65
|
+
/** Called after the value changed through the picker or the × (not on external writes). */
|
|
66
|
+
onChange?: (value: IsoDate | null) => void;
|
|
67
|
+
}
|
|
68
|
+
import type { ValidationResult } from "../../actions/validate.svelte.js";
|
|
69
|
+
declare const FieldDate: import("svelte").Component<Props, {
|
|
70
|
+
validate: () => ValidationResult | undefined;
|
|
71
|
+
clearValidation: () => void;
|
|
72
|
+
getValidation: () => ValidationResult | undefined;
|
|
73
|
+
focus: () => void;
|
|
74
|
+
scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
|
|
75
|
+
open: () => void;
|
|
76
|
+
close: () => void;
|
|
77
|
+
}, "value" | "input">;
|
|
78
|
+
type FieldDate = ReturnType<typeof FieldDate>;
|
|
79
|
+
export default FieldDate;
|