@kubex/zinc 1.1.185 → 2.0.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 (358) 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 +10319 -2666
  6. package/dist/vscode.html-custom-data.json +1012 -57
  7. package/dist/web-types.json +2097 -145
  8. package/dist/zn.d.ts +2294 -416
  9. package/dist/zn.min.css +1 -1
  10. package/dist/zn.min.js +2310 -1637
  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/icon.md +46 -1
  33. package/docs/pages/components/input.md +30 -11
  34. package/docs/pages/components/item.md +35 -0
  35. package/docs/pages/components/key-value.md +25 -0
  36. package/docs/pages/components/menu.md +74 -7
  37. package/docs/pages/components/nav-item.md +15 -0
  38. package/docs/pages/components/nav-list.md +132 -0
  39. package/docs/pages/components/navbar.md +100 -1
  40. package/docs/pages/components/page.md +242 -74
  41. package/docs/pages/components/priority-list.md +43 -5
  42. package/docs/pages/components/query-builder.md +23 -0
  43. package/docs/pages/components/radio-group.md +33 -1
  44. package/docs/pages/components/radio.md +1 -1
  45. package/docs/pages/components/report-table.md +202 -0
  46. package/docs/pages/components/stat.md +30 -0
  47. package/docs/pages/components/tile.md +11 -0
  48. package/docs/pages/components/tooltip.md +1 -1
  49. package/docs/pages/components/translation-grid.md +86 -0
  50. package/docs/pages/components/vertical-stepper.md +108 -64
  51. package/package.json +2 -1
  52. package/scss/_global-spacing.scss +2 -2
  53. package/scss/_root.scss +11 -171
  54. package/scss/boot.scss +14 -0
  55. package/scss/mixins/_helper-mixins.scss +24 -2
  56. package/scss/mixins/_motion-mixins.scss +126 -0
  57. package/scss/mixins/_spacing-mixins.scss +4 -3
  58. package/scss/mixins/_tab-indicator-mixins.scss +12 -0
  59. package/scss/mixins/_typography-mixins.scss +1 -1
  60. package/scss/mixins/index.scss +14 -0
  61. package/scss/shared/_base.scss +32 -1
  62. package/scss/shared/_button-mixin.scss +7 -5
  63. package/scss/shared/_table.scss +35 -4
  64. package/scss/shared/nav.scss +4 -0
  65. package/scss/shared/shimmer.scss +1 -1
  66. package/scss/table-controls.scss +206 -0
  67. package/scss/themes/_dark.scss +1 -3
  68. package/scss/themes/_index.scss +1 -1
  69. package/scss/themes/_light.scss +46 -6
  70. package/scss/themes/_modes.scss +11 -11
  71. package/scss/themes/_openbyte.scss +944 -0
  72. package/scss/themes/_typography.scss +4 -2
  73. package/scss/variables/index.scss +0 -9
  74. package/src/components/alert/alert.component.ts +14 -4
  75. package/src/components/alert/alert.scss +55 -69
  76. package/src/components/alert/alert.test.ts +8 -0
  77. package/src/components/animated-button/animated-button.component.ts +3 -2
  78. package/src/components/animated-button/animated-button.scss +6 -23
  79. package/src/components/animated-button/animated-button.test.ts +4 -3
  80. package/src/components/avatar-group/avatar-group.component.ts +84 -0
  81. package/src/components/avatar-group/avatar-group.scss +54 -0
  82. package/src/components/avatar-group/avatar-group.test.ts +59 -0
  83. package/src/components/avatar-group/index.ts +12 -0
  84. package/src/components/badge/badge.component.ts +49 -0
  85. package/src/components/badge/badge.scss +80 -0
  86. package/src/components/badge/badge.test.ts +34 -0
  87. package/src/components/badge/index.ts +12 -0
  88. package/src/components/bulk-actions/bulk-actions.component.ts +14 -2
  89. package/src/components/button/button.component.ts +228 -8
  90. package/src/components/button/button.scss +115 -32
  91. package/src/components/button/button.test.ts +122 -0
  92. package/src/components/button-menu/button-menu.component.ts +32 -11
  93. package/src/components/button-menu/button-menu.scss +5 -1
  94. package/src/components/channel-tile/channel-tile.component.ts +34 -27
  95. package/src/components/channel-tile/channel-tile.scss +76 -43
  96. package/src/components/chart/builders.test.ts +46 -0
  97. package/src/components/chart/builders.ts +27 -3
  98. package/src/components/chart/chart-live.test.ts +4 -6
  99. package/src/components/chart/chart.component.ts +125 -5
  100. package/src/components/chart/chart.scss +121 -0
  101. package/src/components/chart/chart.test.ts +81 -0
  102. package/src/components/chat-message/chat-message.component.ts +21 -1
  103. package/src/components/chat-message/chat-message.scss +13 -2
  104. package/src/components/chat-message/chat-message.test.ts +9 -0
  105. package/src/components/checkbox/checkbox.component.ts +61 -27
  106. package/src/components/checkbox/checkbox.scss +58 -25
  107. package/src/components/checkbox-group/checkbox-group.component.ts +7 -1
  108. package/src/components/chip/chip.component.ts +11 -1
  109. package/src/components/chip/chip.scss +91 -26
  110. package/src/components/chip/chip.test.ts +17 -0
  111. package/src/components/collapsible/collapsible.component.ts +17 -3
  112. package/src/components/collapsible/collapsible.scss +19 -10
  113. package/src/components/color-picker/color-picker.component.ts +277 -0
  114. package/src/components/color-picker/color-picker.scss +141 -0
  115. package/src/components/color-picker/color-picker.test.ts +57 -0
  116. package/src/components/color-picker/index.ts +12 -0
  117. package/src/components/cols/cols.component.ts +10 -0
  118. package/src/components/cols/cols.test.ts +17 -0
  119. package/src/components/confirm/confirm.component.ts +20 -6
  120. package/src/components/confirm/confirm.test.ts +52 -0
  121. package/src/components/content-block/content-block.component.ts +38 -9
  122. package/src/components/content-block/content-block.scss +11 -5
  123. package/src/components/content-block/content-block.test.ts +26 -0
  124. package/src/components/copy-button/copy-button.component.ts +2 -2
  125. package/src/components/copy-button/copy-button.scss +17 -4
  126. package/src/components/data-select/data-select.component.ts +3 -1
  127. package/src/components/data-table/data-table.component.ts +362 -140
  128. package/src/components/data-table/data-table.scss +136 -60
  129. package/src/components/data-table/data-table.test.ts +356 -4
  130. package/src/components/data-table-filter/data-table-filter.component.ts +426 -406
  131. package/src/components/data-table-filter/data-table-filter.scss +176 -148
  132. package/src/components/data-table-filter/data-table-filter.test.ts +271 -273
  133. package/src/components/data-table-search/data-table-search.component.ts +4 -2
  134. package/src/components/data-table-search/data-table-search.scss +8 -1
  135. package/src/components/data-table-search/data-table-search.test.ts +29 -0
  136. package/src/components/date-range/date-range-math.test.ts +78 -0
  137. package/src/components/date-range/date-range-math.ts +105 -0
  138. package/src/components/date-range/date-range.component.ts +375 -0
  139. package/src/components/date-range/date-range.scss +272 -0
  140. package/src/components/date-range/date-range.test.ts +247 -0
  141. package/src/components/date-range/index.ts +12 -0
  142. package/src/components/datepicker/datepicker.component.ts +44 -7
  143. package/src/components/datepicker/datepicker.scss +9 -0
  144. package/src/components/datepicker/datepicker.test.ts +2 -2
  145. package/src/components/dialog/dialog.component.ts +38 -19
  146. package/src/components/dialog/dialog.scss +65 -36
  147. package/src/components/dialog/dialog.test.ts +14 -0
  148. package/src/components/diff/diff.component.ts +55 -0
  149. package/src/components/diff/diff.scss +46 -0
  150. package/src/components/diff/diff.test.ts +15 -0
  151. package/src/components/diff/index.ts +12 -0
  152. package/src/components/diff-row/diff-row.component.ts +75 -0
  153. package/src/components/diff-row/diff-row.scss +70 -0
  154. package/src/components/diff-row/diff-row.test.ts +37 -0
  155. package/src/components/diff-row/index.ts +12 -0
  156. package/src/components/dropdown/dropdown.component.ts +12 -10
  157. package/src/components/dropdown/dropdown.scss +2 -0
  158. package/src/components/editor/editor.component.ts +159 -11
  159. package/src/components/editor/editor.scss +250 -1
  160. package/src/components/editor/editor.test.ts +31 -3
  161. package/src/components/editor/modules/ai/panel/ai-panel.scss +0 -1
  162. package/src/components/editor/modules/attachment/attachment.ts +53 -31
  163. package/src/components/editor/modules/context-menu/context-menu.ts +17 -0
  164. package/src/components/editor/modules/dialog/dialog.component.ts +23 -1
  165. package/src/components/editor/modules/dialog/dialog.test.ts +28 -0
  166. package/src/components/editor/modules/emoji/emoji.ts +4 -5
  167. package/src/components/editor/modules/merge-field/merge-field.ts +83 -0
  168. package/src/components/editor/modules/toolbar/toolbar.component.ts +121 -68
  169. package/src/components/editor/modules/toolbar/toolbar.scss +38 -0
  170. package/src/components/editor/modules/toolbar/toolbar.ts +25 -1
  171. package/src/components/empty-state/empty-state.component.ts +6 -1
  172. package/src/components/empty-state/empty-state.scss +21 -4
  173. package/src/components/expanding-action/expanding-action.component.ts +22 -34
  174. package/src/components/expanding-action/expanding-action.scss +3 -45
  175. package/src/components/expanding-action/expanding-action.test.ts +2 -1
  176. package/src/components/file/file.component.ts +30 -2
  177. package/src/components/file/file.scss +6 -0
  178. package/src/components/flow-builder/flow-builder.component.ts +36 -3
  179. package/src/components/flow-builder/flow-builder.scss +0 -6
  180. package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +8 -0
  181. package/src/components/flow-builder/modules/flow-canvas/flow-canvas.scss +10 -5
  182. package/src/components/flow-builder/modules/flow-node/flow-node.component.ts +17 -0
  183. package/src/components/flow-builder/modules/flow-node/flow-node.scss +10 -0
  184. package/src/components/form-actions/form-actions.component.ts +48 -2
  185. package/src/components/form-actions/form-actions.scss +1 -1
  186. package/src/components/form-group/form-group.component.ts +10 -3
  187. package/src/components/form-group/form-group.scss +37 -4
  188. package/src/components/form-group/form-group.test.ts +10 -0
  189. package/src/components/header/header.component.ts +11 -4
  190. package/src/components/header/header.scss +47 -5
  191. package/src/components/hover-container/hover-container.component.ts +4 -5
  192. package/src/components/icon/icon.component.ts +95 -9
  193. package/src/components/icon/icon.scss +70 -6
  194. package/src/components/icon/icon.test.ts +55 -0
  195. package/src/components/icon-picker/icon-picker.component.ts +35 -1
  196. package/src/components/inline-edit/inline-edit.component.ts +38 -8
  197. package/src/components/inline-edit/inline-edit.scss +128 -19
  198. package/src/components/input/input.component.ts +173 -297
  199. package/src/components/input/input.scss +65 -43
  200. package/src/components/input/input.test.ts +86 -4
  201. package/src/components/item/item.component.ts +12 -0
  202. package/src/components/item/item.scss +102 -3
  203. package/src/components/item/item.test.ts +23 -0
  204. package/src/components/key/key.component.ts +2 -2
  205. package/src/components/key/key.scss +11 -0
  206. package/src/components/key-value/index.ts +12 -0
  207. package/src/components/key-value/key-value.component.ts +160 -0
  208. package/src/components/key-value/key-value.scss +20 -0
  209. package/src/components/key-value/key-value.test.ts +22 -0
  210. package/src/components/markdown-editor/markdown-editor.component.ts +8 -1
  211. package/src/components/menu/menu.component.ts +21 -2
  212. package/src/components/menu/menu.scss +43 -23
  213. package/src/components/menu/menu.test.ts +33 -0
  214. package/src/components/menu-item/menu-item.component.ts +13 -2
  215. package/src/components/menu-item/menu-item.scss +56 -23
  216. package/src/components/menu-item/menu-item.test.ts +31 -0
  217. package/src/components/menu-item/submenu-controller.ts +25 -15
  218. package/src/components/menu-label/index.ts +12 -0
  219. package/src/components/menu-label/menu-label.component.ts +30 -0
  220. package/src/components/menu-label/menu-label.scss +18 -0
  221. package/src/components/nav-item/index.ts +12 -0
  222. package/src/components/nav-item/nav-item.component.ts +83 -0
  223. package/src/components/nav-item/nav-item.scss +128 -0
  224. package/src/components/nav-list/index.ts +12 -0
  225. package/src/components/nav-list/nav-list.component.ts +313 -0
  226. package/src/components/nav-list/nav-list.scss +87 -0
  227. package/src/components/nav-list/nav-list.test.ts +250 -0
  228. package/src/components/navbar/navbar.component.ts +235 -171
  229. package/src/components/navbar/navbar.scss +333 -169
  230. package/src/components/navbar/navbar.test.ts +63 -143
  231. package/src/components/page/page.component.ts +114 -29
  232. package/src/components/page/page.scss +223 -95
  233. package/src/components/page/page.test.ts +214 -3
  234. package/src/components/page-builder/page-builder.component.ts +51 -5
  235. package/src/components/page-builder/page-builder.scss +6 -1
  236. package/src/components/page-nav/page-nav.component.ts +25 -3
  237. package/src/components/page-nav/page-nav.scss +10 -2
  238. package/src/components/pagination/pagination.scss +1 -1
  239. package/src/components/panel/panel.component.ts +14 -13
  240. package/src/components/panel/panel.scss +14 -10
  241. package/src/components/panel/panel.test.ts +42 -0
  242. package/src/components/popup/popup.component.ts +20 -13
  243. package/src/components/popup/popup.test.ts +21 -1
  244. package/src/components/priority-list/priority-list.component.ts +157 -172
  245. package/src/components/priority-list/priority-list.scss +113 -17
  246. package/src/components/priority-list/priority-list.test.ts +301 -3
  247. package/src/components/progress-bar/progress-bar.component.ts +1 -1
  248. package/src/components/progress-bar/progress-bar.scss +16 -4
  249. package/src/components/progress-tile/progress-tile.scss +1 -1
  250. package/src/components/query-builder/query-builder.component.ts +85 -10
  251. package/src/components/query-builder/query-builder.scss +7 -1
  252. package/src/components/query-builder/query-builder.test.ts +40 -0
  253. package/src/components/radio/radio.component.ts +57 -18
  254. package/src/components/radio/radio.scss +101 -26
  255. package/src/components/radio-group/radio-group.component.ts +45 -7
  256. package/src/components/radio-group/radio-group.scss +17 -0
  257. package/src/components/radio-group/radio-group.test.ts +63 -2
  258. package/src/components/rating/rating.component.ts +23 -0
  259. package/src/components/rating/rating.scss +1 -1
  260. package/src/components/remarkd-editor/remarkd-editor.component.ts +60 -5
  261. package/src/components/remarkd-editor/remarkd-editor.scss +12 -4
  262. package/src/components/report-table/index.ts +12 -0
  263. package/src/components/report-table/report-format.test.ts +86 -0
  264. package/src/components/report-table/report-format.ts +72 -0
  265. package/src/components/report-table/report-math.test.ts +74 -0
  266. package/src/components/report-table/report-math.ts +252 -0
  267. package/src/components/report-table/report-table.component.ts +681 -0
  268. package/src/components/report-table/report-table.scss +328 -0
  269. package/src/components/report-table/report-table.test.ts +702 -0
  270. package/src/components/reveal/reveal.component.ts +15 -1
  271. package/src/components/reveal/reveal.scss +5 -0
  272. package/src/components/schedule-builder/schedule-builder.component.ts +23 -8
  273. package/src/components/scroll-container/scroll-container.component.ts +19 -1
  274. package/src/components/scroll-container/scroll-container.scss +6 -1
  275. package/src/components/select/select.component.ts +52 -36
  276. package/src/components/select/select.scss +42 -25
  277. package/src/components/select/select.test.ts +14 -2
  278. package/src/components/settings-container/settings-container.scss +18 -5
  279. package/src/components/sidebar/sidebar.component.ts +8 -1
  280. package/src/components/sidebar/sidebar.scss +6 -1
  281. package/src/components/skeleton/skeleton.component.ts +3 -3
  282. package/src/components/skeleton/skeleton.scss +6 -6
  283. package/src/components/skeleton/skeleton.test.ts +9 -0
  284. package/src/components/slash-menu/slash-menu.scss +1 -1
  285. package/src/components/slideout/slideout.component.ts +20 -3
  286. package/src/components/slideout/slideout.scss +13 -6
  287. package/src/components/slideout/slideout.test.ts +19 -0
  288. package/src/components/split-button/split-button.component.ts +1 -1
  289. package/src/components/split-pane/split-pane.component.ts +71 -8
  290. package/src/components/split-pane/split-pane.scss +145 -41
  291. package/src/components/split-pane/split-pane.test.ts +23 -3
  292. package/src/components/stat/stat.component.ts +209 -14
  293. package/src/components/stat/stat.scss +305 -5
  294. package/src/components/stat/stat.test.ts +143 -2
  295. package/src/components/status-indicator/status-indicator.scss +22 -21
  296. package/src/components/stepper/stepper.scss +3 -3
  297. package/src/components/tab/tab.component.ts +1 -0
  298. package/src/components/table/table.component.ts +12 -1
  299. package/src/components/table/table.scss +21 -13
  300. package/src/components/table/table.test.ts +17 -0
  301. package/src/components/tabs/tabs.component.ts +150 -37
  302. package/src/components/tabs/tabs.scss +48 -7
  303. package/src/components/tabs/tabs.test.ts +169 -0
  304. package/src/components/textarea/textarea.component.ts +25 -3
  305. package/src/components/textarea/textarea.scss +10 -0
  306. package/src/components/textarea/textarea.test.ts +8 -4
  307. package/src/components/theme-editor/theme-editor.scss +0 -6
  308. package/src/components/tile/tile.component.ts +32 -2
  309. package/src/components/tile/tile.scss +40 -7
  310. package/src/components/tile/tile.test.ts +10 -0
  311. package/src/components/toggle/toggle.component.ts +23 -7
  312. package/src/components/toggle/toggle.scss +36 -29
  313. package/src/components/toggle/toggle.test.ts +17 -0
  314. package/src/components/tooltip/tooltip.component.ts +3 -0
  315. package/src/components/tooltip/tooltip.scss +11 -6
  316. package/src/components/tooltip/tooltip.test.ts +12 -0
  317. package/src/components/translation-field/index.ts +12 -0
  318. package/src/components/translation-field/translation-field.component.ts +55 -0
  319. package/src/components/translation-field/translation-field.scss +101 -0
  320. package/src/components/translation-fields/index.ts +12 -0
  321. package/src/components/translation-fields/translation-fields.component.ts +80 -0
  322. package/src/components/translation-fields/translation-fields.scss +60 -0
  323. package/src/components/translation-grid/index.ts +12 -0
  324. package/src/components/translation-grid/translation-grid.component.ts +157 -0
  325. package/src/components/translation-grid/translation-grid.scss +138 -0
  326. package/src/components/translation-grid/translation-grid.test.ts +28 -0
  327. package/src/components/vertical-stepper/vertical-stepper.component.ts +35 -9
  328. package/src/components/vertical-stepper/vertical-stepper.scss +110 -39
  329. package/src/components/vertical-stepper/vertical-stepper.test.ts +35 -0
  330. package/src/components/well/well.scss +2 -2
  331. package/src/events/events.ts +2 -0
  332. package/src/events/zn-chart-click.ts +13 -0
  333. package/src/events/zn-report-change.ts +7 -0
  334. package/src/form-control.scss +36 -3
  335. package/src/internal/anchored-popover.ts +175 -0
  336. package/src/internal/animate.ts +19 -5
  337. package/src/internal/cosmic-border.ts +15 -0
  338. package/src/internal/form.ts +1 -1
  339. package/src/internal/hover-intent.ts +15 -0
  340. package/src/internal/input-modality.ts +15 -0
  341. package/src/internal/pointer-intent.ts +37 -0
  342. package/src/internal/reorder.ts +632 -0
  343. package/src/internal/scroll.ts +51 -3
  344. package/src/internal/selection-card.ts +1 -1
  345. package/src/internal/tab-indicator.ts +92 -0
  346. package/src/internal/table-sort.ts +25 -0
  347. package/src/internal/table-tabs.ts +108 -0
  348. package/src/internal/test-drag.ts +35 -0
  349. package/src/internal/theme-openbyte.test.ts +126 -0
  350. package/src/internal/theme.ts +4 -4
  351. package/src/internal/zinc-element.ts +2 -2
  352. package/src/selection-card.scss +25 -0
  353. package/src/types/web-test-runner-commands.d.ts +1 -0
  354. package/src/utilities/color.test.ts +43 -0
  355. package/src/utilities/color.ts +189 -0
  356. package/src/zinc.ts +14 -0
  357. package/.github/workflows/eleventy_build.yml +0 -42
  358. package/scss/themes/_aura.scss +0 -114
@@ -0,0 +1,313 @@
1
+ import {clipCornerRadius, ReorderController} from '../../internal/reorder';
2
+ import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
3
+ import {FormControlController, validValidityState} from '../../internal/form';
4
+ import {HasSlotController} from '../../internal/slot';
5
+ import {property, state} from 'lit/decorators.js';
6
+ import ZincElement from '../../internal/zinc-element';
7
+ import ZnInput from '../input';
8
+ import type {ZincFormControl} from '../../internal/zinc-element';
9
+ import type ZnNavItem from '../nav-item';
10
+
11
+ import styles from './nav-list.scss';
12
+
13
+ /**
14
+ * @summary The list side of a master–detail layout: a card of selectable groups shown beside the selected group's
15
+ * contents. Place it in the `left` slot of `<zn-tabs gap>`, which switches the detail panels.
16
+ * @documentation https://zinc.style/components/nav-list
17
+ * @status experimental
18
+ * @since 1.0
19
+ *
20
+ * @dependency zn-input
21
+ *
22
+ * @slot - `zn-nav-item` elements.
23
+ * @slot actions - Actions in the card's header, beside the caption.
24
+ *
25
+ * @csspart base - The component's base wrapper.
26
+ * @csspart header - The header holding the caption and actions.
27
+ * @csspart search - The wrapper around the search input.
28
+ * @csspart list - The scrolling list of items.
29
+ * @csspart empty - The message shown when the search matches nothing.
30
+ *
31
+ * @cssproperty --zn-nav-list-width - The width of the card beside the detail.
32
+ * @cssproperty --zn-nav-list-max-height - Caps the card's height; the list scrolls beyond it.
33
+ *
34
+ * @event zn-change - Emitted when a sortable list's items have been moved.
35
+ * @event zn-reorder - Emitted when a sortable list's items have been moved.
36
+ */
37
+ export default class ZnNavList extends ZincElement implements ZincFormControl {
38
+ static styles: CSSResultGroup = unsafeCSS(styles);
39
+ static dependencies = {
40
+ 'zn-input': ZnInput,
41
+ };
42
+
43
+ protected readonly formControlController = new FormControlController(this, {
44
+ // The same `[{key, priority}]` JSON a zn-priority-list submits, so a handler can take either
45
+ value: (control: ZnNavList) => control.sortable ? JSON.stringify(control.getPriorityMap()) : undefined,
46
+ defaultValue: () => undefined,
47
+ setValue: () => undefined,
48
+ });
49
+
50
+ private readonly hasSlotController = new HasSlotController(this, 'actions');
51
+
52
+ private readonly reorderController: ReorderController = new ReorderController(this, {
53
+ formControl: this.formControlController,
54
+ fallbackFields: () => this.name ? [[this.name, JSON.stringify(this.getPriorityMap())]] : [],
55
+ // Plain arrows move between items; Alt moves the item itself
56
+ modifier: 'alt',
57
+ focus: key => this._item(key)?.focus(),
58
+ });
59
+
60
+ /** Shows a search input that filters the items by their label. */
61
+ @property({type: Boolean, reflect: true}) searchable = false;
62
+
63
+ @property({attribute: 'search-placeholder'}) searchPlaceholder = 'Search';
64
+
65
+ /** Shown when the search matches no items. */
66
+ @property({attribute: 'empty-label'}) emptyLabel = 'No matches';
67
+
68
+ /** A heading for the card, shown with the `actions` slot above the items. */
69
+ @property() caption = '';
70
+
71
+ /** Draws the items as full-width rows split by dividers, marking the selected one with a bar on its edge. */
72
+ @property({type: Boolean, reflect: true}) divide = false;
73
+
74
+ /** Lets the items be dragged, or moved with Alt+Arrow keys, into a new order. */
75
+ @property({type: Boolean, reflect: true}) sortable = false;
76
+
77
+ /** Submits the order as `[{key, priority}]` JSON, keyed by each item's `value` (or `tab`), counting from 1. */
78
+ @property() name = '';
79
+
80
+ /** Associates the list with a form by id. */
81
+ @property({reflect: true}) form = '';
82
+
83
+ @property({type: Boolean, reflect: true}) disabled = false;
84
+
85
+ /**
86
+ * When set, the form is submitted after each reorder, to this URL or, when empty, the form's own action.
87
+ */
88
+ @property({attribute: 'formaction'}) formAction: string;
89
+
90
+ @state() private _query = '';
91
+
92
+ private readonly _selectionObserver = new MutationObserver(() => this._syncTabStops());
93
+
94
+ get validity(): ValidityState {
95
+ return validValidityState;
96
+ }
97
+
98
+ get validationMessage(): string {
99
+ return '';
100
+ }
101
+
102
+ connectedCallback() {
103
+ super.connectedCallback();
104
+ // zn-tabs marks the active item with a class, after the click that selects it
105
+ this._selectionObserver.observe(this, {subtree: true, attributes: true, attributeFilter: ['class']});
106
+ }
107
+
108
+ disconnectedCallback() {
109
+ super.disconnectedCallback();
110
+ this._selectionObserver.disconnect();
111
+ }
112
+
113
+ private get _items(): ZnNavItem[] {
114
+ return [...this.querySelectorAll<ZnNavItem>(':scope > zn-nav-item')];
115
+ }
116
+
117
+ private get _visibleItems() {
118
+ return this._items.filter(item => !item.hidden);
119
+ }
120
+
121
+ private _key(item: ZnNavItem) {
122
+ return item.getAttribute('value') ?? item.getAttribute('tab') ?? '';
123
+ }
124
+
125
+ private _item(key: string) {
126
+ return this._items.find(item => this._key(item) === key) ?? null;
127
+ }
128
+
129
+ /** The order as `[{key, priority}]` JSON. */
130
+ get value() {
131
+ return JSON.stringify(this.getPriorityMap());
132
+ }
133
+
134
+ /** The order as `{key, priority}` pairs, counting from 1. */
135
+ getPriorityMap() {
136
+ return this._items.map((item, index) => ({key: this._key(item), priority: index + 1}));
137
+ }
138
+
139
+ /** @internal ReorderHost: the items' keys in order. */
140
+ reorderKeys() {
141
+ return this._items.map(item => this._key(item));
142
+ }
143
+
144
+ /** @internal ReorderHost: puts the items in a new order. */
145
+ applyReorder(keys: string[]) {
146
+ for (const key of keys) {
147
+ const item = this._item(key);
148
+ if (item) this.append(item);
149
+ }
150
+ // Measured now, in the final place, before the reorder slide moves the rows
151
+ this._syncDragCorners();
152
+ }
153
+
154
+ /** @internal ReorderHost: the row drawn for an item. */
155
+ reorderRow(key: string) {
156
+ return this._item(key);
157
+ }
158
+
159
+ /** @internal ReorderHost: whether an item is pinned where it is. */
160
+ reorderLocked(key: string) {
161
+ return this._item(key)?.hasAttribute('locked') ?? false;
162
+ }
163
+
164
+ /** The key of the item an event happened on, when it is one of this list's. */
165
+ private _eventKey(e: Event) {
166
+ const item = (e.target as Element).closest?.('zn-nav-item') as ZnNavItem | null;
167
+ return item && item.parentElement === this ? this._key(item) : null;
168
+ }
169
+
170
+ private _handleSlotChange() {
171
+ this._syncTabStops();
172
+ this._syncItems();
173
+ }
174
+
175
+ private _syncItems() {
176
+ const dragged = this.reorderController.draggedKey;
177
+ for (const item of this._items) {
178
+ item.toggleAttribute('divide', this.divide);
179
+ item.toggleAttribute('sortable', this.sortable);
180
+ item.toggleAttribute('dragging', this._key(item) === dragged);
181
+ }
182
+ }
183
+
184
+ protected updated(changed: PropertyValues) {
185
+ super.updated(changed);
186
+ if (changed.has('disabled')) this._syncTabStops();
187
+ this._syncItems();
188
+ this._syncDragCorners();
189
+ }
190
+
191
+ /** Rounds a divided list's placeholder into the card corners it moves into while dragging. */
192
+ private _syncDragCorners() {
193
+ const dragged = this.reorderController.draggedKey;
194
+ const item = dragged === null ? null : this._item(dragged);
195
+ if (!item) return;
196
+ const radius = this.divide ? clipCornerRadius(item, item) : null;
197
+ if (radius) {
198
+ item.style.setProperty('--zn-nav-item-drag-radius', radius);
199
+ } else {
200
+ item.style.removeProperty('--zn-nav-item-drag-radius');
201
+ }
202
+ }
203
+
204
+ /** Keeps a single tab stop in the list (none when disabled): the selected item when it is visible, otherwise the first. */
205
+ private _syncTabStops() {
206
+ const visible = this._visibleItems;
207
+ const stop = this.disabled ? null : visible.find(item => item.selected) ?? visible[0];
208
+ for (const item of this._items) {
209
+ item.tabIndex = item === stop ? 0 : -1;
210
+ }
211
+ }
212
+
213
+ private _handleSearch(e: Event) {
214
+ this._query = String((e.target as ZnInput).value ?? '').trim().toLowerCase();
215
+ for (const item of this._items) {
216
+ item.hidden = this._query !== '' && !(item.textContent ?? '').toLowerCase().includes(this._query);
217
+ }
218
+ this._syncTabStops();
219
+ }
220
+
221
+ private _handleKeyDown(e: KeyboardEvent) {
222
+ const visible = this._visibleItems;
223
+ const current = visible.indexOf(e.target as ZnNavItem);
224
+ if (current === -1) return;
225
+
226
+ if (this.sortable && this.reorderController.handleKeyDown(e, this._key(visible[current]))) {
227
+ return;
228
+ }
229
+
230
+ let next: number;
231
+ switch (e.key) {
232
+ case 'ArrowDown':
233
+ next = Math.min(current + 1, visible.length - 1);
234
+ break;
235
+ case 'ArrowUp':
236
+ next = Math.max(current - 1, 0);
237
+ break;
238
+ case 'Home':
239
+ next = 0;
240
+ break;
241
+ case 'End':
242
+ next = visible.length - 1;
243
+ break;
244
+ default:
245
+ return;
246
+ }
247
+
248
+ e.preventDefault();
249
+ for (const item of this._items) {
250
+ item.tabIndex = item === visible[next] ? 0 : -1;
251
+ }
252
+ visible[next].focus();
253
+ }
254
+
255
+ private _handlePointerDown(e: PointerEvent) {
256
+ const key = this._eventKey(e);
257
+ if (this.sortable && key !== null) this.reorderController.handlePointerDown(e, key);
258
+ }
259
+
260
+ // --- Form control ---
261
+
262
+ checkValidity() {
263
+ return true;
264
+ }
265
+
266
+ getForm(): HTMLFormElement | null {
267
+ return this.formControlController.getForm();
268
+ }
269
+
270
+ reportValidity() {
271
+ return true;
272
+ }
273
+
274
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
275
+ setCustomValidity(_message = '') {
276
+ // Nothing to validate
277
+ }
278
+
279
+ render() {
280
+ const noMatches = this._query !== '' && this._visibleItems.length === 0;
281
+
282
+ return html`
283
+ <div part="base" class="nav-list">
284
+ ${this.caption || this.hasSlotController.test('actions') ? html`
285
+ <div part="header" class="nav-list__header">
286
+ <span class="nav-list__caption">${this.caption}</span>
287
+ <slot name="actions" class="nav-list__actions"></slot>
288
+ </div>` : ''}
289
+ ${this.searchable ? html`
290
+ <div part="search" class="nav-list__search">
291
+ <zn-input
292
+ type="search"
293
+ size="small"
294
+ clearable
295
+ placeholder="${this.searchPlaceholder}"
296
+ @zn-input="${this._handleSearch}"
297
+ @zn-clear="${this._handleSearch}">
298
+ </zn-input>
299
+ </div>` : ''}
300
+ <div
301
+ part="list"
302
+ class="nav-list__list"
303
+ role="tablist"
304
+ aria-orientation="vertical"
305
+ @keydown="${this._handleKeyDown}"
306
+ @pointerdown="${this._handlePointerDown}">
307
+ <slot @slotchange="${this._handleSlotChange}"></slot>
308
+ ${noMatches ? html`<p part="empty" class="nav-list__empty">${this.emptyLabel}</p>` : ''}
309
+ </div>
310
+ </div>
311
+ `;
312
+ }
313
+ }
@@ -0,0 +1,87 @@
1
+ @use "../../wc";
2
+
3
+ :host {
4
+ display: block;
5
+ width: var(--zn-nav-list-width, 272px);
6
+ max-width: 100%;
7
+ max-height: var(--zn-nav-list-max-height, 100%);
8
+ }
9
+
10
+ // zn-tabs stacks its side cards at this width; the cap keeps the detail in view
11
+ @container (max-width: 900px) {
12
+ :host {
13
+ width: 100%;
14
+ max-height: var(--zn-nav-list-max-height, 224px);
15
+ }
16
+ }
17
+
18
+ .nav-list {
19
+ display: flex;
20
+ flex-direction: column;
21
+ max-height: inherit;
22
+ overflow: hidden;
23
+ background-color: rgba(var(--zn-panel), var(--zn-panel-opacity));
24
+ border: 1px solid rgb(var(--zn-border-color));
25
+ border-radius: var(--zn-panel-border-radius, var(--zn-border-radius));
26
+ box-shadow: var(--zn-panel-shadow, none);
27
+ }
28
+
29
+ .nav-list__header {
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: space-between;
33
+ gap: var(--zn-spacing-small);
34
+ padding: var(--zn-panel-header-padding, var(--zn-spacing-small) var(--zn-spacing-medium));
35
+ border-bottom: 1px solid var(--zn-panel-header-divider-color, rgb(var(--zn-border-color)));
36
+ }
37
+
38
+ // Matches a zn-panel caption, so the card reads as a panel.
39
+ .nav-list__caption {
40
+ font-family: var(--zn-text-h1-font-family, inherit);
41
+ font-weight: var(--zn-text-h1-font-weight);
42
+ font-size: var(--zn-text-h1-font-size);
43
+ letter-spacing: var(--zn-text-h1-letter-spacing, normal);
44
+ color: rgba(var(--zn-text-panel-title), 100%);
45
+ line-height: var(--zn-icon-size, 20px);
46
+ }
47
+
48
+ .nav-list__actions {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: var(--zn-spacing-x-small);
52
+ }
53
+
54
+ .nav-list__search {
55
+ padding: var(--zn-spacing-small);
56
+ border-bottom: 1px solid var(--zn-panel-header-divider-color, rgb(var(--zn-border-color)));
57
+ }
58
+
59
+ .nav-list__list {
60
+ @include wc.scrollbars;
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 1px;
64
+ padding: var(--zn-spacing-2x-small);
65
+ overflow-y: auto;
66
+ }
67
+
68
+ :host([divide]) .nav-list__list {
69
+ gap: 0;
70
+ padding: 0;
71
+
72
+ ::slotted(zn-nav-item:not(:last-child)) {
73
+ border-bottom: 1px solid rgb(var(--zn-border-color));
74
+ }
75
+
76
+ // Hidden so it doesn't show below the placeholder's dashed outline
77
+ ::slotted(zn-nav-item[dragging]) {
78
+ border-bottom-color: transparent;
79
+ }
80
+ }
81
+
82
+ .nav-list__empty {
83
+ margin: 0;
84
+ padding: var(--zn-spacing-small) 10px;
85
+ font-size: var(--zn-font-size-small);
86
+ color: var(--zn-color-neutral-500);
87
+ }
@@ -0,0 +1,250 @@
1
+ import '../../../dist/zn.min.js';
2
+ import {aTimeout, expect, fixture, html} from '@open-wc/testing';
3
+ import {centre, drop, dragTo, startDrag} from '../../internal/test-drag';
4
+ import type ZnNavItem from '../nav-item';
5
+ import type ZnNavList from './nav-list.component';
6
+
7
+ const search = async (list: ZnNavList, value: string) => {
8
+ const input = list.shadowRoot!.querySelector('zn-input')!;
9
+ input.value = value;
10
+ input.dispatchEvent(new CustomEvent('zn-input', {bubbles: true, composed: true}));
11
+ await list.updateComplete;
12
+ };
13
+
14
+ describe('<zn-nav-list>', () => {
15
+ it('should render a component', async () => {
16
+ const el = await fixture(html`<zn-nav-list></zn-nav-list>`);
17
+ expect(el).to.exist;
18
+ });
19
+
20
+ it('should switch zn-tabs panels and mark the selected item', async () => {
21
+ const tabs = await fixture(html`
22
+ <zn-tabs gap>
23
+ <zn-nav-list slot="left">
24
+ <zn-nav-item tab="" count="4">Monthly plans</zn-nav-item>
25
+ <zn-nav-item tab="annual">Annual plans</zn-nav-item>
26
+ </zn-nav-list>
27
+ <div id="">Monthly contents</div>
28
+ <div id="annual">Annual contents</div>
29
+ </zn-tabs>
30
+ `);
31
+ await aTimeout(50);
32
+
33
+ const items = [...tabs.querySelectorAll<ZnNavItem>('zn-nav-item')];
34
+ expect(items.map(item => item.getAttribute('aria-selected'))).to.deep.equal(['true', 'false']);
35
+
36
+ items[1].click();
37
+ await aTimeout(50);
38
+
39
+ expect(items.map(item => item.getAttribute('aria-selected'))).to.deep.equal(['false', 'true']);
40
+ expect(items.map(item => item.tabIndex)).to.deep.equal([-1, 0]);
41
+ expect(tabs.querySelector('#annual')!.hasAttribute('selected')).to.be.true;
42
+ });
43
+
44
+ it('should render the count', async () => {
45
+ const el = await fixture<ZnNavItem>(html`<zn-nav-item count="12">Leads</zn-nav-item>`);
46
+ expect(el.shadowRoot!.querySelector('[part="count"]')!.textContent).to.equal('12');
47
+ });
48
+
49
+ it('should render the description', async () => {
50
+ const el = await fixture<ZnNavItem>(html`<zn-nav-item description="Wrap-up and follow-up">Closing</zn-nav-item>`);
51
+ expect(el.shadowRoot!.querySelector('[part="description"]')!.textContent).to.equal('Wrap-up and follow-up');
52
+ });
53
+
54
+ it('should mark its items when divided', async () => {
55
+ const list = await fixture<ZnNavList>(html`
56
+ <zn-nav-list divide>
57
+ <zn-nav-item>General</zn-nav-item>
58
+ <zn-nav-item>Closing</zn-nav-item>
59
+ </zn-nav-list>
60
+ `);
61
+ await list.updateComplete;
62
+ const items = [...list.querySelectorAll('zn-nav-item')];
63
+ expect(items.every(item => item.hasAttribute('divide'))).to.be.true;
64
+
65
+ list.divide = false;
66
+ await list.updateComplete;
67
+ expect(items.some(item => item.hasAttribute('divide'))).to.be.false;
68
+ });
69
+
70
+ it('should filter items by label and show the empty message', async () => {
71
+ const list = await fixture<ZnNavList>(html`
72
+ <zn-nav-list searchable>
73
+ <zn-nav-item>Monthly plans</zn-nav-item>
74
+ <zn-nav-item>Annual plans</zn-nav-item>
75
+ <zn-nav-item>Billing</zn-nav-item>
76
+ </zn-nav-list>
77
+ `);
78
+ const items = [...list.querySelectorAll<ZnNavItem>('zn-nav-item')];
79
+
80
+ await search(list, 'plans');
81
+ expect(items.map(item => item.hidden)).to.deep.equal([false, false, true]);
82
+ expect(items.map(item => item.tabIndex)).to.deep.equal([0, -1, -1]);
83
+
84
+ await search(list, 'nothing');
85
+ expect(list.shadowRoot!.querySelector('[part="empty"]')).to.exist;
86
+
87
+ await search(list, '');
88
+ expect(items.map(item => item.hidden)).to.deep.equal([false, false, false]);
89
+ });
90
+
91
+ it('should move focus with the arrow keys', async () => {
92
+ const list = await fixture<ZnNavList>(html`
93
+ <zn-nav-list>
94
+ <zn-nav-item>One</zn-nav-item>
95
+ <zn-nav-item>Two</zn-nav-item>
96
+ </zn-nav-list>
97
+ `);
98
+ const items = [...list.querySelectorAll<ZnNavItem>('zn-nav-item')];
99
+
100
+ items[0].focus();
101
+ items[0].dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', bubbles: true, composed: true}));
102
+
103
+ expect(document.activeElement).to.equal(items[1]);
104
+ expect(items.map(item => item.tabIndex)).to.deep.equal([-1, 0]);
105
+ });
106
+
107
+ describe('sortable', () => {
108
+ const order = (form: HTMLFormElement) =>
109
+ (JSON.parse(String(new FormData(form).get('order'))) as {key: string; priority: number}[])
110
+ .map(({key, priority}) => `${key}=${priority}`);
111
+
112
+ it('shows a caption and header actions', async () => {
113
+ const list = await fixture<ZnNavList>(html`
114
+ <zn-nav-list caption="Sections"><button slot="actions">Add</button></zn-nav-list>
115
+ `);
116
+ await list.updateComplete;
117
+
118
+ expect(list.shadowRoot!.querySelector('.nav-list__caption')!.textContent).to.equal('Sections');
119
+ expect(list.shadowRoot!.querySelector<HTMLSlotElement>('slot[name="actions"]')!.assignedElements()).to.have.length(1);
120
+ });
121
+
122
+ it('rounds a divided list placeholder into the card corner it moves into', async () => {
123
+ const list = await fixture<ZnNavList>(html`
124
+ <zn-nav-list divide sortable style="--zn-panel-border-radius: 12px">
125
+ <zn-nav-item value="a">A</zn-nav-item>
126
+ <zn-nav-item value="b">B</zn-nav-item>
127
+ <zn-nav-item value="c">C</zn-nav-item>
128
+ </zn-nav-list>
129
+ `);
130
+ await list.updateComplete;
131
+ const items = [...list.querySelectorAll<ZnNavItem>('zn-nav-item')];
132
+ const placeholderRadius = () => getComputedStyle(items[1].shadowRoot!.querySelector('.nav-item')!).borderRadius;
133
+
134
+ startDrag(items[1]);
135
+ await aTimeout(50);
136
+ expect(placeholderRadius()).to.equal('0px');
137
+
138
+ const last = items[2].getBoundingClientRect();
139
+ dragTo(centre(items[2]).x, last.bottom - 1);
140
+ await list.updateComplete;
141
+ expect(placeholderRadius()).to.equal('0px 0px 12px 12px');
142
+ window.dispatchEvent(new KeyboardEvent('keydown', {key: 'Escape'}));
143
+ drop(0, 0);
144
+ });
145
+
146
+ it('submits the order with the form and marks the items as sortable', async () => {
147
+ const form = await fixture<HTMLFormElement>(html`
148
+ <form>
149
+ <zn-nav-list sortable name="order">
150
+ <zn-nav-item tab="a" value="a">A</zn-nav-item>
151
+ <zn-nav-item tab="b" value="b">B</zn-nav-item>
152
+ </zn-nav-list>
153
+ </form>
154
+ `);
155
+ const list = form.querySelector<ZnNavList>('zn-nav-list')!;
156
+ await list.updateComplete;
157
+
158
+ const items = [...list.querySelectorAll<ZnNavItem>('zn-nav-item')];
159
+ expect(items.every(item => item.hasAttribute('sortable'))).to.be.true;
160
+ expect(order(form)).to.deep.equal(['a=1', 'b=2']);
161
+ expect(list.querySelector('input')).to.not.exist;
162
+ });
163
+
164
+ it('moves an item with Alt+Arrow keys and submits the new order', async () => {
165
+ const form = await fixture<HTMLFormElement>(html`
166
+ <form>
167
+ <zn-nav-list sortable name="order" formaction="/reorder">
168
+ <zn-nav-item tab="a" value="a">A</zn-nav-item>
169
+ <zn-nav-item tab="b" value="b">B</zn-nav-item>
170
+ </zn-nav-list>
171
+ </form>
172
+ `);
173
+ const list = form.querySelector<ZnNavList>('zn-nav-list')!;
174
+ await list.updateComplete;
175
+
176
+ let submittedTo = '';
177
+ let submitted: string[] = [];
178
+ form.addEventListener('submit', (e: SubmitEvent) => {
179
+ e.preventDefault();
180
+ submittedTo = e.submitter?.getAttribute('formaction') ?? '';
181
+ submitted = order(form);
182
+ });
183
+
184
+ const first = list.querySelector<ZnNavItem>('zn-nav-item[value="a"]')!;
185
+ first.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', altKey: true, bubbles: true, composed: true}));
186
+
187
+ expect([...list.querySelectorAll('zn-nav-item')].map(item => item.getAttribute('value'))).to.deep.equal(['b', 'a']);
188
+ expect(submittedTo).to.equal('/reorder');
189
+ expect(submitted).to.deep.equal(['b=1', 'a=2']);
190
+ });
191
+
192
+ it('reorders by drag and drop', async () => {
193
+ const list = await fixture<ZnNavList>(html`
194
+ <zn-nav-list sortable name="order">
195
+ <zn-nav-item value="a">A</zn-nav-item>
196
+ <zn-nav-item value="b">B</zn-nav-item>
197
+ </zn-nav-list>
198
+ `);
199
+ await list.updateComplete;
200
+ let reordered = 0;
201
+ list.addEventListener('zn-reorder', () => reordered++);
202
+
203
+ const [a, b] = [...list.querySelectorAll<ZnNavItem>('zn-nav-item')];
204
+ const rect = b.getBoundingClientRect();
205
+ startDrag(a);
206
+ dragTo(centre(b).x, rect.bottom - 1);
207
+ drop(centre(b).x, rect.bottom - 1);
208
+
209
+ expect([...list.querySelectorAll('zn-nav-item')].map(item => item.getAttribute('value'))).to.deep.equal(['b', 'a']);
210
+ expect(reordered).to.equal(1);
211
+ });
212
+
213
+ it('restores the order when let go outside the list', async () => {
214
+ const list = await fixture<ZnNavList>(html`
215
+ <zn-nav-list sortable name="order">
216
+ <zn-nav-item value="a">A</zn-nav-item>
217
+ <zn-nav-item value="b">B</zn-nav-item>
218
+ </zn-nav-list>
219
+ `);
220
+ await list.updateComplete;
221
+ let reordered = 0;
222
+ list.addEventListener('zn-reorder', () => reordered++);
223
+
224
+ const [a, b] = [...list.querySelectorAll<ZnNavItem>('zn-nav-item')];
225
+ startDrag(a);
226
+ dragTo(centre(b).x, b.getBoundingClientRect().bottom - 1);
227
+ expect([...list.querySelectorAll('zn-nav-item')].map(item => item.getAttribute('value'))).to.deep.equal(['b', 'a']);
228
+
229
+ const outside = list.getBoundingClientRect();
230
+ drop(outside.right + 50, outside.bottom + 50);
231
+ expect([...list.querySelectorAll('zn-nav-item')].map(item => item.getAttribute('value'))).to.deep.equal(['a', 'b']);
232
+ expect(reordered).to.equal(0);
233
+ });
234
+
235
+ it('leaves the order alone when not sortable', async () => {
236
+ const list = await fixture<ZnNavList>(html`
237
+ <zn-nav-list name="order">
238
+ <zn-nav-item value="a">A</zn-nav-item>
239
+ <zn-nav-item value="b">B</zn-nav-item>
240
+ </zn-nav-list>
241
+ `);
242
+ await list.updateComplete;
243
+ const first = list.querySelector<ZnNavItem>('zn-nav-item')!;
244
+ first.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowDown', altKey: true, bubbles: true, composed: true}));
245
+
246
+ expect([...list.querySelectorAll('zn-nav-item')].map(item => item.getAttribute('value'))).to.deep.equal(['a', 'b']);
247
+ expect(list.querySelector('input')).to.not.exist;
248
+ });
249
+ });
250
+ });