@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.
Files changed (217) hide show
  1. package/dist/FOCUS_STATE_CONTRACT.md +63 -0
  2. package/dist/FOCUS_STATE_REVIEW_TEMPLATE.md +70 -0
  3. package/dist/calendar/README.md +2 -1
  4. package/dist/calendar/TODO.md +21 -107
  5. package/dist/calendar/body-cell/README.md +15 -0
  6. package/dist/calendar/body-cell/calendar-body-cell.svelte +116 -41
  7. package/dist/calendar/grid/README.md +13 -0
  8. package/dist/calendar/grid-body/README.md +13 -0
  9. package/dist/calendar/grid-header/README.md +13 -0
  10. package/dist/calendar/header-cell/README.md +14 -0
  11. package/dist/calendar/heading/README.md +13 -0
  12. package/dist/calendar/root/README.md +24 -0
  13. package/dist/calendar/root/calendar-root-test.svelte +4 -0
  14. package/dist/calendar/root/calendar-root-test.svelte.d.ts +1 -0
  15. package/dist/calendar/root/calendar-root.svelte +3 -0
  16. package/dist/calendar/root/calendar-root.svelte.d.ts +1 -0
  17. package/dist/calendar/root/context.d.ts +4 -0
  18. package/dist/calendar/root/context.js +28 -25
  19. package/dist/calendar/root/date-utils.d.ts +1 -1
  20. package/dist/calendar/root/date-utils.js +16 -26
  21. package/dist/calendar/trigger-next/README.md +14 -0
  22. package/dist/calendar/trigger-previous/README.md +14 -0
  23. package/dist/clock/README.md +75 -0
  24. package/dist/clock/axis/README.md +24 -0
  25. package/dist/clock/axis/clock-axis.svelte +37 -0
  26. package/dist/clock/axis/clock-axis.svelte.d.ts +8 -0
  27. package/dist/clock/hooks/use-wheel-scroll.svelte.d.ts +16 -0
  28. package/dist/clock/hooks/use-wheel-scroll.svelte.js +336 -0
  29. package/dist/clock/index.d.ts +10 -0
  30. package/dist/clock/index.js +10 -0
  31. package/dist/clock/index.parts.d.ts +4 -0
  32. package/dist/clock/index.parts.js +4 -0
  33. package/dist/clock/root/README.md +38 -0
  34. package/dist/clock/root/clock-root-test.svelte +62 -0
  35. package/dist/clock/root/clock-root-test.svelte.d.ts +14 -0
  36. package/dist/clock/root/clock-root.svelte +329 -0
  37. package/dist/clock/root/clock-root.svelte.d.ts +25 -0
  38. package/dist/clock/root/context.d.ts +22 -0
  39. package/dist/clock/root/context.js +15 -0
  40. package/dist/clock/root/resolve-visible-columns.d.ts +7 -0
  41. package/dist/clock/root/resolve-visible-columns.js +16 -0
  42. package/dist/clock/root/time-utils.d.ts +48 -0
  43. package/dist/clock/root/time-utils.js +314 -0
  44. package/dist/clock/root/wheel-options.d.ts +17 -0
  45. package/dist/clock/root/wheel-options.js +63 -0
  46. package/dist/clock/wheel-column/README.md +25 -0
  47. package/dist/clock/wheel-column/clock-wheel-column-bindable-test.svelte +16 -0
  48. package/dist/clock/wheel-column/clock-wheel-column-bindable-test.svelte.d.ts +3 -0
  49. package/dist/clock/wheel-column/clock-wheel-column-custom-snippet-test.svelte +29 -0
  50. package/dist/clock/wheel-column/clock-wheel-column-custom-snippet-test.svelte.d.ts +6 -0
  51. package/dist/clock/wheel-column/clock-wheel-column-default-height-test.svelte +11 -0
  52. package/dist/clock/wheel-column/clock-wheel-column-default-height-test.svelte.d.ts +3 -0
  53. package/dist/clock/wheel-column/clock-wheel-column-test.svelte +38 -0
  54. package/dist/clock/wheel-column/clock-wheel-column-test.svelte.d.ts +12 -0
  55. package/dist/clock/wheel-column/clock-wheel-column-tp-test.svelte +38 -0
  56. package/dist/clock/wheel-column/clock-wheel-column-tp-test.svelte.d.ts +12 -0
  57. package/dist/clock/wheel-column/clock-wheel-column-untagged-snippet-test.svelte +29 -0
  58. package/dist/clock/wheel-column/clock-wheel-column-untagged-snippet-test.svelte.d.ts +6 -0
  59. package/dist/clock/wheel-column/clock-wheel-column.svelte +499 -0
  60. package/dist/clock/wheel-column/clock-wheel-column.svelte.d.ts +17 -0
  61. package/dist/clock/wheel-item/README.md +17 -0
  62. package/dist/clock/wheel-item/clock-wheel-item.svelte +49 -0
  63. package/dist/clock/wheel-item/clock-wheel-item.svelte.d.ts +17 -0
  64. package/dist/combobox/TODO.md +28 -175
  65. package/dist/combobox/button/combobox-button.svelte +2 -0
  66. package/dist/combobox/root/combobox.svelte +30 -0
  67. package/dist/datepicker/README.md +100 -0
  68. package/dist/datepicker/TODO.md +28 -0
  69. package/dist/datepicker/calendar/README.md +19 -0
  70. package/dist/datepicker/calendar/date-picker-calendar-unsafe-props-test.svelte +60 -0
  71. package/dist/datepicker/calendar/date-picker-calendar-unsafe-props-test.svelte.d.ts +3 -0
  72. package/dist/datepicker/calendar/date-picker-calendar.svelte +65 -0
  73. package/dist/datepicker/calendar/date-picker-calendar.svelte.d.ts +10 -0
  74. package/dist/datepicker/index.d.ts +18 -0
  75. package/dist/datepicker/index.js +18 -0
  76. package/dist/datepicker/index.parts.d.ts +14 -0
  77. package/dist/datepicker/index.parts.js +14 -0
  78. package/dist/datepicker/input/README.md +15 -0
  79. package/dist/datepicker/input/date-picker-input.svelte +108 -0
  80. package/dist/datepicker/input/date-picker-input.svelte.d.ts +11 -0
  81. package/dist/datepicker/internal/strict-props.d.ts +2 -0
  82. package/dist/datepicker/internal/strict-props.js +28 -0
  83. package/dist/datepicker/popover/README.md +20 -0
  84. package/dist/datepicker/popover/date-picker-popover-handler-test.svelte +57 -0
  85. package/dist/datepicker/popover/date-picker-popover-handler-test.svelte.d.ts +3 -0
  86. package/dist/datepicker/popover/date-picker-popover-unsafe-props-test.svelte +45 -0
  87. package/dist/datepicker/popover/date-picker-popover-unsafe-props-test.svelte.d.ts +18 -0
  88. package/dist/datepicker/popover/date-picker-popover.svelte +87 -0
  89. package/dist/datepicker/popover/date-picker-popover.svelte.d.ts +7 -0
  90. package/dist/datepicker/root/README.md +38 -0
  91. package/dist/datepicker/root/context.d.ts +43 -0
  92. package/dist/datepicker/root/context.js +15 -0
  93. package/dist/datepicker/root/date-picker-bindable-empty-test.svelte +24 -0
  94. package/dist/datepicker/root/date-picker-bindable-empty-test.svelte.d.ts +3 -0
  95. package/dist/datepicker/root/date-picker-bindable-test.svelte +41 -0
  96. package/dist/datepicker/root/date-picker-bindable-test.svelte.d.ts +3 -0
  97. package/dist/datepicker/root/date-picker-empty-test.svelte +47 -0
  98. package/dist/datepicker/root/date-picker-empty-test.svelte.d.ts +3 -0
  99. package/dist/datepicker/root/date-picker-locale-typing-test.svelte +47 -0
  100. package/dist/datepicker/root/date-picker-locale-typing-test.svelte.d.ts +3 -0
  101. package/dist/datepicker/root/date-picker-open-cancel-test.svelte +54 -0
  102. package/dist/datepicker/root/date-picker-open-cancel-test.svelte.d.ts +8 -0
  103. package/dist/datepicker/root/date-picker-root.svelte +495 -0
  104. package/dist/datepicker/root/date-picker-root.svelte.d.ts +24 -0
  105. package/dist/datepicker/root/date-picker-test.svelte +86 -0
  106. package/dist/datepicker/root/date-picker-test.svelte.d.ts +13 -0
  107. package/dist/datepicker/root/date-utils.d.ts +17 -0
  108. package/dist/datepicker/root/date-utils.js +138 -0
  109. package/dist/datepicker/root/draft-evaluation.d.ts +13 -0
  110. package/dist/datepicker/root/draft-evaluation.js +56 -0
  111. package/dist/datepicker/root/focus-controller.d.ts +3 -0
  112. package/dist/datepicker/root/focus-controller.js +15 -0
  113. package/dist/datepicker/root/open-change.d.ts +5 -0
  114. package/dist/datepicker/root/open-change.js +13 -0
  115. package/dist/datepicker/root/open-controller.d.ts +7 -0
  116. package/dist/datepicker/root/open-controller.js +15 -0
  117. package/dist/datepicker/root/segment-controller.d.ts +8 -0
  118. package/dist/datepicker/root/segment-controller.js +53 -0
  119. package/dist/datepicker/root/segment-state.d.ts +18 -0
  120. package/dist/datepicker/root/segment-state.js +134 -0
  121. package/dist/datepicker/root/value-commit.d.ts +4 -0
  122. package/dist/datepicker/root/value-commit.js +8 -0
  123. package/dist/datepicker/segment/README.md +14 -0
  124. package/dist/datepicker/segment/date-picker-segment.svelte +319 -0
  125. package/dist/datepicker/segment/date-picker-segment.svelte.d.ts +9 -0
  126. package/dist/datepicker/trigger/README.md +14 -0
  127. package/dist/datepicker/trigger/date-picker-trigger.svelte +110 -0
  128. package/dist/datepicker/trigger/date-picker-trigger.svelte.d.ts +9 -0
  129. package/dist/dialog/content/dialog-content.svelte +6 -6
  130. package/dist/dialog/root/context.d.ts +2 -1
  131. package/dist/dialog/root/dialog-root.svelte +9 -2
  132. package/dist/index.d.ts +8 -0
  133. package/dist/index.js +8 -0
  134. package/dist/listbox/root/listbox.svelte +44 -0
  135. package/dist/popover/README.md +10 -0
  136. package/dist/popover/content/popover-content-standalone-test.svelte +28 -0
  137. package/dist/popover/content/popover-content-standalone-test.svelte.d.ts +6 -0
  138. package/dist/popover/content/popover-content-test.svelte +2 -1
  139. package/dist/popover/content/popover-content-test.svelte.d.ts +2 -1
  140. package/dist/popover/content/popover-content.svelte +91 -18
  141. package/dist/popover/content/popover-content.svelte.d.ts +5 -1
  142. package/dist/popover/index.d.ts +1 -1
  143. package/dist/popover/index.js +1 -3
  144. package/dist/popover/root/README.md +10 -15
  145. package/dist/popover/root/context.d.ts +16 -7
  146. package/dist/popover/root/context.js +0 -2
  147. package/dist/popover/root/focus-state.d.ts +4 -0
  148. package/dist/popover/root/focus-state.js +33 -0
  149. package/dist/popover/root/popover-root.svelte +90 -17
  150. package/dist/popover/root/popover-root.svelte.d.ts +2 -1
  151. package/dist/popover/root/popover-test.svelte +2 -1
  152. package/dist/popover/root/popover-test.svelte.d.ts +2 -1
  153. package/dist/popover/trigger/popover-trigger-button.svelte +4 -4
  154. package/dist/popover/trigger/popover-trigger.svelte +1 -1
  155. package/dist/portal/portal.svelte +3 -1
  156. package/dist/primitives/click-outside.d.ts +1 -1
  157. package/dist/primitives/click-outside.js +1 -1
  158. package/dist/primitives/focus-trap.d.ts +7 -2
  159. package/dist/primitives/focus-trap.js +50 -17
  160. package/dist/primitives/index.d.ts +1 -0
  161. package/dist/primitives/index.js +1 -0
  162. package/dist/primitives/input-modality.d.ts +7 -0
  163. package/dist/primitives/input-modality.js +125 -0
  164. package/dist/test-utils/focus-contract.d.ts +3 -0
  165. package/dist/test-utils/focus-contract.js +26 -0
  166. package/dist/timepicker/IMPLEMENTATION_PLAN.md +254 -0
  167. package/dist/timepicker/README.md +97 -0
  168. package/dist/timepicker/TODO.md +86 -0
  169. package/dist/timepicker/clock/README.md +14 -0
  170. package/dist/timepicker/clock/time-picker-clock-test.svelte +45 -0
  171. package/dist/timepicker/clock/time-picker-clock-test.svelte.d.ts +11 -0
  172. package/dist/timepicker/clock/time-picker-clock.svelte +65 -0
  173. package/dist/timepicker/clock/time-picker-clock.svelte.d.ts +10 -0
  174. package/dist/timepicker/index.d.ts +14 -0
  175. package/dist/timepicker/index.js +14 -0
  176. package/dist/timepicker/index.parts.d.ts +8 -0
  177. package/dist/timepicker/index.parts.js +8 -0
  178. package/dist/timepicker/input/README.md +15 -0
  179. package/dist/timepicker/input/time-picker-input-forwarding-test.svelte +40 -0
  180. package/dist/timepicker/input/time-picker-input-forwarding-test.svelte.d.ts +3 -0
  181. package/dist/timepicker/input/time-picker-input.svelte +109 -0
  182. package/dist/timepicker/input/time-picker-input.svelte.d.ts +11 -0
  183. package/dist/timepicker/internal/strict-props.d.ts +4 -0
  184. package/dist/timepicker/internal/strict-props.js +51 -0
  185. package/dist/timepicker/popover/README.md +20 -0
  186. package/dist/timepicker/popover/time-picker-popover-unsafe-props-test.svelte +22 -0
  187. package/dist/timepicker/popover/time-picker-popover-unsafe-props-test.svelte.d.ts +3 -0
  188. package/dist/timepicker/popover/time-picker-popover.svelte +89 -0
  189. package/dist/timepicker/popover/time-picker-popover.svelte.d.ts +7 -0
  190. package/dist/timepicker/root/README.md +42 -0
  191. package/dist/timepicker/root/context.d.ts +51 -0
  192. package/dist/timepicker/root/context.js +15 -0
  193. package/dist/timepicker/root/time-picker-12h-test.svelte +22 -0
  194. package/dist/timepicker/root/time-picker-12h-test.svelte.d.ts +3 -0
  195. package/dist/timepicker/root/time-picker-bindable-test.svelte +25 -0
  196. package/dist/timepicker/root/time-picker-bindable-test.svelte.d.ts +3 -0
  197. package/dist/timepicker/root/time-picker-empty-test.svelte +20 -0
  198. package/dist/timepicker/root/time-picker-empty-test.svelte.d.ts +3 -0
  199. package/dist/timepicker/root/time-picker-root.svelte +625 -0
  200. package/dist/timepicker/root/time-picker-root.svelte.d.ts +28 -0
  201. package/dist/timepicker/root/time-picker-test.svelte +72 -0
  202. package/dist/timepicker/root/time-picker-test.svelte.d.ts +15 -0
  203. package/dist/timepicker/root/time-utils.d.ts +1 -0
  204. package/dist/timepicker/root/time-utils.js +3 -0
  205. package/dist/timepicker/segment/README.md +14 -0
  206. package/dist/timepicker/segment/time-picker-segment.svelte +365 -0
  207. package/dist/timepicker/segment/time-picker-segment.svelte.d.ts +9 -0
  208. package/dist/timepicker/trigger/README.md +14 -0
  209. package/dist/timepicker/trigger/time-picker-trigger-forwarding-test.svelte +35 -0
  210. package/dist/timepicker/trigger/time-picker-trigger-forwarding-test.svelte.d.ts +3 -0
  211. package/dist/timepicker/trigger/time-picker-trigger.svelte +122 -0
  212. package/dist/timepicker/trigger/time-picker-trigger.svelte.d.ts +9 -0
  213. package/dist/utils/date-only.d.ts +11 -0
  214. package/dist/utils/date-only.js +53 -0
  215. package/dist/utils/index.d.ts +1 -0
  216. package/dist/utils/index.js +1 -0
  217. package/package.json +16 -1
@@ -43,7 +43,7 @@
43
43
 
44
44
  if (button && wrapperRef?.contains(button)) {
45
45
  setActiveTrigger(button);
46
- popoverCtx.toggle();
46
+ popoverCtx.toggle('trigger-press', event);
47
47
  }
48
48
  }
49
49
 
@@ -18,6 +18,8 @@
18
18
  if (!browser || !wrapper) return;
19
19
 
20
20
  await tick();
21
+ const wrapperNode = wrapper;
22
+ if (!wrapperNode) return;
21
23
 
22
24
  const targetEl = document.querySelector(target);
23
25
  if (!targetEl) {
@@ -25,7 +27,7 @@
25
27
  return;
26
28
  }
27
29
 
28
- targetEl.appendChild(wrapper);
30
+ targetEl.appendChild(wrapperNode);
29
31
  });
30
32
 
31
33
  onDestroy(() => {
@@ -4,7 +4,7 @@
4
4
  */
5
5
  export type ClickOutsideOptions = {
6
6
  /** Callback when clicking outside. */
7
- handler: () => void;
7
+ handler: (event: MouseEvent) => void;
8
8
  /** Whether the listener is enabled. */
9
9
  enabled?: boolean;
10
10
  /** Elements to ignore (clicks on these won't trigger). */
@@ -41,7 +41,7 @@ export function clickOutside(node, options) {
41
41
  // This prevents closing when clicking on nested popovers/dialogs
42
42
  if (isInTopLayer(target))
43
43
  return;
44
- handler();
44
+ handler(event);
45
45
  }
46
46
  if (enabled) {
47
47
  document.addEventListener('mousedown', handleClick, true);
@@ -2,6 +2,11 @@
2
2
  * Focus trap primitive.
3
3
  * Traps keyboard focus within a container element.
4
4
  */
5
+ export type FocusTrapOptions = {
6
+ enabled?: boolean;
7
+ restoreFocus?: boolean;
8
+ initialFocus?: HTMLElement | string | (() => HTMLElement | null | undefined);
9
+ };
5
10
  /**
6
11
  * Svelte action that traps focus within an element.
7
12
  *
@@ -13,7 +18,7 @@
13
18
  * </div>
14
19
  * ```
15
20
  */
16
- export declare function focusTrap(node: HTMLElement, enabled?: boolean): {
17
- update(newEnabled: boolean): void;
21
+ export declare function focusTrap(node: HTMLElement, options?: boolean | FocusTrapOptions): {
22
+ update(newOptions: boolean | FocusTrapOptions): void;
18
23
  destroy(): void;
19
24
  };
@@ -2,6 +2,7 @@
2
2
  * Focus trap primitive.
3
3
  * Traps keyboard focus within a container element.
4
4
  */
5
+ import { focusWithModality, getInteractionModality } from './input-modality';
5
6
  const FOCUSABLE_SELECTOR = [
6
7
  'a[href]',
7
8
  'button:not([disabled])',
@@ -11,8 +12,29 @@ const FOCUSABLE_SELECTOR = [
11
12
  '[tabindex]:not([tabindex="-1"])',
12
13
  '[contenteditable="true"]'
13
14
  ].join(', ');
15
+ function resolveEnabled(options) {
16
+ if (typeof options === 'boolean')
17
+ return options;
18
+ return options.enabled ?? true;
19
+ }
20
+ function resolveRestoreFocus(options) {
21
+ if (typeof options === 'boolean')
22
+ return true;
23
+ return options.restoreFocus ?? true;
24
+ }
25
+ function resolveInitialFocus(container, initialFocus) {
26
+ if (!initialFocus)
27
+ return null;
28
+ if (typeof initialFocus === 'function') {
29
+ return initialFocus() ?? null;
30
+ }
31
+ if (typeof initialFocus === 'string') {
32
+ return container.querySelector(initialFocus);
33
+ }
34
+ return initialFocus;
35
+ }
14
36
  function getFocusableElements(container) {
15
- return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => el.offsetParent !== null);
37
+ return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((element) => element.offsetParent !== null);
16
38
  }
17
39
  /**
18
40
  * Svelte action that traps focus within an element.
@@ -25,8 +47,11 @@ function getFocusableElements(container) {
25
47
  * </div>
26
48
  * ```
27
49
  */
28
- export function focusTrap(node, enabled = true) {
50
+ export function focusTrap(node, options = true) {
29
51
  let previousActiveElement = null;
52
+ let enabled = resolveEnabled(options);
53
+ let restoreFocus = resolveRestoreFocus(options);
54
+ let initialFocus = typeof options === 'boolean' ? undefined : options.initialFocus;
30
55
  function handleKeydown(event) {
31
56
  if (event.key !== 'Tab')
32
57
  return;
@@ -49,12 +74,11 @@ export function focusTrap(node, enabled = true) {
49
74
  event.preventDefault();
50
75
  lastElement.focus();
51
76
  }
77
+ return;
52
78
  }
53
- else {
54
- if (document.activeElement === lastElement || document.activeElement === node) {
55
- event.preventDefault();
56
- firstElement.focus();
57
- }
79
+ if (document.activeElement === lastElement || document.activeElement === node) {
80
+ event.preventDefault();
81
+ firstElement.focus();
58
82
  }
59
83
  }
60
84
  function activate() {
@@ -62,21 +86,25 @@ export function focusTrap(node, enabled = true) {
62
86
  if (!node.hasAttribute('tabindex')) {
63
87
  node.setAttribute('tabindex', '-1');
64
88
  }
65
- // Focus first focusable element, or the container if none
66
89
  requestAnimationFrame(() => {
90
+ const initialFocusTarget = resolveInitialFocus(node, initialFocus);
91
+ const modality = getInteractionModality();
92
+ if (initialFocusTarget && initialFocusTarget.isConnected) {
93
+ focusWithModality(initialFocusTarget, modality);
94
+ return;
95
+ }
67
96
  const focusableElements = getFocusableElements(node);
68
97
  if (focusableElements.length > 0) {
69
- focusableElements[0].focus();
70
- }
71
- else {
72
- node.focus();
98
+ focusWithModality(focusableElements[0], modality);
99
+ return;
73
100
  }
101
+ focusWithModality(node, modality);
74
102
  });
75
103
  document.addEventListener('keydown', handleKeydown, true);
76
104
  }
77
105
  function deactivate() {
78
106
  document.removeEventListener('keydown', handleKeydown, true);
79
- if (previousActiveElement && previousActiveElement.focus) {
107
+ if (restoreFocus && previousActiveElement && previousActiveElement.focus) {
80
108
  previousActiveElement.focus();
81
109
  }
82
110
  }
@@ -84,14 +112,19 @@ export function focusTrap(node, enabled = true) {
84
112
  activate();
85
113
  }
86
114
  return {
87
- update(newEnabled) {
88
- if (newEnabled && !enabled) {
115
+ update(newOptions) {
116
+ const nextEnabled = resolveEnabled(newOptions);
117
+ const nextRestoreFocus = resolveRestoreFocus(newOptions);
118
+ if (nextEnabled && !enabled) {
89
119
  activate();
90
120
  }
91
- else if (!newEnabled && enabled) {
121
+ else if (!nextEnabled && enabled) {
92
122
  deactivate();
93
123
  }
94
- enabled = newEnabled;
124
+ options = newOptions;
125
+ enabled = nextEnabled;
126
+ restoreFocus = nextRestoreFocus;
127
+ initialFocus = typeof newOptions === 'boolean' ? undefined : newOptions.initialFocus;
95
128
  },
96
129
  destroy() {
97
130
  if (enabled) {
@@ -4,3 +4,4 @@ export * from './keyboard-navigation.js';
4
4
  export * from './scroll-lock.js';
5
5
  export * from './aria-hide-outside.js';
6
6
  export * from './click-outside.js';
7
+ export * from './input-modality.js';
@@ -5,3 +5,4 @@ export * from './keyboard-navigation.js';
5
5
  export * from './scroll-lock.js';
6
6
  export * from './aria-hide-outside.js';
7
7
  export * from './click-outside.js';
8
+ export * from './input-modality.js';
@@ -0,0 +1,7 @@
1
+ export type InputModality = 'keyboard' | 'pointer' | 'virtual';
2
+ export declare function initInputModality(target?: HTMLElement | null): void;
3
+ export declare function trackInteractionModality(event?: Event, target?: HTMLElement | null): void;
4
+ export declare function getInteractionModality(): InputModality;
5
+ export declare function shouldShowFocusVisible(target: HTMLElement | null): boolean;
6
+ export declare function focusWithModality(target: HTMLElement, modality: InputModality, options?: FocusOptions): void;
7
+ export declare function resolveCloseInteractionModality(reason: string, event?: Event): InputModality;
@@ -0,0 +1,125 @@
1
+ let currentModality = 'virtual';
2
+ const listenedWindows = new WeakSet();
3
+ let forcedFocusTarget = null;
4
+ let forcedFocusModality = null;
5
+ function isModifierOnlyKey(event) {
6
+ return (event.key === 'Shift' || event.key === 'Control' || event.key === 'Alt' || event.key === 'Meta');
7
+ }
8
+ function isKeyboardModalityKey(event) {
9
+ if (event.ctrlKey || event.metaKey || event.altKey)
10
+ return false;
11
+ if (isModifierOnlyKey(event))
12
+ return false;
13
+ return true;
14
+ }
15
+ function ensureWindowListeners(win) {
16
+ if (!win)
17
+ return;
18
+ if (listenedWindows.has(win))
19
+ return;
20
+ const onKeyDown = (event) => {
21
+ if (!isKeyboardModalityKey(event))
22
+ return;
23
+ currentModality = 'keyboard';
24
+ };
25
+ const onPointerDown = () => {
26
+ currentModality = 'pointer';
27
+ };
28
+ const onFocusIn = () => {
29
+ if (currentModality === 'pointer' || currentModality === 'keyboard')
30
+ return;
31
+ currentModality = 'virtual';
32
+ };
33
+ win.addEventListener('keydown', onKeyDown, true);
34
+ win.addEventListener('pointerdown', onPointerDown, true);
35
+ win.addEventListener('mousedown', onPointerDown, true);
36
+ win.addEventListener('focusin', onFocusIn, true);
37
+ listenedWindows.add(win);
38
+ }
39
+ export function initInputModality(target) {
40
+ const ownerWindow = target?.ownerDocument?.defaultView;
41
+ if (ownerWindow) {
42
+ ensureWindowListeners(ownerWindow);
43
+ return;
44
+ }
45
+ if (typeof window !== 'undefined') {
46
+ ensureWindowListeners(window);
47
+ }
48
+ }
49
+ function inferModalityFromEvent(event) {
50
+ if (!event)
51
+ return undefined;
52
+ if (event instanceof KeyboardEvent) {
53
+ return isKeyboardModalityKey(event) ? 'keyboard' : undefined;
54
+ }
55
+ if (event instanceof MouseEvent && event.type === 'click' && event.detail === 0) {
56
+ return 'keyboard';
57
+ }
58
+ if (event instanceof PointerEvent || event instanceof MouseEvent) {
59
+ return 'pointer';
60
+ }
61
+ return undefined;
62
+ }
63
+ function resolveModalityForTarget(target) {
64
+ if (target && forcedFocusTarget === target && forcedFocusModality) {
65
+ const modality = forcedFocusModality;
66
+ forcedFocusTarget = null;
67
+ forcedFocusModality = null;
68
+ return modality;
69
+ }
70
+ return currentModality;
71
+ }
72
+ export function trackInteractionModality(event, target) {
73
+ initInputModality(target);
74
+ const inferred = inferModalityFromEvent(event);
75
+ if (inferred) {
76
+ currentModality = inferred;
77
+ }
78
+ }
79
+ export function getInteractionModality() {
80
+ return currentModality;
81
+ }
82
+ export function shouldShowFocusVisible(target) {
83
+ if (!target)
84
+ return false;
85
+ const modality = resolveModalityForTarget(target);
86
+ if (modality === 'pointer')
87
+ return false;
88
+ return target.matches(':focus-visible');
89
+ }
90
+ export function focusWithModality(target, modality, options) {
91
+ initInputModality(target);
92
+ // The forced target/modality pair is a synchronous safety net so a focus handler that runs
93
+ // immediately after target.focus() can resolve the intended modality without racing async timing.
94
+ // If it is consumed later, currentModality still preserves the same modality as fallback.
95
+ forcedFocusTarget = target;
96
+ forcedFocusModality = modality;
97
+ currentModality = modality;
98
+ if (modality === 'pointer') {
99
+ const pointerFocusOptions = {
100
+ ...(options ?? {}),
101
+ focusVisible: false
102
+ };
103
+ target.focus(pointerFocusOptions);
104
+ }
105
+ else {
106
+ target.focus(options);
107
+ }
108
+ queueMicrotask(() => {
109
+ if (forcedFocusTarget !== target)
110
+ return;
111
+ forcedFocusTarget = null;
112
+ forcedFocusModality = null;
113
+ });
114
+ }
115
+ export function resolveCloseInteractionModality(reason, event) {
116
+ if (reason === 'escape-key')
117
+ return 'keyboard';
118
+ if (event instanceof KeyboardEvent)
119
+ return 'keyboard';
120
+ if (event instanceof PointerEvent || event instanceof MouseEvent)
121
+ return 'pointer';
122
+ if (reason === 'outside-press')
123
+ return 'pointer';
124
+ return 'virtual';
125
+ }
@@ -0,0 +1,3 @@
1
+ export declare function expectFocusVisibleImpliesFocusWithin(root: HTMLElement | null): void;
2
+ export declare function expectFocusVisibleImpliesFocused(node: HTMLElement | null): void;
3
+ export declare function expectNoFalseFocusAttributes(scope?: ParentNode): void;
@@ -0,0 +1,26 @@
1
+ import { expect } from 'vitest';
2
+ export function expectFocusVisibleImpliesFocusWithin(root) {
3
+ expect(root).toBeTruthy();
4
+ const focusVisible = root?.getAttribute('data-focus-visible');
5
+ const focusWithin = root?.getAttribute('data-focus-within');
6
+ if (focusVisible === 'true') {
7
+ expect(focusWithin).toBe('true');
8
+ }
9
+ }
10
+ export function expectFocusVisibleImpliesFocused(node) {
11
+ expect(node).toBeTruthy();
12
+ const focusVisible = node?.getAttribute('data-focus-visible');
13
+ const focused = node?.getAttribute('data-focused');
14
+ if (focusVisible === 'true') {
15
+ expect(focused).toBe('true');
16
+ }
17
+ }
18
+ export function expectNoFalseFocusAttributes(scope = document) {
19
+ const focusAttrs = ['data-focused', 'data-focus-visible', 'data-focus-within'];
20
+ for (const attr of focusAttrs) {
21
+ const nodes = Array.from(scope.querySelectorAll(`[${attr}]`));
22
+ for (const node of nodes) {
23
+ expect(node.getAttribute(attr)).not.toBe('false');
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,254 @@
1
+ # TimePicker Implementation Plan
2
+
3
+ ## Scope
4
+
5
+ Implement all open items from `TODO.md` in one delivery, prioritizing correctness and stability first, then accessibility/performance, and finally coverage hardening.
6
+
7
+ ## Goals
8
+
9
+ - Fix state/event correctness regressions first.
10
+ - Keep wheel interaction fluid while improving robustness.
11
+ - Add missing accessibility semantics without introducing noisy behavior.
12
+ - Apply low-risk performance improvements.
13
+ - Expand tests to protect the full wheel pipeline.
14
+
15
+ ## Phase 1 — Correctness (P0/P1 behavior)
16
+
17
+ ### 1a) Allow external clearing via `value = undefined`
18
+
19
+ **Files**
20
+
21
+ - `root/time-picker-root.svelte`
22
+
23
+ **Changes**
24
+
25
+ - Remove the `if (value === undefined) return;` early-return in the `$effect` that observes `value`.
26
+ - When `value` becomes `undefined`, treat it as an explicit clear: reset `segmentDraft` to empty, set `valueInternal = null`, and publish `null` via `publishCommittedValue`.
27
+
28
+ **Risk**
29
+
30
+ - Controlled/uncontrolled sync regressions — initial mount with `value={undefined}` must not trigger a spurious publish.
31
+
32
+ **Validation**
33
+
34
+ - `bun run test -- --run src/lib/timepicker/root/time-picker-root.test.ts`
35
+
36
+ ### 1b) Publish `null` when all segments are cleared
37
+
38
+ **Files**
39
+
40
+ - `root/time-picker-root.svelte`
41
+
42
+ **Changes**
43
+
44
+ - In `commitFromDraft`, when `buildTimePartsFromDraft` returns `null` **and** `hasAnyRequiredValue` is `false` (every segment is empty), call `publishCommittedValue(null)` instead of doing a silent return.
45
+ - This ensures `bind:value` transitions from a valid time to `null` when the user backspaces every segment.
46
+
47
+ **Risk**
48
+
49
+ - Must avoid re-publishing `null` on every keystroke while segments are partially filled — the `hasAnyRequiredValue === true` path already handles that correctly.
50
+
51
+ **Validation**
52
+
53
+ - `bun run test -- --run src/lib/timepicker/root/time-picker-root.test.ts`
54
+
55
+ ### 2) Event forwarding consistency
56
+
57
+ **Files**
58
+
59
+ - `input/time-picker-input.svelte`
60
+ - `trigger/time-picker-trigger.svelte`
61
+ **Changes**
62
+
63
+ - Compose external/internal handlers consistently.
64
+
65
+ - User handlers accidentally suppress internal logic.
66
+
67
+ - `bun run test -- --run src/lib/timepicker/input/time-picker-input.test.ts src/lib/timepicker/trigger/time-picker-trigger.test.ts`
68
+
69
+ - Add safety restoration to current draft value when drift is detected.
70
+
71
+ **Policy**
72
+
73
+ - Implement guard + restore strategy aligned with TODO note.
74
+ **Risk**
75
+
76
+ - IME/paste edge cases and cross-browser input events.
77
+ **Validation**
78
+
79
+ - `bun run test -- --run src/lib/timepicker/segment/time-picker-segment.test.ts`
80
+
81
+ **Files**
82
+
83
+ - `segment/time-picker-segment.svelte`
84
+
85
+ - Use explicit 12h boundaries (`1`/`12`) instead of relying on clamping side effects.
86
+
87
+ - Segment keyboard tests in same run as above.
88
+
89
+ ---
90
+
91
+ ## Phase 2 — Wheel stability and interaction quality
92
+
93
+ ### 5) Wheel settle behavior and anchor sync
94
+
95
+ **Files**
96
+ **Changes**
97
+
98
+ - Increase scroll debounce fallback from 64ms to 120ms. The debounce only fires in browsers without native `scrollend` support, so higher values do not affect perceived latency. 120ms is conservative enough to avoid interrupting touch momentum on iOS Safari.
99
+ **Risk**
100
+
101
+ - Async timing loops (`scroll`, `scrollend`, debounce). The silent flag must be cleared reliably (on `scrollend` or animation completion).
102
+ **Validation**
103
+
104
+ - `bun run test -- --run src/lib/clock/wheel-column/clock-wheel-column.test.ts src/lib/clock/panel/clock-panel.test.ts`
105
+
106
+ ### 6) ResizeObserver loop mitigation
107
+
108
+ **Files**
109
+
110
+ - `../clock/wheel-column/clock-wheel-column.svelte`
111
+ - Batch measurement updates through `requestAnimationFrame` to reduce observer-loop warnings.
112
+
113
+ **Validation**
114
+
115
+ - Re-run wheel and root tests:
116
+ - `bun run test -- --run src/lib/clock/wheel-column/clock-wheel-column.test.ts src/lib/timepicker/root/time-picker-root.test.ts`
117
+
118
+ ## Phase 3 — Accessibility and API surface
119
+
120
+ ### 7) Wheel announcements and semantics
121
+
122
+ - `../clock/wheel-column/clock-wheel-column.svelte`
123
+
124
+ **Changes**
125
+
126
+ - Add `aria-roledescription` on wheel spinbutton.
127
+
128
+ **Risk**
129
+
130
+ - Over-announcement/noise for SR users.
131
+
132
+ ### 8) `isRequired` propagation
133
+
134
+ **Files**
135
+
136
+ - `root/time-picker-root.svelte`
137
+ - `root/context.ts`
138
+ - `input/time-picker-input.svelte`
139
+ - `README.md`
140
+
141
+ - Add `isRequired` to root API and context.
142
+ - Expose `aria-required` on input group.
143
+ - Document API addition.
144
+
145
+ - Input/root tests + typecheck.
146
+
147
+ - `../clock/wheel-item/clock-wheel-item.svelte`
148
+
149
+ **Changes**
150
+
151
+ - Remove default visual inline styles (opacity, cursor, font-weight, etc.) entirely. The component becomes fully headless — consumers style via `data-selected`, `data-disabled`, `data-centered` attributes.
152
+ - This aligns WheelItem with the rest of the library where components ship zero visual opinions.
153
+ **Risk**
154
+
155
+ - Technically breaking for anyone relying on the default inline styles. Acceptable at current maturity — the component is new and the old styles were inconsistent (all dropped when `class` was passed).
156
+
157
+ ## Phase 4 — Performance and code cleanup
158
+
159
+ ### 10) Formatter and locale caching
160
+
161
+ - `root/time-picker-root.svelte`
162
+ - `root/time-utils.ts`
163
+ **Changes**
164
+
165
+ - Cache `Intl.DateTimeFormat` by `locale` + `hourCycle` + `granularity`.
166
+ - Cache system-locale fallback once per component lifetime.
167
+
168
+ **Files**
169
+
170
+ **Changes**
171
+
172
+ - Skip disabled candidate computation when no min/max is set.
173
+
174
+ ### 12) Low-risk code quality cleanups
175
+
176
+ **Files**
177
+
178
+ - `root/time-utils.ts`
179
+ - `root/time-picker-root.svelte`
180
+
181
+ **Changes**
182
+
183
+ - Remove unreachable min>max branch in `isTimeOutOfRange`.
184
+ - Replace meaningless ternary `type === 'hour' ? 2 : 2` with constant.
185
+
186
+ **Validation**
187
+
188
+ ## Phase 5 — Test hardening
189
+
190
+ ### 13) Wheel test matrix expansion
191
+
192
+ **Add coverage for**
193
+
194
+ - Open alignment and external value sync.
195
+ - Focus contract (`data-focus-within`, `data-focus-visible`).
196
+ - Min/max disabled behavior.
197
+
198
+ ### 14) End-to-end wheel integration test
199
+
200
+ **Files**
201
+
202
+ - `root/time-picker-root.test.ts` and/or `wheel-column/time-picker-wheel-column.test.ts`
203
+
204
+ **Flow to cover**
205
+
206
+ - Wheel interaction → snap → `selectWheelValue` → root commit → segment UI + bound value sync.
207
+
208
+ ### 15) Segment paste/IME resilience tests
209
+
210
+ **Files**
211
+
212
+ - `segment/time-picker-segment.test.ts`
213
+
214
+ **Cases**
215
+
216
+ - Paste attempts do not corrupt draft.
217
+ - Input DOM restores to draft when mutation bypass happens.
218
+
219
+ ---
220
+
221
+ ## Execution Order (Single PR)
222
+
223
+ 1. Phase 1
224
+ 2. Phase 2
225
+ 3. Phase 3
226
+ 4. Phase 4
227
+ 5. Phase 5
228
+
229
+ This order minimizes risk: behavior correctness first, wheel timing second, API/accessibility third, refactors fourth, broad tests last.
230
+
231
+ ## Verification Checklist
232
+
233
+ ### Targeted runs after each phase
234
+
235
+ - `bun run test -- --run src/lib/timepicker/root/time-picker-root.test.ts`
236
+ - `bun run test -- --run src/lib/timepicker/input/time-picker-input.test.ts src/lib/timepicker/trigger/time-picker-trigger.test.ts`
237
+ - `bun run test -- --run src/lib/timepicker/segment/time-picker-segment.test.ts`
238
+ - `bun run test -- --run src/lib/clock/wheel-column/clock-wheel-column.test.ts src/lib/clock/panel/clock-panel.test.ts`
239
+ - `bun run test -- --run src/lib/timepicker/root/time-utils.test.ts`
240
+
241
+ ### Final gate
242
+
243
+ - `bun run typecheck`
244
+ - `bun run test -- --run`
245
+ - `bun run lint`
246
+ - `bun run build`
247
+ - `bunx changeset` — generate changeset file (CI requires it for `packages/svelte/src/**` changes)
248
+
249
+ ## Notes
250
+
251
+ - Debounce increase (64→120ms) is safe because it only acts as a fallback for browsers without `scrollend`. Modern browsers use `scrollend` as the primary settle signal.
252
+ - Preserve existing public APIs except where TODO explicitly requests additions (`isRequired`) or removals (WheelItem default styles).
253
+ - Phase 5 tests must be written against the corrected behavior from Phases 1-4, not against the current (buggy) behavior.
254
+ - Update docs/tests together with behavior changes to keep CI stable.