@kubex/zinc 1.1.184 → 2.0.0

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 (362) hide show
  1. package/.browserslistrc +2 -0
  2. package/.github/workflows/js_build_and_deploy.yaml +54 -1
  3. package/.github/workflows/test.yaml +27 -0
  4. package/dist/chunks/zn.UWJLIXDT.js +1 -0
  5. package/dist/custom-elements.json +10344 -2583
  6. package/dist/vscode.html-custom-data.json +1102 -125
  7. package/dist/web-types.json +2217 -224
  8. package/dist/zn.d.ts +2306 -417
  9. package/dist/zn.min.css +1 -1
  10. package/dist/zn.min.js +2313 -1635
  11. package/docs/_includes/default.njk +4 -1
  12. package/docs/assets/scripts/docs.js +22 -6
  13. package/docs/assets/styles/docs.css +9 -2
  14. package/docs/data/report-table-day-compare.json +380 -0
  15. package/docs/data/report-table.json +314 -0
  16. package/docs/pages/components/avatar-group.md +57 -0
  17. package/docs/pages/components/badge.md +67 -0
  18. package/docs/pages/components/button.md +41 -1
  19. package/docs/pages/components/chat-message.md +16 -0
  20. package/docs/pages/components/checkbox.md +1 -1
  21. package/docs/pages/components/chip.md +25 -13
  22. package/docs/pages/components/color-picker.md +37 -0
  23. package/docs/pages/components/copy-button.md +2 -2
  24. package/docs/pages/components/data-table-filter.md +33 -50
  25. package/docs/pages/components/data-table-search.md +4 -4
  26. package/docs/pages/components/data-table.md +27 -3
  27. package/docs/pages/components/date-range.md +116 -0
  28. package/docs/pages/components/dialog.md +29 -37
  29. package/docs/pages/components/diff-row.md +28 -0
  30. package/docs/pages/components/diff.md +46 -0
  31. package/docs/pages/components/editor.md +55 -0
  32. package/docs/pages/components/filter-wrapper.md +20 -0
  33. package/docs/pages/components/icon.md +46 -1
  34. package/docs/pages/components/input.md +30 -11
  35. package/docs/pages/components/item.md +35 -0
  36. package/docs/pages/components/key-value.md +25 -0
  37. package/docs/pages/components/menu.md +74 -7
  38. package/docs/pages/components/nav-item.md +15 -0
  39. package/docs/pages/components/nav-list.md +132 -0
  40. package/docs/pages/components/navbar.md +100 -1
  41. package/docs/pages/components/page.md +242 -74
  42. package/docs/pages/components/priority-list.md +43 -5
  43. package/docs/pages/components/query-builder.md +23 -0
  44. package/docs/pages/components/radio-group.md +33 -1
  45. package/docs/pages/components/radio.md +1 -1
  46. package/docs/pages/components/report-table.md +202 -0
  47. package/docs/pages/components/stat.md +30 -0
  48. package/docs/pages/components/tile.md +11 -0
  49. package/docs/pages/components/tooltip.md +1 -1
  50. package/docs/pages/components/translation-grid.md +86 -0
  51. package/docs/pages/components/vertical-stepper.md +108 -64
  52. package/package.json +2 -1
  53. package/scss/_global-spacing.scss +2 -2
  54. package/scss/_root.scss +11 -171
  55. package/scss/boot.scss +14 -0
  56. package/scss/mixins/_helper-mixins.scss +24 -2
  57. package/scss/mixins/_motion-mixins.scss +126 -0
  58. package/scss/mixins/_spacing-mixins.scss +4 -3
  59. package/scss/mixins/_tab-indicator-mixins.scss +12 -0
  60. package/scss/mixins/_typography-mixins.scss +1 -1
  61. package/scss/mixins/index.scss +14 -0
  62. package/scss/shared/_base.scss +32 -1
  63. package/scss/shared/_button-mixin.scss +7 -5
  64. package/scss/shared/_table.scss +35 -4
  65. package/scss/shared/nav.scss +4 -0
  66. package/scss/shared/shimmer.scss +1 -1
  67. package/scss/table-controls.scss +206 -0
  68. package/scss/themes/_dark.scss +1 -3
  69. package/scss/themes/_index.scss +1 -1
  70. package/scss/themes/_light.scss +46 -6
  71. package/scss/themes/_modes.scss +11 -11
  72. package/scss/themes/_openbyte.scss +944 -0
  73. package/scss/themes/_typography.scss +4 -2
  74. package/scss/variables/index.scss +0 -9
  75. package/src/components/alert/alert.component.ts +14 -4
  76. package/src/components/alert/alert.scss +55 -69
  77. package/src/components/alert/alert.test.ts +8 -0
  78. package/src/components/animated-button/animated-button.component.ts +3 -2
  79. package/src/components/animated-button/animated-button.scss +6 -23
  80. package/src/components/animated-button/animated-button.test.ts +4 -3
  81. package/src/components/avatar-group/avatar-group.component.ts +84 -0
  82. package/src/components/avatar-group/avatar-group.scss +54 -0
  83. package/src/components/avatar-group/avatar-group.test.ts +59 -0
  84. package/src/components/avatar-group/index.ts +12 -0
  85. package/src/components/badge/badge.component.ts +49 -0
  86. package/src/components/badge/badge.scss +80 -0
  87. package/src/components/badge/badge.test.ts +34 -0
  88. package/src/components/badge/index.ts +12 -0
  89. package/src/components/bulk-actions/bulk-actions.component.ts +14 -2
  90. package/src/components/button/button.component.ts +242 -11
  91. package/src/components/button/button.scss +133 -25
  92. package/src/components/button/button.test.ts +122 -0
  93. package/src/components/button-menu/button-menu.component.ts +32 -11
  94. package/src/components/button-menu/button-menu.scss +5 -1
  95. package/src/components/channel-tile/channel-tile.component.ts +34 -27
  96. package/src/components/channel-tile/channel-tile.scss +76 -43
  97. package/src/components/chart/builders.test.ts +46 -0
  98. package/src/components/chart/builders.ts +27 -3
  99. package/src/components/chart/chart-live.test.ts +4 -6
  100. package/src/components/chart/chart.component.ts +125 -5
  101. package/src/components/chart/chart.scss +121 -0
  102. package/src/components/chart/chart.test.ts +81 -0
  103. package/src/components/chat-message/chat-message.component.ts +21 -1
  104. package/src/components/chat-message/chat-message.scss +13 -2
  105. package/src/components/chat-message/chat-message.test.ts +9 -0
  106. package/src/components/checkbox/checkbox.component.ts +61 -27
  107. package/src/components/checkbox/checkbox.scss +58 -25
  108. package/src/components/checkbox-group/checkbox-group.component.ts +7 -1
  109. package/src/components/chip/chip.component.ts +11 -1
  110. package/src/components/chip/chip.scss +91 -26
  111. package/src/components/chip/chip.test.ts +17 -0
  112. package/src/components/collapsible/collapsible.component.ts +17 -3
  113. package/src/components/collapsible/collapsible.scss +19 -10
  114. package/src/components/color-picker/color-picker.component.ts +277 -0
  115. package/src/components/color-picker/color-picker.scss +141 -0
  116. package/src/components/color-picker/color-picker.test.ts +57 -0
  117. package/src/components/color-picker/index.ts +12 -0
  118. package/src/components/cols/cols.component.ts +17 -3
  119. package/src/components/cols/cols.scss +24 -13
  120. package/src/components/cols/cols.test.ts +33 -0
  121. package/src/components/confirm/confirm.component.ts +20 -6
  122. package/src/components/confirm/confirm.test.ts +52 -0
  123. package/src/components/content-block/content-block.component.ts +38 -9
  124. package/src/components/content-block/content-block.scss +11 -5
  125. package/src/components/content-block/content-block.test.ts +26 -0
  126. package/src/components/copy-button/copy-button.component.ts +2 -2
  127. package/src/components/copy-button/copy-button.scss +17 -4
  128. package/src/components/data-select/data-select.component.ts +3 -1
  129. package/src/components/data-table/data-table.component.ts +364 -142
  130. package/src/components/data-table/data-table.scss +133 -60
  131. package/src/components/data-table/data-table.test.ts +356 -4
  132. package/src/components/data-table-filter/data-table-filter.component.ts +426 -406
  133. package/src/components/data-table-filter/data-table-filter.scss +176 -148
  134. package/src/components/data-table-filter/data-table-filter.test.ts +271 -273
  135. package/src/components/data-table-search/data-table-search.component.ts +4 -2
  136. package/src/components/data-table-search/data-table-search.scss +8 -1
  137. package/src/components/data-table-search/data-table-search.test.ts +29 -0
  138. package/src/components/date-range/date-range-math.test.ts +78 -0
  139. package/src/components/date-range/date-range-math.ts +105 -0
  140. package/src/components/date-range/date-range.component.ts +375 -0
  141. package/src/components/date-range/date-range.scss +272 -0
  142. package/src/components/date-range/date-range.test.ts +247 -0
  143. package/src/components/date-range/index.ts +12 -0
  144. package/src/components/datepicker/datepicker.component.ts +44 -7
  145. package/src/components/datepicker/datepicker.scss +9 -0
  146. package/src/components/datepicker/datepicker.test.ts +2 -2
  147. package/src/components/dialog/dialog.component.ts +38 -19
  148. package/src/components/dialog/dialog.scss +65 -36
  149. package/src/components/dialog/dialog.test.ts +14 -0
  150. package/src/components/diff/diff.component.ts +55 -0
  151. package/src/components/diff/diff.scss +46 -0
  152. package/src/components/diff/diff.test.ts +15 -0
  153. package/src/components/diff/index.ts +12 -0
  154. package/src/components/diff-row/diff-row.component.ts +75 -0
  155. package/src/components/diff-row/diff-row.scss +70 -0
  156. package/src/components/diff-row/diff-row.test.ts +37 -0
  157. package/src/components/diff-row/index.ts +12 -0
  158. package/src/components/dropdown/dropdown.component.ts +12 -10
  159. package/src/components/dropdown/dropdown.scss +2 -0
  160. package/src/components/editor/editor.component.ts +159 -11
  161. package/src/components/editor/editor.scss +250 -1
  162. package/src/components/editor/editor.test.ts +31 -3
  163. package/src/components/editor/modules/ai/panel/ai-panel.scss +0 -1
  164. package/src/components/editor/modules/attachment/attachment.ts +53 -31
  165. package/src/components/editor/modules/context-menu/context-menu.ts +17 -0
  166. package/src/components/editor/modules/dialog/dialog.component.ts +23 -1
  167. package/src/components/editor/modules/dialog/dialog.test.ts +28 -0
  168. package/src/components/editor/modules/emoji/emoji.ts +4 -5
  169. package/src/components/editor/modules/merge-field/merge-field.ts +83 -0
  170. package/src/components/editor/modules/toolbar/toolbar.component.ts +121 -68
  171. package/src/components/editor/modules/toolbar/toolbar.scss +38 -0
  172. package/src/components/editor/modules/toolbar/toolbar.ts +25 -1
  173. package/src/components/empty-state/empty-state.component.ts +6 -1
  174. package/src/components/empty-state/empty-state.scss +21 -4
  175. package/src/components/expanding-action/expanding-action.component.ts +22 -34
  176. package/src/components/expanding-action/expanding-action.scss +3 -45
  177. package/src/components/expanding-action/expanding-action.test.ts +2 -1
  178. package/src/components/file/file.component.ts +30 -2
  179. package/src/components/file/file.scss +6 -0
  180. package/src/components/filter-wrapper/filter-wrapper.component.ts +45 -10
  181. package/src/components/filter-wrapper/filter-wrapper.test.ts +58 -2
  182. package/src/components/flow-builder/flow-builder.component.ts +36 -3
  183. package/src/components/flow-builder/flow-builder.scss +0 -6
  184. package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +8 -0
  185. package/src/components/flow-builder/modules/flow-canvas/flow-canvas.scss +10 -5
  186. package/src/components/flow-builder/modules/flow-node/flow-node.component.ts +17 -0
  187. package/src/components/flow-builder/modules/flow-node/flow-node.scss +10 -0
  188. package/src/components/form-actions/form-actions.component.ts +48 -2
  189. package/src/components/form-actions/form-actions.scss +1 -1
  190. package/src/components/form-group/form-group.component.ts +10 -3
  191. package/src/components/form-group/form-group.scss +37 -4
  192. package/src/components/form-group/form-group.test.ts +10 -0
  193. package/src/components/header/header.component.ts +11 -4
  194. package/src/components/header/header.scss +47 -5
  195. package/src/components/hover-container/hover-container.component.ts +4 -5
  196. package/src/components/icon/icon.component.ts +95 -9
  197. package/src/components/icon/icon.scss +70 -6
  198. package/src/components/icon/icon.test.ts +55 -0
  199. package/src/components/icon-picker/icon-picker.component.ts +35 -1
  200. package/src/components/inline-edit/inline-edit.component.ts +38 -8
  201. package/src/components/inline-edit/inline-edit.scss +128 -19
  202. package/src/components/input/input.component.ts +173 -297
  203. package/src/components/input/input.scss +65 -43
  204. package/src/components/input/input.test.ts +86 -4
  205. package/src/components/item/item.component.ts +12 -0
  206. package/src/components/item/item.scss +102 -3
  207. package/src/components/item/item.test.ts +23 -0
  208. package/src/components/key/key.component.ts +2 -2
  209. package/src/components/key/key.scss +11 -0
  210. package/src/components/key-value/index.ts +12 -0
  211. package/src/components/key-value/key-value.component.ts +160 -0
  212. package/src/components/key-value/key-value.scss +20 -0
  213. package/src/components/key-value/key-value.test.ts +22 -0
  214. package/src/components/markdown-editor/markdown-editor.component.ts +8 -1
  215. package/src/components/menu/menu.component.ts +21 -2
  216. package/src/components/menu/menu.scss +43 -23
  217. package/src/components/menu/menu.test.ts +33 -0
  218. package/src/components/menu-item/menu-item.component.ts +13 -2
  219. package/src/components/menu-item/menu-item.scss +56 -23
  220. package/src/components/menu-item/menu-item.test.ts +31 -0
  221. package/src/components/menu-item/submenu-controller.ts +25 -15
  222. package/src/components/menu-label/index.ts +12 -0
  223. package/src/components/menu-label/menu-label.component.ts +30 -0
  224. package/src/components/menu-label/menu-label.scss +18 -0
  225. package/src/components/nav-item/index.ts +12 -0
  226. package/src/components/nav-item/nav-item.component.ts +83 -0
  227. package/src/components/nav-item/nav-item.scss +128 -0
  228. package/src/components/nav-list/index.ts +12 -0
  229. package/src/components/nav-list/nav-list.component.ts +313 -0
  230. package/src/components/nav-list/nav-list.scss +87 -0
  231. package/src/components/nav-list/nav-list.test.ts +250 -0
  232. package/src/components/navbar/navbar.component.ts +235 -171
  233. package/src/components/navbar/navbar.scss +333 -169
  234. package/src/components/navbar/navbar.test.ts +63 -143
  235. package/src/components/page/page.component.ts +114 -29
  236. package/src/components/page/page.scss +223 -95
  237. package/src/components/page/page.test.ts +214 -3
  238. package/src/components/page-builder/page-builder.component.ts +51 -5
  239. package/src/components/page-builder/page-builder.scss +6 -1
  240. package/src/components/page-nav/page-nav.component.ts +25 -3
  241. package/src/components/page-nav/page-nav.scss +10 -2
  242. package/src/components/pagination/pagination.scss +1 -1
  243. package/src/components/panel/panel.component.ts +14 -13
  244. package/src/components/panel/panel.scss +14 -10
  245. package/src/components/panel/panel.test.ts +42 -0
  246. package/src/components/popup/popup.component.ts +20 -13
  247. package/src/components/popup/popup.test.ts +21 -1
  248. package/src/components/priority-list/priority-list.component.ts +157 -172
  249. package/src/components/priority-list/priority-list.scss +113 -17
  250. package/src/components/priority-list/priority-list.test.ts +301 -3
  251. package/src/components/progress-bar/progress-bar.component.ts +1 -1
  252. package/src/components/progress-bar/progress-bar.scss +16 -4
  253. package/src/components/progress-tile/progress-tile.scss +1 -1
  254. package/src/components/query-builder/query-builder.component.ts +85 -10
  255. package/src/components/query-builder/query-builder.scss +7 -1
  256. package/src/components/query-builder/query-builder.test.ts +40 -0
  257. package/src/components/radio/radio.component.ts +57 -18
  258. package/src/components/radio/radio.scss +101 -26
  259. package/src/components/radio-group/radio-group.component.ts +45 -7
  260. package/src/components/radio-group/radio-group.scss +17 -0
  261. package/src/components/radio-group/radio-group.test.ts +63 -2
  262. package/src/components/rating/rating.component.ts +23 -0
  263. package/src/components/rating/rating.scss +1 -1
  264. package/src/components/remarkd-editor/remarkd-editor.component.ts +60 -5
  265. package/src/components/remarkd-editor/remarkd-editor.scss +12 -4
  266. package/src/components/report-table/index.ts +12 -0
  267. package/src/components/report-table/report-format.test.ts +86 -0
  268. package/src/components/report-table/report-format.ts +72 -0
  269. package/src/components/report-table/report-math.test.ts +74 -0
  270. package/src/components/report-table/report-math.ts +252 -0
  271. package/src/components/report-table/report-table.component.ts +681 -0
  272. package/src/components/report-table/report-table.scss +328 -0
  273. package/src/components/report-table/report-table.test.ts +702 -0
  274. package/src/components/reveal/reveal.component.ts +15 -1
  275. package/src/components/reveal/reveal.scss +5 -0
  276. package/src/components/schedule-builder/schedule-builder.component.ts +23 -8
  277. package/src/components/scroll-container/scroll-container.component.ts +19 -1
  278. package/src/components/scroll-container/scroll-container.scss +6 -1
  279. package/src/components/select/select.component.ts +52 -36
  280. package/src/components/select/select.scss +42 -25
  281. package/src/components/select/select.test.ts +14 -2
  282. package/src/components/settings-container/settings-container.scss +18 -5
  283. package/src/components/sidebar/sidebar.component.ts +8 -1
  284. package/src/components/sidebar/sidebar.scss +6 -1
  285. package/src/components/skeleton/skeleton.component.ts +3 -3
  286. package/src/components/skeleton/skeleton.scss +6 -6
  287. package/src/components/skeleton/skeleton.test.ts +9 -0
  288. package/src/components/slash-menu/slash-menu.scss +1 -1
  289. package/src/components/slideout/slideout.component.ts +20 -3
  290. package/src/components/slideout/slideout.scss +13 -6
  291. package/src/components/slideout/slideout.test.ts +19 -0
  292. package/src/components/split-button/split-button.component.ts +1 -1
  293. package/src/components/split-pane/split-pane.component.ts +71 -8
  294. package/src/components/split-pane/split-pane.scss +145 -41
  295. package/src/components/split-pane/split-pane.test.ts +23 -3
  296. package/src/components/stat/stat.component.ts +209 -14
  297. package/src/components/stat/stat.scss +305 -5
  298. package/src/components/stat/stat.test.ts +143 -2
  299. package/src/components/status-indicator/status-indicator.scss +22 -21
  300. package/src/components/stepper/stepper.scss +3 -3
  301. package/src/components/tab/tab.component.ts +1 -0
  302. package/src/components/table/table.component.ts +12 -1
  303. package/src/components/table/table.scss +21 -13
  304. package/src/components/table/table.test.ts +17 -0
  305. package/src/components/tabs/tabs.component.ts +150 -37
  306. package/src/components/tabs/tabs.scss +48 -7
  307. package/src/components/tabs/tabs.test.ts +169 -0
  308. package/src/components/textarea/textarea.component.ts +25 -3
  309. package/src/components/textarea/textarea.scss +10 -0
  310. package/src/components/textarea/textarea.test.ts +8 -4
  311. package/src/components/theme-editor/theme-editor.scss +0 -6
  312. package/src/components/tile/tile.component.ts +32 -2
  313. package/src/components/tile/tile.scss +40 -7
  314. package/src/components/tile/tile.test.ts +10 -0
  315. package/src/components/toggle/toggle.component.ts +23 -7
  316. package/src/components/toggle/toggle.scss +36 -29
  317. package/src/components/toggle/toggle.test.ts +17 -0
  318. package/src/components/tooltip/tooltip.component.ts +3 -0
  319. package/src/components/tooltip/tooltip.scss +11 -6
  320. package/src/components/tooltip/tooltip.test.ts +12 -0
  321. package/src/components/translation-field/index.ts +12 -0
  322. package/src/components/translation-field/translation-field.component.ts +55 -0
  323. package/src/components/translation-field/translation-field.scss +101 -0
  324. package/src/components/translation-fields/index.ts +12 -0
  325. package/src/components/translation-fields/translation-fields.component.ts +80 -0
  326. package/src/components/translation-fields/translation-fields.scss +60 -0
  327. package/src/components/translation-grid/index.ts +12 -0
  328. package/src/components/translation-grid/translation-grid.component.ts +157 -0
  329. package/src/components/translation-grid/translation-grid.scss +138 -0
  330. package/src/components/translation-grid/translation-grid.test.ts +28 -0
  331. package/src/components/vertical-stepper/vertical-stepper.component.ts +35 -9
  332. package/src/components/vertical-stepper/vertical-stepper.scss +110 -39
  333. package/src/components/vertical-stepper/vertical-stepper.test.ts +35 -0
  334. package/src/components/well/well.scss +2 -2
  335. package/src/events/events.ts +2 -0
  336. package/src/events/zn-chart-click.ts +13 -0
  337. package/src/events/zn-report-change.ts +7 -0
  338. package/src/form-control.scss +36 -3
  339. package/src/internal/anchored-popover.ts +175 -0
  340. package/src/internal/animate.ts +19 -5
  341. package/src/internal/cosmic-border.ts +15 -0
  342. package/src/internal/form.ts +1 -1
  343. package/src/internal/hover-intent.ts +15 -0
  344. package/src/internal/input-modality.ts +15 -0
  345. package/src/internal/pointer-intent.ts +37 -0
  346. package/src/internal/reorder.ts +625 -0
  347. package/src/internal/scroll.ts +51 -3
  348. package/src/internal/selection-card.ts +1 -1
  349. package/src/internal/tab-indicator.ts +92 -0
  350. package/src/internal/table-sort.ts +25 -0
  351. package/src/internal/table-tabs.ts +108 -0
  352. package/src/internal/test-drag.ts +35 -0
  353. package/src/internal/theme-openbyte.test.ts +126 -0
  354. package/src/internal/theme.ts +4 -4
  355. package/src/internal/zinc-element.ts +2 -2
  356. package/src/selection-card.scss +25 -0
  357. package/src/types/web-test-runner-commands.d.ts +1 -0
  358. package/src/utilities/color.test.ts +43 -0
  359. package/src/utilities/color.ts +189 -0
  360. package/src/zinc.ts +14 -0
  361. package/.github/workflows/eleventy_build.yml +0 -42
  362. package/scss/themes/_aura.scss +0 -114
@@ -1,378 +1,376 @@
1
1
  import '../../../dist/zn.min.js';
2
2
  import {expect, fixture, html, waitUntil} from '@open-wc/testing';
3
3
  import type ZnDataTableFilter from './data-table-filter.component';
4
- import type ZnDatepicker from '../datepicker/datepicker.component';
5
- import type ZnDropdown from '../dropdown/dropdown.component';
6
4
  import type ZnInput from '../input/input.component';
5
+ import type ZnSelect from '../select/select.component';
7
6
 
8
7
  const FILTERS = [
9
8
  {id: 'role', name: 'role', operators: ['in'], options: {pm: 'Project Manager', eng: 'Engineer'}},
10
- {id: 'status', name: 'status', operators: ['eq'], options: {active: 'Active', inactive: 'Inactive'}},
11
- {id: 'email', name: 'email', operators: ['contains']},
12
- {id: 'created', name: 'created', type: 'date', operators: ['eq'], dateSubmitFormat: 'timestamp'},
13
- {id: 'age', name: 'age', type: 'number', operators: ['eq', 'gte']}
9
+ {id: 'status', name: 'Status', operators: ['eq', 'neq'], options: {active: 'Active', inactive: 'Inactive'}},
10
+ {id: 'email', name: 'Email', operators: ['contains']},
11
+ {id: 'created', name: 'Created', type: 'date', operators: ['eq', 'before']},
12
+ {id: 'age', name: 'Age', type: 'number', operators: ['eq', 'gte']},
13
+ {id: 'state', name: 'State', operators: ['eq', 'in'], options: {open: 'Open', closed: 'Closed'}},
14
14
  ];
15
15
 
16
- const clickOn = (el: Element | null | undefined) =>
16
+ const MANY = Array.from({length: 10}, (_, i) => ({id: `f${i}`, name: i === 4 ? 'Mac Hotspot Shield' : `Field ${i}`, operators: ['eq']}));
17
+
18
+ const click = (el: Element | null | undefined) =>
17
19
  el?.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true, cancelable: true}));
18
20
 
19
- const decode = (value: string) => JSON.parse(atob(value)) as { key: string; comparator: string; value: string }[];
21
+ const choose = (control: Element, value: string | string[]) => {
22
+ (control as HTMLElement & {value: string | string[]}).value = value;
23
+ control.dispatchEvent(new CustomEvent('zn-change', {bubbles: true, composed: true}));
24
+ };
25
+
26
+ const decode = (value: string) => JSON.parse(atob(value)) as {key: string; comparator: string; value: string}[];
27
+ const encode = (rules: {key: string; comparator: string; value: string}[]) => btoa(JSON.stringify(rules));
20
28
 
21
- async function filterBar(defaultFilters = '') {
29
+ async function filter(attrs: {defaultFilters?: string; value?: string; filters?: unknown[]} = {}) {
22
30
  const el = await fixture<ZnDataTableFilter>(html`
23
- <zn-data-table-filter default-filters="${defaultFilters}" .filters="${FILTERS}"></zn-data-table-filter>`);
31
+ <zn-data-table-filter default-filters="${attrs.defaultFilters ?? ''}" .filters="${attrs.filters ?? FILTERS}"
32
+ .value="${attrs.value ?? ''}"></zn-data-table-filter>`);
24
33
  await el.updateComplete;
25
34
  return el;
26
35
  }
27
36
 
28
- const pills = (el: ZnDataTableFilter) =>
29
- [...el.shadowRoot!.querySelectorAll('.filter-bar__pill')].map(p => p.textContent!.trim());
37
+ const root = (el: ZnDataTableFilter) => el.shadowRoot!;
38
+ const rules = (el: ZnDataTableFilter) => [...root(el).querySelectorAll('.filter__rule')];
39
+ const control = <T extends Element>(el: ZnDataTableFilter, label: string, index = 0) =>
40
+ rules(el)[index].querySelector<T>(`[aria-label="${label}"]`)!;
41
+ const chips = (el: ZnDataTableFilter) =>
42
+ [...root(el).querySelectorAll('.filter__chip__text')].map(chip => chip.textContent!.trim());
30
43
 
31
- describe('<zn-data-table-filter>', () => {
32
- it('should render a component', async () => {
33
- const el = await fixture(html`
34
- <zn-data-table-filter></zn-data-table-filter>`);
44
+ async function open(el: ZnDataTableFilter) {
45
+ click(root(el).querySelector('.filter__toggle'));
46
+ await el.updateComplete;
47
+ }
35
48
 
36
- expect(el).to.exist;
37
- });
49
+ async function apply(el: ZnDataTableFilter) {
50
+ click(root(el).querySelector('.filter__pop__foot zn-button[color="primary"]'));
51
+ await el.updateComplete;
52
+ }
38
53
 
39
- it('renders a pill for each default filter', async () => {
40
- const el = await filterBar('role');
54
+ async function setRule(el: ZnDataTableFilter, key: string, value?: string | string[], index = 0) {
55
+ choose(control(el, 'Filter field', index), key);
56
+ await el.updateComplete;
57
+ if (value === undefined) return;
58
+ await waitUntil(() => control(el, 'Filter value', index));
59
+ choose(control(el, 'Filter value', index), value);
60
+ await el.updateComplete;
61
+ }
41
62
 
42
- expect(pills(el)).to.deep.equal(['Role']);
63
+ describe('<zn-data-table-filter>', () => {
64
+ it('opens the filter popover from the funnel with one empty rule', async () => {
65
+ const el = await filter();
66
+ const toggle = root(el).querySelector('.filter__toggle')!;
67
+ expect(root(el).querySelector('.filter__pop')).to.not.exist;
68
+ expect(toggle.getAttribute('aria-expanded')).to.equal('false');
69
+ expect(toggle.classList.contains('filter__toggle--open')).to.be.false;
70
+
71
+ await open(el);
72
+ expect(toggle.getAttribute('aria-expanded')).to.equal('true');
73
+ expect(toggle.classList.contains('filter__toggle--open')).to.be.true;
74
+
75
+ const pop = root(el).querySelector('.filter__pop')!;
76
+ expect(pop).to.exist;
77
+ const head = () => pop.querySelector('.filter__pop__head')!.textContent!.replace(/\s+/g, ' ').trim();
78
+ expect(head()).to.equal('Filters Choose a value for the empty filter to apply it');
79
+ expect(rules(el).length).to.equal(1);
80
+ expect(control<ZnSelect>(el, 'Filter field').value).to.equal('');
81
+
82
+ await setRule(el, 'status', 'active');
83
+ expect(head()).to.equal('Filters Show records that match all of these');
43
84
  });
44
85
 
45
- it('offers the filters that are not already active', async () => {
46
- const el = await filterBar('role');
86
+ it('encodes field, operator and value exactly as the table builder did', async () => {
87
+ const el = await filter();
88
+ await open(el);
47
89
 
48
- const options = [...el.shadowRoot!.querySelectorAll('.filter-bar__add + zn-menu zn-menu-item, zn-menu zn-menu-item')]
49
- .map(i => i.getAttribute('value'));
90
+ await setRule(el, 'status', 'active');
91
+ await apply(el);
50
92
 
51
- expect(options).to.include('status');
52
- expect(options).to.include('email');
93
+ expect(el.value).to.equal('W3sia2V5Ijoic3RhdHVzIiwiY29tcGFyYXRvciI6ImVxIiwidmFsdWUiOiJhY3RpdmUifV0=');
94
+ expect(el.activeCount).to.equal(1);
53
95
  });
54
96
 
55
- it('encodes a selected value as the query-builder payload', async () => {
56
- const el = await filterBar('status');
57
-
58
- clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="active"]'));
97
+ it('offers the field\'s operators and resets operator and value when the field changes', async () => {
98
+ const el = await filter();
99
+ await open(el);
100
+ await setRule(el, 'age', '18');
101
+ choose(control(el, 'Filter operator'), 'gte');
59
102
  await el.updateComplete;
103
+ await apply(el);
104
+ expect(decode(el.value)).to.deep.equal([{key: 'age', comparator: 'gte', value: '18'}]);
60
105
 
61
- expect(decode(el.value)).to.deep.equal([{key: 'status', comparator: 'eq', value: 'active'}]);
62
- });
63
-
64
- it('accumulates values for an `in` filter and counts them on the pill', async () => {
65
- const el = await filterBar('role');
66
-
67
- clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="pm"]'));
68
- await el.updateComplete;
69
- clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="eng"]'));
70
- await el.updateComplete;
106
+ await open(el);
107
+ const operators = [...control(el, 'Filter operator').querySelectorAll('zn-option')].map(o => o.textContent!.trim());
108
+ expect(operators).to.deep.equal(['is', 'is at least']);
71
109
 
72
- expect(decode(el.value)[0].value).to.equal('pm eng');
73
- expect(pills(el)).to.deep.equal(['Role (2)']);
110
+ await setRule(el, 'email');
111
+ expect(control<ZnSelect>(el, 'Filter operator').value).to.equal('contains');
112
+ expect(control<ZnInput>(el, 'Filter value').value).to.equal('');
113
+ await apply(el);
114
+ expect(el.value).to.equal('');
74
115
  });
75
116
 
76
- it('unsets a single-value filter when its selected option is picked again', async () => {
77
- const el = await filterBar('status');
117
+ it('narrows a long field list with its search', async () => {
118
+ const el = await filter({filters: MANY});
119
+ await open(el);
120
+ const select = control<ZnSelect>(el, 'Filter field');
121
+ expect(select.hasAttribute('search')).to.be.true;
78
122
 
79
- clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="active"]'));
80
- await el.updateComplete;
81
- clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="active"]'));
123
+ expect(select.placeholder).to.equal('Please select...');
124
+ await select.show();
82
125
  await el.updateComplete;
126
+ expect(select.placeholder).to.equal('Search 10 fields...');
83
127
 
84
- expect(el.value).to.equal('');
128
+ await select.updateComplete;
129
+ const input = select.shadowRoot!.querySelector<HTMLInputElement>('.select__display-input')!;
130
+ input.value = 'HOTSPOT';
131
+ input.dispatchEvent(new Event('input', {bubbles: true, composed: true}));
132
+ await select.updateComplete;
133
+
134
+ const visible = [...select.querySelectorAll<HTMLElement>('zn-option')].filter(o => !o.hidden).map(o => o.textContent!.trim());
135
+ expect(visible).to.deep.equal(['Mac Hotspot Shield']);
85
136
  });
86
137
 
87
- it('leaves valueless filters out of the payload', async () => {
88
- const el = await filterBar('role,status');
138
+ it('joins an `in` multi-select\'s values with spaces', async () => {
139
+ const el = await filter();
140
+ await open(el);
141
+ choose(control(el, 'Filter field'), 'role');
142
+ await el.updateComplete;
89
143
 
90
- clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="active"]'));
144
+ expect(control(el, 'Filter value').hasAttribute('multiple')).to.be.true;
145
+ choose(control(el, 'Filter value'), ['pm', 'eng']);
91
146
  await el.updateComplete;
147
+ await apply(el);
92
148
 
93
- expect(decode(el.value)).to.deep.equal([{key: 'status', comparator: 'eq', value: 'active'}]);
149
+ expect(decode(el.value)).to.deep.equal([{key: 'role', comparator: 'in', value: 'pm eng'}]);
150
+ expect(chips(el)).to.deep.equal(['Role is any of Project Manager, Engineer']);
94
151
  });
95
152
 
96
- it('emits zn-filter-change when a value changes', async () => {
97
- const el = await filterBar('status');
98
- let emitted = 0;
99
- el.addEventListener('zn-filter-change', () => emitted++);
153
+ it('keeps the first value when an `in` rule moves to a single-value operator', async () => {
154
+ const el = await filter();
155
+ await open(el);
156
+ await setRule(el, 'state');
157
+ choose(control(el, 'Filter operator'), 'in');
158
+ await el.updateComplete;
159
+ choose(control(el, 'Filter value'), ['open', 'closed']);
160
+ await el.updateComplete;
100
161
 
101
- clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="active"]'));
102
- await waitUntil(() => emitted > 0);
162
+ choose(control(el, 'Filter operator'), 'eq');
163
+ await el.updateComplete;
164
+ await apply(el);
103
165
 
104
- expect(emitted).to.equal(1);
166
+ expect(decode(el.value)).to.deep.equal([{key: 'state', comparator: 'eq', value: 'open'}]);
105
167
  });
106
168
 
107
- it('keeps a multi-value filter open on select and closes a single-value one', async () => {
108
- const el = await filterBar('role,status');
109
- const dropdowns = [...el.shadowRoot!.querySelectorAll<ZnDropdown>('zn-dropdown')];
110
-
111
- const multi = dropdowns.find(d => d.querySelector('zn-menu-item[value="pm"]'))!;
112
- const single = dropdowns.find(d => d.querySelector('zn-menu-item[value="active"]'))!;
169
+ it('renders a number input for a number field', async () => {
170
+ const el = await filter();
171
+ await open(el);
172
+ await setRule(el, 'age');
113
173
 
114
- expect(multi.stayOpenOnSelect).to.be.true;
115
- expect(single.stayOpenOnSelect).to.be.false;
174
+ expect(control(el, 'Filter value').getAttribute('type')).to.equal('number');
116
175
  });
117
176
 
118
- it('counts a typed value as one, however many spaces it contains', async () => {
119
- const el = await filterBar('email');
177
+ it('keeps the legacy date encoding and re-encodes on an operator change', async () => {
178
+ const el = await filter();
179
+ await open(el);
180
+ await setRule(el, 'created');
181
+ expect(control(el, 'Filter value').getAttribute('type')).to.equal('date');
120
182
 
121
- const input = el.shadowRoot!.querySelector<ZnInput>('zn-input')!;
122
- input.value = 'thats really bad';
123
- input.dispatchEvent(new Event('zn-input', {bubbles: true, composed: true}));
124
- await waitUntil(() => el.value.length > 0);
183
+ choose(control(el, 'Filter value'), '2026-10-20');
125
184
  await el.updateComplete;
185
+ await apply(el);
186
+ const timestamp = new Date(2026, 9, 20).getTime();
187
+ expect(decode(el.value)).to.deep.equal([{key: 'created', comparator: 'eq', value: (timestamp / 1000).toString()}]);
188
+ expect(chips(el)).to.deep.equal(['Created is 2026-10-20']);
126
189
 
127
- expect(pills(el)).to.deep.equal(['Email: thats really bad']);
190
+ await open(el);
191
+ choose(control(el, 'Filter operator'), 'before');
192
+ await el.updateComplete;
193
+ await apply(el);
194
+ const [rule] = decode(el.value);
195
+ expect(rule.comparator).to.equal('before');
196
+ expect(Number(rule.value)).to.be.closeTo(-Math.floor((Date.now() - timestamp) / 60_000), 1);
128
197
  });
129
198
 
130
- it('matches suggestions without regard to case', async () => {
131
- const el = await filterBar('email');
132
- el.suggestions = {email: ['Ada@example.com', 'grace@example.com']};
133
-
134
- const input = el.shadowRoot!.querySelector<ZnInput>('zn-input')!;
135
- input.value = 'ADA';
136
- input.dispatchEvent(new Event('zn-input', {bubbles: true, composed: true}));
137
- await waitUntil(() => el.shadowRoot!.querySelectorAll('.filter-bar__suggestion').length > 0);
138
-
139
- const shown = [...el.shadowRoot!.querySelectorAll('.filter-bar__suggestion')].map(b => b.textContent!.trim());
199
+ it('shows an empty row for each default filter', async () => {
200
+ const el = await filter({defaultFilters: 'email'});
201
+ await open(el);
140
202
 
141
- expect(shown).to.deep.equal(['Ada@example.com']);
203
+ expect(rules(el).length).to.equal(1);
204
+ expect(control<ZnSelect>(el, 'Filter field').value).to.equal('email');
205
+ expect(control<ZnInput>(el, 'Filter value').value).to.equal('');
206
+ expect(el.value).to.equal('');
142
207
  });
143
208
 
144
- it('renders a text input for a filter with no options', async () => {
145
- const el = await filterBar('email');
209
+ it('shows chips for a value it is given', async () => {
210
+ const el = await filter({
211
+ value: encode([
212
+ {key: 'status', comparator: 'eq', value: 'active'},
213
+ {key: 'email', comparator: 'contains', value: 'acme'},
214
+ ]),
215
+ });
146
216
 
147
- expect(el.shadowRoot!.querySelector('.filter-bar__editor zn-input')).to.exist;
217
+ expect(chips(el)).to.deep.equal(['Status is Active', 'Email contains acme']);
218
+ expect(el.activeCount).to.equal(2);
219
+ expect(root(el).querySelector('.filter__toggle')!.getAttribute('notification')).to.equal('2');
148
220
  });
149
221
 
150
- it('renders an inline calendar for a date filter', async () => {
151
- const el = await filterBar('created');
152
-
153
- const picker = el.shadowRoot!.querySelector('.filter-bar__editor zn-datepicker');
222
+ it('keeps a single option value with spaces whole', async () => {
223
+ const el = await filter({
224
+ filters: [{id: 'stage', name: 'Stage', operators: ['eq', 'in'], options: {'in progress': 'In progress', done: 'Done'}}],
225
+ value: encode([{key: 'stage', comparator: 'eq', value: 'in progress'}]),
226
+ });
154
227
 
155
- expect(picker).to.exist;
156
- expect(picker!.hasAttribute('inline')).to.be.true;
228
+ expect(chips(el)).to.deep.equal(['Stage is In progress']);
157
229
  });
158
230
 
159
- it('encodes a picked date and labels the pill with it', async () => {
160
- const el = await filterBar('created');
161
- const picker = el.shadowRoot!.querySelector<ZnDatepicker>('zn-datepicker')!;
162
- await picker.updateComplete;
231
+ it('rebuilds its rules when the value is changed from outside', async () => {
232
+ const el = await filter({defaultFilters: 'role'});
163
233
 
164
- picker.shadowRoot!.querySelector<HTMLElement>('.air-datepicker-cell.-day-:not(.-other-month-)')!.click();
165
- // AirDatepicker reports a selection on the next task, not synchronously
166
- await waitUntil(() => el.value.length > 0);
234
+ el.value = encode([{key: 'status', comparator: 'eq', value: 'active'}]);
167
235
  await el.updateComplete;
236
+ expect(chips(el)).to.deep.equal(['Status is Active']);
168
237
 
169
- const [filter] = decode(el.value);
170
-
171
- expect(filter.key).to.equal('created');
172
- expect(filter.comparator).to.equal('eq');
173
- expect(Number(filter.value)).to.be.greaterThan(0);
174
- expect(pills(el)[0]).to.equal(`Created: ${picker.value as string}`);
175
- });
176
-
177
- it('offers each operator when a filter defines more than one', async () => {
178
- const el = await filterBar('age');
179
-
180
- const operators = [...el.shadowRoot!.querySelectorAll('.filter-bar__operator')].map(o => o.textContent!.trim());
181
-
182
- expect(operators).to.deep.equal(['is', '\u2265']);
238
+ await open(el);
239
+ expect(rules(el).length).to.equal(1);
240
+ expect(control<ZnSelect>(el, 'Filter field').value).to.equal('status');
183
241
  });
184
242
 
185
- it('re-encodes the value with the operator the user picks', async () => {
186
- const el = await filterBar('age');
243
+ it('emits zn-filter-change only when Apply changes the encoded value', async () => {
244
+ const el = await filter();
245
+ let emitted = 0;
246
+ el.addEventListener('zn-filter-change', () => emitted++);
247
+ await open(el);
187
248
 
188
- const input = el.shadowRoot!.querySelector<ZnInput>('zn-input')!;
189
- input.value = '18';
190
- input.dispatchEvent(new Event('zn-input', {bubbles: true, composed: true}));
191
- await waitUntil(() => el.value.length > 0);
249
+ await setRule(el, 'status', 'active');
250
+ expect(emitted).to.equal(0);
251
+ expect(el.value).to.equal('');
252
+ expect(chips(el)).to.deep.equal([]);
192
253
 
193
- clickOn(el.shadowRoot!.querySelectorAll('.filter-bar__operator')[1]);
194
- await el.updateComplete;
254
+ await apply(el);
255
+ expect(emitted).to.equal(1);
256
+ expect(root(el).querySelector('.filter__pop')).to.not.exist;
195
257
 
196
- expect(decode(el.value)).to.deep.equal([{key: 'age', comparator: 'gte', value: '18'}]);
197
- expect(pills(el)).to.deep.equal(['Age \u2265 18']);
258
+ await open(el);
259
+ await apply(el);
260
+ expect(emitted).to.equal(1);
198
261
  });
199
262
 
200
- it('lists operators above the options for a filter that has both', async () => {
201
- const el = await fixture<ZnDataTableFilter>(html`
202
- <zn-data-table-filter default-filters="state"
203
- .filters="${[{
204
- id: 'state',
205
- name: 'state',
206
- operators: ['eq', 'in'],
207
- options: {open: 'Open', closed: 'Closed'}
208
- }]}"></zn-data-table-filter>`);
263
+ it('closes on Escape and discards unapplied rules', async () => {
264
+ const el = await filter();
265
+ await open(el);
266
+ await setRule(el, 'status', 'active');
267
+ click(root(el).querySelector('.filter__pop__foot zn-button[icon="plus@lu"]'));
209
268
  await el.updateComplete;
269
+ expect(rules(el).length).to.equal(2);
210
270
 
211
- const items = [...el.shadowRoot!.querySelectorAll('zn-menu-item')].map(i => i.textContent!.trim());
212
-
213
- expect(items).to.deep.equal(['is', 'is any of', 'Open', 'Closed']);
214
- });
215
-
216
- it('opens the new pill so its value can be set without a second click', async () => {
217
- const el = await filterBar();
218
-
219
- clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="status"]'));
271
+ document.dispatchEvent(new KeyboardEvent('keydown', {key: 'Escape', bubbles: true}));
220
272
  await el.updateComplete;
273
+ expect(root(el).querySelector('.filter__pop')).to.not.exist;
274
+ expect(root(el).querySelector('.filter__toggle')!.classList.contains('filter__toggle--open')).to.be.false;
221
275
 
222
- const pill = [...el.shadowRoot!.querySelectorAll<ZnDropdown>('zn-dropdown')]
223
- .find(dropdown => dropdown.dataset.key === 'status')!;
224
-
225
- await waitUntil(() => pill.open, 'the new filter\'s panel never opened');
226
- expect(pill.open).to.be.true;
276
+ expect(el.value).to.equal('');
277
+ await open(el);
278
+ expect(rules(el).length).to.equal(1);
279
+ expect(control<ZnSelect>(el, 'Filter field').value).to.equal('');
227
280
  });
228
281
 
229
- it('keeps the panel open for an operator but closes it for a value', async () => {
230
- const el = await fixture<ZnDataTableFilter>(html`
231
- <zn-data-table-filter default-filters="state"
232
- .filters="${[{
233
- id: 'state',
234
- name: 'state',
235
- operators: ['eq', 'in'],
236
- options: {open: 'Open', closed: 'Closed'}
237
- }]}"></zn-data-table-filter>`);
238
- await el.updateComplete;
239
-
240
- const pill = el.shadowRoot!.querySelector<ZnDropdown>('zn-dropdown')!;
241
- await pill.show();
282
+ it('closes on a click outside and drops incomplete rules', async () => {
283
+ const el = await filter();
284
+ await open(el);
285
+ await setRule(el, 'email');
242
286
 
243
- const [operator] = [...pill.querySelectorAll('zn-menu-item')];
244
- clickOn(operator);
287
+ click(document.body);
245
288
  await el.updateComplete;
246
- expect(pill.open, 'picking an operator should leave the panel open').to.be.true;
247
-
248
- clickOn(pill.querySelector('zn-menu-item[value="open"]'));
249
- await waitUntil(() => !pill.open, 'picking a value should close the panel');
250
- expect(pill.open).to.be.false;
289
+ expect(root(el).querySelector('.filter__pop')).to.not.exist;
290
+ expect(chips(el)).to.deep.equal([]);
251
291
  });
252
292
 
253
- describe('long option lists', () => {
254
- const many = Object.fromEntries(
255
- ['How To', 'Linux', 'Beta', 'Router', 'TV', 'Other', 'Mac Hotspot Shield', 'General', 'iOS', 'Tutorials']
256
- .map((label, index) => [`c${index}`, label])
257
- );
258
-
259
- async function categoryBar() {
260
- const el = await fixture<ZnDataTableFilter>(html`
261
- <zn-data-table-filter default-filters="category"
262
- .filters="${[{
263
- id: 'category',
264
- name: 'category',
265
- operators: ['eq'],
266
- options: many
267
- }]}"></zn-data-table-filter>`);
268
- await el.updateComplete;
269
- return el;
270
- }
271
-
272
- const search = async (el: ZnDataTableFilter, term: string) => {
273
- const input = el.shadowRoot!.querySelector<ZnInput>('.filter-bar__option-search')!;
274
- input.value = term;
275
- input.dispatchEvent(new Event('zn-input', {bubbles: true, composed: true}));
276
- await el.updateComplete;
277
- };
278
-
279
- const labels = (el: ZnDataTableFilter) =>
280
- [...el.shadowRoot!.querySelectorAll('zn-menu-item')].map(item => item.textContent!.trim());
281
-
282
- it('offers a search above the options', async () => {
283
- const el = await categoryBar();
284
-
285
- expect(el.shadowRoot!.querySelector('.filter-bar__option-search')).to.exist;
286
- });
287
-
288
- it('leaves the search off a short list', async () => {
289
- const el = await filterBar('status');
290
-
291
- expect(el.shadowRoot!.querySelector('.filter-bar__option-search')).to.not.exist;
292
- });
293
-
294
- it('narrows the options without regard to case', async () => {
295
- const el = await categoryBar();
296
-
297
- await search(el, 'HOTSPOT');
298
-
299
- expect(labels(el)).to.deep.equal(['Mac Hotspot Shield']);
300
- });
293
+ it('drops a rule when its chip\'s remove button is clicked', async () => {
294
+ const el = await filter({value: encode([{key: 'status', comparator: 'eq', value: 'active'}])});
301
295
 
302
- it('says so when nothing matches', async () => {
303
- const el = await categoryBar();
304
-
305
- await search(el, 'nothing here');
306
-
307
- expect(labels(el)).to.deep.equal(['No matches']);
308
- });
309
- });
310
-
311
- it('drops a filter when its chip\'s remove button is clicked', async () => {
312
- const el = await filterBar('role,status');
313
- clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="active"]'));
314
- await el.updateComplete;
315
-
316
- const chip = [...el.shadowRoot!.querySelectorAll('.filter-bar__chip')]
317
- .find(item => item.querySelector('zn-dropdown')!.getAttribute('data-key') === 'status')!;
318
- clickOn(chip.querySelector('.filter-bar__remove'));
296
+ click(root(el).querySelector('.filter__chip__remove'));
319
297
  await el.updateComplete;
320
298
 
321
- expect(pills(el)).to.deep.equal(['Role']);
299
+ expect(chips(el)).to.deep.equal([]);
322
300
  expect(el.value).to.equal('');
323
301
  });
324
302
 
325
- it('clear() removes every pill and empties the value', async () => {
326
- const el = await filterBar('role,status');
327
- clickOn(el.shadowRoot!.querySelector('zn-menu-item[value="active"]'));
328
- await el.updateComplete;
303
+ it('clear() removes every rule and empties the value', async () => {
304
+ const el = await filter({value: encode([{key: 'status', comparator: 'eq', value: 'active'}])});
329
305
 
330
306
  el.clear();
331
307
  await el.updateComplete;
332
308
 
333
- expect(pills(el)).to.deep.equal([]);
309
+ expect(chips(el)).to.deep.equal([]);
334
310
  expect(el.value).to.equal('');
335
311
  });
336
312
 
337
- // Setting `value` (the inverse of emitChange) rebuilds the active pills, so a filter restored
338
- // from a shared URL renders exactly as it was when encoded.
339
- describe('restore from value', () => {
340
- const encode = (conditions: {key: string; comparator: string; value: string}[]) =>
341
- btoa(JSON.stringify(conditions));
342
-
343
- it('rebuilds the active pills from an encoded value', async () => {
344
- const value = encode([
345
- {key: 'status', comparator: 'eq', value: 'active'},
346
- {key: 'email', comparator: 'contains', value: 'acme'},
347
- ]);
313
+ it('offers suggestions matching a typed value, without regard to case', async () => {
314
+ const el = await filter();
315
+ el.suggestions = {email: ['Ada@example.com', 'grace@example.com']};
316
+ await open(el);
317
+ await setRule(el, 'email');
348
318
 
349
- const el = await fixture<ZnDataTableFilter>(html`
350
- <zn-data-table-filter .filters="${FILTERS}" .value="${value}"></zn-data-table-filter>`);
351
- await el.updateComplete;
319
+ const input = control<ZnInput>(el, 'Filter value');
320
+ input.value = 'ADA';
321
+ input.dispatchEvent(new Event('zn-input', {bubbles: true, composed: true}));
322
+ await el.updateComplete;
352
323
 
353
- expect(pills(el)).to.deep.equal(['Status: Active', 'Email: acme']);
354
- });
324
+ const shown = [...root(el).querySelectorAll('.filter__suggestion')];
325
+ expect(shown.map(b => b.textContent!.trim())).to.deep.equal(['Ada@example.com']);
355
326
 
356
- it('renders no pills for an absent value', async () => {
357
- const el = await fixture<ZnDataTableFilter>(html`
358
- <zn-data-table-filter .filters="${FILTERS}"></zn-data-table-filter>`);
359
- await el.updateComplete;
327
+ click(shown[0]);
328
+ await el.updateComplete;
329
+ expect(root(el).querySelector('.filter__suggestion')).to.not.exist;
330
+ await apply(el);
331
+ expect(decode(el.value)).to.deep.equal([{key: 'email', comparator: 'contains', value: 'Ada@example.com'}]);
332
+ });
360
333
 
361
- expect(pills(el)).to.deep.equal([]);
362
- });
334
+ it('forgets typed text when the field changes', async () => {
335
+ const el = await filter({filters: [...FILTERS, {id: 'name', name: 'Name', operators: ['contains']}]});
336
+ el.suggestions = {email: ['Ada@example.com'], name: ['Adam']};
337
+ await open(el);
338
+ await setRule(el, 'email');
363
339
 
364
- // The production flow sets `value` after `filters` (and after default-filters seeds an empty
365
- // pill), so a later value must still win over the valueless default pill.
366
- it('lets a value set after default-filters win over the empty default pill', async () => {
367
- const el = await fixture<ZnDataTableFilter>(html`
368
- <zn-data-table-filter default-filters="role" .filters="${FILTERS}"></zn-data-table-filter>`);
369
- await el.updateComplete;
370
- expect(pills(el)).to.deep.equal(['Role']);
340
+ const input = control<ZnInput>(el, 'Filter value');
341
+ input.value = 'ADA';
342
+ input.dispatchEvent(new Event('zn-input', {bubbles: true, composed: true}));
343
+ await el.updateComplete;
344
+ expect(root(el).querySelector('.filter__suggestion')).to.exist;
371
345
 
372
- el.value = encode([{key: 'status', comparator: 'eq', value: 'active'}]);
373
- await el.updateComplete;
346
+ await setRule(el, 'name');
347
+ expect(root(el).querySelector('.filter__suggestion')).to.not.exist;
348
+ });
374
349
 
375
- expect(pills(el)).to.deep.equal(['Status: Active']);
376
- });
350
+ it('hands its chips to a filter whose source it is', async () => {
351
+ const wrap = await fixture<HTMLDivElement>(html`
352
+ <div>
353
+ <zn-data-table-filter .filters="${FILTERS}"
354
+ .value="${encode([{key: 'status', comparator: 'eq', value: 'active'}])}"></zn-data-table-filter>
355
+ <zn-data-table-filter></zn-data-table-filter>
356
+ </div>`);
357
+ const [source, mirror] = [...wrap.querySelectorAll<ZnDataTableFilter>('zn-data-table-filter')];
358
+ mirror.source = source;
359
+ await mirror.updateComplete;
360
+ await source.updateComplete;
361
+
362
+ expect(chips(source)).to.deep.equal([]);
363
+ expect(chips(mirror)).to.deep.equal(['Status is Active']);
364
+ expect(root(mirror).querySelector('.filter__toggle')).to.not.exist;
365
+
366
+ click(root(mirror).querySelector('.filter__chip__text'));
367
+ await source.updateComplete;
368
+ expect(root(source).querySelector('.filter__pop')).to.exist;
369
+
370
+ click(root(mirror).querySelector('.filter__chip__remove'));
371
+ await source.updateComplete;
372
+ await mirror.updateComplete;
373
+ expect(source.value).to.equal('');
374
+ expect(chips(mirror)).to.deep.equal([]);
377
375
  });
378
376
  });