@vielzeug/craftit 2.1.0 → 3.0.3

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 (325) hide show
  1. package/README.md +58 -124
  2. package/dist/controls/a11y-control.cjs +1 -1
  3. package/dist/controls/a11y-control.cjs.map +1 -1
  4. package/dist/controls/a11y-control.d.ts +1 -1
  5. package/dist/controls/a11y-control.d.ts.map +1 -1
  6. package/dist/controls/a11y-control.js +1 -1
  7. package/dist/controls/a11y-control.js.map +1 -1
  8. package/dist/controls/checkable-control.cjs +1 -1
  9. package/dist/controls/checkable-control.cjs.map +1 -1
  10. package/dist/controls/checkable-control.d.ts +7 -7
  11. package/dist/controls/checkable-control.d.ts.map +1 -1
  12. package/dist/controls/checkable-control.js +1 -1
  13. package/dist/controls/checkable-control.js.map +1 -1
  14. package/dist/controls/choice-field-control.cjs +2 -0
  15. package/dist/controls/choice-field-control.cjs.map +1 -0
  16. package/dist/controls/choice-field-control.d.ts +3 -0
  17. package/dist/controls/choice-field-control.d.ts.map +1 -0
  18. package/dist/controls/choice-field-control.js +2 -0
  19. package/dist/controls/choice-field-control.js.map +1 -0
  20. package/dist/controls/field-control.cjs +1 -1
  21. package/dist/controls/field-control.cjs.map +1 -1
  22. package/dist/controls/field-control.d.ts +28 -73
  23. package/dist/controls/field-control.d.ts.map +1 -1
  24. package/dist/controls/field-control.js +1 -1
  25. package/dist/controls/field-control.js.map +1 -1
  26. package/dist/controls/index.d.ts +11 -9
  27. package/dist/controls/index.d.ts.map +1 -1
  28. package/dist/controls/internal/control-state.cjs +1 -1
  29. package/dist/controls/internal/control-state.cjs.map +1 -1
  30. package/dist/controls/internal/control-state.d.ts +6 -4
  31. package/dist/controls/internal/control-state.d.ts.map +1 -1
  32. package/dist/controls/internal/control-state.js +1 -1
  33. package/dist/controls/internal/control-state.js.map +1 -1
  34. package/dist/controls/internal/keyboard-utils.cjs.map +1 -1
  35. package/dist/controls/internal/keyboard-utils.js.map +1 -1
  36. package/dist/controls/internal/number-utils.cjs.map +1 -1
  37. package/dist/controls/internal/number-utils.js.map +1 -1
  38. package/dist/controls/internal/validation-utils.cjs.map +1 -1
  39. package/dist/controls/internal/validation-utils.js.map +1 -1
  40. package/dist/controls/list-control.cjs +1 -1
  41. package/dist/controls/list-control.cjs.map +1 -1
  42. package/dist/controls/list-control.d.ts +10 -8
  43. package/dist/controls/list-control.d.ts.map +1 -1
  44. package/dist/controls/list-control.js +1 -1
  45. package/dist/controls/list-control.js.map +1 -1
  46. package/dist/controls/overlay-control.cjs +1 -1
  47. package/dist/controls/overlay-control.cjs.map +1 -1
  48. package/dist/controls/overlay-control.d.ts +17 -14
  49. package/dist/controls/overlay-control.d.ts.map +1 -1
  50. package/dist/controls/overlay-control.js +1 -1
  51. package/dist/controls/overlay-control.js.map +1 -1
  52. package/dist/controls/popup-list-control.cjs +2 -0
  53. package/dist/controls/popup-list-control.cjs.map +1 -0
  54. package/dist/controls/popup-list-control.d.ts +160 -0
  55. package/dist/controls/popup-list-control.d.ts.map +1 -0
  56. package/dist/controls/popup-list-control.js +2 -0
  57. package/dist/controls/popup-list-control.js.map +1 -0
  58. package/dist/controls/press-control.cjs.map +1 -1
  59. package/dist/controls/press-control.js.map +1 -1
  60. package/dist/controls/slider-control.cjs.map +1 -1
  61. package/dist/controls/slider-control.js.map +1 -1
  62. package/dist/controls/spinner-control.cjs.map +1 -1
  63. package/dist/controls/spinner-control.js.map +1 -1
  64. package/dist/controls/swipe-control.cjs +2 -0
  65. package/dist/controls/swipe-control.cjs.map +1 -0
  66. package/dist/controls/swipe-control.d.ts +32 -0
  67. package/dist/controls/swipe-control.d.ts.map +1 -0
  68. package/dist/controls/swipe-control.js +2 -0
  69. package/dist/controls/swipe-control.js.map +1 -0
  70. package/dist/controls/text-field-control.cjs +2 -0
  71. package/dist/controls/text-field-control.cjs.map +1 -0
  72. package/dist/controls/text-field-control.d.ts +3 -0
  73. package/dist/controls/text-field-control.d.ts.map +1 -0
  74. package/dist/controls/text-field-control.js +2 -0
  75. package/dist/controls/text-field-control.js.map +1 -0
  76. package/dist/controls.cjs +1 -1
  77. package/dist/controls.js +1 -1
  78. package/dist/craftit.cjs +1 -1
  79. package/dist/craftit.cjs.map +1 -1
  80. package/dist/craftit.js +1 -1
  81. package/dist/craftit.js.map +1 -1
  82. package/dist/directives/classMap.cjs +2 -0
  83. package/dist/directives/classMap.cjs.map +1 -0
  84. package/dist/directives/classMap.d.ts +19 -0
  85. package/dist/directives/classMap.d.ts.map +1 -0
  86. package/dist/directives/classMap.js +2 -0
  87. package/dist/directives/classMap.js.map +1 -0
  88. package/dist/directives/each.cjs +1 -1
  89. package/dist/directives/each.cjs.map +1 -1
  90. package/dist/directives/each.d.ts +5 -30
  91. package/dist/directives/each.d.ts.map +1 -1
  92. package/dist/directives/each.js +1 -1
  93. package/dist/directives/each.js.map +1 -1
  94. package/dist/directives/guard.cjs +2 -0
  95. package/dist/directives/guard.cjs.map +1 -0
  96. package/dist/directives/guard.d.ts +10 -0
  97. package/dist/directives/guard.d.ts.map +1 -0
  98. package/dist/directives/guard.js +2 -0
  99. package/dist/directives/guard.js.map +1 -0
  100. package/dist/directives/live.cjs +2 -0
  101. package/dist/directives/live.cjs.map +1 -0
  102. package/dist/directives/live.d.ts +23 -0
  103. package/dist/directives/live.d.ts.map +1 -0
  104. package/dist/directives/live.js +2 -0
  105. package/dist/directives/live.js.map +1 -0
  106. package/dist/directives/raw.cjs +1 -1
  107. package/dist/directives/raw.cjs.map +1 -1
  108. package/dist/directives/raw.d.ts +3 -5
  109. package/dist/directives/raw.d.ts.map +1 -1
  110. package/dist/directives/raw.js +1 -1
  111. package/dist/directives/raw.js.map +1 -1
  112. package/dist/directives/resource.cjs +2 -0
  113. package/dist/directives/resource.cjs.map +1 -0
  114. package/dist/directives/resource.d.ts +32 -0
  115. package/dist/directives/resource.d.ts.map +1 -0
  116. package/dist/directives/resource.js +2 -0
  117. package/dist/directives/resource.js.map +1 -0
  118. package/dist/directives/styleMap.cjs +2 -0
  119. package/dist/directives/styleMap.cjs.map +1 -0
  120. package/dist/directives/styleMap.d.ts +11 -0
  121. package/dist/directives/styleMap.d.ts.map +1 -0
  122. package/dist/directives/styleMap.js +2 -0
  123. package/dist/directives/styleMap.js.map +1 -0
  124. package/dist/directives/when.cjs +1 -1
  125. package/dist/directives/when.cjs.map +1 -1
  126. package/dist/directives/when.d.ts +6 -19
  127. package/dist/directives/when.d.ts.map +1 -1
  128. package/dist/directives/when.js +1 -1
  129. package/dist/directives/when.js.map +1 -1
  130. package/dist/errors.cjs +2 -0
  131. package/dist/errors.cjs.map +1 -0
  132. package/dist/errors.d.ts +12 -0
  133. package/dist/errors.d.ts.map +1 -0
  134. package/dist/errors.js +2 -0
  135. package/dist/errors.js.map +1 -0
  136. package/dist/form.cjs +1 -1
  137. package/dist/form.cjs.map +1 -1
  138. package/dist/form.d.ts +3 -17
  139. package/dist/form.d.ts.map +1 -1
  140. package/dist/form.js +1 -1
  141. package/dist/form.js.map +1 -1
  142. package/dist/host.cjs +1 -1
  143. package/dist/host.cjs.map +1 -1
  144. package/dist/host.d.ts +40 -37
  145. package/dist/host.d.ts.map +1 -1
  146. package/dist/host.js +1 -1
  147. package/dist/host.js.map +1 -1
  148. package/dist/index.cjs +1 -1
  149. package/dist/index.d.ts +16 -8
  150. package/dist/index.d.ts.map +1 -1
  151. package/dist/index.js +1 -1
  152. package/dist/internal.cjs +1 -1
  153. package/dist/internal.cjs.map +1 -1
  154. package/dist/internal.d.ts +60 -120
  155. package/dist/internal.d.ts.map +1 -1
  156. package/dist/internal.js +1 -1
  157. package/dist/internal.js.map +1 -1
  158. package/dist/observers/index.d.ts +1 -0
  159. package/dist/observers/index.d.ts.map +1 -1
  160. package/dist/observers/intersection-observe.cjs +1 -1
  161. package/dist/observers/intersection-observe.cjs.map +1 -1
  162. package/dist/observers/intersection-observe.d.ts +1 -1
  163. package/dist/observers/intersection-observe.js +1 -1
  164. package/dist/observers/intersection-observe.js.map +1 -1
  165. package/dist/observers/media-observe.cjs +1 -1
  166. package/dist/observers/media-observe.cjs.map +1 -1
  167. package/dist/observers/media-observe.d.ts +1 -1
  168. package/dist/observers/media-observe.js +1 -1
  169. package/dist/observers/media-observe.js.map +1 -1
  170. package/dist/observers/mutation-observe.cjs +2 -0
  171. package/dist/observers/mutation-observe.cjs.map +1 -0
  172. package/dist/observers/mutation-observe.d.ts +10 -0
  173. package/dist/observers/mutation-observe.d.ts.map +1 -0
  174. package/dist/observers/mutation-observe.js +2 -0
  175. package/dist/observers/mutation-observe.js.map +1 -0
  176. package/dist/observers/resize-observe.cjs +1 -1
  177. package/dist/observers/resize-observe.cjs.map +1 -1
  178. package/dist/observers/resize-observe.d.ts +1 -1
  179. package/dist/observers/resize-observe.js +1 -1
  180. package/dist/observers/resize-observe.js.map +1 -1
  181. package/dist/observers.cjs +1 -1
  182. package/dist/observers.js +1 -1
  183. package/dist/props.cjs +1 -1
  184. package/dist/props.cjs.map +1 -1
  185. package/dist/props.d.ts +18 -31
  186. package/dist/props.d.ts.map +1 -1
  187. package/dist/props.js +1 -1
  188. package/dist/props.js.map +1 -1
  189. package/dist/registration.cjs +1 -1
  190. package/dist/registration.cjs.map +1 -1
  191. package/dist/registration.d.ts +27 -7
  192. package/dist/registration.d.ts.map +1 -1
  193. package/dist/registration.js +1 -1
  194. package/dist/registration.js.map +1 -1
  195. package/dist/runtime.cjs +1 -1
  196. package/dist/runtime.cjs.map +1 -1
  197. package/dist/runtime.d.ts +29 -17
  198. package/dist/runtime.d.ts.map +1 -1
  199. package/dist/runtime.js +1 -1
  200. package/dist/runtime.js.map +1 -1
  201. package/dist/template-bindings.cjs +1 -1
  202. package/dist/template-bindings.cjs.map +1 -1
  203. package/dist/template-bindings.d.ts +10 -47
  204. package/dist/template-bindings.d.ts.map +1 -1
  205. package/dist/template-bindings.js +1 -1
  206. package/dist/template-bindings.js.map +1 -1
  207. package/dist/template-compiler.cjs +1 -1
  208. package/dist/template-compiler.cjs.map +1 -1
  209. package/dist/template-compiler.d.ts +1 -21
  210. package/dist/template-compiler.d.ts.map +1 -1
  211. package/dist/template-compiler.js +1 -1
  212. package/dist/template-compiler.js.map +1 -1
  213. package/dist/testing/testing.cjs +1 -1
  214. package/dist/testing/testing.cjs.map +1 -1
  215. package/dist/testing/testing.d.ts +12 -5
  216. package/dist/testing/testing.d.ts.map +1 -1
  217. package/dist/testing/testing.js +1 -1
  218. package/dist/testing/testing.js.map +1 -1
  219. package/package.json +6 -12
  220. package/dist/component.cjs +0 -2
  221. package/dist/component.cjs.map +0 -1
  222. package/dist/component.d.ts +0 -39
  223. package/dist/component.d.ts.map +0 -1
  224. package/dist/component.js +0 -2
  225. package/dist/component.js.map +0 -1
  226. package/dist/controls/list-key-control.cjs +0 -2
  227. package/dist/controls/list-key-control.cjs.map +0 -1
  228. package/dist/controls/list-key-control.d.ts +0 -14
  229. package/dist/controls/list-key-control.d.ts.map +0 -1
  230. package/dist/controls/list-key-control.js +0 -2
  231. package/dist/controls/list-key-control.js.map +0 -1
  232. package/dist/directives/attr.cjs +0 -2
  233. package/dist/directives/attr.cjs.map +0 -1
  234. package/dist/directives/attr.d.ts +0 -12
  235. package/dist/directives/attr.d.ts.map +0 -1
  236. package/dist/directives/attr.js +0 -2
  237. package/dist/directives/attr.js.map +0 -1
  238. package/dist/directives/bind.cjs +0 -2
  239. package/dist/directives/bind.cjs.map +0 -1
  240. package/dist/directives/bind.d.ts +0 -38
  241. package/dist/directives/bind.d.ts.map +0 -1
  242. package/dist/directives/bind.js +0 -2
  243. package/dist/directives/bind.js.map +0 -1
  244. package/dist/directives/choose.cjs +0 -2
  245. package/dist/directives/choose.cjs.map +0 -1
  246. package/dist/directives/choose.d.ts +0 -39
  247. package/dist/directives/choose.d.ts.map +0 -1
  248. package/dist/directives/choose.js +0 -2
  249. package/dist/directives/choose.js.map +0 -1
  250. package/dist/directives/classes.cjs +0 -2
  251. package/dist/directives/classes.cjs.map +0 -1
  252. package/dist/directives/classes.d.ts +0 -20
  253. package/dist/directives/classes.d.ts.map +0 -1
  254. package/dist/directives/classes.js +0 -2
  255. package/dist/directives/classes.js.map +0 -1
  256. package/dist/directives/index.d.ts +0 -13
  257. package/dist/directives/index.d.ts.map +0 -1
  258. package/dist/directives/memo.cjs +0 -2
  259. package/dist/directives/memo.cjs.map +0 -1
  260. package/dist/directives/memo.d.ts +0 -27
  261. package/dist/directives/memo.d.ts.map +0 -1
  262. package/dist/directives/memo.js +0 -2
  263. package/dist/directives/memo.js.map +0 -1
  264. package/dist/directives/on.cjs +0 -2
  265. package/dist/directives/on.cjs.map +0 -1
  266. package/dist/directives/on.d.ts +0 -25
  267. package/dist/directives/on.d.ts.map +0 -1
  268. package/dist/directives/on.js +0 -2
  269. package/dist/directives/on.js.map +0 -1
  270. package/dist/directives/spread.cjs +0 -2
  271. package/dist/directives/spread.cjs.map +0 -1
  272. package/dist/directives/spread.d.ts +0 -14
  273. package/dist/directives/spread.d.ts.map +0 -1
  274. package/dist/directives/spread.js +0 -2
  275. package/dist/directives/spread.js.map +0 -1
  276. package/dist/directives/style.cjs +0 -2
  277. package/dist/directives/style.cjs.map +0 -1
  278. package/dist/directives/style.d.ts +0 -22
  279. package/dist/directives/style.d.ts.map +0 -1
  280. package/dist/directives/style.js +0 -2
  281. package/dist/directives/style.js.map +0 -1
  282. package/dist/directives/until.cjs +0 -2
  283. package/dist/directives/until.cjs.map +0 -1
  284. package/dist/directives/until.d.ts +0 -26
  285. package/dist/directives/until.d.ts.map +0 -1
  286. package/dist/directives/until.js +0 -2
  287. package/dist/directives/until.js.map +0 -1
  288. package/dist/directives.cjs +0 -1
  289. package/dist/directives.js +0 -1
  290. package/dist/runtime-bindings.cjs +0 -2
  291. package/dist/runtime-bindings.cjs.map +0 -1
  292. package/dist/runtime-bindings.d.ts +0 -6
  293. package/dist/runtime-bindings.d.ts.map +0 -1
  294. package/dist/runtime-bindings.js +0 -2
  295. package/dist/runtime-bindings.js.map +0 -1
  296. package/dist/runtime-core.cjs +0 -2
  297. package/dist/runtime-core.cjs.map +0 -1
  298. package/dist/runtime-core.d.ts +0 -21
  299. package/dist/runtime-core.d.ts.map +0 -1
  300. package/dist/runtime-core.js +0 -2
  301. package/dist/runtime-core.js.map +0 -1
  302. package/dist/runtime-lifecycle.cjs +0 -2
  303. package/dist/runtime-lifecycle.cjs.map +0 -1
  304. package/dist/runtime-lifecycle.d.ts +0 -24
  305. package/dist/runtime-lifecycle.d.ts.map +0 -1
  306. package/dist/runtime-lifecycle.js +0 -2
  307. package/dist/runtime-lifecycle.js.map +0 -1
  308. package/dist/template-dom.cjs +0 -2
  309. package/dist/template-dom.cjs.map +0 -1
  310. package/dist/template-dom.d.ts +0 -13
  311. package/dist/template-dom.d.ts.map +0 -1
  312. package/dist/template-dom.js +0 -2
  313. package/dist/template-dom.js.map +0 -1
  314. package/dist/template-html.cjs +0 -2
  315. package/dist/template-html.cjs.map +0 -1
  316. package/dist/template-html.d.ts +0 -23
  317. package/dist/template-html.d.ts.map +0 -1
  318. package/dist/template-html.js +0 -2
  319. package/dist/template-html.js.map +0 -1
  320. package/dist/template.cjs +0 -2
  321. package/dist/template.cjs.map +0 -1
  322. package/dist/template.d.ts +0 -10
  323. package/dist/template.d.ts.map +0 -1
  324. package/dist/template.js +0 -2
  325. package/dist/template.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"list-control.cjs","names":[],"sources":["../../src/controls/list-control.ts"],"sourcesContent":["import { findBackward, findForward } from './internal/validation-utils';\n\nexport type ListNavigationOptions<T> = {\n getIndex: () => number;\n getItems: () => T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n loop?: boolean;\n setIndex: (index: number) => void;\n};\n\nexport type ListControlResult = {\n index: number;\n moved: boolean;\n};\n\nexport type ListControl<T> = {\n first(): ListControlResult;\n getActiveItem(): T | undefined;\n last(): ListControlResult;\n next(): ListControlResult;\n prev(): ListControlResult;\n reset(): void;\n};\n\nexport const createListControl = <T>(options: ListNavigationOptions<T>): ListControl<T> => {\n const isDisabled = (item: T, index: number): boolean =>\n options.isItemDisabled?.(item, index) ?? (item as any).disabled;\n\n const result = (idx: number, current: number): ListControlResult => {\n return {\n index: Math.max(idx, -1),\n moved: idx >= 0 && idx !== current,\n };\n };\n\n const commitIndex = (idx: number, current: number): ListControlResult => {\n if (idx >= 0) options.setIndex(idx);\n\n return result(idx, current);\n };\n\n const findEnabledIndex = (items: T[], start: number, direction: 'forward' | 'backward'): number => {\n if (direction === 'forward') return findForward(items, start, (item, i) => !isDisabled(item, i));\n\n return findBackward(items, start, (item, i) => !isDisabled(item, i));\n };\n\n const first = (): ListControlResult => {\n const items = options.getItems();\n const current = options.getIndex();\n\n if (!items.length) return result(-1, current);\n\n return commitIndex(findEnabledIndex(items, 0, 'forward'), current);\n };\n\n const last = (): ListControlResult => {\n const items = options.getItems();\n const current = options.getIndex();\n\n if (!items.length) return result(-1, current);\n\n return commitIndex(findEnabledIndex(items, items.length - 1, 'backward'), current);\n };\n\n const move = (direction: 'forward' | 'backward'): ListControlResult => {\n const items = options.getItems();\n const current = options.getIndex();\n\n if (!items.length) return result(-1, current);\n\n const start = direction === 'forward' ? Math.max(0, current + 1) : current - 1;\n const idx = findEnabledIndex(items, start, direction);\n\n if (idx >= 0) return commitIndex(idx, current);\n\n if (options.loop) {\n const wrapStart = direction === 'forward' ? 0 : items.length - 1;\n const wrapped = findEnabledIndex(items, wrapStart, direction);\n\n if (wrapped >= 0) return commitIndex(wrapped, current);\n }\n\n return result(current, current);\n };\n\n const next = (): ListControlResult => move('forward');\n\n const prev = (): ListControlResult => move('backward');\n\n const getActiveItem = (): T | undefined => {\n const items = options.getItems();\n const index = options.getIndex();\n\n return index >= 0 && index < items.length ? items[index] : undefined;\n };\n\n const reset = (): void => {\n options.setIndex(-1);\n };\n\n return {\n first,\n getActiveItem,\n last,\n next,\n prev,\n reset,\n };\n};\n"],"mappings":"mDAwBA,IAAa,EAAwB,GAAsD,CACzF,IAAM,GAAc,EAAS,IAC3B,EAAQ,iBAAiB,EAAM,EAAM,EAAK,EAAa,SAEnD,GAAU,EAAa,KACpB,CACL,MAAO,KAAK,IAAI,EAAK,GAAG,CACxB,MAAO,GAAO,GAAK,IAAQ,EAC5B,EAGG,GAAe,EAAa,KAC5B,GAAO,GAAG,EAAQ,SAAS,EAAI,CAE5B,EAAO,EAAK,EAAQ,EAGvB,GAAoB,EAAY,EAAe,IAC/C,IAAc,UAAkB,EAAA,YAAY,EAAO,GAAQ,EAAM,IAAM,CAAC,EAAW,EAAM,EAAE,CAAC,CAEzF,EAAA,aAAa,EAAO,GAAQ,EAAM,IAAM,CAAC,EAAW,EAAM,EAAE,CAAC,CAGhE,MAAiC,CACrC,IAAM,EAAQ,EAAQ,UAAU,CAC1B,EAAU,EAAQ,UAAU,CAIlC,OAFK,EAAM,OAEJ,EAAY,EAAiB,EAAO,EAAG,UAAU,CAAE,EAAQ,CAFxC,EAAO,GAAI,EAAQ,EAKzC,MAAgC,CACpC,IAAM,EAAQ,EAAQ,UAAU,CAC1B,EAAU,EAAQ,UAAU,CAIlC,OAFK,EAAM,OAEJ,EAAY,EAAiB,EAAO,EAAM,OAAS,EAAG,WAAW,CAAE,EAAQ,CAFxD,EAAO,GAAI,EAAQ,EAKzC,EAAQ,GAAyD,CACrE,IAAM,EAAQ,EAAQ,UAAU,CAC1B,EAAU,EAAQ,UAAU,CAElC,GAAI,CAAC,EAAM,OAAQ,OAAO,EAAO,GAAI,EAAQ,CAG7C,IAAM,EAAM,EAAiB,EADf,IAAc,UAAY,KAAK,IAAI,EAAG,EAAU,EAAE,CAAG,EAAU,EAClC,EAAU,CAErD,GAAI,GAAO,EAAG,OAAO,EAAY,EAAK,EAAQ,CAE9C,GAAI,EAAQ,KAAM,CAEhB,IAAM,EAAU,EAAiB,EADf,IAAc,UAAY,EAAI,EAAM,OAAS,EACZ,EAAU,CAE7D,GAAI,GAAW,EAAG,OAAO,EAAY,EAAS,EAAQ,CAGxD,OAAO,EAAO,EAAS,EAAQ,EAkBjC,MAAO,CACL,QACA,kBAbyC,CACzC,IAAM,EAAQ,EAAQ,UAAU,CAC1B,EAAQ,EAAQ,UAAU,CAEhC,OAAO,GAAS,GAAK,EAAQ,EAAM,OAAS,EAAM,GAAS,IAAA,IAU3D,OACA,SAnBoC,EAAK,UAAU,CAoBnD,SAlBoC,EAAK,WAAW,CAmBpD,UAVwB,CACxB,EAAQ,SAAS,GAAG,EAUrB"}
1
+ {"version":3,"file":"list-control.cjs","names":[],"sources":["../../src/controls/list-control.ts"],"sourcesContent":["import { dispatchKeyboardAction } from './internal/keyboard-utils';\nimport { findBackward, findForward } from './internal/validation-utils';\n\nexport type ListKeyAction = 'first' | 'last' | 'next' | 'prev';\n\nconst DEFAULT_KEYS: Record<ListKeyAction, string[]> = {\n first: ['Home'],\n last: ['End'],\n next: ['ArrowDown'],\n prev: ['ArrowUp'],\n};\n\nexport type ListNavigationOptions<T> = {\n disabled?: () => boolean;\n getIndex: () => number;\n getItems: () => T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListKeyAction, string[]>> | (() => Partial<Record<ListKeyAction, string[]>>);\n loop?: boolean;\n onNavigate?: (action: ListKeyAction, index: number, event: KeyboardEvent) => void;\n setIndex: (index: number) => void;\n};\n\nexport type ListControl<T> = {\n first(): number;\n getActiveItem(): T | undefined;\n handleKeydown(event: KeyboardEvent): boolean;\n last(): number;\n next(): number;\n prev(): number;\n reset(): void;\n set(index: number): number;\n};\n\nexport const createListControl = <T>(options: ListNavigationOptions<T>): ListControl<T> => {\n const isDisabled = (item: T, index: number): boolean =>\n options.isItemDisabled?.(item, index) ?? (item as any).disabled;\n\n const commitIndex = (idx: number): number => {\n if (idx >= 0) options.setIndex(idx);\n\n return idx;\n };\n\n const findEnabledIndex = (items: T[], start: number, direction: 'forward' | 'backward'): number => {\n if (direction === 'forward') return findForward(items, start, (item, i) => !isDisabled(item, i));\n\n return findBackward(items, start, (item, i) => !isDisabled(item, i));\n };\n\n const first = (): number => {\n const items = options.getItems();\n\n if (!items.length) return -1;\n\n const idx = findEnabledIndex(items, 0, 'forward');\n\n if (idx < 0) return -1;\n\n return commitIndex(idx);\n };\n\n const last = (): number => {\n const items = options.getItems();\n\n if (!items.length) return -1;\n\n const idx = findEnabledIndex(items, items.length - 1, 'backward');\n\n if (idx < 0) return -1;\n\n return commitIndex(idx);\n };\n\n const set = (index: number): number => {\n const items = options.getItems();\n\n if (!items.length) return -1;\n\n const clamped = Math.min(Math.max(index, 0), items.length - 1);\n\n if (isDisabled(items[clamped], clamped)) {\n return options.getIndex();\n }\n\n return commitIndex(clamped);\n };\n\n const move = (direction: 'forward' | 'backward'): number => {\n const items = options.getItems();\n const current = options.getIndex();\n\n if (!items.length) return -1;\n\n const start =\n current < 0\n ? direction === 'forward'\n ? 0\n : items.length - 1\n : direction === 'forward'\n ? current + 1\n : current - 1;\n const idx = findEnabledIndex(items, start, direction);\n\n if (idx >= 0) return commitIndex(idx);\n\n if (options.loop) {\n const wrapStart = direction === 'forward' ? 0 : items.length - 1;\n const wrapped = findEnabledIndex(items, wrapStart, direction);\n\n if (wrapped >= 0) return commitIndex(wrapped);\n }\n\n return current;\n };\n\n const next = (): number => move('forward');\n\n const prev = (): number => move('backward');\n\n const getActiveItem = (): T | undefined => {\n const items = options.getItems();\n const index = options.getIndex();\n\n return index >= 0 && index < items.length ? items[index] : undefined;\n };\n\n const reset = (): void => {\n options.setIndex(-1);\n };\n\n // ── Keyboard handling ──────────────────────────────────────────────────────\n\n const isKeyDisabled = (): boolean => Boolean(options.disabled?.());\n\n const resolveKeys = (): Record<ListKeyAction, string[]> => {\n const keys = typeof options.keys === 'function' ? options.keys() : options.keys;\n\n return {\n first: keys?.first ?? DEFAULT_KEYS.first,\n last: keys?.last ?? DEFAULT_KEYS.last,\n next: keys?.next ?? DEFAULT_KEYS.next,\n prev: keys?.prev ?? DEFAULT_KEYS.prev,\n };\n };\n\n let cachedKeymap: Record<string, (keyboardEvent: KeyboardEvent) => void> | null = null;\n let cachedKeysRef: Partial<Record<ListKeyAction, string[]>> | undefined;\n\n const buildKeymap = (): Record<string, (keyboardEvent: KeyboardEvent) => void> => {\n const keys = resolveKeys();\n const keymap: Record<string, (keyboardEvent: KeyboardEvent) => void> = {};\n\n for (const action of ['next', 'prev', 'first', 'last'] as const) {\n for (const key of keys[action]) {\n keymap[key] = (keyboardEvent: KeyboardEvent) => {\n const index = { first, last, next, prev }[action]();\n\n options.onNavigate?.(action, index, keyboardEvent);\n };\n }\n }\n\n return keymap;\n };\n\n const getKeymap = (): Record<string, (keyboardEvent: KeyboardEvent) => void> => {\n if (typeof options.keys === 'function') {\n const currentKeys = options.keys();\n\n if (currentKeys !== cachedKeysRef) {\n cachedKeysRef = currentKeys;\n cachedKeymap = buildKeymap();\n }\n } else if (!cachedKeymap) {\n cachedKeymap = buildKeymap();\n }\n\n return cachedKeymap!;\n };\n\n const handleKeydown = (event: KeyboardEvent): boolean =>\n dispatchKeyboardAction(event, { disabled: isKeyDisabled, keymap: getKeymap() });\n\n return {\n first,\n getActiveItem,\n handleKeydown,\n last,\n next,\n prev,\n reset,\n set,\n };\n};\n"],"mappings":"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"}
@@ -1,21 +1,23 @@
1
+ export type ListKeyAction = 'first' | 'last' | 'next' | 'prev';
1
2
  export type ListNavigationOptions<T> = {
3
+ disabled?: () => boolean;
2
4
  getIndex: () => number;
3
5
  getItems: () => T[];
4
6
  isItemDisabled?: (item: T, index: number) => boolean;
7
+ keys?: Partial<Record<ListKeyAction, string[]>> | (() => Partial<Record<ListKeyAction, string[]>>);
5
8
  loop?: boolean;
9
+ onNavigate?: (action: ListKeyAction, index: number, event: KeyboardEvent) => void;
6
10
  setIndex: (index: number) => void;
7
11
  };
8
- export type ListControlResult = {
9
- index: number;
10
- moved: boolean;
11
- };
12
12
  export type ListControl<T> = {
13
- first(): ListControlResult;
13
+ first(): number;
14
14
  getActiveItem(): T | undefined;
15
- last(): ListControlResult;
16
- next(): ListControlResult;
17
- prev(): ListControlResult;
15
+ handleKeydown(event: KeyboardEvent): boolean;
16
+ last(): number;
17
+ next(): number;
18
+ prev(): number;
18
19
  reset(): void;
20
+ set(index: number): number;
19
21
  };
20
22
  export declare const createListControl: <T>(options: ListNavigationOptions<T>) => ListControl<T>;
21
23
  //# sourceMappingURL=list-control.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"list-control.d.ts","sourceRoot":"","sources":["../../src/controls/list-control.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI;IACrC,QAAQ,EAAE,MAAM,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;IACpB,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC3B,KAAK,IAAI,iBAAiB,CAAC;IAC3B,aAAa,IAAI,CAAC,GAAG,SAAS,CAAC;IAC/B,IAAI,IAAI,iBAAiB,CAAC;IAC1B,IAAI,IAAI,iBAAiB,CAAC;IAC1B,IAAI,IAAI,iBAAiB,CAAC;IAC1B,KAAK,IAAI,IAAI,CAAC;CACf,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,CAAC,EAAE,SAAS,qBAAqB,CAAC,CAAC,CAAC,KAAG,WAAW,CAAC,CAAC,CAqFrF,CAAC"}
1
+ {"version":3,"file":"list-control.d.ts","sourceRoot":"","sources":["../../src/controls/list-control.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAS/D,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI;IACrC,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC;IACzB,QAAQ,EAAE,MAAM,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;IACpB,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;IACnG,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAClF,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;IAC3B,KAAK,IAAI,MAAM,CAAC;IAChB,aAAa,IAAI,CAAC,GAAG,SAAS,CAAC;IAC/B,aAAa,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC;IAC7C,IAAI,IAAI,MAAM,CAAC;IACf,IAAI,IAAI,MAAM,CAAC;IACf,IAAI,IAAI,MAAM,CAAC;IACf,KAAK,IAAI,IAAI,CAAC;IACd,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CAC5B,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,CAAC,EAAE,SAAS,qBAAqB,CAAC,CAAC,CAAC,KAAG,WAAW,CAAC,CAAC,CAgKrF,CAAC"}
@@ -1,2 +1,2 @@
1
- import{findBackward as e,findForward as t}from"./internal/validation-utils.js";var n=n=>{let r=(e,t)=>n.isItemDisabled?.(e,t)??e.disabled,i=(e,t)=>({index:Math.max(e,-1),moved:e>=0&&e!==t}),a=(e,t)=>(e>=0&&n.setIndex(e),i(e,t)),o=(n,i,a)=>a===`forward`?t(n,i,(e,t)=>!r(e,t)):e(n,i,(e,t)=>!r(e,t)),s=()=>{let e=n.getItems(),t=n.getIndex();return e.length?a(o(e,0,`forward`),t):i(-1,t)},c=()=>{let e=n.getItems(),t=n.getIndex();return e.length?a(o(e,e.length-1,`backward`),t):i(-1,t)},l=e=>{let t=n.getItems(),r=n.getIndex();if(!t.length)return i(-1,r);let s=o(t,e===`forward`?Math.max(0,r+1):r-1,e);if(s>=0)return a(s,r);if(n.loop){let n=o(t,e===`forward`?0:t.length-1,e);if(n>=0)return a(n,r)}return i(r,r)};return{first:s,getActiveItem:()=>{let e=n.getItems(),t=n.getIndex();return t>=0&&t<e.length?e[t]:void 0},last:c,next:()=>l(`forward`),prev:()=>l(`backward`),reset:()=>{n.setIndex(-1)}}};export{n as createListControl};
1
+ import{dispatchKeyboardAction as e}from"./internal/keyboard-utils.js";import{findBackward as t,findForward as n}from"./internal/validation-utils.js";var r={first:[`Home`],last:[`End`],next:[`ArrowDown`],prev:[`ArrowUp`]},i=i=>{let a=(e,t)=>i.isItemDisabled?.(e,t)??e.disabled,o=e=>(e>=0&&i.setIndex(e),e),s=(e,r,i)=>i===`forward`?n(e,r,(e,t)=>!a(e,t)):t(e,r,(e,t)=>!a(e,t)),c=()=>{let e=i.getItems();if(!e.length)return-1;let t=s(e,0,`forward`);return t<0?-1:o(t)},l=()=>{let e=i.getItems();if(!e.length)return-1;let t=s(e,e.length-1,`backward`);return t<0?-1:o(t)},u=e=>{let t=i.getItems();if(!t.length)return-1;let n=Math.min(Math.max(e,0),t.length-1);return a(t[n],n)?i.getIndex():o(n)},d=e=>{let t=i.getItems(),n=i.getIndex();if(!t.length)return-1;let r=s(t,n<0?e===`forward`?0:t.length-1:e===`forward`?n+1:n-1,e);if(r>=0)return o(r);if(i.loop){let n=s(t,e===`forward`?0:t.length-1,e);if(n>=0)return o(n)}return n},f=()=>d(`forward`),p=()=>d(`backward`),m=()=>{let e=i.getItems(),t=i.getIndex();return t>=0&&t<e.length?e[t]:void 0},h=()=>{i.setIndex(-1)},g=()=>!!i.disabled?.(),_=()=>{let e=typeof i.keys==`function`?i.keys():i.keys;return{first:e?.first??r.first,last:e?.last??r.last,next:e?.next??r.next,prev:e?.prev??r.prev}},v=null,y,b=()=>{let e=_(),t={};for(let n of[`next`,`prev`,`first`,`last`])for(let r of e[n])t[r]=e=>{let t={first:c,last:l,next:f,prev:p}[n]();i.onNavigate?.(n,t,e)};return t},x=()=>{if(typeof i.keys==`function`){let e=i.keys();e!==y&&(y=e,v=b())}else v||=b();return v};return{first:c,getActiveItem:m,handleKeydown:t=>e(t,{disabled:g,keymap:x()}),last:l,next:f,prev:p,reset:h,set:u}};export{i as createListControl};
2
2
  //# sourceMappingURL=list-control.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"list-control.js","names":[],"sources":["../../src/controls/list-control.ts"],"sourcesContent":["import { findBackward, findForward } from './internal/validation-utils';\n\nexport type ListNavigationOptions<T> = {\n getIndex: () => number;\n getItems: () => T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n loop?: boolean;\n setIndex: (index: number) => void;\n};\n\nexport type ListControlResult = {\n index: number;\n moved: boolean;\n};\n\nexport type ListControl<T> = {\n first(): ListControlResult;\n getActiveItem(): T | undefined;\n last(): ListControlResult;\n next(): ListControlResult;\n prev(): ListControlResult;\n reset(): void;\n};\n\nexport const createListControl = <T>(options: ListNavigationOptions<T>): ListControl<T> => {\n const isDisabled = (item: T, index: number): boolean =>\n options.isItemDisabled?.(item, index) ?? (item as any).disabled;\n\n const result = (idx: number, current: number): ListControlResult => {\n return {\n index: Math.max(idx, -1),\n moved: idx >= 0 && idx !== current,\n };\n };\n\n const commitIndex = (idx: number, current: number): ListControlResult => {\n if (idx >= 0) options.setIndex(idx);\n\n return result(idx, current);\n };\n\n const findEnabledIndex = (items: T[], start: number, direction: 'forward' | 'backward'): number => {\n if (direction === 'forward') return findForward(items, start, (item, i) => !isDisabled(item, i));\n\n return findBackward(items, start, (item, i) => !isDisabled(item, i));\n };\n\n const first = (): ListControlResult => {\n const items = options.getItems();\n const current = options.getIndex();\n\n if (!items.length) return result(-1, current);\n\n return commitIndex(findEnabledIndex(items, 0, 'forward'), current);\n };\n\n const last = (): ListControlResult => {\n const items = options.getItems();\n const current = options.getIndex();\n\n if (!items.length) return result(-1, current);\n\n return commitIndex(findEnabledIndex(items, items.length - 1, 'backward'), current);\n };\n\n const move = (direction: 'forward' | 'backward'): ListControlResult => {\n const items = options.getItems();\n const current = options.getIndex();\n\n if (!items.length) return result(-1, current);\n\n const start = direction === 'forward' ? Math.max(0, current + 1) : current - 1;\n const idx = findEnabledIndex(items, start, direction);\n\n if (idx >= 0) return commitIndex(idx, current);\n\n if (options.loop) {\n const wrapStart = direction === 'forward' ? 0 : items.length - 1;\n const wrapped = findEnabledIndex(items, wrapStart, direction);\n\n if (wrapped >= 0) return commitIndex(wrapped, current);\n }\n\n return result(current, current);\n };\n\n const next = (): ListControlResult => move('forward');\n\n const prev = (): ListControlResult => move('backward');\n\n const getActiveItem = (): T | undefined => {\n const items = options.getItems();\n const index = options.getIndex();\n\n return index >= 0 && index < items.length ? items[index] : undefined;\n };\n\n const reset = (): void => {\n options.setIndex(-1);\n };\n\n return {\n first,\n getActiveItem,\n last,\n next,\n prev,\n reset,\n };\n};\n"],"mappings":"+EAwBA,IAAa,EAAwB,GAAsD,CACzF,IAAM,GAAc,EAAS,IAC3B,EAAQ,iBAAiB,EAAM,EAAM,EAAK,EAAa,SAEnD,GAAU,EAAa,KACpB,CACL,MAAO,KAAK,IAAI,EAAK,GAAG,CACxB,MAAO,GAAO,GAAK,IAAQ,EAC5B,EAGG,GAAe,EAAa,KAC5B,GAAO,GAAG,EAAQ,SAAS,EAAI,CAE5B,EAAO,EAAK,EAAQ,EAGvB,GAAoB,EAAY,EAAe,IAC/C,IAAc,UAAkB,EAAY,EAAO,GAAQ,EAAM,IAAM,CAAC,EAAW,EAAM,EAAE,CAAC,CAEzF,EAAa,EAAO,GAAQ,EAAM,IAAM,CAAC,EAAW,EAAM,EAAE,CAAC,CAGhE,MAAiC,CACrC,IAAM,EAAQ,EAAQ,UAAU,CAC1B,EAAU,EAAQ,UAAU,CAIlC,OAFK,EAAM,OAEJ,EAAY,EAAiB,EAAO,EAAG,UAAU,CAAE,EAAQ,CAFxC,EAAO,GAAI,EAAQ,EAKzC,MAAgC,CACpC,IAAM,EAAQ,EAAQ,UAAU,CAC1B,EAAU,EAAQ,UAAU,CAIlC,OAFK,EAAM,OAEJ,EAAY,EAAiB,EAAO,EAAM,OAAS,EAAG,WAAW,CAAE,EAAQ,CAFxD,EAAO,GAAI,EAAQ,EAKzC,EAAQ,GAAyD,CACrE,IAAM,EAAQ,EAAQ,UAAU,CAC1B,EAAU,EAAQ,UAAU,CAElC,GAAI,CAAC,EAAM,OAAQ,OAAO,EAAO,GAAI,EAAQ,CAG7C,IAAM,EAAM,EAAiB,EADf,IAAc,UAAY,KAAK,IAAI,EAAG,EAAU,EAAE,CAAG,EAAU,EAClC,EAAU,CAErD,GAAI,GAAO,EAAG,OAAO,EAAY,EAAK,EAAQ,CAE9C,GAAI,EAAQ,KAAM,CAEhB,IAAM,EAAU,EAAiB,EADf,IAAc,UAAY,EAAI,EAAM,OAAS,EACZ,EAAU,CAE7D,GAAI,GAAW,EAAG,OAAO,EAAY,EAAS,EAAQ,CAGxD,OAAO,EAAO,EAAS,EAAQ,EAkBjC,MAAO,CACL,QACA,kBAbyC,CACzC,IAAM,EAAQ,EAAQ,UAAU,CAC1B,EAAQ,EAAQ,UAAU,CAEhC,OAAO,GAAS,GAAK,EAAQ,EAAM,OAAS,EAAM,GAAS,IAAA,IAU3D,OACA,SAnBoC,EAAK,UAAU,CAoBnD,SAlBoC,EAAK,WAAW,CAmBpD,UAVwB,CACxB,EAAQ,SAAS,GAAG,EAUrB"}
1
+ {"version":3,"file":"list-control.js","names":[],"sources":["../../src/controls/list-control.ts"],"sourcesContent":["import { dispatchKeyboardAction } from './internal/keyboard-utils';\nimport { findBackward, findForward } from './internal/validation-utils';\n\nexport type ListKeyAction = 'first' | 'last' | 'next' | 'prev';\n\nconst DEFAULT_KEYS: Record<ListKeyAction, string[]> = {\n first: ['Home'],\n last: ['End'],\n next: ['ArrowDown'],\n prev: ['ArrowUp'],\n};\n\nexport type ListNavigationOptions<T> = {\n disabled?: () => boolean;\n getIndex: () => number;\n getItems: () => T[];\n isItemDisabled?: (item: T, index: number) => boolean;\n keys?: Partial<Record<ListKeyAction, string[]>> | (() => Partial<Record<ListKeyAction, string[]>>);\n loop?: boolean;\n onNavigate?: (action: ListKeyAction, index: number, event: KeyboardEvent) => void;\n setIndex: (index: number) => void;\n};\n\nexport type ListControl<T> = {\n first(): number;\n getActiveItem(): T | undefined;\n handleKeydown(event: KeyboardEvent): boolean;\n last(): number;\n next(): number;\n prev(): number;\n reset(): void;\n set(index: number): number;\n};\n\nexport const createListControl = <T>(options: ListNavigationOptions<T>): ListControl<T> => {\n const isDisabled = (item: T, index: number): boolean =>\n options.isItemDisabled?.(item, index) ?? (item as any).disabled;\n\n const commitIndex = (idx: number): number => {\n if (idx >= 0) options.setIndex(idx);\n\n return idx;\n };\n\n const findEnabledIndex = (items: T[], start: number, direction: 'forward' | 'backward'): number => {\n if (direction === 'forward') return findForward(items, start, (item, i) => !isDisabled(item, i));\n\n return findBackward(items, start, (item, i) => !isDisabled(item, i));\n };\n\n const first = (): number => {\n const items = options.getItems();\n\n if (!items.length) return -1;\n\n const idx = findEnabledIndex(items, 0, 'forward');\n\n if (idx < 0) return -1;\n\n return commitIndex(idx);\n };\n\n const last = (): number => {\n const items = options.getItems();\n\n if (!items.length) return -1;\n\n const idx = findEnabledIndex(items, items.length - 1, 'backward');\n\n if (idx < 0) return -1;\n\n return commitIndex(idx);\n };\n\n const set = (index: number): number => {\n const items = options.getItems();\n\n if (!items.length) return -1;\n\n const clamped = Math.min(Math.max(index, 0), items.length - 1);\n\n if (isDisabled(items[clamped], clamped)) {\n return options.getIndex();\n }\n\n return commitIndex(clamped);\n };\n\n const move = (direction: 'forward' | 'backward'): number => {\n const items = options.getItems();\n const current = options.getIndex();\n\n if (!items.length) return -1;\n\n const start =\n current < 0\n ? direction === 'forward'\n ? 0\n : items.length - 1\n : direction === 'forward'\n ? current + 1\n : current - 1;\n const idx = findEnabledIndex(items, start, direction);\n\n if (idx >= 0) return commitIndex(idx);\n\n if (options.loop) {\n const wrapStart = direction === 'forward' ? 0 : items.length - 1;\n const wrapped = findEnabledIndex(items, wrapStart, direction);\n\n if (wrapped >= 0) return commitIndex(wrapped);\n }\n\n return current;\n };\n\n const next = (): number => move('forward');\n\n const prev = (): number => move('backward');\n\n const getActiveItem = (): T | undefined => {\n const items = options.getItems();\n const index = options.getIndex();\n\n return index >= 0 && index < items.length ? items[index] : undefined;\n };\n\n const reset = (): void => {\n options.setIndex(-1);\n };\n\n // ── Keyboard handling ──────────────────────────────────────────────────────\n\n const isKeyDisabled = (): boolean => Boolean(options.disabled?.());\n\n const resolveKeys = (): Record<ListKeyAction, string[]> => {\n const keys = typeof options.keys === 'function' ? options.keys() : options.keys;\n\n return {\n first: keys?.first ?? DEFAULT_KEYS.first,\n last: keys?.last ?? DEFAULT_KEYS.last,\n next: keys?.next ?? DEFAULT_KEYS.next,\n prev: keys?.prev ?? DEFAULT_KEYS.prev,\n };\n };\n\n let cachedKeymap: Record<string, (keyboardEvent: KeyboardEvent) => void> | null = null;\n let cachedKeysRef: Partial<Record<ListKeyAction, string[]>> | undefined;\n\n const buildKeymap = (): Record<string, (keyboardEvent: KeyboardEvent) => void> => {\n const keys = resolveKeys();\n const keymap: Record<string, (keyboardEvent: KeyboardEvent) => void> = {};\n\n for (const action of ['next', 'prev', 'first', 'last'] as const) {\n for (const key of keys[action]) {\n keymap[key] = (keyboardEvent: KeyboardEvent) => {\n const index = { first, last, next, prev }[action]();\n\n options.onNavigate?.(action, index, keyboardEvent);\n };\n }\n }\n\n return keymap;\n };\n\n const getKeymap = (): Record<string, (keyboardEvent: KeyboardEvent) => void> => {\n if (typeof options.keys === 'function') {\n const currentKeys = options.keys();\n\n if (currentKeys !== cachedKeysRef) {\n cachedKeysRef = currentKeys;\n cachedKeymap = buildKeymap();\n }\n } else if (!cachedKeymap) {\n cachedKeymap = buildKeymap();\n }\n\n return cachedKeymap!;\n };\n\n const handleKeydown = (event: KeyboardEvent): boolean =>\n dispatchKeyboardAction(event, { disabled: isKeyDisabled, keymap: getKeymap() });\n\n return {\n first,\n getActiveItem,\n handleKeydown,\n last,\n next,\n prev,\n reset,\n set,\n };\n};\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require(`../runtime-lifecycle.cjs`),t=require(`./internal/control-state.cjs`);let n=require(`@vielzeug/stateit`),r=require(`@vielzeug/floatit`);var i=i=>{let a=t.createControlState(i);e.effect(()=>{if(!i.isOpen.value||!i.positioner)return;let e=i.positioner.reference(),t=i.positioner.floating();if(!e||!t){i.positioner.update();return}(0,n.onCleanup)((0,r.autoUpdate)(e,t,()=>i.positioner?.update()))});let o=()=>typeof i.restoreFocus==`function`?i.restoreFocus():i.restoreFocus??!0,s=(e=`programmatic`)=>{a.disabled.value||i.isOpen.value||(i.setOpen(!0,e),requestAnimationFrame(()=>i.positioner?.update()),i.onOpen?.(e))},c=(e=`programmatic`,t)=>{i.isOpen.value&&(i.setOpen(!1,e),(t??o())&&i.elements.trigger?.focus(),i.onClose?.(e))},l=()=>{if(i.isOpen.value){c(`trigger`);return}s(`trigger`)},u=(e,t)=>e?e.contains(t):!1;return{bindOutsideClick:(e=document,t=!0)=>{let n=e=>{if(!i.isOpen.value)return;let t=e.composedPath()[0];t&&(u(i.elements.boundary,t)||u(i.elements.panel,t)||c(`outside-click`))};return e.addEventListener(`click`,n,{capture:t}),()=>e.removeEventListener(`click`,n,{capture:t})},close:c,open:s,toggle:l}};exports.createOverlayControl=i;
1
+ let e=require(`@vielzeug/floatit`);var t=new Set,n=null,r=e=>e instanceof Error&&e.message.includes(`[stateit] Cannot read disposed computed signal`),i=()=>{if(n)return;let e=e=>{for(let n of[...t])try{n(e)}catch(e){if(r(e)){t.delete(n);continue}throw e}a()};document.addEventListener(`click`,e,{capture:!0}),n=()=>{document.removeEventListener(`click`,e,{capture:!0}),n=null}},a=()=>{t.size===0&&n&&n()},o=n=>{let o=null,s=null,c=()=>typeof n.restoreFocus==`function`?n.restoreFocus():n.restoreFocus??!0,l=(e,o)=>{try{n.setOpen(e,o)}catch(e){if(r(e)&&s){t.delete(s),a();return}throw e}e&&s?(t.add(s),i()):!e&&s&&(t.delete(s),a())},u=(t={})=>{let r=t.reason??`programmatic`;if(!(n.isDisabled?.()||n.isOpen())){if(l(!0,{reason:r}),n.positioner){let t=n.positioner.reference(),r=n.positioner.floating();t&&r&&(o=(0,e.autoUpdate)(t,r,()=>n.positioner?.update())),n.positioner.update()}n.onOpen?.(r)}},d=(e={})=>{let t=e.reason??`programmatic`;n.isOpen()&&(l(!1,{reason:t}),o&&=(o(),null),(e.restoreFocus??c())&&n.getTriggerElement?.()?.focus(),n.onClose?.(t))};return s=e=>{if(!n.isOpen())return;let t=e.composedPath?.()??[],r=n.getBoundaryElement(),i=n.getPanelElement?.()??null,a=t[0]??e.target,o=a instanceof Node?a:null,s=t.some(e=>e===r||e===i),c=o?(r?.contains(o)??!1)||(i?.contains(o)??!1):!1;s||c||d({reason:`outside-click`})},{close:d,open:u,toggle:()=>{if(n.isOpen()){d({reason:`trigger`});return}u({reason:`trigger`})}}};exports.createOverlayControl=o;
2
2
  //# sourceMappingURL=overlay-control.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"overlay-control.cjs","names":[],"sources":["../../src/controls/overlay-control.ts"],"sourcesContent":["import { autoUpdate } from '@vielzeug/floatit';\nimport { onCleanup as onSignalCleanup, type ReadonlySignal } from '@vielzeug/stateit';\n\nimport { effect } from '../runtime-lifecycle';\nimport { createControlState, type ControlContextOptions } from './internal/control-state';\n\nexport type OverlayOpenReason = 'programmatic' | 'trigger';\nexport type OverlayCloseReason = 'escape' | 'outside-click' | 'programmatic' | 'trigger';\nexport type OverlayCloseDetail = { reason: OverlayCloseReason };\nexport type OverlayOpenDetail = { reason: OverlayOpenReason };\n\ntype OverlayPositioner = {\n floating: () => HTMLElement | null;\n reference: () => HTMLElement | null;\n update: () => void;\n};\n\nexport type OverlayControlOptions = ControlContextOptions & {\n elements: {\n boundary: HTMLElement;\n panel?: HTMLElement | null;\n trigger?: HTMLElement | null;\n };\n isOpen: ReadonlySignal<boolean>;\n onClose?: (reason: OverlayCloseReason) => void;\n onOpen?: (reason: OverlayOpenReason) => void;\n positioner?: OverlayPositioner;\n restoreFocus?: boolean | (() => boolean);\n setOpen: (next: boolean, reason: OverlayOpenReason | OverlayCloseReason) => void;\n};\n\nexport type OverlayControl = {\n bindOutsideClick(target?: Document | HTMLElement, capture?: boolean): () => void;\n close(reason?: OverlayCloseReason, restoreFocus?: boolean): void;\n open(reason?: OverlayOpenReason): void;\n toggle(): void;\n};\n\nexport const createOverlayControl = (options: OverlayControlOptions): OverlayControl => {\n const controlState = createControlState(options);\n\n // Effect handles positioning lifecycle automatically\n effect(() => {\n if (!options.isOpen.value || !options.positioner) return;\n\n const reference = options.positioner.reference();\n const floating = options.positioner.floating();\n\n if (!reference || !floating) {\n options.positioner.update();\n\n return;\n }\n\n const cleanup = autoUpdate(reference, floating, () => options.positioner?.update());\n\n onSignalCleanup(cleanup); // Auto-cleanup on effect re-run/end\n });\n\n const shouldRestoreFocus = (): boolean => {\n if (typeof options.restoreFocus === 'function') return options.restoreFocus();\n\n return options.restoreFocus ?? true;\n };\n\n const open = (reason: OverlayOpenReason = 'programmatic'): void => {\n if (controlState.disabled.value || options.isOpen.value) return;\n\n options.setOpen(true, reason);\n requestAnimationFrame(() => options.positioner?.update());\n options.onOpen?.(reason);\n };\n\n const close = (reason: OverlayCloseReason = 'programmatic', restoreFocus?: boolean): void => {\n if (!options.isOpen.value) return;\n\n options.setOpen(false, reason);\n\n const restore = restoreFocus ?? shouldRestoreFocus();\n\n if (restore) options.elements.trigger?.focus();\n\n options.onClose?.(reason);\n };\n\n const toggle = (): void => {\n if (options.isOpen.value) {\n close('trigger');\n\n return;\n }\n\n open('trigger');\n };\n\n const isInside = (element: HTMLElement | null | undefined, target: Node): boolean => {\n return element ? element.contains(target) : false;\n };\n\n const bindOutsideClick = (target: Document | HTMLElement = document, capture = true): (() => void) => {\n const handler = (event: Event) => {\n if (!options.isOpen.value) return;\n\n const el = event.composedPath()[0] as Node | null;\n\n if (!el) return;\n\n const inside = isInside(options.elements.boundary, el) || isInside(options.elements.panel, el);\n\n if (!inside) close('outside-click');\n };\n\n target.addEventListener('click', handler, { capture });\n\n return () => target.removeEventListener('click', handler, { capture });\n };\n\n return {\n bindOutsideClick,\n close,\n open,\n toggle,\n };\n};\n"],"mappings":"wJAsCA,IAAa,EAAwB,GAAmD,CACtF,IAAM,EAAe,EAAA,mBAAmB,EAAQ,CAGhD,EAAA,WAAa,CACX,GAAI,CAAC,EAAQ,OAAO,OAAS,CAAC,EAAQ,WAAY,OAElD,IAAM,EAAY,EAAQ,WAAW,WAAW,CAC1C,EAAW,EAAQ,WAAW,UAAU,CAE9C,GAAI,CAAC,GAAa,CAAC,EAAU,CAC3B,EAAQ,WAAW,QAAQ,CAE3B,QAKF,EAAA,EAAA,YAAA,EAAA,EAAA,YAF2B,EAAW,MAAgB,EAAQ,YAAY,QAAQ,CAAC,CAE3D,EACxB,CAEF,IAAM,MACA,OAAO,EAAQ,cAAiB,WAAmB,EAAQ,cAAc,CAEtE,EAAQ,cAAgB,GAG3B,GAAQ,EAA4B,iBAAyB,CAC7D,EAAa,SAAS,OAAS,EAAQ,OAAO,QAElD,EAAQ,QAAQ,GAAM,EAAO,CAC7B,0BAA4B,EAAQ,YAAY,QAAQ,CAAC,CACzD,EAAQ,SAAS,EAAO,GAGpB,GAAS,EAA6B,eAAgB,IAAiC,CACtF,EAAQ,OAAO,QAEpB,EAAQ,QAAQ,GAAO,EAAO,EAEd,GAAgB,GAAoB,GAEvC,EAAQ,SAAS,SAAS,OAAO,CAE9C,EAAQ,UAAU,EAAO,GAGrB,MAAqB,CACzB,GAAI,EAAQ,OAAO,MAAO,CACxB,EAAM,UAAU,CAEhB,OAGF,EAAK,UAAU,EAGX,GAAY,EAAyC,IAClD,EAAU,EAAQ,SAAS,EAAO,CAAG,GAqB9C,MAAO,CACL,kBAnBwB,EAAiC,SAAU,EAAU,KAAuB,CACpG,IAAM,EAAW,GAAiB,CAChC,GAAI,CAAC,EAAQ,OAAO,MAAO,OAE3B,IAAM,EAAK,EAAM,cAAc,CAAC,GAE3B,IAEU,EAAS,EAAQ,SAAS,SAAU,EAAG,EAAI,EAAS,EAAQ,SAAS,MAAO,EAAG,EAEjF,EAAM,gBAAgB,GAKrC,OAFA,EAAO,iBAAiB,QAAS,EAAS,CAAE,UAAS,CAAC,KAEzC,EAAO,oBAAoB,QAAS,EAAS,CAAE,UAAS,CAAC,EAKtE,QACA,OACA,SACD"}
1
+ {"version":3,"file":"overlay-control.cjs","names":[],"sources":["../../src/controls/overlay-control.ts"],"sourcesContent":["import { autoUpdate } from '@vielzeug/floatit';\n\nexport type OverlayOpenReason = 'programmatic' | 'trigger';\nexport type OverlayCloseReason = 'escape' | 'outside-click' | 'programmatic' | 'swipe' | 'trigger';\nexport type OverlayCloseDetail = { reason: OverlayCloseReason };\nexport type OverlayOpenDetail = { reason: OverlayOpenReason };\n\ntype OverlayPositioner = {\n floating: () => HTMLElement | null;\n reference: () => HTMLElement | null;\n update: () => void;\n};\n\nexport type OverlayControlOptions = {\n getBoundaryElement: () => HTMLElement | null;\n getPanelElement?: () => HTMLElement | null;\n getTriggerElement?: () => HTMLElement | null;\n isDisabled?: () => boolean;\n isOpen: () => boolean;\n onClose?: (reason: OverlayCloseReason) => void;\n onOpen?: (reason: OverlayOpenReason) => void;\n positioner?: OverlayPositioner;\n restoreFocus?: boolean | (() => boolean);\n setOpen: (next: boolean, context: { reason: OverlayOpenReason | OverlayCloseReason }) => void;\n};\n\nexport type OverlayControl = {\n close(opts?: { reason?: OverlayCloseReason; restoreFocus?: boolean }): void;\n open(opts?: { reason?: OverlayOpenReason }): void;\n toggle(): void;\n};\n\n// Module-level document click listener management for outside-click dismissal\nconst activeOverlayListeners = new Set<(event: Event) => void>();\nlet documentClickUnsubscribe: (() => void) | null = null;\n\nconst isDisposedComputedSignalError = (error: unknown): boolean =>\n error instanceof Error && error.message.includes('[stateit] Cannot read disposed computed signal');\n\nconst ensureDocumentClickListener = (): void => {\n if (documentClickUnsubscribe) return; // Already attached\n\n const handler = (event: Event) => {\n for (const listener of [...activeOverlayListeners]) {\n try {\n listener(event);\n } catch (error) {\n if (isDisposedComputedSignalError(error)) {\n activeOverlayListeners.delete(listener);\n\n continue;\n }\n\n throw error;\n }\n }\n\n removeDocumentClickListener();\n };\n\n document.addEventListener('click', handler, { capture: true });\n documentClickUnsubscribe = () => {\n document.removeEventListener('click', handler, { capture: true });\n documentClickUnsubscribe = null;\n };\n};\n\nconst removeDocumentClickListener = (): void => {\n if (activeOverlayListeners.size === 0 && documentClickUnsubscribe) {\n documentClickUnsubscribe();\n }\n};\n\nexport const createOverlayControl = (options: OverlayControlOptions): OverlayControl => {\n let positionerCleanup: (() => void) | null = null;\n let clickListener: ((event: Event) => void) | null = null;\n\n const shouldRestoreFocus = (): boolean => {\n if (typeof options.restoreFocus === 'function') return options.restoreFocus();\n\n return options.restoreFocus ?? true;\n };\n\n // Local wrapper — handles click-listener registration without mutating options.\n const commitOpen = (next: boolean, context: { reason: OverlayOpenReason | OverlayCloseReason }): void => {\n try {\n options.setOpen(next, context);\n } catch (error) {\n if (isDisposedComputedSignalError(error) && clickListener) {\n activeOverlayListeners.delete(clickListener);\n removeDocumentClickListener();\n\n return;\n }\n\n throw error;\n }\n\n if (next && clickListener) {\n activeOverlayListeners.add(clickListener);\n ensureDocumentClickListener();\n } else if (!next && clickListener) {\n activeOverlayListeners.delete(clickListener);\n removeDocumentClickListener();\n }\n };\n\n const open = (opts: { reason?: OverlayOpenReason } = {}): void => {\n const reason = opts.reason ?? 'programmatic';\n\n if (options.isDisabled?.() || options.isOpen()) return;\n\n commitOpen(true, { reason });\n\n if (options.positioner) {\n const reference = options.positioner.reference();\n const floating = options.positioner.floating();\n\n if (reference && floating) {\n positionerCleanup = autoUpdate(reference, floating, () => options.positioner?.update());\n }\n\n options.positioner.update();\n }\n\n options.onOpen?.(reason);\n };\n\n const close = (opts: { reason?: OverlayCloseReason; restoreFocus?: boolean } = {}): void => {\n const reason = opts.reason ?? 'programmatic';\n\n if (!options.isOpen()) return;\n\n commitOpen(false, { reason });\n\n if (positionerCleanup) {\n positionerCleanup();\n positionerCleanup = null;\n }\n\n const restore = opts.restoreFocus ?? shouldRestoreFocus();\n\n if (restore) options.getTriggerElement?.()?.focus();\n\n options.onClose?.(reason);\n };\n\n const toggle = (): void => {\n if (options.isOpen()) {\n close({ reason: 'trigger' });\n\n return;\n }\n\n open({ reason: 'trigger' });\n };\n\n clickListener = (event: Event) => {\n if (!options.isOpen()) return;\n\n const path = (event as Event & { composedPath?: () => EventTarget[] }).composedPath?.() ?? [];\n const boundary = options.getBoundaryElement();\n const panel = options.getPanelElement?.() ?? null;\n const target = (path[0] ?? event.target) as EventTarget | null;\n const nodeTarget = target instanceof Node ? target : null;\n\n const insideByPath = path.some((entry) => entry === boundary || entry === panel);\n const insideByContainment = nodeTarget\n ? (boundary?.contains(nodeTarget) ?? false) || (panel?.contains(nodeTarget) ?? false)\n : false;\n const inside = insideByPath || insideByContainment;\n\n if (!inside) close({ reason: 'outside-click' });\n };\n\n return {\n close,\n open,\n toggle,\n };\n};\n"],"mappings":"mCAiCA,IAAM,EAAyB,IAAI,IAC/B,EAAgD,KAE9C,EAAiC,GACrC,aAAiB,OAAS,EAAM,QAAQ,SAAS,gDAAgD,EAE7F,MAA0C,CAC9C,GAAI,EAA0B,OAE9B,IAAM,EAAW,GAAiB,CAChC,IAAK,IAAM,IAAY,CAAC,GAAG,CAAsB,EAC/C,GAAI,CACF,EAAS,CAAK,CAChB,OAAS,EAAO,CACd,GAAI,EAA8B,CAAK,EAAG,CACxC,EAAuB,OAAO,CAAQ,EAEtC,QACF,CAEA,MAAM,CACR,CAGF,EAA4B,CAC9B,EAEA,SAAS,iBAAiB,QAAS,EAAS,CAAE,QAAS,EAAK,CAAC,EAC7D,MAAiC,CAC/B,SAAS,oBAAoB,QAAS,EAAS,CAAE,QAAS,EAAK,CAAC,EAChE,EAA2B,IAC7B,CACF,EAEM,MAA0C,CAC1C,EAAuB,OAAS,GAAK,GACvC,EAAyB,CAE7B,EAEa,EAAwB,GAAmD,CACtF,IAAI,EAAyC,KACzC,EAAiD,KAE/C,MACA,OAAO,EAAQ,cAAiB,WAAmB,EAAQ,aAAa,EAErE,EAAQ,cAAgB,GAI3B,GAAc,EAAe,IAAsE,CACvG,GAAI,CACF,EAAQ,QAAQ,EAAM,CAAO,CAC/B,OAAS,EAAO,CACd,GAAI,EAA8B,CAAK,GAAK,EAAe,CACzD,EAAuB,OAAO,CAAa,EAC3C,EAA4B,EAE5B,MACF,CAEA,MAAM,CACR,CAEI,GAAQ,GACV,EAAuB,IAAI,CAAa,EACxC,EAA4B,GACnB,CAAC,GAAQ,IAClB,EAAuB,OAAO,CAAa,EAC3C,EAA4B,EAEhC,EAEM,GAAQ,EAAuC,CAAC,IAAY,CAChE,IAAM,EAAS,EAAK,QAAU,eAE1B,OAAQ,aAAa,GAAK,EAAQ,OAAO,GAI7C,IAFA,EAAW,GAAM,CAAE,QAAO,CAAC,EAEvB,EAAQ,WAAY,CACtB,IAAM,EAAY,EAAQ,WAAW,UAAU,EACzC,EAAW,EAAQ,WAAW,SAAS,EAEzC,GAAa,IACf,GAAA,EAAA,EAAA,YAA+B,EAAW,MAAgB,EAAQ,YAAY,OAAO,CAAC,GAGxF,EAAQ,WAAW,OAAO,CAC5B,CAEA,EAAQ,SAAS,CAAM,CAFvB,CAGF,EAEM,GAAS,EAAgE,CAAC,IAAY,CAC1F,IAAM,EAAS,EAAK,QAAU,eAEzB,EAAQ,OAAO,IAEpB,EAAW,GAAO,CAAE,QAAO,CAAC,EAE5B,AAEE,KADA,EAAkB,EACE,OAGN,EAAK,cAAgB,EAAmB,IAE3C,EAAQ,oBAAoB,GAAG,MAAM,EAElD,EAAQ,UAAU,CAAM,EAC1B,EA8BA,MAlBA,GAAiB,GAAiB,CAChC,GAAI,CAAC,EAAQ,OAAO,EAAG,OAEvB,IAAM,EAAQ,EAAyD,eAAe,GAAK,CAAC,EACtF,EAAW,EAAQ,mBAAmB,EACtC,EAAQ,EAAQ,kBAAkB,GAAK,KACvC,EAAU,EAAK,IAAM,EAAM,OAC3B,EAAa,aAAkB,KAAO,EAAS,KAE/C,EAAe,EAAK,KAAM,GAAU,IAAU,GAAY,IAAU,CAAK,EACzE,EAAsB,GACvB,GAAU,SAAS,CAAU,GAAK,MAAW,GAAO,SAAS,CAAU,GAAK,IAC7E,GACW,GAAgB,GAElB,EAAM,CAAE,OAAQ,eAAgB,CAAC,CAChD,EAEO,CACL,QACA,OACA,WA/ByB,CACzB,GAAI,EAAQ,OAAO,EAAG,CACpB,EAAM,CAAE,OAAQ,SAAU,CAAC,EAE3B,MACF,CAEA,EAAK,CAAE,OAAQ,SAAU,CAAC,CAC5B,CAwBA,CACF"}
@@ -1,7 +1,5 @@
1
- import { type ReadonlySignal } from '@vielzeug/stateit';
2
- import { type ControlContextOptions } from './internal/control-state';
3
1
  export type OverlayOpenReason = 'programmatic' | 'trigger';
4
- export type OverlayCloseReason = 'escape' | 'outside-click' | 'programmatic' | 'trigger';
2
+ export type OverlayCloseReason = 'escape' | 'outside-click' | 'programmatic' | 'swipe' | 'trigger';
5
3
  export type OverlayCloseDetail = {
6
4
  reason: OverlayCloseReason;
7
5
  };
@@ -13,23 +11,28 @@ type OverlayPositioner = {
13
11
  reference: () => HTMLElement | null;
14
12
  update: () => void;
15
13
  };
16
- export type OverlayControlOptions = ControlContextOptions & {
17
- elements: {
18
- boundary: HTMLElement;
19
- panel?: HTMLElement | null;
20
- trigger?: HTMLElement | null;
21
- };
22
- isOpen: ReadonlySignal<boolean>;
14
+ export type OverlayControlOptions = {
15
+ getBoundaryElement: () => HTMLElement | null;
16
+ getPanelElement?: () => HTMLElement | null;
17
+ getTriggerElement?: () => HTMLElement | null;
18
+ isDisabled?: () => boolean;
19
+ isOpen: () => boolean;
23
20
  onClose?: (reason: OverlayCloseReason) => void;
24
21
  onOpen?: (reason: OverlayOpenReason) => void;
25
22
  positioner?: OverlayPositioner;
26
23
  restoreFocus?: boolean | (() => boolean);
27
- setOpen: (next: boolean, reason: OverlayOpenReason | OverlayCloseReason) => void;
24
+ setOpen: (next: boolean, context: {
25
+ reason: OverlayOpenReason | OverlayCloseReason;
26
+ }) => void;
28
27
  };
29
28
  export type OverlayControl = {
30
- bindOutsideClick(target?: Document | HTMLElement, capture?: boolean): () => void;
31
- close(reason?: OverlayCloseReason, restoreFocus?: boolean): void;
32
- open(reason?: OverlayOpenReason): void;
29
+ close(opts?: {
30
+ reason?: OverlayCloseReason;
31
+ restoreFocus?: boolean;
32
+ }): void;
33
+ open(opts?: {
34
+ reason?: OverlayOpenReason;
35
+ }): void;
33
36
  toggle(): void;
34
37
  };
35
38
  export declare const createOverlayControl: (options: OverlayControlOptions) => OverlayControl;
@@ -1 +1 @@
1
- {"version":3,"file":"overlay-control.d.ts","sourceRoot":"","sources":["../../src/controls/overlay-control.ts"],"names":[],"mappings":"AACA,OAAO,EAAgC,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGtF,OAAO,EAAsB,KAAK,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAE1F,MAAM,MAAM,iBAAiB,GAAG,cAAc,GAAG,SAAS,CAAC;AAC3D,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,eAAe,GAAG,cAAc,GAAG,SAAS,CAAC;AACzF,MAAM,MAAM,kBAAkB,GAAG;IAAE,MAAM,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAChE,MAAM,MAAM,iBAAiB,GAAG;IAAE,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAE9D,KAAK,iBAAiB,GAAG;IACvB,QAAQ,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACnC,SAAS,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACpC,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,qBAAqB,GAAG;IAC1D,QAAQ,EAAE;QACR,QAAQ,EAAE,WAAW,CAAC;QACtB,KAAK,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;QAC3B,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;KAC9B,CAAC;IACF,MAAM,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IAChC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC/C,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC7C,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,YAAY,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAC;IACzC,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,iBAAiB,GAAG,kBAAkB,KAAK,IAAI,CAAC;CAClF,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,gBAAgB,CAAC,MAAM,CAAC,EAAE,QAAQ,GAAG,WAAW,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,IAAI,CAAC;IACjF,KAAK,CAAC,MAAM,CAAC,EAAE,kBAAkB,EAAE,YAAY,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACjE,IAAI,CAAC,MAAM,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACvC,MAAM,IAAI,IAAI,CAAC;CAChB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,SAAS,qBAAqB,KAAG,cAqFrE,CAAC"}
1
+ {"version":3,"file":"overlay-control.d.ts","sourceRoot":"","sources":["../../src/controls/overlay-control.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,iBAAiB,GAAG,cAAc,GAAG,SAAS,CAAC;AAC3D,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,eAAe,GAAG,cAAc,GAAG,OAAO,GAAG,SAAS,CAAC;AACnG,MAAM,MAAM,kBAAkB,GAAG;IAAE,MAAM,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAChE,MAAM,MAAM,iBAAiB,GAAG;IAAE,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAE9D,KAAK,iBAAiB,GAAG;IACvB,QAAQ,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACnC,SAAS,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IACpC,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,kBAAkB,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IAC7C,eAAe,CAAC,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IAC3C,iBAAiB,CAAC,EAAE,MAAM,WAAW,GAAG,IAAI,CAAC;IAC7C,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;IAC3B,MAAM,EAAE,MAAM,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC/C,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC7C,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,YAAY,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAC;IACzC,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE,iBAAiB,GAAG,kBAAkB,CAAA;KAAE,KAAK,IAAI,CAAC;CAC/F,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,CAAC,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,kBAAkB,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5E,IAAI,CAAC,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,iBAAiB,CAAA;KAAE,GAAG,IAAI,CAAC;IAClD,MAAM,IAAI,IAAI,CAAC;CAChB,CAAC;AA2CF,eAAO,MAAM,oBAAoB,GAAI,SAAS,qBAAqB,KAAG,cA2GrE,CAAC"}
@@ -1,2 +1,2 @@
1
- import{effect as e}from"../runtime-lifecycle.js";import{createControlState as t}from"./internal/control-state.js";import{onCleanup as n}from"@vielzeug/stateit";import{autoUpdate as r}from"@vielzeug/floatit";var i=i=>{let a=t(i);e(()=>{if(!i.isOpen.value||!i.positioner)return;let e=i.positioner.reference(),t=i.positioner.floating();if(!e||!t){i.positioner.update();return}n(r(e,t,()=>i.positioner?.update()))});let o=()=>typeof i.restoreFocus==`function`?i.restoreFocus():i.restoreFocus??!0,s=(e=`programmatic`)=>{a.disabled.value||i.isOpen.value||(i.setOpen(!0,e),requestAnimationFrame(()=>i.positioner?.update()),i.onOpen?.(e))},c=(e=`programmatic`,t)=>{i.isOpen.value&&(i.setOpen(!1,e),(t??o())&&i.elements.trigger?.focus(),i.onClose?.(e))},l=()=>{if(i.isOpen.value){c(`trigger`);return}s(`trigger`)},u=(e,t)=>e?e.contains(t):!1;return{bindOutsideClick:(e=document,t=!0)=>{let n=e=>{if(!i.isOpen.value)return;let t=e.composedPath()[0];t&&(u(i.elements.boundary,t)||u(i.elements.panel,t)||c(`outside-click`))};return e.addEventListener(`click`,n,{capture:t}),()=>e.removeEventListener(`click`,n,{capture:t})},close:c,open:s,toggle:l}};export{i as createOverlayControl};
1
+ import{autoUpdate as e}from"@vielzeug/floatit";var t=new Set,n=null,r=e=>e instanceof Error&&e.message.includes(`[stateit] Cannot read disposed computed signal`),i=()=>{if(n)return;let e=e=>{for(let n of[...t])try{n(e)}catch(e){if(r(e)){t.delete(n);continue}throw e}a()};document.addEventListener(`click`,e,{capture:!0}),n=()=>{document.removeEventListener(`click`,e,{capture:!0}),n=null}},a=()=>{t.size===0&&n&&n()},o=n=>{let o=null,s=null,c=()=>typeof n.restoreFocus==`function`?n.restoreFocus():n.restoreFocus??!0,l=(e,o)=>{try{n.setOpen(e,o)}catch(e){if(r(e)&&s){t.delete(s),a();return}throw e}e&&s?(t.add(s),i()):!e&&s&&(t.delete(s),a())},u=(t={})=>{let r=t.reason??`programmatic`;if(!(n.isDisabled?.()||n.isOpen())){if(l(!0,{reason:r}),n.positioner){let t=n.positioner.reference(),r=n.positioner.floating();t&&r&&(o=e(t,r,()=>n.positioner?.update())),n.positioner.update()}n.onOpen?.(r)}},d=(e={})=>{let t=e.reason??`programmatic`;n.isOpen()&&(l(!1,{reason:t}),o&&=(o(),null),(e.restoreFocus??c())&&n.getTriggerElement?.()?.focus(),n.onClose?.(t))};return s=e=>{if(!n.isOpen())return;let t=e.composedPath?.()??[],r=n.getBoundaryElement(),i=n.getPanelElement?.()??null,a=t[0]??e.target,o=a instanceof Node?a:null,s=t.some(e=>e===r||e===i),c=o?(r?.contains(o)??!1)||(i?.contains(o)??!1):!1;s||c||d({reason:`outside-click`})},{close:d,open:u,toggle:()=>{if(n.isOpen()){d({reason:`trigger`});return}u({reason:`trigger`})}}};export{o as createOverlayControl};
2
2
  //# sourceMappingURL=overlay-control.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"overlay-control.js","names":[],"sources":["../../src/controls/overlay-control.ts"],"sourcesContent":["import { autoUpdate } from '@vielzeug/floatit';\nimport { onCleanup as onSignalCleanup, type ReadonlySignal } from '@vielzeug/stateit';\n\nimport { effect } from '../runtime-lifecycle';\nimport { createControlState, type ControlContextOptions } from './internal/control-state';\n\nexport type OverlayOpenReason = 'programmatic' | 'trigger';\nexport type OverlayCloseReason = 'escape' | 'outside-click' | 'programmatic' | 'trigger';\nexport type OverlayCloseDetail = { reason: OverlayCloseReason };\nexport type OverlayOpenDetail = { reason: OverlayOpenReason };\n\ntype OverlayPositioner = {\n floating: () => HTMLElement | null;\n reference: () => HTMLElement | null;\n update: () => void;\n};\n\nexport type OverlayControlOptions = ControlContextOptions & {\n elements: {\n boundary: HTMLElement;\n panel?: HTMLElement | null;\n trigger?: HTMLElement | null;\n };\n isOpen: ReadonlySignal<boolean>;\n onClose?: (reason: OverlayCloseReason) => void;\n onOpen?: (reason: OverlayOpenReason) => void;\n positioner?: OverlayPositioner;\n restoreFocus?: boolean | (() => boolean);\n setOpen: (next: boolean, reason: OverlayOpenReason | OverlayCloseReason) => void;\n};\n\nexport type OverlayControl = {\n bindOutsideClick(target?: Document | HTMLElement, capture?: boolean): () => void;\n close(reason?: OverlayCloseReason, restoreFocus?: boolean): void;\n open(reason?: OverlayOpenReason): void;\n toggle(): void;\n};\n\nexport const createOverlayControl = (options: OverlayControlOptions): OverlayControl => {\n const controlState = createControlState(options);\n\n // Effect handles positioning lifecycle automatically\n effect(() => {\n if (!options.isOpen.value || !options.positioner) return;\n\n const reference = options.positioner.reference();\n const floating = options.positioner.floating();\n\n if (!reference || !floating) {\n options.positioner.update();\n\n return;\n }\n\n const cleanup = autoUpdate(reference, floating, () => options.positioner?.update());\n\n onSignalCleanup(cleanup); // Auto-cleanup on effect re-run/end\n });\n\n const shouldRestoreFocus = (): boolean => {\n if (typeof options.restoreFocus === 'function') return options.restoreFocus();\n\n return options.restoreFocus ?? true;\n };\n\n const open = (reason: OverlayOpenReason = 'programmatic'): void => {\n if (controlState.disabled.value || options.isOpen.value) return;\n\n options.setOpen(true, reason);\n requestAnimationFrame(() => options.positioner?.update());\n options.onOpen?.(reason);\n };\n\n const close = (reason: OverlayCloseReason = 'programmatic', restoreFocus?: boolean): void => {\n if (!options.isOpen.value) return;\n\n options.setOpen(false, reason);\n\n const restore = restoreFocus ?? shouldRestoreFocus();\n\n if (restore) options.elements.trigger?.focus();\n\n options.onClose?.(reason);\n };\n\n const toggle = (): void => {\n if (options.isOpen.value) {\n close('trigger');\n\n return;\n }\n\n open('trigger');\n };\n\n const isInside = (element: HTMLElement | null | undefined, target: Node): boolean => {\n return element ? element.contains(target) : false;\n };\n\n const bindOutsideClick = (target: Document | HTMLElement = document, capture = true): (() => void) => {\n const handler = (event: Event) => {\n if (!options.isOpen.value) return;\n\n const el = event.composedPath()[0] as Node | null;\n\n if (!el) return;\n\n const inside = isInside(options.elements.boundary, el) || isInside(options.elements.panel, el);\n\n if (!inside) close('outside-click');\n };\n\n target.addEventListener('click', handler, { capture });\n\n return () => target.removeEventListener('click', handler, { capture });\n };\n\n return {\n bindOutsideClick,\n close,\n open,\n toggle,\n };\n};\n"],"mappings":"+MAsCA,IAAa,EAAwB,GAAmD,CACtF,IAAM,EAAe,EAAmB,EAAQ,CAGhD,MAAa,CACX,GAAI,CAAC,EAAQ,OAAO,OAAS,CAAC,EAAQ,WAAY,OAElD,IAAM,EAAY,EAAQ,WAAW,WAAW,CAC1C,EAAW,EAAQ,WAAW,UAAU,CAE9C,GAAI,CAAC,GAAa,CAAC,EAAU,CAC3B,EAAQ,WAAW,QAAQ,CAE3B,OAKF,EAFgB,EAAW,EAAW,MAAgB,EAAQ,YAAY,QAAQ,CAAC,CAE3D,EACxB,CAEF,IAAM,MACA,OAAO,EAAQ,cAAiB,WAAmB,EAAQ,cAAc,CAEtE,EAAQ,cAAgB,GAG3B,GAAQ,EAA4B,iBAAyB,CAC7D,EAAa,SAAS,OAAS,EAAQ,OAAO,QAElD,EAAQ,QAAQ,GAAM,EAAO,CAC7B,0BAA4B,EAAQ,YAAY,QAAQ,CAAC,CACzD,EAAQ,SAAS,EAAO,GAGpB,GAAS,EAA6B,eAAgB,IAAiC,CACtF,EAAQ,OAAO,QAEpB,EAAQ,QAAQ,GAAO,EAAO,EAEd,GAAgB,GAAoB,GAEvC,EAAQ,SAAS,SAAS,OAAO,CAE9C,EAAQ,UAAU,EAAO,GAGrB,MAAqB,CACzB,GAAI,EAAQ,OAAO,MAAO,CACxB,EAAM,UAAU,CAEhB,OAGF,EAAK,UAAU,EAGX,GAAY,EAAyC,IAClD,EAAU,EAAQ,SAAS,EAAO,CAAG,GAqB9C,MAAO,CACL,kBAnBwB,EAAiC,SAAU,EAAU,KAAuB,CACpG,IAAM,EAAW,GAAiB,CAChC,GAAI,CAAC,EAAQ,OAAO,MAAO,OAE3B,IAAM,EAAK,EAAM,cAAc,CAAC,GAE3B,IAEU,EAAS,EAAQ,SAAS,SAAU,EAAG,EAAI,EAAS,EAAQ,SAAS,MAAO,EAAG,EAEjF,EAAM,gBAAgB,GAKrC,OAFA,EAAO,iBAAiB,QAAS,EAAS,CAAE,UAAS,CAAC,KAEzC,EAAO,oBAAoB,QAAS,EAAS,CAAE,UAAS,CAAC,EAKtE,QACA,OACA,SACD"}
1
+ {"version":3,"file":"overlay-control.js","names":[],"sources":["../../src/controls/overlay-control.ts"],"sourcesContent":["import { autoUpdate } from '@vielzeug/floatit';\n\nexport type OverlayOpenReason = 'programmatic' | 'trigger';\nexport type OverlayCloseReason = 'escape' | 'outside-click' | 'programmatic' | 'swipe' | 'trigger';\nexport type OverlayCloseDetail = { reason: OverlayCloseReason };\nexport type OverlayOpenDetail = { reason: OverlayOpenReason };\n\ntype OverlayPositioner = {\n floating: () => HTMLElement | null;\n reference: () => HTMLElement | null;\n update: () => void;\n};\n\nexport type OverlayControlOptions = {\n getBoundaryElement: () => HTMLElement | null;\n getPanelElement?: () => HTMLElement | null;\n getTriggerElement?: () => HTMLElement | null;\n isDisabled?: () => boolean;\n isOpen: () => boolean;\n onClose?: (reason: OverlayCloseReason) => void;\n onOpen?: (reason: OverlayOpenReason) => void;\n positioner?: OverlayPositioner;\n restoreFocus?: boolean | (() => boolean);\n setOpen: (next: boolean, context: { reason: OverlayOpenReason | OverlayCloseReason }) => void;\n};\n\nexport type OverlayControl = {\n close(opts?: { reason?: OverlayCloseReason; restoreFocus?: boolean }): void;\n open(opts?: { reason?: OverlayOpenReason }): void;\n toggle(): void;\n};\n\n// Module-level document click listener management for outside-click dismissal\nconst activeOverlayListeners = new Set<(event: Event) => void>();\nlet documentClickUnsubscribe: (() => void) | null = null;\n\nconst isDisposedComputedSignalError = (error: unknown): boolean =>\n error instanceof Error && error.message.includes('[stateit] Cannot read disposed computed signal');\n\nconst ensureDocumentClickListener = (): void => {\n if (documentClickUnsubscribe) return; // Already attached\n\n const handler = (event: Event) => {\n for (const listener of [...activeOverlayListeners]) {\n try {\n listener(event);\n } catch (error) {\n if (isDisposedComputedSignalError(error)) {\n activeOverlayListeners.delete(listener);\n\n continue;\n }\n\n throw error;\n }\n }\n\n removeDocumentClickListener();\n };\n\n document.addEventListener('click', handler, { capture: true });\n documentClickUnsubscribe = () => {\n document.removeEventListener('click', handler, { capture: true });\n documentClickUnsubscribe = null;\n };\n};\n\nconst removeDocumentClickListener = (): void => {\n if (activeOverlayListeners.size === 0 && documentClickUnsubscribe) {\n documentClickUnsubscribe();\n }\n};\n\nexport const createOverlayControl = (options: OverlayControlOptions): OverlayControl => {\n let positionerCleanup: (() => void) | null = null;\n let clickListener: ((event: Event) => void) | null = null;\n\n const shouldRestoreFocus = (): boolean => {\n if (typeof options.restoreFocus === 'function') return options.restoreFocus();\n\n return options.restoreFocus ?? true;\n };\n\n // Local wrapper — handles click-listener registration without mutating options.\n const commitOpen = (next: boolean, context: { reason: OverlayOpenReason | OverlayCloseReason }): void => {\n try {\n options.setOpen(next, context);\n } catch (error) {\n if (isDisposedComputedSignalError(error) && clickListener) {\n activeOverlayListeners.delete(clickListener);\n removeDocumentClickListener();\n\n return;\n }\n\n throw error;\n }\n\n if (next && clickListener) {\n activeOverlayListeners.add(clickListener);\n ensureDocumentClickListener();\n } else if (!next && clickListener) {\n activeOverlayListeners.delete(clickListener);\n removeDocumentClickListener();\n }\n };\n\n const open = (opts: { reason?: OverlayOpenReason } = {}): void => {\n const reason = opts.reason ?? 'programmatic';\n\n if (options.isDisabled?.() || options.isOpen()) return;\n\n commitOpen(true, { reason });\n\n if (options.positioner) {\n const reference = options.positioner.reference();\n const floating = options.positioner.floating();\n\n if (reference && floating) {\n positionerCleanup = autoUpdate(reference, floating, () => options.positioner?.update());\n }\n\n options.positioner.update();\n }\n\n options.onOpen?.(reason);\n };\n\n const close = (opts: { reason?: OverlayCloseReason; restoreFocus?: boolean } = {}): void => {\n const reason = opts.reason ?? 'programmatic';\n\n if (!options.isOpen()) return;\n\n commitOpen(false, { reason });\n\n if (positionerCleanup) {\n positionerCleanup();\n positionerCleanup = null;\n }\n\n const restore = opts.restoreFocus ?? shouldRestoreFocus();\n\n if (restore) options.getTriggerElement?.()?.focus();\n\n options.onClose?.(reason);\n };\n\n const toggle = (): void => {\n if (options.isOpen()) {\n close({ reason: 'trigger' });\n\n return;\n }\n\n open({ reason: 'trigger' });\n };\n\n clickListener = (event: Event) => {\n if (!options.isOpen()) return;\n\n const path = (event as Event & { composedPath?: () => EventTarget[] }).composedPath?.() ?? [];\n const boundary = options.getBoundaryElement();\n const panel = options.getPanelElement?.() ?? null;\n const target = (path[0] ?? event.target) as EventTarget | null;\n const nodeTarget = target instanceof Node ? target : null;\n\n const insideByPath = path.some((entry) => entry === boundary || entry === panel);\n const insideByContainment = nodeTarget\n ? (boundary?.contains(nodeTarget) ?? false) || (panel?.contains(nodeTarget) ?? false)\n : false;\n const inside = insideByPath || insideByContainment;\n\n if (!inside) close({ reason: 'outside-click' });\n };\n\n return {\n close,\n open,\n toggle,\n };\n};\n"],"mappings":"+CAiCA,IAAM,EAAyB,IAAI,IAC/B,EAAgD,KAE9C,EAAiC,GACrC,aAAiB,OAAS,EAAM,QAAQ,SAAS,gDAAgD,EAE7F,MAA0C,CAC9C,GAAI,EAA0B,OAE9B,IAAM,EAAW,GAAiB,CAChC,IAAK,IAAM,IAAY,CAAC,GAAG,CAAsB,EAC/C,GAAI,CACF,EAAS,CAAK,CAChB,OAAS,EAAO,CACd,GAAI,EAA8B,CAAK,EAAG,CACxC,EAAuB,OAAO,CAAQ,EAEtC,QACF,CAEA,MAAM,CACR,CAGF,EAA4B,CAC9B,EAEA,SAAS,iBAAiB,QAAS,EAAS,CAAE,QAAS,EAAK,CAAC,EAC7D,MAAiC,CAC/B,SAAS,oBAAoB,QAAS,EAAS,CAAE,QAAS,EAAK,CAAC,EAChE,EAA2B,IAC7B,CACF,EAEM,MAA0C,CAC1C,EAAuB,OAAS,GAAK,GACvC,EAAyB,CAE7B,EAEa,EAAwB,GAAmD,CACtF,IAAI,EAAyC,KACzC,EAAiD,KAE/C,MACA,OAAO,EAAQ,cAAiB,WAAmB,EAAQ,aAAa,EAErE,EAAQ,cAAgB,GAI3B,GAAc,EAAe,IAAsE,CACvG,GAAI,CACF,EAAQ,QAAQ,EAAM,CAAO,CAC/B,OAAS,EAAO,CACd,GAAI,EAA8B,CAAK,GAAK,EAAe,CACzD,EAAuB,OAAO,CAAa,EAC3C,EAA4B,EAE5B,MACF,CAEA,MAAM,CACR,CAEI,GAAQ,GACV,EAAuB,IAAI,CAAa,EACxC,EAA4B,GACnB,CAAC,GAAQ,IAClB,EAAuB,OAAO,CAAa,EAC3C,EAA4B,EAEhC,EAEM,GAAQ,EAAuC,CAAC,IAAY,CAChE,IAAM,EAAS,EAAK,QAAU,eAE1B,OAAQ,aAAa,GAAK,EAAQ,OAAO,GAI7C,IAFA,EAAW,GAAM,CAAE,QAAO,CAAC,EAEvB,EAAQ,WAAY,CACtB,IAAM,EAAY,EAAQ,WAAW,UAAU,EACzC,EAAW,EAAQ,WAAW,SAAS,EAEzC,GAAa,IACf,EAAoB,EAAW,EAAW,MAAgB,EAAQ,YAAY,OAAO,CAAC,GAGxF,EAAQ,WAAW,OAAO,CAC5B,CAEA,EAAQ,SAAS,CAAM,CAFvB,CAGF,EAEM,GAAS,EAAgE,CAAC,IAAY,CAC1F,IAAM,EAAS,EAAK,QAAU,eAEzB,EAAQ,OAAO,IAEpB,EAAW,GAAO,CAAE,QAAO,CAAC,EAE5B,AAEE,KADA,EAAkB,EACE,OAGN,EAAK,cAAgB,EAAmB,IAE3C,EAAQ,oBAAoB,GAAG,MAAM,EAElD,EAAQ,UAAU,CAAM,EAC1B,EA8BA,MAlBA,GAAiB,GAAiB,CAChC,GAAI,CAAC,EAAQ,OAAO,EAAG,OAEvB,IAAM,EAAQ,EAAyD,eAAe,GAAK,CAAC,EACtF,EAAW,EAAQ,mBAAmB,EACtC,EAAQ,EAAQ,kBAAkB,GAAK,KACvC,EAAU,EAAK,IAAM,EAAM,OAC3B,EAAa,aAAkB,KAAO,EAAS,KAE/C,EAAe,EAAK,KAAM,GAAU,IAAU,GAAY,IAAU,CAAK,EACzE,EAAsB,GACvB,GAAU,SAAS,CAAU,GAAK,MAAW,GAAO,SAAS,CAAU,GAAK,IAC7E,GACW,GAAgB,GAElB,EAAM,CAAE,OAAQ,eAAgB,CAAC,CAChD,EAEO,CACL,QACA,OACA,WA/ByB,CACzB,GAAI,EAAQ,OAAO,EAAG,CACpB,EAAM,CAAE,OAAQ,SAAU,CAAC,EAE3B,MACF,CAEA,EAAK,CAAE,OAAQ,SAAU,CAAC,CAC5B,CAwBA,CACF"}
@@ -0,0 +1,2 @@
1
+ const e=require(`./list-control.cjs`),t=require(`./overlay-control.cjs`),n=require(`../host.cjs`);let r=require(`@vielzeug/stateit`);var i=i=>{let a=e.createListControl({disabled:()=>!i.isOpen(),getIndex:i.getIndex,getItems:i.getItems,isItemDisabled:i.isItemDisabled,keys:i.keyboardMapping,loop:i.loop??!0,onNavigate:(e,t,n)=>{t>=0&&i.onNavigate?.(e,t,n)},setIndex:i.setIndex}),o=t.createOverlayControl({getBoundaryElement:i.getBoundaryElement,getPanelElement:i.getPanelElement,getTriggerElement:i.getTriggerElement,isDisabled:i.isDisabled,isOpen:i.isOpen,onClose:i.onClose,onOpen:i.onOpen,positioner:i.positioner,restoreFocus:i.restoreFocus,setOpen:i.setOpen}),s=(e=`listbox`)=>e===`menu`?{defaultTriggerAttributes:{controls:()=>i.listId,haspopup:`menu`},defaultTriggerRole:`button`}:{defaultTriggerAttributes:{controls:()=>i.listId,haspopup:`listbox`},defaultTriggerRole:`combobox`},c=(e,t)=>{let r=s(t?.role??i.ariaSync?.role??`listbox`);return n.syncAria(e,{...r.defaultTriggerAttributes,...t?.additional,...i.ariaSync?.additional,disabled:()=>i.isDisabled?.()??!1,expanded:()=>i.isOpen()?`true`:`false`,role:r.defaultTriggerRole})};return i.triggerRef&&(0,r.watch)(()=>i.triggerRef?.value,e=>{if(e)return c(e,i.ariaSync)}),{close:e=>o.close({reason:e??`programmatic`}),first:a.first,getActiveItem:a.getActiveItem,handleListKeydown:a.handleKeydown,last:a.last,next:a.next,open:e=>o.open({reason:e??`programmatic`}),prev:a.prev,reset:a.reset,set:a.set,toggle:o.toggle}};exports.createPopupListControl=i;
2
+ //# sourceMappingURL=popup-list-control.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"popup-list-control.cjs","names":[],"sources":["../../src/controls/popup-list-control.ts"],"sourcesContent":["import { watch } from '@vielzeug/stateit';\n\nimport type { OverlayCloseReason, OverlayOpenReason } from './overlay-control';\n\nimport { syncAria } from '../host';\nimport { createListControl } from './list-control';\nimport { createOverlayControl } from './overlay-control';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * ARIA role for the popup list. Affects which attributes are synced to the trigger.\n * - 'listbox': For select/combobox (role=\"combobox\" on trigger, role=\"listbox\" on panel)\n * - 'menu': For menus (role=\"menu\" on trigger, role=\"menu\" on panel)\n */\nexport type PopupListRole = 'listbox' | 'menu';\n\n/**\n * Configuration for syncing ARIA attributes to the trigger element.\n * Automatically handles role-specific attributes like aria-haspopup.\n */\nexport type PopupListAriaSyncConfig = {\n /**\n * Additional ARIA attributes to sync to the trigger beyond the standard ones.\n * These will be merged with role-specific defaults.\n */\n additional?: Record<\n string,\n boolean | null | number | string | undefined | (() => boolean | null | number | string | undefined)\n >;\n /**\n * The role of the popup list component. Controls which ARIA attributes are synced.\n * @default 'listbox'\n */\n role?: PopupListRole;\n};\n\n/**\n * Configuration for positioning the popup list using floatit.\n */\nexport type PopupListPositioner = {\n /** Gets the floating element (popup panel) */\n floating: () => HTMLElement | null;\n /** Get the reference element (trigger) */\n reference: () => HTMLElement | null;\n /** Update the position (called on open and during auto-update) */\n update: () => void;\n};\n\n/**\n * Main options for createPopupListControl.\n */\nexport type PopupListControlOptions<T> = {\n // ARIA\n /** Configuration for ARIA attributes */\n ariaSync?: PopupListAriaSyncConfig;\n // Overlay\n /** Get the boundary element (for click-outside detection) */\n getBoundaryElement: () => HTMLElement | null;\n\n // List state\n /** Get the current focused index (-1 if none focused) */\n getIndex: () => number;\n /** Get all items in the list */\n getItems: () => T[];\n /** Get the panel element */\n getPanelElement: () => HTMLElement | null;\n\n /** Get the trigger element */\n getTriggerElement: () => HTMLElement | null;\n // Overlay state\n /** Check if disabled */\n isDisabled?: () => boolean;\n\n // List navigation\n /** Check if an item is disabled */\n isItemDisabled?: (item: T, index: number) => boolean;\n\n /** Whether the popup is open */\n isOpen: () => boolean;\n /** Custom keyboard mappings for list navigation. Defaults to arrow keys. */\n keyboardMapping?:\n | (() => Partial<Record<'first' | 'last' | 'next' | 'prev', string[]>>)\n | Partial<Record<'first' | 'last' | 'next' | 'prev', string[]>>;\n\n /** The ID of the list element (for aria-controls/aria-owns) */\n listId?: string;\n\n /** Whether list navigation should loop (wrap around). @default true */\n loop?: boolean;\n\n /** Called when popup closes */\n onClose?: (reason: OverlayCloseReason) => void;\n\n /** Called when keyboard navigation is invoked */\n onNavigate?: (action: 'first' | 'last' | 'next' | 'prev', index: number, event: KeyboardEvent) => void;\n\n /** Called when popup opens */\n onOpen?: (reason: OverlayOpenReason) => void;\n\n /** Positioner for floating/positioning library integration */\n positioner?: PopupListPositioner;\n\n /** Whether to restore focus to trigger when closing. @default true */\n restoreFocus?: boolean | (() => boolean);\n\n /** Set the focused index */\n setIndex: (index: number) => void;\n\n /** Set open state */\n setOpen: (next: boolean, context: { reason: OverlayCloseReason | OverlayOpenReason }) => void;\n\n /** Ref-like trigger element used for internal ARIA syncing. */\n triggerRef?: { value: HTMLElement | null };\n};\n\n/**\n * Handle returned by createPopupListControl.\n * Provides methods to control the popup and list, and exposes underlying controls for advanced use.\n */\nexport type PopupListControl<T> = {\n /** Close the popup */\n close(reason?: OverlayCloseReason): void;\n\n // List navigation methods\n /** Move to first enabled item */\n first(): void;\n\n /** Get currently focused item */\n getActiveItem(): T | undefined;\n\n /** Handle keydown events for list navigation. Returns true if event was handled. */\n handleListKeydown(event: KeyboardEvent): boolean;\n\n /** Move to last enabled item */\n last(): void;\n\n /** Move to next enabled item */\n next(): void;\n\n /** Open the popup */\n open(reason?: OverlayOpenReason): void;\n\n /** Move to previous enabled item */\n prev(): void;\n\n /** Reset to no selection */\n reset(): void;\n\n /** Move to specific index */\n set(index: number): void;\n\n /** Toggle popup open/closed state */\n toggle(): void;\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Implementation\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Creates a unified popup list control that manages overlay positioning, list navigation,\n * keyboard handling, and ARIA attributes for select, combobox, and menu components.\n *\n * This composable eliminates duplication across components by providing a single,\n * flexible API for:\n * - Opening/closing popups with proper positioning\n * - Navigating lists with keyboard (arrow keys, Home, End)\n * - Syncing ARIA attributes for accessibility\n * - Managing focus restoration\n * - Handling outside-click dismissal\n *\n * @example\n * ```ts\n * // In a select component setup\n * const popupList = createPopupListControl({\n * // List state\n * getIndex: () => focusedIndex.value,\n * getItems: () => options.value,\n * setIndex: (idx) => { focusedIndex.value = idx; },\n *\n * // Elements\n * getBoundaryElement: () => host.el,\n * getTriggerElement: () => triggerEl,\n * getPanelElement: () => panelEl,\n * isOpen: () => isOpen.value,\n * setOpen: (next, ctx) => { isOpen.value = next; },\n *\n * // Positioning\n * positioner: {\n * reference: () => triggerEl,\n * floating: () => panelEl,\n * update: () => positioner.updatePosition(),\n * },\n *\n * // ARIA\n * ariaSync: { role: 'listbox' },\n * listId: `${selectId}-listbox`,\n * });\n *\n * // Keyboard events\n * function handleKeydown(e: KeyboardEvent) {\n * if (popupList.handleListKeydown(e)) return; // Consumed by navigation\n * // Custom keydown logic\n * }\n * ```\n */\nexport const createPopupListControl = <T>(options: PopupListControlOptions<T>): PopupListControl<T> => {\n // Create underlying list control\n const list = createListControl<T>({\n disabled: () => !options.isOpen(),\n getIndex: options.getIndex,\n getItems: options.getItems,\n isItemDisabled: options.isItemDisabled,\n keys: options.keyboardMapping,\n loop: options.loop ?? true,\n onNavigate: (action, index, event) => {\n if (index >= 0) {\n options.onNavigate?.(action, index, event);\n }\n },\n setIndex: options.setIndex,\n });\n\n // Create underlying overlay control\n const overlay = createOverlayControl({\n getBoundaryElement: options.getBoundaryElement,\n getPanelElement: options.getPanelElement,\n getTriggerElement: options.getTriggerElement,\n isDisabled: options.isDisabled,\n isOpen: options.isOpen,\n onClose: options.onClose,\n onOpen: options.onOpen,\n positioner: options.positioner,\n restoreFocus: options.restoreFocus,\n setOpen: options.setOpen,\n });\n\n // ─────────────────────────────────────────────────────────────────────────\n // ARIA Syncing\n // ─────────────────────────────────────────────────────────────────────────\n\n const getRoleConfig = (role: PopupListRole = 'listbox') => {\n if (role === 'menu') {\n return {\n defaultTriggerAttributes: {\n controls: () => options.listId,\n haspopup: 'menu' as const,\n },\n defaultTriggerRole: 'button' as const,\n };\n }\n\n return {\n defaultTriggerAttributes: {\n controls: () => options.listId,\n haspopup: 'listbox' as const,\n },\n defaultTriggerRole: 'combobox' as const,\n };\n };\n\n const syncTriggerAria = (trigger: Element, config?: PopupListAriaSyncConfig): (() => void) => {\n const role = config?.role ?? options.ariaSync?.role ?? 'listbox';\n const roleConfig = getRoleConfig(role);\n\n return syncAria(trigger, {\n ...roleConfig.defaultTriggerAttributes,\n ...config?.additional,\n ...options.ariaSync?.additional,\n disabled: () => options.isDisabled?.() ?? false,\n expanded: () => (options.isOpen() ? 'true' : 'false'),\n role: roleConfig.defaultTriggerRole,\n });\n };\n\n if (options.triggerRef) {\n watch(\n () => options.triggerRef?.value,\n (trigger) => {\n if (!trigger) return;\n\n return syncTriggerAria(trigger, options.ariaSync);\n },\n );\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Public API\n // ─────────────────────────────────────────────────────────────────────────\n\n return {\n close: (reason) => overlay.close({ reason: reason ?? 'programmatic' }),\n first: list.first,\n getActiveItem: list.getActiveItem,\n handleListKeydown: list.handleKeydown,\n last: list.last,\n next: list.next,\n open: (reason) => overlay.open({ reason: reason ?? 'programmatic' }),\n prev: list.prev,\n reset: list.reset,\n set: list.set,\n toggle: overlay.toggle,\n };\n};\n"],"mappings":"qIAiNA,IAAa,EAA6B,GAA6D,CAErG,IAAM,EAAO,EAAA,kBAAqB,CAChC,aAAgB,CAAC,EAAQ,OAAO,EAChC,SAAU,EAAQ,SAClB,SAAU,EAAQ,SAClB,eAAgB,EAAQ,eACxB,KAAM,EAAQ,gBACd,KAAM,EAAQ,MAAQ,GACtB,YAAa,EAAQ,EAAO,IAAU,CAChC,GAAS,GACX,EAAQ,aAAa,EAAQ,EAAO,CAAK,CAE7C,EACA,SAAU,EAAQ,QACpB,CAAC,EAGK,EAAU,EAAA,qBAAqB,CACnC,mBAAoB,EAAQ,mBAC5B,gBAAiB,EAAQ,gBACzB,kBAAmB,EAAQ,kBAC3B,WAAY,EAAQ,WACpB,OAAQ,EAAQ,OAChB,QAAS,EAAQ,QACjB,OAAQ,EAAQ,OAChB,WAAY,EAAQ,WACpB,aAAc,EAAQ,aACtB,QAAS,EAAQ,OACnB,CAAC,EAMK,GAAiB,EAAsB,YACvC,IAAS,OACJ,CACL,yBAA0B,CACxB,aAAgB,EAAQ,OACxB,SAAU,MACZ,EACA,mBAAoB,QACtB,EAGK,CACL,yBAA0B,CACxB,aAAgB,EAAQ,OACxB,SAAU,SACZ,EACA,mBAAoB,UACtB,EAGI,GAAmB,EAAkB,IAAmD,CAE5F,IAAM,EAAa,EADN,GAAQ,MAAQ,EAAQ,UAAU,MAAQ,SAClB,EAErC,OAAO,EAAA,SAAS,EAAS,CACvB,GAAG,EAAW,yBACd,GAAG,GAAQ,WACX,GAAG,EAAQ,UAAU,WACrB,aAAgB,EAAQ,aAAa,GAAK,GAC1C,aAAiB,EAAQ,OAAO,EAAI,OAAS,QAC7C,KAAM,EAAW,kBACnB,CAAC,CACH,EAiBA,OAfI,EAAQ,aACV,EAAA,EAAA,WACQ,EAAQ,YAAY,MACzB,GAAY,CACN,KAEL,OAAO,EAAgB,EAAS,EAAQ,QAAQ,CAClD,CACF,EAOK,CACL,MAAQ,GAAW,EAAQ,MAAM,CAAE,OAAQ,GAAU,cAAe,CAAC,EACrE,MAAO,EAAK,MACZ,cAAe,EAAK,cACpB,kBAAmB,EAAK,cACxB,KAAM,EAAK,KACX,KAAM,EAAK,KACX,KAAO,GAAW,EAAQ,KAAK,CAAE,OAAQ,GAAU,cAAe,CAAC,EACnE,KAAM,EAAK,KACX,MAAO,EAAK,MACZ,IAAK,EAAK,IACV,OAAQ,EAAQ,MAClB,CACF"}
@@ -0,0 +1,160 @@
1
+ import type { OverlayCloseReason, OverlayOpenReason } from './overlay-control';
2
+ /**
3
+ * ARIA role for the popup list. Affects which attributes are synced to the trigger.
4
+ * - 'listbox': For select/combobox (role="combobox" on trigger, role="listbox" on panel)
5
+ * - 'menu': For menus (role="menu" on trigger, role="menu" on panel)
6
+ */
7
+ export type PopupListRole = 'listbox' | 'menu';
8
+ /**
9
+ * Configuration for syncing ARIA attributes to the trigger element.
10
+ * Automatically handles role-specific attributes like aria-haspopup.
11
+ */
12
+ export type PopupListAriaSyncConfig = {
13
+ /**
14
+ * Additional ARIA attributes to sync to the trigger beyond the standard ones.
15
+ * These will be merged with role-specific defaults.
16
+ */
17
+ additional?: Record<string, boolean | null | number | string | undefined | (() => boolean | null | number | string | undefined)>;
18
+ /**
19
+ * The role of the popup list component. Controls which ARIA attributes are synced.
20
+ * @default 'listbox'
21
+ */
22
+ role?: PopupListRole;
23
+ };
24
+ /**
25
+ * Configuration for positioning the popup list using floatit.
26
+ */
27
+ export type PopupListPositioner = {
28
+ /** Gets the floating element (popup panel) */
29
+ floating: () => HTMLElement | null;
30
+ /** Get the reference element (trigger) */
31
+ reference: () => HTMLElement | null;
32
+ /** Update the position (called on open and during auto-update) */
33
+ update: () => void;
34
+ };
35
+ /**
36
+ * Main options for createPopupListControl.
37
+ */
38
+ export type PopupListControlOptions<T> = {
39
+ /** Configuration for ARIA attributes */
40
+ ariaSync?: PopupListAriaSyncConfig;
41
+ /** Get the boundary element (for click-outside detection) */
42
+ getBoundaryElement: () => HTMLElement | null;
43
+ /** Get the current focused index (-1 if none focused) */
44
+ getIndex: () => number;
45
+ /** Get all items in the list */
46
+ getItems: () => T[];
47
+ /** Get the panel element */
48
+ getPanelElement: () => HTMLElement | null;
49
+ /** Get the trigger element */
50
+ getTriggerElement: () => HTMLElement | null;
51
+ /** Check if disabled */
52
+ isDisabled?: () => boolean;
53
+ /** Check if an item is disabled */
54
+ isItemDisabled?: (item: T, index: number) => boolean;
55
+ /** Whether the popup is open */
56
+ isOpen: () => boolean;
57
+ /** Custom keyboard mappings for list navigation. Defaults to arrow keys. */
58
+ keyboardMapping?: (() => Partial<Record<'first' | 'last' | 'next' | 'prev', string[]>>) | Partial<Record<'first' | 'last' | 'next' | 'prev', string[]>>;
59
+ /** The ID of the list element (for aria-controls/aria-owns) */
60
+ listId?: string;
61
+ /** Whether list navigation should loop (wrap around). @default true */
62
+ loop?: boolean;
63
+ /** Called when popup closes */
64
+ onClose?: (reason: OverlayCloseReason) => void;
65
+ /** Called when keyboard navigation is invoked */
66
+ onNavigate?: (action: 'first' | 'last' | 'next' | 'prev', index: number, event: KeyboardEvent) => void;
67
+ /** Called when popup opens */
68
+ onOpen?: (reason: OverlayOpenReason) => void;
69
+ /** Positioner for floating/positioning library integration */
70
+ positioner?: PopupListPositioner;
71
+ /** Whether to restore focus to trigger when closing. @default true */
72
+ restoreFocus?: boolean | (() => boolean);
73
+ /** Set the focused index */
74
+ setIndex: (index: number) => void;
75
+ /** Set open state */
76
+ setOpen: (next: boolean, context: {
77
+ reason: OverlayCloseReason | OverlayOpenReason;
78
+ }) => void;
79
+ /** Ref-like trigger element used for internal ARIA syncing. */
80
+ triggerRef?: {
81
+ value: HTMLElement | null;
82
+ };
83
+ };
84
+ /**
85
+ * Handle returned by createPopupListControl.
86
+ * Provides methods to control the popup and list, and exposes underlying controls for advanced use.
87
+ */
88
+ export type PopupListControl<T> = {
89
+ /** Close the popup */
90
+ close(reason?: OverlayCloseReason): void;
91
+ /** Move to first enabled item */
92
+ first(): void;
93
+ /** Get currently focused item */
94
+ getActiveItem(): T | undefined;
95
+ /** Handle keydown events for list navigation. Returns true if event was handled. */
96
+ handleListKeydown(event: KeyboardEvent): boolean;
97
+ /** Move to last enabled item */
98
+ last(): void;
99
+ /** Move to next enabled item */
100
+ next(): void;
101
+ /** Open the popup */
102
+ open(reason?: OverlayOpenReason): void;
103
+ /** Move to previous enabled item */
104
+ prev(): void;
105
+ /** Reset to no selection */
106
+ reset(): void;
107
+ /** Move to specific index */
108
+ set(index: number): void;
109
+ /** Toggle popup open/closed state */
110
+ toggle(): void;
111
+ };
112
+ /**
113
+ * Creates a unified popup list control that manages overlay positioning, list navigation,
114
+ * keyboard handling, and ARIA attributes for select, combobox, and menu components.
115
+ *
116
+ * This composable eliminates duplication across components by providing a single,
117
+ * flexible API for:
118
+ * - Opening/closing popups with proper positioning
119
+ * - Navigating lists with keyboard (arrow keys, Home, End)
120
+ * - Syncing ARIA attributes for accessibility
121
+ * - Managing focus restoration
122
+ * - Handling outside-click dismissal
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * // In a select component setup
127
+ * const popupList = createPopupListControl({
128
+ * // List state
129
+ * getIndex: () => focusedIndex.value,
130
+ * getItems: () => options.value,
131
+ * setIndex: (idx) => { focusedIndex.value = idx; },
132
+ *
133
+ * // Elements
134
+ * getBoundaryElement: () => host.el,
135
+ * getTriggerElement: () => triggerEl,
136
+ * getPanelElement: () => panelEl,
137
+ * isOpen: () => isOpen.value,
138
+ * setOpen: (next, ctx) => { isOpen.value = next; },
139
+ *
140
+ * // Positioning
141
+ * positioner: {
142
+ * reference: () => triggerEl,
143
+ * floating: () => panelEl,
144
+ * update: () => positioner.updatePosition(),
145
+ * },
146
+ *
147
+ * // ARIA
148
+ * ariaSync: { role: 'listbox' },
149
+ * listId: `${selectId}-listbox`,
150
+ * });
151
+ *
152
+ * // Keyboard events
153
+ * function handleKeydown(e: KeyboardEvent) {
154
+ * if (popupList.handleListKeydown(e)) return; // Consumed by navigation
155
+ * // Custom keydown logic
156
+ * }
157
+ * ```
158
+ */
159
+ export declare const createPopupListControl: <T>(options: PopupListControlOptions<T>) => PopupListControl<T>;
160
+ //# sourceMappingURL=popup-list-control.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"popup-list-control.d.ts","sourceRoot":"","sources":["../../src/controls/popup-list-control.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{createListControl as e}from"./list-control.js";import{createOverlayControl as t}from"./overlay-control.js";import{syncAria as n}from"../host.js";import{watch as r}from"@vielzeug/stateit";var i=i=>{let a=e({disabled:()=>!i.isOpen(),getIndex:i.getIndex,getItems:i.getItems,isItemDisabled:i.isItemDisabled,keys:i.keyboardMapping,loop:i.loop??!0,onNavigate:(e,t,n)=>{t>=0&&i.onNavigate?.(e,t,n)},setIndex:i.setIndex}),o=t({getBoundaryElement:i.getBoundaryElement,getPanelElement:i.getPanelElement,getTriggerElement:i.getTriggerElement,isDisabled:i.isDisabled,isOpen:i.isOpen,onClose:i.onClose,onOpen:i.onOpen,positioner:i.positioner,restoreFocus:i.restoreFocus,setOpen:i.setOpen}),s=(e=`listbox`)=>e===`menu`?{defaultTriggerAttributes:{controls:()=>i.listId,haspopup:`menu`},defaultTriggerRole:`button`}:{defaultTriggerAttributes:{controls:()=>i.listId,haspopup:`listbox`},defaultTriggerRole:`combobox`},c=(e,t)=>{let r=s(t?.role??i.ariaSync?.role??`listbox`);return n(e,{...r.defaultTriggerAttributes,...t?.additional,...i.ariaSync?.additional,disabled:()=>i.isDisabled?.()??!1,expanded:()=>i.isOpen()?`true`:`false`,role:r.defaultTriggerRole})};return i.triggerRef&&r(()=>i.triggerRef?.value,e=>{if(e)return c(e,i.ariaSync)}),{close:e=>o.close({reason:e??`programmatic`}),first:a.first,getActiveItem:a.getActiveItem,handleListKeydown:a.handleKeydown,last:a.last,next:a.next,open:e=>o.open({reason:e??`programmatic`}),prev:a.prev,reset:a.reset,set:a.set,toggle:o.toggle}};export{i as createPopupListControl};
2
+ //# sourceMappingURL=popup-list-control.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"popup-list-control.js","names":[],"sources":["../../src/controls/popup-list-control.ts"],"sourcesContent":["import { watch } from '@vielzeug/stateit';\n\nimport type { OverlayCloseReason, OverlayOpenReason } from './overlay-control';\n\nimport { syncAria } from '../host';\nimport { createListControl } from './list-control';\nimport { createOverlayControl } from './overlay-control';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * ARIA role for the popup list. Affects which attributes are synced to the trigger.\n * - 'listbox': For select/combobox (role=\"combobox\" on trigger, role=\"listbox\" on panel)\n * - 'menu': For menus (role=\"menu\" on trigger, role=\"menu\" on panel)\n */\nexport type PopupListRole = 'listbox' | 'menu';\n\n/**\n * Configuration for syncing ARIA attributes to the trigger element.\n * Automatically handles role-specific attributes like aria-haspopup.\n */\nexport type PopupListAriaSyncConfig = {\n /**\n * Additional ARIA attributes to sync to the trigger beyond the standard ones.\n * These will be merged with role-specific defaults.\n */\n additional?: Record<\n string,\n boolean | null | number | string | undefined | (() => boolean | null | number | string | undefined)\n >;\n /**\n * The role of the popup list component. Controls which ARIA attributes are synced.\n * @default 'listbox'\n */\n role?: PopupListRole;\n};\n\n/**\n * Configuration for positioning the popup list using floatit.\n */\nexport type PopupListPositioner = {\n /** Gets the floating element (popup panel) */\n floating: () => HTMLElement | null;\n /** Get the reference element (trigger) */\n reference: () => HTMLElement | null;\n /** Update the position (called on open and during auto-update) */\n update: () => void;\n};\n\n/**\n * Main options for createPopupListControl.\n */\nexport type PopupListControlOptions<T> = {\n // ARIA\n /** Configuration for ARIA attributes */\n ariaSync?: PopupListAriaSyncConfig;\n // Overlay\n /** Get the boundary element (for click-outside detection) */\n getBoundaryElement: () => HTMLElement | null;\n\n // List state\n /** Get the current focused index (-1 if none focused) */\n getIndex: () => number;\n /** Get all items in the list */\n getItems: () => T[];\n /** Get the panel element */\n getPanelElement: () => HTMLElement | null;\n\n /** Get the trigger element */\n getTriggerElement: () => HTMLElement | null;\n // Overlay state\n /** Check if disabled */\n isDisabled?: () => boolean;\n\n // List navigation\n /** Check if an item is disabled */\n isItemDisabled?: (item: T, index: number) => boolean;\n\n /** Whether the popup is open */\n isOpen: () => boolean;\n /** Custom keyboard mappings for list navigation. Defaults to arrow keys. */\n keyboardMapping?:\n | (() => Partial<Record<'first' | 'last' | 'next' | 'prev', string[]>>)\n | Partial<Record<'first' | 'last' | 'next' | 'prev', string[]>>;\n\n /** The ID of the list element (for aria-controls/aria-owns) */\n listId?: string;\n\n /** Whether list navigation should loop (wrap around). @default true */\n loop?: boolean;\n\n /** Called when popup closes */\n onClose?: (reason: OverlayCloseReason) => void;\n\n /** Called when keyboard navigation is invoked */\n onNavigate?: (action: 'first' | 'last' | 'next' | 'prev', index: number, event: KeyboardEvent) => void;\n\n /** Called when popup opens */\n onOpen?: (reason: OverlayOpenReason) => void;\n\n /** Positioner for floating/positioning library integration */\n positioner?: PopupListPositioner;\n\n /** Whether to restore focus to trigger when closing. @default true */\n restoreFocus?: boolean | (() => boolean);\n\n /** Set the focused index */\n setIndex: (index: number) => void;\n\n /** Set open state */\n setOpen: (next: boolean, context: { reason: OverlayCloseReason | OverlayOpenReason }) => void;\n\n /** Ref-like trigger element used for internal ARIA syncing. */\n triggerRef?: { value: HTMLElement | null };\n};\n\n/**\n * Handle returned by createPopupListControl.\n * Provides methods to control the popup and list, and exposes underlying controls for advanced use.\n */\nexport type PopupListControl<T> = {\n /** Close the popup */\n close(reason?: OverlayCloseReason): void;\n\n // List navigation methods\n /** Move to first enabled item */\n first(): void;\n\n /** Get currently focused item */\n getActiveItem(): T | undefined;\n\n /** Handle keydown events for list navigation. Returns true if event was handled. */\n handleListKeydown(event: KeyboardEvent): boolean;\n\n /** Move to last enabled item */\n last(): void;\n\n /** Move to next enabled item */\n next(): void;\n\n /** Open the popup */\n open(reason?: OverlayOpenReason): void;\n\n /** Move to previous enabled item */\n prev(): void;\n\n /** Reset to no selection */\n reset(): void;\n\n /** Move to specific index */\n set(index: number): void;\n\n /** Toggle popup open/closed state */\n toggle(): void;\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Implementation\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Creates a unified popup list control that manages overlay positioning, list navigation,\n * keyboard handling, and ARIA attributes for select, combobox, and menu components.\n *\n * This composable eliminates duplication across components by providing a single,\n * flexible API for:\n * - Opening/closing popups with proper positioning\n * - Navigating lists with keyboard (arrow keys, Home, End)\n * - Syncing ARIA attributes for accessibility\n * - Managing focus restoration\n * - Handling outside-click dismissal\n *\n * @example\n * ```ts\n * // In a select component setup\n * const popupList = createPopupListControl({\n * // List state\n * getIndex: () => focusedIndex.value,\n * getItems: () => options.value,\n * setIndex: (idx) => { focusedIndex.value = idx; },\n *\n * // Elements\n * getBoundaryElement: () => host.el,\n * getTriggerElement: () => triggerEl,\n * getPanelElement: () => panelEl,\n * isOpen: () => isOpen.value,\n * setOpen: (next, ctx) => { isOpen.value = next; },\n *\n * // Positioning\n * positioner: {\n * reference: () => triggerEl,\n * floating: () => panelEl,\n * update: () => positioner.updatePosition(),\n * },\n *\n * // ARIA\n * ariaSync: { role: 'listbox' },\n * listId: `${selectId}-listbox`,\n * });\n *\n * // Keyboard events\n * function handleKeydown(e: KeyboardEvent) {\n * if (popupList.handleListKeydown(e)) return; // Consumed by navigation\n * // Custom keydown logic\n * }\n * ```\n */\nexport const createPopupListControl = <T>(options: PopupListControlOptions<T>): PopupListControl<T> => {\n // Create underlying list control\n const list = createListControl<T>({\n disabled: () => !options.isOpen(),\n getIndex: options.getIndex,\n getItems: options.getItems,\n isItemDisabled: options.isItemDisabled,\n keys: options.keyboardMapping,\n loop: options.loop ?? true,\n onNavigate: (action, index, event) => {\n if (index >= 0) {\n options.onNavigate?.(action, index, event);\n }\n },\n setIndex: options.setIndex,\n });\n\n // Create underlying overlay control\n const overlay = createOverlayControl({\n getBoundaryElement: options.getBoundaryElement,\n getPanelElement: options.getPanelElement,\n getTriggerElement: options.getTriggerElement,\n isDisabled: options.isDisabled,\n isOpen: options.isOpen,\n onClose: options.onClose,\n onOpen: options.onOpen,\n positioner: options.positioner,\n restoreFocus: options.restoreFocus,\n setOpen: options.setOpen,\n });\n\n // ─────────────────────────────────────────────────────────────────────────\n // ARIA Syncing\n // ─────────────────────────────────────────────────────────────────────────\n\n const getRoleConfig = (role: PopupListRole = 'listbox') => {\n if (role === 'menu') {\n return {\n defaultTriggerAttributes: {\n controls: () => options.listId,\n haspopup: 'menu' as const,\n },\n defaultTriggerRole: 'button' as const,\n };\n }\n\n return {\n defaultTriggerAttributes: {\n controls: () => options.listId,\n haspopup: 'listbox' as const,\n },\n defaultTriggerRole: 'combobox' as const,\n };\n };\n\n const syncTriggerAria = (trigger: Element, config?: PopupListAriaSyncConfig): (() => void) => {\n const role = config?.role ?? options.ariaSync?.role ?? 'listbox';\n const roleConfig = getRoleConfig(role);\n\n return syncAria(trigger, {\n ...roleConfig.defaultTriggerAttributes,\n ...config?.additional,\n ...options.ariaSync?.additional,\n disabled: () => options.isDisabled?.() ?? false,\n expanded: () => (options.isOpen() ? 'true' : 'false'),\n role: roleConfig.defaultTriggerRole,\n });\n };\n\n if (options.triggerRef) {\n watch(\n () => options.triggerRef?.value,\n (trigger) => {\n if (!trigger) return;\n\n return syncTriggerAria(trigger, options.ariaSync);\n },\n );\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Public API\n // ─────────────────────────────────────────────────────────────────────────\n\n return {\n close: (reason) => overlay.close({ reason: reason ?? 'programmatic' }),\n first: list.first,\n getActiveItem: list.getActiveItem,\n handleListKeydown: list.handleKeydown,\n last: list.last,\n next: list.next,\n open: (reason) => overlay.open({ reason: reason ?? 'programmatic' }),\n prev: list.prev,\n reset: list.reset,\n set: list.set,\n toggle: overlay.toggle,\n };\n};\n"],"mappings":"kMAiNA,IAAa,EAA6B,GAA6D,CAErG,IAAM,EAAO,EAAqB,CAChC,aAAgB,CAAC,EAAQ,OAAO,EAChC,SAAU,EAAQ,SAClB,SAAU,EAAQ,SAClB,eAAgB,EAAQ,eACxB,KAAM,EAAQ,gBACd,KAAM,EAAQ,MAAQ,GACtB,YAAa,EAAQ,EAAO,IAAU,CAChC,GAAS,GACX,EAAQ,aAAa,EAAQ,EAAO,CAAK,CAE7C,EACA,SAAU,EAAQ,QACpB,CAAC,EAGK,EAAU,EAAqB,CACnC,mBAAoB,EAAQ,mBAC5B,gBAAiB,EAAQ,gBACzB,kBAAmB,EAAQ,kBAC3B,WAAY,EAAQ,WACpB,OAAQ,EAAQ,OAChB,QAAS,EAAQ,QACjB,OAAQ,EAAQ,OAChB,WAAY,EAAQ,WACpB,aAAc,EAAQ,aACtB,QAAS,EAAQ,OACnB,CAAC,EAMK,GAAiB,EAAsB,YACvC,IAAS,OACJ,CACL,yBAA0B,CACxB,aAAgB,EAAQ,OACxB,SAAU,MACZ,EACA,mBAAoB,QACtB,EAGK,CACL,yBAA0B,CACxB,aAAgB,EAAQ,OACxB,SAAU,SACZ,EACA,mBAAoB,UACtB,EAGI,GAAmB,EAAkB,IAAmD,CAE5F,IAAM,EAAa,EADN,GAAQ,MAAQ,EAAQ,UAAU,MAAQ,SAClB,EAErC,OAAO,EAAS,EAAS,CACvB,GAAG,EAAW,yBACd,GAAG,GAAQ,WACX,GAAG,EAAQ,UAAU,WACrB,aAAgB,EAAQ,aAAa,GAAK,GAC1C,aAAiB,EAAQ,OAAO,EAAI,OAAS,QAC7C,KAAM,EAAW,kBACnB,CAAC,CACH,EAiBA,OAfI,EAAQ,YACV,MACQ,EAAQ,YAAY,MACzB,GAAY,CACN,KAEL,OAAO,EAAgB,EAAS,EAAQ,QAAQ,CAClD,CACF,EAOK,CACL,MAAQ,GAAW,EAAQ,MAAM,CAAE,OAAQ,GAAU,cAAe,CAAC,EACrE,MAAO,EAAK,MACZ,cAAe,EAAK,cACpB,kBAAmB,EAAK,cACxB,KAAM,EAAK,KACX,KAAM,EAAK,KACX,KAAO,GAAW,EAAQ,KAAK,CAAE,OAAQ,GAAU,cAAe,CAAC,EACnE,KAAM,EAAK,KACX,MAAO,EAAK,MACZ,IAAK,EAAK,IACV,OAAQ,EAAQ,MAClB,CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"press-control.cjs","names":[],"sources":["../../src/controls/press-control.ts"],"sourcesContent":["import { dispatchKeyboardAction } from './internal/keyboard-utils';\n\nexport type PressTrigger = 'keyboard' | 'pointer';\n\nexport type PressControlOptions = {\n disabled?: () => boolean;\n keys?: string[];\n onPress: (originalEvent: KeyboardEvent | MouseEvent, trigger: PressTrigger) => void;\n};\n\nexport type PressControl = {\n handleClick: (event: MouseEvent) => boolean;\n handleKeydown: (event: KeyboardEvent) => boolean;\n};\n\nexport const createPressControl = (options: PressControlOptions): PressControl => {\n const isDisabled = (): boolean => Boolean(options.disabled?.());\n const keyboardKeys = options.keys ?? ['Enter', ' '];\n const keymap = Object.fromEntries(\n keyboardKeys.map((key) => [key, (keyboardEvent: KeyboardEvent) => options.onPress(keyboardEvent, 'keyboard')]),\n );\n\n const handleClick = (event: MouseEvent): boolean => {\n if (isDisabled()) return false;\n\n options.onPress(event, 'pointer');\n\n return true;\n };\n\n const handleKeydown = (event: KeyboardEvent): boolean => {\n return dispatchKeyboardAction(event, {\n disabled: isDisabled,\n keymap,\n });\n };\n\n return {\n handleClick,\n handleKeydown,\n };\n};\n"],"mappings":"iDAeA,IAAa,EAAsB,GAA+C,CAChF,IAAM,MAA4B,EAAQ,EAAQ,YAAY,CACxD,EAAe,EAAQ,MAAQ,CAAC,QAAS,IAAI,CAC7C,EAAS,OAAO,YACpB,EAAa,IAAK,GAAQ,CAAC,EAAM,GAAiC,EAAQ,QAAQ,EAAe,WAAW,CAAC,CAAC,CAC/G,CAiBD,MAAO,CACL,YAhBmB,GACf,GAAY,CAAS,IAEzB,EAAQ,QAAQ,EAAO,UAAU,CAE1B,IAYP,cATqB,GACd,EAAA,uBAAuB,EAAO,CACnC,SAAU,EACV,SACD,CAAC,CAMH"}
1
+ {"version":3,"file":"press-control.cjs","names":[],"sources":["../../src/controls/press-control.ts"],"sourcesContent":["import { dispatchKeyboardAction } from './internal/keyboard-utils';\n\nexport type PressTrigger = 'keyboard' | 'pointer';\n\nexport type PressControlOptions = {\n disabled?: () => boolean;\n keys?: string[];\n onPress: (originalEvent: KeyboardEvent | MouseEvent, trigger: PressTrigger) => void;\n};\n\nexport type PressControl = {\n handleClick: (event: MouseEvent) => boolean;\n handleKeydown: (event: KeyboardEvent) => boolean;\n};\n\nexport const createPressControl = (options: PressControlOptions): PressControl => {\n const isDisabled = (): boolean => Boolean(options.disabled?.());\n const keyboardKeys = options.keys ?? ['Enter', ' '];\n const keymap = Object.fromEntries(\n keyboardKeys.map((key) => [key, (keyboardEvent: KeyboardEvent) => options.onPress(keyboardEvent, 'keyboard')]),\n );\n\n const handleClick = (event: MouseEvent): boolean => {\n if (isDisabled()) return false;\n\n options.onPress(event, 'pointer');\n\n return true;\n };\n\n const handleKeydown = (event: KeyboardEvent): boolean => {\n return dispatchKeyboardAction(event, {\n disabled: isDisabled,\n keymap,\n });\n };\n\n return {\n handleClick,\n handleKeydown,\n };\n};\n"],"mappings":"iDAeA,IAAa,EAAsB,GAA+C,CAChF,IAAM,MAA4B,EAAQ,EAAQ,WAAW,EACvD,EAAe,EAAQ,MAAQ,CAAC,QAAS,GAAG,EAC5C,EAAS,OAAO,YACpB,EAAa,IAAK,GAAQ,CAAC,EAAM,GAAiC,EAAQ,QAAQ,EAAe,UAAU,CAAC,CAAC,CAC/G,EAiBA,MAAO,CACL,YAhBmB,GACf,EAAW,EAAU,IAEzB,EAAQ,QAAQ,EAAO,SAAS,EAEzB,IAYP,cATqB,GACd,EAAA,uBAAuB,EAAO,CACnC,SAAU,EACV,QACF,CAAC,CAMH,CACF"}