@kouji-ui/core 0.0.3 → 0.1.1

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 (534) hide show
  1. package/.turbo/turbo-build.log +6 -3
  2. package/CHANGELOG.md +177 -0
  3. package/ng-package.json +5 -1
  4. package/package.json +18 -6
  5. package/src/a11y/aria-describedby.ts +7 -3
  6. package/src/a11y/aria-labelledby.spec.ts +55 -0
  7. package/src/a11y/aria-labelledby.ts +38 -0
  8. package/src/a11y/focus-trap.ts +4 -2
  9. package/src/a11y/index.ts +1 -0
  10. package/src/a11y/live-region.ts +4 -2
  11. package/src/a11y/roving-tabindex.spec.ts +139 -0
  12. package/src/a11y/roving-tabindex.ts +56 -7
  13. package/src/a11y/visually-hidden.ts +4 -2
  14. package/src/accordion/accordion.context.ts +22 -0
  15. package/src/accordion/accordion.spec.ts +233 -23
  16. package/src/accordion/accordion.ts +311 -39
  17. package/src/accordion/index.ts +8 -2
  18. package/src/alert/alert.context.ts +28 -0
  19. package/src/alert/alert.spec.ts +217 -0
  20. package/src/alert/alert.ts +417 -0
  21. package/src/alert/config.ts +46 -0
  22. package/src/alert/index.ts +19 -0
  23. package/src/avatar/avatar-group.context.ts +45 -0
  24. package/src/avatar/avatar-group.spec.ts +358 -0
  25. package/src/avatar/avatar-group.ts +259 -0
  26. package/src/avatar/avatar.ts +17 -4
  27. package/src/avatar/index.ts +11 -1
  28. package/src/badge/badge.spec.ts +8 -0
  29. package/src/badge/badge.ts +24 -3
  30. package/src/breadcrumb/breadcrumb-current.ts +38 -0
  31. package/src/breadcrumb/breadcrumb-ellipsis.ts +71 -0
  32. package/src/breadcrumb/breadcrumb-item.ts +41 -0
  33. package/src/breadcrumb/breadcrumb-link.ts +44 -0
  34. package/src/breadcrumb/breadcrumb-list.ts +42 -0
  35. package/src/breadcrumb/breadcrumb-separator.ts +45 -0
  36. package/src/breadcrumb/breadcrumb.context.ts +45 -0
  37. package/src/breadcrumb/breadcrumb.spec.ts +354 -0
  38. package/src/breadcrumb/breadcrumb.ts +239 -0
  39. package/src/breadcrumb/config.ts +84 -0
  40. package/src/breadcrumb/index.ts +18 -0
  41. package/src/button/_examples/button.example.ts +71 -0
  42. package/src/button/_examples/button.finance.example.ts +83 -0
  43. package/src/button/_examples/button.retro.example.ts +91 -0
  44. package/src/button/_examples/button.sizes.example.ts +60 -0
  45. package/src/button/_examples/index.ts +6 -0
  46. package/src/button/button.spec.ts +139 -8
  47. package/src/button/button.ts +114 -16
  48. package/src/button/config.spec.ts +33 -0
  49. package/src/button/config.ts +45 -0
  50. package/src/button/index.ts +2 -1
  51. package/src/button-group/button-group.context.ts +28 -0
  52. package/src/button-group/button-group.spec.ts +99 -0
  53. package/src/button-group/button-group.ts +99 -0
  54. package/src/button-group/index.ts +2 -0
  55. package/src/calendar/calendar-day.ts +102 -0
  56. package/src/calendar/calendar-grid.ts +90 -0
  57. package/src/calendar/calendar-header.ts +65 -0
  58. package/src/calendar/calendar.context.ts +53 -0
  59. package/src/calendar/calendar.spec.ts +225 -0
  60. package/src/calendar/calendar.ts +233 -0
  61. package/src/calendar/date-utils.ts +225 -0
  62. package/src/calendar/index.ts +28 -0
  63. package/src/carousel/carousel.context.ts +81 -0
  64. package/src/carousel/carousel.spec.ts +337 -0
  65. package/src/carousel/carousel.ts +919 -0
  66. package/src/carousel/index.ts +22 -0
  67. package/src/cascade-select/cascade-select-option.ts +205 -0
  68. package/src/cascade-select/cascade-select-panel.ts +106 -0
  69. package/src/cascade-select/cascade-select-root.ts +265 -0
  70. package/src/cascade-select/cascade-select-sub-panel.ts +168 -0
  71. package/src/cascade-select/cascade-select-trigger.ts +44 -0
  72. package/src/cascade-select/cascade-select.context.ts +71 -0
  73. package/src/cascade-select/cascade-select.spec.ts +241 -0
  74. package/src/cascade-select/cascade-select.ts +5 -0
  75. package/src/cascade-select/index.ts +11 -0
  76. package/src/chart/chart.ts +6 -2
  77. package/src/chat/chat-avatar.ts +31 -0
  78. package/src/chat/chat-bubble.ts +49 -0
  79. package/src/chat/chat-footer.ts +67 -0
  80. package/src/chat/chat-header.ts +41 -0
  81. package/src/chat/chat-log.ts +43 -0
  82. package/src/chat/chat.context.ts +64 -0
  83. package/src/chat/chat.spec.ts +341 -0
  84. package/src/chat/chat.ts +191 -0
  85. package/src/chat/config.ts +53 -0
  86. package/src/chat/index.ts +21 -0
  87. package/src/checkbox/checkbox.ts +9 -2
  88. package/src/color-picker/color-picker.context.ts +74 -0
  89. package/src/color-picker/color-picker.spec.ts +241 -0
  90. package/src/color-picker/color-picker.ts +698 -0
  91. package/src/color-picker/color-picker.utils.ts +140 -0
  92. package/src/color-picker/index.ts +28 -0
  93. package/src/combobox/combobox-input.ts +143 -0
  94. package/src/combobox/combobox-listbox.ts +49 -0
  95. package/src/combobox/combobox-option.ts +43 -0
  96. package/src/combobox/combobox-root.ts +281 -0
  97. package/src/combobox/combobox.spec.ts +75 -0
  98. package/src/combobox/combobox.ts +4 -0
  99. package/src/combobox/index.ts +4 -0
  100. package/src/command-palette/command-empty.ts +36 -0
  101. package/src/command-palette/command-group.ts +21 -0
  102. package/src/command-palette/command-input.ts +70 -0
  103. package/src/command-palette/command-item.ts +60 -0
  104. package/src/command-palette/command-list.ts +25 -0
  105. package/src/command-palette/command-palette-dialog.ts +74 -0
  106. package/src/command-palette/command-palette-trigger.ts +64 -0
  107. package/src/command-palette/command-palette.filters.ts +53 -0
  108. package/src/command-palette/command-palette.spec.ts +349 -0
  109. package/src/command-palette/command-palette.ts +204 -0
  110. package/src/command-palette/command-separator.ts +18 -0
  111. package/src/command-palette/index.ts +15 -0
  112. package/src/confirm-popup/confirm-popup-action.ts +39 -0
  113. package/src/confirm-popup/confirm-popup-cancel.ts +38 -0
  114. package/src/confirm-popup/confirm-popup-content.ts +113 -0
  115. package/src/confirm-popup/confirm-popup-message.ts +36 -0
  116. package/src/confirm-popup/confirm-popup-trigger.ts +31 -0
  117. package/src/confirm-popup/confirm-popup.context.ts +41 -0
  118. package/src/confirm-popup/confirm-popup.spec.ts +337 -0
  119. package/src/confirm-popup/confirm-popup.ts +109 -0
  120. package/src/confirm-popup/index.ts +12 -0
  121. package/src/date-picker/date-picker-calendar.ts +65 -0
  122. package/src/date-picker/date-picker-trigger.ts +215 -0
  123. package/src/date-picker/date-picker.context.ts +50 -0
  124. package/src/date-picker/date-picker.spec.ts +117 -0
  125. package/src/date-picker/date-picker.ts +127 -0
  126. package/src/date-picker/index.ts +7 -0
  127. package/src/dialog/_examples/dialog.confirm.example.ts +145 -0
  128. package/src/dialog/_examples/dialog.example.ts +106 -0
  129. package/src/dialog/_examples/dialog.finance.example.ts +121 -0
  130. package/src/dialog/_examples/dialog.retro.example.ts +126 -0
  131. package/src/dialog/_examples/index.ts +6 -0
  132. package/src/dialog/dialog.ref.ts +42 -0
  133. package/src/dialog/dialog.service.ts +57 -167
  134. package/src/dialog/dialog.spec.ts +35 -120
  135. package/src/dialog/dialog.ts +17 -192
  136. package/src/dialog/index.ts +3 -9
  137. package/src/divider/divider.spec.ts +223 -0
  138. package/src/divider/divider.ts +137 -0
  139. package/src/divider/index.ts +5 -0
  140. package/src/drawer/drawer.ref.ts +63 -0
  141. package/src/drawer/drawer.service.ts +106 -0
  142. package/src/drawer/drawer.spec.ts +117 -0
  143. package/src/drawer/drawer.ts +122 -0
  144. package/src/drawer/index.ts +10 -0
  145. package/src/dropdown-menu/dropdown-menu-content.ts +258 -0
  146. package/src/dropdown-menu/dropdown-menu-group.ts +31 -0
  147. package/src/dropdown-menu/dropdown-menu-item.ts +51 -0
  148. package/src/dropdown-menu/dropdown-menu-label.ts +41 -0
  149. package/src/dropdown-menu/dropdown-menu-separator.ts +27 -0
  150. package/src/dropdown-menu/dropdown-menu-trigger.ts +140 -0
  151. package/src/dropdown-menu/dropdown-menu.spec.ts +265 -0
  152. package/src/dropdown-menu/dropdown-menu.ts +60 -0
  153. package/src/dropdown-menu/index.ts +15 -0
  154. package/src/example-components.ts +39 -30
  155. package/src/field/field-error.ts +49 -0
  156. package/src/field/field-group.ts +39 -0
  157. package/src/field/field-help.ts +47 -0
  158. package/src/field/field-label.ts +30 -0
  159. package/src/field/field.context.ts +28 -0
  160. package/src/field/field.spec.ts +179 -0
  161. package/src/field/field.ts +121 -0
  162. package/src/field/index.ts +6 -0
  163. package/src/file-upload/file-upload.context.ts +89 -0
  164. package/src/file-upload/file-upload.spec.ts +290 -0
  165. package/src/file-upload/file-upload.ts +639 -0
  166. package/src/file-upload/file-upload.types.ts +59 -0
  167. package/src/file-upload/index.ts +23 -0
  168. package/src/form/form-error-summary.ts +87 -0
  169. package/src/form/form-field.context.ts +3 -0
  170. package/src/form/form-field.spec.ts +25 -0
  171. package/src/form/form-field.ts +52 -0
  172. package/src/form/form.context.ts +52 -2
  173. package/src/form/form.spec.ts +155 -18
  174. package/src/form/form.ts +315 -35
  175. package/src/form/index.ts +11 -2
  176. package/src/icon/E2E.md +14 -0
  177. package/src/icon/icon.css +40 -0
  178. package/src/icon/icon.directive.spec.ts +198 -0
  179. package/src/icon/icon.directive.ts +113 -0
  180. package/src/icon/icon.mode.spec.ts +29 -0
  181. package/src/icon/icon.mode.ts +23 -0
  182. package/src/icon/icon.providers.spec.ts +63 -0
  183. package/src/icon/icon.providers.ts +47 -0
  184. package/src/icon/icon.resolver.spec.ts +96 -0
  185. package/src/icon/icon.resolver.ts +49 -0
  186. package/src/icon/icon.tokens.ts +88 -0
  187. package/src/icon/icon.types.ts +48 -0
  188. package/src/icon/index.ts +21 -0
  189. package/src/input/input.ts +12 -3
  190. package/src/input-group/index.ts +3 -0
  191. package/src/input-group/input-group-addon.ts +92 -0
  192. package/src/input-group/input-group.context.ts +27 -0
  193. package/src/input-group/input-group.spec.ts +186 -0
  194. package/src/input-group/input-group.ts +136 -0
  195. package/src/input-mask/index.ts +11 -0
  196. package/src/input-mask/input-mask.compile.ts +69 -0
  197. package/src/input-mask/input-mask.engine.ts +376 -0
  198. package/src/input-mask/input-mask.presets.ts +24 -0
  199. package/src/input-mask/input-mask.spec.ts +304 -0
  200. package/src/input-mask/input-mask.tokens.ts +49 -0
  201. package/src/input-mask/input-mask.ts +280 -0
  202. package/src/input-otp/index.ts +2 -0
  203. package/src/input-otp/input-otp.context.ts +41 -0
  204. package/src/input-otp/input-otp.spec.ts +308 -0
  205. package/src/input-otp/input-otp.ts +470 -0
  206. package/src/kbd/index.ts +1 -0
  207. package/src/kbd/kbd.spec.ts +127 -0
  208. package/src/kbd/kbd.ts +132 -0
  209. package/src/link/config.ts +44 -0
  210. package/src/link/index.ts +2 -0
  211. package/src/link/link.spec.ts +246 -0
  212. package/src/link/link.ts +226 -0
  213. package/src/list/index.ts +11 -0
  214. package/src/list/list.spec.ts +237 -0
  215. package/src/list/list.ts +277 -0
  216. package/src/menubar/index.ts +7 -0
  217. package/src/menubar/menubar-item.ts +204 -0
  218. package/src/menubar/menubar.context.ts +43 -0
  219. package/src/menubar/menubar.spec.ts +433 -0
  220. package/src/menubar/menubar.ts +309 -0
  221. package/src/number-input/index.ts +4 -0
  222. package/src/number-input/number-input-group.ts +64 -0
  223. package/src/number-input/number-input.context.ts +31 -0
  224. package/src/number-input/number-input.format.ts +148 -0
  225. package/src/number-input/number-input.spec.ts +170 -0
  226. package/src/number-input/number-input.ts +457 -0
  227. package/src/number-input/number-stepper.ts +193 -0
  228. package/src/overlay-badge/index.ts +7 -0
  229. package/src/overlay-badge/overlay-badge-content.ts +73 -0
  230. package/src/overlay-badge/overlay-badge.context.ts +37 -0
  231. package/src/overlay-badge/overlay-badge.spec.ts +220 -0
  232. package/src/overlay-badge/overlay-badge.ts +128 -0
  233. package/src/pagination/config.ts +82 -0
  234. package/src/pagination/index.ts +19 -0
  235. package/src/pagination/page-tokens.ts +78 -0
  236. package/src/pagination/pagination-ellipsis.ts +69 -0
  237. package/src/pagination/pagination-first.ts +84 -0
  238. package/src/pagination/pagination-info.ts +70 -0
  239. package/src/pagination/pagination-item.ts +81 -0
  240. package/src/pagination/pagination-last.ts +85 -0
  241. package/src/pagination/pagination-next.ts +86 -0
  242. package/src/pagination/pagination-previous.ts +90 -0
  243. package/src/pagination/pagination.context.ts +52 -0
  244. package/src/pagination/pagination.spec.ts +388 -0
  245. package/src/pagination/pagination.ts +288 -0
  246. package/src/password-input/index.ts +15 -0
  247. package/src/password-input/password-input.context.ts +65 -0
  248. package/src/password-input/password-input.scorer.ts +34 -0
  249. package/src/password-input/password-input.spec.ts +274 -0
  250. package/src/password-input/password-input.ts +488 -0
  251. package/src/popover/_examples/index.ts +5 -0
  252. package/src/popover/_examples/popover.example.ts +92 -0
  253. package/src/popover/_examples/popover.finance.example.ts +106 -0
  254. package/src/popover/_examples/popover.retro.example.ts +103 -0
  255. package/src/popover/index.ts +5 -12
  256. package/src/popover/popover-arrow.ts +30 -0
  257. package/src/popover/popover-close.ts +36 -0
  258. package/src/popover/popover-content.ts +41 -0
  259. package/src/popover/popover-title.ts +39 -0
  260. package/src/popover/popover-trigger.ts +40 -0
  261. package/src/popover/popover.spec.ts +51 -44
  262. package/src/popover/popover.ts +5 -124
  263. package/src/presets/bind-presets.spec.ts +50 -0
  264. package/src/presets/bind-presets.ts +43 -0
  265. package/src/presets/index.ts +3 -0
  266. package/src/presets/size.spec.ts +67 -0
  267. package/src/presets/size.ts +59 -0
  268. package/src/presets/variant.spec.ts +72 -0
  269. package/src/presets/variant.ts +62 -0
  270. package/src/primitives/directionality/directionality.spec.ts +112 -0
  271. package/src/primitives/directionality/directionality.ts +89 -0
  272. package/src/primitives/directionality/index.ts +1 -0
  273. package/src/primitives/forms/form-control.spec.ts +14 -6
  274. package/src/primitives/forms/form-control.ts +6 -2
  275. package/src/primitives/index.ts +3 -0
  276. package/src/primitives/interaction/disabled.ts +12 -5
  277. package/src/primitives/interaction/focus-ring.ts +4 -2
  278. package/src/primitives/list/filterable-list.spec.ts +114 -0
  279. package/src/primitives/list/filterable-list.ts +126 -0
  280. package/src/primitives/list/group.spec.ts +99 -0
  281. package/src/primitives/list/group.ts +139 -0
  282. package/src/primitives/list/index.ts +8 -0
  283. package/src/primitives/list/inject-helpers.ts +8 -0
  284. package/src/primitives/list/item.spec.ts +173 -0
  285. package/src/primitives/list/item.ts +222 -0
  286. package/src/primitives/list/navigator.spec.ts +286 -0
  287. package/src/primitives/list/navigator.ts +238 -0
  288. package/src/primitives/list/selection.spec.ts +214 -0
  289. package/src/primitives/list/selection.ts +339 -0
  290. package/src/primitives/list/tokens.ts +106 -0
  291. package/src/primitives/list/type-ahead.spec.ts +58 -0
  292. package/src/primitives/list/type-ahead.ts +46 -0
  293. package/src/primitives/overlay/backdrop.spec.ts +54 -0
  294. package/src/primitives/overlay/backdrop.ts +31 -0
  295. package/src/primitives/overlay/builder.spec.ts +38 -0
  296. package/src/primitives/overlay/builder.ts +151 -0
  297. package/src/primitives/overlay/container.ts +42 -0
  298. package/src/primitives/overlay/context.ts +14 -0
  299. package/src/primitives/overlay/controller.spec.ts +86 -0
  300. package/src/primitives/overlay/controller.ts +190 -0
  301. package/src/primitives/overlay/id.spec.ts +24 -0
  302. package/src/primitives/overlay/id.ts +17 -0
  303. package/src/primitives/overlay/index.ts +18 -1
  304. package/src/primitives/overlay/overlay.css +42 -0
  305. package/src/primitives/overlay/panel.spec.ts +52 -0
  306. package/src/primitives/overlay/panel.ts +102 -0
  307. package/src/primitives/overlay/stack.spec.ts +55 -0
  308. package/src/primitives/overlay/stack.ts +113 -0
  309. package/src/primitives/overlay/strategies/backdrop/blurred.spec.ts +11 -0
  310. package/src/primitives/overlay/strategies/backdrop/blurred.ts +5 -0
  311. package/src/primitives/overlay/strategies/backdrop/none.spec.ts +10 -0
  312. package/src/primitives/overlay/strategies/backdrop/none.ts +9 -0
  313. package/src/primitives/overlay/strategies/backdrop/solid.spec.ts +18 -0
  314. package/src/primitives/overlay/strategies/backdrop/solid.ts +23 -0
  315. package/src/primitives/overlay/strategies/focus-trap/inert-based.spec.ts +28 -0
  316. package/src/primitives/overlay/strategies/focus-trap/inert-based.ts +38 -0
  317. package/src/primitives/overlay/strategies/focus-trap/no-trap.spec.ts +9 -0
  318. package/src/primitives/overlay/strategies/focus-trap/no-trap.ts +8 -0
  319. package/src/primitives/overlay/strategies/focus-trap/tab-cycle.spec.ts +49 -0
  320. package/src/primitives/overlay/strategies/focus-trap/tab-cycle.ts +85 -0
  321. package/src/primitives/overlay/strategies/index.ts +34 -0
  322. package/src/primitives/overlay/strategies/live-announcer/_announce.ts +35 -0
  323. package/src/primitives/overlay/strategies/live-announcer/assertive.spec.ts +21 -0
  324. package/src/primitives/overlay/strategies/live-announcer/assertive.ts +11 -0
  325. package/src/primitives/overlay/strategies/live-announcer/polite.spec.ts +21 -0
  326. package/src/primitives/overlay/strategies/live-announcer/polite.ts +11 -0
  327. package/src/primitives/overlay/strategies/live-announcer/silent.spec.ts +12 -0
  328. package/src/primitives/overlay/strategies/live-announcer/silent.ts +5 -0
  329. package/src/primitives/overlay/strategies/mount/body-portal.spec.ts +40 -0
  330. package/src/primitives/overlay/strategies/mount/body-portal.ts +91 -0
  331. package/src/primitives/overlay/strategies/mount/in-container.spec.ts +43 -0
  332. package/src/primitives/overlay/strategies/mount/in-container.ts +39 -0
  333. package/src/primitives/overlay/strategies/mount/in-place.spec.ts +40 -0
  334. package/src/primitives/overlay/strategies/mount/in-place.ts +20 -0
  335. package/src/primitives/overlay/strategies/position/anchored-to.spec.ts +56 -0
  336. package/src/primitives/overlay/strategies/position/anchored-to.ts +248 -0
  337. package/src/primitives/overlay/strategies/position/corner.spec.ts +36 -0
  338. package/src/primitives/overlay/strategies/position/corner.ts +33 -0
  339. package/src/primitives/overlay/strategies/position/edge-sheet.spec.ts +32 -0
  340. package/src/primitives/overlay/strategies/position/edge-sheet.ts +36 -0
  341. package/src/primitives/overlay/strategies/position/in-place-sibling.spec.ts +25 -0
  342. package/src/primitives/overlay/strategies/position/in-place-sibling.ts +13 -0
  343. package/src/primitives/overlay/strategies/position/point-at.spec.ts +40 -0
  344. package/src/primitives/overlay/strategies/position/point-at.ts +34 -0
  345. package/src/primitives/overlay/strategies/position/viewport-centered.spec.ts +31 -0
  346. package/src/primitives/overlay/strategies/position/viewport-centered.ts +31 -0
  347. package/src/primitives/overlay/strategies/scroll-lock/css-clip.spec.ts +15 -0
  348. package/src/primitives/overlay/strategies/scroll-lock/css-clip.ts +39 -0
  349. package/src/primitives/overlay/strategies/scroll-lock/html-overflow.spec.ts +21 -0
  350. package/src/primitives/overlay/strategies/scroll-lock/html-overflow.ts +44 -0
  351. package/src/primitives/overlay/strategies/scroll-lock/none.spec.ts +9 -0
  352. package/src/primitives/overlay/strategies/scroll-lock/none.ts +5 -0
  353. package/src/primitives/overlay/strategies/trigger-event/on-click.spec.ts +32 -0
  354. package/src/primitives/overlay/strategies/trigger-event/on-click.ts +28 -0
  355. package/src/primitives/overlay/strategies/trigger-event/on-context-menu.spec.ts +45 -0
  356. package/src/primitives/overlay/strategies/trigger-event/on-context-menu.ts +70 -0
  357. package/src/primitives/overlay/strategies/trigger-event/on-focus-or-input.spec.ts +35 -0
  358. package/src/primitives/overlay/strategies/trigger-event/on-focus-or-input.ts +34 -0
  359. package/src/primitives/overlay/strategies/trigger-event/on-focus.spec.ts +64 -0
  360. package/src/primitives/overlay/strategies/trigger-event/on-focus.ts +37 -0
  361. package/src/primitives/overlay/strategies/trigger-event/on-hotkey.spec.ts +37 -0
  362. package/src/primitives/overlay/strategies/trigger-event/on-hotkey.ts +59 -0
  363. package/src/primitives/overlay/strategies/trigger-event/on-hover.spec.ts +34 -0
  364. package/src/primitives/overlay/strategies/trigger-event/on-hover.ts +86 -0
  365. package/src/primitives/overlay/strategies/trigger-event/programmatic.spec.ts +16 -0
  366. package/src/primitives/overlay/strategies/trigger-event/programmatic.ts +12 -0
  367. package/src/primitives/overlay/tokens.ts +54 -0
  368. package/src/primitives/overlay/trigger.spec.ts +103 -0
  369. package/src/primitives/overlay/trigger.ts +75 -0
  370. package/src/primitives/overlay/types.ts +24 -0
  371. package/src/primitives/overlay/wrapper.ts +50 -0
  372. package/src/primitives/signals/deep-signal.ts +109 -0
  373. package/src/primitives/signals/index.ts +1 -0
  374. package/src/progress-bar/config.ts +48 -0
  375. package/src/progress-bar/index.ts +12 -0
  376. package/src/progress-bar/progress-bar-fill.ts +37 -0
  377. package/src/progress-bar/progress-bar.context.ts +34 -0
  378. package/src/progress-bar/progress-bar.spec.ts +423 -0
  379. package/src/progress-bar/progress-bar.ts +217 -0
  380. package/src/public-api.ts +58 -9
  381. package/src/radio/radio.ts +14 -3
  382. package/src/select/index.ts +4 -2
  383. package/src/select/select-content.ts +69 -0
  384. package/src/select/select-option.ts +35 -0
  385. package/src/select/select-root.ts +129 -0
  386. package/src/select/select-trigger.ts +93 -0
  387. package/src/select/select.spec.ts +133 -140
  388. package/src/select/select.ts +4 -250
  389. package/src/skeleton/index.ts +5 -0
  390. package/src/skeleton/skeleton.spec.ts +115 -0
  391. package/src/skeleton/skeleton.ts +56 -0
  392. package/src/slider/index.ts +10 -0
  393. package/src/slider/slider-range.ts +49 -0
  394. package/src/slider/slider-thumb.ts +335 -0
  395. package/src/slider/slider-track.ts +70 -0
  396. package/src/slider/slider.context.ts +79 -0
  397. package/src/slider/slider.geometry.ts +73 -0
  398. package/src/slider/slider.spec.ts +216 -0
  399. package/src/slider/slider.ts +443 -0
  400. package/src/speed-dial/index.ts +10 -0
  401. package/src/speed-dial/speed-dial-action.ts +40 -0
  402. package/src/speed-dial/speed-dial-actions.ts +31 -0
  403. package/src/speed-dial/speed-dial-trigger.ts +55 -0
  404. package/src/speed-dial/speed-dial.context.ts +36 -0
  405. package/src/speed-dial/speed-dial.spec.ts +143 -0
  406. package/src/speed-dial/speed-dial.ts +127 -0
  407. package/src/spinner/config.ts +48 -0
  408. package/src/spinner/index.ts +8 -0
  409. package/src/spinner/spinner.spec.ts +262 -0
  410. package/src/spinner/spinner.ts +129 -0
  411. package/src/stepper/index.ts +15 -0
  412. package/src/stepper/stepper.context.ts +63 -0
  413. package/src/stepper/stepper.spec.ts +403 -0
  414. package/src/stepper/stepper.ts +544 -0
  415. package/src/styles.css +17 -0
  416. package/src/table/column-helpers.spec.ts +74 -0
  417. package/src/table/column-helpers.ts +68 -0
  418. package/src/table/filter-fns.ts +230 -0
  419. package/src/table/filter-models.spec.ts +240 -0
  420. package/src/table/filter-models.ts +124 -0
  421. package/src/table/filter-params.ts +90 -0
  422. package/src/table/grid-api-impl.ts +321 -0
  423. package/src/table/grid-api.spec.ts +178 -0
  424. package/src/table/grid-api.ts +285 -0
  425. package/src/table/index.ts +73 -1
  426. package/src/table/table-cell.spec.ts +43 -0
  427. package/src/table/table-cell.ts +38 -0
  428. package/src/table/table-filter-outlet.ts +120 -0
  429. package/src/table/table-header.spec.ts +54 -0
  430. package/src/table/table-header.ts +63 -0
  431. package/src/table/table-keyboard.spec.ts +68 -0
  432. package/src/table/table-keyboard.ts +107 -0
  433. package/src/table/table-resource.spec.ts +37 -0
  434. package/src/table/table-resource.ts +36 -0
  435. package/src/table/table-row.spec.ts +54 -0
  436. package/src/table/table-row.ts +31 -0
  437. package/src/table/table-storage.spec.ts +67 -0
  438. package/src/table/table-storage.ts +76 -0
  439. package/src/table/table.spec.ts +102 -43
  440. package/src/table/table.ts +228 -73
  441. package/src/table/table.types.ts +83 -0
  442. package/src/tabs/index.ts +7 -2
  443. package/src/tabs/tabs.context.ts +27 -4
  444. package/src/tabs/tabs.spec.ts +336 -24
  445. package/src/tabs/tabs.ts +313 -79
  446. package/src/tag/config.ts +53 -0
  447. package/src/tag/index.ts +7 -0
  448. package/src/tag/tag-list.ts +76 -0
  449. package/src/tag/tag-remove.ts +61 -0
  450. package/src/tag/tag.context.ts +36 -0
  451. package/src/tag/tag.spec.ts +240 -0
  452. package/src/tag/tag.ts +203 -0
  453. package/src/textarea/config.ts +42 -0
  454. package/src/textarea/index.ts +8 -0
  455. package/src/textarea/textarea.spec.ts +176 -0
  456. package/src/textarea/textarea.ts +235 -0
  457. package/src/time-picker/index.ts +15 -0
  458. package/src/time-picker/time-picker-meridiem.ts +123 -0
  459. package/src/time-picker/time-picker-segment.ts +318 -0
  460. package/src/time-picker/time-picker.context.ts +38 -0
  461. package/src/time-picker/time-picker.format.ts +181 -0
  462. package/src/time-picker/time-picker.spec.ts +189 -0
  463. package/src/time-picker/time-picker.ts +274 -0
  464. package/src/toast/_examples/index.ts +5 -0
  465. package/src/toast/_examples/toast.example.ts +282 -0
  466. package/src/toast/_examples/toast.finance.example.ts +180 -0
  467. package/src/toast/_examples/toast.retro.example.ts +161 -0
  468. package/src/toast/index.ts +8 -1
  469. package/src/toast/toast.ref.ts +26 -0
  470. package/src/toast/toast.service.ts +227 -41
  471. package/src/toast/toast.spec.ts +178 -25
  472. package/src/toast/toast.strategy.ts +10 -0
  473. package/src/toast/toast.ts +178 -22
  474. package/src/toggle/toggle.ts +10 -2
  475. package/src/tooltip/_examples/index.ts +6 -0
  476. package/src/tooltip/_examples/tooltip.example.ts +82 -0
  477. package/src/tooltip/_examples/tooltip.finance.example.ts +97 -0
  478. package/src/tooltip/_examples/tooltip.placements.example.ts +83 -0
  479. package/src/tooltip/_examples/tooltip.retro.example.ts +107 -0
  480. package/src/tooltip/index.ts +4 -5
  481. package/src/tooltip/tooltip-arrow.ts +31 -0
  482. package/src/tooltip/tooltip-content.ts +38 -0
  483. package/src/tooltip/tooltip-group.ts +32 -0
  484. package/src/tooltip/tooltip-trigger.ts +47 -0
  485. package/src/tooltip/tooltip.spec.ts +38 -102
  486. package/src/tooltip/tooltip.ts +5 -129
  487. package/src/tree-select/index.ts +12 -0
  488. package/src/tree-select/tree-select-content.ts +199 -0
  489. package/src/tree-select/tree-select-node.ts +132 -0
  490. package/src/tree-select/tree-select-root.ts +272 -0
  491. package/src/tree-select/tree-select-trigger.ts +90 -0
  492. package/src/tree-select/tree-select.context.ts +63 -0
  493. package/src/tree-select/tree-select.spec.ts +433 -0
  494. package/src/tree-select/tree-select.ts +2 -0
  495. package/src/typography/blockquote.ts +57 -0
  496. package/src/typography/code.ts +53 -0
  497. package/src/typography/index.ts +22 -0
  498. package/src/typography/lead.ts +49 -0
  499. package/src/typography/muted.ts +29 -0
  500. package/src/typography/prose.css +312 -0
  501. package/src/typography/truncate.ts +99 -0
  502. package/src/typography/typography.spec.ts +203 -0
  503. package/tsconfig.lib.json +9 -1
  504. package/tsconfig.lib.prod.json +7 -1
  505. package/tsconfig.spec.json +9 -1
  506. package/vite.config.ts +8 -0
  507. package/src/button/button.example.ts +0 -30
  508. package/src/button/button.finance.example.ts +0 -37
  509. package/src/button/button.retro.example.ts +0 -47
  510. package/src/button/button.sizes.example.ts +0 -25
  511. package/src/dialog/dialog.confirm.example.ts +0 -57
  512. package/src/dialog/dialog.context.ts +0 -11
  513. package/src/dialog/dialog.example.ts +0 -40
  514. package/src/dialog/dialog.finance.example.ts +0 -47
  515. package/src/dialog/dialog.retro.example.ts +0 -46
  516. package/src/menu/index.ts +0 -2
  517. package/src/menu/menu.context.ts +0 -10
  518. package/src/menu/menu.example.ts +0 -39
  519. package/src/menu/menu.spec.ts +0 -168
  520. package/src/menu/menu.ts +0 -176
  521. package/src/popover/popover.context.ts +0 -18
  522. package/src/popover/popover.example.ts +0 -50
  523. package/src/popover/popover.finance.example.ts +0 -52
  524. package/src/popover/popover.retro.example.ts +0 -53
  525. package/src/primitives/overlay/overlay.spec.ts +0 -90
  526. package/src/primitives/overlay/overlay.ts +0 -102
  527. package/src/select/select.context.ts +0 -3
  528. package/src/toast/toast.example.ts +0 -134
  529. package/src/toast/toast.finance.example.ts +0 -68
  530. package/src/toast/toast.retro.example.ts +0 -69
  531. package/src/tooltip/tooltip.example.ts +0 -46
  532. package/src/tooltip/tooltip.finance.example.ts +0 -54
  533. package/src/tooltip/tooltip.placements.example.ts +0 -56
  534. package/src/tooltip/tooltip.retro.example.ts +0 -65
@@ -0,0 +1,222 @@
1
+ import {
2
+ AfterContentInit,
3
+ Directive,
4
+ ElementRef,
5
+ booleanAttribute,
6
+ computed,
7
+ forwardRef,
8
+ inject,
9
+ input,
10
+ output,
11
+ signal,
12
+ } from '@angular/core';
13
+ import { KjListNavigator } from './navigator';
14
+ import { KjSelectionModel } from './selection';
15
+ import { KJ_LIST_FOCUS_MODE, KJ_LIST_NAVIGATOR_CONFIG } from './tokens';
16
+
17
+ let _id = 0;
18
+
19
+ /**
20
+ * Behavior primitive for any item rendered inside a listbox / menu / tree.
21
+ * Composes `KjDisabled`; mints a stable id; exposes value/label/haystacks;
22
+ * binds `aria-disabled`, `aria-selected` (via injected `KjSelectionModel`),
23
+ * `aria-posinset`/`aria-setsize` (stamped by `KjFilterableList`), and
24
+ * `aria-keyshortcuts`. Activation (click / Enter / Space) fires an
25
+ * `activate` output the consumer subscribes to. Role is set by the
26
+ * consumer's outer directive — varies per cluster.
27
+ *
28
+ * @doc-category Core/Primitives
29
+ */
30
+ @Directive({
31
+ selector: '[kjListItem]',
32
+ exportAs: 'kjListItem',
33
+ standalone: true,
34
+ host: {
35
+ '[hidden]': '!visible()',
36
+ '[attr.tabindex]': 'tabIndex()',
37
+ '[attr.aria-disabled]': 'disabled() ? "true" : null',
38
+ '[attr.data-disabled]': 'disabled() ? "" : null',
39
+ '[attr.aria-selected]': 'ariaSelected()',
40
+ '[attr.aria-checked]': 'ariaChecked()',
41
+ '[attr.aria-posinset]': 'posInSet()',
42
+ '[attr.aria-setsize]': 'setSize()',
43
+ '[attr.aria-keyshortcuts]': 'kjShortcut() || null',
44
+ '(click)': '_activate($event)',
45
+ '(keydown.enter)': '$event.preventDefault(); _activate($event)',
46
+ '(keydown.space)': '$event.preventDefault(); _activate($event)',
47
+ },
48
+ })
49
+ export class KjListItem<T = unknown> implements AfterContentInit {
50
+ /** Typed value emitted on activation. `undefined` when unset. */
51
+ readonly kjItemValue = input<T | undefined>(undefined);
52
+ /** Display label. Falls back to element text content when empty. */
53
+ readonly kjItemLabel = input<string>('');
54
+ /** Extra search terms merged into `haystacks` for type-ahead / filter matching. */
55
+ readonly kjItemKeywords = input<readonly string[]>([]);
56
+ /** ARIA keyboard shortcut string bound to `aria-keyshortcuts`. Omit to suppress the attribute. */
57
+ readonly kjShortcut = input<string | null>(null);
58
+ /**
59
+ * Disabled flag. Declared as a top-level input (rather than as a
60
+ * `hostDirective` forward) so consumers that compose `KjListItem`
61
+ * inside their own `hostDirectives` (`KjOption`, `KjTreeSelectNode`,
62
+ * …) can re-forward it without falling foul of NG0311. The composed
63
+ * `KjDisabled` instance follows this signal via an effect below.
64
+ */
65
+ readonly kjDisabled = input<boolean, unknown>(false, { transform: booleanAttribute });
66
+
67
+ /** Fires when the item is activated (click / Enter / Space) and not disabled. Emits `value()`. */
68
+ readonly activate = output<T | undefined>();
69
+
70
+ /**
71
+ * Stable id used by `aria-activedescendant`. Resolved at construction.
72
+ * Priority: existing host `id` attribute (static markup) → generated
73
+ * `kj-list-item-N`. To pin an id, write `<el id="my-id" kjListItem>`.
74
+ */
75
+ readonly id: string;
76
+
77
+ private readonly el = inject(ElementRef<HTMLElement>);
78
+ readonly disabled = this.kjDisabled;
79
+
80
+ /**
81
+ * Nearest ancestor `KjListItem` resolved via the element injector
82
+ * (`skipSelf`, `optional`). Surfaces the DOM-nested parent → child
83
+ * relationship to the selection model, which auto-derives a tree
84
+ * shape from these pointers when the consumer does not supply one
85
+ * via `cfg.treeShape`. Flat consumers (e.g. data-driven tree-select)
86
+ * pass an explicit shape and this pointer is ignored.
87
+ */
88
+ readonly parent = inject<KjListItem<unknown> | null>(KjListItem, {
89
+ skipSelf: true,
90
+ optional: true,
91
+ });
92
+
93
+ // Optional — only present when this item lives inside a
94
+ // `[kjListNavigator]` (the common case). Read by `tabIndex()` so the
95
+ // item can reactively re-bind its host `tabindex` as activeId moves.
96
+ // forwardRef avoids the navigator <-> item circular reference issue.
97
+ private readonly navigator = inject<KjListNavigator | null>(
98
+ forwardRef(() => KjListNavigator),
99
+ { optional: true },
100
+ );
101
+ // Focus mode signal provided by the navigator. Optional: when no
102
+ // navigator hosts this item, fall back to activedescendant behavior
103
+ // (always `-1`) — matches the legacy tabindex output.
104
+ private readonly focusMode = inject(KJ_LIST_FOCUS_MODE, { optional: true });
105
+
106
+ constructor() {
107
+ const host = this.el.nativeElement;
108
+ this.id = host.id || `kj-list-item-${++_id}`;
109
+ host.id = this.id;
110
+ }
111
+
112
+ /** @internal Native host element. Used by `KjListNavigator` to call `.focus()` in roving mode. */
113
+ _host(): HTMLElement {
114
+ return this.el.nativeElement;
115
+ }
116
+
117
+ /**
118
+ * Reactive `tabindex` for the host element:
119
+ * - No navigator / `'activedescendant'`: always `-1` — the navigator
120
+ * host owns the Tab stop and signals activation via
121
+ * `aria-activedescendant`.
122
+ * - `'roving'`: `0` for the currently active (focused) item and `-1`
123
+ * for the rest. Disabled items are never active because
124
+ * `KjListNavigator.navigable` filters them out, so they naturally
125
+ * read `-1` here.
126
+ */
127
+ readonly tabIndex = computed<string>(() => {
128
+ const mode = this.focusMode?.();
129
+ if (mode !== 'roving') return '-1';
130
+ if (this.disabled()) return '-1';
131
+ return this.navigator?.activeId() === this.id ? '0' : '-1';
132
+ });
133
+
134
+ private readonly _elText = signal('');
135
+ readonly value = computed<T | undefined>(() => this.kjItemValue());
136
+ readonly label = computed(() => (this.kjItemLabel() || this._elText()).trim());
137
+ readonly haystacks = computed<readonly string[]>(() => [this.label(), ...this.kjItemKeywords()]);
138
+
139
+ private readonly _visible = signal(true);
140
+ readonly visible = this._visible.asReadonly();
141
+ setVisible(v: boolean): void { this._visible.set(v); }
142
+
143
+ /**
144
+ * Position in the visible (filter-aware) set, bound to `aria-posinset`.
145
+ * Writable for internal use by `KjFilterableList` only; consumers
146
+ * should treat it as read-only.
147
+ */
148
+ readonly posInSet = signal<number | null>(null);
149
+ /**
150
+ * Total size of the visible (filter-aware) set, bound to `aria-setsize`.
151
+ * Writable for internal use by `KjFilterableList` only; consumers
152
+ * should treat it as read-only.
153
+ */
154
+ readonly setSize = signal<number | null>(null);
155
+
156
+ private readonly selection = inject(KjSelectionModel, { optional: true }) as KjSelectionModel<T> | null;
157
+ private readonly cfg = inject(KJ_LIST_NAVIGATOR_CONFIG, { optional: true });
158
+
159
+ /**
160
+ * `aria-selected` driven by the injected selection model. `null` when
161
+ * no model is provided, the value is undefined, or the mode is
162
+ * `'cascade'` (which uses `aria-checked` instead, per WAI-ARIA tree).
163
+ */
164
+ readonly ariaSelected = computed<'true' | 'false' | null>(() => {
165
+ if (!this.selection) return null;
166
+ if (this.selection.mode() === 'cascade') return null;
167
+ const v = this.value();
168
+ if (v === undefined) return null;
169
+ return this.selection.isSelected(v) ? 'true' : 'false';
170
+ });
171
+
172
+ /**
173
+ * `aria-checked` driven by `KjSelectionModel.cascadeState()` when the
174
+ * mode is `'cascade'`. Returns `'true'` / `'false'` / `'mixed'` per
175
+ * the WAI-ARIA tree checkbox tri-state pattern. `null` (attribute
176
+ * omitted) in any other mode.
177
+ */
178
+ readonly ariaChecked = computed<'true' | 'false' | 'mixed' | null>(() => {
179
+ if (!this.selection || this.selection.mode() !== 'cascade') return null;
180
+ const v = this.value();
181
+ if (v === undefined) return null;
182
+ return this.selection.cascadeState(v);
183
+ });
184
+
185
+ ngAfterContentInit(): void {
186
+ this._elText.set(this.el.nativeElement.textContent ?? '');
187
+ }
188
+
189
+ _activate(event?: Event): void {
190
+ if (this.disabled()) return;
191
+ const v = this.value();
192
+ // Branch gate owned by the selection model: in `'single'` mode
193
+ // with a tree shape (consumer-supplied or auto-derived from
194
+ // KjListItem parent DI), non-leaf targets must not commit a value
195
+ // — they exist purely as disclosure controls (e.g. cascade-select
196
+ // branch options that only toggle their own sub-panel). Runs
197
+ // before `stopPropagation` so a blocked click can still reach the
198
+ // consumer's own host listener.
199
+ if (v !== undefined && this.selection && !this.selection.canActivate(v)) return;
200
+ // Stop bubbling so a click on a nested `kjListItem` doesn't also
201
+ // activate the ancestor item (matters for tree-style consumers
202
+ // where leaf nodes are rendered inside their parent branch's host
203
+ // element — without this, the leaf activation would be followed
204
+ // by the branch's `_activate` overriding the selection).
205
+ event?.stopPropagation();
206
+ // Selection toggle + consumer hook live here — not in the option
207
+ // wrapper — because every list-style cluster that wires a
208
+ // `KjSelectionModel` repeats the same two steps after activation:
209
+ // (1) toggle the value, (2) run consumer-specific side-effects
210
+ // (close the overlay, set an input query, emit a commit event).
211
+ // Centralizing both removes the per-option `activate.subscribe(...)`
212
+ // boilerplate. `afterSelect` is invoked even when the toggle was a
213
+ // no-op (e.g. leaf-mode branch click) so consumers can decide what
214
+ // counts as "select" — they receive `closeRequested` to gate on.
215
+ let closeRequested = false;
216
+ if (this.selection && v !== undefined) {
217
+ ({ closeRequested } = this.selection.toggle(v));
218
+ }
219
+ this.cfg?.afterSelect?.(v, closeRequested);
220
+ this.activate.emit(v);
221
+ }
222
+ }
@@ -0,0 +1,286 @@
1
+ // packages/core/src/primitives/list/navigator.spec.ts
2
+ import { Component, Directive, contentChildren, forwardRef, signal } from '@angular/core';
3
+ import { By } from '@angular/platform-browser';
4
+ import { render, type RenderResult } from '@testing-library/angular';
5
+ import { describe, it, expect } from 'vitest';
6
+ import { KjListNavigator } from './navigator';
7
+ import { KjListItem } from './item';
8
+ import { KjTypeAhead } from './type-ahead';
9
+ import { KJ_LIST_NAVIGATOR_CONFIG } from './tokens';
10
+
11
+ /**
12
+ * Minimal test root that registers child KjListItems into
13
+ * KJ_LIST_NAVIGATOR_CONFIG via contentChildren — same pattern as
14
+ * KjSelect / KjCombobox. Used by the focus-mode tests below.
15
+ */
16
+ @Directive({
17
+ selector: '[kjTestListRoot]',
18
+ standalone: true,
19
+ providers: [
20
+ { provide: KJ_LIST_NAVIGATOR_CONFIG, useExisting: forwardRef(() => TestListRoot) },
21
+ ],
22
+ })
23
+ class TestListRoot {
24
+ readonly items = contentChildren(KjListItem, { descendants: true });
25
+ }
26
+
27
+ /** Shape that mirrors the parts of KjListItem the navigator reads. */
28
+ function fakeItem(id: string, label: string, disabled = false) {
29
+ let activated = 0;
30
+ return {
31
+ id,
32
+ label: () => label,
33
+ disabled: () => disabled,
34
+ _activate: () => { activated++; },
35
+ get activated() { return activated; },
36
+ } as unknown as KjListItem<unknown> & { activated: number; _activate: () => void };
37
+ }
38
+
39
+ function setup(items: ReturnType<typeof fakeItem>[], opts: { wrap?: boolean; orientation?: 'vertical' | 'horizontal' | 'both' } = {}) {
40
+ const orientationAttr = opts.orientation ? ` [kjOrientation]="'${opts.orientation}'"` : '';
41
+ const wrapAttr = opts.wrap === false ? ` [kjWrap]="false"` : '';
42
+ return render(
43
+ `<div kjListNavigator${orientationAttr}${wrapAttr}></div>`,
44
+ {
45
+ imports: [KjListNavigator],
46
+ providers: [
47
+ KjTypeAhead,
48
+ {
49
+ provide: KJ_LIST_NAVIGATOR_CONFIG,
50
+ useValue: { items: signal(items) },
51
+ },
52
+ ],
53
+ },
54
+ );
55
+ }
56
+
57
+ /** Dispatch a keydown and flush change detection so host bindings reach the DOM. */
58
+ function press(host: HTMLElement, fixture: RenderResult<unknown>['fixture'], key: string): KeyboardEvent {
59
+ const e = new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true });
60
+ host.dispatchEvent(e);
61
+ fixture.detectChanges();
62
+ return e;
63
+ }
64
+
65
+ describe('KjListNavigator', () => {
66
+ it('starts with no active item', async () => {
67
+ const { container } = await setup([fakeItem('1', 'A'), fakeItem('2', 'B')]);
68
+ const host = container.querySelector('[kjListNavigator]')!;
69
+ expect(host.hasAttribute('aria-activedescendant')).toBe(false);
70
+ });
71
+
72
+ it('ArrowDown moves to next item, wraps past last when kjWrap=true', async () => {
73
+ const items = [fakeItem('1', 'A'), fakeItem('2', 'B')];
74
+ const { container, fixture } = await setup(items);
75
+ const host = container.querySelector('[kjListNavigator]') as HTMLElement;
76
+ press(host, fixture, 'ArrowDown');
77
+ expect(host.getAttribute('aria-activedescendant')).toBe('1');
78
+ press(host, fixture, 'ArrowDown');
79
+ expect(host.getAttribute('aria-activedescendant')).toBe('2');
80
+ press(host, fixture, 'ArrowDown');
81
+ expect(host.getAttribute('aria-activedescendant')).toBe('1');
82
+ });
83
+
84
+ it('ArrowUp wraps to last item from first', async () => {
85
+ const items = [fakeItem('1', 'A'), fakeItem('2', 'B')];
86
+ const { container, fixture } = await setup(items);
87
+ const host = container.querySelector('[kjListNavigator]') as HTMLElement;
88
+ press(host, fixture, 'ArrowUp');
89
+ expect(host.getAttribute('aria-activedescendant')).toBe('2');
90
+ });
91
+
92
+ it('skips disabled items during navigation', async () => {
93
+ const items = [fakeItem('1', 'A'), fakeItem('2', 'B', true), fakeItem('3', 'C')];
94
+ const { container, fixture } = await setup(items);
95
+ const host = container.querySelector('[kjListNavigator]') as HTMLElement;
96
+ press(host, fixture, 'ArrowDown');
97
+ expect(host.getAttribute('aria-activedescendant')).toBe('1');
98
+ press(host, fixture, 'ArrowDown');
99
+ expect(host.getAttribute('aria-activedescendant')).toBe('3');
100
+ });
101
+
102
+ it('Home moves to first, End moves to last', async () => {
103
+ const items = [fakeItem('1', 'A'), fakeItem('2', 'B'), fakeItem('3', 'C')];
104
+ const { container, fixture } = await setup(items);
105
+ const host = container.querySelector('[kjListNavigator]') as HTMLElement;
106
+ press(host, fixture, 'End');
107
+ expect(host.getAttribute('aria-activedescendant')).toBe('3');
108
+ press(host, fixture, 'Home');
109
+ expect(host.getAttribute('aria-activedescendant')).toBe('1');
110
+ });
111
+
112
+ it('Enter calls _activate on the current item and preventDefaults', async () => {
113
+ const items = [fakeItem('1', 'A'), fakeItem('2', 'B')];
114
+ const { container, fixture } = await setup(items);
115
+ const host = container.querySelector('[kjListNavigator]') as HTMLElement;
116
+ press(host, fixture, 'ArrowDown');
117
+ const e = press(host, fixture, 'Enter');
118
+ expect((items[0] as { activated: number }).activated).toBe(1);
119
+ expect(e.defaultPrevented).toBe(true);
120
+ });
121
+
122
+ it('Enter does NOT preventDefault when there is no active item', async () => {
123
+ const items = [fakeItem('1', 'A')];
124
+ const { container, fixture } = await setup(items);
125
+ const host = container.querySelector('[kjListNavigator]') as HTMLElement;
126
+ const e = press(host, fixture, 'Enter');
127
+ expect(e.defaultPrevented).toBe(false);
128
+ expect((items[0] as { activated: number }).activated).toBe(0);
129
+ });
130
+
131
+ it('Space activates current item', async () => {
132
+ const items = [fakeItem('1', 'A')];
133
+ const { container, fixture } = await setup(items);
134
+ const host = container.querySelector('[kjListNavigator]') as HTMLElement;
135
+ press(host, fixture, 'ArrowDown');
136
+ press(host, fixture, ' ');
137
+ expect((items[0] as { activated: number }).activated).toBe(1);
138
+ });
139
+
140
+ it('PageDown moves by kjPageSize (default 10)', async () => {
141
+ const items = Array.from({ length: 15 }, (_, i) => fakeItem(String(i + 1), 'Item ' + (i + 1)));
142
+ const { container, fixture } = await setup(items);
143
+ const host = container.querySelector('[kjListNavigator]') as HTMLElement;
144
+ press(host, fixture, 'ArrowDown');
145
+ press(host, fixture, 'PageDown');
146
+ expect(host.getAttribute('aria-activedescendant')).toBe('11');
147
+ });
148
+
149
+ it('delegates single-char keys to KjTypeAhead and activates the match', async () => {
150
+ const items = [fakeItem('1', 'Apple'), fakeItem('2', 'Banana')];
151
+ const { container, fixture } = await setup(items);
152
+ const host = container.querySelector('[kjListNavigator]') as HTMLElement;
153
+ press(host, fixture, 'b');
154
+ expect(host.getAttribute('aria-activedescendant')).toBe('2');
155
+ });
156
+
157
+ it('kjOrientation="horizontal" responds to ArrowLeft/Right not Up/Down', async () => {
158
+ const items = [fakeItem('1', 'A'), fakeItem('2', 'B')];
159
+ const { container, fixture } = await setup(items, { orientation: 'horizontal' });
160
+ const host = container.querySelector('[kjListNavigator]') as HTMLElement;
161
+ press(host, fixture, 'ArrowDown');
162
+ expect(host.hasAttribute('aria-activedescendant')).toBe(false);
163
+ press(host, fixture, 'ArrowRight');
164
+ expect(host.getAttribute('aria-activedescendant')).toBe('1');
165
+ });
166
+
167
+ it('kjWrap=false clamps at last item instead of wrapping', async () => {
168
+ const items = [fakeItem('1', 'A'), fakeItem('2', 'B')];
169
+ const { container, fixture } = await setup(items, { wrap: false });
170
+ const host = container.querySelector('[kjListNavigator]') as HTMLElement;
171
+ press(host, fixture, 'End');
172
+ press(host, fixture, 'ArrowDown');
173
+ expect(host.getAttribute('aria-activedescendant')).toBe('2');
174
+ });
175
+ });
176
+
177
+ describe('KjListNavigator focus modes', () => {
178
+ it('default activedescendant: all KjListItem tabindex=-1', async () => {
179
+ @Component({
180
+ standalone: true,
181
+ imports: [KjListNavigator, KjListItem, TestListRoot],
182
+ template: `
183
+ <div kjTestListRoot kjListNavigator>
184
+ <div role="option" aria-selected="false" kjListItem id="a" [kjItemValue]="'a'">A</div>
185
+ <div role="option" aria-selected="false" kjListItem id="b" [kjItemValue]="'b'">B</div>
186
+ </div>
187
+ `,
188
+ })
189
+ class Host {}
190
+ const { container } = await render(Host);
191
+ const els = container.querySelectorAll('[kjListItem]');
192
+ els.forEach(el => expect(el.getAttribute('tabindex')).toBe('-1'));
193
+ });
194
+
195
+ it('roving mode: first item is tabindex=0 on first render, others -1', async () => {
196
+ @Component({
197
+ standalone: true,
198
+ imports: [KjListNavigator, KjListItem, TestListRoot],
199
+ template: `
200
+ <div kjTestListRoot kjListNavigator [kjFocusMode]="'roving'">
201
+ <div role="option" aria-selected="false" kjListItem id="a" [kjItemValue]="'a'">A</div>
202
+ <div role="option" aria-selected="false" kjListItem id="b" [kjItemValue]="'b'">B</div>
203
+ <div role="option" aria-selected="false" kjListItem id="c" [kjItemValue]="'c'">C</div>
204
+ </div>
205
+ `,
206
+ })
207
+ class Host {}
208
+ const { container, fixture } = await render(Host);
209
+ fixture.detectChanges();
210
+ const a = container.querySelector('#a')!;
211
+ const b = container.querySelector('#b')!;
212
+ const c = container.querySelector('#c')!;
213
+ expect(a.getAttribute('tabindex')).toBe('0');
214
+ expect(b.getAttribute('tabindex')).toBe('-1');
215
+ expect(c.getAttribute('tabindex')).toBe('-1');
216
+ });
217
+
218
+ it('roving mode: moveBy(1) flips the tabbable item to the next', async () => {
219
+ @Component({
220
+ standalone: true,
221
+ imports: [KjListNavigator, KjListItem, TestListRoot],
222
+ template: `
223
+ <div kjTestListRoot kjListNavigator [kjFocusMode]="'roving'" #nav="kjListNavigator">
224
+ <div role="option" aria-selected="false" kjListItem id="a" [kjItemValue]="'a'">A</div>
225
+ <div role="option" aria-selected="false" kjListItem id="b" [kjItemValue]="'b'">B</div>
226
+ </div>
227
+ `,
228
+ })
229
+ class Host {}
230
+ const { container, fixture } = await render(Host);
231
+ fixture.detectChanges();
232
+ const nav = fixture.debugElement.query(By.directive(KjListNavigator)).injector.get(KjListNavigator);
233
+ nav.moveBy(1);
234
+ fixture.detectChanges();
235
+ const a = container.querySelector('#a')!;
236
+ const b = container.querySelector('#b')!;
237
+ expect(a.getAttribute('tabindex')).toBe('-1');
238
+ expect(b.getAttribute('tabindex')).toBe('0');
239
+ });
240
+
241
+ it('roving mode: disabled items get -1 even when first; first navigable seeded', async () => {
242
+ @Component({
243
+ standalone: true,
244
+ imports: [KjListNavigator, KjListItem, TestListRoot],
245
+ template: `
246
+ <div kjTestListRoot kjListNavigator [kjFocusMode]="'roving'">
247
+ <div role="option" aria-selected="false" kjListItem id="a" [kjDisabled]="true" [kjItemValue]="'a'">A</div>
248
+ <div role="option" aria-selected="false" kjListItem id="b" [kjItemValue]="'b'">B</div>
249
+ <div role="option" aria-selected="false" kjListItem id="c" [kjItemValue]="'c'">C</div>
250
+ </div>
251
+ `,
252
+ })
253
+ class Host {}
254
+ const { container, fixture } = await render(Host);
255
+ fixture.detectChanges();
256
+ const a = container.querySelector('#a')!;
257
+ const b = container.querySelector('#b')!;
258
+ const c = container.querySelector('#c')!;
259
+ // Disabled first item: tabindex=-1, first navigable (b) gets 0.
260
+ expect(a.getAttribute('tabindex')).toBe('-1');
261
+ expect(b.getAttribute('tabindex')).toBe('0');
262
+ expect(c.getAttribute('tabindex')).toBe('-1');
263
+ });
264
+
265
+ it('roving mode: setActive(id) moves focus to that item\'s host element', async () => {
266
+ @Component({
267
+ standalone: true,
268
+ imports: [KjListNavigator, KjListItem, TestListRoot],
269
+ template: `
270
+ <div kjTestListRoot kjListNavigator [kjFocusMode]="'roving'">
271
+ <div role="option" aria-selected="false" kjListItem id="a" [kjItemValue]="'a'">A</div>
272
+ <div role="option" aria-selected="false" kjListItem id="b" [kjItemValue]="'b'">B</div>
273
+ </div>
274
+ `,
275
+ })
276
+ class Host {}
277
+ const { container, fixture } = await render(Host);
278
+ fixture.detectChanges();
279
+ const nav = fixture.debugElement.query(By.directive(KjListNavigator)).injector.get(KjListNavigator);
280
+ const b = container.querySelector('#b') as HTMLElement;
281
+ nav.setActive('b');
282
+ fixture.detectChanges();
283
+ expect(document.activeElement).toBe(b);
284
+ expect(b.getAttribute('tabindex')).toBe('0');
285
+ });
286
+ });