@human-kit/svelte-components 1.0.0-alpha.3 → 1.0.0-alpha.4
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/dist/FOCUS_STATE_CONTRACT.md +12 -0
- package/dist/calendar/body-cell/README.md +15 -0
- package/dist/calendar/grid/README.md +13 -0
- package/dist/calendar/grid-body/README.md +13 -0
- package/dist/calendar/grid-header/README.md +13 -0
- package/dist/calendar/header-cell/README.md +14 -0
- package/dist/calendar/heading/README.md +13 -0
- package/dist/calendar/root/README.md +24 -0
- package/dist/calendar/trigger-next/README.md +14 -0
- package/dist/calendar/trigger-previous/README.md +14 -0
- package/dist/clock/README.md +75 -0
- package/dist/clock/axis/README.md +24 -0
- package/dist/clock/axis/clock-axis.svelte +37 -0
- package/dist/clock/axis/clock-axis.svelte.d.ts +8 -0
- package/dist/clock/hooks/use-wheel-scroll.svelte.d.ts +16 -0
- package/dist/clock/hooks/use-wheel-scroll.svelte.js +336 -0
- package/dist/clock/index.d.ts +10 -0
- package/dist/clock/index.js +10 -0
- package/dist/clock/index.parts.d.ts +4 -0
- package/dist/clock/index.parts.js +4 -0
- package/dist/clock/root/README.md +38 -0
- package/dist/clock/root/clock-root-test.svelte +62 -0
- package/dist/clock/root/clock-root-test.svelte.d.ts +14 -0
- package/dist/clock/root/clock-root.svelte +329 -0
- package/dist/clock/root/clock-root.svelte.d.ts +25 -0
- package/dist/clock/root/context.d.ts +22 -0
- package/dist/clock/root/context.js +15 -0
- package/dist/clock/root/resolve-visible-columns.d.ts +7 -0
- package/dist/clock/root/resolve-visible-columns.js +16 -0
- package/dist/clock/root/time-utils.d.ts +48 -0
- package/dist/clock/root/time-utils.js +314 -0
- package/dist/clock/root/wheel-options.d.ts +17 -0
- package/dist/clock/root/wheel-options.js +63 -0
- package/dist/clock/wheel-column/README.md +25 -0
- package/dist/clock/wheel-column/clock-wheel-column-bindable-test.svelte +16 -0
- package/dist/clock/wheel-column/clock-wheel-column-bindable-test.svelte.d.ts +3 -0
- package/dist/clock/wheel-column/clock-wheel-column-custom-snippet-test.svelte +29 -0
- package/dist/clock/wheel-column/clock-wheel-column-custom-snippet-test.svelte.d.ts +6 -0
- package/dist/clock/wheel-column/clock-wheel-column-default-height-test.svelte +11 -0
- package/dist/clock/wheel-column/clock-wheel-column-default-height-test.svelte.d.ts +3 -0
- package/dist/clock/wheel-column/clock-wheel-column-test.svelte +38 -0
- package/dist/clock/wheel-column/clock-wheel-column-test.svelte.d.ts +12 -0
- package/dist/clock/wheel-column/clock-wheel-column-tp-test.svelte +38 -0
- package/dist/clock/wheel-column/clock-wheel-column-tp-test.svelte.d.ts +12 -0
- package/dist/clock/wheel-column/clock-wheel-column-untagged-snippet-test.svelte +29 -0
- package/dist/clock/wheel-column/clock-wheel-column-untagged-snippet-test.svelte.d.ts +6 -0
- package/dist/clock/wheel-column/clock-wheel-column.svelte +499 -0
- package/dist/clock/wheel-column/clock-wheel-column.svelte.d.ts +17 -0
- package/dist/clock/wheel-item/README.md +17 -0
- package/dist/clock/wheel-item/clock-wheel-item.svelte +49 -0
- package/dist/clock/wheel-item/clock-wheel-item.svelte.d.ts +17 -0
- package/dist/datepicker/TODO.md +2 -2
- package/dist/datepicker/calendar/README.md +19 -0
- package/dist/datepicker/input/README.md +15 -0
- package/dist/datepicker/popover/README.md +20 -0
- package/dist/datepicker/root/README.md +38 -0
- package/dist/datepicker/segment/README.md +14 -0
- package/dist/datepicker/trigger/README.md +14 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/primitives/focus-trap.js +11 -12
- package/dist/primitives/input-modality.js +10 -1
- package/dist/timepicker/IMPLEMENTATION_PLAN.md +254 -0
- package/dist/timepicker/README.md +97 -0
- package/dist/timepicker/TODO.md +86 -0
- package/dist/timepicker/clock/README.md +14 -0
- package/dist/timepicker/clock/time-picker-clock-test.svelte +45 -0
- package/dist/timepicker/clock/time-picker-clock-test.svelte.d.ts +11 -0
- package/dist/timepicker/clock/time-picker-clock.svelte +65 -0
- package/dist/timepicker/clock/time-picker-clock.svelte.d.ts +10 -0
- package/dist/timepicker/index.d.ts +14 -0
- package/dist/timepicker/index.js +14 -0
- package/dist/timepicker/index.parts.d.ts +8 -0
- package/dist/timepicker/index.parts.js +8 -0
- package/dist/timepicker/input/README.md +15 -0
- package/dist/timepicker/input/time-picker-input-forwarding-test.svelte +40 -0
- package/dist/timepicker/input/time-picker-input-forwarding-test.svelte.d.ts +3 -0
- package/dist/timepicker/input/time-picker-input.svelte +109 -0
- package/dist/timepicker/input/time-picker-input.svelte.d.ts +11 -0
- package/dist/timepicker/internal/strict-props.d.ts +4 -0
- package/dist/timepicker/internal/strict-props.js +51 -0
- package/dist/timepicker/popover/README.md +20 -0
- package/dist/timepicker/popover/time-picker-popover-unsafe-props-test.svelte +22 -0
- package/dist/timepicker/popover/time-picker-popover-unsafe-props-test.svelte.d.ts +3 -0
- package/dist/timepicker/popover/time-picker-popover.svelte +89 -0
- package/dist/timepicker/popover/time-picker-popover.svelte.d.ts +7 -0
- package/dist/timepicker/root/README.md +42 -0
- package/dist/timepicker/root/context.d.ts +51 -0
- package/dist/timepicker/root/context.js +15 -0
- package/dist/timepicker/root/time-picker-12h-test.svelte +22 -0
- package/dist/timepicker/root/time-picker-12h-test.svelte.d.ts +3 -0
- package/dist/timepicker/root/time-picker-bindable-test.svelte +25 -0
- package/dist/timepicker/root/time-picker-bindable-test.svelte.d.ts +3 -0
- package/dist/timepicker/root/time-picker-empty-test.svelte +20 -0
- package/dist/timepicker/root/time-picker-empty-test.svelte.d.ts +3 -0
- package/dist/timepicker/root/time-picker-root.svelte +625 -0
- package/dist/timepicker/root/time-picker-root.svelte.d.ts +28 -0
- package/dist/timepicker/root/time-picker-test.svelte +72 -0
- package/dist/timepicker/root/time-picker-test.svelte.d.ts +15 -0
- package/dist/timepicker/root/time-utils.d.ts +1 -0
- package/dist/timepicker/root/time-utils.js +3 -0
- package/dist/timepicker/segment/README.md +14 -0
- package/dist/timepicker/segment/time-picker-segment.svelte +365 -0
- package/dist/timepicker/segment/time-picker-segment.svelte.d.ts +9 -0
- package/dist/timepicker/trigger/README.md +14 -0
- package/dist/timepicker/trigger/time-picker-trigger-forwarding-test.svelte +35 -0
- package/dist/timepicker/trigger/time-picker-trigger-forwarding-test.svelte.d.ts +3 -0
- package/dist/timepicker/trigger/time-picker-trigger.svelte +122 -0
- package/dist/timepicker/trigger/time-picker-trigger.svelte.d.ts +9 -0
- package/package.json +11 -1
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack, type Snippet } from 'svelte';
|
|
3
|
+
import { useLocaleContextOptional } from '../../locale-provider/context';
|
|
4
|
+
import type { TimePickerOpenChangeDetails, TimePickerOpenChangeReason } from './context';
|
|
5
|
+
import { setTimePickerContext, type TimePickerContext } from './context';
|
|
6
|
+
import { buildWheelOptions } from '../../clock/root/wheel-options';
|
|
7
|
+
import {
|
|
8
|
+
adjustSegmentWithStep,
|
|
9
|
+
buildTimePartsFromDraft,
|
|
10
|
+
buildTimePickerSegments,
|
|
11
|
+
clampToStep,
|
|
12
|
+
createEmptyTimePickerDraft,
|
|
13
|
+
formatTimePickerValue,
|
|
14
|
+
getEditableSegmentOrder,
|
|
15
|
+
getRequiredSegments,
|
|
16
|
+
getSegmentLabel,
|
|
17
|
+
isSegmentValueEmpty,
|
|
18
|
+
isTimeOutOfRange,
|
|
19
|
+
isValidTimePickerValue,
|
|
20
|
+
normalizeSegmentNumberInput,
|
|
21
|
+
toDraftFromTimeValue,
|
|
22
|
+
type TimePickerDraft,
|
|
23
|
+
type TimePickerEditableSegmentType,
|
|
24
|
+
type TimePickerGranularity,
|
|
25
|
+
type TimePickerHourCycle,
|
|
26
|
+
type TimePickerSegmentType,
|
|
27
|
+
type TimePickerTimeValue
|
|
28
|
+
} from './time-utils';
|
|
29
|
+
|
|
30
|
+
type TimePickerRootProps = {
|
|
31
|
+
id?: string;
|
|
32
|
+
value?: TimePickerTimeValue | null;
|
|
33
|
+
defaultValue?: TimePickerTimeValue | null;
|
|
34
|
+
onChange?: (value: TimePickerTimeValue | null) => void;
|
|
35
|
+
minValue?: TimePickerTimeValue;
|
|
36
|
+
maxValue?: TimePickerTimeValue;
|
|
37
|
+
hourCycle?: TimePickerHourCycle;
|
|
38
|
+
granularity?: TimePickerGranularity;
|
|
39
|
+
hourStep?: number;
|
|
40
|
+
minuteStep?: number;
|
|
41
|
+
secondStep?: number;
|
|
42
|
+
isDisabled?: boolean;
|
|
43
|
+
isReadOnly?: boolean;
|
|
44
|
+
isRequired?: boolean;
|
|
45
|
+
open?: boolean;
|
|
46
|
+
defaultOpen?: boolean;
|
|
47
|
+
onOpenChange?: (open: boolean, details: TimePickerOpenChangeDetails) => void;
|
|
48
|
+
children?: Snippet;
|
|
49
|
+
class?: string;
|
|
50
|
+
'aria-label'?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const generatedInstanceId = $props.id();
|
|
54
|
+
|
|
55
|
+
let {
|
|
56
|
+
id,
|
|
57
|
+
value = $bindable(),
|
|
58
|
+
defaultValue,
|
|
59
|
+
onChange,
|
|
60
|
+
minValue,
|
|
61
|
+
maxValue,
|
|
62
|
+
hourCycle,
|
|
63
|
+
granularity = 'minute',
|
|
64
|
+
hourStep = 1,
|
|
65
|
+
minuteStep = 1,
|
|
66
|
+
secondStep = 1,
|
|
67
|
+
isDisabled = false,
|
|
68
|
+
isReadOnly = false,
|
|
69
|
+
isRequired = false,
|
|
70
|
+
open = $bindable(),
|
|
71
|
+
defaultOpen = false,
|
|
72
|
+
onOpenChange,
|
|
73
|
+
children,
|
|
74
|
+
class: className = '',
|
|
75
|
+
'aria-label': ariaLabel
|
|
76
|
+
}: TimePickerRootProps = $props();
|
|
77
|
+
|
|
78
|
+
const instanceId = untrack(() => id) ?? generatedInstanceId;
|
|
79
|
+
const localeContext = useLocaleContextOptional();
|
|
80
|
+
const localeStore = localeContext?.locale;
|
|
81
|
+
const localeFromContext = $derived.by(() => {
|
|
82
|
+
if (!localeStore) return undefined;
|
|
83
|
+
return $localeStore;
|
|
84
|
+
});
|
|
85
|
+
const systemLocale = untrack(() => Intl.DateTimeFormat().resolvedOptions().locale);
|
|
86
|
+
const resolvedLocale = $derived(localeFromContext ?? systemLocale);
|
|
87
|
+
const resolvedHourCycle = $derived.by<TimePickerHourCycle>(() => {
|
|
88
|
+
if (hourCycle) return hourCycle;
|
|
89
|
+
const localeCycle = new Intl.DateTimeFormat(resolvedLocale, {
|
|
90
|
+
hour: 'numeric'
|
|
91
|
+
}).resolvedOptions().hourCycle;
|
|
92
|
+
return localeCycle === 'h11' || localeCycle === 'h12' ? 12 : 24;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
let openInternal = $state((() => defaultOpen)());
|
|
96
|
+
let focusVisible = $state(false);
|
|
97
|
+
let focusWithin = $state(false);
|
|
98
|
+
let activeSegment = $state<Exclude<TimePickerSegmentType, 'literal'> | null>(null);
|
|
99
|
+
let triggerRef: HTMLElement | null = $state(null);
|
|
100
|
+
|
|
101
|
+
const initialValueProp = untrack(() => value);
|
|
102
|
+
const initialDefaultValue = untrack(() =>
|
|
103
|
+
isValidTimePickerValue(defaultValue) ? defaultValue : null
|
|
104
|
+
);
|
|
105
|
+
const initialPropValue =
|
|
106
|
+
initialValueProp === undefined
|
|
107
|
+
? initialDefaultValue
|
|
108
|
+
: isValidTimePickerValue(initialValueProp)
|
|
109
|
+
? initialValueProp
|
|
110
|
+
: null;
|
|
111
|
+
const initialHourCycle = untrack<TimePickerHourCycle>(() => {
|
|
112
|
+
if (hourCycle) return hourCycle;
|
|
113
|
+
const localeCycle = new Intl.DateTimeFormat(resolvedLocale, {
|
|
114
|
+
hour: 'numeric'
|
|
115
|
+
}).resolvedOptions().hourCycle;
|
|
116
|
+
return localeCycle === 'h11' || localeCycle === 'h12' ? 12 : 24;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
let valueInternal = $state<TimePickerTimeValue | null>(initialPropValue);
|
|
120
|
+
let lastPublishedValue = $state<TimePickerTimeValue | null>(initialPropValue);
|
|
121
|
+
let segmentDraft = $state<TimePickerDraft>(
|
|
122
|
+
initialPropValue
|
|
123
|
+
? toDraftFromTimeValue(initialPropValue, initialHourCycle)
|
|
124
|
+
: createEmptyTimePickerDraft()
|
|
125
|
+
);
|
|
126
|
+
let segmentTypeBuffer = $state<TimePickerDraft>(createEmptyTimePickerDraft());
|
|
127
|
+
|
|
128
|
+
const segmentRefs: Record<TimePickerEditableSegmentType, HTMLElement | null> = {
|
|
129
|
+
hour: null,
|
|
130
|
+
minute: null,
|
|
131
|
+
second: null,
|
|
132
|
+
dayPeriod: null
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (initialValueProp === undefined) {
|
|
136
|
+
value = initialPropValue;
|
|
137
|
+
} else if (initialValueProp !== initialPropValue) {
|
|
138
|
+
value = initialPropValue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
$effect(() => {
|
|
142
|
+
if (open !== undefined && open !== openInternal) {
|
|
143
|
+
openInternal = open;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
$effect(() => {
|
|
148
|
+
const nextValue = value === undefined ? null : isValidTimePickerValue(value) ? value : null;
|
|
149
|
+
if (nextValue === lastPublishedValue) return;
|
|
150
|
+
publishCommittedValue(nextValue, false);
|
|
151
|
+
segmentDraft = nextValue
|
|
152
|
+
? toDraftFromTimeValue(nextValue, resolvedHourCycle)
|
|
153
|
+
: createEmptyTimePickerDraft();
|
|
154
|
+
segmentTypeBuffer = createEmptyTimePickerDraft();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const segmentFormatter = $derived.by(
|
|
158
|
+
() =>
|
|
159
|
+
new Intl.DateTimeFormat(resolvedLocale, {
|
|
160
|
+
hour: 'numeric',
|
|
161
|
+
minute: granularity !== 'hour' ? '2-digit' : undefined,
|
|
162
|
+
second: granularity === 'second' ? '2-digit' : undefined,
|
|
163
|
+
hourCycle: resolvedHourCycle === 12 ? 'h12' : 'h23',
|
|
164
|
+
timeZone: 'UTC'
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const segments = $derived.by(() =>
|
|
169
|
+
buildTimePickerSegments({
|
|
170
|
+
locale: resolvedLocale,
|
|
171
|
+
hourCycle: resolvedHourCycle,
|
|
172
|
+
granularity,
|
|
173
|
+
draft: segmentDraft,
|
|
174
|
+
formatter: segmentFormatter
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
const segmentOrder = $derived(getEditableSegmentOrder(segments));
|
|
178
|
+
const requiredSegments = $derived(getRequiredSegments(granularity, resolvedHourCycle));
|
|
179
|
+
|
|
180
|
+
const normalizedMinValue = $derived(isValidTimePickerValue(minValue) ? minValue : undefined);
|
|
181
|
+
const normalizedMaxValue = $derived(isValidTimePickerValue(maxValue) ? maxValue : undefined);
|
|
182
|
+
|
|
183
|
+
const isInvalidDraft = $derived.by(() => {
|
|
184
|
+
const required = requiredSegments;
|
|
185
|
+
const hasAnyValue = required.some((type) => !isSegmentValueEmpty(getSegmentValue(type)));
|
|
186
|
+
if (!hasAnyValue) return false;
|
|
187
|
+
const parts = buildTimePartsFromDraft(segmentDraft, granularity, resolvedHourCycle);
|
|
188
|
+
if (!parts) return true;
|
|
189
|
+
const candidate = formatTimePickerValue(parts, granularity);
|
|
190
|
+
return isTimeOutOfRange(candidate, normalizedMinValue, normalizedMaxValue, granularity);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
function publishCommittedValue(
|
|
194
|
+
nextValue: TimePickerTimeValue | null,
|
|
195
|
+
emitChange: boolean
|
|
196
|
+
): boolean {
|
|
197
|
+
const bindableValue = value === undefined ? valueInternal : value;
|
|
198
|
+
const normalizedBindableValue = isValidTimePickerValue(bindableValue) ? bindableValue : null;
|
|
199
|
+
const didInternalChange = valueInternal !== nextValue;
|
|
200
|
+
const didBindableChange = normalizedBindableValue !== nextValue;
|
|
201
|
+
if (!didInternalChange && !didBindableChange) return false;
|
|
202
|
+
|
|
203
|
+
valueInternal = nextValue;
|
|
204
|
+
lastPublishedValue = nextValue;
|
|
205
|
+
if (didBindableChange) {
|
|
206
|
+
value = nextValue;
|
|
207
|
+
}
|
|
208
|
+
if (emitChange && didInternalChange) {
|
|
209
|
+
onChange?.(nextValue);
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function setOpen(
|
|
215
|
+
nextOpen: boolean,
|
|
216
|
+
details?: TimePickerOpenChangeDetails | { reason?: TimePickerOpenChangeReason; event?: Event }
|
|
217
|
+
) {
|
|
218
|
+
if (openInternal === nextOpen) return;
|
|
219
|
+
let canceled = false;
|
|
220
|
+
const resolvedDetails: TimePickerOpenChangeDetails = {
|
|
221
|
+
reason: details?.reason ?? 'imperative-action',
|
|
222
|
+
event: details?.event,
|
|
223
|
+
cancel: () => {
|
|
224
|
+
canceled = true;
|
|
225
|
+
},
|
|
226
|
+
get isCanceled() {
|
|
227
|
+
return canceled;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
onOpenChange?.(nextOpen, resolvedDetails);
|
|
232
|
+
if (resolvedDetails.isCanceled) return;
|
|
233
|
+
|
|
234
|
+
openInternal = nextOpen;
|
|
235
|
+
open = nextOpen;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function setFocusVisible(visible: boolean) {
|
|
239
|
+
if (focusVisible === visible) return;
|
|
240
|
+
focusVisible = visible;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function setTriggerRef(element: HTMLElement | null) {
|
|
244
|
+
if (triggerRef === element) return;
|
|
245
|
+
triggerRef = element;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function syncFocusWithin() {
|
|
249
|
+
const root = document.getElementById(instanceId);
|
|
250
|
+
const activeElement = document.activeElement;
|
|
251
|
+
const nextWithin = !!root && !!activeElement && root.contains(activeElement);
|
|
252
|
+
if (!nextWithin && focusVisible) {
|
|
253
|
+
focusVisible = false;
|
|
254
|
+
}
|
|
255
|
+
if (!nextWithin && activeSegment !== null) {
|
|
256
|
+
activeSegment = null;
|
|
257
|
+
segmentTypeBuffer = createEmptyTimePickerDraft();
|
|
258
|
+
}
|
|
259
|
+
if (focusWithin === nextWithin) return;
|
|
260
|
+
focusWithin = nextWithin;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function setActiveSegment(segment: Exclude<TimePickerSegmentType, 'literal'> | null) {
|
|
264
|
+
if (activeSegment === segment) return;
|
|
265
|
+
activeSegment = segment;
|
|
266
|
+
segmentTypeBuffer = createEmptyTimePickerDraft();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function getSegmentValue(type: Exclude<TimePickerSegmentType, 'literal'>): string {
|
|
270
|
+
if (type === 'hour') return segmentDraft.hour;
|
|
271
|
+
if (type === 'minute') return segmentDraft.minute;
|
|
272
|
+
if (type === 'second') return segmentDraft.second;
|
|
273
|
+
return segmentDraft.dayPeriod;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function setSegmentValue(type: Exclude<TimePickerSegmentType, 'literal'>, nextValue: string) {
|
|
277
|
+
if (isDisabled || isReadOnly) return;
|
|
278
|
+
if (type === 'dayPeriod') {
|
|
279
|
+
const normalized = nextValue.trim().toUpperCase();
|
|
280
|
+
if (normalized === '' || normalized === 'AM' || normalized === 'PM') {
|
|
281
|
+
segmentDraft.dayPeriod = normalized;
|
|
282
|
+
} else {
|
|
283
|
+
segmentDraft.dayPeriod = '';
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
const maxDigits = 2;
|
|
287
|
+
let normalized = normalizeSegmentNumberInput(nextValue, maxDigits);
|
|
288
|
+
if (normalized.length > 0) {
|
|
289
|
+
const numeric = Number(normalized);
|
|
290
|
+
if (type === 'hour') {
|
|
291
|
+
if (resolvedHourCycle === 12) {
|
|
292
|
+
normalized = String(clampToStep(numeric, Math.max(1, hourStep), 1, 12));
|
|
293
|
+
} else {
|
|
294
|
+
normalized = String(clampToStep(numeric, Math.max(1, hourStep), 0, 23));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (type === 'minute') {
|
|
298
|
+
normalized = String(clampToStep(numeric, Math.max(1, minuteStep), 0, 59));
|
|
299
|
+
}
|
|
300
|
+
if (type === 'second') {
|
|
301
|
+
normalized = String(clampToStep(numeric, Math.max(1, secondStep), 0, 59));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (type === 'hour') segmentDraft.hour = normalized;
|
|
306
|
+
if (type === 'minute') segmentDraft.minute = normalized;
|
|
307
|
+
if (type === 'second') segmentDraft.second = normalized;
|
|
308
|
+
|
|
309
|
+
if (resolvedHourCycle === 12 && isSegmentValueEmpty(segmentDraft.dayPeriod)) {
|
|
310
|
+
segmentDraft.dayPeriod = 'AM';
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
commitFromDraft();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function commitFromDraft() {
|
|
318
|
+
const nextParts = buildTimePartsFromDraft(segmentDraft, granularity, resolvedHourCycle);
|
|
319
|
+
if (!nextParts) {
|
|
320
|
+
publishCommittedValue(null, true);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const candidateValue = formatTimePickerValue(nextParts, granularity);
|
|
325
|
+
if (isTimeOutOfRange(candidateValue, normalizedMinValue, normalizedMaxValue, granularity)) {
|
|
326
|
+
publishCommittedValue(null, true);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
publishCommittedValue(candidateValue, true);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function openPopover(reason: TimePickerOpenChangeReason = 'imperative-action', event?: Event) {
|
|
334
|
+
if (isDisabled || isReadOnly) return;
|
|
335
|
+
setOpen(true, { reason, event });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function closePopover(reason: TimePickerOpenChangeReason = 'imperative-action', event?: Event) {
|
|
339
|
+
setOpen(false, { reason, event });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function togglePopover(reason: TimePickerOpenChangeReason = 'trigger-press', event?: Event) {
|
|
343
|
+
if (isDisabled || isReadOnly) return;
|
|
344
|
+
setOpen(!openInternal, { reason, event });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function getTypingThreshold(
|
|
348
|
+
type: Exclude<TimePickerSegmentType, 'literal' | 'dayPeriod'>
|
|
349
|
+
): number {
|
|
350
|
+
if (type === 'hour') {
|
|
351
|
+
return resolvedHourCycle === 12 ? 2 : 3;
|
|
352
|
+
}
|
|
353
|
+
return 6;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function typeSegmentDigit(
|
|
357
|
+
type: Exclude<TimePickerSegmentType, 'literal'>,
|
|
358
|
+
digit: string
|
|
359
|
+
): boolean {
|
|
360
|
+
if (isDisabled || isReadOnly) return false;
|
|
361
|
+
if (!/^\d$/.test(digit)) return false;
|
|
362
|
+
if (type === 'dayPeriod') return false;
|
|
363
|
+
|
|
364
|
+
const currentBuffer = (segmentTypeBuffer[type] || '').slice(0, 2);
|
|
365
|
+
const seededBuffer = currentBuffer.length >= 2 ? '' : currentBuffer;
|
|
366
|
+
let candidate = `${seededBuffer}${digit}`.slice(0, 2);
|
|
367
|
+
if (!candidate) return false;
|
|
368
|
+
|
|
369
|
+
const threshold = getTypingThreshold(type);
|
|
370
|
+
if (candidate.length === 1) {
|
|
371
|
+
setSegmentValue(type, candidate);
|
|
372
|
+
segmentTypeBuffer[type] = candidate;
|
|
373
|
+
const numeric = Number(candidate);
|
|
374
|
+
if (Number.isFinite(numeric) && numeric >= threshold) {
|
|
375
|
+
segmentTypeBuffer[type] = '';
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const numericCandidate = Number(candidate);
|
|
382
|
+
if (!Number.isFinite(numericCandidate)) {
|
|
383
|
+
segmentTypeBuffer[type] = '';
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
let min = 0;
|
|
388
|
+
let max = 59;
|
|
389
|
+
if (type === 'hour') {
|
|
390
|
+
min = resolvedHourCycle === 12 ? 1 : 0;
|
|
391
|
+
max = resolvedHourCycle === 12 ? 12 : 23;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (numericCandidate < min || numericCandidate > max) {
|
|
395
|
+
candidate = digit;
|
|
396
|
+
setSegmentValue(type, candidate);
|
|
397
|
+
segmentTypeBuffer[type] = candidate;
|
|
398
|
+
const fallbackNumeric = Number(candidate);
|
|
399
|
+
if (fallbackNumeric >= threshold) {
|
|
400
|
+
segmentTypeBuffer[type] = '';
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
setSegmentValue(type, candidate);
|
|
407
|
+
segmentTypeBuffer[type] = '';
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function adjustSegmentValue(type: Exclude<TimePickerSegmentType, 'literal'>, step: number) {
|
|
412
|
+
if (isDisabled || isReadOnly) return;
|
|
413
|
+
if (type === 'dayPeriod') {
|
|
414
|
+
segmentDraft.dayPeriod = segmentDraft.dayPeriod === 'PM' ? 'AM' : 'PM';
|
|
415
|
+
commitFromDraft();
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const current = getSegmentValue(type);
|
|
420
|
+
const next = adjustSegmentWithStep(current, type, step, {
|
|
421
|
+
hourCycle: resolvedHourCycle,
|
|
422
|
+
hourStep,
|
|
423
|
+
minuteStep,
|
|
424
|
+
secondStep
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
setSegmentValue(type, next);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function registerSegmentRef(type: TimePickerEditableSegmentType, element: HTMLElement | null) {
|
|
431
|
+
segmentRefs[type] = element;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function focusNextSegment(type: TimePickerEditableSegmentType): boolean {
|
|
435
|
+
const index = segmentOrder.indexOf(type);
|
|
436
|
+
if (index < 0) return false;
|
|
437
|
+
for (let cursor = index + 1; cursor < segmentOrder.length; cursor += 1) {
|
|
438
|
+
const nextType = segmentOrder[cursor];
|
|
439
|
+
const nextRef = segmentRefs[nextType];
|
|
440
|
+
if (!nextRef || !nextRef.isConnected) continue;
|
|
441
|
+
nextRef.focus();
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function focusPreviousSegment(type: TimePickerEditableSegmentType): boolean {
|
|
448
|
+
const index = segmentOrder.indexOf(type);
|
|
449
|
+
if (index <= 0) return false;
|
|
450
|
+
for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
|
|
451
|
+
const prevType = segmentOrder[cursor];
|
|
452
|
+
const prevRef = segmentRefs[prevType];
|
|
453
|
+
if (!prevRef || !prevRef.isConnected) continue;
|
|
454
|
+
prevRef.focus();
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function focusLastSegment(): boolean {
|
|
461
|
+
for (let cursor = segmentOrder.length - 1; cursor >= 0; cursor -= 1) {
|
|
462
|
+
const segmentType = segmentOrder[cursor];
|
|
463
|
+
const ref = segmentRefs[segmentType];
|
|
464
|
+
if (!ref || !ref.isConnected) continue;
|
|
465
|
+
ref.focus();
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function focusNextPlaceholderOrLastSegment(): boolean {
|
|
472
|
+
for (const segmentType of segmentOrder) {
|
|
473
|
+
const ref = segmentRefs[segmentType];
|
|
474
|
+
if (!ref || !ref.isConnected) continue;
|
|
475
|
+
const currentValue = getSegmentValue(segmentType);
|
|
476
|
+
if (isSegmentValueEmpty(currentValue)) {
|
|
477
|
+
ref.focus();
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return focusLastSegment();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function setValue(nextValue: TimePickerTimeValue | null) {
|
|
485
|
+
if (isDisabled || isReadOnly) return;
|
|
486
|
+
if (nextValue !== null && !isValidTimePickerValue(nextValue)) return;
|
|
487
|
+
if (
|
|
488
|
+
nextValue &&
|
|
489
|
+
isTimeOutOfRange(nextValue, normalizedMinValue, normalizedMaxValue, granularity)
|
|
490
|
+
)
|
|
491
|
+
return;
|
|
492
|
+
|
|
493
|
+
publishCommittedValue(nextValue, true);
|
|
494
|
+
segmentDraft = nextValue
|
|
495
|
+
? toDraftFromTimeValue(nextValue, resolvedHourCycle)
|
|
496
|
+
: createEmptyTimePickerDraft();
|
|
497
|
+
segmentTypeBuffer = createEmptyTimePickerDraft();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function getSegmentLabelByType(type: TimePickerEditableSegmentType): string {
|
|
501
|
+
return getSegmentLabel(type, resolvedLocale);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function selectWheelValue(type: TimePickerEditableSegmentType, optionValue: string) {
|
|
505
|
+
if (isDisabled || isReadOnly) return;
|
|
506
|
+
|
|
507
|
+
if (type === 'dayPeriod') {
|
|
508
|
+
setSegmentValue(type, optionValue.toUpperCase());
|
|
509
|
+
} else {
|
|
510
|
+
setSegmentValue(type, optionValue);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function getSelectedWheelValue(type: TimePickerEditableSegmentType): string | null {
|
|
515
|
+
const selected = getSegmentValue(type);
|
|
516
|
+
return selected.trim().length > 0 ? selected : null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function getWheelOptions(type: TimePickerEditableSegmentType) {
|
|
520
|
+
const hasRangeBounds = normalizedMinValue !== undefined || normalizedMaxValue !== undefined;
|
|
521
|
+
|
|
522
|
+
const getCandidateFromPartial = (
|
|
523
|
+
partial: Partial<TimePickerDraft>
|
|
524
|
+
): TimePickerTimeValue | null => {
|
|
525
|
+
const candidateDraft: TimePickerDraft = {
|
|
526
|
+
hour: partial.hour ?? segmentDraft.hour,
|
|
527
|
+
minute: partial.minute ?? segmentDraft.minute,
|
|
528
|
+
second: partial.second ?? segmentDraft.second,
|
|
529
|
+
dayPeriod: partial.dayPeriod ?? segmentDraft.dayPeriod
|
|
530
|
+
};
|
|
531
|
+
const parts = buildTimePartsFromDraft(candidateDraft, granularity, resolvedHourCycle);
|
|
532
|
+
if (!parts) return null;
|
|
533
|
+
return formatTimePickerValue(parts, granularity);
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
return buildWheelOptions({
|
|
537
|
+
type,
|
|
538
|
+
granularity,
|
|
539
|
+
hourCycle: resolvedHourCycle,
|
|
540
|
+
hourStep,
|
|
541
|
+
minuteStep,
|
|
542
|
+
secondStep,
|
|
543
|
+
hasRangeBounds,
|
|
544
|
+
getCandidateFromPartial,
|
|
545
|
+
isOutOfRange: (candidate) =>
|
|
546
|
+
isTimeOutOfRange(candidate, normalizedMinValue, normalizedMaxValue, granularity)
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const context: TimePickerContext = {
|
|
551
|
+
get id() {
|
|
552
|
+
return instanceId;
|
|
553
|
+
},
|
|
554
|
+
get isDisabled() {
|
|
555
|
+
return isDisabled;
|
|
556
|
+
},
|
|
557
|
+
get isReadOnly() {
|
|
558
|
+
return isReadOnly;
|
|
559
|
+
},
|
|
560
|
+
get isRequired() {
|
|
561
|
+
return isRequired;
|
|
562
|
+
},
|
|
563
|
+
get granularity() {
|
|
564
|
+
return granularity;
|
|
565
|
+
},
|
|
566
|
+
get hourCycle() {
|
|
567
|
+
return resolvedHourCycle;
|
|
568
|
+
},
|
|
569
|
+
get open() {
|
|
570
|
+
return openInternal;
|
|
571
|
+
},
|
|
572
|
+
get focusVisible() {
|
|
573
|
+
return focusVisible;
|
|
574
|
+
},
|
|
575
|
+
get focusWithin() {
|
|
576
|
+
return focusWithin;
|
|
577
|
+
},
|
|
578
|
+
get isInvalidDraft() {
|
|
579
|
+
return isInvalidDraft;
|
|
580
|
+
},
|
|
581
|
+
get value() {
|
|
582
|
+
return valueInternal;
|
|
583
|
+
},
|
|
584
|
+
get locale() {
|
|
585
|
+
return resolvedLocale;
|
|
586
|
+
},
|
|
587
|
+
get triggerRef() {
|
|
588
|
+
return triggerRef;
|
|
589
|
+
},
|
|
590
|
+
get activeSegment() {
|
|
591
|
+
return activeSegment;
|
|
592
|
+
},
|
|
593
|
+
setTriggerRef,
|
|
594
|
+
setFocusVisible,
|
|
595
|
+
syncFocusWithin,
|
|
596
|
+
setActiveSegment,
|
|
597
|
+
openPopover,
|
|
598
|
+
closePopover,
|
|
599
|
+
togglePopover,
|
|
600
|
+
onOpenChange: setOpen,
|
|
601
|
+
setValue,
|
|
602
|
+
getSegments: () => segments,
|
|
603
|
+
getSegmentValue,
|
|
604
|
+
setSegmentValue,
|
|
605
|
+
typeSegmentDigit,
|
|
606
|
+
adjustSegmentValue,
|
|
607
|
+
getSegmentLabel: getSegmentLabelByType,
|
|
608
|
+
registerSegmentRef,
|
|
609
|
+
focusNextPlaceholderOrLastSegment,
|
|
610
|
+
focusNextSegment,
|
|
611
|
+
focusPreviousSegment,
|
|
612
|
+
focusLastSegment,
|
|
613
|
+
selectWheelValue,
|
|
614
|
+
getSelectedWheelValue,
|
|
615
|
+
getWheelOptions
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
setTimePickerContext(context);
|
|
619
|
+
</script>
|
|
620
|
+
|
|
621
|
+
<div id={instanceId} class={className} aria-label={ariaLabel}>
|
|
622
|
+
{#if children}
|
|
623
|
+
{@render children()}
|
|
624
|
+
{/if}
|
|
625
|
+
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import type { TimePickerOpenChangeDetails } from './context';
|
|
3
|
+
import { type TimePickerGranularity, type TimePickerHourCycle, type TimePickerTimeValue } from './time-utils';
|
|
4
|
+
type TimePickerRootProps = {
|
|
5
|
+
id?: string;
|
|
6
|
+
value?: TimePickerTimeValue | null;
|
|
7
|
+
defaultValue?: TimePickerTimeValue | null;
|
|
8
|
+
onChange?: (value: TimePickerTimeValue | null) => void;
|
|
9
|
+
minValue?: TimePickerTimeValue;
|
|
10
|
+
maxValue?: TimePickerTimeValue;
|
|
11
|
+
hourCycle?: TimePickerHourCycle;
|
|
12
|
+
granularity?: TimePickerGranularity;
|
|
13
|
+
hourStep?: number;
|
|
14
|
+
minuteStep?: number;
|
|
15
|
+
secondStep?: number;
|
|
16
|
+
isDisabled?: boolean;
|
|
17
|
+
isReadOnly?: boolean;
|
|
18
|
+
isRequired?: boolean;
|
|
19
|
+
open?: boolean;
|
|
20
|
+
defaultOpen?: boolean;
|
|
21
|
+
onOpenChange?: (open: boolean, details: TimePickerOpenChangeDetails) => void;
|
|
22
|
+
children?: Snippet;
|
|
23
|
+
class?: string;
|
|
24
|
+
'aria-label'?: string;
|
|
25
|
+
};
|
|
26
|
+
declare const TimePickerRoot: import("svelte").Component<TimePickerRootProps, {}, "value" | "open">;
|
|
27
|
+
type TimePickerRoot = ReturnType<typeof TimePickerRoot>;
|
|
28
|
+
export default TimePickerRoot;
|