@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,632 @@
1
+ import {EASE_OUT, prefersReducedMotion} from './animate';
2
+ import type {FormControlController} from './form';
3
+ import type {ReactiveController, ReactiveControllerHost} from 'lit';
4
+
5
+ /** A component whose keyed items can be dragged, or moved by keyboard, into a new order. */
6
+ export interface ReorderHost extends ReactiveControllerHost, HTMLElement {
7
+ /** The item keys in their current order. */
8
+ reorderKeys: () => string[];
9
+
10
+ /** Shows the items in a new order. */
11
+ applyReorder: (keys: string[]) => void;
12
+
13
+ /** The row drawn for an item, measured to slide it from its old position. */
14
+ reorderRow: (key: string) => HTMLElement | null;
15
+
16
+ /** Every element that slides with an item, when more than its row does (a table column's cells). */
17
+ reorderSlides?: (key: string) => HTMLElement[];
18
+
19
+ /** Whether an item is pinned where it is; nothing can move past it either. */
20
+ reorderLocked?: (key: string) => boolean;
21
+
22
+ /** Where the pointer must be for a drag to land; letting go outside it cancels. Defaults to the rows' parent. */
23
+ reorderArea?: () => HTMLElement | null;
24
+
25
+ /** The element that follows the pointer while dragging. Defaults to a copy of the item's row. */
26
+ reorderGhost?: (key: string) => HTMLElement;
27
+
28
+ /** Submits the form after each reorder when set: to this URL, or the form's own action when empty. */
29
+ formAction?: string;
30
+
31
+ disabled?: boolean;
32
+ }
33
+
34
+ export interface ReorderOptions {
35
+ /** Submits through the host's form, which announces the form so a host page can intercept the submit. */
36
+ formControl?: FormControlController;
37
+
38
+ /** Fields for a temporary form when the host has a `formaction` but no form of its own. */
39
+ fallbackFields?: () => [string, string][];
40
+
41
+ /** Keys that move the focused item. With `alt`, plain arrows are left to navigate between items. */
42
+ modifier?: 'none' | 'alt';
43
+
44
+ /** Focuses an item after it moves by keyboard. */
45
+ focus?: (key: string) => void;
46
+
47
+ /** Which way the items run: arrow keys, the drag midpoint and the slide follow it. */
48
+ axis?: 'vertical' | 'horizontal';
49
+
50
+ /**
51
+ * Leaves the items in place while dragging and tracks `dropIndex` for the host to draw a drop line, moving the item
52
+ * only on drop. Suits items of very different sizes, which would jump under the pointer when moved live.
53
+ */
54
+ indicator?: boolean;
55
+ }
56
+
57
+ type ReorderEvent = 'zn-change' | 'zn-reorder';
58
+
59
+ interface Point {
60
+ x: number;
61
+ y: number;
62
+ }
63
+
64
+ // How far a mouse or pen moves before a press becomes a drag, so a click still clicks
65
+ const DRAG_THRESHOLD = 4;
66
+ // How long a touch holds still before it drags; moving sooner scrolls instead
67
+ const TOUCH_HOLD = 300;
68
+ // The band at a scroller's edge that scrolls it while dragging
69
+ const SCROLL_EDGE = 40;
70
+ const SCROLL_SPEED = 12;
71
+
72
+ /**
73
+ * Pointer and keyboard reordering for a keyed list, with the change announced through `zn-change` and `zn-reorder`
74
+ * and, when the host has a `formaction`, submitted with its form.
75
+ *
76
+ * Dragging runs on pointer events rather than native drag and drop, which hands the cursor to the browser for the
77
+ * whole drag.
78
+ */
79
+ export class ReorderController implements ReactiveController {
80
+ /** The item being dragged, styled as its placeholder. */
81
+ draggedKey: string | null = null;
82
+
83
+ /** Whether a drag is under way, for transitions while items shift. */
84
+ isDragging = false;
85
+
86
+ /** With `indicator`, the boundary the dragged item would drop at: before the item at this index, or after the last. */
87
+ dropIndex: number | null = null;
88
+
89
+ private readonly host: ReorderHost;
90
+ private readonly options: ReorderOptions;
91
+ private dragKey: string | null = null;
92
+ private startOrder: string[] = [];
93
+ private sliding = 0;
94
+ private listOverflow = '';
95
+ private press: {key: string; pointerId: number; touch: boolean; origin: Point; hold?: number} | null = null;
96
+ private point: Point = {x: 0, y: 0};
97
+ private ghost: HTMLElement | null = null;
98
+ private ghostOffset: Point = {x: 0, y: 0};
99
+ private shield: HTMLElement | null = null;
100
+ private scroller: Element | null = null;
101
+ private scrollFrame = 0;
102
+ private pressRoot: Node | null = null;
103
+
104
+ constructor(host: ReorderHost, options: ReorderOptions = {}) {
105
+ this.host = host;
106
+ this.options = options;
107
+ host.addController(this);
108
+ }
109
+
110
+ hostConnected() {
111
+ // Nothing to set up; the host wires pointerdown to its own rows.
112
+ }
113
+
114
+ hostDisconnected() {
115
+ if (this.isDragging) this.endDrag(true);
116
+ this.release();
117
+ }
118
+
119
+ private get horizontal() {
120
+ return this.options.axis === 'horizontal';
121
+ }
122
+
123
+ /** The leading edge of a row along the axis. */
124
+ private start(row: HTMLElement | null) {
125
+ const rect = row?.getBoundingClientRect();
126
+ return this.horizontal ? rect?.left : rect?.top;
127
+ }
128
+
129
+ private keys() {
130
+ return this.host.reorderKeys();
131
+ }
132
+
133
+ isLocked(key: string) {
134
+ return this.host.reorderLocked?.(key) ?? false;
135
+ }
136
+
137
+ /** A move is allowed when neither the moved item nor any item it would shift past is locked. */
138
+ canMove(fromIndex: number, toIndex: number) {
139
+ const keys = this.keys();
140
+ if (fromIndex < 0 || toIndex < 0 || fromIndex >= keys.length || toIndex >= keys.length) return false;
141
+ if (this.isLocked(keys[fromIndex])) return false;
142
+ const [low, high] = fromIndex < toIndex ? [fromIndex + 1, toIndex] : [toIndex, fromIndex - 1];
143
+ for (let i = low; i <= high; i++) {
144
+ if (this.isLocked(keys[i])) return false;
145
+ }
146
+ return true;
147
+ }
148
+
149
+ canStep(key: string, step: number) {
150
+ const index = this.keys().indexOf(key);
151
+ return index !== -1 && this.canMove(index, index + step);
152
+ }
153
+
154
+ /** Moves an item and announces the new order. */
155
+ move(fromIndex: number, toIndex: number) {
156
+ if (fromIndex === toIndex || !this.canMove(fromIndex, toIndex)) return false;
157
+ this.reorder(moved(this.keys(), fromIndex, toIndex));
158
+ this.commit();
159
+ return true;
160
+ }
161
+
162
+ step(key: string, step: number) {
163
+ const index = this.keys().indexOf(key);
164
+ return this.move(index, index + step);
165
+ }
166
+
167
+ /** Arrow keys move the item one place and Home/End to either end. Returns whether the key was handled. */
168
+ handleKeyDown(event: KeyboardEvent, key: string) {
169
+ if (this.host.disabled) return false;
170
+ if ((this.options.modifier ?? 'none') === 'alt' && !event.altKey) return false;
171
+
172
+ const keys = this.keys();
173
+ const index = keys.indexOf(key);
174
+ if (index === -1) return false;
175
+
176
+ const [back, forward] = this.horizontal ? ['ArrowLeft', 'ArrowRight'] : ['ArrowUp', 'ArrowDown'];
177
+ const target = {
178
+ [back]: index - 1,
179
+ [forward]: index + 1,
180
+ Home: 0,
181
+ End: keys.length - 1,
182
+ }[event.key];
183
+ if (target === undefined || target === index || target < 0 || target >= keys.length) return false;
184
+
185
+ event.preventDefault();
186
+ if (this.move(index, target)) {
187
+ void this.host.updateComplete.then(() => this.options.focus?.(key));
188
+ }
189
+ return true;
190
+ }
191
+
192
+ /** Starts tracking a press on an item; it becomes a drag once the pointer moves, or a touch holds still. */
193
+ handlePointerDown(event: PointerEvent, key: string) {
194
+ if (event.button !== 0 || this.press || this.isDragging) return;
195
+ if (this.host.disabled || this.isLocked(key) || !this.host.reorderRow(key)) return;
196
+ if (event.composedPath().some(el => el instanceof HTMLElement && el.matches('input, textarea, select, [contenteditable]'))) return;
197
+
198
+ const touch = event.pointerType === 'touch';
199
+ this.point = {x: event.clientX, y: event.clientY};
200
+ this.press = {key, pointerId: event.pointerId, touch, origin: this.point};
201
+ if (touch) this.press.hold = window.setTimeout(() => this.beginDrag(), TOUCH_HOLD);
202
+
203
+ window.addEventListener('pointermove', this.onPointerMove, true);
204
+ window.addEventListener('pointerup', this.onPointerUp, true);
205
+ window.addEventListener('pointercancel', this.onPointerCancel, true);
206
+ window.addEventListener('touchmove', this.onTouchMove, {capture: true, passive: false});
207
+ // A link or image in the row would otherwise start a native drag and take the pointer
208
+ window.addEventListener('dragstart', this.cancel, true);
209
+ // The press would otherwise also start a text selection that sweeps the page as it drags; selectstart isn't
210
+ // composed, so it's caught in the row's own tree
211
+ this.pressRoot = this.host.reorderRow(key)!.getRootNode();
212
+ this.pressRoot.addEventListener('selectstart', this.cancel, true);
213
+ }
214
+
215
+ private onPointerMove = (event: PointerEvent) => {
216
+ if (event.pointerId !== this.press?.pointerId) return;
217
+ this.point = {x: event.clientX, y: event.clientY};
218
+
219
+ if (!this.isDragging) {
220
+ const {origin, touch} = this.press;
221
+ if (Math.hypot(this.point.x - origin.x, this.point.y - origin.y) < DRAG_THRESHOLD) return;
222
+ // A touch that moves before the hold is a scroll
223
+ if (touch) this.release();
224
+ else this.beginDrag();
225
+ return;
226
+ }
227
+
228
+ this.placeGhost();
229
+ this.dragOver();
230
+ };
231
+
232
+ private onPointerUp = (event: PointerEvent) => {
233
+ if (event.pointerId !== this.press?.pointerId) return;
234
+ this.point = {x: event.clientX, y: event.clientY};
235
+ if (this.isDragging) {
236
+ this.endDrag(!this.inArea());
237
+ // The release still clicks, which would sort a column or select an item
238
+ window.addEventListener('click', swallow, {capture: true, once: true});
239
+ setTimeout(() => window.removeEventListener('click', swallow, true));
240
+ }
241
+ this.release();
242
+ };
243
+
244
+ private onPointerCancel = (event: PointerEvent) => {
245
+ if (event.pointerId !== this.press?.pointerId) return;
246
+ if (this.isDragging) this.endDrag(true);
247
+ this.release();
248
+ };
249
+
250
+ private onTouchMove = (event: TouchEvent) => {
251
+ if (this.isDragging) event.preventDefault();
252
+ };
253
+
254
+ private cancel = (event: Event) => {
255
+ event.preventDefault();
256
+ };
257
+
258
+ private onKeyDown = (event: KeyboardEvent) => {
259
+ if (event.key !== 'Escape') return;
260
+ event.preventDefault();
261
+ event.stopPropagation();
262
+ this.endDrag(true);
263
+ this.release();
264
+ };
265
+
266
+ private beginDrag() {
267
+ const key = this.press?.key;
268
+ const row = key ? this.host.reorderRow(key) : null;
269
+ if (!key || !row) {
270
+ this.release();
271
+ return;
272
+ }
273
+ window.clearTimeout(this.press!.hold);
274
+
275
+ const rect = row.getBoundingClientRect();
276
+ const custom = this.host.reorderGhost?.(key);
277
+ const ghost = custom ?? snapshot(row);
278
+ this.ghostOffset = custom ? {x: 12, y: 16} : {x: this.point.x - rect.left, y: this.point.y - rect.top};
279
+ Object.assign(ghost.style, {
280
+ position: 'fixed',
281
+ left: '0',
282
+ top: '0',
283
+ margin: '0',
284
+ zIndex: '2147483647',
285
+ pointerEvents: 'none',
286
+ });
287
+ if (!custom) {
288
+ Object.assign(ghost.style, {
289
+ width: `${rect.width}px`,
290
+ height: `${rect.height}px`,
291
+ boxSizing: 'border-box',
292
+ background: 'rgb(var(--zn-panel, 255, 255, 255))',
293
+ boxShadow: 'var(--zn-shadow-large, 0 8px 24px rgba(13, 23, 56, 0.16))',
294
+ borderRadius: getComputedStyle(row).borderRadius,
295
+ opacity: '0.95',
296
+ });
297
+ }
298
+ ghost.inert = true;
299
+
300
+ // Covers the page while dragging, so the cursor stays a closed hand over everything and nothing else reacts
301
+ const shield = document.createElement('div');
302
+ Object.assign(shield.style, {position: 'fixed', inset: '0', zIndex: '2147483646', cursor: 'grabbing', touchAction: 'none'});
303
+
304
+ // Inside the row's own tree so its styles reach the copy, and a modal dialog around it doesn't make it inert
305
+ const root = row.getRootNode();
306
+ const container = root instanceof ShadowRoot ? root : document.body;
307
+ container.append(shield, ghost);
308
+ this.ghost = ghost;
309
+ this.shield = shield;
310
+ this.placeGhost();
311
+
312
+ getSelection()?.removeAllRanges();
313
+ this.dragKey = key;
314
+ this.draggedKey = key;
315
+ this.startOrder = this.keys();
316
+ this.isDragging = true;
317
+ this.scroller = scrollerOf(this.area(), this.horizontal);
318
+ this.scrollFrame = requestAnimationFrame(this.autoScroll);
319
+ window.addEventListener('keydown', this.onKeyDown, true);
320
+ this.host.requestUpdate();
321
+ }
322
+
323
+ private placeGhost() {
324
+ if (!this.ghost) return;
325
+ this.ghost.style.transform = `translate(${this.point.x - this.ghostOffset.x}px, ${this.point.y - this.ghostOffset.y}px)`;
326
+ }
327
+
328
+ private area() {
329
+ return this.host.reorderArea?.() ?? this.host.reorderRow(this.keys()[0])?.parentElement ?? null;
330
+ }
331
+
332
+ private inArea() {
333
+ const rect = this.area()?.getBoundingClientRect();
334
+ return !!rect && this.point.x >= rect.left && this.point.x <= rect.right && this.point.y >= rect.top && this.point.y <= rect.bottom;
335
+ }
336
+
337
+ // The item whose span along the axis holds the pointer; none in a gap between items or outside the list
338
+ private hoveredKey() {
339
+ if (!this.inArea()) return null;
340
+ const at = this.horizontal ? this.point.x : this.point.y;
341
+ return this.keys().find(key => {
342
+ const rect = this.host.reorderRow(key)?.getBoundingClientRect();
343
+ if (!rect) return false;
344
+ return this.horizontal ? at >= rect.left && at < rect.right : at >= rect.top && at < rect.bottom;
345
+ }) ?? null;
346
+ }
347
+
348
+ /**
349
+ * Moves the dragged item to the hovered one's place, once the pointer crosses that item's midpoint. In a gap
350
+ * between items nothing moves, and letting go there keeps the order so far.
351
+ */
352
+ private dragOver() {
353
+ const key = this.hoveredKey();
354
+ if (this.options.indicator) {
355
+ this.trackDropIndex(key);
356
+ return;
357
+ }
358
+ if (!key || !this.dragKey || this.dragKey === key) return;
359
+
360
+ const keys = this.keys();
361
+ const fromIndex = keys.indexOf(this.dragKey);
362
+ const toIndex = keys.indexOf(key);
363
+ if (fromIndex === -1 || toIndex === -1) return;
364
+
365
+ // Rows of different sizes would otherwise flip back and forth
366
+ if (fromIndex < toIndex !== this.pastMidpoint(key)) return;
367
+ if (!this.canMove(fromIndex, toIndex)) return;
368
+
369
+ this.reorder(moved(keys, fromIndex, toIndex));
370
+ }
371
+
372
+ private pastMidpoint(key: string) {
373
+ const rect = this.host.reorderRow(key)!.getBoundingClientRect();
374
+ return this.horizontal
375
+ ? this.point.x > rect.left + rect.width / 2
376
+ : this.point.y > rect.top + rect.height / 2;
377
+ }
378
+
379
+ // The nearest boundary of the hovered item; a gap between items keeps the last one, and leaving the list clears it
380
+ private trackDropIndex(key: string | null) {
381
+ if (!this.dragKey) return;
382
+ if (!key) {
383
+ if (this.inArea()) return;
384
+ this.setDropIndex(null);
385
+ return;
386
+ }
387
+
388
+ const keys = this.keys();
389
+ const fromIndex = keys.indexOf(this.dragKey);
390
+ const index = keys.indexOf(key);
391
+ if (fromIndex === -1 || index === -1) return;
392
+
393
+ const boundary = index + (this.pastMidpoint(key) ? 1 : 0);
394
+ const toIndex = this.dropTarget(fromIndex, boundary);
395
+ this.setDropIndex(toIndex !== fromIndex && this.canMove(fromIndex, toIndex) ? boundary : null);
396
+ }
397
+
398
+ private setDropIndex(dropIndex: number | null) {
399
+ if (dropIndex === this.dropIndex) return;
400
+ this.dropIndex = dropIndex;
401
+ this.host.requestUpdate();
402
+ }
403
+
404
+ // The index the item moves to once taken out from before the boundary
405
+ private dropTarget(fromIndex: number, boundary: number) {
406
+ return boundary > fromIndex ? boundary - 1 : boundary;
407
+ }
408
+
409
+ // Scrolls the list's scroller while the pointer sits in the band at either end of it
410
+ private autoScroll = () => {
411
+ if (!this.isDragging) return;
412
+ this.scrollFrame = requestAnimationFrame(this.autoScroll);
413
+
414
+ const scroller = this.scroller;
415
+ if (!scroller) return;
416
+ const page = scroller === document.scrollingElement;
417
+ const rect = page
418
+ ? {left: 0, top: 0, right: innerWidth, bottom: innerHeight}
419
+ : scroller.getBoundingClientRect();
420
+ const [at, start, end] = this.horizontal
421
+ ? [this.point.x, rect.left, rect.right]
422
+ : [this.point.y, rect.top, rect.bottom];
423
+
424
+ const into = at < start + SCROLL_EDGE ? at - (start + SCROLL_EDGE) : at > end - SCROLL_EDGE ? at - (end - SCROLL_EDGE) : 0;
425
+ if (!into) return;
426
+ const by = Math.max(-1, Math.min(1, into / SCROLL_EDGE)) * SCROLL_SPEED;
427
+ const before = this.horizontal ? scroller.scrollLeft : scroller.scrollTop;
428
+ scroller.scrollBy(this.horizontal ? {left: by} : {top: by});
429
+ if ((this.horizontal ? scroller.scrollLeft : scroller.scrollTop) !== before) this.dragOver();
430
+ };
431
+
432
+ private endDrag(cancel: boolean) {
433
+ if (!this.isDragging) return;
434
+
435
+ if (!cancel && this.options.indicator && this.dropIndex !== null && this.dragKey) {
436
+ const fromIndex = this.keys().indexOf(this.dragKey);
437
+ this.reorder(moved(this.keys(), fromIndex, this.dropTarget(fromIndex, this.dropIndex)));
438
+ }
439
+
440
+ const startOrder = this.startOrder;
441
+ this.dragKey = null;
442
+ this.draggedKey = null;
443
+ this.dropIndex = null;
444
+ this.isDragging = false;
445
+ cancelAnimationFrame(this.scrollFrame);
446
+ window.removeEventListener('keydown', this.onKeyDown, true);
447
+ this.ghost?.remove();
448
+ this.shield?.remove();
449
+ this.ghost = null;
450
+ this.shield = null;
451
+ this.scroller = null;
452
+ this.host.requestUpdate();
453
+
454
+ const changed = startOrder.join('\n') !== this.keys().join('\n');
455
+ if (!changed) return;
456
+ if (cancel) {
457
+ this.reorder(startOrder);
458
+ return;
459
+ }
460
+ this.commit();
461
+ }
462
+
463
+ // Stops tracking the press, whether or not it became a drag
464
+ private release() {
465
+ if (this.press) window.clearTimeout(this.press.hold);
466
+ this.press = null;
467
+ window.removeEventListener('pointermove', this.onPointerMove, true);
468
+ window.removeEventListener('pointerup', this.onPointerUp, true);
469
+ window.removeEventListener('pointercancel', this.onPointerCancel, true);
470
+ window.removeEventListener('touchmove', this.onTouchMove, true);
471
+ window.removeEventListener('dragstart', this.cancel, true);
472
+ this.pressRoot?.removeEventListener('selectstart', this.cancel, true);
473
+ this.pressRoot = null;
474
+ }
475
+
476
+ /** Shows the new order, sliding each row from its old position. */
477
+ reorder(keys: string[]) {
478
+ const before = new Map(this.keys().map(key => [key, this.start(this.host.reorderRow(key))]));
479
+ this.host.applyReorder(keys);
480
+
481
+ if (prefersReducedMotion()) return;
482
+ void this.host.updateComplete.then(() => {
483
+ const slides: Animation[] = [];
484
+ for (const key of keys) {
485
+ const row = this.host.reorderRow(key);
486
+ const start = before.get(key);
487
+ if (!row || start === undefined) continue;
488
+ const delta = start - this.start(row)!;
489
+ if (!delta) continue;
490
+ const from = this.horizontal ? `translateX(${delta}px)` : `translateY(${delta}px)`;
491
+ for (const el of this.host.reorderSlides?.(key) ?? [row]) {
492
+ slides.push(el.animate([{transform: from}, {transform: 'none'}], {duration: 150, easing: EASE_OUT}));
493
+ }
494
+ }
495
+ this.clipWhileSliding(this.host.reorderRow(keys[0])?.parentElement, slides);
496
+ });
497
+ }
498
+
499
+ // A row can change size with its place (a flush list's last row has no border), so it pokes out of the list
500
+ // mid-slide and flashes a scrollbar on the scroller around it
501
+ private clipWhileSliding(list: HTMLElement | null | undefined, slides: Animation[]) {
502
+ if (!list || !slides.length) return;
503
+ if (this.sliding++ === 0) {
504
+ this.listOverflow = list.style.overflow;
505
+ list.style.overflow = 'clip';
506
+ }
507
+ void Promise.allSettled(slides.map(slide => slide.finished)).then(() => {
508
+ if (--this.sliding === 0) list.style.overflow = this.listOverflow;
509
+ });
510
+ }
511
+
512
+ private commit() {
513
+ this.emit('zn-change');
514
+ this.emit('zn-reorder');
515
+ this.submit();
516
+ }
517
+
518
+ private emit(name: ReorderEvent) {
519
+ this.host.dispatchEvent(new CustomEvent(name, {bubbles: true, composed: true}));
520
+ }
521
+
522
+ /** Submits the host's form when it has a `formaction`. */
523
+ private submit() {
524
+ const formAction = this.host.formAction;
525
+ if (formAction === undefined || formAction === null) return;
526
+
527
+ const formControl = this.options.formControl;
528
+ if (formControl?.getForm()) {
529
+ // Submitting through the controller announces the form before it is submitted, which is what lets a host
530
+ // page intercept the submit: a submit event raised on a form inside a shadow root never reaches the document.
531
+ const submitter = document.createElement('input');
532
+ submitter.type = 'submit';
533
+ if (formAction) {
534
+ submitter.setAttribute('formaction', formAction);
535
+ }
536
+ formControl.submit(submitter);
537
+ return;
538
+ }
539
+
540
+ if (!formAction) return;
541
+ const form = document.createElement('form');
542
+ form.method = 'post';
543
+ form.action = formAction;
544
+ form.hidden = true;
545
+ for (const [name, value] of this.options.fallbackFields?.() ?? []) {
546
+ const input = document.createElement('input');
547
+ input.type = 'hidden';
548
+ input.name = name;
549
+ input.value = value;
550
+ form.append(input);
551
+ }
552
+ document.body.append(form);
553
+ // requestSubmit raises a cancellable submit event, so a host page can handle it rather than navigating
554
+ form.requestSubmit();
555
+ form.remove();
556
+ }
557
+ }
558
+
559
+ function swallow(event: Event) {
560
+ event.preventDefault();
561
+ event.stopPropagation();
562
+ }
563
+
564
+ /** A copy of a row to drag, with each slot replaced by what it shows, as a copy outside the slot's host shows nothing. */
565
+ function snapshot(row: HTMLElement) {
566
+ const copy = row.cloneNode(true) as HTMLElement;
567
+ const slots = row.querySelectorAll('slot');
568
+ const copies = copy.querySelectorAll('slot');
569
+ slots.forEach((slot, i) => copies[i].replaceWith(...slot.assignedNodes({flatten: true}).map(node => node.cloneNode(true))));
570
+ copy.removeAttribute('id');
571
+ return copy;
572
+ }
573
+
574
+ /** The nearest element that scrolls along the axis, crossing shadow boundaries, or the page. */
575
+ function scrollerOf(from: Element | null, horizontal: boolean): Element | null {
576
+ for (let el: Element | null = from; el; el = el.parentElement ?? ((el.getRootNode() as ShadowRoot).host ?? null)) {
577
+ const style = getComputedStyle(el);
578
+ const overflow = horizontal ? style.overflowX : style.overflowY;
579
+ const scrolls = horizontal ? el.scrollWidth > el.clientWidth : el.scrollHeight > el.clientHeight;
580
+ if (scrolls && (overflow === 'auto' || overflow === 'scroll')) return el;
581
+ }
582
+ return document.scrollingElement;
583
+ }
584
+
585
+ function moved(keys: string[], fromIndex: number, toIndex: number) {
586
+ const order = [...keys];
587
+ const [item] = order.splice(fromIndex, 1);
588
+ order.splice(toIndex, 0, item);
589
+ return order;
590
+ }
591
+
592
+ /**
593
+ * The border radius that rounds a row's corners where it meets the corners of the nearest rounded ancestor of `host`
594
+ * that clips it (a flush list in a panel), so a drag placeholder's dashed outline follows the clip there instead of
595
+ * being cut off. Null when nothing rounded clips the row.
596
+ */
597
+ export function clipCornerRadius(host: Element, row: HTMLElement): string | null {
598
+ const parent = (el: Element): Element | null =>
599
+ el.assignedSlot ?? el.parentElement ?? (el.getRootNode() as ShadowRoot).host ?? null;
600
+
601
+ let clip: Element | null = null;
602
+ for (let el = parent(host); el; el = parent(el)) {
603
+ const style = getComputedStyle(el);
604
+ if (style.overflow !== 'visible' && style.borderRadius !== '0px') {
605
+ clip = el;
606
+ break;
607
+ }
608
+ }
609
+ if (!clip) return null;
610
+
611
+ const style = getComputedStyle(clip);
612
+ const outer = clip.getBoundingClientRect();
613
+ const bl = parseFloat(style.borderLeftWidth);
614
+ const br = parseFloat(style.borderRightWidth);
615
+ const bt = parseFloat(style.borderTopWidth);
616
+ const bb = parseFloat(style.borderBottomWidth);
617
+ const r = row.getBoundingClientRect();
618
+ const top = Math.abs(r.top - (outer.top + bt)) < 1;
619
+ const bottom = Math.abs(r.bottom - (outer.bottom - bb)) < 1;
620
+ const left = Math.abs(r.left - (outer.left + bl)) < 1;
621
+ const right = Math.abs(r.right - (outer.right - br)) < 1;
622
+ // The clip follows the padding edge, whose radius is the border radius less the border width
623
+ const corner = (touches: boolean, radius: string, border: number) =>
624
+ touches ? `${Math.max(0, parseFloat(radius) - border)}px` : '0px';
625
+
626
+ return [
627
+ corner(top && left, style.borderTopLeftRadius, Math.max(bt, bl)),
628
+ corner(top && right, style.borderTopRightRadius, Math.max(bt, br)),
629
+ corner(bottom && right, style.borderBottomRightRadius, Math.max(bb, br)),
630
+ corner(bottom && left, style.borderBottomLeftRadius, Math.max(bb, bl)),
631
+ ].join(' ');
632
+ }
@@ -86,9 +86,57 @@ export function getScrolledAncestors(element: Element | null | undefined): Eleme
86
86
  return scrolled;
87
87
  }
88
88
 
89
- /** Returns containers to the top. */
90
- export function scrollToTop(containers: Iterable<Element>, behavior: ScrollBehavior = 'auto') {
89
+ // A display: contents host has no box of its own, so it starts where its first rendered part does.
90
+ function topOf(host: HTMLElement): number {
91
+ const rect = host.getBoundingClientRect();
92
+ if (rect.height > 0) {
93
+ return rect.top;
94
+ }
95
+
96
+ const parts = Array.from(host.shadowRoot?.children ?? []).flatMap(child =>
97
+ child instanceof HTMLSlotElement ? child.assignedElements() : [child]
98
+ );
99
+ const tops = parts.map(part => part.getBoundingClientRect()).filter(box => box.height > 0).map(box => box.top);
100
+ return tops.length ? Math.min(...tops) : Infinity;
101
+ }
102
+
103
+ /**
104
+ * Shows a newly selected tab from its start. Scrollers inside `host` return to their top; scrollers around it only
105
+ * scroll back far enough to show the top of `host`, leaving the reader's place alone while the tabs are in view.
106
+ */
107
+ export function revealTop(containers: Iterable<Element>, host: HTMLElement) {
91
108
  for (const container of containers) {
92
- container.scrollTo({ top: 0, behavior });
109
+ if (host.shadowRoot?.contains(container)) {
110
+ container.scrollTo({ top: 0 });
111
+ continue;
112
+ }
113
+
114
+ // Measured per scroller, as scrolling an inner one moves the tabs
115
+ const top = topOf(host);
116
+ const viewTop = container === document.scrollingElement ? 0 : container.getBoundingClientRect().top;
117
+ if (top < viewTop) {
118
+ container.scrollBy({ top: top - viewTop });
119
+ }
120
+ }
121
+ }
122
+
123
+ /** Scrolls each scroller around `element`, innermost first, just far enough to show its bottom edge. */
124
+ export function revealBottom(element: Element) {
125
+ let node: Node | null = element.parentNode;
126
+
127
+ while (node) {
128
+ if (node instanceof Element && node.scrollHeight > node.clientHeight
129
+ && (node === document.scrollingElement || /auto|scroll/.test(getComputedStyle(node).overflowY))) {
130
+ // Measured per scroller, as scrolling an inner one moves the element
131
+ const viewBottom = node === document.scrollingElement
132
+ ? window.innerHeight
133
+ : node.getBoundingClientRect().top + node.clientTop + node.clientHeight;
134
+ const overflow = element.getBoundingClientRect().bottom - viewBottom;
135
+ if (overflow > 0) {
136
+ node.scrollBy({top: overflow});
137
+ }
138
+ }
139
+ // Through slots too, as a host's scroller sits in its shadow root around the slotted content
140
+ node = node instanceof ShadowRoot ? node.host : (node as Element).assignedSlot ?? node.parentNode;
93
141
  }
94
142
  }