@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
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ import { Popover } from '../index';
3
+ import type { PopoverOpenChangeDetails } from '../root/context';
4
+
5
+ type Props = {
6
+ preventClose?: boolean;
7
+ };
8
+
9
+ let { preventClose = true }: Props = $props();
10
+
11
+ let open = $state(true);
12
+ let triggerRef = $state<HTMLElement | null>(null);
13
+
14
+ function handleOpenChange(nextOpen: boolean, details: PopoverOpenChangeDetails) {
15
+ if (!nextOpen && preventClose) {
16
+ details.cancel();
17
+ }
18
+ if (!details.isCanceled) {
19
+ open = nextOpen;
20
+ }
21
+ }
22
+ </script>
23
+
24
+ <button bind:this={triggerRef} type="button">Standalone Trigger</button>
25
+
26
+ <Popover.Content {open} {triggerRef} onOpenChange={handleOpenChange}>
27
+ <div>Standalone content</div>
28
+ </Popover.Content>
@@ -0,0 +1,6 @@
1
+ type Props = {
2
+ preventClose?: boolean;
3
+ };
4
+ declare const PopoverContentStandaloneTest: import("svelte").Component<Props, {}, "">;
5
+ type PopoverContentStandaloneTest = ReturnType<typeof PopoverContentStandaloneTest>;
6
+ export default PopoverContentStandaloneTest;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { Popover } from '../index';
3
+ import type { PopoverOpenChangeDetails } from '../root/context';
3
4
 
4
5
  type Props = {
5
6
  open?: boolean;
@@ -8,7 +9,7 @@
8
9
  shouldCloseOnInteractOutside?: boolean;
9
10
  shouldCloseOnEscape?: boolean;
10
11
  shouldCloseOnBlur?: boolean;
11
- onOpenChange?: (open: boolean) => void;
12
+ onOpenChange?: (open: boolean, details: PopoverOpenChangeDetails) => void;
12
13
  };
13
14
 
14
15
  let {
@@ -1,3 +1,4 @@
1
+ import type { PopoverOpenChangeDetails } from '../root/context';
1
2
  type Props = {
2
3
  open?: boolean;
3
4
  defaultOpen?: boolean;
@@ -5,7 +6,7 @@ type Props = {
5
6
  shouldCloseOnInteractOutside?: boolean;
6
7
  shouldCloseOnEscape?: boolean;
7
8
  shouldCloseOnBlur?: boolean;
8
- onOpenChange?: (open: boolean) => void;
9
+ onOpenChange?: (open: boolean, details: PopoverOpenChangeDetails) => void;
9
10
  };
10
11
  declare const PopoverContentTest: import("svelte").Component<Props, {}, "">;
11
12
  type PopoverContentTest = ReturnType<typeof PopoverContentTest>;
@@ -4,12 +4,21 @@
4
4
  import { onMount, onDestroy } from 'svelte';
5
5
  import { browser } from '$app/environment';
6
6
  import { floating, type ExtendedPlacement } from '../../primitives/floating';
7
- import { focusTrap } from '../../primitives/focus-trap';
7
+ import { focusTrap, type FocusTrapOptions } from '../../primitives/focus-trap';
8
8
  import { scrollLock } from '../../primitives/scroll-lock';
9
9
  import { clickOutside } from '../../primitives/click-outside';
10
10
  import { ariaHideOutside } from '../../primitives/aria-hide-outside';
11
11
  import { Portal } from '../../portal';
12
- import { getPopoverContext } from '../root/context';
12
+ import {
13
+ getPopoverContext,
14
+ type PopoverOpenChangeDetails,
15
+ type PopoverCloseReason
16
+ } from '../root/context';
17
+ import {
18
+ addTriggerBlurCleanup,
19
+ applyTriggerCloseFocusState,
20
+ clearTriggerFocusState
21
+ } from '../root/focus-state';
13
22
 
14
23
  /**
15
24
  * Popover.Content - The floating content panel.
@@ -36,13 +45,15 @@
36
45
  shouldCloseOnEscape?: boolean;
37
46
  /** Whether losing focus (blur) should close the popover. Defaults to true for non-modal popovers. */
38
47
  shouldCloseOnBlur?: boolean;
48
+ /** Element or selector to focus first when modal trap activates. */
49
+ initialFocus?: FocusTrapOptions['initialFocus'];
39
50
  // Standalone mode props (used when not inside Popover.Root)
40
51
  /** Controlled open state (standalone mode). */
41
52
  open?: boolean;
42
53
  /** Reference to the trigger element (standalone mode). */
43
54
  triggerRef?: HTMLElement | null;
44
55
  /** Callback when open state changes (standalone mode). */
45
- onOpenChange?: (open: boolean) => void;
56
+ onOpenChange?: (open: boolean, details: PopoverOpenChangeDetails) => void;
46
57
  } & Omit<HTMLAttributes<HTMLDivElement>, 'class' | 'children'>;
47
58
 
48
59
  let {
@@ -56,6 +67,7 @@
56
67
  shouldCloseOnInteractOutside = true,
57
68
  shouldCloseOnEscape = true,
58
69
  shouldCloseOnBlur,
70
+ initialFocus,
59
71
  // Standalone mode props
60
72
  open: openProp,
61
73
  triggerRef: triggerRefProp = null,
@@ -73,28 +85,64 @@
73
85
  const shouldCloseOnBlurResolved = $derived(shouldCloseOnBlur ?? isNonModal);
74
86
 
75
87
  let popoverRef: HTMLElement | undefined = $state();
88
+ let cleanupStandaloneTriggerBlurListener: (() => void) | undefined;
89
+ let pendingStandaloneTriggerCloseFocusFrame: number | undefined;
90
+ let trackedStandaloneTrigger: HTMLElement | null = null;
91
+
92
+ function clearPendingStandaloneTriggerCloseFocus() {
93
+ if (pendingStandaloneTriggerCloseFocusFrame === undefined) return;
94
+ cancelAnimationFrame(pendingStandaloneTriggerCloseFocusFrame);
95
+ pendingStandaloneTriggerCloseFocusFrame = undefined;
96
+ }
76
97
 
77
- function close() {
78
- if (isStandalone) {
79
- onOpenChangeProp?.(false);
80
- triggerRefProp?.focus();
81
- } else {
82
- ctx!.close();
83
- }
98
+ function clearStandaloneTriggerTracking() {
99
+ clearPendingStandaloneTriggerCloseFocus();
100
+ cleanupStandaloneTriggerBlurListener?.();
101
+ cleanupStandaloneTriggerBlurListener = undefined;
84
102
  }
85
103
 
86
- function handleOpenChange(value: boolean) {
104
+ function applyStandaloneTriggerCloseState(
105
+ trigger: HTMLElement,
106
+ reason: PopoverCloseReason,
107
+ event?: Event
108
+ ) {
109
+ clearStandaloneTriggerTracking();
110
+ pendingStandaloneTriggerCloseFocusFrame = requestAnimationFrame(() => {
111
+ pendingStandaloneTriggerCloseFocusFrame = undefined;
112
+ if (!trigger.isConnected) return;
113
+ applyTriggerCloseFocusState(trigger, reason, event);
114
+ cleanupStandaloneTriggerBlurListener = addTriggerBlurCleanup(trigger, true);
115
+ });
116
+ }
117
+
118
+ function close(reason: PopoverCloseReason = 'imperative-action', event?: Event) {
87
119
  if (isStandalone) {
88
- onOpenChangeProp?.(value);
120
+ let canceled = false;
121
+ const details: PopoverOpenChangeDetails = {
122
+ reason,
123
+ event,
124
+ cancel: () => {
125
+ canceled = true;
126
+ },
127
+ get isCanceled() {
128
+ return canceled;
129
+ }
130
+ };
131
+
132
+ onOpenChangeProp?.(false, details);
133
+ if (details.isCanceled) return;
134
+ if (triggerRefProp) {
135
+ applyStandaloneTriggerCloseState(triggerRefProp, reason, event);
136
+ }
89
137
  } else {
90
- ctx!.onOpenChange(value);
138
+ ctx!.close(reason, event);
91
139
  }
92
140
  }
93
141
 
94
142
  function handleKeydown(event: KeyboardEvent) {
95
143
  if (event.key === 'Escape' && isOpen && shouldCloseOnEscape) {
96
144
  event.preventDefault();
97
- close();
145
+ close('escape-key', event);
98
146
  }
99
147
  }
100
148
 
@@ -107,7 +155,7 @@
107
155
  const focusInTrigger = triggerRef?.contains(target) || target === triggerRef;
108
156
 
109
157
  if (!focusInPopover && !focusInTrigger) {
110
- handleOpenChange(false);
158
+ close('focus-out', event);
111
159
  }
112
160
  }
113
161
 
@@ -121,10 +169,28 @@
121
169
 
122
170
  // Only close on external scroll
123
171
  if (!isInsidePopover && !isInsideTrigger) {
124
- close();
172
+ close('outside-press', event);
125
173
  }
126
174
  }
127
175
 
176
+ $effect(() => {
177
+ if (!isStandalone) {
178
+ if (trackedStandaloneTrigger) {
179
+ clearTriggerFocusState(trackedStandaloneTrigger);
180
+ }
181
+ trackedStandaloneTrigger = null;
182
+ clearStandaloneTriggerTracking();
183
+ return;
184
+ }
185
+
186
+ if (trackedStandaloneTrigger && trackedStandaloneTrigger !== triggerRefProp) {
187
+ clearTriggerFocusState(trackedStandaloneTrigger);
188
+ clearStandaloneTriggerTracking();
189
+ }
190
+
191
+ trackedStandaloneTrigger = triggerRefProp;
192
+ });
193
+
128
194
  onMount(() => {
129
195
  if (!browser) return;
130
196
  document.addEventListener('keydown', handleKeydown);
@@ -134,6 +200,10 @@
134
200
 
135
201
  onDestroy(() => {
136
202
  if (!browser) return;
203
+ if (trackedStandaloneTrigger) {
204
+ clearTriggerFocusState(trackedStandaloneTrigger);
205
+ }
206
+ clearStandaloneTriggerTracking();
137
207
  document.removeEventListener('keydown', handleKeydown);
138
208
  document.removeEventListener('focusin', handleDocumentFocusIn);
139
209
  document.removeEventListener('scroll', handleScroll, true);
@@ -149,11 +219,14 @@
149
219
  aria-modal={isModal}
150
220
  use:floating={{ anchor: triggerRef, offset, placement, shouldFlip, boundaryElement }}
151
221
  use:clickOutside={{
152
- handler: close,
222
+ handler: (event) => {
223
+ event.preventDefault();
224
+ close('outside-press', event);
225
+ },
153
226
  enabled: shouldCloseOnInteractOutside,
154
227
  ignore: [triggerRef]
155
228
  }}
156
- use:focusTrap={isModal}
229
+ use:focusTrap={{ enabled: isModal, restoreFocus: false, initialFocus }}
157
230
  use:scrollLock={isModal}
158
231
  use:ariaHideOutside={isModal}
159
232
  style="position: fixed; z-index: 9999;"
@@ -1,6 +1,8 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  import { type ExtendedPlacement } from '../../primitives/floating';
4
+ import { type FocusTrapOptions } from '../../primitives/focus-trap';
5
+ import { type PopoverOpenChangeDetails } from '../root/context';
4
6
  /**
5
7
  * Popover.Content - The floating content panel.
6
8
  * Can be used inside Popover.Root (reads context) or standalone (props required).
@@ -26,12 +28,14 @@ type PopoverContentProps = {
26
28
  shouldCloseOnEscape?: boolean;
27
29
  /** Whether losing focus (blur) should close the popover. Defaults to true for non-modal popovers. */
28
30
  shouldCloseOnBlur?: boolean;
31
+ /** Element or selector to focus first when modal trap activates. */
32
+ initialFocus?: FocusTrapOptions['initialFocus'];
29
33
  /** Controlled open state (standalone mode). */
30
34
  open?: boolean;
31
35
  /** Reference to the trigger element (standalone mode). */
32
36
  triggerRef?: HTMLElement | null;
33
37
  /** Callback when open state changes (standalone mode). */
34
- onOpenChange?: (open: boolean) => void;
38
+ onOpenChange?: (open: boolean, details: PopoverOpenChangeDetails) => void;
35
39
  } & Omit<HTMLAttributes<HTMLDivElement>, 'class' | 'children'>;
36
40
  declare const PopoverContent: import("svelte").Component<PopoverContentProps, {}, "">;
37
41
  type PopoverContent = ReturnType<typeof PopoverContent>;
@@ -3,6 +3,6 @@ export { default as PopoverRoot } from './root/popover-root.svelte';
3
3
  export { default as PopoverContent } from './content/popover-content.svelte';
4
4
  export { default as PopoverTrigger } from './trigger/popover-trigger.svelte';
5
5
  export { default as PopoverTriggerButton } from './trigger/popover-trigger-button.svelte';
6
- export { getPopoverContext, setPopoverContext, type PopoverContext, getPopoverTriggerContext, setPopoverTriggerContext, type PopoverTriggerContext } from './root/context.ts';
6
+ export { getPopoverContext, setPopoverContext, type PopoverContext, type PopoverCanonicalCloseReason, type PopoverCloseReason, type PopoverOpenReason, type PopoverChangeReason, type PopoverOpenChangeDetails } from './root/context.ts';
7
7
  import * as PopoverParts from './index.parts.ts';
8
8
  export default PopoverParts;
@@ -6,9 +6,7 @@ export { default as PopoverContent } from './content/popover-content.svelte';
6
6
  export { default as PopoverTrigger } from './trigger/popover-trigger.svelte';
7
7
  export { default as PopoverTriggerButton } from './trigger/popover-trigger-button.svelte';
8
8
  // Context and types
9
- export { getPopoverContext, setPopoverContext,
10
- // Legacy aliases
11
- getPopoverTriggerContext, setPopoverTriggerContext } from './root/context.ts';
9
+ export { getPopoverContext, setPopoverContext } from './root/context.ts';
12
10
  // Default export as namespace object
13
11
  import * as PopoverParts from './index.parts.ts';
14
12
  export default PopoverParts;
@@ -7,24 +7,19 @@
7
7
  Name: `Popover.Root`
8
8
  Description: Root state container that controls open state, trigger ref, and toggle/open/close actions.
9
9
 
10
- | Prop | Type | Default | Description |
11
- | -------------- | ------------------------- | ----------- | ---------------------------------------------- |
12
- | `open` | `boolean` | `undefined` | Controlled open state. Supports `bind:open`. |
13
- | `defaultOpen` | `boolean` | `false` | Initial open state in uncontrolled mode. |
14
- | `onOpenChange` | `(open: boolean) => void` | `undefined` | Called whenever open state changes. |
15
- | `triggerRef` | `HTMLElement \| null` | `null` | Trigger reference. Supports `bind:triggerRef`. |
16
- | `children` | `Snippet` | `undefined` | Composed trigger and content parts. |
10
+ - `open`: `boolean` (default: `undefined`) — Controlled open state. Supports `bind:open`.
11
+ - `defaultOpen`: `boolean` (default: `false`) Initial open state in uncontrolled mode.
12
+ - `onOpenChange`: `(open: boolean, details) => void` (default: `undefined`) Called whenever open state changes. `details` includes `reason`, optional `event`, `cancel()`, and `isCanceled`.
13
+ - `triggerRef`: `HTMLElement | null` (default: `null`) Trigger reference. Supports `bind:triggerRef`.
14
+ - `children`: `Snippet` (default: `undefined`) Composed trigger and content parts.
17
15
 
18
16
  ### Context utilities
19
17
 
20
18
  Name: `context.ts` helpers
21
19
  Description: Context APIs used by trigger and content parts.
22
20
 
23
- | Prop | Type | Default | Description |
24
- | -------------------------- | ----------------------------------- | ------------------- | ---------------------------------------- |
25
- | `setPopoverContext` | `(ctx: PopoverContext) => void` | `-` | Registers popover context. |
26
- | `getPopoverContext` | `() => PopoverContext \| undefined` | `-` | Returns popover context when available. |
27
- | `PopoverContext` | `type` | `-` | Context contract with state and actions. |
28
- | `PopoverTriggerContext` | `type alias` | `PopoverContext` | Backward-compatible alias. |
29
- | `setPopoverTriggerContext` | `alias` | `setPopoverContext` | Backward-compatible alias. |
30
- | `getPopoverTriggerContext` | `alias` | `getPopoverContext` | Backward-compatible alias. |
21
+ | Prop | Type | Default | Description |
22
+ | ------------------- | ----------------------------------- | ------- | ---------------------------------------- |
23
+ | `setPopoverContext` | `(ctx: PopoverContext) => void` | `-` | Registers popover context. |
24
+ | `getPopoverContext` | `() => PopoverContext \| undefined` | `-` | Returns popover context when available. |
25
+ | `PopoverContext` | `type` | `-` | Context contract with state and actions. |
@@ -1,7 +1,19 @@
1
+ export type PopoverCanonicalCloseReason = 'escape-key' | 'outside-press' | 'focus-out' | 'close-press' | 'imperative-action' | 'none';
2
+ export type PopoverCloseReason = PopoverCanonicalCloseReason;
3
+ export type PopoverOpenReason = 'trigger-press' | 'imperative-action' | 'none';
4
+ export type PopoverChangeReason = PopoverOpenReason | PopoverCloseReason;
5
+ export type PopoverOpenChangeDetails = {
6
+ reason: PopoverChangeReason;
7
+ event?: Event;
8
+ cancel: () => void;
9
+ isCanceled: boolean;
10
+ };
1
11
  /**
2
12
  * Context shared between Popover components (Root, Trigger, Content).
3
13
  */
4
14
  export type PopoverContext = {
15
+ /** Reason why popover is closing */
16
+ closeReason: PopoverCanonicalCloseReason;
5
17
  /** Whether the popover is open */
6
18
  isOpen: boolean;
7
19
  /** Reference to the trigger element */
@@ -9,16 +21,13 @@ export type PopoverContext = {
9
21
  /** Set the trigger ref (used by Trigger component) */
10
22
  setTriggerRef: (el: HTMLElement | null) => void;
11
23
  /** Toggle popover open state */
12
- toggle: () => void;
24
+ toggle: (reason?: PopoverOpenReason, event?: Event) => void;
13
25
  /** Open the popover */
14
- open: () => void;
26
+ open: (reason?: PopoverOpenReason, event?: Event) => void;
15
27
  /** Close the popover and return focus to trigger */
16
- close: () => void;
28
+ close: (reason?: PopoverCloseReason, event?: Event) => void;
17
29
  /** Called when popover open state changes */
18
- onOpenChange: (open: boolean) => void;
30
+ onOpenChange: (open: boolean, details: PopoverOpenChangeDetails) => void;
19
31
  };
20
32
  export declare function setPopoverContext(ctx: PopoverContext): void;
21
33
  export declare function getPopoverContext(): PopoverContext | undefined;
22
- export type PopoverTriggerContext = PopoverContext;
23
- export declare const setPopoverTriggerContext: typeof setPopoverContext;
24
- export declare const getPopoverTriggerContext: typeof getPopoverContext;
@@ -6,5 +6,3 @@ export function setPopoverContext(ctx) {
6
6
  export function getPopoverContext() {
7
7
  return getContext(POPOVER_CONTEXT_KEY);
8
8
  }
9
- export const setPopoverTriggerContext = setPopoverContext;
10
- export const getPopoverTriggerContext = getPopoverContext;
@@ -0,0 +1,4 @@
1
+ import type { PopoverCloseReason } from './context';
2
+ export declare function clearTriggerFocusState(trigger: HTMLElement): void;
3
+ export declare function applyTriggerCloseFocusState(trigger: HTMLElement, reason: PopoverCloseReason, event?: Event): void;
4
+ export declare function addTriggerBlurCleanup(trigger: HTMLElement, once?: boolean): () => void;
@@ -0,0 +1,33 @@
1
+ import { focusWithModality, resolveCloseInteractionModality } from '../../primitives/input-modality';
2
+ export function clearTriggerFocusState(trigger) {
3
+ delete trigger.dataset.focused;
4
+ delete trigger.dataset.focusVisible;
5
+ }
6
+ export function applyTriggerCloseFocusState(trigger, reason, event) {
7
+ const closeModality = resolveCloseInteractionModality(reason, event);
8
+ focusWithModality(trigger, closeModality);
9
+ if (reason === 'outside-press' || reason === 'escape-key') {
10
+ trigger.dataset.focused = 'true';
11
+ }
12
+ else {
13
+ delete trigger.dataset.focused;
14
+ }
15
+ if (closeModality === 'keyboard') {
16
+ trigger.dataset.focusVisible = 'true';
17
+ }
18
+ else {
19
+ delete trigger.dataset.focusVisible;
20
+ }
21
+ }
22
+ export function addTriggerBlurCleanup(trigger, once = false) {
23
+ const handleBlur = () => {
24
+ clearTriggerFocusState(trigger);
25
+ if (once) {
26
+ trigger.removeEventListener('blur', handleBlur);
27
+ }
28
+ };
29
+ trigger.addEventListener('blur', handleBlur);
30
+ return () => {
31
+ trigger.removeEventListener('blur', handleBlur);
32
+ };
33
+ }
@@ -1,6 +1,20 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
- import { setPopoverContext, type PopoverContext } from './context';
3
+ import { onDestroy } from 'svelte';
4
+ import {
5
+ setPopoverContext,
6
+ type PopoverCanonicalCloseReason,
7
+ type PopoverChangeReason,
8
+ type PopoverCloseReason,
9
+ type PopoverOpenChangeDetails,
10
+ type PopoverOpenReason,
11
+ type PopoverContext
12
+ } from './context';
13
+ import {
14
+ addTriggerBlurCleanup,
15
+ applyTriggerCloseFocusState,
16
+ clearTriggerFocusState
17
+ } from './focus-state';
4
18
 
5
19
  /**
6
20
  * Popover.Root - State management wrapper for Popover components.
@@ -12,7 +26,7 @@
12
26
  /** Initial open state for uncontrolled mode. */
13
27
  defaultOpen?: boolean;
14
28
  /** Callback when open state changes. */
15
- onOpenChange?: (open: boolean) => void;
29
+ onOpenChange?: (open: boolean, details: PopoverOpenChangeDetails) => void;
16
30
  /** Reference to the trigger element. Can be set manually or via Popover.Trigger. */
17
31
  triggerRef?: HTMLElement | null;
18
32
  /** Children (Trigger and Content) */
@@ -26,6 +40,9 @@
26
40
  triggerRef = $bindable<HTMLElement | null>(null),
27
41
  children
28
42
  }: PopoverRootProps = $props();
43
+ let closeReason: PopoverCanonicalCloseReason = $state('none');
44
+ let cleanupTriggerBlurListener: (() => void) | undefined;
45
+ let pendingTriggerCloseFocusFrame: number | undefined;
29
46
 
30
47
  // Use function to capture initial value only (not reactive)
31
48
  let isOpenInternal = $state((() => defaultOpen)());
@@ -33,39 +50,90 @@
33
50
  const isControlled = $derived(open !== undefined);
34
51
  const isOpen = $derived(isControlled ? open! : isOpenInternal);
35
52
 
36
- function setOpen(value: boolean) {
37
- if (isControlled) {
38
- onOpenChange?.(value);
39
- } else {
53
+ function setOpenWithDetails(
54
+ value: boolean,
55
+ incomingDetails: { reason: PopoverChangeReason; event?: Event }
56
+ ) {
57
+ let canceled = false;
58
+ const details: PopoverOpenChangeDetails = {
59
+ reason: incomingDetails.reason,
60
+ event: incomingDetails.event,
61
+ cancel: () => {
62
+ canceled = true;
63
+ },
64
+ get isCanceled() {
65
+ return canceled;
66
+ }
67
+ };
68
+
69
+ onOpenChange?.(value, details);
70
+ if (details.isCanceled) return;
71
+
72
+ if (!isControlled) {
40
73
  isOpenInternal = value;
41
- onOpenChange?.(value);
42
74
  }
43
- // Sync bindable prop
75
+
44
76
  open = value;
45
77
  }
46
78
 
47
- function toggle() {
48
- setOpen(!isOpen);
79
+ function toggle(reason: PopoverOpenReason = 'trigger-press', event?: Event) {
80
+ setOpenWithDetails(!isOpen, { reason, event });
81
+ }
82
+
83
+ function openPopover(reason: PopoverOpenReason = 'imperative-action', event?: Event) {
84
+ closeReason = 'none';
85
+ setOpenWithDetails(true, { reason, event });
49
86
  }
50
87
 
51
- function openPopover() {
52
- setOpen(true);
88
+ function clearPendingTriggerCloseFocus() {
89
+ if (pendingTriggerCloseFocusFrame === undefined) return;
90
+ cancelAnimationFrame(pendingTriggerCloseFocusFrame);
91
+ pendingTriggerCloseFocusFrame = undefined;
53
92
  }
54
93
 
55
- function closePopover() {
56
- setOpen(false);
57
- triggerRef?.focus();
94
+ function scheduleTriggerCloseFocus(
95
+ trigger: HTMLElement,
96
+ reason: PopoverCanonicalCloseReason,
97
+ event?: Event
98
+ ) {
99
+ clearPendingTriggerCloseFocus();
100
+ pendingTriggerCloseFocusFrame = requestAnimationFrame(() => {
101
+ pendingTriggerCloseFocusFrame = undefined;
102
+ if (!trigger.isConnected) return;
103
+ applyTriggerCloseFocusState(trigger, reason, event);
104
+ });
105
+ }
106
+
107
+ function closePopover(reason: PopoverCloseReason = 'imperative-action', event?: Event) {
108
+ closeReason = reason;
109
+ const wasOpen = isOpen;
110
+ setOpenWithDetails(false, { reason, event });
111
+ if (!wasOpen || isOpen) return;
112
+ if (!triggerRef) return;
113
+ scheduleTriggerCloseFocus(triggerRef, reason, event);
58
114
  }
59
115
 
60
116
  function setTriggerRef(el: HTMLElement | null) {
117
+ clearPendingTriggerCloseFocus();
118
+ cleanupTriggerBlurListener?.();
119
+ cleanupTriggerBlurListener = undefined;
120
+ if (triggerRef && triggerRef !== el) {
121
+ clearTriggerFocusState(triggerRef);
122
+ }
61
123
  triggerRef = el;
124
+ if (!triggerRef) return;
125
+ const currentTrigger = triggerRef;
126
+ cleanupTriggerBlurListener = addTriggerBlurCleanup(currentTrigger);
62
127
  }
63
128
 
64
- function handleOpenChange(newOpen: boolean) {
65
- setOpen(newOpen);
129
+ function handleOpenChange(newOpen: boolean, details: PopoverOpenChangeDetails) {
130
+ setOpenWithDetails(newOpen, { reason: details.reason, event: details.event });
66
131
  }
67
132
 
68
133
  const ctx: PopoverContext = {
134
+ get closeReason() {
135
+ return closeReason;
136
+ },
69
137
  get isOpen() {
70
138
  return isOpen;
71
139
  },
@@ -80,6 +148,11 @@
80
148
  };
81
149
 
82
150
  setPopoverContext(ctx);
151
+
152
+ onDestroy(() => {
153
+ clearPendingTriggerCloseFocus();
154
+ cleanupTriggerBlurListener?.();
155
+ });
83
156
  </script>
84
157
 
85
158
  {#if children}
@@ -1,4 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
+ import { type PopoverOpenChangeDetails } from './context';
2
3
  /**
3
4
  * Popover.Root - State management wrapper for Popover components.
4
5
  * Provides context for Trigger and Content children.
@@ -9,7 +10,7 @@ type PopoverRootProps = {
9
10
  /** Initial open state for uncontrolled mode. */
10
11
  defaultOpen?: boolean;
11
12
  /** Callback when open state changes. */
12
- onOpenChange?: (open: boolean) => void;
13
+ onOpenChange?: (open: boolean, details: PopoverOpenChangeDetails) => void;
13
14
  /** Reference to the trigger element. Can be set manually or via Popover.Trigger. */
14
15
  triggerRef?: HTMLElement | null;
15
16
  /** Children (Trigger and Content) */
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { Popover } from '../index';
3
+ import type { PopoverOpenChangeDetails } from './context';
3
4
 
4
5
  type Props = {
5
6
  open?: boolean;
@@ -7,7 +8,7 @@
7
8
  isNonModal?: boolean;
8
9
  shouldCloseOnInteractOutside?: boolean;
9
10
  shouldCloseOnEscape?: boolean;
10
- onOpenChange?: (open: boolean) => void;
11
+ onOpenChange?: (open: boolean, details: PopoverOpenChangeDetails) => void;
11
12
  };
12
13
 
13
14
  let {
@@ -1,10 +1,11 @@
1
+ import type { PopoverOpenChangeDetails } from './context';
1
2
  type Props = {
2
3
  open?: boolean;
3
4
  defaultOpen?: boolean;
4
5
  isNonModal?: boolean;
5
6
  shouldCloseOnInteractOutside?: boolean;
6
7
  shouldCloseOnEscape?: boolean;
7
- onOpenChange?: (open: boolean) => void;
8
+ onOpenChange?: (open: boolean, details: PopoverOpenChangeDetails) => void;
8
9
  };
9
10
  declare const PopoverTest: import("svelte").Component<Props, {}, "">;
10
11
  type PopoverTest = ReturnType<typeof PopoverTest>;
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLButtonAttributes } from 'svelte/elements';
4
- import { getPopoverTriggerContext } from '../root/context';
4
+ import { getPopoverContext } from '../root/context';
5
5
 
6
6
  type PopoverTriggerButtonProps = Omit<
7
7
  HTMLButtonAttributes,
@@ -17,7 +17,7 @@
17
17
  let { children, class: className = '', ...restProps }: PopoverTriggerButtonProps = $props();
18
18
 
19
19
  let buttonRef: HTMLButtonElement | null = $state(null);
20
- const ctx = getPopoverTriggerContext();
20
+ const ctx = getPopoverContext();
21
21
 
22
22
  $effect(() => {
23
23
  if (buttonRef && ctx) {
@@ -25,8 +25,8 @@
25
25
  }
26
26
  });
27
27
 
28
- function handleClick() {
29
- ctx?.toggle();
28
+ function handleClick(event: MouseEvent) {
29
+ ctx?.toggle('trigger-press', event);
30
30
  }
31
31
  </script>
32
32