@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,97 @@
|
|
|
1
|
+
# TimePicker
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
`TimePicker` composes a segmented time input with a popover containing wheel-based spinbutton columns.
|
|
6
|
+
|
|
7
|
+
## Anatomy
|
|
8
|
+
|
|
9
|
+
- `TimePicker.Root`
|
|
10
|
+
- `TimePicker.Input`
|
|
11
|
+
- `TimePicker.Segment`
|
|
12
|
+
- `TimePicker.Trigger`
|
|
13
|
+
- `TimePicker.Popover`
|
|
14
|
+
- `TimePicker.Clock`
|
|
15
|
+
- `TimePicker.WheelColumn`
|
|
16
|
+
- `TimePicker.WheelItem`
|
|
17
|
+
|
|
18
|
+
```svelte
|
|
19
|
+
<TimePicker.Root>
|
|
20
|
+
<TimePicker.Input aria-label="Time input">
|
|
21
|
+
{#snippet children(segment)}
|
|
22
|
+
<TimePicker.Segment {segment} />
|
|
23
|
+
{/snippet}
|
|
24
|
+
</TimePicker.Input>
|
|
25
|
+
<TimePicker.Trigger />
|
|
26
|
+
|
|
27
|
+
<TimePicker.Popover>
|
|
28
|
+
<TimePicker.Clock />
|
|
29
|
+
</TimePicker.Popover>
|
|
30
|
+
</TimePicker.Root>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```svelte
|
|
34
|
+
<TimePicker.Popover>
|
|
35
|
+
<TimePicker.Clock class="flex gap-2">
|
|
36
|
+
{#snippet column(col)}
|
|
37
|
+
<TimePicker.WheelColumn type={col.type} class="h-44 rounded-md">
|
|
38
|
+
{#snippet children(option)}
|
|
39
|
+
<TimePicker.WheelItem type={col.type} {option} class="..." />
|
|
40
|
+
{/snippet}
|
|
41
|
+
</TimePicker.WheelColumn>
|
|
42
|
+
{/snippet}
|
|
43
|
+
</TimePicker.Clock>
|
|
44
|
+
</TimePicker.Popover>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Root API
|
|
48
|
+
|
|
49
|
+
- `value?: string | null` (`HH:mm` or `HH:mm:ss`)
|
|
50
|
+
- `defaultValue?: string | null` (`HH:mm` or `HH:mm:ss`)
|
|
51
|
+
- `onChange?: (value: string | null) => void`
|
|
52
|
+
- `minValue?: string`
|
|
53
|
+
- `maxValue?: string`
|
|
54
|
+
- `hourCycle?: 12 | 24`
|
|
55
|
+
- `granularity?: 'hour' | 'minute' | 'second'`
|
|
56
|
+
- `hourStep?: number`
|
|
57
|
+
- `minuteStep?: number`
|
|
58
|
+
- `secondStep?: number`
|
|
59
|
+
- `isDisabled?: boolean`
|
|
60
|
+
- `isReadOnly?: boolean`
|
|
61
|
+
- `isRequired?: boolean`
|
|
62
|
+
- `open?: boolean`
|
|
63
|
+
- `defaultOpen?: boolean`
|
|
64
|
+
- `onOpenChange?: (open: boolean, details: { reason, event?, cancel(), isCanceled }) => void`
|
|
65
|
+
- Null-first empty contract: when `value` and `defaultValue` are omitted, the empty state is `null`.
|
|
66
|
+
- `TimePicker.Input` exposes `aria-invalid` and `data-invalid` when the current segment draft is not committeable.
|
|
67
|
+
|
|
68
|
+
## Popover API
|
|
69
|
+
|
|
70
|
+
- `TimePicker.Popover` forwards `Popover.Content` props (for example `placement`, `offset`, `shouldFlip`, `boundaryElement`, `isNonModal`, and close behavior props).
|
|
71
|
+
- The following are controlled internally by `TimePicker` and are not accepted on `TimePicker.Popover`: `open`, `triggerRef`, `onOpenChange`, `id`.
|
|
72
|
+
- Defaults:
|
|
73
|
+
- `placement` defaults to `bottom`.
|
|
74
|
+
- `aria-label` defaults to `Time picker`.
|
|
75
|
+
- `initialFocus` defaults to the first wheel column (`role="spinbutton"`).
|
|
76
|
+
|
|
77
|
+
## Wheel API
|
|
78
|
+
|
|
79
|
+
- `TimePicker.WheelColumn` renders one wheel (`role="spinbutton"`) for one editable segment (`hour`, `minute`, `second`, or `dayPeriod`).
|
|
80
|
+
- `TimePicker.WheelItem` is headless: it renders one item (`data-wheel-item`) with state attributes (`data-selected`, `data-disabled`, `data-centered`) and leaves all visual styling to consumers.
|
|
81
|
+
|
|
82
|
+
## Clock API
|
|
83
|
+
|
|
84
|
+
- `TimePicker.Clock` resolves visible wheel columns from root state (`granularity`, `hourCycle`) in stable order: `hour → minute? → second? → dayPeriod?`.
|
|
85
|
+
- `class?: string` uses default layout (`flex gap-2`) when omitted.
|
|
86
|
+
- `column?: Snippet<[ClockColumnInfo]>` allows custom per-column rendering.
|
|
87
|
+
- `ClockColumnInfo` shape:
|
|
88
|
+
- `type: 'hour' | 'minute' | 'second' | 'dayPeriod'`
|
|
89
|
+
- `label?: string`
|
|
90
|
+
|
|
91
|
+
## Notes
|
|
92
|
+
|
|
93
|
+
- Locale is read from `LocaleProvider` when available.
|
|
94
|
+
- Internally, values are normalized to 24-hour representation; 12-hour rendering only affects UI segments.
|
|
95
|
+
- `granularity='hour'` emits `HH:00` values.
|
|
96
|
+
- Min/max comparisons do not support midnight-wrapping ranges (`minValue > maxValue` is treated as out-of-range).
|
|
97
|
+
- Wheel selection commits immediately on snap; popover close is controlled by standard popover interactions (escape, outside press, programmatic close).
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# TimePicker TODO
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Track TimePicker work with a single mandatory TODO format.
|
|
6
|
+
|
|
7
|
+
## Backlog
|
|
8
|
+
|
|
9
|
+
- [x] [M][P0][Area: Parsing][Owner: Unassigned][Target: TBD] Make time parsing strict so invalid strings like "1.5:30" or ":30" are rejected. _(Already implemented via `isValidTimePickerValue` regex. `parseTimePickerValue` now returns `null` for invalid input.)_
|
|
10
|
+
- [x] [S][P0][Area: Validation][Owner: Unassigned][Target: TBD] Guard 12h draft conversion against invalid hour values outside 1-12. _(Already implemented in `buildTimePartsFromDraft`.)_
|
|
11
|
+
- [x] [M][P0][Area: Input][Owner: Unassigned][Target: TBD] Apply step clamping on typed values when segment edit is confirmed. _(Already implemented in `setSegmentValue` via `clampToStep`.)_
|
|
12
|
+
- [x] [S][P0][Area: Formatting][Owner: Unassigned][Target: TBD] Force "HH:00" output for hour granularity regardless of leftover minutes. _(Already implemented in `formatTimePickerValue`.)_
|
|
13
|
+
- [x] [S][P0][Area: Validation][Owner: Unassigned][Target: TBD] Treat missing dayPeriod as invalid in 12h draft-to-24h conversion. _(Already implemented in `buildTimePartsFromDraft`.)_
|
|
14
|
+
- [x] [M][P1][Area: Locale][Owner: Unassigned][Target: TBD] Build segment order and literals from Intl formatToParts instead of hardcoded templates. _(Already implemented in `buildTimePickerSegments`.)_
|
|
15
|
+
- [x] [S][P1][Area: Validation][Owner: Unassigned][Target: TBD] Compare min/max using the active granularity to avoid false out-of-range states. _(Fixed: `isTimeOutOfRange` now accepts `granularity` and truncates before comparison.)_
|
|
16
|
+
- [x] [S][P1][Area: Accessibility][Owner: Unassigned][Target: TBD] Ensure time picker columns and options expose complete listbox/option ARIA contract. _(Already implemented in column/column-cell components.)_
|
|
17
|
+
- [x] [S][P1][Area: Composition][Owner: Unassigned][Target: TBD] Wire selection-close behavior so `shouldCloseOnSelect` controls whether column selection closes popover when draft is complete. _(Implemented in `selectColumnOption`; default is `false`.)_
|
|
18
|
+
- [x] [S][P2][Area: Accessibility][Owner: Unassigned][Target: TBD] Verify dayPeriod segment uses 0-1 ARIA bounds and localized value text. _(Fixed: segment now uses `timePicker.hourCycle` for correct `aria-valuemin`/`aria-valuemax`.)_
|
|
19
|
+
- [x] [S][P2][Area: Testing][Owner: Unassigned][Target: TBD] Ensure segment data attributes are consistent for styling and test selectors. _(Already implemented: `data-time-picker-segment="true"`.)_
|
|
20
|
+
- [x] [M][P2][Area: Testing][Owner: Unassigned][Target: TBD] Expand unit coverage for parsing, format output, 12h conversion, clamping, and draft evaluation pipeline. _(Added 47 unit tests in `time-utils.test.ts`.)_
|
|
21
|
+
- [x] [S][P2][Area: Documentation][Owner: Unassigned][Target: TBD] Add TimePicker references to focus contract documentation. _(Added component coverage section to `FOCUS_STATE_CONTRACT.md`.)_
|
|
22
|
+
|
|
23
|
+
## Active
|
|
24
|
+
|
|
25
|
+
### Bugs
|
|
26
|
+
|
|
27
|
+
- [x] [M][P0][Area: State][Owner: Unassigned][Target: TBD] Permitir que el componente se limpie a undefined/null externamente. En `time-picker-root.svelte`, la observación de Svelte 5 ($effect sobre value) hace un early return con `if (value === undefined) return;`. Esto impide que se limpie la hora si el developer resetea el form manualmente enviando value = undefined. **Fix:** Quitar el early return por `undefined` y despachar la asignación nula para vaciar el TimePicker de forma controlada.
|
|
28
|
+
|
|
29
|
+
- [x] [S][P0][Area: State][Owner: Unassigned][Target: TBD] Arreglar deshidratación al vaciar todos los segmentos (`commitFromDraft`). Al borrar backspace tras backspace, si el usuario vacía el _último_ segmento, la variable `hasAnyRequiredValue` rompe el flujo dando `false` lo que dispara un early return ocultando el borrado de `publishCommittedValue`. **Fix:** Quitar o condicionar el early return condicionado de hasAnyRequiredValue para que el componente notifique a los bindings de que la hora quedó completamente `null`.
|
|
30
|
+
|
|
31
|
+
- [x] [M][P0][Area: Events][Owner: Unassigned][Target: TBD] Reparar Event Forwarding y omisiones en componentes internos. Actualmente los wrappers tragan eventos porque fallan en reingeniar el event payload natural de Svelte 5.
|
|
32
|
+
1. `Input`: Extrae variables pero no usa `composeEventHandlers`. Los onkey/onfocus externos mueren ahogados.
|
|
33
|
+
2. `Trigger`: Esparce `...restProps` **después** de sus eventos atados a Svelte 5, lo que cancela las rutinas internas si el usuario pasa `<Trigger onmousedown>`
|
|
34
|
+
**Fix:** Extraer las declaraciones conflictivas de `$props()` y usar `composeEventHandlers` en línea para todas en vez de pisarlas u omitirlas.
|
|
35
|
+
|
|
36
|
+
- [x] [M][P0][Area: Input][Owner: Unassigned][Target: TBD] Guard segment `contenteditable` against paste, IME composition, and drag-drop. The `<span contenteditable>` in `time-picker-segment.svelte` has no `onbeforeinput`, `onpaste`, or `oncompositionend` handler. Ctrl+V, IME input, or drag-drop can modify the DOM directly without going through `typeSegmentDigit` / `setSegmentValue`, creating drift between the visible text and the internal `segmentDraft`. **Fix:** Add `onbeforeinput={(e) => e.preventDefault()}` to block all non-keyboard mutations. Alternatively, add an `oninput` handler that immediately restores the span's `textContent` to the current draft value as a safety net.
|
|
37
|
+
|
|
38
|
+
- [x] [M][P0][Area: Wheel][Owner: Unassigned][Target: TBD] Increase scroll debounce timeout in `use-wheel-scroll.svelte.ts` from 64ms to ~120-150ms. On touch devices with momentum scrolling (especially iOS Safari), gaps between `scroll` events during deceleration can exceed 64ms, causing premature snap that interrupts the user's inertia. Since browsers that support `scrollend` use it as the primary settle signal and the debounce only acts as a safety net, a higher value does not affect perceived latency. **File:** `hooks/use-wheel-scroll.svelte.ts` line ~215.
|
|
39
|
+
|
|
40
|
+
- [x] [M][P0][Area: Wheel][Owner: Unassigned][Target: TBD] Sync `lastCenteredIndex` when value changes externally while popover is open. `lastCenteredIndex` in `time-picker-wheel-column.svelte` is a plain `let` used as anchor for `moveBy()`. If the root value changes via `bind:value` while the popover is open, `lastCenteredIndex` still points to the old index. A subsequent `ArrowDown` jumps from the stale position instead of the current one. **Fix:** Add a `$effect` that observes `selectedValue` + `timePicker.open`: when both are truthy and `selectedIndex` changes, update `lastCenteredIndex = selectedIndex` and optionally scroll to it.
|
|
41
|
+
|
|
42
|
+
- [x] [S][P1][Area: Wheel][Owner: Unassigned][Target: TBD] Suppress unnecessary re-snap chain after `scrollToIndex('smooth')` when value is already committed. When `handleCenterRequest` (click on WheelItem) eagerly commits the value then calls `scrollToIndex(i, 'smooth')`, the ensuing scroll events trigger debounce → `snapToCenter` → `animateSnapTo` for micro-alignment → more scroll events. The chain is usually idempotent but can cause micro-jitter. **Fix:** Add a `silent` parameter to `scrollToIndex` that suppresses `snapToCenter` for the duration of that smooth scroll. Use it only for clicks where the commit already happened, NOT for corrective disabled-skip (which must snap and commit).
|
|
43
|
+
|
|
44
|
+
### Accesibilidad
|
|
45
|
+
|
|
46
|
+
- [x] [S][P1][Area: Accessibility][Owner: Unassigned][Target: TBD] Add `aria-live` region to announce wheel value changes for screen readers. When the user scrolls a `WheelColumn`, `aria-valuenow` / `aria-valuetext` update, but screen readers don't automatically announce `aria-valuenow` changes on a `spinbutton`. **Fix:** Add a visually-hidden `<span role="status" aria-live="polite" class="sr-only">{valueText}</span>` inside each `WheelColumn` that updates when the selected value changes. This announces the new value after each snap.
|
|
47
|
+
|
|
48
|
+
- [x] [S][P1][Area: Accessibility][Owner: Unassigned][Target: TBD] Fix `Home`/`End` in segment to use correct boundary values for 12h mode. In `time-picker-segment.svelte`, `Home` sends `setSegmentValue('hour', '0')` and `End` sends `'23'` regardless of `hourCycle`. In 12h mode the valid range is 1-12. This currently works by accident (clamping corrects it), but the intent is wrong and fragile. **Fix:** Use `timePicker.hourCycle` to determine boundaries: `Home` → `hourCycle === 12 ? '1' : '0'`, `End` → `hourCycle === 12 ? '12' : '23'`.
|
|
49
|
+
|
|
50
|
+
- [x] [S][P2][Area: Accessibility][Owner: Unassigned][Target: TBD] Add `aria-roledescription` to `WheelColumn`. A generic `role="spinbutton"` doesn't communicate that the widget is a wheel/picker. Adding `aria-roledescription="wheel picker"` (or a localized equivalent) would improve the experience for screen reader users. Apply to the root `<div role="spinbutton">` in `time-picker-wheel-column.svelte`.
|
|
51
|
+
|
|
52
|
+
- [x] [S][P2][Area: Accessibility][Owner: Unassigned][Target: TBD] Add `isRequired` prop to Root and propagate `aria-required` to the Input group. There's currently no way to indicate the field is required for form validation. The prop should flow from Root → context → `time-picker-input.svelte` as `aria-required` on the `role="group"` element.
|
|
53
|
+
|
|
54
|
+
### Performance
|
|
55
|
+
|
|
56
|
+
- [x] [M][P1][Area: Performance][Owner: Unassigned][Target: TBD] Cache `Intl.DateTimeFormat` instance across segment draft changes. `buildTimePickerSegments` in `time-utils.ts` creates a `new Intl.DateTimeFormat(locale, formatOptions)` on every call. This function is invoked from a `$derived` that depends on `segmentDraft`, so every keystroke recreates the formatter. The formatter only depends on `locale`, `hourCycle`, and `granularity`. **Fix:** In `time-picker-root.svelte`, derive the formatter once from those 3 variables and pass it to `buildTimePickerSegments` as a parameter.
|
|
57
|
+
|
|
58
|
+
- [x] [S][P2][Area: Performance][Owner: Unassigned][Target: TBD] Short-circuit disabled computation in `getWheelOptions` when no min/max range is set. `getWheelOptions` runs `buildTimePartsFromDraft` + `isTimeOutOfRange` for every option in every column (~122 iterations for minute granularity). When `normalizedMinValue` and `normalizedMaxValue` are both `undefined`, all options are always enabled. **Fix:** Add an early check: `if (!normalizedMinValue && !normalizedMaxValue)` → skip disabled computation and return `disabled: false` for all.
|
|
59
|
+
|
|
60
|
+
- [x] [S][P2][Area: Performance][Owner: Unassigned][Target: TBD] Cache system locale so `Intl.DateTimeFormat().resolvedOptions().locale` isn't called on every `$derived` reevaluation. In `time-picker-root.svelte`, `resolvedLocale` falls back to `Intl.DateTimeFormat().resolvedOptions().locale` when no context locale is provided. This creates a new formatter on each reevaluation. The system locale doesn't change during the component's lifetime. **Fix:** Capture it once with `untrack` at initialization and store in a constant.
|
|
61
|
+
|
|
62
|
+
### Arquitectura / Robustez
|
|
63
|
+
|
|
64
|
+
- [x] [S][P2][Area: Wheel][Owner: Unassigned][Target: TBD] Batch `ResizeObserver` callbacks with `requestAnimationFrame` to avoid layout thrashing. The `ResizeObserver` in `time-picker-wheel-column.svelte` calls `syncMeasurements()` which updates `$state` variables (`itemHeight`, `spacerHeight`). This triggers Svelte re-render → layout shift → observer fires again, producing `ResizeObserver loop completed with undelivered notifications` warnings in tests. **Fix:** Wrap the observer callback: `resizeObserver = new ResizeObserver(() => { requestAnimationFrame(() => syncMeasurements()); });`
|
|
65
|
+
|
|
66
|
+
- [x] [S][P2][Area: Wheel][Owner: Unassigned][Target: TBD] Make `WheelItem` default visual style consistent with headless pattern. `time-picker-wheel-item.svelte` applies inline styles (opacity, cursor, font-weight, etc.) by default but drops ALL of them if the consumer passes `class`. This all-or-nothing behavior is inconsistent with the rest of the library (fully headless). **Fix:** Either remove default visual styles entirely (consumer styles via `data-selected`, `data-disabled` attributes — headless), or separate structural styles (always applied) from decorative styles (opt-out via prop). Preferred: go fully headless for consistency.
|
|
67
|
+
|
|
68
|
+
- [x] [S][P3][Area: Code Quality][Owner: Unassigned][Target: TBD] Remove dead code in `isTimeOutOfRange`. The `if (minValue && maxValue) { ... if (min > max) return true }` block at the end is unreachable — if `min > max`, any value `v` is already rejected by the individual `v < min` or `v > max` checks above. Remove to reduce reader confusion. **File:** `root/time-utils.ts` lines ~219-228.
|
|
69
|
+
|
|
70
|
+
- [x] [S][P3][Area: Code Quality][Owner: Unassigned][Target: TBD] Fix meaningless ternary in `setSegmentValue`. `const maxDigits = type === 'hour' ? 2 : 2;` — both branches are `2`. Was likely a draft for differentiating types. Replace with `const maxDigits = 2;`. **File:** `root/time-picker-root.svelte` line ~280.
|
|
71
|
+
|
|
72
|
+
### Testing
|
|
73
|
+
|
|
74
|
+
- [x] [C][P1][Area: Testing][Owner: Unassigned][Target: TBD] Expand `WheelColumn` test coverage. Current: 4 tests (render, aria values, cross-column nav, spacers). Missing critical scenarios: (a) ArrowUp/Down keyboard changes value, (b) disabled item skip on scroll, (c) PageUp/PageDown/Home/End navigation, (d) scroll-to-selected on popover open, (e) external value change syncs wheel position, (f) full focus contract (`data-focus-within`, `data-focus-visible`), (g) behavior with `minValue`/`maxValue` producing disabled options.
|
|
75
|
+
|
|
76
|
+
- [x] [M][P1][Area: Testing][Owner: Unassigned][Target: TBD] Add integration test for wheel → root → segment sync. No test verifies the full flow: user scrolls wheel → snap detects centered item → `selectWheelValue` called → `commitFromDraft` runs → segment text updates → `bind:value` reflects new time. This is the core happy-path of the wheel and should be covered.
|
|
77
|
+
|
|
78
|
+
- [x] [S][P2][Area: Testing][Owner: Unassigned][Target: TBD] Add test for contenteditable paste/IME resilience in segment. Verify that pasting text into a segment does not corrupt `segmentDraft` and that the visible text is restored to the draft value. Important because `contenteditable` is inherently fragile.
|
|
79
|
+
|
|
80
|
+
## Notes
|
|
81
|
+
|
|
82
|
+
TimePicker locale is sourced from LocaleProvider, not from a root locale prop.
|
|
83
|
+
|
|
84
|
+
### Review context (2026-03-02)
|
|
85
|
+
|
|
86
|
+
The wheel architecture (`WheelColumn` / `WheelItem` / `use-wheel-scroll`) replaced the original column-based `listbox`/`option` architecture. The wheel uses JS-driven snapping (no CSS `scroll-snap-type`) with a 120ms ease-out animation. Selection occurs when scroll settles and the centered item is committed. Disabled items are auto-skipped. `shouldCloseOnSelect` was removed — popover closes only via Escape, click outside, or programmatically.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# TimePicker Clock
|
|
2
|
+
|
|
3
|
+
## API reference
|
|
4
|
+
|
|
5
|
+
### TimePicker.Clock
|
|
6
|
+
|
|
7
|
+
Name: `TimePicker.Clock`
|
|
8
|
+
Description: Clock panel composition part that resolves visible wheel columns from `TimePicker.Root` state.
|
|
9
|
+
|
|
10
|
+
| Prop | Type | Default | Description |
|
|
11
|
+
| -------------- | -------------------------------- | ---------------------------- | ------------------------------------------------------- |
|
|
12
|
+
| `column` | `Snippet<[ClockColumnInfo]>` | `undefined` | Optional custom per-column renderer. |
|
|
13
|
+
| `class` | `string` | `'flex items-stretch gap-2'` | CSS class names for the clock container. |
|
|
14
|
+
| `...restProps` | `HTMLAttributes<HTMLDivElement>` | `-` | Additional attributes forwarded to the clock container. |
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import TimePicker from '../index';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
defaultValue?: string;
|
|
6
|
+
hourCycle?: 12 | 24;
|
|
7
|
+
granularity?: 'hour' | 'minute' | 'second';
|
|
8
|
+
isDisabled?: boolean;
|
|
9
|
+
clockClass?: string;
|
|
10
|
+
useSnippet?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
defaultValue = '14:30',
|
|
15
|
+
hourCycle,
|
|
16
|
+
granularity = 'minute',
|
|
17
|
+
isDisabled = false,
|
|
18
|
+
clockClass = '',
|
|
19
|
+
useSnippet = false
|
|
20
|
+
}: Props = $props();
|
|
21
|
+
|
|
22
|
+
let value = $state<string | null>((() => defaultValue)());
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<TimePicker.Root bind:value defaultOpen {hourCycle} {granularity} {isDisabled}>
|
|
26
|
+
<TimePicker.Input aria-label="Time input">
|
|
27
|
+
{#snippet children(segment)}
|
|
28
|
+
<TimePicker.Segment {segment} />
|
|
29
|
+
{/snippet}
|
|
30
|
+
</TimePicker.Input>
|
|
31
|
+
|
|
32
|
+
<TimePicker.Popover>
|
|
33
|
+
{#if useSnippet}
|
|
34
|
+
<TimePicker.Clock class={clockClass}>
|
|
35
|
+
{#snippet column(col)}
|
|
36
|
+
<div data-testid="custom-column" data-type={col.type}>{col.label}</div>
|
|
37
|
+
{/snippet}
|
|
38
|
+
</TimePicker.Clock>
|
|
39
|
+
{:else}
|
|
40
|
+
<TimePicker.Clock class={clockClass} />
|
|
41
|
+
{/if}
|
|
42
|
+
</TimePicker.Popover>
|
|
43
|
+
</TimePicker.Root>
|
|
44
|
+
|
|
45
|
+
<p data-testid="clock-test-value">{value}</p>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
defaultValue?: string;
|
|
3
|
+
hourCycle?: 12 | 24;
|
|
4
|
+
granularity?: 'hour' | 'minute' | 'second';
|
|
5
|
+
isDisabled?: boolean;
|
|
6
|
+
clockClass?: string;
|
|
7
|
+
useSnippet?: boolean;
|
|
8
|
+
};
|
|
9
|
+
declare const TimePickerClockTest: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type TimePickerClockTest = ReturnType<typeof TimePickerClockTest>;
|
|
11
|
+
export default TimePickerClockTest;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import { setClockContext, type ClockContext } from '../../clock/root/context';
|
|
5
|
+
import {
|
|
6
|
+
resolveVisibleColumns,
|
|
7
|
+
type ClockColumnInfo
|
|
8
|
+
} from '../../clock/root/resolve-visible-columns';
|
|
9
|
+
import ClockWheelColumn from '../../clock/wheel-column/clock-wheel-column.svelte';
|
|
10
|
+
import { useTimePickerContext } from '../root/context';
|
|
11
|
+
|
|
12
|
+
type TimePickerClockProps = Omit<HTMLAttributes<HTMLDivElement>, 'class' | 'children'> & {
|
|
13
|
+
column?: Snippet<[ClockColumnInfo]>;
|
|
14
|
+
class?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
column: columnSnippet,
|
|
19
|
+
class: className = '',
|
|
20
|
+
...restProps
|
|
21
|
+
}: TimePickerClockProps = $props();
|
|
22
|
+
|
|
23
|
+
const timePicker = useTimePickerContext();
|
|
24
|
+
|
|
25
|
+
const context: ClockContext = {
|
|
26
|
+
get id() {
|
|
27
|
+
return timePicker.id;
|
|
28
|
+
},
|
|
29
|
+
get locale() {
|
|
30
|
+
return timePicker.locale;
|
|
31
|
+
},
|
|
32
|
+
get isDisabled() {
|
|
33
|
+
return timePicker.isDisabled;
|
|
34
|
+
},
|
|
35
|
+
get granularity() {
|
|
36
|
+
return timePicker.granularity;
|
|
37
|
+
},
|
|
38
|
+
get hourCycle() {
|
|
39
|
+
return timePicker.hourCycle;
|
|
40
|
+
},
|
|
41
|
+
get open() {
|
|
42
|
+
return timePicker.open;
|
|
43
|
+
},
|
|
44
|
+
selectWheelValue: timePicker.selectWheelValue,
|
|
45
|
+
getSelectedWheelValue: timePicker.getSelectedWheelValue,
|
|
46
|
+
getWheelOptions: timePicker.getWheelOptions,
|
|
47
|
+
getSegmentLabel: timePicker.getSegmentLabel
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
setClockContext(context);
|
|
51
|
+
|
|
52
|
+
const visibleColumns = $derived.by(() =>
|
|
53
|
+
resolveVisibleColumns(context.granularity, context.hourCycle, context.getSegmentLabel)
|
|
54
|
+
);
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<div class={className || 'flex items-stretch gap-2'} data-clock="true" {...restProps}>
|
|
58
|
+
{#each visibleColumns as col (col.type)}
|
|
59
|
+
{#if columnSnippet}
|
|
60
|
+
{@render columnSnippet(col)}
|
|
61
|
+
{:else}
|
|
62
|
+
<ClockWheelColumn type={col.type} />
|
|
63
|
+
{/if}
|
|
64
|
+
{/each}
|
|
65
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { type ClockColumnInfo } from '../../clock/root/resolve-visible-columns';
|
|
4
|
+
type TimePickerClockProps = Omit<HTMLAttributes<HTMLDivElement>, 'class' | 'children'> & {
|
|
5
|
+
column?: Snippet<[ClockColumnInfo]>;
|
|
6
|
+
class?: string;
|
|
7
|
+
};
|
|
8
|
+
declare const TimePickerClock: import("svelte").Component<TimePickerClockProps, {}, "">;
|
|
9
|
+
type TimePickerClock = ReturnType<typeof TimePickerClock>;
|
|
10
|
+
export default TimePickerClock;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * as TimePicker from './index.parts.ts';
|
|
2
|
+
export { default as TimePickerRoot } from './root/time-picker-root.svelte';
|
|
3
|
+
export { default as TimePickerInput } from './input/time-picker-input.svelte';
|
|
4
|
+
export { default as TimePickerSegment } from './segment/time-picker-segment.svelte';
|
|
5
|
+
export { default as TimePickerTrigger } from './trigger/time-picker-trigger.svelte';
|
|
6
|
+
export { default as TimePickerPopover } from './popover/time-picker-popover.svelte';
|
|
7
|
+
export { default as TimePickerClock } from './clock/time-picker-clock.svelte';
|
|
8
|
+
export { default as TimePickerWheelColumn } from '../clock/wheel-column/clock-wheel-column.svelte';
|
|
9
|
+
export { default as TimePickerWheelItem } from '../clock/wheel-item/clock-wheel-item.svelte';
|
|
10
|
+
export { getTimePickerContext, setTimePickerContext, useTimePickerContext, type TimePickerContext, type TimePickerOpenChangeDetails, type TimePickerOpenChangeReason, type TimePickerSegmentPart, type TimePickerSegmentType, type TimePickerEditableSegmentType } from './root/context.ts';
|
|
11
|
+
export { type TimePickerGranularity, type TimePickerHourCycle, type TimePickerTimeValue } from './root/time-utils';
|
|
12
|
+
export { type ClockColumnInfo } from '../clock/root/resolve-visible-columns';
|
|
13
|
+
import * as TimePickerParts from './index.parts.ts';
|
|
14
|
+
export default TimePickerParts;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * as TimePicker from './index.parts.ts';
|
|
2
|
+
export { default as TimePickerRoot } from './root/time-picker-root.svelte';
|
|
3
|
+
export { default as TimePickerInput } from './input/time-picker-input.svelte';
|
|
4
|
+
export { default as TimePickerSegment } from './segment/time-picker-segment.svelte';
|
|
5
|
+
export { default as TimePickerTrigger } from './trigger/time-picker-trigger.svelte';
|
|
6
|
+
export { default as TimePickerPopover } from './popover/time-picker-popover.svelte';
|
|
7
|
+
export { default as TimePickerClock } from './clock/time-picker-clock.svelte';
|
|
8
|
+
export { default as TimePickerWheelColumn } from '../clock/wheel-column/clock-wheel-column.svelte';
|
|
9
|
+
export { default as TimePickerWheelItem } from '../clock/wheel-item/clock-wheel-item.svelte';
|
|
10
|
+
export { getTimePickerContext, setTimePickerContext, useTimePickerContext } from './root/context.ts';
|
|
11
|
+
export {} from './root/time-utils';
|
|
12
|
+
export {} from '../clock/root/resolve-visible-columns';
|
|
13
|
+
import * as TimePickerParts from './index.parts.ts';
|
|
14
|
+
export default TimePickerParts;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { default as Root } from './root/time-picker-root.svelte';
|
|
2
|
+
export { default as Input } from './input/time-picker-input.svelte';
|
|
3
|
+
export { default as Segment } from './segment/time-picker-segment.svelte';
|
|
4
|
+
export { default as Trigger } from './trigger/time-picker-trigger.svelte';
|
|
5
|
+
export { default as Popover } from './popover/time-picker-popover.svelte';
|
|
6
|
+
export { default as Clock } from './clock/time-picker-clock.svelte';
|
|
7
|
+
export { default as WheelColumn } from '../clock/wheel-column/clock-wheel-column.svelte';
|
|
8
|
+
export { default as WheelItem } from '../clock/wheel-item/clock-wheel-item.svelte';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { default as Root } from './root/time-picker-root.svelte';
|
|
2
|
+
export { default as Input } from './input/time-picker-input.svelte';
|
|
3
|
+
export { default as Segment } from './segment/time-picker-segment.svelte';
|
|
4
|
+
export { default as Trigger } from './trigger/time-picker-trigger.svelte';
|
|
5
|
+
export { default as Popover } from './popover/time-picker-popover.svelte';
|
|
6
|
+
export { default as Clock } from './clock/time-picker-clock.svelte';
|
|
7
|
+
export { default as WheelColumn } from '../clock/wheel-column/clock-wheel-column.svelte';
|
|
8
|
+
export { default as WheelItem } from '../clock/wheel-item/clock-wheel-item.svelte';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# TimePicker Input
|
|
2
|
+
|
|
3
|
+
## API reference
|
|
4
|
+
|
|
5
|
+
### TimePicker.Input
|
|
6
|
+
|
|
7
|
+
Name: `TimePicker.Input`
|
|
8
|
+
Description: Group wrapper for rendered time segments; manages focus entry and state attributes from `TimePicker.Root`.
|
|
9
|
+
|
|
10
|
+
| Prop | Type | Default | Description |
|
|
11
|
+
| -------------- | ---------------------------------- | ----------- | ----------------------------------------------------- |
|
|
12
|
+
| `children` | `Snippet<[TimePickerSegmentPart]>` | `undefined` | Optional custom renderer for each segment part. |
|
|
13
|
+
| `class` | `string` | `''` | CSS class names for the group element. |
|
|
14
|
+
| `aria-label` | `string` | `undefined` | Accessible label for the input group. |
|
|
15
|
+
| `...restProps` | `HTMLAttributes<HTMLDivElement>` | `-` | Additional attributes forwarded to the group element. |
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import TimePicker from '../index';
|
|
3
|
+
|
|
4
|
+
let downCount = $state(0);
|
|
5
|
+
let focusCount = $state(0);
|
|
6
|
+
let blurCount = $state(0);
|
|
7
|
+
let keyCount = $state(0);
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<TimePicker.Root defaultValue="14:30">
|
|
11
|
+
<TimePicker.Input
|
|
12
|
+
aria-label="Forwarding input"
|
|
13
|
+
onmousedown={() => {
|
|
14
|
+
downCount += 1;
|
|
15
|
+
}}
|
|
16
|
+
onfocus={() => {
|
|
17
|
+
focusCount += 1;
|
|
18
|
+
}}
|
|
19
|
+
onblur={() => {
|
|
20
|
+
blurCount += 1;
|
|
21
|
+
}}
|
|
22
|
+
onkeydown={() => {
|
|
23
|
+
keyCount += 1;
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
{#snippet children(segment)}
|
|
27
|
+
<TimePicker.Segment {segment} />
|
|
28
|
+
{/snippet}
|
|
29
|
+
</TimePicker.Input>
|
|
30
|
+
<TimePicker.Trigger>Open time picker</TimePicker.Trigger>
|
|
31
|
+
<TimePicker.Popover>
|
|
32
|
+
<TimePicker.Clock />
|
|
33
|
+
</TimePicker.Popover>
|
|
34
|
+
</TimePicker.Root>
|
|
35
|
+
|
|
36
|
+
<p data-testid="input-down-count">{downCount}</p>
|
|
37
|
+
<p data-testid="input-focus-count">{focusCount}</p>
|
|
38
|
+
<p data-testid="input-blur-count">{blurCount}</p>
|
|
39
|
+
<p data-testid="input-key-count">{keyCount}</p>
|
|
40
|
+
<button type="button" data-testid="outside-button">Outside</button>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import { useTimePickerContext, type TimePickerSegmentPart } from '../root/context';
|
|
5
|
+
import TimePickerSegment from '../segment/time-picker-segment.svelte';
|
|
6
|
+
import {
|
|
7
|
+
shouldShowFocusVisible,
|
|
8
|
+
trackInteractionModality
|
|
9
|
+
} from '../../primitives/input-modality';
|
|
10
|
+
|
|
11
|
+
import { composeEventHandlers } from '../internal/strict-props';
|
|
12
|
+
|
|
13
|
+
type TimePickerInputProps = Omit<
|
|
14
|
+
HTMLAttributes<HTMLDivElement>,
|
|
15
|
+
'children' | 'class' | 'id' | 'role' | 'tabindex'
|
|
16
|
+
> & {
|
|
17
|
+
children?: Snippet<[TimePickerSegmentPart]>;
|
|
18
|
+
class?: string;
|
|
19
|
+
'aria-label'?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
children,
|
|
24
|
+
class: className = '',
|
|
25
|
+
'aria-label': ariaLabel,
|
|
26
|
+
onmousedown: onMouseDownExternal,
|
|
27
|
+
onfocus: onFocusExternal,
|
|
28
|
+
onblur: onBlurExternal,
|
|
29
|
+
onkeydown: onKeydownExternal,
|
|
30
|
+
...restProps
|
|
31
|
+
}: TimePickerInputProps = $props();
|
|
32
|
+
|
|
33
|
+
const timePicker = useTimePickerContext();
|
|
34
|
+
const segments = $derived(timePicker.getSegments());
|
|
35
|
+
const inputId = $derived(`${timePicker.id}-input`);
|
|
36
|
+
|
|
37
|
+
function handleMouseDown(event: MouseEvent) {
|
|
38
|
+
if (timePicker.isDisabled) return;
|
|
39
|
+
trackInteractionModality(event, event.currentTarget as HTMLElement);
|
|
40
|
+
timePicker.setFocusVisible(false);
|
|
41
|
+
|
|
42
|
+
const target = event.target as HTMLElement | null;
|
|
43
|
+
if (target?.closest('[data-time-picker-segment="true"]')) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
timePicker.focusNextPlaceholderOrLastSegment();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handleFocus(event: FocusEvent) {
|
|
52
|
+
if (timePicker.isDisabled) return;
|
|
53
|
+
timePicker.syncFocusWithin();
|
|
54
|
+
timePicker.setFocusVisible(shouldShowFocusVisible(event.target as HTMLElement | null));
|
|
55
|
+
const target = event.target as HTMLElement | null;
|
|
56
|
+
if (target?.closest('[data-time-picker-segment="true"]')) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
timePicker.focusNextPlaceholderOrLastSegment();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function handleBlur() {
|
|
63
|
+
queueMicrotask(() => {
|
|
64
|
+
timePicker.syncFocusWithin();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
69
|
+
if (timePicker.isDisabled) return;
|
|
70
|
+
if (event.key !== 'Enter' && event.key !== ' ') return;
|
|
71
|
+
trackInteractionModality(event, event.currentTarget as HTMLElement);
|
|
72
|
+
timePicker.setFocusVisible(true);
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
timePicker.focusNextPlaceholderOrLastSegment();
|
|
75
|
+
}
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
79
|
+
<!-- svelte-ignore a11y_role_supports_aria_props -->
|
|
80
|
+
<div
|
|
81
|
+
id={inputId}
|
|
82
|
+
class={className}
|
|
83
|
+
{...restProps}
|
|
84
|
+
role="group"
|
|
85
|
+
aria-label={ariaLabel}
|
|
86
|
+
aria-invalid={timePicker.isInvalidDraft || undefined}
|
|
87
|
+
aria-required={timePicker.isRequired || undefined}
|
|
88
|
+
tabindex={timePicker.isDisabled ? -1 : 0}
|
|
89
|
+
data-disabled={timePicker.isDisabled || undefined}
|
|
90
|
+
data-readonly={timePicker.isReadOnly || undefined}
|
|
91
|
+
data-open={timePicker.open || undefined}
|
|
92
|
+
data-focus-visible={timePicker.focusVisible || undefined}
|
|
93
|
+
data-focus-within={timePicker.focusWithin || undefined}
|
|
94
|
+
data-invalid={timePicker.isInvalidDraft || undefined}
|
|
95
|
+
onmousedown={composeEventHandlers(handleMouseDown, onMouseDownExternal ?? undefined)}
|
|
96
|
+
onfocus={composeEventHandlers(handleFocus, onFocusExternal ?? undefined)}
|
|
97
|
+
onblur={composeEventHandlers(handleBlur, onBlurExternal ?? undefined)}
|
|
98
|
+
onkeydown={composeEventHandlers(handleKeydown, onKeydownExternal ?? undefined, {
|
|
99
|
+
skipExternalOnDefaultPrevented: true
|
|
100
|
+
})}
|
|
101
|
+
>
|
|
102
|
+
{#each segments as segment, index (segment.type === 'literal' ? `literal-${index}` : segment.type)}
|
|
103
|
+
{#if children}
|
|
104
|
+
{@render children(segment)}
|
|
105
|
+
{:else}
|
|
106
|
+
<TimePickerSegment {segment} />
|
|
107
|
+
{/if}
|
|
108
|
+
{/each}
|
|
109
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { type TimePickerSegmentPart } from '../root/context';
|
|
4
|
+
type TimePickerInputProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'class' | 'id' | 'role' | 'tabindex'> & {
|
|
5
|
+
children?: Snippet<[TimePickerSegmentPart]>;
|
|
6
|
+
class?: string;
|
|
7
|
+
'aria-label'?: string;
|
|
8
|
+
};
|
|
9
|
+
declare const TimePickerInput: import("svelte").Component<TimePickerInputProps, {}, "">;
|
|
10
|
+
type TimePickerInput = ReturnType<typeof TimePickerInput>;
|
|
11
|
+
export default TimePickerInput;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function sanitizeTimePickerProps(componentName: string, props: Record<string, unknown>, forbiddenProps: string[]): Record<string, unknown>;
|
|
2
|
+
export declare function composeEventHandlers<TEvent extends Event>(internalHandler: ((event: TEvent) => void) | undefined, externalHandler: ((event: TEvent) => void) | undefined, options?: {
|
|
3
|
+
skipExternalOnDefaultPrevented?: boolean;
|
|
4
|
+
}): (event: TEvent) => void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const warnedMessages = new Set();
|
|
2
|
+
const sanitizeCache = new WeakMap();
|
|
3
|
+
function warnOnce(message) {
|
|
4
|
+
if (!import.meta.env.DEV)
|
|
5
|
+
return;
|
|
6
|
+
if (warnedMessages.has(message))
|
|
7
|
+
return;
|
|
8
|
+
warnedMessages.add(message);
|
|
9
|
+
console.warn(message);
|
|
10
|
+
}
|
|
11
|
+
export function sanitizeTimePickerProps(componentName, props, forbiddenProps) {
|
|
12
|
+
if (props === null || typeof props !== 'object')
|
|
13
|
+
return {};
|
|
14
|
+
const forbiddenKey = [...forbiddenProps].sort().join('|');
|
|
15
|
+
const cachedByProps = sanitizeCache.get(props);
|
|
16
|
+
const cached = cachedByProps?.get(forbiddenKey);
|
|
17
|
+
if (cached)
|
|
18
|
+
return cached;
|
|
19
|
+
const sanitized = {};
|
|
20
|
+
for (const [key, value] of Object.entries(props)) {
|
|
21
|
+
if (forbiddenProps.includes(key)) {
|
|
22
|
+
warnOnce(`[TimePicker.${componentName}]: Prop "${key}" is controlled by TimePicker.Root and has been ignored.`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
sanitized[key] = value;
|
|
26
|
+
}
|
|
27
|
+
let cacheBucket = cachedByProps;
|
|
28
|
+
if (!cacheBucket) {
|
|
29
|
+
cacheBucket = new Map();
|
|
30
|
+
sanitizeCache.set(props, cacheBucket);
|
|
31
|
+
}
|
|
32
|
+
cacheBucket.set(forbiddenKey, sanitized);
|
|
33
|
+
return sanitized;
|
|
34
|
+
}
|
|
35
|
+
export function composeEventHandlers(internalHandler, externalHandler, options) {
|
|
36
|
+
return (event) => {
|
|
37
|
+
let preventDefaultCalled = false;
|
|
38
|
+
const originalPreventDefault = event.preventDefault.bind(event);
|
|
39
|
+
event.preventDefault = () => {
|
|
40
|
+
preventDefaultCalled = true;
|
|
41
|
+
originalPreventDefault();
|
|
42
|
+
};
|
|
43
|
+
internalHandler?.(event);
|
|
44
|
+
event.preventDefault = originalPreventDefault;
|
|
45
|
+
if (options?.skipExternalOnDefaultPrevented &&
|
|
46
|
+
(event.defaultPrevented || preventDefaultCalled)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
externalHandler?.(event);
|
|
50
|
+
};
|
|
51
|
+
}
|