@human-kit/svelte-components 1.0.0-alpha.2 → 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 +63 -0
- package/dist/FOCUS_STATE_REVIEW_TEMPLATE.md +70 -0
- package/dist/calendar/README.md +2 -1
- package/dist/calendar/TODO.md +21 -107
- package/dist/calendar/body-cell/README.md +15 -0
- package/dist/calendar/body-cell/calendar-body-cell.svelte +116 -41
- 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/root/calendar-root-test.svelte +4 -0
- package/dist/calendar/root/calendar-root-test.svelte.d.ts +1 -0
- package/dist/calendar/root/calendar-root.svelte +3 -0
- package/dist/calendar/root/calendar-root.svelte.d.ts +1 -0
- package/dist/calendar/root/context.d.ts +4 -0
- package/dist/calendar/root/context.js +28 -25
- package/dist/calendar/root/date-utils.d.ts +1 -1
- package/dist/calendar/root/date-utils.js +16 -26
- 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/combobox/TODO.md +28 -175
- package/dist/combobox/button/combobox-button.svelte +2 -0
- package/dist/combobox/root/combobox.svelte +30 -0
- package/dist/datepicker/README.md +100 -0
- package/dist/datepicker/TODO.md +28 -0
- package/dist/datepicker/calendar/README.md +19 -0
- package/dist/datepicker/calendar/date-picker-calendar-unsafe-props-test.svelte +60 -0
- package/dist/datepicker/calendar/date-picker-calendar-unsafe-props-test.svelte.d.ts +3 -0
- package/dist/datepicker/calendar/date-picker-calendar.svelte +65 -0
- package/dist/datepicker/calendar/date-picker-calendar.svelte.d.ts +10 -0
- package/dist/datepicker/index.d.ts +18 -0
- package/dist/datepicker/index.js +18 -0
- package/dist/datepicker/index.parts.d.ts +14 -0
- package/dist/datepicker/index.parts.js +14 -0
- package/dist/datepicker/input/README.md +15 -0
- package/dist/datepicker/input/date-picker-input.svelte +108 -0
- package/dist/datepicker/input/date-picker-input.svelte.d.ts +11 -0
- package/dist/datepicker/internal/strict-props.d.ts +2 -0
- package/dist/datepicker/internal/strict-props.js +28 -0
- package/dist/datepicker/popover/README.md +20 -0
- package/dist/datepicker/popover/date-picker-popover-handler-test.svelte +57 -0
- package/dist/datepicker/popover/date-picker-popover-handler-test.svelte.d.ts +3 -0
- package/dist/datepicker/popover/date-picker-popover-unsafe-props-test.svelte +45 -0
- package/dist/datepicker/popover/date-picker-popover-unsafe-props-test.svelte.d.ts +18 -0
- package/dist/datepicker/popover/date-picker-popover.svelte +87 -0
- package/dist/datepicker/popover/date-picker-popover.svelte.d.ts +7 -0
- package/dist/datepicker/root/README.md +38 -0
- package/dist/datepicker/root/context.d.ts +43 -0
- package/dist/datepicker/root/context.js +15 -0
- package/dist/datepicker/root/date-picker-bindable-empty-test.svelte +24 -0
- package/dist/datepicker/root/date-picker-bindable-empty-test.svelte.d.ts +3 -0
- package/dist/datepicker/root/date-picker-bindable-test.svelte +41 -0
- package/dist/datepicker/root/date-picker-bindable-test.svelte.d.ts +3 -0
- package/dist/datepicker/root/date-picker-empty-test.svelte +47 -0
- package/dist/datepicker/root/date-picker-empty-test.svelte.d.ts +3 -0
- package/dist/datepicker/root/date-picker-locale-typing-test.svelte +47 -0
- package/dist/datepicker/root/date-picker-locale-typing-test.svelte.d.ts +3 -0
- package/dist/datepicker/root/date-picker-open-cancel-test.svelte +54 -0
- package/dist/datepicker/root/date-picker-open-cancel-test.svelte.d.ts +8 -0
- package/dist/datepicker/root/date-picker-root.svelte +495 -0
- package/dist/datepicker/root/date-picker-root.svelte.d.ts +24 -0
- package/dist/datepicker/root/date-picker-test.svelte +86 -0
- package/dist/datepicker/root/date-picker-test.svelte.d.ts +13 -0
- package/dist/datepicker/root/date-utils.d.ts +17 -0
- package/dist/datepicker/root/date-utils.js +138 -0
- package/dist/datepicker/root/draft-evaluation.d.ts +13 -0
- package/dist/datepicker/root/draft-evaluation.js +56 -0
- package/dist/datepicker/root/focus-controller.d.ts +3 -0
- package/dist/datepicker/root/focus-controller.js +15 -0
- package/dist/datepicker/root/open-change.d.ts +5 -0
- package/dist/datepicker/root/open-change.js +13 -0
- package/dist/datepicker/root/open-controller.d.ts +7 -0
- package/dist/datepicker/root/open-controller.js +15 -0
- package/dist/datepicker/root/segment-controller.d.ts +8 -0
- package/dist/datepicker/root/segment-controller.js +53 -0
- package/dist/datepicker/root/segment-state.d.ts +18 -0
- package/dist/datepicker/root/segment-state.js +134 -0
- package/dist/datepicker/root/value-commit.d.ts +4 -0
- package/dist/datepicker/root/value-commit.js +8 -0
- package/dist/datepicker/segment/README.md +14 -0
- package/dist/datepicker/segment/date-picker-segment.svelte +319 -0
- package/dist/datepicker/segment/date-picker-segment.svelte.d.ts +9 -0
- package/dist/datepicker/trigger/README.md +14 -0
- package/dist/datepicker/trigger/date-picker-trigger.svelte +110 -0
- package/dist/datepicker/trigger/date-picker-trigger.svelte.d.ts +9 -0
- package/dist/dialog/content/dialog-content.svelte +6 -6
- package/dist/dialog/root/context.d.ts +2 -1
- package/dist/dialog/root/dialog-root.svelte +9 -2
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/listbox/root/listbox.svelte +44 -0
- package/dist/popover/README.md +10 -0
- package/dist/popover/content/popover-content-standalone-test.svelte +28 -0
- package/dist/popover/content/popover-content-standalone-test.svelte.d.ts +6 -0
- package/dist/popover/content/popover-content-test.svelte +2 -1
- package/dist/popover/content/popover-content-test.svelte.d.ts +2 -1
- package/dist/popover/content/popover-content.svelte +91 -18
- package/dist/popover/content/popover-content.svelte.d.ts +5 -1
- package/dist/popover/index.d.ts +1 -1
- package/dist/popover/index.js +1 -3
- package/dist/popover/root/README.md +10 -15
- package/dist/popover/root/context.d.ts +16 -7
- package/dist/popover/root/context.js +0 -2
- package/dist/popover/root/focus-state.d.ts +4 -0
- package/dist/popover/root/focus-state.js +33 -0
- package/dist/popover/root/popover-root.svelte +90 -17
- package/dist/popover/root/popover-root.svelte.d.ts +2 -1
- package/dist/popover/root/popover-test.svelte +2 -1
- package/dist/popover/root/popover-test.svelte.d.ts +2 -1
- package/dist/popover/trigger/popover-trigger-button.svelte +4 -4
- package/dist/popover/trigger/popover-trigger.svelte +1 -1
- package/dist/portal/portal.svelte +3 -1
- package/dist/primitives/click-outside.d.ts +1 -1
- package/dist/primitives/click-outside.js +1 -1
- package/dist/primitives/focus-trap.d.ts +7 -2
- package/dist/primitives/focus-trap.js +50 -17
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/input-modality.d.ts +7 -0
- package/dist/primitives/input-modality.js +125 -0
- package/dist/test-utils/focus-contract.d.ts +3 -0
- package/dist/test-utils/focus-contract.js +26 -0
- 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/dist/utils/date-only.d.ts +11 -0
- package/dist/utils/date-only.js +53 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +16 -1
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import { useClockContext, type ClockEditableSegmentType } from '../root/context';
|
|
5
|
+
import TimePickerWheelItem from '../wheel-item/clock-wheel-item.svelte';
|
|
6
|
+
import { useWheelScroll, type WheelScrollBehavior } from '../hooks/use-wheel-scroll.svelte';
|
|
7
|
+
import {
|
|
8
|
+
focusWithModality,
|
|
9
|
+
shouldShowFocusVisible,
|
|
10
|
+
trackInteractionModality
|
|
11
|
+
} from '../../primitives/input-modality';
|
|
12
|
+
|
|
13
|
+
type TimePickerWheelOption = {
|
|
14
|
+
value: string;
|
|
15
|
+
label: string;
|
|
16
|
+
disabled: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type TimePickerWheelColumnProps = Omit<
|
|
20
|
+
HTMLAttributes<HTMLDivElement>,
|
|
21
|
+
'children' | 'class' | 'role' | 'tabindex' | 'aria-label' | 'onkeydown'
|
|
22
|
+
> & {
|
|
23
|
+
type: ClockEditableSegmentType;
|
|
24
|
+
children?: Snippet<[TimePickerWheelOption]>;
|
|
25
|
+
class?: string;
|
|
26
|
+
'aria-label'?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
type,
|
|
31
|
+
children,
|
|
32
|
+
class: className = '',
|
|
33
|
+
'aria-label': ariaLabel,
|
|
34
|
+
...restProps
|
|
35
|
+
}: TimePickerWheelColumnProps = $props();
|
|
36
|
+
|
|
37
|
+
function hasExplicitHeightClass(value: string): boolean {
|
|
38
|
+
return /(^|\s)(?:[\w-]+:)*(?:h-|min-h-|max-h-|size-)/.test(value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const resolvedClassName = $derived.by(() => {
|
|
42
|
+
const trimmed = className.trim();
|
|
43
|
+
if (hasExplicitHeightClass(trimmed)) return trimmed;
|
|
44
|
+
return trimmed.length > 0 ? `${trimmed} h-55` : 'h-55';
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const clock = useClockContext();
|
|
48
|
+
const options = $derived.by(() => clock.getWheelOptions(type));
|
|
49
|
+
const selectedValue = $derived(clock.getSelectedWheelValue(type));
|
|
50
|
+
const label = $derived(
|
|
51
|
+
ariaLabel ?? (type === 'dayPeriod' ? 'Day period' : clock.getSegmentLabel(type))
|
|
52
|
+
);
|
|
53
|
+
const wheelRoleDescription = $derived.by(() => {
|
|
54
|
+
const normalizedLocale = clock.locale.toLowerCase();
|
|
55
|
+
if (normalizedLocale.startsWith('es')) return 'selector en rueda';
|
|
56
|
+
if (normalizedLocale.startsWith('pt')) return 'seletor em roda';
|
|
57
|
+
return 'wheel picker';
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let wheelRef: HTMLElement | null = null;
|
|
61
|
+
let wheelApi: ReturnType<typeof useWheelScroll> | null = null;
|
|
62
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
63
|
+
let resizeFrameId: number | null = null;
|
|
64
|
+
let itemHeight = $state(0);
|
|
65
|
+
let spacerHeight = $state(0);
|
|
66
|
+
let focusWithin = $state(false);
|
|
67
|
+
let focusVisible = $state(false);
|
|
68
|
+
let lastCenteredIndex = -1;
|
|
69
|
+
let didAlignForCurrentOpen = false;
|
|
70
|
+
let lastOpenOptionsSignature = '';
|
|
71
|
+
|
|
72
|
+
function getWheelItemElements(): HTMLElement[] {
|
|
73
|
+
const taggedItems = Array.from(
|
|
74
|
+
wheelRef?.querySelectorAll<HTMLElement>('[data-wheel-item]') ?? []
|
|
75
|
+
);
|
|
76
|
+
if (taggedItems.length > 0) return taggedItems;
|
|
77
|
+
if (!wheelRef) return [];
|
|
78
|
+
return Array.from(wheelRef.children).filter((child): child is HTMLElement => {
|
|
79
|
+
if (!(child instanceof HTMLElement)) return false;
|
|
80
|
+
if (child.matches('[data-wheel-spacer], [data-wheel-highlight], [role="status"], .sr-only')) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const enabledOptionIndexes = $derived.by(() => {
|
|
88
|
+
const indexes: number[] = [];
|
|
89
|
+
for (let index = 0; index < options.length; index += 1) {
|
|
90
|
+
if (!options[index].disabled) {
|
|
91
|
+
indexes.push(index);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return indexes;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const selectedIndex = $derived.by(() => {
|
|
98
|
+
if (selectedValue === null) return -1;
|
|
99
|
+
return options.findIndex((option) => option.value === selectedValue);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const selectedOption = $derived.by(() => {
|
|
103
|
+
if (selectedIndex < 0) return null;
|
|
104
|
+
return options[selectedIndex] ?? null;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const minValue = $derived.by(() => {
|
|
108
|
+
const firstEnabledIndex = enabledOptionIndexes[0];
|
|
109
|
+
if (firstEnabledIndex === undefined) return undefined;
|
|
110
|
+
return getAriaValueNow(options[firstEnabledIndex]?.value ?? null);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const maxValue = $derived.by(() => {
|
|
114
|
+
const lastEnabledIndex = enabledOptionIndexes[enabledOptionIndexes.length - 1];
|
|
115
|
+
if (lastEnabledIndex === undefined) return undefined;
|
|
116
|
+
return getAriaValueNow(options[lastEnabledIndex]?.value ?? null);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const valueNow = $derived.by(() => getAriaValueNow(selectedOption?.value ?? null));
|
|
120
|
+
const valueText = $derived.by(() => selectedOption?.label ?? undefined);
|
|
121
|
+
|
|
122
|
+
function getAriaValueNow(value: string | null): number | undefined {
|
|
123
|
+
if (!value) return undefined;
|
|
124
|
+
if (type === 'dayPeriod') {
|
|
125
|
+
const normalized = value.toUpperCase();
|
|
126
|
+
return normalized === 'PM' ? 1 : 0;
|
|
127
|
+
}
|
|
128
|
+
const numeric = Number(value);
|
|
129
|
+
if (!Number.isFinite(numeric)) return undefined;
|
|
130
|
+
return numeric;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function syncMeasurements() {
|
|
134
|
+
if (!wheelRef) return;
|
|
135
|
+
const firstItem = getWheelItemElements()[0] ?? null;
|
|
136
|
+
if (!firstItem) return;
|
|
137
|
+
|
|
138
|
+
const nextItemHeight = firstItem.offsetHeight;
|
|
139
|
+
if (nextItemHeight <= 0) return;
|
|
140
|
+
itemHeight = nextItemHeight;
|
|
141
|
+
spacerHeight = Math.max(0, Math.floor((wheelRef.clientHeight - nextItemHeight) / 2));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function mountWheelApi() {
|
|
145
|
+
if (!wheelRef) return;
|
|
146
|
+
wheelApi?.destroy();
|
|
147
|
+
wheelApi = useWheelScroll(wheelRef, handleSnapToIndex);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function destroyWheelApi() {
|
|
151
|
+
wheelApi?.destroy();
|
|
152
|
+
wheelApi = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function findClosestEnabledIndex(fromIndex: number, direction: 1 | -1): number {
|
|
156
|
+
if (enabledOptionIndexes.length === 0) return -1;
|
|
157
|
+
|
|
158
|
+
for (
|
|
159
|
+
let index = Math.min(options.length - 1, Math.max(0, fromIndex));
|
|
160
|
+
index >= 0 && index < options.length;
|
|
161
|
+
index += direction
|
|
162
|
+
) {
|
|
163
|
+
if (!options[index]?.disabled) {
|
|
164
|
+
return index;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (
|
|
169
|
+
let index = Math.min(options.length - 1, Math.max(0, fromIndex));
|
|
170
|
+
index >= 0 && index < options.length;
|
|
171
|
+
index += direction * -1
|
|
172
|
+
) {
|
|
173
|
+
if (!options[index]?.disabled) {
|
|
174
|
+
return index;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return -1;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function handleSnapToIndex(nextIndex: number): number | null {
|
|
182
|
+
if (nextIndex < 0 || nextIndex >= options.length) return null;
|
|
183
|
+
const option = options[nextIndex];
|
|
184
|
+
if (!option) return null;
|
|
185
|
+
|
|
186
|
+
const direction: 1 | -1 = lastCenteredIndex >= 0 && nextIndex < lastCenteredIndex ? -1 : 1;
|
|
187
|
+
|
|
188
|
+
if (option.disabled) {
|
|
189
|
+
const fallbackIndex = findClosestEnabledIndex(nextIndex, direction);
|
|
190
|
+
if (fallbackIndex >= 0 && fallbackIndex !== nextIndex) {
|
|
191
|
+
lastCenteredIndex = fallbackIndex;
|
|
192
|
+
return fallbackIndex;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
lastCenteredIndex = nextIndex;
|
|
198
|
+
|
|
199
|
+
if (selectedValue !== option.value) {
|
|
200
|
+
clock.selectWheelValue(type, option.value);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return nextIndex;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function scrollToSelected(behavior: 'smooth' | 'instant') {
|
|
207
|
+
if (!wheelApi) return;
|
|
208
|
+
if (options.length === 0) return;
|
|
209
|
+
|
|
210
|
+
const nextIndex =
|
|
211
|
+
selectedIndex >= 0
|
|
212
|
+
? selectedIndex
|
|
213
|
+
: enabledOptionIndexes[0] !== undefined
|
|
214
|
+
? enabledOptionIndexes[0]
|
|
215
|
+
: 0;
|
|
216
|
+
|
|
217
|
+
if (nextIndex < 0) return;
|
|
218
|
+
lastCenteredIndex = nextIndex;
|
|
219
|
+
wheelApi.scrollToIndex(nextIndex, behavior);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function syncFocusWithin(currentTarget: HTMLElement) {
|
|
223
|
+
focusWithin =
|
|
224
|
+
!!document.activeElement &&
|
|
225
|
+
(currentTarget === document.activeElement || currentTarget.contains(document.activeElement));
|
|
226
|
+
if (!focusWithin) {
|
|
227
|
+
focusVisible = false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function focusSiblingColumn(direction: 1 | -1): boolean {
|
|
232
|
+
const panel = wheelRef?.closest<HTMLElement>('[data-clock="true"]');
|
|
233
|
+
if (!panel || !wheelRef) return false;
|
|
234
|
+
const columns = Array.from(panel.querySelectorAll<HTMLElement>('[role="spinbutton"]'));
|
|
235
|
+
const currentIndex = columns.findIndex((column) => column === wheelRef);
|
|
236
|
+
if (currentIndex < 0) return false;
|
|
237
|
+
|
|
238
|
+
const nextIndex = currentIndex + direction;
|
|
239
|
+
if (nextIndex < 0 || nextIndex >= columns.length) return false;
|
|
240
|
+
|
|
241
|
+
const nextColumn = columns[nextIndex];
|
|
242
|
+
focusWithModality(nextColumn, 'keyboard');
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function moveBy(step: number, behavior: WheelScrollBehavior = 'smooth') {
|
|
247
|
+
if (options.length === 0) return;
|
|
248
|
+
// Use lastCenteredIndex so rapid key-repeat steps correctly
|
|
249
|
+
// instead of re-anchoring to the (potentially stale) reactive value.
|
|
250
|
+
const anchor =
|
|
251
|
+
lastCenteredIndex >= 0
|
|
252
|
+
? lastCenteredIndex
|
|
253
|
+
: selectedIndex >= 0
|
|
254
|
+
? selectedIndex
|
|
255
|
+
: (enabledOptionIndexes[0] ?? 0);
|
|
256
|
+
const target = findClosestEnabledIndex(anchor + step, step < 0 ? -1 : 1);
|
|
257
|
+
if (target < 0) return;
|
|
258
|
+
lastCenteredIndex = target;
|
|
259
|
+
|
|
260
|
+
// Immediately update value so the UI reacts without waiting for scrollend.
|
|
261
|
+
const option = options[target];
|
|
262
|
+
if (option && !option.disabled && selectedValue !== option.value) {
|
|
263
|
+
clock.selectWheelValue(type, option.value);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
wheelApi?.scrollToIndex(target, behavior);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function moveToBoundary(boundary: 'start' | 'end', behavior: WheelScrollBehavior = 'smooth') {
|
|
270
|
+
if (enabledOptionIndexes.length === 0) return;
|
|
271
|
+
const target =
|
|
272
|
+
boundary === 'start'
|
|
273
|
+
? enabledOptionIndexes[0]
|
|
274
|
+
: enabledOptionIndexes[enabledOptionIndexes.length - 1];
|
|
275
|
+
lastCenteredIndex = target;
|
|
276
|
+
|
|
277
|
+
const option = options[target];
|
|
278
|
+
if (option && !option.disabled && selectedValue !== option.value) {
|
|
279
|
+
clock.selectWheelValue(type, option.value);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
wheelApi?.scrollToIndex(target, behavior);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function handleFocusIn(event: FocusEvent) {
|
|
286
|
+
focusWithin = true;
|
|
287
|
+
focusVisible = shouldShowFocusVisible(event.target as HTMLElement | null);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function handleFocusOut(event: FocusEvent) {
|
|
291
|
+
const currentTarget = event.currentTarget as HTMLElement;
|
|
292
|
+
queueMicrotask(() => syncFocusWithin(currentTarget));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function handleMouseDown(event: MouseEvent) {
|
|
296
|
+
trackInteractionModality(event, event.target as HTMLElement | null);
|
|
297
|
+
focusVisible = false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function handleClick(event: MouseEvent) {
|
|
301
|
+
const target = event.target as Node | null;
|
|
302
|
+
if (!target) return;
|
|
303
|
+
const items = getWheelItemElements();
|
|
304
|
+
const index = items.findIndex((item) => item === target || item.contains(target));
|
|
305
|
+
if (index >= 0) {
|
|
306
|
+
handleCenterRequest(index);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
311
|
+
trackInteractionModality(event, event.target as HTMLElement | null);
|
|
312
|
+
if (focusWithin) {
|
|
313
|
+
focusVisible = true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// When a key is held down (repeat), use instant scrolling so the column
|
|
317
|
+
// flies through items instead of queuing up slow smooth-scroll animations.
|
|
318
|
+
const kb: WheelScrollBehavior = event.repeat ? 'instant' : 'smooth';
|
|
319
|
+
|
|
320
|
+
if (event.key === 'ArrowDown') {
|
|
321
|
+
event.preventDefault();
|
|
322
|
+
moveBy(1, kb);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (event.key === 'ArrowUp') {
|
|
327
|
+
event.preventDefault();
|
|
328
|
+
moveBy(-1, kb);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (event.key === 'PageDown') {
|
|
333
|
+
event.preventDefault();
|
|
334
|
+
moveBy(type === 'dayPeriod' ? 1 : 5, kb);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (event.key === 'PageUp') {
|
|
339
|
+
event.preventDefault();
|
|
340
|
+
moveBy(type === 'dayPeriod' ? -1 : -5, kb);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (event.key === 'Home') {
|
|
345
|
+
event.preventDefault();
|
|
346
|
+
moveToBoundary('start', kb);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (event.key === 'End') {
|
|
351
|
+
event.preventDefault();
|
|
352
|
+
moveToBoundary('end', kb);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (event.key === 'ArrowRight') {
|
|
357
|
+
event.preventDefault();
|
|
358
|
+
focusSiblingColumn(1);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (event.key === 'ArrowLeft') {
|
|
363
|
+
event.preventDefault();
|
|
364
|
+
focusSiblingColumn(-1);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function handleCenterRequest(index: number) {
|
|
369
|
+
if (index < 0 || index >= options.length) return;
|
|
370
|
+
const option = options[index];
|
|
371
|
+
if (!option || option.disabled) return;
|
|
372
|
+
|
|
373
|
+
lastCenteredIndex = index;
|
|
374
|
+
|
|
375
|
+
if (selectedValue !== option.value) {
|
|
376
|
+
clock.selectWheelValue(type, option.value);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
wheelApi?.scrollToIndex(index, 'smooth', { silent: true });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
$effect(() => {
|
|
383
|
+
if (!wheelRef) return;
|
|
384
|
+
|
|
385
|
+
mountWheelApi();
|
|
386
|
+
syncMeasurements();
|
|
387
|
+
|
|
388
|
+
resizeObserver?.disconnect();
|
|
389
|
+
resizeObserver = new ResizeObserver(() => {
|
|
390
|
+
if (resizeFrameId !== null) {
|
|
391
|
+
cancelAnimationFrame(resizeFrameId);
|
|
392
|
+
}
|
|
393
|
+
resizeFrameId = requestAnimationFrame(() => {
|
|
394
|
+
resizeFrameId = null;
|
|
395
|
+
syncMeasurements();
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
resizeObserver.observe(wheelRef);
|
|
399
|
+
|
|
400
|
+
return () => {
|
|
401
|
+
if (resizeFrameId !== null) {
|
|
402
|
+
cancelAnimationFrame(resizeFrameId);
|
|
403
|
+
resizeFrameId = null;
|
|
404
|
+
}
|
|
405
|
+
resizeObserver?.disconnect();
|
|
406
|
+
resizeObserver = null;
|
|
407
|
+
destroyWheelApi();
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
$effect(() => {
|
|
412
|
+
if (!clock.open || !wheelApi) return;
|
|
413
|
+
if (selectedIndex < 0) return;
|
|
414
|
+
if (selectedIndex === lastCenteredIndex) return;
|
|
415
|
+
lastCenteredIndex = selectedIndex;
|
|
416
|
+
wheelApi.scrollToIndex(selectedIndex, 'instant');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
$effect(() => {
|
|
420
|
+
const optionsSignature = options
|
|
421
|
+
.map((option) => `${option.value}:${option.disabled ? 1 : 0}`)
|
|
422
|
+
.join('|');
|
|
423
|
+
const nextSignature = `${clock.open ? 1 : 0}::${optionsSignature}`;
|
|
424
|
+
if (lastOpenOptionsSignature !== nextSignature) {
|
|
425
|
+
lastOpenOptionsSignature = nextSignature;
|
|
426
|
+
didAlignForCurrentOpen = false;
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
$effect(() => {
|
|
431
|
+
if (!wheelRef || !wheelApi) return;
|
|
432
|
+
if (!clock.open) {
|
|
433
|
+
didAlignForCurrentOpen = false;
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (didAlignForCurrentOpen) return;
|
|
437
|
+
if (options.length === 0) return;
|
|
438
|
+
// Guard: wait until syncMeasurements() has run (itemHeight > 0).
|
|
439
|
+
// itemHeight is $state, so the effect will re-run once it's set.
|
|
440
|
+
if (itemHeight <= 0) return;
|
|
441
|
+
|
|
442
|
+
didAlignForCurrentOpen = true;
|
|
443
|
+
|
|
444
|
+
// Defer to rAF so the DOM has been painted with the correct spacer
|
|
445
|
+
// heights. Without this, offsetTop calculations are wrong because
|
|
446
|
+
// Svelte batches state→DOM updates and the spacers still have the
|
|
447
|
+
// stale height when effects run synchronously.
|
|
448
|
+
const rafId = requestAnimationFrame(() => {
|
|
449
|
+
syncMeasurements();
|
|
450
|
+
scrollToSelected('instant');
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return () => cancelAnimationFrame(rafId);
|
|
454
|
+
});
|
|
455
|
+
</script>
|
|
456
|
+
|
|
457
|
+
<div
|
|
458
|
+
bind:this={wheelRef}
|
|
459
|
+
role="spinbutton"
|
|
460
|
+
tabindex={clock.isDisabled ? -1 : 0}
|
|
461
|
+
aria-label={label}
|
|
462
|
+
aria-valuemin={minValue}
|
|
463
|
+
aria-valuemax={maxValue}
|
|
464
|
+
aria-valuenow={valueNow}
|
|
465
|
+
aria-valuetext={valueText}
|
|
466
|
+
aria-roledescription={wheelRoleDescription}
|
|
467
|
+
aria-disabled={clock.isDisabled || undefined}
|
|
468
|
+
data-focus-within={focusWithin || undefined}
|
|
469
|
+
data-focus-visible={focusVisible || undefined}
|
|
470
|
+
class={resolvedClassName}
|
|
471
|
+
style="overflow-y:auto;position:relative;-webkit-overflow-scrolling:touch"
|
|
472
|
+
onfocusin={handleFocusIn}
|
|
473
|
+
onfocusout={handleFocusOut}
|
|
474
|
+
onmousedown={handleMouseDown}
|
|
475
|
+
onclick={handleClick}
|
|
476
|
+
onkeydown={handleKeydown}
|
|
477
|
+
{...restProps}
|
|
478
|
+
>
|
|
479
|
+
<div data-wheel-spacer="top" style={`height:${spacerHeight}px`}></div>
|
|
480
|
+
<span role="status" aria-live="polite" class="sr-only">{valueText}</span>
|
|
481
|
+
{#each options as option, index (option.value)}
|
|
482
|
+
{#if children}
|
|
483
|
+
{@render children(option)}
|
|
484
|
+
{:else}
|
|
485
|
+
<TimePickerWheelItem
|
|
486
|
+
{type}
|
|
487
|
+
{option}
|
|
488
|
+
selected={selectedValue === option.value}
|
|
489
|
+
onrequestcenter={() => handleCenterRequest(index)}
|
|
490
|
+
id={`${clock.id}-wheel-${type}-${option.value}`}
|
|
491
|
+
/>
|
|
492
|
+
{/if}
|
|
493
|
+
{/each}
|
|
494
|
+
<div data-wheel-spacer="bottom" style={`height:${spacerHeight}px`}></div>
|
|
495
|
+
<div
|
|
496
|
+
data-wheel-highlight
|
|
497
|
+
style={`position:absolute;top:50%;transform:translateY(-50%);height:${Math.max(itemHeight, 1)}px;width:100%;pointer-events:none`}
|
|
498
|
+
></div>
|
|
499
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { type ClockEditableSegmentType } from '../root/context';
|
|
4
|
+
type TimePickerWheelOption = {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
disabled: boolean;
|
|
8
|
+
};
|
|
9
|
+
type TimePickerWheelColumnProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'class' | 'role' | 'tabindex' | 'aria-label' | 'onkeydown'> & {
|
|
10
|
+
type: ClockEditableSegmentType;
|
|
11
|
+
children?: Snippet<[TimePickerWheelOption]>;
|
|
12
|
+
class?: string;
|
|
13
|
+
'aria-label'?: string;
|
|
14
|
+
};
|
|
15
|
+
declare const ClockWheelColumn: import("svelte").Component<TimePickerWheelColumnProps, {}, "">;
|
|
16
|
+
type ClockWheelColumn = ReturnType<typeof ClockWheelColumn>;
|
|
17
|
+
export default ClockWheelColumn;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Clock WheelItem
|
|
2
|
+
|
|
3
|
+
## API reference
|
|
4
|
+
|
|
5
|
+
### Clock.WheelItem
|
|
6
|
+
|
|
7
|
+
Name: `Clock.WheelItem`
|
|
8
|
+
Description: Headless item renderer for wheel options used by `Clock.WheelColumn` and `TimePicker.WheelColumn`.
|
|
9
|
+
|
|
10
|
+
| Prop | Type | Default | Description |
|
|
11
|
+
| ----------------- | ----------------------------------------------------- | ----------- | ------------------------------------------------------------ |
|
|
12
|
+
| `type` | `'hour' \| 'minute' \| 'second' \| 'dayPeriod'` | `required` | Segment type associated with the option. |
|
|
13
|
+
| `option` | `{ value: string; label: string; disabled: boolean }` | `required` | Option payload rendered by this item. |
|
|
14
|
+
| `selected` | `boolean` | `false` | Marks the item as currently selected. |
|
|
15
|
+
| `onrequestcenter` | `() => void` | `undefined` | Callback invoked to request centering/selecting this option. |
|
|
16
|
+
| `class` | `string` | `''` | CSS class names for the item element. |
|
|
17
|
+
| `...restProps` | `HTMLAttributes<HTMLDivElement>` | `-` | Additional attributes forwarded to the item element. |
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import type { ClockEditableSegmentType } from '../root/context';
|
|
4
|
+
|
|
5
|
+
type TimePickerWheelOption = {
|
|
6
|
+
value: string;
|
|
7
|
+
label: string;
|
|
8
|
+
disabled: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type TimePickerWheelItemProps = Omit<
|
|
12
|
+
HTMLAttributes<HTMLDivElement>,
|
|
13
|
+
'class' | 'children' | 'onclick'
|
|
14
|
+
> & {
|
|
15
|
+
type: ClockEditableSegmentType;
|
|
16
|
+
option: TimePickerWheelOption;
|
|
17
|
+
selected?: boolean;
|
|
18
|
+
onrequestcenter?: () => void;
|
|
19
|
+
class?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
type,
|
|
24
|
+
option,
|
|
25
|
+
selected = false,
|
|
26
|
+
onrequestcenter,
|
|
27
|
+
class: className = '',
|
|
28
|
+
...restProps
|
|
29
|
+
}: TimePickerWheelItemProps = $props();
|
|
30
|
+
|
|
31
|
+
function handleClick() {
|
|
32
|
+
onrequestcenter?.();
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<div
|
|
37
|
+
data-wheel-item
|
|
38
|
+
data-type={type}
|
|
39
|
+
data-value={option.value}
|
|
40
|
+
data-disabled={option.disabled || undefined}
|
|
41
|
+
data-selected={selected || undefined}
|
|
42
|
+
data-centered={selected || undefined}
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
class={className}
|
|
45
|
+
onclick={handleClick}
|
|
46
|
+
{...restProps}
|
|
47
|
+
>
|
|
48
|
+
{option.label}
|
|
49
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import type { ClockEditableSegmentType } from '../root/context';
|
|
3
|
+
type TimePickerWheelOption = {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
disabled: boolean;
|
|
7
|
+
};
|
|
8
|
+
type TimePickerWheelItemProps = Omit<HTMLAttributes<HTMLDivElement>, 'class' | 'children' | 'onclick'> & {
|
|
9
|
+
type: ClockEditableSegmentType;
|
|
10
|
+
option: TimePickerWheelOption;
|
|
11
|
+
selected?: boolean;
|
|
12
|
+
onrequestcenter?: () => void;
|
|
13
|
+
class?: string;
|
|
14
|
+
};
|
|
15
|
+
declare const ClockWheelItem: import("svelte").Component<TimePickerWheelItemProps, {}, "">;
|
|
16
|
+
type ClockWheelItem = ReturnType<typeof ClockWheelItem>;
|
|
17
|
+
export default ClockWheelItem;
|