@kouji-ui/core 0.0.4 → 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 +7 -4
  2. package/CHANGELOG.md +169 -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 +11 -3
  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 +101 -42
  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 +166 -14
  472. package/src/toast/toast.strategy.ts +10 -0
  473. package/src/toast/toast.ts +177 -21
  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 -106
  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,179 @@
1
+ import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { describe, expect, test, beforeEach } from 'vitest';
4
+ import { KjAriaDescribedBy } from '../a11y/aria-describedby';
5
+ import { KjField } from './field';
6
+ import { KjFieldError } from './field-error';
7
+ import { KjFieldGroup } from './field-group';
8
+ import { KjFieldHelp } from './field-help';
9
+ import { KjFieldLabel } from './field-label';
10
+
11
+ @Component({
12
+ standalone: true,
13
+ imports: [KjField, KjFieldLabel, KjFieldHelp, KjFieldError, KjFieldGroup, KjAriaDescribedBy],
14
+ changeDetection: ChangeDetectionStrategy.Eager,
15
+ template: `
16
+ <div
17
+ kjField
18
+ #f="kjField"
19
+ [kjRequired]="required()"
20
+ [kjDisabled]="disabled()"
21
+ [kjInvalid]="invalid()"
22
+ >
23
+ <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -->
24
+ <label kjFieldLabel>Email</label>
25
+ <input
26
+ type="email"
27
+ kjAriaDescribedBy
28
+ [id]="f.controlId()"
29
+ [kjDescribedBy]="f.describedByIds()"
30
+ />
31
+ @if (showHelp()) {
32
+ <span kjFieldHelp>Format: name@example.com</span>
33
+ }
34
+ @if (showError()) {
35
+ <span kjFieldError>Enter a valid email.</span>
36
+ }
37
+ </div>
38
+ `,
39
+ })
40
+ class HostComponent {
41
+ required = signal(false);
42
+ disabled = signal(false);
43
+ invalid = signal(false);
44
+ showHelp = signal(true);
45
+ showError = signal(true);
46
+ }
47
+
48
+ describe('KjField', () => {
49
+ beforeEach(() => {
50
+ TestBed.configureTestingModule({ imports: [HostComponent] });
51
+ });
52
+
53
+ test('mints stable ids and wires label[for] to control id', () => {
54
+ const fixture = TestBed.createComponent(HostComponent);
55
+ fixture.detectChanges();
56
+ const label = fixture.nativeElement.querySelector('label') as HTMLLabelElement;
57
+ const input = fixture.nativeElement.querySelector('input') as HTMLInputElement;
58
+ expect(label.getAttribute('for')).toBe(input.id);
59
+ expect(input.id).toMatch(/^kj-field-\d+$/);
60
+ expect(label.id).toMatch(/^kj-field-label-\d+$/);
61
+ });
62
+
63
+ test('appends help id to aria-describedby when not invalid', () => {
64
+ const fixture = TestBed.createComponent(HostComponent);
65
+ fixture.detectChanges();
66
+ const help = fixture.nativeElement.querySelector('[kjFieldHelp]') as HTMLElement;
67
+ const input = fixture.nativeElement.querySelector('input') as HTMLInputElement;
68
+ expect(input.getAttribute('aria-describedby')).toBe(help.id);
69
+ });
70
+
71
+ test('appends both help and error ids when invalid', () => {
72
+ const fixture = TestBed.createComponent(HostComponent);
73
+ fixture.componentInstance.invalid.set(true);
74
+ fixture.detectChanges();
75
+ const help = fixture.nativeElement.querySelector('[kjFieldHelp]') as HTMLElement;
76
+ const error = fixture.nativeElement.querySelector('[kjFieldError]') as HTMLElement;
77
+ const input = fixture.nativeElement.querySelector('input') as HTMLInputElement;
78
+ const ids = input.getAttribute('aria-describedby')!.split(' ');
79
+ expect(ids).toContain(help.id);
80
+ expect(ids).toContain(error.id);
81
+ // hints first, errors second
82
+ expect(ids.indexOf(help.id)).toBeLessThan(ids.indexOf(error.id));
83
+ });
84
+
85
+ test('toggles data-invalid / data-required / data-disabled on the field root', () => {
86
+ const fixture = TestBed.createComponent(HostComponent);
87
+ fixture.componentInstance.invalid.set(true);
88
+ fixture.componentInstance.required.set(true);
89
+ fixture.componentInstance.disabled.set(true);
90
+ fixture.detectChanges();
91
+ const root = fixture.nativeElement.querySelector('[kjField]') as HTMLElement;
92
+ expect(root.hasAttribute('data-invalid')).toBe(true);
93
+ expect(root.hasAttribute('data-required')).toBe(true);
94
+ expect(root.hasAttribute('data-disabled')).toBe(true);
95
+ });
96
+
97
+ test('error element is hidden when not invalid and visible when invalid', () => {
98
+ const fixture = TestBed.createComponent(HostComponent);
99
+ fixture.detectChanges();
100
+ const error = fixture.nativeElement.querySelector('[kjFieldError]') as HTMLElement;
101
+ expect(error.hasAttribute('hidden')).toBe(true);
102
+ fixture.componentInstance.invalid.set(true);
103
+ fixture.detectChanges();
104
+ expect(error.hasAttribute('hidden')).toBe(false);
105
+ });
106
+
107
+ test('help element hides when invalid (error takes precedence)', () => {
108
+ const fixture = TestBed.createComponent(HostComponent);
109
+ fixture.detectChanges();
110
+ const help = fixture.nativeElement.querySelector('[kjFieldHelp]') as HTMLElement;
111
+ expect(help.hasAttribute('hidden')).toBe(false);
112
+ fixture.componentInstance.invalid.set(true);
113
+ fixture.detectChanges();
114
+ expect(help.hasAttribute('hidden')).toBe(true);
115
+ });
116
+
117
+ test('error element carries role=alert and aria-live=polite', () => {
118
+ const fixture = TestBed.createComponent(HostComponent);
119
+ fixture.detectChanges();
120
+ const error = fixture.nativeElement.querySelector('[kjFieldError]') as HTMLElement;
121
+ expect(error.getAttribute('role')).toBe('alert');
122
+ expect(error.getAttribute('aria-live')).toBe('polite');
123
+ });
124
+
125
+ test('label mirrors required / disabled / invalid via data-*', () => {
126
+ const fixture = TestBed.createComponent(HostComponent);
127
+ fixture.componentInstance.required.set(true);
128
+ fixture.componentInstance.disabled.set(true);
129
+ fixture.componentInstance.invalid.set(true);
130
+ fixture.detectChanges();
131
+ const label = fixture.nativeElement.querySelector('label') as HTMLLabelElement;
132
+ expect(label.hasAttribute('data-required')).toBe(true);
133
+ expect(label.hasAttribute('data-disabled')).toBe(true);
134
+ expect(label.hasAttribute('data-invalid')).toBe(true);
135
+ });
136
+
137
+ test('removing a help element deregisters its id from describedby', () => {
138
+ const fixture = TestBed.createComponent(HostComponent);
139
+ fixture.detectChanges();
140
+ const input = fixture.nativeElement.querySelector('input') as HTMLInputElement;
141
+ expect(input.getAttribute('aria-describedby')).toBeTruthy();
142
+ fixture.componentInstance.showHelp.set(false);
143
+ fixture.detectChanges();
144
+ expect(input.getAttribute('aria-describedby')).toBeNull();
145
+ });
146
+
147
+ test('booleanAttribute coerces string "true"/"" / present', () => {
148
+ @Component({
149
+ standalone: true,
150
+ imports: [KjField],
151
+ template: `<div kjField kjRequired kjDisabled kjInvalid #f="kjField"></div>`,
152
+ })
153
+ class A {}
154
+ const fixture = TestBed.createComponent(A);
155
+ fixture.detectChanges();
156
+ const root = fixture.nativeElement.querySelector('[kjField]') as HTMLElement;
157
+ expect(root.hasAttribute('data-required')).toBe(true);
158
+ expect(root.hasAttribute('data-disabled')).toBe(true);
159
+ expect(root.hasAttribute('data-invalid')).toBe(true);
160
+ });
161
+
162
+ test('field-group mirrors invalid / disabled state', () => {
163
+ @Component({
164
+ standalone: true,
165
+ imports: [KjField, KjFieldGroup],
166
+ template: `
167
+ <div kjField [kjInvalid]="true" [kjDisabled]="true">
168
+ <div kjFieldGroup data-test="group"></div>
169
+ </div>
170
+ `,
171
+ })
172
+ class A {}
173
+ const fixture = TestBed.createComponent(A);
174
+ fixture.detectChanges();
175
+ const group = fixture.nativeElement.querySelector('[data-test="group"]') as HTMLElement;
176
+ expect(group.hasAttribute('data-invalid')).toBe(true);
177
+ expect(group.hasAttribute('data-disabled')).toBe(true);
178
+ });
179
+ });
@@ -0,0 +1,121 @@
1
+ import {
2
+ Directive,
3
+ booleanAttribute,
4
+ computed,
5
+ input,
6
+ signal,
7
+ } from '@angular/core';
8
+ import { KJ_FIELD, KjFieldContext } from './field.context';
9
+
10
+ let kjFieldIdCounter = 0;
11
+
12
+ /**
13
+ * Root directive for a form field. Owns id minting, the
14
+ * `aria-describedby` chain, and the required / disabled / invalid state
15
+ * propagation that label, help, error, and the inner control all read.
16
+ *
17
+ * Project a single labelled control plus any number of `[kjFieldLabel]`,
18
+ * `[kjFieldHelp]`, and `[kjFieldError]` children. The label auto-wires
19
+ * `for=`; help and error ids are appended to the inner control's
20
+ * `aria-describedby` via {@link KjAriaDescribedBy}; `aria-invalid` is
21
+ * toggled based on `kjInvalid`.
22
+ *
23
+ * @example
24
+ * ```html
25
+ * <div kjField [kjInvalid]="ctrl.touched && ctrl.invalid" [kjRequired]="true">
26
+ * <label kjFieldLabel>Email</label>
27
+ * <input kjInput type="email" [formControl]="ctrl"
28
+ * kjAriaDescribedBy [kjDescribedBy]="describedByIds()" />
29
+ * <span kjFieldHelp>We'll never share it.</span>
30
+ * <span kjFieldError>Enter a valid email.</span>
31
+ * </div>
32
+ * ```
33
+ * @doc-category Core/Inputs
34
+ * @doc
35
+ * @doc-name field
36
+ * @doc-description Groups a form control with its label, help, and error messages and keeps their accessibility wiring in sync.
37
+ * @doc-is-main
38
+ */
39
+ @Directive({
40
+ selector: '[kjField]',
41
+ standalone: true,
42
+ exportAs: 'kjField',
43
+ providers: [{ provide: KJ_FIELD, useExisting: KjField }],
44
+ host: {
45
+ '[attr.data-orientation]': 'kjFieldOrientation()',
46
+ '[attr.data-required]': 'required() ? "" : null',
47
+ '[attr.data-invalid]': 'invalid() ? "" : null',
48
+ '[attr.data-disabled]': 'disabled() ? "" : null',
49
+ },
50
+ })
51
+ export class KjField implements KjFieldContext {
52
+ private readonly uid = ++kjFieldIdCounter;
53
+
54
+ /** Layout orientation. Reflected as `data-orientation`. */
55
+ readonly kjFieldOrientation = input<'vertical' | 'horizontal'>('vertical');
56
+
57
+ /** Whether the inner control is required. Drives the visual `*` and
58
+ * `aria-required` on the control. */
59
+ readonly kjRequired = input<boolean, unknown>(false, {
60
+ transform: booleanAttribute,
61
+ });
62
+
63
+ /** Whether the field (and its inner control) is disabled. Forwarded to
64
+ * the control via the field context. */
65
+ readonly kjDisabled = input<boolean, unknown>(false, {
66
+ transform: booleanAttribute,
67
+ });
68
+
69
+ /** Whether the inner control is currently in error state. Drives
70
+ * `aria-invalid` on the control and toggles visibility of error
71
+ * children. */
72
+ readonly kjInvalid = input<boolean, unknown>(false, {
73
+ transform: booleanAttribute,
74
+ });
75
+
76
+ /** Override the auto-minted id for the inner control. When unset, the
77
+ * field generates a stable `kj-field-N` id. */
78
+ readonly kjFieldId = input<string | undefined>(undefined);
79
+
80
+ /** Override the auto-minted id for the field's label element. */
81
+ readonly kjFieldLabelId = input<string | undefined>(undefined);
82
+
83
+ /** @internal */ readonly controlId = computed(
84
+ () => this.kjFieldId() ?? `kj-field-${this.uid}`,
85
+ );
86
+ /** @internal */ readonly labelId = computed(
87
+ () => this.kjFieldLabelId() ?? `kj-field-label-${this.uid}`,
88
+ );
89
+ /** @internal */ readonly required = computed(() => this.kjRequired());
90
+ /** @internal */ readonly disabled = computed(() => this.kjDisabled());
91
+ /** @internal */ readonly invalid = computed(() => this.kjInvalid());
92
+
93
+ // Two parallel registrations (help + error) keep the order stable: hints
94
+ // first, errors second. Each registration is an ordered array of ids.
95
+ private readonly helpIds = signal<readonly string[]>([]);
96
+ private readonly errorIds = signal<readonly string[]>([]);
97
+
98
+ /** @internal */ readonly describedByIds = computed<string[]>(() => {
99
+ const help = this.helpIds();
100
+ // Errors only contribute to describedby when the field is invalid —
101
+ // otherwise SR would announce error text on focus when the user hasn't
102
+ // yet failed validation.
103
+ const errors = this.invalid() ? this.errorIds() : [];
104
+ const seen = new Set<string>();
105
+ const out: string[] = [];
106
+ for (const id of [...help, ...errors]) {
107
+ if (id && !seen.has(id)) {
108
+ seen.add(id);
109
+ out.push(id);
110
+ }
111
+ }
112
+ return out;
113
+ });
114
+
115
+ /** @internal */
116
+ registerDescribedBy(id: string, kind: 'help' | 'error'): () => void {
117
+ const target = kind === 'help' ? this.helpIds : this.errorIds;
118
+ target.update((ids) => (ids.includes(id) ? ids : [...ids, id]));
119
+ return () => target.update((ids) => ids.filter((x) => x !== id));
120
+ }
121
+ }
@@ -0,0 +1,6 @@
1
+ export { KjField } from './field';
2
+ export { KjFieldLabel } from './field-label';
3
+ export { KjFieldHelp } from './field-help';
4
+ export { KjFieldError } from './field-error';
5
+ export { KjFieldGroup } from './field-group';
6
+ export { KJ_FIELD, type KjFieldContext } from './field.context';
@@ -0,0 +1,89 @@
1
+ import { InjectionToken, Signal } from '@angular/core';
2
+ import { KjFileStatus, KjUploadableFile } from './file-upload.types';
3
+
4
+ /**
5
+ * Aggregate status across the full file list. Drives `data-status` on the
6
+ * root host so theme CSS can style the wrapper differently while uploads are
7
+ * in flight.
8
+ */
9
+ export type KjFileUploadAggregateStatus =
10
+ | 'idle'
11
+ | 'pending'
12
+ | 'uploading'
13
+ | 'complete'
14
+ | 'partial-error';
15
+
16
+ /**
17
+ * Public context the root `KjFileUpload` exposes via DI. Child directives
18
+ * (`KjFileUploadTrigger`, `KjFileUploadDropzone`, `KjFileUploadList`,
19
+ * `KjFileUploadItem`) inject this token.
20
+ */
21
+ export interface KjFileUploadContext {
22
+ /** Files currently selected (in selection order). Removed entries are dropped. */
23
+ readonly files: Signal<readonly KjUploadableFile[]>;
24
+ /** Whether the root is disabled. Drop-zone, trigger, and per-row controls all gate on this. */
25
+ readonly disabled: Signal<boolean>;
26
+ /** True while at least one file is being dragged over the drop-zone host. */
27
+ readonly dragActive: Signal<boolean>;
28
+ /** Aggregate status for the whole list. */
29
+ readonly aggregateStatus: Signal<KjFileUploadAggregateStatus>;
30
+ /** Whether `kjMultiple` is on. */
31
+ readonly multiple: Signal<boolean>;
32
+ /** Configured `accept` MIME / extension list (or `undefined`). */
33
+ readonly accept: Signal<string | undefined>;
34
+ /** Maximum bytes per file (or `undefined` for no per-file cap). */
35
+ readonly maxSize: Signal<number | undefined>;
36
+ /** Maximum total file count (or `undefined`). */
37
+ readonly maxFiles: Signal<number | undefined>;
38
+
39
+ /** Open the native file picker. No-op when disabled. */
40
+ openPicker(): void;
41
+ /**
42
+ * Add candidate files (from drop, programmatic add, or the trigger's file
43
+ * input change event). Validates against `accept` / `maxSize` / `maxFiles`,
44
+ * mints ids for survivors, appends to the list, and announces.
45
+ */
46
+ addFiles(files: readonly File[] | FileList): void;
47
+ /** Remove a file from the list by id. Aborts an in-flight upload. */
48
+ remove(id: string): void;
49
+ /** Clear all files. */
50
+ clear(): void;
51
+ /** Re-run the upload simulation / handler for a single file. */
52
+ retry(id: string): void;
53
+ /** Cancel an in-flight upload (sets status to `'cancelled'`). */
54
+ cancel(id: string): void;
55
+ /** Update per-file progress and status (called by simulators / examples). */
56
+ setFileStatus(id: string, status: KjFileStatus, progress?: number | null, error?: string): void;
57
+
58
+ /** @internal Used by `KjFileUploadTrigger` to register its hidden file input. */
59
+ registerPickerInput(el: HTMLInputElement): void;
60
+ /** @internal Used by the trigger / drop-zone to forward `change` events. */
61
+ onPickerChange(event: Event): void;
62
+ /** @internal Drag-event handlers delegated from the drop-zone host bindings. */
63
+ onDragEnter(event: Event): void;
64
+ onDragOver(event: Event): void;
65
+ onDragLeave(event: Event): void;
66
+ onDrop(event: Event): void;
67
+ }
68
+
69
+ /** DI token for the root `KjFileUpload`. */
70
+ export const KJ_FILE_UPLOAD = new InjectionToken<KjFileUploadContext>('KjFileUpload');
71
+
72
+ /** Per-row context exposed by `KjFileUploadItem`. */
73
+ export interface KjFileUploadItemContext {
74
+ /** The current `KjUploadableFile` for the row. */
75
+ readonly file: Signal<KjUploadableFile>;
76
+ /** `0..100` or `null`. */
77
+ readonly progress: Signal<number | null>;
78
+ /** Per-row status. */
79
+ readonly status: Signal<KjFileStatus>;
80
+ /** Re-run the upload for this row. */
81
+ retry(): void;
82
+ /** Cancel an in-flight upload for this row. */
83
+ cancel(): void;
84
+ /** Remove this row from the list. */
85
+ remove(): void;
86
+ }
87
+
88
+ /** DI token for an individual file row. */
89
+ export const KJ_FILE_UPLOAD_ITEM = new InjectionToken<KjFileUploadItemContext>('KjFileUploadItem');
@@ -0,0 +1,290 @@
1
+ import { Component, ViewChild, signal, ChangeDetectionStrategy } from '@angular/core';
2
+ import { render, fireEvent } from '@testing-library/angular';
3
+ import { describe, expect, it } from 'vitest';
4
+ import {
5
+ KjFileUpload,
6
+ KjFileUploadDropzone,
7
+ KjFileUploadItem,
8
+ KjFileUploadList,
9
+ KjFileUploadTrigger,
10
+ kjFileMatchesAccept,
11
+ } from './file-upload';
12
+ import type { KjFileRejection } from './file-upload.types';
13
+
14
+ const imports = [
15
+ KjFileUpload,
16
+ KjFileUploadTrigger,
17
+ KjFileUploadDropzone,
18
+ KjFileUploadList,
19
+ KjFileUploadItem,
20
+ ];
21
+
22
+ function makeFile(name: string, size = 100, type = 'text/plain'): File {
23
+ const blob = new Blob(['x'.repeat(size)], { type });
24
+ return new File([blob], name, { type });
25
+ }
26
+
27
+ @Component({
28
+ standalone: true,
29
+ imports,
30
+ changeDetection: ChangeDetectionStrategy.Eager,
31
+ template: `
32
+ <div
33
+ kjFileUpload
34
+ #u="kjFileUpload"
35
+ [kjAccept]="accept()"
36
+ [kjMultiple]="multiple()"
37
+ [kjMaxSize]="maxSize()"
38
+ [kjMaxFiles]="maxFiles()"
39
+ [kjDisabled]="disabled()"
40
+ >
41
+ <button kjFileUploadTrigger>Choose</button>
42
+ <div kjFileUploadDropzone>Drop files here</div>
43
+ <ul kjFileUploadList>
44
+ @for (f of u.files(); track f.id) {
45
+ <li kjFileUploadItem [kjFile]="f" #row="kjFileUploadItem">
46
+ <span class="row-name">{{ f.file.name }}</span>
47
+ <span class="row-status">{{ row.status() }}</span>
48
+ <button class="row-remove" (click)="row.remove()">Remove</button>
49
+ </li>
50
+ }
51
+ </ul>
52
+ </div>
53
+ `,
54
+ })
55
+ class Host {
56
+ readonly accept = signal<string | undefined>(undefined);
57
+ readonly multiple = signal<boolean>(true);
58
+ readonly maxSize = signal<number | undefined>(undefined);
59
+ readonly maxFiles = signal<number | undefined>(undefined);
60
+ readonly disabled = signal<boolean>(false);
61
+ @ViewChild(KjFileUpload, { static: true }) upload!: KjFileUpload;
62
+ }
63
+
64
+ describe('KjFileUpload', () => {
65
+ describe('selection + validation', () => {
66
+ it('mints ids and exposes files via context', async () => {
67
+ const { container, fixture } = await render(Host);
68
+ const root = container.querySelector('[kjFileUpload]')!;
69
+ fixture.componentInstance.upload.addFiles([makeFile('a.txt')]);
70
+ fixture.detectChanges();
71
+ expect(container.querySelectorAll('[kjFileUploadItem]').length).toBe(1);
72
+ expect(container.querySelector('.row-name')?.textContent).toBe('a.txt');
73
+ expect(root.getAttribute('data-status')).toBe('pending');
74
+ });
75
+
76
+ it('rejects files over kjMaxSize', async () => {
77
+ const { container, fixture } = await render(Host);
78
+ fixture.componentInstance.maxSize.set(50);
79
+ fixture.detectChanges();
80
+ const small = makeFile('s.txt', 10);
81
+ const big = makeFile('b.txt', 200);
82
+ fixture.componentInstance.upload.addFiles([small, big]);
83
+ fixture.detectChanges();
84
+ const rows = container.querySelectorAll('[kjFileUploadItem]');
85
+ expect(rows.length).toBe(1);
86
+ expect(container.querySelector('.row-name')?.textContent).toBe('s.txt');
87
+ });
88
+
89
+ it('rejects files outside kjAccept', async () => {
90
+ const { container, fixture } = await render(Host);
91
+ fixture.componentInstance.accept.set('image/*');
92
+ fixture.detectChanges();
93
+ const png = makeFile('p.png', 10, 'image/png');
94
+ const txt = makeFile('t.txt', 10, 'text/plain');
95
+ fixture.componentInstance.upload.addFiles([png, txt]);
96
+ fixture.detectChanges();
97
+ const rows = container.querySelectorAll('[kjFileUploadItem]');
98
+ expect(rows.length).toBe(1);
99
+ expect(container.querySelector('.row-name')?.textContent).toBe('p.png');
100
+ });
101
+
102
+ it('caps at kjMaxFiles when adding in multiple mode', async () => {
103
+ const { container, fixture } = await render(Host);
104
+ fixture.componentInstance.maxFiles.set(2);
105
+ fixture.detectChanges();
106
+ fixture.componentInstance.upload.addFiles([makeFile('a'), makeFile('b'), makeFile('c')]);
107
+ fixture.detectChanges();
108
+ expect(container.querySelectorAll('[kjFileUploadItem]').length).toBe(2);
109
+ });
110
+
111
+ it('replaces existing file in single mode', async () => {
112
+ const { container, fixture } = await render(Host);
113
+ fixture.componentInstance.multiple.set(false);
114
+ fixture.detectChanges();
115
+ fixture.componentInstance.upload.addFiles([makeFile('a.txt')]);
116
+ fixture.detectChanges();
117
+ fixture.componentInstance.upload.addFiles([makeFile('b.txt')]);
118
+ fixture.detectChanges();
119
+ const rows = container.querySelectorAll('[kjFileUploadItem]');
120
+ expect(rows.length).toBe(1);
121
+ expect(container.querySelector('.row-name')?.textContent).toBe('b.txt');
122
+ });
123
+
124
+ it('emits kjReject for size violations', async () => {
125
+ const { fixture } = await render(Host);
126
+ fixture.componentInstance.maxSize.set(50);
127
+ fixture.detectChanges();
128
+ let rejected: KjFileRejection[] | null = null;
129
+ fixture.componentInstance.upload.kjReject.subscribe((r) => (rejected = r));
130
+ fixture.componentInstance.upload.addFiles([makeFile('big', 200)]);
131
+ expect(rejected).toBeTruthy();
132
+ expect(rejected[0].reason).toBe('size');
133
+ });
134
+ });
135
+
136
+ describe('drop-zone', () => {
137
+ it('reflects data-drag-active on dragenter and clears on dragleave', async () => {
138
+ const { container, fixture } = await render(Host);
139
+ const dz = container.querySelector('[kjFileUploadDropzone]') as HTMLElement;
140
+ expect(dz.hasAttribute('data-drag-active')).toBe(false);
141
+
142
+ const ev = new Event('dragenter', { bubbles: true, cancelable: true }) as DragEvent;
143
+ Object.defineProperty(ev, 'dataTransfer', {
144
+ value: { types: ['Files'], files: [] as unknown as FileList },
145
+ });
146
+ dz.dispatchEvent(ev);
147
+ fixture.detectChanges();
148
+ expect(dz.hasAttribute('data-drag-active')).toBe(true);
149
+
150
+ const leave = new Event('dragleave', { bubbles: true, cancelable: true }) as DragEvent;
151
+ Object.defineProperty(leave, 'dataTransfer', {
152
+ value: { types: ['Files'], files: [] as unknown as FileList },
153
+ });
154
+ dz.dispatchEvent(leave);
155
+ fixture.detectChanges();
156
+ expect(dz.hasAttribute('data-drag-active')).toBe(false);
157
+ });
158
+
159
+ it('drop adds files', async () => {
160
+ const { container, fixture } = await render(Host);
161
+ const dz = container.querySelector('[kjFileUploadDropzone]') as HTMLElement;
162
+ const file = makeFile('drop.txt');
163
+ const ev = new Event('drop', { bubbles: true, cancelable: true }) as DragEvent;
164
+ Object.defineProperty(ev, 'dataTransfer', {
165
+ value: { types: ['Files'], files: [file] as unknown as FileList },
166
+ });
167
+ dz.dispatchEvent(ev);
168
+ fixture.detectChanges();
169
+ expect(container.querySelectorAll('[kjFileUploadItem]').length).toBe(1);
170
+ });
171
+
172
+ it('Enter / Space on drop-zone opens picker', async () => {
173
+ const { container } = await render(Host);
174
+ const dz = container.querySelector('[kjFileUploadDropzone]') as HTMLElement;
175
+ const hidden = container.querySelector('input[type=file]') as HTMLInputElement;
176
+ let clicked = 0;
177
+ hidden.addEventListener('click', (e) => {
178
+ e.preventDefault();
179
+ clicked++;
180
+ });
181
+ fireEvent.keyDown(dz, { key: 'Enter' });
182
+ fireEvent.keyDown(dz, { key: ' ' });
183
+ expect(clicked).toBe(2);
184
+ });
185
+
186
+ it('drop-zone has role=button, tabindex=0, and aria-label', async () => {
187
+ const { container } = await render(Host);
188
+ const dz = container.querySelector('[kjFileUploadDropzone]') as HTMLElement;
189
+ expect(dz.getAttribute('role')).toBe('button');
190
+ expect(dz.getAttribute('aria-label')).toBe('Drag files here, or click to browse');
191
+ expect(dz.getAttribute('tabindex')).toBe('0');
192
+ });
193
+ });
194
+
195
+ describe('disabled', () => {
196
+ it('disabled prevents addFiles and reflects aria-disabled on drop-zone', async () => {
197
+ const { container, fixture } = await render(Host);
198
+ fixture.componentInstance.disabled.set(true);
199
+ fixture.detectChanges();
200
+ const dz = container.querySelector('[kjFileUploadDropzone]') as HTMLElement;
201
+ expect(dz.getAttribute('aria-disabled')).toBe('true');
202
+ fixture.componentInstance.upload.addFiles([makeFile('x.txt')]);
203
+ fixture.detectChanges();
204
+ expect(container.querySelectorAll('[kjFileUploadItem]').length).toBe(0);
205
+ });
206
+ });
207
+
208
+ describe('per-row controls', () => {
209
+ it('remove button removes the row', async () => {
210
+ const { container, fixture } = await render(Host);
211
+ fixture.componentInstance.upload.addFiles([makeFile('a'), makeFile('b')]);
212
+ fixture.detectChanges();
213
+ expect(container.querySelectorAll('[kjFileUploadItem]').length).toBe(2);
214
+ const removeBtn = container.querySelector('.row-remove') as HTMLElement;
215
+ fireEvent.click(removeBtn);
216
+ fixture.detectChanges();
217
+ expect(container.querySelectorAll('[kjFileUploadItem]').length).toBe(1);
218
+ });
219
+
220
+ it('retry resets a row to pending', async () => {
221
+ const { fixture } = await render(Host);
222
+ fixture.componentInstance.upload.addFiles([makeFile('a')]);
223
+ const id = fixture.componentInstance.upload.files()[0].id;
224
+ fixture.componentInstance.upload.setFileStatus(id, 'error', null, 'boom');
225
+ expect(fixture.componentInstance.upload.files()[0].status).toBe('error');
226
+ fixture.componentInstance.upload.retry(id);
227
+ expect(fixture.componentInstance.upload.files()[0].status).toBe('pending');
228
+ });
229
+ });
230
+
231
+ describe('aggregateStatus', () => {
232
+ it('idle when empty, pending after add, uploading then complete after status updates', async () => {
233
+ const { container, fixture } = await render(Host);
234
+ const root = container.querySelector('[kjFileUpload]') as HTMLElement;
235
+ expect(root.getAttribute('data-status')).toBe('idle');
236
+ fixture.componentInstance.upload.addFiles([makeFile('a')]);
237
+ fixture.detectChanges();
238
+ expect(root.getAttribute('data-status')).toBe('pending');
239
+ const id = fixture.componentInstance.upload.files()[0].id;
240
+ fixture.componentInstance.upload.setFileStatus(id, 'uploading', 50);
241
+ fixture.detectChanges();
242
+ expect(root.getAttribute('data-status')).toBe('uploading');
243
+ fixture.componentInstance.upload.setFileStatus(id, 'done', 100);
244
+ fixture.detectChanges();
245
+ expect(root.getAttribute('data-status')).toBe('complete');
246
+ });
247
+ });
248
+
249
+ describe('list semantics', () => {
250
+ it('list has role=list and items have role=listitem with data-status', async () => {
251
+ const { container, fixture } = await render(Host);
252
+ const list = container.querySelector('[kjFileUploadList]') as HTMLElement;
253
+ expect(list.getAttribute('role')).toBe('list');
254
+ fixture.componentInstance.upload.addFiles([makeFile('a')]);
255
+ fixture.detectChanges();
256
+ const item = container.querySelector('[kjFileUploadItem]') as HTMLElement;
257
+ expect(item.getAttribute('role')).toBe('listitem');
258
+ expect(item.getAttribute('data-status')).toBe('pending');
259
+ });
260
+ });
261
+ });
262
+
263
+ describe('kjFileMatchesAccept', () => {
264
+ const f = (name: string, type = '') => new File([new Blob([])], name, { type });
265
+
266
+ it('returns true when accept is empty / undefined', () => {
267
+ expect(kjFileMatchesAccept(f('a.txt'), undefined)).toBe(true);
268
+ expect(kjFileMatchesAccept(f('a.txt'), '')).toBe(true);
269
+ });
270
+
271
+ it('matches by exact MIME type', () => {
272
+ expect(kjFileMatchesAccept(f('a.png', 'image/png'), 'image/png')).toBe(true);
273
+ expect(kjFileMatchesAccept(f('a.png', 'image/png'), 'image/jpeg')).toBe(false);
274
+ });
275
+
276
+ it('matches by MIME prefix (image/*)', () => {
277
+ expect(kjFileMatchesAccept(f('a.png', 'image/png'), 'image/*')).toBe(true);
278
+ expect(kjFileMatchesAccept(f('a.txt', 'text/plain'), 'image/*')).toBe(false);
279
+ });
280
+
281
+ it('falls back to extension matching when file.type is empty', () => {
282
+ expect(kjFileMatchesAccept(f('a.heic', ''), '.heic')).toBe(true);
283
+ expect(kjFileMatchesAccept(f('a.heic', ''), 'image/*')).toBe(false);
284
+ });
285
+
286
+ it('matches comma-separated lists', () => {
287
+ expect(kjFileMatchesAccept(f('a.pdf', 'application/pdf'), 'image/*,.pdf')).toBe(true);
288
+ expect(kjFileMatchesAccept(f('a.png', 'image/png'), 'image/*,.pdf')).toBe(true);
289
+ });
290
+ });