@ds-mo/ui 15.5.1 → 15.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +1 -1
  3. package/dist/agent.json +46 -21
  4. package/dist/angular/ds-data-preferences.d.ts +1 -1
  5. package/dist/angular/ds-data-preferences.js +3 -3
  6. package/dist/angular/ds-menu.d.ts +7 -1
  7. package/dist/angular/ds-menu.js +5 -2
  8. package/dist/angular/ds-table.d.ts +1 -1
  9. package/dist/angular/ds-table.js +3 -3
  10. package/dist/components/THIRD-PARTY-NOTICES +1 -1
  11. package/dist/components/bundled-dependencies.json +1 -1
  12. package/dist/components/ds-agent-questionnaire.js +1 -1
  13. package/dist/components/ds-agent-response.js +1 -1
  14. package/dist/components/ds-banner.js +1 -1
  15. package/dist/components/ds-bar-nav.js +1 -1
  16. package/dist/components/ds-bar-page-title.js +1 -1
  17. package/dist/components/ds-bar-title.js +1 -1
  18. package/dist/components/ds-bar-workflow.js +1 -1
  19. package/dist/components/ds-button-unfilled.js +1 -1
  20. package/dist/components/ds-calendar.js +1 -1
  21. package/dist/components/ds-card-chart.js +1 -1
  22. package/dist/components/ds-card-overview.js +1 -1
  23. package/dist/components/ds-card-setting.js +1 -1
  24. package/dist/components/ds-code-block.js +1 -1
  25. package/dist/components/ds-data-customize.js +1 -1
  26. package/dist/components/ds-data-filter.js +1 -1
  27. package/dist/components/ds-data-group.js +1 -1
  28. package/dist/components/ds-data-preferences.js +1 -1
  29. package/dist/components/ds-data-preferences.js.map +1 -1
  30. package/dist/components/ds-data-saved-views.js +1 -1
  31. package/dist/components/ds-data-search.js +1 -1
  32. package/dist/components/ds-data-search.js.map +1 -1
  33. package/dist/components/ds-data-sort.js +1 -1
  34. package/dist/components/ds-filter-menu.js +1 -1
  35. package/dist/components/ds-input-date.js +1 -1
  36. package/dist/components/ds-input-time.js +1 -1
  37. package/dist/components/ds-input.js +1 -1
  38. package/dist/components/ds-markdown.js +1 -1
  39. package/dist/components/ds-menu.js +1 -1
  40. package/dist/components/ds-message-actions.js +1 -1
  41. package/dist/components/ds-message-composer.js +1 -1
  42. package/dist/components/ds-message-scroller.js +1 -1
  43. package/dist/components/ds-mobile-header.js +1 -1
  44. package/dist/components/ds-mobile-section-switcher.js +1 -1
  45. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  46. package/dist/components/ds-modal.js +1 -1
  47. package/dist/components/ds-pagination.js +1 -1
  48. package/dist/components/ds-panel-nav.js +1 -1
  49. package/dist/components/ds-panel-tool-header.js +1 -1
  50. package/dist/components/ds-panel-tool-search.js +1 -1
  51. package/dist/components/ds-panel-tools.js +1 -1
  52. package/dist/components/ds-select.js +1 -1
  53. package/dist/components/ds-shell-app.js +1 -1
  54. package/dist/components/ds-shell-tools.js +1 -1
  55. package/dist/components/ds-table.js +1 -1
  56. package/dist/components/ds-table.js.map +1 -1
  57. package/dist/components/ds-toast.js +1 -1
  58. package/dist/components/ds-toast.js.map +1 -1
  59. package/dist/components/ds-tooltip.js +1 -1
  60. package/dist/components/{p-sDm8BEMy.js → p-B-pkoIX9.js} +2 -2
  61. package/dist/components/{p-DYsRXun3.js → p-B3o9zn3Y.js} +2 -2
  62. package/dist/components/p-BR9Pjp15.js +2 -0
  63. package/dist/components/{p-CXlklL-0.js.map → p-BR9Pjp15.js.map} +1 -1
  64. package/dist/components/{p-CcwxySms.js → p-BZY40-pE.js} +2 -2
  65. package/dist/components/{p-BLeHHoVb.js → p-BqipGGua.js} +2 -2
  66. package/dist/components/{p-BuDxlhnQ.js → p-C6kUn-vb.js} +2 -2
  67. package/dist/components/p-CElsKlxj.js +2 -0
  68. package/dist/components/{p-CzdE3M3g.js.map → p-CElsKlxj.js.map} +1 -1
  69. package/dist/components/p-CIMyD_6H.js +2 -0
  70. package/dist/components/{p-C5BBIDVm.js.map → p-CIMyD_6H.js.map} +1 -1
  71. package/dist/components/{p-C35Tki38.js → p-CSxwWcwW.js} +2 -2
  72. package/dist/components/p-CUYLGe5d.js +2 -0
  73. package/dist/components/{p-D9_iDlkT.js.map → p-CUYLGe5d.js.map} +1 -1
  74. package/dist/components/{p-BlbfbYg6.js → p-CaFXrn_8.js} +2 -2
  75. package/dist/components/{p-DZLtFn-r.js → p-DObhRLBa.js} +2 -2
  76. package/dist/components/{p-bQE1NZVe.js → p-DP07JCed.js} +2 -2
  77. package/dist/components/p-DYGllrVT.js +2 -0
  78. package/dist/components/{p-CNeDJTGX.js.map → p-DYGllrVT.js.map} +1 -1
  79. package/dist/components/{p-C378wwRj.js → p-DYtVsnDS.js} +2 -2
  80. package/dist/components/p-DbW8S5qe.js +2 -0
  81. package/dist/components/p-DbW8S5qe.js.map +1 -0
  82. package/dist/components/p-DcxY_pdJ.js +2 -0
  83. package/dist/components/{p-Df9S0aWi.js.map → p-DcxY_pdJ.js.map} +1 -1
  84. package/dist/components/{p-C9H-3JOA.js → p-DdFeWC-e.js} +2 -2
  85. package/dist/components/p-DlCLfwP4.js +2 -0
  86. package/dist/components/p-DuCfCvPs.js +2 -0
  87. package/dist/components/p-DuCfCvPs.js.map +1 -0
  88. package/dist/components/{p-BYrBU_U9.js → p-HMDczkQf.js} +2 -2
  89. package/dist/components/p-HfZ1gI2q.js +2 -0
  90. package/dist/components/{p-Da3aFRk2.js.map → p-HfZ1gI2q.js.map} +1 -1
  91. package/dist/components/{p--VQc5GDy.js → p-KA8srEed.js} +2 -2
  92. package/dist/components/{p-BfVMaZZE.js → p-OJ5WeR05.js} +2 -2
  93. package/dist/components/p-Ouk6FCxc.js +2 -0
  94. package/dist/components/{p-a-cIN_BX.js.map → p-Ouk6FCxc.js.map} +1 -1
  95. package/dist/components/p-leZFu-cd.js +2 -0
  96. package/dist/components/{p-BuGlC5r8.js.map → p-leZFu-cd.js.map} +1 -1
  97. package/dist/components/{p-DPwptvZl.js → p-wXKIVUTn.js} +2 -2
  98. package/dist/components/p-xMBaOeKa.js +2 -0
  99. package/dist/components/p-xMBaOeKa.js.map +1 -0
  100. package/dist/docs/components.json +207 -29
  101. package/dist/lint/index.js +1 -1
  102. package/dist/mcp-data/registry/agent-activity.json +1 -1
  103. package/dist/mcp-data/registry/agent-questionnaire.json +1 -1
  104. package/dist/mcp-data/registry/agent-response.json +1 -1
  105. package/dist/mcp-data/registry/agent-source-list.json +1 -1
  106. package/dist/mcp-data/registry/agent-tool-call.json +1 -1
  107. package/dist/mcp-data/registry/attachment-list.json +1 -1
  108. package/dist/mcp-data/registry/avatar.json +1 -1
  109. package/dist/mcp-data/registry/badge.json +1 -1
  110. package/dist/mcp-data/registry/banner.json +1 -1
  111. package/dist/mcp-data/registry/bar-action.json +1 -1
  112. package/dist/mcp-data/registry/bar-nav.json +1 -1
  113. package/dist/mcp-data/registry/bar-page-title.json +1 -1
  114. package/dist/mcp-data/registry/bar-title.json +1 -1
  115. package/dist/mcp-data/registry/bar-workflow.json +1 -1
  116. package/dist/mcp-data/registry/breadcrumb.json +1 -1
  117. package/dist/mcp-data/registry/button-filled.json +1 -1
  118. package/dist/mcp-data/registry/button-inverted.json +1 -1
  119. package/dist/mcp-data/registry/button-unfilled.json +1 -1
  120. package/dist/mcp-data/registry/calendar.json +1 -1
  121. package/dist/mcp-data/registry/card-action-center.json +1 -1
  122. package/dist/mcp-data/registry/card-chart.json +1 -1
  123. package/dist/mcp-data/registry/card-navigation.json +1 -1
  124. package/dist/mcp-data/registry/card-overview.json +1 -1
  125. package/dist/mcp-data/registry/card-setting.json +1 -1
  126. package/dist/mcp-data/registry/card-settings-scope.json +1 -1
  127. package/dist/mcp-data/registry/chart-legend.json +1 -1
  128. package/dist/mcp-data/registry/chart.json +1 -1
  129. package/dist/mcp-data/registry/checkbox-group.json +1 -1
  130. package/dist/mcp-data/registry/checkbox.json +1 -1
  131. package/dist/mcp-data/registry/chip.json +1 -1
  132. package/dist/mcp-data/registry/code-block.json +1 -1
  133. package/dist/mcp-data/registry/conversation-list-item.json +1 -1
  134. package/dist/mcp-data/registry/conversation-list-section.json +1 -1
  135. package/dist/mcp-data/registry/conversation-list.json +1 -1
  136. package/dist/mcp-data/registry/data-customize.json +1 -1
  137. package/dist/mcp-data/registry/data-filter.json +1 -1
  138. package/dist/mcp-data/registry/data-group.json +1 -1
  139. package/dist/mcp-data/registry/data-preferences.json +22 -6
  140. package/dist/mcp-data/registry/data-saved-views.json +1 -1
  141. package/dist/mcp-data/registry/data-search.json +1 -1
  142. package/dist/mcp-data/registry/data-sort.json +1 -1
  143. package/dist/mcp-data/registry/data-toolbar.json +1 -1
  144. package/dist/mcp-data/registry/divider.json +1 -1
  145. package/dist/mcp-data/registry/empty-state.json +1 -1
  146. package/dist/mcp-data/registry/field.json +1 -1
  147. package/dist/mcp-data/registry/filter-menu.json +1 -1
  148. package/dist/mcp-data/registry/icon.json +1 -1
  149. package/dist/mcp-data/registry/inline-banner-settings.json +1 -1
  150. package/dist/mcp-data/registry/input-date.json +1 -1
  151. package/dist/mcp-data/registry/input-time.json +1 -1
  152. package/dist/mcp-data/registry/input.json +1 -1
  153. package/dist/mcp-data/registry/loader.json +1 -1
  154. package/dist/mcp-data/registry/map-cluster-marker.json +1 -1
  155. package/dist/mcp-data/registry/map-entity-marker.json +1 -1
  156. package/dist/mcp-data/registry/markdown.json +1 -1
  157. package/dist/mcp-data/registry/menu.json +31 -12
  158. package/dist/mcp-data/registry/message-actions.json +1 -1
  159. package/dist/mcp-data/registry/message-bubble.json +1 -1
  160. package/dist/mcp-data/registry/message-composer.json +1 -1
  161. package/dist/mcp-data/registry/message-scroller.json +1 -1
  162. package/dist/mcp-data/registry/message.json +1 -1
  163. package/dist/mcp-data/registry/mobile-bar-nav.json +1 -1
  164. package/dist/mcp-data/registry/mobile-header.json +1 -1
  165. package/dist/mcp-data/registry/mobile-section-switcher.json +1 -1
  166. package/dist/mcp-data/registry/mobile-sheet-nav.json +1 -1
  167. package/dist/mcp-data/registry/modal.json +1 -1
  168. package/dist/mcp-data/registry/pagination.json +1 -1
  169. package/dist/mcp-data/registry/panel-nav.json +1 -1
  170. package/dist/mcp-data/registry/panel-sub-nav.json +1 -1
  171. package/dist/mcp-data/registry/panel-tool-header.json +1 -1
  172. package/dist/mcp-data/registry/panel-tool-search.json +1 -1
  173. package/dist/mcp-data/registry/panel-tools.json +1 -1
  174. package/dist/mcp-data/registry/paper-texture.json +1 -1
  175. package/dist/mcp-data/registry/radio-tile.json +1 -1
  176. package/dist/mcp-data/registry/radio.json +1 -1
  177. package/dist/mcp-data/registry/registry.json +46 -21
  178. package/dist/mcp-data/registry/score.json +1 -1
  179. package/dist/mcp-data/registry/scroll-overlay.json +1 -1
  180. package/dist/mcp-data/registry/select.json +1 -1
  181. package/dist/mcp-data/registry/setting-row-checkbox.json +1 -1
  182. package/dist/mcp-data/registry/setting-row-radio.json +1 -1
  183. package/dist/mcp-data/registry/setting-row-toggle.json +1 -1
  184. package/dist/mcp-data/registry/shell-app.json +1 -1
  185. package/dist/mcp-data/registry/shell-page.json +1 -1
  186. package/dist/mcp-data/registry/shell-tools.json +1 -1
  187. package/dist/mcp-data/registry/skeleton.json +1 -1
  188. package/dist/mcp-data/registry/slider.json +1 -1
  189. package/dist/mcp-data/registry/swatch-picker.json +1 -1
  190. package/dist/mcp-data/registry/switch.json +1 -1
  191. package/dist/mcp-data/registry/tab-group.json +1 -1
  192. package/dist/mcp-data/registry/table.json +59 -19
  193. package/dist/mcp-data/registry/tag.json +1 -1
  194. package/dist/mcp-data/registry/text.json +1 -1
  195. package/dist/mcp-data/registry/textarea.json +1 -1
  196. package/dist/mcp-data/registry/time-picker.json +1 -1
  197. package/dist/mcp-data/registry/toast.json +1 -1
  198. package/dist/mcp-data/registry/tooltip-chart.json +1 -1
  199. package/dist/mcp-data/registry/tooltip.json +1 -1
  200. package/dist/mcp-data/registry/typing-indicator.json +1 -1
  201. package/dist/react/ds-menu.d.ts +2 -1
  202. package/dist/react/ds-menu.js +1 -0
  203. package/dist/styles/table.css +41 -0
  204. package/dist/types/components/DataPreferences/DataPreferences.d.ts +3 -0
  205. package/dist/types/components/Menu/Menu.d.ts +5 -1
  206. package/dist/types/components/Menu/menu-types.d.ts +36 -0
  207. package/dist/types/components/Table/Table.d.ts +11 -2
  208. package/dist/types/components/Table/index.d.ts +1 -1
  209. package/dist/types/components/Table/table-column-customizer.d.ts +23 -4
  210. package/dist/types/components/Table/table-layout-controller.d.ts +2 -1
  211. package/dist/types/components/Table/table-types.d.ts +3 -1
  212. package/dist/types/components.d.ts +42 -6
  213. package/dist/vue/ds-data-preferences.js +1 -0
  214. package/dist/vue/ds-menu.js +2 -0
  215. package/dist/vue/ds-table.js +2 -0
  216. package/package.json +3 -3
  217. package/dist/components/p-BuGlC5r8.js +0 -2
  218. package/dist/components/p-C5BBIDVm.js +0 -2
  219. package/dist/components/p-CE6MaXBg.js +0 -2
  220. package/dist/components/p-CE6MaXBg.js.map +0 -1
  221. package/dist/components/p-CNeDJTGX.js +0 -2
  222. package/dist/components/p-CXlklL-0.js +0 -2
  223. package/dist/components/p-CzdE3M3g.js +0 -2
  224. package/dist/components/p-D9_iDlkT.js +0 -2
  225. package/dist/components/p-Da3aFRk2.js +0 -2
  226. package/dist/components/p-Df9S0aWi.js +0 -2
  227. package/dist/components/p-DvGrsLgt.js +0 -2
  228. package/dist/components/p-DvGrsLgt.js.map +0 -1
  229. package/dist/components/p-a-cIN_BX.js +0 -2
  230. package/dist/components/p-aD5cw_m2.js +0 -2
  231. package/dist/components/p-aD5cw_m2.js.map +0 -1
  232. package/dist/components/p-kDZ0sEII.js +0 -2
  233. package/dist/components/p-t2Ebn8si.js +0 -2
  234. package/dist/components/p-t2Ebn8si.js.map +0 -1
  235. /package/dist/components/{p-sDm8BEMy.js.map → p-B-pkoIX9.js.map} +0 -0
  236. /package/dist/components/{p-DYsRXun3.js.map → p-B3o9zn3Y.js.map} +0 -0
  237. /package/dist/components/{p-CcwxySms.js.map → p-BZY40-pE.js.map} +0 -0
  238. /package/dist/components/{p-BLeHHoVb.js.map → p-BqipGGua.js.map} +0 -0
  239. /package/dist/components/{p-BuDxlhnQ.js.map → p-C6kUn-vb.js.map} +0 -0
  240. /package/dist/components/{p-C35Tki38.js.map → p-CSxwWcwW.js.map} +0 -0
  241. /package/dist/components/{p-BlbfbYg6.js.map → p-CaFXrn_8.js.map} +0 -0
  242. /package/dist/components/{p-DZLtFn-r.js.map → p-DObhRLBa.js.map} +0 -0
  243. /package/dist/components/{p-bQE1NZVe.js.map → p-DP07JCed.js.map} +0 -0
  244. /package/dist/components/{p-C378wwRj.js.map → p-DYtVsnDS.js.map} +0 -0
  245. /package/dist/components/{p-C9H-3JOA.js.map → p-DdFeWC-e.js.map} +0 -0
  246. /package/dist/components/{p-kDZ0sEII.js.map → p-DlCLfwP4.js.map} +0 -0
  247. /package/dist/components/{p-BYrBU_U9.js.map → p-HMDczkQf.js.map} +0 -0
  248. /package/dist/components/{p--VQc5GDy.js.map → p-KA8srEed.js.map} +0 -0
  249. /package/dist/components/{p-BfVMaZZE.js.map → p-OJ5WeR05.js.map} +0 -0
  250. /package/dist/components/{p-DPwptvZl.js.map → p-wXKIVUTn.js.map} +0 -0
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
3
  "name": "table",
4
4
  "title": "Table",
5
- "description": "Semantic, controlled data table with durable visual primitives for cell geometry, wrapping multiple-Tag cells, grouping, sorting, multi-selection, sticky overflow, incremental loading, top-level pagination, and optional row virtualization.",
5
+ "description": "Semantic, controlled data table with contained or edge-to-edge chrome, durable visual primitives for cell geometry, wrapping multiple-Tag cells, grouping, sorting, multi-selection, sticky overflow, incremental loading, top-level pagination, and optional row virtualization.",
6
6
  "type": "registry:ui",
7
7
  "meta": {
8
8
  "library": "@ds-mo/ui",
@@ -13,11 +13,12 @@
13
13
  "intent": {
14
14
  "audience": "general",
15
15
  "status": "stable",
16
- "summary": "Semantic, controlled data table with durable visual primitives for cell geometry, wrapping multiple-Tag cells, grouping, sorting, multi-selection, sticky overflow, incremental loading, top-level pagination, and optional row virtualization.",
16
+ "summary": "Semantic, controlled data table with contained or edge-to-edge chrome, durable visual primitives for cell geometry, wrapping multiple-Tag cells, grouping, sorting, multi-selection, sticky overflow, incremental loading, top-level pagination, and optional row virtualization.",
17
17
  "useWhen": [
18
18
  "A structured dataset needs native row and column relationships, optional one-level grouping, or comparable values across records.",
19
19
  "An application needs controlled sorting, loaded-row selection, infinite loading, pagination, virtual row recycling, or opted-in table-owned data-mode and column controls while retaining ownership of the data query, catalog, and persistence.",
20
- "A product table needs a token-backed recipe whose individual header, group, row, cell, selection, state, and load-more layers remain easy to restyle."
20
+ "A product table needs a token-backed recipe whose individual header, group, row, cell, selection, state, and load-more layers remain easy to restyle.",
21
+ "A full-bleed page needs edge-to-edge toolbar and footer surfaces around an inset data frame with a tertiary outer border while other tables retain the contained composition."
21
22
  ],
22
23
  "avoidWhen": [
23
24
  "Content is primarily editorial prose; use semantic product-owned markup with the prose recipe instead.",
@@ -48,7 +49,7 @@
48
49
  "accessibility": [
49
50
  "The rendered structure uses a native table, caption, column headers, row groups, and data cells; keep the caption non-empty. The native caption remains visually hidden as the table's accessible name. When caption visibility is enabled, a 48px header bar renders as component chrome outside the data scrollport. It defaults to a presentational caption title that does not duplicate the accessible name. Application-owned labeled controls supplied through the single named header slot replace that fallback and share one horizontal caption overflow row with table-owned actions inside the component-owned 8px inset. Table caption buttons keep pressed interaction paint without physically scaling. When columnCustomizer is enabled, the table adds a trailing Customize control named Customize table. Its label uses regular weight and its resting foreground remains neutral when hidden or reordered columns differ from the catalog default. The control is icon-label at typical widths and the icon-only Customize menu button below 900px, where a tooltip preserves the Customize label. The always-icon-only table-variant control also exposes its label through a tooltip. Otherwise the header slot receives the complete content width.",
50
51
  "The data-mode switcher is the shared Menu of the table's supported infinite, pagination, and virtual modes. It uses an Ellipses trigger. The switcher is the final caption-bar action; when the column customizer is also present, a decorative vertical divider separates the two controls. dataMode remains controlled, and the application responds to change intent by supplying the matching records, pagination state, loading behavior, and query consequences. Escape, dismissal, and selection return focus to the trigger.",
51
- "The column customizer is the shared Menu of switch rows, not a table-owned dialog. Data columns can be shown, hidden, and reordered. Action columns and the selection checkbox never appear in the menu. The last remaining visible data column cannot be hidden, but it stays focusable so it can still be reordered. Escape and outside click restore focus to the trigger. Persistence stays in the application.",
52
+ "The column customizer is the shared Menu surface of composite preference rows. Data columns can be shown, hidden, left-pinned, and reordered. Visibility and pin state are independent, so hidden pinned columns retain their pin state. A tertiary divider separates the visibility switch from an unbordered Pin toggle. Pinned and unpinned sections constrain drag reorder to their own partition; a single pinned row keeps its drag handle visible but inactive in quaternary foreground. Action columns and the selection checkbox remain system-managed and never appear in the menu. Escape and outside click restore focus to the trigger. Persistence stays in the application.",
52
53
  "Sorting controls remain buttons inside column headers and expose the table's one controlled member-row sort through aria-sort. ds-data-sort in the toolbar trailing cluster is the matching menu of Data and Order sections; it emits the same dsSortChange shape so header clicks and menu choices stay in sync. Grouping never adds a second sort indicator or header control; ds-data-group controls the grouping data point and ascending or descending group-section order while the application supplies the resulting groups. Optional column help underlines the header label with the shared dotted decoration and shows ds-tooltip from that label; it does not add a second header control or replace the column name.",
53
54
  "Selection controls expose checkbox semantics and explicit row names; select-all affects selectable loaded rows only and preserves selected identities outside the loaded window. In virtual mode the supplied in-memory rows are that loaded set, including rows whose DOM is recycled. When grouping is active with multi-selection, each group section header also exposes a checkbox that selects or clears that group's selectable loaded rows.",
54
55
  "Interactive rows are focusable as native rows, activate with Enter or Space, and do not steal activation from their nested selection, action, overflow-menu, or primary-text link controls. When an application-owned details surface navigates between records, scrollRowIntoView reveals the newly associated row with nearest scrolling while preserving focus in that surface. It resolves false when the identity is absent from the supplied, expanded table model so the application can first load data, change its controlled page, or expand its group.",
@@ -62,9 +63,9 @@
62
63
  "Cell presentation moves the complete content stack for deliberate middle or bottom alignment; the default remains first-track alignment. Semantic highlight borders require an accessible explanation, and flags require labels. Presentation metadata never changes the cell value or the shared text, icon, tag and action track anatomy.",
63
64
  "Report merges name ordered row and column identities. Only a complete contiguous non-overlapping rectangle is accepted, and invalid definitions leave original values visible. Selection checkboxes are never merged. Merges cannot cross pin or elastic-spacer boundaries, and grouped or editable grids do not merge. Cross-row merges require non-windowed reports; column-only merges can use virtual rows. Applications recompute spans with their final data order and retain covered values.",
64
65
  "Editing has two opt-in ungrouped modes for scalar values. Edit mode keeps normal table semantics and reveals a floating pencil on cell hover or keyboard focus; touch keeps the pencil visible. Grid mode adds per-cell hover/press feedback, keyboard focus and controlled rectangular ranges. Arrows and Home/End skip read-only cells; Shift+click or Shift+arrows extends the range without selecting read-only cells or opening an editor. A single click, Enter, F2 or typing opens the grid editor, while edit mode requires the pencil. Both modes share validation and controlled change events: Enter or Tab proposes a change; Escape or leaving the editor cancels. Tab advances to the next editable cell or pencil. Read-only content is muted without fading dividers, and those cells cannot receive cell selection or hover feedback. Native controls retain their own keyboard behavior. Plain TSV grid paste is bounded and rejected atomically for invalid, disabled, out-of-range or read-only destinations. Applications accept or reject proposed edits, own persistence and domain validation, and replace controlled data. Row checkbox selection remains independent of cell selection.",
65
- "Columns, rows, groups, grouping, sorting, selected row IDs, collapsed group IDs, highlightTerms, highlightFieldIds, hiddenFieldIds, fieldOrder, dataMode, pagination, displayedCount, totalCount, and incremental-loading state are controlled inputs. Events communicate intent and never mutate those inputs. The optional header is one composable table-local surface: the component owns its 48px chrome, 8px inset on every side, and sticky placement while the application owns the leading slot's internal layout, controls, labels, filtering behavior, and action handling. highlightTerms only marks matching table-owned text and never filters rows, changes query semantics, or highlights Tag labels. highlightFieldIds optionally narrows that marking to matching column IDs or compound header-segment sort keys. Opting into dataModeSwitcher and columnCustomizer adds their trailing Menu triggers and requires caption visibility. When both are present, Customize precedes a decorative divider and the data-mode trigger remains the final action. The Customize trigger keeps one stable label and neutral resting foreground whether or not hiddenFieldIds or fieldOrder differ from the catalog default; the configured state remains visible inside the Menu. The table closes one trailing Menu before opening the other. The footer retains its compact-bar composition: application-owned leading status sits left while a controlled result summary or Pagination aligns right, before any trailing action. Content-sized tables remain natural for sparse or fixed-count results. height bounds the complete header, frame, and footer composition. fitViewport reserves the compact-state composition height in the nearest vertical scrollport while growing the visible table into that reservation as surrounding page chrome collapses.",
66
+ "Columns, rows, groups, grouping, sorting, selected row IDs, collapsed group IDs, highlightTerms, highlightFieldIds, hiddenFieldIds, fieldOrder, pinnedFieldIds, dataMode, pagination, displayedCount, totalCount, and incremental-loading state are controlled inputs. Events communicate intent and never mutate those inputs. The optional header is one composable table-local surface: the component owns its 48px chrome, 8px inset on every side, and sticky placement while the application owns the leading slot's internal layout, controls, labels, filtering behavior, and action handling. highlightTerms only marks matching table-owned text and never filters rows, changes query semantics, or highlights Tag labels. highlightFieldIds optionally narrows that marking to matching column IDs or compound header-segment sort keys. Opting into dataModeSwitcher and columnCustomizer adds their trailing Menu triggers and requires caption visibility. When both are present, Customize precedes a decorative divider and the data-mode trigger remains the final action. The Customize trigger keeps one stable label and neutral resting foreground whether or not column configuration differs from the catalog default; the configured state remains visible inside the Menu. The table closes one trailing Menu before opening the other. The footer retains its compact-bar composition: application-owned leading status sits left while a controlled result summary or Pagination aligns right, before any trailing action. Content-sized tables remain natural for sparse or fixed-count results. height bounds the complete header, frame, and footer composition. fitViewport reserves the compact-state composition height in the nearest vertical scrollport while growing the visible table into that reservation as surrounding page chrome collapses.",
66
67
  "Grouping supports one level. The application supplies groups in a fixed product-defined order and supplies ordered member rows independently. Group section headers show an emphasized text-body-medium label at the start and plain secondary loaded progress at the trailing edge beside the disclosure. A 20px tertiary divider centered on the label track marks the action-column boundary between the count and disclosure. The loaded value comes from the supplied member rows and the total comes from totalCount. A localized countLabel total phrase remains available for the complete assistive progress announcement. Virtual mode shows totals only, for example 166, because there is no loaded window. A collapsed section with no supplied member rows uses that same total-only copy so collapse does not advertise a zero loaded window, including 0 when no total is known. An expanded empty section still shows loaded progress, for example 0 of 166. Optional accessories occupy a second 24px track under the label, middle-dot separated, at most four items. An accessory with help uses the shared dotted underline and a bottom-start ds-tooltip from that copy; explicit line breaks create an unbulleted list. Without accessories the section stays the single-track row height; with accessories it matches a two-track body row, with the checkbox, trailing count, divider, and disclosure kept on the label track. An optional hero accessory sits before the label. The first supported hero is a safety-score fill. Its one-track presentation uses the default ds-score at sm and aligns to the label track on either row height. Setting hero tracks to 2 creates or spans the two-track header, renders the default ds-score at lg, and centers it across the label and accessory tracks. In selectable tables the two-track score starts at the selection-column boundary; selectionMode none omits that lane and uses the regular group-content start inset. A score hero does not use group intent coloring. Hero presentation does not change the 40 or 64 row-height contract beyond the explicitly requested second track. Groups without intent use the secondary background surface. Groups may declare an optional intent (brand, neutral, negative, warning, caution, positive) that paints an LTR gradient from the faint intent surface at the label edge into faint neutral at the trailing edge, plus a bold intent title color. Neutral groups remain solid faint neutral. Section separators are painted inside that viewport-width surface so translucent border tokens composite with each intent background. Internal sections keep their separators; a collapsed or empty section header yields its separator only when it is the table's final rendered item. The complete section header is the expand/collapse target and paints hover and press on that viewport-width surface. The section separator stays on the dedicated trailing-edge rule so the interaction wash cannot replace it. Trailing loaded progress sits on the label track beside a visually neutral icon-only ButtonUnfilled. The centered 20px tertiary divider at the action-column boundary separates the count from the 24px md double-inset disclosure. The disclosure keeps its accessible name and does not paint its own hover or press wash, showing ChevronUp while expanded and ChevronDown while collapsed. While any group is expanded, a ChevronDownUp action uses the xs sort-control recipe widened to a table-owned 24px lane and collapses every group. It appears at the inline-end edge of the trailing column declared with kind action when one exists; otherwise a button-hugging 24px by 16px medium-elevation surface floats at the visible scrollport's header edge so the action remains reachable while member columns scroll beneath it. With stickyHeader enabled, each contained section uses its one real section row as a native sticky header inside its real row-group boundary. The browser therefore owns pinning, push-off, scrolling, and resize layout in one pass without a duplicated lane, active-section state, transforms, or scroll-time geometry. Shared grid tracks retain exact header and member-cell alignment. The section surface also stays pinned to the visible horizontal scrollport and spans the smaller of the visible viewport or table width. Virtual grouped tables flatten headers and members into that same scrollport; expanding a section never introduces an inner scroller.",
67
- "When columnCustomizer is false, columns render in catalog order. When it is true, columns remains the catalog: hiddenFieldIds and fieldOrder control visibility and data-column sequence, action columns stay last, cannot be hidden, and remain absent from the customizer Menu, and dsFieldsConfigChange reports the next hidden ids and data-column order without mutating those inputs. The customizer Menu live-applies show/hide and data-column reorder. The table does not persist preferences.",
68
+ "When columnCustomizer is false, columns render in catalog order. When it is true, columns remains the catalog: hiddenFieldIds, fieldOrder, and pinnedFieldIds control visibility, partitioned data-column sequence, and user-managed sticky-start columns. Fixed sticky columns remain system-managed. User-pinned columns require a resolved fixed width, append to the pinned block when pinned, and become the first unpinned column when released. Action columns stay last, cannot be hidden or user-pinned, and remain absent from the customizer. dsFieldsConfigChange reports all three next values without mutating inputs. The table does not persist preferences.",
68
69
  "An active member sort toggles only between ascending and descending; activating a different sortable label moves the one sorting state to that key. Applications may still clear the controlled sort state explicitly. Grouping does not introduce another interactive direction.",
69
70
  "Compound headers can expose multiple related labels with independent sort keys. Each label owns only its content-width interaction target while the active direction remains in the column's alignment-aware sort lane.",
70
71
  "Initial loading defaults to ten row-shaped skeletons and preserves row dividers, column dividers, selection lanes, and sticky boundaries. A selectable table renders a checkbox-sized control skeleton in its selection lane so loading geometry matches the real checkbox instead of a generic icon. Each column may declare representative text, multiline text, image, Tag, icon, icon-and-text, action, or intentionally blank loading geometry; the table applies the matching resolved-cell padding and track anatomy so the placeholder row has the same height and rhythm as its data row. Columns without a declaration use one text line, while action columns infer an icon action placeholder. chromeLoading replaces opted-in table-owned caption controls with non-interactive control skeletons in the same slots and at the same dimensions, without changing dataMode or the surrounding caption geometry. Empty and initial error replace only the body with ds-empty-state, using Inbox or ErrorTriangle plus the table heading and body copy. In height-bounded and maxHeight tables that region fills the remaining viewport below the column header; content-sized tables keep a minimum empty-region height. Empty, initial error, load-more, retry, loading-more, and terminal bands remain fixed to the visible table width while data columns scroll horizontally beneath them. Incremental loading retains existing rows and supports automatic sentinel, manual load-more, retry, and terminal states. When grouping is active, each expanded group owns its load band at the end of its member rows; internal group load bands keep their bottom divider, while a terminal group load band yields it to the table edge or footer. The global bottom-of-table loader is not rendered.",
@@ -82,8 +83,8 @@
82
83
  ],
83
84
  "responsiveBehavior": [
84
85
  "Editable cells compose the shared borderless controls with cell-owned interaction and focus chrome. Only the active cell mounts a field. Single-select choices complete the edit immediately; multiline fields reserve Enter for a newline and use Ctrl/Command+Enter to commit. Date and time fields keep their picker keyboard behavior and emit canonical scalar values. Drafts stay local until a controlled change is proposed.",
85
- "Horizontal scrolling is the default. Opted-in cards reuse the same native table in a narrow-container layout: a record is one card with vertically stacked labeled fields, inset separators aligned to the content origin, and space between labels and values. One row-owned interaction surface covers the card. Actions and selection sit together at the top-right, separated by a short divider. The content region owns card gutters and gaps; the toolbar and footer remain separate edge-to-edge surfaces. Page owners remove their outer table inset at the responsive breakpoint. Column headers remain available to assistive technology but are visually hidden and contain no interactive controls in card mode. Row selection and action controls preserve focus across resizing instead of mounting a duplicate mobile tree. Merged reports and grid interaction retain table layout. Card compositions do not use the duplicated document-flow sticky header.",
86
- "Inside the named ds-table container, caption Search retains its standard maximum width and flexes smaller only when the complete row needs space. Filter, Group, and Sort controls that opt into collapseLabel become icon-only below 900px by omitting their labels and chevrons. Active filtering or grouping promotes the visible icon to primary. Sort keeps resting secondary chrome because its selection lives in the menu and headers. Customize switches to the icon-only Customize menu button below 900px and keeps its neutral resting foreground regardless of the controlled column configuration. The caption, frame, and footer remain within the table host at every width. The complete caption row scrolls together only when those compact controls no longer fit, while data columns scroll inside the viewport; table-owned trailing actions never stay pinned over the scrolling toolbar.",
86
+ "Horizontal scrolling is the default. Opted-in cards reuse the same native table below the 768px browser viewport breakpoint: a record is one card with vertically stacked labeled fields, inset separators aligned to the content origin, and space between labels and values. A narrow table on a desktop viewport remains a table. One row-owned interaction surface covers the card. Actions and selection sit together at the top-right, separated by a short divider. The content region owns card gutters and gaps; the toolbar and footer remain separate edge-to-edge surfaces. The edge-to-edge chrome layout removes its desktop frame inset and outer border while cards provide their own gutters. Column headers remain available to assistive technology but are visually hidden and contain no interactive controls in card mode. Row selection and action controls preserve focus across resizing instead of mounting a duplicate mobile tree. Merged reports and grid interaction retain table layout. Card compositions do not use the duplicated document-flow sticky header.",
87
+ "Inside the named ds-table container, caption Search retains its standard maximum width and flexes smaller only when the complete row needs space. Filter, Group, and Sort controls that opt into collapseLabel become icon-only below 900px by omitting their labels and chevrons. Active filtering or grouping promotes the visible icon to primary. Sort keeps resting secondary chrome because its selection lives in the menu and headers. Customize switches to the icon-only Customize menu button below 900px and keeps its neutral resting foreground regardless of the controlled column configuration. Contained chrome is the default. Edge-to-edge chrome keeps the caption and footer flush to the host, gives the footer a tertiary divider painted outside its content box, and places the data frame in a transparent, token-backed 16px inset with a tertiary outer border so the owning page surface remains visible. The complete caption row scrolls together only when those compact controls no longer fit, while data columns scroll inside the viewport; table-owned trailing actions never stay pinned over the scrolling toolbar.",
87
88
  "stickyHeader follows the active vertical scroll owner: a maxHeight, height, or fitViewport table keeps its native header and group sections inside one bounded table scrollport, while a document-flow table retains a semantic native header and renders a sticky control header whose columns follow the body's native inline scroll timeline. Consumers set --ds-table-sticky-header-offset when page chrome occupies an unbounded document scrollport edge. When the 48px table header bar is visible, it sticks at that external offset and the column header stacks immediately below it.",
88
89
  "fitViewport uses viewportInsetBlockStart and viewportInsetBlockEnd to describe compact surrounding chrome and page gutters relative to the nearest vertical scroll owner. Before the table reaches that compact position, its host reserves the eventual height while the visible composition grows and vertical input remains owned by the outer page. Once settled, the internal table viewport becomes the vertical scroll owner. Bounded grouped tables use browser-native sticky section rows inside real block row-group boundaries, so scroll and resize do not run component JavaScript or trigger reactive section swaps. Document-flow tables retain the synchronized visual lane required by their separate horizontal scrollport.",
89
90
  "Without maxHeight the table grows the document and automatic lazy loading observes the browser viewport. Horizontal scrolling chains vertical wheel and trackpad movement to the page instead of trapping it.",
@@ -94,12 +95,12 @@
94
95
  ],
95
96
  "frameworkCaveats": {
96
97
  "customElements": [
97
- "Assign columns, rows, groups, grouping, sort, selectedRowIds, collapsedGroupIds, highlightTerms, highlightFieldIds, hiddenFieldIds, fieldOrder, pagination, displayedCount, and totalCount through JavaScript properties rather than HTML attribute strings.",
98
+ "Assign columns, rows, groups, grouping, sort, selectedRowIds, collapsedGroupIds, highlightTerms, highlightFieldIds, hiddenFieldIds, fieldOrder, pinnedFieldIds, pagination, displayedCount, and totalCount through JavaScript properties rather than HTML attribute strings.",
98
99
  "dataMode=virtual recycles row DOM over the supplied in-memory rows or groups and requires height, maxHeight, or fitViewport. It does not fetch or paginate.",
99
100
  "When an action cell supplies items, the table opens one shared ds-menu from the Ellipses trigger; dsCellAction fires for the selected command, not for opening or closing the menu.",
100
- "When columnCustomizer is enabled, the Table trigger opens the same shared ds-menu of reorderable switch rows; toggling a switch or reordering a row emits dsFieldsConfigChange and does not close the menu.",
101
+ "When columnCustomizer is enabled, the Table trigger opens the same shared ds-menu of reorderable visibility rows with independent pin toggles; changing visibility, pinning, or partition-local order emits dsFieldsConfigChange and does not close the menu.",
101
102
  "Overflowing text tracks present one shared ds-tooltip with the omitted value. Unlimited wrapping tracks and disabled rows never show it. The tooltip is not linked with aria-describedby because the full string remains in the cell.",
102
- "Replace controlled property values in response to dsSortChange, dsGroupCollapseChange, dsSelectionChange, dsLoadMore, dsGroupLoadMore, dsPaginationChange, and dsFieldsConfigChange; route dsRowActivate in the application owner. When columnCustomizer is enabled, keep columns as the full catalog and replace hiddenFieldIds and fieldOrder from that event."
103
+ "Replace controlled property values in response to dsSortChange, dsGroupCollapseChange, dsSelectionChange, dsLoadMore, dsGroupLoadMore, dsPaginationChange, and dsFieldsConfigChange; route dsRowActivate in the application owner. When columnCustomizer is enabled, keep columns as the full catalog and replace hiddenFieldIds, fieldOrder, and pinnedFieldIds from that event."
103
104
  ],
104
105
  "react": [
105
106
  "Pass new array and state-object references when the controlled data or query state changes.",
@@ -172,6 +173,11 @@
172
173
  "source": "src/wc/styles/table.css",
173
174
  "reason": "Public table recipe customization; preserve semantic table and scroll behavior."
174
175
  },
176
+ {
177
+ "name": "--ds-table-frame-inset",
178
+ "source": "src/wc/styles/table.css",
179
+ "reason": "Controls the data-frame inset in the edge-to-edge chrome layout."
180
+ },
175
181
  {
176
182
  "name": "--ds-table-inline-scroll-max-offset",
177
183
  "source": "src/wc/styles/table.css",
@@ -274,6 +280,15 @@
274
280
  "mutable": false,
275
281
  "description": "Explicit report merges; invalid combinations leave the original cells visible."
276
282
  },
283
+ "chromeLayout": {
284
+ "type": "TableChromeLayout",
285
+ "resolvedType": "\"contained\" | \"edge-to-edge\"",
286
+ "attribute": "chrome-layout",
287
+ "default": "'contained'",
288
+ "required": false,
289
+ "mutable": false,
290
+ "description": "Keep all table chrome contained, or let the caption and footer span edge to edge around an inset data frame."
291
+ },
277
292
  "chromeLoading": {
278
293
  "type": "boolean",
279
294
  "resolvedType": "boolean",
@@ -656,6 +671,14 @@
656
671
  "required": false,
657
672
  "mutable": false
658
673
  },
674
+ "pinnedFieldIds": {
675
+ "type": "string[]",
676
+ "resolvedType": "string[]",
677
+ "default": "[]",
678
+ "required": false,
679
+ "mutable": false,
680
+ "description": "Controlled user-pinned data-column identities. Hidden columns retain their pin state."
681
+ },
659
682
  "responsiveLayout": {
660
683
  "type": "'scroll' | 'cards'",
661
684
  "resolvedType": "\"cards\" | \"scroll\"",
@@ -663,7 +686,7 @@
663
686
  "default": "'scroll'",
664
687
  "required": false,
665
688
  "mutable": false,
666
- "description": "Opt-in native card presentation below the 768px table container breakpoint."
689
+ "description": "Opt-in native card presentation below the 768px browser viewport breakpoint."
667
690
  },
668
691
  "resultSummaryLabel": {
669
692
  "type": "string",
@@ -944,6 +967,15 @@
944
967
  "mutable": false,
945
968
  "description": "Explicit report merges; invalid combinations leave the original cells visible."
946
969
  },
970
+ "chromeLayout": {
971
+ "type": "TableChromeLayout",
972
+ "resolvedType": "\"contained\" | \"edge-to-edge\"",
973
+ "attribute": "chrome-layout",
974
+ "default": "'contained'",
975
+ "required": false,
976
+ "mutable": false,
977
+ "description": "Keep all table chrome contained, or let the caption and footer span edge to edge around an inset data frame."
978
+ },
947
979
  "chromeLoading": {
948
980
  "type": "boolean",
949
981
  "resolvedType": "boolean",
@@ -1326,6 +1358,14 @@
1326
1358
  "required": false,
1327
1359
  "mutable": false
1328
1360
  },
1361
+ "pinnedFieldIds": {
1362
+ "type": "string[]",
1363
+ "resolvedType": "string[]",
1364
+ "default": "[]",
1365
+ "required": false,
1366
+ "mutable": false,
1367
+ "description": "Controlled user-pinned data-column identities. Hidden columns retain their pin state."
1368
+ },
1329
1369
  "responsiveLayout": {
1330
1370
  "type": "'scroll' | 'cards'",
1331
1371
  "resolvedType": "\"cards\" | \"scroll\"",
@@ -1333,7 +1373,7 @@
1333
1373
  "default": "'scroll'",
1334
1374
  "required": false,
1335
1375
  "mutable": false,
1336
- "description": "Opt-in native card presentation below the 768px table container breakpoint."
1376
+ "description": "Opt-in native card presentation below the 768px browser viewport breakpoint."
1337
1377
  },
1338
1378
  "resultSummaryLabel": {
1339
1379
  "type": "string",
@@ -1595,10 +1635,10 @@
1595
1635
  "import": "import { DsTable } from '@ds-mo/ui/angular/ds-table';",
1596
1636
  "example": "<ds-table></ds-table>"
1597
1637
  },
1598
- "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: cellRange, cellSpans, collapsedGroupIds, columns, customizeOptions, fieldOrder, grouping, groups, hiddenFieldIds, highlightFieldIds, highlightTerms, pagination, rows, selectedRowIds, sort.",
1638
+ "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: cellRange, cellSpans, collapsedGroupIds, columns, customizeOptions, fieldOrder, grouping, groups, hiddenFieldIds, highlightFieldIds, highlightTerms, pagination, pinnedFieldIds, rows, selectedRowIds, sort.",
1599
1639
  "peerDependencies": {
1600
1640
  "required": [
1601
- "@ds-mo/tokens ^6.6.1",
1641
+ "@ds-mo/tokens ^6.7.0",
1602
1642
  "@ds-mo/icons ^8.1.1"
1603
1643
  ],
1604
1644
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -1638,7 +1678,7 @@
1638
1678
  },
1639
1679
  {
1640
1680
  "path": "src/wc/components/Table/Table.tsx",
1641
- "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Listen,\n Method,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n deriveTableSelectionState,\n formatTableResultSummary,\n formatTableTotalSummary,\n hasOwnedTableFooterSlot,\n isTableCellAction,\n nextDataSortState,\n tableColumnSize,\n tableModelIssues,\n TABLE_GROUP_HERO_SCORE_PRESENTATION,\n tableRows,\n toggleAllLoadedTableRows,\n toggleTableGroupCollapsed,\n toggleTableGroupSelection,\n nextTableGroupsCollapsed,\n toggleTableRowSelection,\n} from './table-model';\nimport { createTableRenderModel, type TableRenderModel } from './table-render-model';\nimport {\n isRenderableTableActionMenu,\n nextTableActionMenuElementId,\n tableActionMenuSections,\n tableActionTriggerId,\n} from './table-action-menu';\nimport {\n nextTableColumnCustomizerElementId,\n resolveTableColumnOrder,\n resolveTableHiddenColumnIds,\n resolveTableVisibleColumns,\n tableColumnCustomizerMenuItems,\n toggleTableColumnHidden,\n} from './table-column-customizer';\nimport {\n nextTableDataModeSwitcherElementId,\n tableDataModeFromMenuItem,\n tableDataModeMenuItems,\n} from './table-data-mode-switcher';\nimport { resolveTableTruncateTrack, tableTruncateLabel } from './table-truncate';\nimport { renderTableRow as renderTableRowView } from './table-row-view';\nimport { TableBodyRenderer } from './table-body-renderer';\nimport { tableHeaderBands, tablePinnedColumns, tableSpanModel } from './table-structure';\nimport {\n tableGridModel,\n tableRangeBounds,\n tableEditable,\n tableEditValue,\n tablePasteChanges,\n tableNextEditable,\n} from './table-grid-model';\nimport { renderTableCellEditor } from './table-cell-editor';\nimport type {\n TableCellSpan,\n TableCellRange,\n TableCellAddress,\n TableCellsChangeDetail,\n} from './table-types';\nimport { renderTableSkeletonBody } from './table-skeleton-view';\nimport { renderTableLoadContent } from './table-load-view';\nimport { TableLayoutController } from './table-layout-controller';\nimport { TableLoadController } from './table-load-controller';\nimport { TableGroupLoadController } from './table-group-load-controller';\nimport {\n estimateTableRowBlockSize,\n flattenTableVirtualItems,\n resolveTableVirtualPlan,\n TABLE_VIRTUAL_TOTAL_SUMMARY_LABEL,\n TABLE_VIRTUAL_VIEWPORT_REQUIRED_BODY,\n TABLE_VIRTUAL_VIEWPORT_REQUIRED_HEADING,\n type TableVirtualItem,\n type TableVirtualPlan,\n} from './table-virtual-model';\nimport { TableVirtualController } from './table-virtual-controller';\nimport type { PaginationChangeDetail } from '../Pagination/pagination-types';\nimport type { MenuItemData, MenuReorderDetail } from '../Menu/menu-types';\nimport { resolvePaginationState } from '../Pagination/pagination-model';\nimport { resolveCssLengthPx } from '../../utils/resolve-css-length-px';\nimport { isElementTruncated } from '../../utils/is-element-truncated';\nimport { observeTableCaptionCompact } from '../../utils/table-caption-compact';\nimport { resolveTableFitPageSize } from './table-pagination-fit';\nimport { createTableHighlightMatcher, type TableHighlightMatcher } from './table-highlight';\nimport {\n paginationShortcutBlockedByPath,\n shouldHandleContainingPagePaginationShortcut,\n} from './table-pagination-shortcut';\nimport {\n TableViewportFitController,\n type TableViewportFitMetrics,\n} from './table-viewport-fit-controller';\nimport type {\n TableCaptionVisibility,\n TableCellActionDetail,\n TableColumn,\n DataFieldsConfigChangeDetail,\n TableDataMode,\n TableDataModeChangeDetail,\n DataGroup,\n TableGroupCollapseChangeDetail,\n TableGroupLoadMoreDetail,\n DataGroupingState,\n TableLoadMoreDetail,\n TableLoadMoreMode,\n TablePaginationState,\n TableRow,\n TableRowActivateDetail,\n TableSelectionChangeDetail,\n TableSelectionMode,\n DataSortChangeDetail,\n DataSortState,\n} from './table-types';\n\nconst TABLE_FOOTER_SLOT_LEADING = 1;\nconst TABLE_FOOTER_SLOT_COPY = 2;\nconst TABLE_FOOTER_SLOT_TRAILING = 4;\nconst TABLE_NON_VIRTUAL_WINDOW_THRESHOLD = 50;\n\n@Component({\n tag: 'ds-table',\n styleUrls: ['../../utils/focus-ring.css', '../../utils/interaction-fill.css', 'Table.css'],\n scoped: true,\n})\nexport class Table {\n @Element() el!: HTMLElement;\n /** Stable column definitions. Assign through JavaScript. */\n @Prop() columns: TableColumn[] = [];\n /** Opt-in native card presentation below the 768px table container breakpoint. */\n @Prop() responsiveLayout: 'scroll' | 'cards' = 'scroll';\n /** Explicit report merges; invalid combinations leave the original cells visible. */\n @Prop() cellSpans: TableCellSpan[] = [];\n /** Table is read-only; edit uses per-cell pencil buttons; grid adds cell selection and keyboard navigation. Grouped tables remain read-only. */\n @Prop() interactionMode: 'table' | 'edit' | 'grid' = 'table';\n /** Controlled rectangular cell selection, independent of row checkbox selection. */\n @Prop() cellRange: TableCellRange | null = null;\n @Event() dsCellRangeChange!: EventEmitter<TableCellRange>;\n @Event() dsCellsChange!: EventEmitter<TableCellsChangeDetail>;\n /** Additional toggle options in the column customizer. */\n @Prop() customizeOptions: MenuItemData[] = [];\n @Event() dsCustomizeOptionChange!: EventEmitter<string>;\n /** Ungrouped row data. Ignored while grouping is active. Assign through JavaScript. */\n @Prop() rows: TableRow[] = [];\n /** One level of application-owned grouped data. Assign through JavaScript. */\n @Prop() groups: DataGroup[] = [];\n /** Controlled grouping column. Applications supply groups in their final fixed order. */\n @Prop() grouping: DataGroupingState | null = null;\n /** Controlled member-row sort state. */\n @Prop() sort: DataSortState | null = null;\n /** Controlled collapsed group identities. Groups not listed remain expanded. */\n @Prop() collapsedGroupIds: string[] = [];\n /** Literal terms to highlight in table-owned text cells. Applications still own filtering. */\n @Prop() highlightTerms: string[] = [];\n /** Optional DataSearch field identities that restrict which data-point tracks are highlighted. */\n @Prop() highlightFieldIds: string[] = [];\n\n /** Required accessible table name, retained as a native caption. */\n @Prop() caption!: string;\n /** Shows a matching presentational title bar above the native table frame. */\n @Prop() captionVisibility: TableCaptionVisibility = 'hidden';\n /**\n * Opt in to the table-owned column customizer. The `columns` prop remains the\n * catalog; hidden and ordered columns are controlled separately. The trigger\n * opens the shared Menu of live show/hide switch rows.\n */\n @Prop() columnCustomizer: boolean = false;\n /** Hide only the built-in Customize trigger when an external preferences surface owns it. */\n /** Remove borders from table-owned caption controls. */\n @Prop() captionControlsBorderless: boolean = false;\n @Prop() hideColumnCustomizerTrigger: boolean = false;\n /** Controlled hidden data-column identities. Action ids are ignored. */\n @Prop() hiddenFieldIds: string[] = [];\n /** Controlled data-column identities in display order. Omitted ids append in catalog order. */\n @Prop() fieldOrder: string[] = [];\n /**\n * Optional result summary footer. When both `displayedCount` and `totalCount`\n * are finite numbers, infinite mode shows “Displaying {displayed} of {total}”.\n * Virtual mode ignores `displayedCount` and derives its total from the\n * complete supplied rows; a mismatched `totalCount` emits a warning.\n */\n @Prop() displayedCount: number | undefined;\n @Prop() totalCount: number | undefined;\n /** Supports {displayed} and {total} placeholders. */\n @Prop() resultSummaryLabel: string = 'Displaying {displayed} of {total}';\n /** Supports the {total} placeholder. Used when dataMode is virtual. */\n @Prop() resultTotalSummaryLabel: string = TABLE_VIRTUAL_TOTAL_SUMMARY_LABEL;\n @Prop() stickyHeader: boolean = false;\n /** Maximum scroll-region height. Numbers resolve to CSS pixels. */\n @Prop() maxHeight: string | number | undefined;\n /** Fixed height for the complete header, table frame, and footer composition. */\n @Prop() height: string | number | undefined;\n /** Fit the complete table composition to its nearest vertical scrollport. */\n @Prop() fitViewport: boolean = false;\n /** Reserved space above a viewport-fitted table once surrounding chrome is compact. */\n @Prop() viewportInsetBlockStart: string | number = 0;\n /** Reserved space below a viewport-fitted table. */\n @Prop() viewportInsetBlockEnd: string | number = 0;\n /** Optional explicit label for the horizontal/vertical scroll region. */\n @Prop() scrollLabel: string | undefined;\n\n @Prop() selectionMode: TableSelectionMode = 'none';\n /** Controlled selected row identities. IDs outside the loaded rows are preserved. */\n @Prop() selectedRowIds: string[] = [];\n\n /**\n * Identity of the interactive row whose associated surface is visible.\n * Holds its pressed wash independently of checkbox selection. Keep the ID\n * through exit motion and clear it after close; the application owns toggling\n * and switching the surface. Off-window identities survive virtualization.\n */\n @Prop() surfaceOpenRowId: string | undefined;\n\n /** Initial loading state. Existing rows stay visible; incremental loading uses loadingMore. */\n @Prop() loading: boolean = false;\n /** Replace opted-in table-owned caption controls with same-size visual skeletons. */\n @Prop() chromeLoading: boolean = false;\n /** Initial-loading rows. Defaults to ten so bounded tables retain a useful filled viewport. */\n @Prop() skeletonRows: number = 10;\n @Prop() emptyHeading: string = 'No results';\n @Prop() emptyBody: string = 'No data is available.';\n /** Initial error state. Existing rows stay visible; incremental failures use loadMoreError. */\n @Prop() error: boolean = false;\n @Prop() errorHeading: string = 'Unable to load data';\n @Prop() errorBody: string = 'The data could not be loaded.';\n @Prop() emptyCellLabel: string = 'Not available';\n\n /** Top-level data-window strategy. Virtual mode recycles row DOM only. */\n @Prop() dataMode: TableDataMode = 'infinite';\n /** Opt in to the table-owned control for choosing between supported data modes. */\n @Prop() dataModeSwitcher: boolean = false;\n @Prop() dataModeSwitcherLabel: string = 'Change table variation';\n @Prop() dataModeMenuLabel: string = 'Table variation';\n @Prop() infiniteModeLabel: string = 'Infinite scroll';\n @Prop() paginationModeLabel: string = 'Pagination + Infinite groups';\n @Prop() virtualModeLabel: string = 'Virtual scroll';\n /** Controlled top-level pagination state. Required when dataMode is pagination. */\n @Prop() pagination: TablePaginationState | null = null;\n @Prop() loadMoreMode: TableLoadMoreMode = 'auto';\n @Prop() hasMore: boolean = false;\n @Prop() loadingMore: boolean = false;\n @Prop() loadMoreError: string | undefined;\n /** Reset key for a new query/group/sort dataset. */\n @Prop() loadIdentity: string | number = 'default';\n /** IntersectionObserver root margin in CSS pixels. */\n @Prop() loadMoreThreshold: number = 0;\n @Prop() loadMoreLabel: string = 'Load more';\n @Prop() retryLabel: string = 'Retry';\n @Prop() loadingMoreLabel: string = 'Loading more items';\n @Prop() endOfResultsLabel: string = 'All results loaded';\n /** Supports {count} and {total} placeholders. */\n @Prop() rowsLoadedLabel: string = '{count} more rows loaded. {total} rows loaded.';\n @Prop() groupLoadMoreLabel: string = 'Load more';\n /** Supports the {group} placeholder. */\n @Prop() groupLoadMoreAriaLabel: string = 'Load more {group} results';\n /** Supports the {group} placeholder. */\n @Prop() groupRetryLabel: string = 'Retry loading {group} results';\n /** Supports the {group} placeholder. */\n @Prop() groupLoadingMoreLabel: string = 'Loading more items';\n /** Supports the {group} placeholder. */\n @Prop() groupEndOfResultsLabel: string = 'All {group} results loaded';\n /** Supports {group}, {count}, {loaded}, and {total} placeholders. */\n @Prop() groupRowsLoadedLabel: string =\n '{count} more rows loaded in {group}. {loaded} of {total} rows loaded.';\n\n @Event() dsSortChange!: EventEmitter<DataSortChangeDetail>;\n @Event() dsGroupCollapseChange!: EventEmitter<TableGroupCollapseChangeDetail>;\n @Event() dsSelectionChange!: EventEmitter<TableSelectionChangeDetail>;\n @Event() dsLoadMore!: EventEmitter<TableLoadMoreDetail>;\n @Event() dsGroupLoadMore!: EventEmitter<TableGroupLoadMoreDetail>;\n @Event() dsPaginationChange!: EventEmitter<PaginationChangeDetail>;\n @Event() dsCellAction!: EventEmitter<TableCellActionDetail>;\n @Event() dsRowActivate!: EventEmitter<TableRowActivateDetail>;\n @Event() dsFieldsConfigChange!: EventEmitter<DataFieldsConfigChangeDetail>;\n @Event() dsDataModeChange!: EventEmitter<TableDataModeChangeDetail>;\n\n @State() private overflowStart = false;\n @State() private overflowEnd = false;\n @State() private scrollable = false;\n @State() private announcement = '';\n @State() private activeStickyGroupId: string | null = null;\n @State() private viewportFitSettled = false;\n @State() private headerPresent = false;\n @State() private headerUsesToolbar = false;\n @State() private footerSlotPresence = 0;\n @State() private fitPageSize: number | undefined;\n @State() private actionMenu: { rowId: string; columnId: string } | null = null;\n @State() private actionMenuInitialFocusVisible = false;\n @State() private actionMenuSurface: { rowId: string; columnId: string } | null = null;\n @State() private truncateTooltipLabel = '';\n @State() private captionCompact = false;\n @State() private narrowViewport = false;\n @State() private columnCustomizerOpen = false;\n @State() private columnCustomizerSurfaceOpen = false;\n @State() private columnCustomizerInitialFocusVisible = false;\n @State() private dataModeSwitcherOpen = false;\n @State() private dataModeSwitcherSurfaceOpen = false;\n @State() private dataModeSwitcherInitialFocusVisible = false;\n @State() private virtualWindow: TableVirtualPlan | null = null;\n @State() private activeCell: TableCellAddress | null = null;\n @State() private editingCell: TableCellAddress | null = null;\n private editDraft = '';\n private gridModelCache?: ReturnType<typeof tableGridModel>;\n private get gridEnabled(): boolean {\n return this.interactionMode === 'grid' && !this.grouped;\n }\n private get editingEnabled(): boolean {\n return this.interactionMode !== 'table' && !this.grouped;\n }\n private get gridModel() {\n if (\n !this.gridModelCache ||\n this.gridModelCache.rows !== this.rows ||\n this.gridModelCache.columns !== this.visibleColumns\n ) {\n this.gridModelCache = tableGridModel(this.rows, this.visibleColumns);\n }\n return this.gridModelCache;\n }\n private gridAddress(event: Event): TableCellAddress | null {\n if (\n !this.gridEnabled ||\n !(event.target instanceof HTMLElement) ||\n !event.target.matches('td[role=\"gridcell\"][data-cell-editable=\"true\"]')\n )\n return null;\n const rowId = event.target.closest<HTMLElement>('[data-row-id]')?.dataset.rowId;\n const columnId = event.target.dataset.columnId;\n return rowId && columnId ? { rowId, columnId } : null;\n }\n private selectCell(address: TableCellAddress, extend = false): void {\n if (!this.isEditableAddress(address)) return;\n this.activeCell = address;\n this.dsCellRangeChange.emit({\n anchor: extend ? (this.cellRange?.anchor ?? address) : address,\n focus: address,\n });\n }\n private async focusCell(address: TableCellAddress): Promise<void> {\n if (!this.isEditableAddress(address)) return;\n this.activeCell = address;\n this.focusedRowId = address.rowId;\n await this.scrollRowIntoView(address.rowId);\n await this.nextAnimationFrame();\n const cell = this.findRenderedRow(address.rowId)?.querySelector<HTMLElement>(\n `[data-column-id=\"${CSS.escape(address.columnId)}\"]`\n );\n if (this.gridEnabled) cell?.focus();\n else\n await cell\n ?.querySelector<HTMLDsButtonUnfilledElement>('.ds-table__edit-trigger ds-button-unfilled')\n ?.setFocus();\n }\n private isEditableAddress(address: TableCellAddress): boolean {\n const model = this.gridModel;\n const row = model.rows[model.rowIndices.get(address.rowId) ?? -1];\n const column = model.columns[model.columnIndices.get(address.columnId) ?? -1];\n return this.editingEnabled && !this.loading && !!row && !!column && tableEditable(row, column);\n }\n private beginCellEdit(address: TableCellAddress, initial?: string): void {\n const model = this.gridModel;\n const row = model.rows[model.rowIndices.get(address.rowId) ?? -1];\n const column = model.columns[model.columnIndices.get(address.columnId) ?? -1];\n if (!this.editingEnabled || this.loading || !row || !column || !tableEditable(row, column))\n return;\n this.activeCell = address;\n this.focusedRowId = address.rowId;\n this.editDraft = initial ?? String(row.cells[column.id] ?? '');\n this.editingCell = address;\n requestAnimationFrame(async () => {\n const control = this.el.querySelector<HTMLDsInputElement>('.ds-table__cell-editor > *');\n await control?.componentOnReady?.();\n if (this.editingCell !== address || !control?.isConnected) return;\n await control.setFocus();\n const input = control.querySelector<HTMLInputElement | HTMLTextAreaElement>('input,textarea');\n if (initial == null && input?.type !== 'number') input?.select();\n });\n }\n private handleGridKeyDown(event: KeyboardEvent): void {\n const address = this.gridAddress(event);\n if (!address || event.altKey || event.metaKey || event.ctrlKey || event.isComposing) return;\n if (event.key === 'Enter' || event.key === 'F2' || event.key.length === 1) {\n event.preventDefault();\n this.beginCellEdit(address, event.key.length === 1 ? event.key : undefined);\n return;\n }\n if (!['ArrowDown', 'ArrowUp', 'ArrowRight', 'ArrowLeft', 'Home', 'End'].includes(event.key))\n return;\n event.preventDefault();\n const next = tableNextEditable(this.gridModel, address, event.key);\n if (!next) return;\n this.selectCell(next, event.shiftKey);\n void this.focusCell(next);\n }\n private handleGridClipboard(event: ClipboardEvent): void {\n const address = this.gridAddress(event);\n if (!address || !event.clipboardData) return;\n if (event.type === 'paste') {\n event.preventDefault();\n if (this.loading) return;\n const changes = tablePasteChanges(\n this.gridModel,\n address,\n event.clipboardData.getData('text/plain')\n );\n if (!changes) {\n this.announcement =\n 'Paste rejected. Use a rectangular set of valid values in editable cells.';\n return;\n }\n this.dsCellsChange.emit({ changes, reason: 'paste' });\n this.announcement = `${changes.length} cell changes proposed.`;\n } else {\n const model = this.gridModel;\n const bounds = tableRangeBounds(model, this.cellRange ?? { anchor: address, focus: address });\n if (\n !bounds ||\n (bounds.lastRow - bounds.firstRow + 1) * (bounds.lastColumn - bounds.firstColumn + 1) >\n 10000\n )\n return;\n const text = model.rows\n .slice(bounds.firstRow, bounds.lastRow + 1)\n .map(row =>\n model.columns\n .slice(bounds.firstColumn, bounds.lastColumn + 1)\n .map(column => {\n if (!tableEditable(row, column)) return '';\n const value = row.cells[column.id];\n return typeof value === 'object' ? '' : String(value ?? '').replace(/[\\t\\r\\n]/g, ' ');\n })\n .join('\\t')\n )\n .join('\\n');\n event.preventDefault();\n event.clipboardData.setData('text/plain', text);\n }\n }\n private renderCellEditor(row: TableRow, column: TableColumn) {\n if (\n this.editingCell?.rowId !== row.id ||\n this.editingCell.columnId !== column.id ||\n !tableEditable(row, column) ||\n this.loading\n )\n return null;\n const address = this.editingCell;\n const editor = column.editor!;\n const cell = this.findRenderedRow(row.id)?.querySelector<HTMLElement>(\n `[data-column-id=\"${CSS.escape(column.id)}\"]`\n );\n const commit = (text: string, next = address) => {\n if (this.editingCell !== address || !this.isEditableAddress(address)) return false;\n const value = tableEditValue(text, column);\n if (value === undefined) {\n this.announcement = `Enter a valid value for ${column.label}.`;\n return false;\n }\n this.editingCell = null;\n this.dsCellsChange.emit({ changes: [{ ...address, value }], reason: 'edit' });\n void this.focusCell(next);\n return true;\n };\n return (\n <div\n class=\"ds-table__cell-editor\"\n key={`${row.id}:${column.id}`}\n onClick={event => event.stopPropagation()}\n onFocusout={event => {\n const wrapper = event.currentTarget as HTMLElement;\n requestAnimationFrame(() => {\n const root = wrapper.getRootNode() as Document | ShadowRoot;\n if (this.editingCell === address && !wrapper.contains(root.activeElement))\n this.editingCell = null;\n });\n }}\n onKeyDown={event => {\n event.stopPropagation();\n if (event.isComposing || event.defaultPrevented) return;\n const wrapper = event.currentTarget as HTMLElement;\n if (event.key === 'Escape') {\n event.preventDefault();\n this.editingCell = null;\n void this.focusCell(address);\n return;\n }\n if (event.key !== 'Enter' && event.key !== 'Tab') return;\n // Picker keyboard interactions belong to the picker, including its internal Tab stops.\n if ((event.target as HTMLElement).closest('[popover]')) return;\n if (\n event.key === 'Enter' &&\n editor.type === 'textarea' &&\n !event.ctrlKey &&\n !event.metaKey\n )\n return;\n event.preventDefault();\n const input =\n editor.type === 'select'\n ? null\n : wrapper.querySelector<HTMLInputElement | HTMLTextAreaElement>('input,textarea');\n // Unconstrained numeric columns retain decimal support; explicit steps use the\n // shared control's native step validity as well as the paste validator.\n if (\n input &&\n !input.checkValidity() &&\n !(editor.type === 'number' && editor.step == null && input.validity.stepMismatch)\n ) {\n input.reportValidity();\n return;\n }\n const next =\n event.key === 'Tab'\n ? (tableNextEditable(this.gridModel, address, 'Tab', event.shiftKey) ?? address)\n : address;\n commit(input?.value ?? this.editDraft, next);\n }}\n >\n {renderTableCellEditor(\n editor,\n this.editDraft,\n `Edit ${column.label} for ${row.selectionLabel ?? row.id}`,\n cell ?? undefined,\n value => {\n this.editDraft = value;\n // A single-select choice is a complete edit; let its selection handler finish first.\n if (editor.type === 'select') queueMicrotask(() => commit(value));\n }\n )}\n </div>\n );\n }\n\n private renderCellEditTrigger(row: TableRow, column: TableColumn) {\n if (\n this.interactionMode !== 'edit' ||\n !tableEditable(row, column) ||\n this.loading ||\n (this.editingCell?.rowId === row.id && this.editingCell.columnId === column.id)\n )\n return null;\n return (\n <span class=\"ds-table__edit-trigger\">\n <ds-button-unfilled\n variant=\"icon\"\n icon=\"Pencil\"\n size=\"sm\"\n aria-label={`Edit ${column.label} for ${row.selectionLabel ?? row.id}`}\n pressScale={false}\n onDsClick={event => {\n event.stopPropagation();\n this.beginCellEdit({ rowId: row.id, columnId: column.id });\n }}\n />\n </span>\n );\n }\n\n private gridCellAttributes(row: TableRow, column: TableColumn): Record<string, unknown> {\n const model = this.gridModel;\n const bounds = tableRangeBounds(model, this.cellRange);\n const ri = model.rowIndices.get(row.id)!,\n ci = model.columnIndices.get(column.id)!;\n const editable = tableEditable(row, column) && !this.loading;\n const editing =\n editable && this.editingCell?.rowId === row.id && this.editingCell.columnId === column.id;\n const active =\n this.activeCell && this.isEditableAddress(this.activeCell)\n ? this.activeCell\n : model.firstEditable;\n if (!this.gridEnabled)\n return {\n 'data-cell-editable': String(editable),\n 'data-cell-editing': String(editing),\n 'aria-disabled': editable ? undefined : 'true',\n };\n return {\n role: 'gridcell',\n tabIndex: editable\n ? active?.rowId === row.id && active.columnId === column.id\n ? 0\n : -1\n : undefined,\n 'data-cell-editable': String(editable),\n 'data-cell-editing': String(editing),\n 'aria-readonly': String(!editable),\n 'aria-disabled': editable ? undefined : 'true',\n 'aria-selected': String(\n editable &&\n !!bounds &&\n ri >= bounds.firstRow &&\n ri <= bounds.lastRow &&\n ci >= bounds.firstColumn &&\n ci <= bounds.lastColumn\n ),\n onClick: (event: MouseEvent) => {\n if (\n event.target !== event.currentTarget &&\n (event.target as HTMLElement).closest(\n 'button,a,input,textarea,ds-button-unfilled,.ds-table__cell-editor'\n )\n )\n return;\n event.stopPropagation();\n if (!editable) return;\n this.selectCell({ rowId: row.id, columnId: column.id }, event.shiftKey);\n (event.currentTarget as HTMLElement).focus();\n if (!event.shiftKey) this.beginCellEdit({ rowId: row.id, columnId: column.id });\n },\n };\n }\n\n private readonly actionMenuElementId = nextTableActionMenuElementId();\n private structureCache?: {\n columns: TableColumn[];\n rows: TableRow[];\n spans: TableCellSpan[];\n selectable: boolean;\n grouped: boolean;\n windowed: boolean;\n grid: boolean;\n pins: ReturnType<typeof tablePinnedColumns>;\n bands: ReturnType<typeof tableHeaderBands>;\n merges: ReturnType<typeof tableSpanModel>;\n };\n\n private get structure() {\n const columns = this.visibleColumns;\n const model = this.createRenderModel();\n const cached = this.structureCache;\n const windowed = this.rowWindowingEnabled;\n const grid = this.interactionMode !== 'table';\n if (\n cached &&\n cached.columns === columns &&\n cached.rows === this.rows &&\n cached.spans === this.cellSpans &&\n cached.selectable === model.selectable &&\n cached.grouped === model.grouped &&\n cached.windowed === windowed &&\n cached.grid === grid\n )\n return cached;\n return (this.structureCache = {\n columns,\n rows: this.rows,\n spans: this.cellSpans,\n selectable: model.selectable,\n grouped: model.grouped,\n windowed,\n grid,\n pins: tablePinnedColumns(columns, model.selectable),\n bands: tableHeaderBands(columns, model.selectable, model.elasticSpacerIndex),\n merges: tableSpanModel(this.cellSpans, this.rows, columns, {\n windowed,\n grouped: model.grouped,\n grid,\n selectable: model.selectable,\n spacer: model.elasticSpacerIndex,\n }),\n });\n }\n\n private headerId(columnId: string): string {\n return `${this.actionMenuElementId}-header-${encodeURIComponent(columnId)}`;\n }\n\n private columnBandId(columnId: string): string | undefined {\n const model = this.createRenderModel();\n const index = this.visibleColumns.findIndex(column => column.id === columnId);\n const lane =\n index +\n 1 +\n Number(model.selectable) +\n Number(model.elasticSpacerIndex != null && index >= model.elasticSpacerIndex);\n const band = this.structure.bands.find(\n band => band.label && lane >= band.start && lane < band.start + band.span\n );\n return band ? `${this.actionMenuElementId}-${band.key}` : undefined;\n }\n private readonly columnCustomizerElementId = nextTableColumnCustomizerElementId();\n private readonly dataModeSwitcherElementId = nextTableDataModeSwitcherElementId();\n private truncateTooltipEl?: HTMLDsTooltipElement;\n private truncateAnchor: HTMLElement | null = null;\n private truncateTooltipBound = false;\n private focusedRowId: string | null = null;\n private virtualRenderFocus: HTMLElement | null = null;\n private virtualItems: TableVirtualItem[] = [];\n private visibleColumnsCache: {\n columns: TableColumn[];\n columnCustomizer: boolean;\n hiddenFieldIds: string[];\n fieldOrder: string[];\n value: TableColumn[];\n } | null = null;\n private renderModelCache: {\n columns: TableColumn[];\n rows: TableRow[];\n groups: DataGroup[];\n grouped: boolean;\n selectionMode: TableSelectionMode;\n selectedRowIds: string[];\n collapsedGroupIds: string[];\n virtualCounts: boolean;\n value: TableRenderModel;\n } | null = null;\n private highlightMatcherCache: {\n terms: string[];\n value: TableHighlightMatcher;\n } | null = null;\n private virtualItemsCache: {\n columns: TableColumn[];\n rows: TableRow[];\n groups: DataGroup[];\n grouped: boolean;\n collapsedGroupIds: string[];\n } | null = null;\n private rootEl: HTMLElement | null = null;\n private viewportEl: HTMLElement | null = null;\n private frameEl: HTMLElement | null = null;\n private interactiveHeadEl: HTMLTableSectionElement | null = null;\n private collapseAllOverlayEl: HTMLElement | null = null;\n private stickyHeaderTableEl: HTMLTableElement | null = null;\n private tableEl: HTMLTableElement | null = null;\n private sentinelEl: HTMLElement | null = null;\n private incrementalWindowActive = false;\n private readonly groupSentinelEls = new Map<string, HTMLElement>();\n private previousModelWarning = '';\n private modelWarningQueued = false;\n private initialModelWarningFrame: number | undefined;\n private hasLoaded = false;\n private renderedModel: TableRenderModel | null = null;\n private stickyGroupConnected = false;\n private headerSlotObserver: MutationObserver | null = null;\n private footerSlotObserver: MutationObserver | null = null;\n private captionCompactDisconnect: (() => void) | undefined;\n private fitResizeObserver: ResizeObserver | null = null;\n private fitObservedViewport: HTMLElement | null = null;\n private fitObservedTable: HTMLTableElement | null = null;\n private fitMeasurementPending = false;\n private readonly bodyRenderer = new TableBodyRenderer();\n private readonly layoutController = new TableLayoutController({\n elements: () => ({\n viewport: this.viewportEl,\n contentTable: this.tableEl,\n stickyHeaderTable: this.stickyHeaderTableEl,\n collapseAllOverlay: this.collapseAllOverlayEl,\n frame: this.frameEl,\n interactiveHead: this.interactiveHeadEl,\n }),\n mode: () => ({\n documentStickyHeader: this.documentStickyHeader,\n floatingCollapseAll: this.renderedModel?.collapseAllHost?.mode === 'floating',\n clampVerticalOverscroll: this.fitViewport && this.viewportFitSettled,\n }),\n verticalEdgeWheel: deltaY => this.viewportFitController.scrollOuterBy(deltaY),\n narrowChanged: narrow => {\n if (this.narrowViewport !== narrow) this.narrowViewport = narrow;\n },\n overflowChanged: state => {\n if (state.start !== this.overflowStart) this.overflowStart = state.start;\n if (state.end !== this.overflowEnd) this.overflowEnd = state.end;\n if (state.scrollable !== this.scrollable) this.scrollable = state.scrollable;\n },\n });\n private readonly loadController = new TableLoadController({\n state: () => ({\n lazyLoading: this.dataMode === 'infinite' && !this.grouped,\n loadMoreMode: this.loadMoreMode,\n hasMore: this.hasMore,\n loadingMore: this.loadingMore,\n loadMoreError: this.loadMoreError,\n loadIdentity: this.loadIdentity,\n loadMoreThreshold: this.loadMoreThreshold,\n containedScroll: this.containedScroll,\n loadingMoreLabel: this.loadingMoreLabel,\n endOfResultsLabel: this.endOfResultsLabel,\n rowsLoadedLabel: this.rowsLoadedLabel,\n loadedRowCount: this.currentLoadedRowCount(),\n viewport: this.viewportEl,\n sentinel: this.sentinelEl,\n }),\n announce: message => {\n this.announcement = message;\n },\n request: detail => {\n this.dsLoadMore.emit(detail);\n },\n });\n private readonly groupLoadController = new TableGroupLoadController({\n state: () => ({\n enabled: this.grouped && this.dataMode !== 'virtual',\n loadMoreMode: this.loadMoreMode,\n loadMoreThreshold: this.loadMoreThreshold,\n containedScroll: this.containedScroll,\n groups: this.groups,\n viewport: this.viewportEl,\n sentinels: this.groupSentinelEls,\n loadingMoreLabel: this.groupLoadingMoreLabel,\n endOfResultsLabel: this.groupEndOfResultsLabel,\n rowsLoadedLabel: this.groupRowsLoadedLabel,\n }),\n announce: message => {\n this.announcement = message;\n },\n request: detail => {\n this.dsGroupLoadMore.emit(detail);\n },\n });\n private readonly virtualController = new TableVirtualController({\n state: () => ({\n enabled: this.rowWindowingEnabled,\n items: this.virtualItems,\n pinnedRowIds: this.virtualPinnedRowIds(),\n viewport: this.viewportEl,\n viewportSize: this.estimateVirtualViewportSize(),\n }),\n windowChanged: plan => {\n this.virtualWindow = plan;\n },\n });\n private readonly viewportFitController = new TableViewportFitController({\n enabled: () => this.fitViewport,\n elements: () => ({ host: this.el, surface: this.rootEl }),\n insets: () => ({\n blockStart: this.viewportInsetBlockStart,\n blockEnd: this.viewportInsetBlockEnd,\n }),\n fitChanged: (metrics: TableViewportFitMetrics | null) => {\n const settled = metrics?.settled ?? false;\n if (settled !== this.viewportFitSettled) this.viewportFitSettled = settled;\n },\n });\n\n componentWillLoad(): void {\n // Reserve the final viewport-fit height before the first paint. The fitted\n // surface is connected after render, but the host can already resolve its\n // owning scrollport and prevent an initial auto-height frame.\n this.viewportFitController.connect();\n this.incrementalWindowActive = this.hasIncrementalState;\n this.syncHeaderSlotPresence();\n this.syncFooterSlotPresence();\n this.loadController.initialize();\n this.groupLoadController.initialize();\n }\n\n componentDidLoad(): void {\n this.hasLoaded = true;\n this.layoutController.connect();\n this.loadController.connect();\n this.groupLoadController.connect();\n this.virtualController.connect();\n this.viewportFitController.connect();\n this.syncStickyGroupConnection();\n this.connectHeaderSlotObserver();\n this.connectFooterSlotObserver();\n this.connectFitObserver();\n this.connectCaptionCompactObserver();\n this.syncFitPageSize();\n this.connectTruncateTooltip();\n this.scheduleInitialModelIssueWarning();\n }\n\n componentWillRender(): void {\n const root = this.el.getRootNode() as Document | ShadowRoot;\n const active = root.activeElement;\n this.virtualRenderFocus =\n this.rowWindowingEnabled &&\n active instanceof HTMLElement &&\n this.tableEl?.contains(active) &&\n active.closest('[data-row-id]')\n ? active\n : null;\n }\n\n componentDidRender(): void {\n // Moving a retained pooled row can clear native focus in Firefox. Restore\n // only the same connected node, without scrolling or stealing external focus.\n const retainedFocus = this.virtualRenderFocus;\n this.virtualRenderFocus = null;\n if (retainedFocus?.isConnected) {\n const root = retainedFocus.getRootNode() as Document | ShadowRoot;\n if (!root.activeElement || root.activeElement === this.el.ownerDocument.body) {\n // Resolve the moved rows' scroll geometry before restoring native focus.\n // WebKit otherwise may reveal the caret using the pre-patch layout even\n // with preventScroll. Preserve both axes within the resulting bounds.\n const viewport = this.viewportEl;\n const top = viewport?.scrollTop ?? 0;\n const left = viewport?.scrollLeft ?? 0;\n retainedFocus.focus({ preventScroll: true });\n if (viewport) {\n if (viewport.scrollTop !== top) viewport.scrollTop = top;\n if (viewport.scrollLeft !== left) viewport.scrollLeft = left;\n }\n }\n }\n this.layoutController.refresh(false);\n this.loadController.refresh();\n this.groupLoadController.refresh();\n this.virtualController.refresh();\n this.virtualController.collectMeasurements(this.tableEl);\n if (\n this.rowWindowingEnabled &&\n this.truncateAnchor &&\n this.tableEl &&\n !this.tableEl.contains(this.truncateAnchor)\n ) {\n this.dismissTruncateTooltip();\n }\n this.viewportFitController.refresh(false);\n this.syncStickyGroupConnection();\n this.connectFitObserver();\n this.connectCaptionCompactObserver();\n this.syncFitPageSize();\n if (this.stickyGroupConnected) this.updateStickyGroup();\n }\n\n /**\n * Scroll the nearest vertical owner only enough to reveal a supplied row.\n * Resolves false when the row is absent from the supplied, expanded table model.\n * This method never moves focus.\n */\n @Method()\n async scrollRowIntoView(rowId: string): Promise<boolean> {\n if (!rowId) return false;\n\n if (this.rowWindowingEnabled) {\n const headerSize = this.tableEl?.tHead?.getBoundingClientRect().height ?? 0;\n const revealed = this.virtualController.scrollRowIntoView(\n rowId,\n headerSize,\n this.stickyHeader ? headerSize : 0\n );\n if (!revealed) return false;\n await this.nextAnimationFrame();\n }\n\n const row = this.findRenderedRow(rowId);\n if (!row) return false;\n this.revealRenderedRow(row);\n return true;\n }\n\n connectedCallback(): void {\n if (!this.hasLoaded) return;\n this.layoutController.connect();\n this.loadController.connect();\n this.groupLoadController.connect();\n this.virtualController.connect();\n this.viewportFitController.connect();\n this.syncStickyGroupConnection();\n this.connectHeaderSlotObserver();\n this.connectFooterSlotObserver();\n this.connectFitObserver();\n this.connectCaptionCompactObserver();\n this.connectTruncateTooltip();\n }\n\n disconnectedCallback(): void {\n this.editingCell = null;\n this.layoutController.disconnect();\n this.loadController.disconnect();\n this.groupLoadController.disconnect();\n this.virtualController.disconnect();\n this.viewportFitController.disconnect();\n this.disconnectStickyGroup();\n this.headerSlotObserver?.disconnect();\n this.headerSlotObserver = null;\n this.footerSlotObserver?.disconnect();\n this.footerSlotObserver = null;\n this.disconnectFitObserver();\n this.disconnectCaptionCompactObserver();\n this.disconnectTruncateTooltip();\n this.closeColumnCustomizer();\n if (this.initialModelWarningFrame !== undefined) {\n cancelAnimationFrame(this.initialModelWarningFrame);\n this.initialModelWarningFrame = undefined;\n }\n }\n\n private syncHeaderSlotPresence = () => {\n const header = this.el.querySelector<HTMLElement>('[slot=\"header\"]');\n this.headerPresent = !!header;\n this.headerUsesToolbar = header?.tagName === 'DS-DATA-TOOLBAR';\n };\n\n private connectHeaderSlotObserver(): void {\n if (this.headerSlotObserver || typeof MutationObserver === 'undefined') return;\n this.headerSlotObserver = new MutationObserver(this.syncHeaderSlotPresence);\n this.headerSlotObserver.observe(this.el, { childList: true });\n this.syncHeaderSlotPresence();\n }\n\n private syncFooterSlotPresence = () => {\n let presence = 0;\n if (hasOwnedTableFooterSlot(this.el, 'footer-leading')) presence |= TABLE_FOOTER_SLOT_LEADING;\n if (hasOwnedTableFooterSlot(this.el, 'footer')) presence |= TABLE_FOOTER_SLOT_COPY;\n if (hasOwnedTableFooterSlot(this.el, 'footer-trailing')) presence |= TABLE_FOOTER_SLOT_TRAILING;\n if (presence !== this.footerSlotPresence) this.footerSlotPresence = presence;\n };\n\n private connectFooterSlotObserver(): void {\n if (this.footerSlotObserver || typeof MutationObserver === 'undefined') return;\n this.footerSlotObserver = new MutationObserver(this.syncFooterSlotPresence);\n this.footerSlotObserver.observe(this.el, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['slot'],\n });\n this.syncFooterSlotPresence();\n }\n\n private connectStickyGroup(): void {\n if (this.stickyGroupConnected || typeof window === 'undefined') return;\n this.stickyGroupConnected = true;\n document.addEventListener('scroll', this.updateStickyGroup, {\n capture: true,\n passive: true,\n });\n window.addEventListener('resize', this.updateStickyGroup, { passive: true });\n this.updateStickyGroup();\n }\n\n private syncStickyGroupConnection(): void {\n if (this.documentStickyHeader && this.grouped) {\n this.connectStickyGroup();\n return;\n }\n\n this.disconnectStickyGroup();\n this.frameEl?.style.removeProperty('--_table-sticky-group-top');\n if (this.activeStickyGroupId !== null) {\n this.activeStickyGroupId = null;\n }\n }\n\n private disconnectStickyGroup(): void {\n if (!this.stickyGroupConnected || typeof window === 'undefined') return;\n this.stickyGroupConnected = false;\n document.removeEventListener('scroll', this.updateStickyGroup, true);\n window.removeEventListener('resize', this.updateStickyGroup);\n }\n\n private readonly updateStickyGroup = (): void => {\n if (!this.documentStickyHeader || !this.grouped || !this.frameEl || !this.tableEl) {\n if (this.activeStickyGroupId !== null) this.activeStickyGroupId = null;\n return;\n }\n\n const stickyHeader = this.frameEl.querySelector<HTMLElement>(\n '.ds-table__document-sticky-header'\n );\n if (!stickyHeader) return;\n const threshold = stickyHeader.getBoundingClientRect().bottom;\n let activeGroupId: string | null = null;\n let activeGroupTop = threshold;\n for (const body of this.tableEl.querySelectorAll<HTMLElement>('tbody[data-group-id]')) {\n const source = body.querySelector<HTMLElement>('.ds-table__group-cell');\n if (!source) continue;\n const sourceRect = source.getBoundingClientRect();\n const bodyRect = body.getBoundingClientRect();\n if (sourceRect.top <= threshold && bodyRect.bottom > threshold) {\n activeGroupId = body.dataset.groupId ?? null;\n // A sticky section is bounded by its own row group. As that boundary\n // approaches the sticky lane, move the outgoing section bar with it so\n // the incoming source bar pushes it away instead of rendering beneath it.\n activeGroupTop = Math.min(threshold, bodyRect.bottom - sourceRect.height);\n }\n }\n\n const frameRect = this.frameEl.getBoundingClientRect();\n this.frameEl.style.setProperty(\n '--_table-sticky-group-top',\n `${activeGroupTop - frameRect.top}px`\n );\n\n if (activeGroupId !== this.activeStickyGroupId) {\n this.activeStickyGroupId = activeGroupId;\n return;\n }\n };\n\n @Watch('columns')\n @Watch('grouping')\n @Watch('hiddenFieldIds')\n @Watch('fieldOrder')\n @Watch('columnCustomizer')\n @Watch('cellSpans')\n @Watch('interactionMode')\n @Watch('responsiveLayout')\n handleStructureChange(): void {\n this.virtualItemsCache = null;\n this.scheduleModelIssueWarning();\n this.loadController.structureChanged();\n this.groupLoadController.structureChanged();\n this.virtualController.invalidateMeasures();\n }\n\n @Watch('rows')\n @Watch('groups')\n handleDataChange(): void {\n if (this.grouped) this.groupLoadController.dataChanged();\n else this.loadController.dataChanged();\n this.scheduleModelIssueWarning();\n this.syncActionMenu();\n this.virtualController.invalidateMeasures();\n }\n\n @Watch('totalCount')\n handleTotalCountChange(): void {\n this.scheduleModelIssueWarning();\n }\n\n @Watch('caption')\n handleCaptionChange(): void {\n this.scheduleModelIssueWarning();\n }\n\n @Watch('loadIdentity')\n handleLoadIdentityChange(): void {\n this.incrementalWindowActive = this.hasIncrementalState;\n this.loadController.identityChanged();\n this.virtualController.resetToTop();\n }\n\n @Watch('sort')\n @Watch('grouping')\n handleVirtualIndexReset(): void {\n this.virtualController.resetToTop();\n }\n\n @Watch('loadMoreMode')\n @Watch('loadMoreThreshold')\n handleLazyConfigurationChange(): void {\n this.loadController.configurationChanged();\n this.groupLoadController.configurationChanged();\n }\n\n @Watch('dataMode')\n handleDataModeChange(): void {\n this.incrementalWindowActive = this.hasIncrementalState;\n this.scheduleModelIssueWarning();\n this.handleLazyConfigurationChange();\n this.virtualController.resetToTop();\n }\n\n @Watch('pagination')\n handlePaginationChange(\n pagination: TablePaginationState | null,\n previous: TablePaginationState | null\n ): void {\n if (pagination?.pageSizeMode === 'fit' && pagination.fitIdentity !== previous?.fitIdentity) {\n this.fitMeasurementPending = true;\n }\n }\n\n @Watch('loadingMore')\n handleLoadingMoreChange(loading: boolean): void {\n if (loading) this.incrementalWindowActive = true;\n if (!this.grouped) this.loadController.loadingChanged(loading);\n }\n\n @Watch('loadMoreError')\n handleLoadMoreErrorChange(error: string | undefined): void {\n if (error?.trim()) this.incrementalWindowActive = true;\n if (!this.grouped) this.loadController.errorChanged(error);\n }\n\n @Watch('hasMore')\n handleHasMoreChange(hasMore: boolean, hadMore: boolean): void {\n if (hasMore) this.incrementalWindowActive = true;\n if (!this.grouped) this.loadController.hasMoreChanged(hasMore, hadMore);\n }\n\n @Watch('columnCustomizer')\n @Watch('captionVisibility')\n handleColumnCustomizerAvailability(): void {\n if (!this.showsColumnCustomizer) {\n this.closeColumnCustomizer();\n this.columnCustomizerSurfaceOpen = false;\n }\n }\n\n @Watch('dataModeSwitcher')\n @Watch('captionVisibility')\n handleDataModeSwitcherAvailability(): void {\n if (!this.showsDataModeSwitcher) {\n this.closeDataModeSwitcher();\n this.dataModeSwitcherSurfaceOpen = false;\n }\n }\n\n @Watch('chromeLoading')\n handleChromeLoadingChange(loading: boolean): void {\n if (!loading) return;\n this.closeColumnCustomizer();\n this.closeDataModeSwitcher();\n this.columnCustomizerSurfaceOpen = false;\n this.dataModeSwitcherSurfaceOpen = false;\n }\n\n private get grouped(): boolean {\n return this.grouping !== null;\n }\n\n private get hasIncrementalState(): boolean {\n return (\n this.dataMode === 'infinite' &&\n (this.hasMore || this.loadingMore || !!this.loadMoreError?.trim())\n );\n }\n\n private get selectable(): boolean {\n return this.selectionMode === 'multiple';\n }\n\n private get visibleColumns(): TableColumn[] {\n const cached = this.visibleColumnsCache;\n if (\n cached &&\n cached.columns === this.columns &&\n cached.columnCustomizer === this.columnCustomizer &&\n cached.hiddenFieldIds === this.hiddenFieldIds &&\n cached.fieldOrder === this.fieldOrder\n ) {\n return cached.value;\n }\n const value = resolveTableVisibleColumns(this.columns, {\n columnCustomizer: this.columnCustomizer,\n hiddenFieldIds: this.hiddenFieldIds,\n fieldOrder: this.fieldOrder,\n });\n this.visibleColumnsCache = {\n columns: this.columns,\n columnCustomizer: this.columnCustomizer,\n hiddenFieldIds: this.hiddenFieldIds,\n fieldOrder: this.fieldOrder,\n value,\n };\n return value;\n }\n\n private get showsColumnCustomizer(): boolean {\n return this.columnCustomizer && this.captionVisibility === 'visible';\n }\n\n private get showsDataModeSwitcher(): boolean {\n return this.dataModeSwitcher && this.captionVisibility === 'visible';\n }\n\n private get showsCaptionTrailing(): boolean {\n return (\n this.showsDataModeSwitcher ||\n (this.showsColumnCustomizer && !this.hideColumnCustomizerTrigger)\n );\n }\n\n private get documentStickyHeader(): boolean {\n return this.stickyHeader && !this.containedScroll && this.responsiveLayout !== 'cards';\n }\n\n private get fixedHeight(): boolean {\n return !this.fitViewport && this.resolvedHeight !== undefined;\n }\n\n private get boundedComposition(): boolean {\n return this.fitViewport || this.fixedHeight;\n }\n\n private get containedScroll(): boolean {\n return this.boundedComposition || this.resolvedMaxHeight !== undefined;\n }\n\n private connectFitObserver(): void {\n if (typeof ResizeObserver === 'undefined') return;\n if (this.fitObservedViewport === this.viewportEl && this.fitObservedTable === this.tableEl) {\n return;\n }\n this.disconnectFitObserver();\n this.fitObservedViewport = this.viewportEl;\n this.fitObservedTable = this.tableEl;\n this.fitResizeObserver = new ResizeObserver(this.syncFitPageSize);\n if (this.fitObservedViewport) this.fitResizeObserver.observe(this.fitObservedViewport);\n if (this.fitObservedTable) this.fitResizeObserver.observe(this.fitObservedTable);\n }\n\n private disconnectFitObserver(): void {\n this.fitResizeObserver?.disconnect();\n this.fitResizeObserver = null;\n this.fitObservedViewport = null;\n this.fitObservedTable = null;\n }\n\n private connectCaptionCompactObserver(): void {\n if (this.captionCompactDisconnect) return;\n this.captionCompactDisconnect = observeTableCaptionCompact(this.el, compact => {\n if (this.captionCompact !== compact) this.captionCompact = compact;\n });\n }\n\n private disconnectCaptionCompactObserver(): void {\n this.captionCompactDisconnect?.();\n this.captionCompactDisconnect = undefined;\n }\n\n private readonly syncFitPageSize = (): void => {\n const pagination = this.dataMode === 'pagination' ? this.pagination : null;\n if (\n !pagination?.fitToPage ||\n pagination.fitToPageInactive ||\n !this.containedScroll ||\n !this.viewportEl ||\n !this.tableEl\n ) {\n if (this.fitPageSize !== undefined) this.fitPageSize = undefined;\n this.fitMeasurementPending = false;\n return;\n }\n const header = this.tableEl.querySelector<HTMLElement>('.ds-table__head');\n const item = this.tableEl.querySelector<HTMLElement>(\n this.grouped ? '.ds-table__group-row' : '.ds-table__body .ds-table__row'\n );\n const itemBlockSize =\n item?.getBoundingClientRect().height ||\n resolveCssLengthPx(\n 'var(--ds-table-row-min-block-size, var(--dimension-size-500))',\n 0,\n this.rootEl ?? this.el\n );\n const next = resolveTableFitPageSize({\n viewportBlockSize: this.viewportEl.clientHeight,\n headerBlockSize: header?.getBoundingClientRect().height ?? 0,\n itemBlockSize,\n });\n if (next !== this.fitPageSize) this.fitPageSize = next;\n if (!this.fitMeasurementPending || pagination.pageSizeMode !== 'fit' || next === undefined) {\n return;\n }\n this.fitMeasurementPending = false;\n if (next === pagination.pageSize && pagination.pageIndex === 0) return;\n this.dsPaginationChange.emit({\n pageIndex: 0,\n pageSize: next,\n pageSizeMode: 'fit',\n totalItems: pagination.totalItems,\n pageSizeOptions: pagination.pageSizeOptions ?? [25, 50, 100, 200],\n fitToPage: true,\n fitToPageInactive: false,\n fitPageSize: next,\n fitPageSizeLabel: pagination.fitPageSizeLabel ?? 'Fit to page',\n fitPageSizeTriggerLabel: pagination.fitPageSizeTriggerLabel ?? 'Fit',\n itemLabel: pagination.itemLabel ?? 'items',\n pageSizeLabel: pagination.pageSizeLabel ?? 'Items',\n ariaLabel: pagination.ariaLabel ?? `${this.caption} pagination`,\n showFirstLastButtons: pagination.showFirstLastButtons ?? false,\n previousPageIndex: pagination.pageIndex,\n previousPageSize: pagination.pageSize,\n previousPageSizeMode: 'fit',\n reason: 'fit',\n });\n };\n\n @Listen('keydown', { target: 'window' })\n handleWindowPaginationKeyDown(event: KeyboardEvent): void {\n this.handlePaginationKeyDown(event, true);\n }\n\n private handlePaginationKeyDown(event: KeyboardEvent, fromWindow = false): void {\n const pagination = this.dataMode === 'pagination' ? this.pagination : null;\n if (\n !pagination ||\n this.loading ||\n event.defaultPrevented ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey ||\n event.shiftKey ||\n (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')\n ) {\n return;\n }\n\n if (paginationShortcutBlockedByPath(event.composedPath())) return;\n\n if (fromWindow) {\n const origin = event.composedPath()[0];\n const originNode = origin instanceof Text ? origin.parentNode : origin;\n if (\n !shouldHandleContainingPagePaginationShortcut({\n origin: originNode,\n table: this.el,\n eventPath: event.composedPath(),\n })\n ) {\n return;\n }\n }\n\n const state = resolvePaginationState(pagination);\n const pageIndex = event.key === 'ArrowLeft' ? state.pageIndex - 1 : state.pageIndex + 1;\n if (pageIndex < 0 || pageIndex >= state.totalPages) return;\n\n event.preventDefault();\n this.dsPaginationChange.emit({\n pageIndex,\n pageSize: state.pageSize,\n pageSizeMode: state.pageSizeMode,\n totalItems: state.totalItems,\n pageSizeOptions: state.pageSizeOptions,\n fitToPage: pagination.fitToPage ?? false,\n fitToPageInactive: pagination.fitToPageInactive ?? false,\n fitPageSize: this.fitPageSize ?? pagination.fitPageSize,\n fitPageSizeLabel: pagination.fitPageSizeLabel ?? 'Fit to page',\n fitPageSizeTriggerLabel: pagination.fitPageSizeTriggerLabel ?? 'Fit',\n itemLabel: pagination.itemLabel ?? 'items',\n pageSizeLabel: pagination.pageSizeLabel ?? 'Items',\n ariaLabel: pagination.ariaLabel ?? `${this.caption} pagination`,\n showFirstLastButtons: pagination.showFirstLastButtons ?? false,\n previousPageIndex: state.pageIndex,\n previousPageSize: state.pageSize,\n previousPageSizeMode: state.pageSizeMode,\n reason: 'page',\n });\n }\n\n private currentLoadedRows(): TableRow[] {\n return tableRows(this.rows, this.groups, this.grouped);\n }\n\n private currentLoadedRowCount(): number {\n return this.grouped\n ? this.groups.reduce((count, group) => count + group.rows.length, 0)\n : this.rows.length;\n }\n\n private createRenderModel(): TableRenderModel {\n const columns = this.visibleColumns;\n const grouped = this.grouped;\n const virtualCounts = this.dataMode === 'virtual';\n const cached = this.renderModelCache;\n if (\n cached &&\n cached.columns === columns &&\n cached.rows === this.rows &&\n cached.groups === this.groups &&\n cached.grouped === grouped &&\n cached.selectionMode === this.selectionMode &&\n cached.selectedRowIds === this.selectedRowIds &&\n cached.collapsedGroupIds === this.collapsedGroupIds &&\n cached.virtualCounts === virtualCounts\n ) {\n return cached.value;\n }\n const value = createTableRenderModel({\n columns,\n rows: this.rows,\n groups: this.groups,\n grouped,\n selectionMode: this.selectionMode,\n selectedRowIds: this.selectedRowIds,\n collapsedGroupIds: this.collapsedGroupIds,\n groupCountPresentation: virtualCounts ? 'total' : 'loaded-progress',\n });\n this.renderModelCache = {\n columns,\n rows: this.rows,\n groups: this.groups,\n grouped,\n selectionMode: this.selectionMode,\n selectedRowIds: this.selectedRowIds,\n collapsedGroupIds: this.collapsedGroupIds,\n virtualCounts,\n value,\n };\n return value;\n }\n\n private get highlightMatcher(): TableHighlightMatcher {\n if (this.highlightMatcherCache?.terms === this.highlightTerms) {\n return this.highlightMatcherCache.value;\n }\n const value = createTableHighlightMatcher(this.highlightTerms);\n this.highlightMatcherCache = { terms: this.highlightTerms, value };\n return value;\n }\n\n private get rowWindowingEnabled(): boolean {\n if (!this.hasVirtualViewportBounds) return false;\n if (this.dataMode === 'virtual') return true;\n return !this.grouped && this.currentLoadedRowCount() > TABLE_NON_VIRTUAL_WINDOW_THRESHOLD;\n }\n\n private get virtualViewportMissing(): boolean {\n return this.dataMode === 'virtual' && !this.hasVirtualViewportBounds;\n }\n\n private get hasVirtualViewportBounds(): boolean {\n return this.containedScroll && this.estimateVirtualViewportSize() > 0;\n }\n\n private estimateVirtualViewportSize(): number {\n const measured =\n this.virtualController.currentViewportSize() || this.viewportEl?.clientHeight || 0;\n const chrome =\n (this.captionVisibility === 'visible' ? 48 : 0) + (this.hasResultFooter ? 48 : 0);\n if (this.fixedHeight && this.resolvedHeight) {\n if (measured > 0) return measured;\n return Math.max(\n 0,\n resolveCssLengthPx(this.resolvedHeight, 0, this.rootEl ?? this.el) - chrome\n );\n }\n if (this.fitViewport) return Math.max(0, measured);\n if (this.resolvedMaxHeight) {\n if (this.resolvedMaxHeight.trim().toLowerCase() === 'none') return 0;\n return resolveCssLengthPx(this.resolvedMaxHeight, 0, this.rootEl ?? this.el);\n }\n return 0;\n }\n\n private virtualPinnedRowIds(): Set<string> {\n const pinned = new Set<string>();\n if (this.focusedRowId) pinned.add(this.focusedRowId);\n if (this.actionMenu?.rowId) pinned.add(this.actionMenu.rowId);\n return pinned;\n }\n\n private findRenderedRow(rowId: string): HTMLElement | null {\n return (\n [...this.el.querySelectorAll<HTMLElement>('[data-row-id]')].find(\n candidate => candidate.dataset.rowId === rowId\n ) ?? null\n );\n }\n\n private revealRenderedRow(row: HTMLElement): void {\n if (!this.containedScroll || !this.viewportEl) {\n row.scrollIntoView({ block: 'nearest', inline: 'nearest' });\n return;\n }\n\n const viewportRect = this.viewportEl.getBoundingClientRect();\n const rowRect = row.getBoundingClientRect();\n let visibleStart = viewportRect.top;\n if (this.stickyHeader) {\n visibleStart = Math.max(\n visibleStart,\n this.tableEl?.tHead?.getBoundingClientRect().bottom ?? visibleStart\n );\n }\n const groupRow = row.closest('tbody')?.querySelector<HTMLElement>('.ds-table__group-row');\n if (groupRow && getComputedStyle(groupRow).position === 'sticky') {\n visibleStart = Math.max(visibleStart, groupRow.getBoundingClientRect().bottom);\n }\n\n if (rowRect.top < visibleStart) {\n this.viewportEl.scrollTop += rowRect.top - visibleStart;\n } else if (rowRect.bottom > viewportRect.bottom) {\n this.viewportEl.scrollTop += rowRect.bottom - viewportRect.bottom;\n }\n }\n\n private nextAnimationFrame(): Promise<void> {\n return new Promise(resolve => requestAnimationFrame(() => resolve()));\n }\n\n private syncVirtualItems(model: TableRenderModel): void {\n if (!this.rowWindowingEnabled) {\n this.virtualItems = [];\n this.virtualItemsCache = null;\n this.bodyRenderer.resetVirtualState();\n return;\n }\n const columns = this.visibleColumns;\n const cached = this.virtualItemsCache;\n if (\n cached &&\n cached.columns === columns &&\n cached.rows === this.rows &&\n cached.groups === this.groups &&\n cached.grouped === model.grouped &&\n cached.collapsedGroupIds === this.collapsedGroupIds\n ) {\n return;\n }\n this.virtualItems = flattenTableVirtualItems({\n grouped: model.grouped,\n rows: this.rows,\n groups: this.groups,\n collapsedGroupIds: this.collapsedGroupIds,\n columns,\n });\n if (this.responsiveLayout === 'cards') {\n this.virtualItems = this.virtualItems.map(item => ({ ...item, variableSize: true }));\n }\n this.virtualItemsCache = {\n columns,\n rows: this.rows,\n groups: this.groups,\n grouped: model.grouped,\n collapsedGroupIds: this.collapsedGroupIds,\n };\n }\n\n private shouldWindowRows(model: TableRenderModel): boolean {\n return (\n this.rowWindowingEnabled && (model.hasData || (model.grouped && model.groups.length > 0))\n );\n }\n\n private get resolvedMaxHeight(): string | undefined {\n if (this.maxHeight == null || this.maxHeight === '') return undefined;\n return typeof this.maxHeight === 'number' ? `${Math.max(0, this.maxHeight)}px` : this.maxHeight;\n }\n\n private get resolvedHeight(): string | undefined {\n if (this.height == null || this.height === '') return undefined;\n return typeof this.height === 'number' ? `${Math.max(0, this.height)}px` : this.height;\n }\n\n private warnModelIssues(): void {\n const issues = tableModelIssues(this.columns, this.rows, this.groups, this.grouped);\n const visibleColumns = this.visibleColumns;\n if (!this.caption?.trim()) issues.unshift('A non-empty caption is required.');\n if (this.grouping) {\n const groupingColumn = this.columns.find(column => column.id === this.grouping!.fieldId);\n if (!groupingColumn) {\n issues.push(`Grouping references unknown column id: ${this.grouping.fieldId}`);\n } else if (!visibleColumns.some(column => column.id === groupingColumn.id)) {\n issues.push(`Grouping references hidden column id: ${this.grouping.fieldId}`);\n }\n }\n if (this.sort) {\n const sortColumn = this.columns.find(\n column =>\n column.id === this.sort!.fieldId ||\n column.segments?.some(segment => segment.sortKey === this.sort!.fieldId)\n );\n if (!sortColumn) {\n issues.push(`Sorting references unknown column id: ${this.sort.fieldId}`);\n } else if (!visibleColumns.some(column => column.id === sortColumn.id)) {\n issues.push(`Sorting references hidden column id: ${this.sort.fieldId}`);\n }\n }\n if (\n this.dataMode === 'virtual' &&\n Number.isFinite(this.totalCount) &&\n Math.max(0, Math.trunc(this.totalCount!)) !== this.currentLoadedRowCount()\n ) {\n issues.push('Virtual mode requires totalCount to match the complete supplied row count.');\n }\n const stickyStart = visibleColumns.filter(column => column.sticky === 'start');\n const stickyEnd = visibleColumns.filter(column => column.sticky === 'end');\n issues.push(...this.structure.merges.issues);\n if (this.interactionMode === 'grid' && this.grouped)\n issues.push('Grid interaction requires ungrouped rows.');\n for (const column of [...stickyStart, ...stickyEnd]) {\n if (!tableColumnSize(column))\n issues.push(`Sticky column ${column.id} requires an explicit size.`);\n }\n const message = issues.join(' ');\n if (!message) {\n this.previousModelWarning = '';\n return;\n }\n if (message === this.previousModelWarning) return;\n this.previousModelWarning = message;\n console.warn(`[ds-table] ${message}`);\n }\n\n private scheduleModelIssueWarning(): void {\n if (this.modelWarningQueued) return;\n this.modelWarningQueued = true;\n queueMicrotask(() => {\n this.modelWarningQueued = false;\n this.warnModelIssues();\n });\n }\n\n private scheduleInitialModelIssueWarning(): void {\n if (this.initialModelWarningFrame !== undefined) {\n cancelAnimationFrame(this.initialModelWarningFrame);\n }\n this.initialModelWarningFrame = requestAnimationFrame(() => {\n this.initialModelWarningFrame = undefined;\n this.warnModelIssues();\n });\n }\n\n private emitSort(column: TableColumn, sortKey = column.id): void {\n if (!column.sortable) return;\n this.dsSortChange.emit({ sort: nextDataSortState(this.sort, sortKey) });\n }\n\n private sortButtonLabel(column: TableColumn, sortKey = column.id, label = column.label): string {\n if (this.sort?.fieldId !== sortKey) return `Sort ${label} ascending`;\n if (this.sort.direction === 'asc') return `Sort ${label} descending. Currently ascending.`;\n return `Sort ${label} ascending. Currently descending.`;\n }\n\n private emitRowSelection(row: TableRow): void {\n const next = toggleTableRowSelection(this.selectedRowIds, row);\n const selected = next.includes(row.id);\n this.dsSelectionChange.emit({\n selectedRowIds: next,\n scope: 'row',\n changedRowId: row.id,\n selected,\n });\n }\n\n private emitAllSelection(): void {\n const loadedRows = this.currentLoadedRows();\n const selection = deriveTableSelectionState(loadedRows, this.selectedRowIds);\n this.dsSelectionChange.emit({\n selectedRowIds: toggleAllLoadedTableRows(this.selectedRowIds, loadedRows),\n scope: 'all-loaded',\n selected: !selection.allSelected,\n });\n }\n\n private emitGroupSelection(group: DataGroup): void {\n const state = deriveTableSelectionState(group.rows, this.selectedRowIds);\n this.dsSelectionChange.emit({\n selectedRowIds: toggleTableGroupSelection(this.selectedRowIds, group.rows),\n scope: 'group',\n groupId: group.id,\n selected: !state.allSelected,\n });\n }\n\n private rowEventOwnsActivation(event: Event): boolean {\n const currentTarget = event.currentTarget;\n return !event.composedPath().some(target => {\n if (!(target instanceof HTMLElement) || target === currentTarget) return false;\n return target.matches(\n 'button, a, input, select, textarea, [role=\"button\"], [role=\"checkbox\"], [popover], ds-button-unfilled, ds-menu'\n );\n });\n }\n\n private groupRowEventOwnsCollapse(event: Event): boolean {\n const currentTarget = event.currentTarget;\n return !event.composedPath().some(target => {\n if (!(target instanceof HTMLElement) || target === currentTarget) return false;\n return target.matches(\n 'input, select, textarea, [role=\"checkbox\"], .ds-table__selection-control, .ds-table__group-selection, ds-tooltip'\n );\n });\n }\n\n private handleGroupRowClick(group: DataGroup, event: Event): void {\n if (!this.groupRowEventOwnsCollapse(event)) return;\n this.emitGroupCollapse(group);\n }\n\n private emitRowActivation(row: TableRow, event: Event): void {\n if (this.gridEnabled || row.disabled || !this.rowEventOwnsActivation(event)) return;\n if (this.selectionMode === 'multiple' && this.selectedRowIds.length > 0) {\n if (row.selectable !== false) this.emitRowSelection(row);\n return;\n }\n if (row.interactive) this.dsRowActivate.emit({ rowId: row.id });\n }\n\n private syncActionMenu(): void {\n if (!this.actionMenu) return;\n const row = tableRows(this.rows, this.groups, this.grouped).find(\n candidate => candidate.id === this.actionMenu?.rowId\n );\n const value = row && !row.disabled ? row.cells[this.actionMenu.columnId] : undefined;\n if (!isTableCellAction(value) || !isRenderableTableActionMenu(value)) {\n this.actionMenu = null;\n }\n }\n\n private closeActionMenu(): void {\n this.actionMenu = null;\n }\n\n private handleActionMenuSelect(item: MenuItemData): void {\n const open = this.actionMenu;\n const actionId = item.value;\n if (!open || !actionId || item.isInactive) return;\n this.dsCellAction.emit({\n actionId,\n rowId: open.rowId,\n columnId: open.columnId,\n });\n const triggerId = tableActionTriggerId(this.actionMenuElementId, open.rowId, open.columnId);\n this.closeActionMenu();\n requestAnimationFrame(() => {\n const trigger = this.el.querySelector<HTMLElement & { setFocus?: () => void }>(\n `#${CSS.escape(triggerId)}`\n );\n trigger?.setFocus?.();\n });\n }\n\n private toggleActionMenu(row: TableRow, column: TableColumn, event: MouseEvent): void {\n if (row.disabled) return;\n const open = this.actionMenu?.rowId === row.id && this.actionMenu?.columnId === column.id;\n this.actionMenuInitialFocusVisible = event.detail === 0;\n this.actionMenu = open ? null : { rowId: row.id, columnId: column.id };\n if (this.actionMenu) this.actionMenuSurface = this.actionMenu;\n }\n\n private renderOverflowActionMenu() {\n const open = this.actionMenu ?? this.actionMenuSurface;\n const row = open\n ? tableRows(this.rows, this.groups, this.grouped).find(\n candidate => candidate.id === open.rowId\n )\n : undefined;\n const value = row && open ? row.cells[open.columnId] : undefined;\n const menu = isTableCellAction(value) && isRenderableTableActionMenu(value) ? value : undefined;\n const sections = menu ? tableActionMenuSections(menu.items) : [];\n const triggerId = open\n ? tableActionTriggerId(this.actionMenuElementId, open.rowId, open.columnId)\n : undefined;\n\n return (\n <ds-menu\n id={this.actionMenuElementId}\n open={Boolean(this.actionMenu && sections.length)}\n anchorId={triggerId}\n align=\"end\"\n side=\"bottom\"\n menuLabel={menu?.ariaLabel ?? 'More actions'}\n initialFocusVisible={this.actionMenuInitialFocusVisible}\n sections={sections}\n onDsClose={() => this.closeActionMenu()}\n onDsAfterClose={() => {\n if (!this.actionMenu) this.actionMenuSurface = null;\n }}\n onDsSelect={event => this.handleActionMenuSelect(event.detail)}\n />\n );\n }\n\n private connectTruncateTooltip(): void {\n if (this.truncateTooltipBound) return;\n this.el.addEventListener('pointerover', this.onTruncatePointerOver);\n this.el.addEventListener('pointerout', this.onTruncatePointerOut);\n this.el.addEventListener('focusin', this.onTruncateFocusIn);\n this.el.addEventListener('focusout', this.onTruncateFocusOut);\n this.truncateTooltipBound = true;\n }\n\n private disconnectTruncateTooltip(): void {\n if (!this.truncateTooltipBound) return;\n this.el.removeEventListener('pointerover', this.onTruncatePointerOver);\n this.el.removeEventListener('pointerout', this.onTruncatePointerOut);\n this.el.removeEventListener('focusin', this.onTruncateFocusIn);\n this.el.removeEventListener('focusout', this.onTruncateFocusOut);\n this.truncateTooltipBound = false;\n this.dismissTruncateTooltip();\n }\n\n private onTruncatePointerOver = (event: PointerEvent) => {\n if (event.pointerType === 'touch') return;\n const track = resolveTableTruncateTrack(event.target);\n if (!track) return;\n this.presentOrDismissTruncateTrack(track);\n };\n\n private onTruncatePointerOut = (event: PointerEvent) => {\n if (event.pointerType === 'touch') return;\n if (resolveTableTruncateTrack(event.relatedTarget)) return;\n const next = event.relatedTarget;\n const cell = this.truncateAnchor?.closest('.ds-table__cell');\n if (cell && next instanceof Node && cell.contains(next)) return;\n this.dismissTruncateTooltip();\n };\n\n private onTruncateFocusIn = (event: FocusEvent) => {\n const track = resolveTableTruncateTrack(event.target);\n if (!track) {\n this.dismissTruncateTooltip();\n return;\n }\n this.presentOrDismissTruncateTrack(track);\n };\n\n private onTruncateFocusOut = (event: FocusEvent) => {\n if (resolveTableTruncateTrack(event.relatedTarget)) return;\n this.dismissTruncateTooltip();\n };\n\n private presentOrDismissTruncateTrack(track: HTMLElement): void {\n if (!isElementTruncated(track)) {\n this.dismissTruncateTooltip();\n return;\n }\n const label = tableTruncateLabel(track);\n if (!label) {\n this.dismissTruncateTooltip();\n return;\n }\n this.truncateAnchor = track;\n this.truncateTooltipLabel = label;\n void this.truncateTooltipEl?.presentFrom(track, label);\n }\n\n private dismissTruncateTooltip(): void {\n this.truncateAnchor = null;\n if (!this.truncateTooltipLabel && !this.truncateTooltipEl) return;\n this.truncateTooltipLabel = '';\n void this.truncateTooltipEl?.dismiss();\n }\n\n private renderTruncateTooltip() {\n return (\n <ds-tooltip\n ref={element => {\n this.truncateTooltipEl = element;\n }}\n label={this.truncateTooltipLabel}\n describedBy={false}\n wrapLabel={true}\n size=\"sm\"\n delay={0}\n side=\"top\"\n />\n );\n }\n\n private handleRowKeydown(row: TableRow, event: KeyboardEvent): void {\n if (event.target !== event.currentTarget || (event.key !== 'Enter' && event.key !== ' '))\n return;\n event.preventDefault();\n this.emitRowActivation(row, event);\n }\n\n private onVirtualFocusIn = (event: FocusEvent): void => {\n const target = event.target;\n if (!(target instanceof HTMLElement)) return;\n const row = target.closest('[data-row-id]');\n const id = row?.getAttribute('data-row-id') ?? null;\n if (id === this.focusedRowId) return;\n this.focusedRowId = id;\n if (this.rowWindowingEnabled) this.virtualController.schedule();\n };\n\n private renderSelectionControl(\n label: string,\n checked: boolean,\n indeterminate: boolean,\n disabled: boolean,\n onActivate: () => void\n ) {\n return (\n <button\n class=\"ds-table__selection-control ds-focus-ring ds-interaction-fill__content\"\n type=\"button\"\n role=\"checkbox\"\n aria-label={label}\n aria-checked={indeterminate ? 'mixed' : String(checked)}\n disabled={disabled}\n onClick={onActivate}\n >\n <ds-checkbox\n label=\"\"\n size=\"md\"\n checked={checked}\n indeterminate={indeterminate}\n presentation={true}\n />\n </button>\n );\n }\n\n private renderStickyEdge(sticky: TableColumn['sticky'], shadow = true) {\n if (!sticky) return null;\n return (\n <span\n class={{\n 'ds-table__sticky-edge': true,\n [`ds-table__sticky-edge--${sticky}`]: true,\n 'ds-table__sticky-edge--internal': !shadow,\n }}\n aria-hidden=\"true\"\n />\n );\n }\n\n private renderCollapseAllButton() {\n return (\n <ds-button-unfilled\n class=\"ds-table__collapse-all\"\n variant=\"icon\"\n icon=\"ChevronDownUp\"\n size=\"xs\"\n style={{ width: 'var(--dimension-size-300)' }}\n aria-label=\"Collapse all groups\"\n hasBorder={false}\n activeFill={false}\n pressScale={false}\n onDsClick={event => {\n event.stopPropagation();\n this.emitAllGroupsCollapse();\n }}\n />\n );\n }\n\n private renderFloatingCollapseAll(model: TableRenderModel) {\n if (model.collapseAllHost?.mode !== 'floating') return null;\n return (\n <span\n class=\"ds-table__collapse-all-overlay\"\n ref={element => {\n this.collapseAllOverlayEl = element ?? null;\n }}\n >\n <span class=\"ds-table__collapse-all-surface ds-control-elevation ds-control-elevation--md\">\n {this.renderCollapseAllButton()}\n </span>\n </span>\n );\n }\n\n private renderColumnHeader(\n column: TableColumn,\n model: TableRenderModel,\n interactive = true,\n presentational = false,\n headerLane?: { start: number; spanning: boolean }\n ) {\n const groupedColumn = this.grouping?.fieldId === column.id;\n const previousColumn = headerLane\n ? this.visibleColumns[this.visibleColumns.indexOf(column) - 1]\n : undefined;\n const segments = column.segments?.length\n ? column.segments\n : [{ label: column.label, sortKey: column.id }];\n const activeMemberSegment = segments.find(segment => segment.sortKey === this.sort?.fieldId);\n const activeMemberSort = !!activeMemberSegment;\n const activeSort = activeMemberSort;\n const align = column.align ?? 'start';\n const direction = activeMemberSort ? this.sort!.direction : undefined;\n const memberAriaSort = activeMemberSort\n ? this.sort!.direction === 'asc'\n ? 'ascending'\n : 'descending'\n : undefined;\n\n const help = column.help?.trim();\n const labels = (\n <span\n class=\"ds-table__header-labels\"\n tabIndex={interactive && help && !column.sortable ? 0 : undefined}\n data-header-help={help ? '' : undefined}\n >\n {segments.map((segment, index) => {\n const segmentActive = activeMemberSegment?.sortKey === segment.sortKey;\n const segmentInteractive = interactive && !!column.sortable;\n const label = (\n <ds-text\n class=\"ds-table__header-label-box ds-control-label-box\"\n as=\"span\"\n variant=\"text-caption\"\n emphasis={segmentActive}\n color=\"inherit\"\n decoration={help ? 'dotted-underline' : undefined}\n lineTruncation={1}\n >\n {segment.label}\n </ds-text>\n );\n const control = segmentInteractive ? (\n <button\n class=\"ds-table__header-label ds-table__header-label--interactive ds-focus-ring\"\n type=\"button\"\n aria-label={this.sortButtonLabel(\n column,\n segment.sortKey,\n segment.dataLabel?.trim() || segment.label\n )}\n data-sort-control=\"label\"\n data-sort-key={segment.sortKey}\n data-sort-active={segmentActive ? 'true' : undefined}\n onClick={() => this.emitSort(column, segment.sortKey)}\n >\n {label}\n </button>\n ) : (\n <span class=\"ds-table__header-label ds-table__header-static\">{label}</span>\n );\n\n return (\n <span class=\"ds-table__header-segment\" key={segment.sortKey}>\n {control}\n {index < segments.length - 1 && (\n <ds-text\n class=\"ds-table__header-separator\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"tertiary\"\n aria-hidden=\"true\"\n >\n {segment.separator ?? '/'}\n </ds-text>\n )}\n </span>\n );\n })}\n </span>\n );\n const labelControl =\n interactive && help ? (\n <ds-tooltip label={help} side=\"top\" size=\"sm\" wrapLabel={true}>\n {labels}\n </ds-tooltip>\n ) : (\n labels\n );\n const sortControl = interactive ? (\n <span class=\"ds-table__sort-slot\">\n {activeSort && (\n <ds-button-unfilled\n class=\"ds-table__sort-direction\"\n variant=\"icon\"\n icon={direction === 'asc' ? 'ArrowUp' : 'ArrowDown'}\n size=\"xs\"\n aria-label={this.sortButtonLabel(\n column,\n activeMemberSegment?.sortKey ?? column.id,\n activeMemberSegment?.label ?? column.label\n )}\n hasBorder={false}\n activeFill={false}\n isActive={true}\n pressScale={false}\n data-sort-control=\"direction\"\n onDsClick={() => this.emitSort(column, activeMemberSegment?.sortKey ?? column.id)}\n />\n )}\n </span>\n ) : null;\n const collapseHost = model.collapseAllHost;\n const actionCollapseHost =\n collapseHost?.mode === 'action' && collapseHost.columnId === column.id;\n const blankActionCollapseHost =\n actionCollapseHost && !column.label.trim() && !column.segments?.length;\n const collapseControl =\n interactive && actionCollapseHost ? (\n <span class=\"ds-table__collapse-slot\">{this.renderCollapseAllButton()}</span>\n ) : null;\n\n return (\n <th\n key={column.id}\n id={presentational ? undefined : this.headerId(column.id)}\n headers={presentational ? undefined : this.columnBandId(column.id)}\n rowSpan={headerLane?.spanning ? 2 : undefined}\n style={{\n ...this.structure.pins.get(column.id)?.style,\n ...(headerLane\n ? {\n gridColumn: String(headerLane.start),\n gridRow: headerLane.spanning ? '1 / span 2' : '2',\n }\n : {}),\n }}\n class={{\n 'ds-table__header-cell': true,\n 'ds-table__header-cell--rowspan': !!headerLane?.spanning,\n 'ds-table__header-cell--after-rowspan':\n !!headerLane &&\n !headerLane.spanning &&\n !!previousColumn &&\n !previousColumn.group &&\n !this.structure.pins.has(previousColumn.id),\n [`ds-table__cell--align-${align}`]: true,\n 'ds-table__cell--sticky-start':\n column.sticky === 'start' && this.structure.pins.has(column.id),\n 'ds-table__cell--sticky-end':\n column.sticky === 'end' && this.structure.pins.has(column.id),\n 'ds-table__header-cell--collapse-all': actionCollapseHost,\n }}\n scope={presentational ? undefined : 'col'}\n aria-sort={presentational ? undefined : memberAriaSort}\n data-column-id={column.id}\n data-grouped={groupedColumn ? 'true' : undefined}\n data-sort-active={activeSort ? 'true' : undefined}\n >\n {!column.label.trim() && column.accessibleLabel?.trim() && (\n <span class=\"ds-visually-hidden\">{column.accessibleLabel}</span>\n )}\n {blankActionCollapseHost ? (\n <span class=\"ds-table__header-content ds-table__header-content--collapse-all\">\n {collapseControl}\n </span>\n ) : (\n <span class=\"ds-table__header-content\">\n {align === 'end' && sortControl}\n {align === 'center' && (\n <span\n class=\"ds-table__sort-slot ds-table__sort-slot--balance\"\n data-sort-balance=\"true\"\n aria-hidden=\"true\"\n />\n )}\n {labelControl}\n {align !== 'end' && sortControl}\n {collapseControl}\n </span>\n )}\n {this.renderStickyEdge(\n this.structure.pins.has(column.id) ? column.sticky : undefined,\n !!this.structure.pins.get(column.id)?.edge\n )}\n </th>\n );\n }\n\n private renderColgroup(model: TableRenderModel) {\n const beforeSpacer =\n model.elasticSpacerIndex == null\n ? this.visibleColumns\n : this.visibleColumns.slice(0, model.elasticSpacerIndex);\n const afterSpacer =\n model.elasticSpacerIndex == null ? [] : this.visibleColumns.slice(model.elasticSpacerIndex);\n const renderColumn = (column: TableColumn) => {\n const width = tableColumnSize(column);\n return (\n <col\n key={column.id}\n class={{ 'ds-table__action-column': column.kind === 'action' }}\n style={width ? { width } : undefined}\n />\n );\n };\n return (\n <colgroup>\n {model.selectable && <col class=\"ds-table__selection-column\" />}\n {beforeSpacer.map(renderColumn)}\n {model.elasticSpacerIndex != null && <col class=\"ds-table__elastic-spacer-column\" />}\n {afterSpacer.map(renderColumn)}\n </colgroup>\n );\n }\n\n private renderHeader(\n model: TableRenderModel,\n interactive = true,\n presentational = false,\n ariaRowIndex?: number\n ) {\n const selection = model.selection;\n const bands = this.structure.bands;\n const multirow = bands.length > 0;\n const controls =\n interactive &&\n !(\n this.responsiveLayout === 'cards' &&\n this.narrowViewport &&\n this.interactionMode !== 'grid' &&\n !this.cellSpans.length\n );\n const selectionHeader = model.selectable && (\n <th\n class={{\n 'ds-table__header-cell': true,\n 'ds-table__selection-cell': true,\n 'ds-table__cell--sticky-start': true,\n 'ds-table__header-cell--rowspan': multirow,\n }}\n rowSpan={multirow ? 2 : undefined}\n style={multirow ? { gridColumn: '1', gridRow: '1 / span 2' } : undefined}\n scope={presentational ? undefined : 'col'}\n >\n {controls ? (\n this.renderSelectionControl(\n selection.allSelected ? 'Deselect all loaded rows' : 'Select all loaded rows',\n selection.allSelected,\n selection.indeterminate,\n selection.selectableRowIds.length === 0,\n () => this.emitAllSelection()\n )\n ) : (\n <span class=\"ds-visually-hidden\">Select rows</span>\n )}\n {this.renderStickyEdge(\n 'start',\n ![...this.structure.pins.values()].some(pin => pin.edge === 'start')\n )}\n </th>\n );\n const lanes: (TableColumn | undefined)[] = [...this.visibleColumns];\n if (model.elasticSpacerIndex != null) lanes.splice(model.elasticSpacerIndex, 0, undefined);\n const renderLane = (column: TableColumn | undefined, index: number) => {\n const start = index + 1 + Number(model.selectable);\n if (column)\n return this.renderColumnHeader(\n column,\n model,\n controls,\n presentational,\n multirow ? { start, spanning: !column.group } : undefined\n );\n return (\n <th\n class=\"ds-table__header-cell ds-table__elastic-spacer-cell\"\n rowSpan={multirow ? 2 : undefined}\n style={multirow ? { gridColumn: String(start), gridRow: '1 / span 2' } : undefined}\n aria-hidden=\"true\"\n role=\"presentation\"\n data-elastic-spacer=\"true\"\n />\n );\n };\n return (\n <thead\n class={{\n 'ds-table__head': true,\n 'ds-table__head--multirow': multirow,\n 'ds-table__head--semantic-copy': !interactive,\n }}\n ref={element => {\n if (interactive) this.interactiveHeadEl = element ?? null;\n }}\n >\n {multirow && (\n <tr class=\"ds-table__header-row ds-table__super-header-row\" aria-rowindex={ariaRowIndex}>\n {selectionHeader}\n {bands.map(band =>\n !band.label ? (\n lanes.map((column, index) => {\n const start = index + 1 + Number(model.selectable);\n return start >= band.start && start < band.start + band.span\n ? renderLane(column, index)\n : null;\n })\n ) : (\n <th\n key={band.key}\n id={presentational ? undefined : `${this.actionMenuElementId}-${band.key}`}\n class={{\n 'ds-table__header-cell': true,\n 'ds-table__super-header-cell': true,\n 'ds-table__cell--sticky-start': band.sticky === 'start',\n 'ds-table__cell--sticky-end': band.sticky === 'end',\n }}\n scope={presentational || !band.label ? undefined : 'colgroup'}\n colSpan={band.span}\n style={{\n gridColumn: `${band.start} / span ${band.span}`,\n gridRow: '1',\n ...this.structure.pins.get(\n (band.sticky === 'end' ? band.lastColumnId : band.firstColumnId) ?? ''\n )?.style,\n }}\n aria-hidden={!band.label ? 'true' : undefined}\n >\n <span class=\"ds-table__header-content\">\n <span class=\"ds-table__header-labels\">\n <span class=\"ds-table__header-segment\">\n {/* eslint-disable-next-line compomo/prefer-direct-ds-text -- Match the normal header's structural label canvas and inner text inset. */}\n <span class=\"ds-table__header-label ds-table__header-static\">\n <ds-text\n class=\"ds-table__header-label-box ds-control-label-box\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"inherit\"\n lineTruncation={1}\n >\n {band.label}\n </ds-text>\n </span>\n </span>\n </span>\n </span>\n {this.renderStickyEdge(\n band.sticky,\n !!this.structure.pins.get(\n (band.sticky === 'end' ? band.firstColumnId : band.lastColumnId) ?? ''\n )?.edge\n )}\n </th>\n )\n )}\n </tr>\n )}\n <tr\n class=\"ds-table__header-row\"\n aria-rowindex={\n ariaRowIndex == null\n ? undefined\n : ariaRowIndex + Number(this.structure.bands.length > 0)\n }\n >\n {!multirow && selectionHeader}\n {lanes.map((column, index) => (!multirow || column?.group) && renderLane(column, index))}\n </tr>\n </thead>\n );\n }\n\n private renderRow(\n row: TableRow,\n model: TableRenderModel,\n ariaRowIndex?: number,\n variableVirtualSize = false,\n rowKey = row.id\n ) {\n return renderTableRowView({\n grid: this.gridEnabled,\n cellAttributes: this.editingEnabled\n ? (targetRow, column) => this.gridCellAttributes(targetRow, column)\n : undefined,\n renderEditor: this.editingEnabled\n ? (targetRow, column) => this.renderCellEditor(targetRow, column)\n : undefined,\n renderEditTrigger: this.editingEnabled\n ? (targetRow, column) => this.renderCellEditTrigger(targetRow, column)\n : undefined,\n pins: this.structure.pins,\n spans: this.structure.merges.cells.get(row.id),\n headerId: id => this.headerId(id),\n responsiveCards: this.responsiveLayout === 'cards',\n row,\n model,\n visibleColumns: this.visibleColumns,\n emptyCellLabel: this.emptyCellLabel,\n actionMenuElementId: this.actionMenuElementId,\n actionMenu: this.actionMenu,\n actionMenuSurface: this.actionMenuSurface,\n surfaceOpenRowId: this.surfaceOpenRowId,\n highlightMatcher: this.highlightMatcher,\n highlightFieldIds: this.highlightFieldIds,\n ariaRowIndex:\n ariaRowIndex == null ? undefined : ariaRowIndex + Number(this.structure.bands.length > 0),\n variableVirtualSize,\n intrinsicBlockSize:\n this.dataMode !== 'virtual'\n ? estimateTableRowBlockSize(row, this.visibleColumns)\n : undefined,\n rowKey,\n renderSelectionControl: (label, checked, indeterminate, disabled, onActivate) =>\n this.renderSelectionControl(label, checked, indeterminate, disabled, onActivate),\n renderStickyEdge: (sticky, shadow) => this.renderStickyEdge(sticky, shadow),\n onRowActivate: (targetRow, event) => this.emitRowActivation(targetRow, event),\n onRowKeyDown: (targetRow, event) => this.handleRowKeydown(targetRow, event),\n onRowSelection: targetRow => this.emitRowSelection(targetRow),\n onCellAction: detail => this.dsCellAction.emit(detail),\n onActionMenuToggle: (targetRow, column, event) =>\n this.toggleActionMenu(targetRow, column, event),\n });\n }\n\n private emitGroupCollapse(group: DataGroup) {\n const collapsedGroupIds = toggleTableGroupCollapsed(this.collapsedGroupIds, group.id);\n this.dsGroupCollapseChange.emit({\n scope: 'group',\n groupId: group.id,\n collapsed: collapsedGroupIds.includes(group.id),\n collapsedGroupIds,\n });\n }\n\n private emitAllGroupsCollapse() {\n const groupIds = this.groups.map(group => group.id);\n const collapsedGroupIds = nextTableGroupsCollapsed(this.collapsedGroupIds, groupIds);\n this.dsGroupCollapseChange.emit({\n scope: 'all',\n collapsed: collapsedGroupIds.length > 0,\n collapsedGroupIds,\n });\n }\n\n private renderGroupAccessory(\n item: TableRenderModel['groups'][number]['accessories'][number],\n index: number\n ) {\n const text = (\n <ds-text\n key={`acc-${index}`}\n class=\"ds-table__group-accessory\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"secondary\"\n decoration={item.help ? 'dotted-underline' : undefined}\n lineTruncation={1}\n >\n {item.text}\n </ds-text>\n );\n if (!item.help) return text;\n return (\n <ds-tooltip\n key={`acc-${index}`}\n label={item.help}\n side=\"bottom\"\n align=\"start\"\n size=\"sm\"\n wrapLabel={true}\n >\n <span class=\"ds-table__group-accessory-tip\" tabIndex={0}>\n {text}\n </span>\n </ds-tooltip>\n );\n }\n\n private renderGroupContent(groupModel: TableRenderModel['groups'][number]) {\n const {\n group,\n countLabel,\n collapsed: isCollapsed,\n labelColor,\n selection: groupSelection,\n accessories,\n hero,\n } = groupModel;\n const heroTracks = hero?.kind === 'score' && hero.tracks === 2 ? 2 : 1;\n const heroPresentation = TABLE_GROUP_HERO_SCORE_PRESENTATION[heroTracks];\n return (\n <span\n class={{\n 'ds-table__group-content': true,\n 'ds-table__group-content--multi': accessories.length > 0 || heroTracks === 2,\n 'ds-table__group-content--hero': Boolean(hero),\n 'ds-table__group-content--hero-two-track': Boolean(hero) && heroTracks === 2,\n }}\n onClick={event => this.handleGroupRowClick(group, event)}\n >\n {groupSelection && (\n <span class=\"ds-table__group-selection\">\n {this.renderSelectionControl(\n groupSelection.allSelected\n ? `Deselect loaded rows in ${group.label} group`\n : `Select loaded rows in ${group.label} group`,\n groupSelection.allSelected,\n groupSelection.indeterminate,\n groupSelection.selectableRowIds.length === 0,\n () => this.emitGroupSelection(group)\n )}\n </span>\n )}\n {hero?.kind === 'score' && (\n <span class=\"ds-table__group-hero\">\n <ds-score\n size={heroPresentation.size}\n variant={heroPresentation.variant}\n value={hero.value}\n level={hero.level}\n label={hero.label ?? 'Safety score'}\n isLoading={hero.isLoading === true}\n />\n </span>\n )}\n <span class=\"ds-table__group-copy\">\n <span class=\"ds-table__group-primary\">\n <ds-text\n class=\"ds-table__group-label\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis={true}\n color={labelColor}\n >\n {group.label}\n </ds-text>\n <span class=\"ds-visually-hidden\">{countLabel}</span>\n </span>\n {accessories.length > 0 && (\n <span class=\"ds-table__group-accessories\">\n {accessories.flatMap((item, index) => [\n ...(index > 0\n ? [\n <ds-text\n key={`sep-${index}`}\n class=\"ds-table__group-accessory-separator\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>,\n ]\n : []),\n this.renderGroupAccessory(item, index),\n ])}\n </span>\n )}\n </span>\n <span class=\"ds-table__group-trailing\">\n <ds-text\n class=\"ds-table__group-count\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n {groupModel.visibleCountText}\n </ds-text>\n <span class=\"ds-table__group-action\">\n <ds-button-unfilled\n class=\"ds-table__group-toggle\"\n variant=\"icon\"\n size=\"md\"\n isInset={true}\n insetDepth=\"double\"\n icon={isCollapsed ? 'ChevronDown' : 'ChevronUp'}\n expanded={!isCollapsed}\n aria-label={\n isCollapsed ? `Expand ${group.label} group` : `Collapse ${group.label} group`\n }\n hasBorder={false}\n pressScale={false}\n />\n </span>\n </span>\n </span>\n );\n }\n\n private formatGroupLoadLabel(template: string, group: DataGroup): string {\n return template.split('{group}').join(group.label);\n }\n\n private renderGroupLoadRow(group: DataGroup, totalColumns: number) {\n const error = group.loadMoreError?.trim();\n if (!error && !group.loadingMore && !group.hasMore) return null;\n const manualFallback =\n this.loadMoreMode === 'manual' || !this.groupLoadController.intersectionSupported;\n\n return (\n <tr\n class=\"ds-table__load-row ds-table__group-load-row\"\n data-group-id={group.id}\n ref={element => {\n if (element) this.groupSentinelEls.set(group.id, element);\n else this.groupSentinelEls.delete(group.id);\n }}\n >\n <td class=\"ds-table__load-cell\" colSpan={totalColumns}>\n <div class=\"ds-table__viewport-band ds-table__load-band\">\n {renderTableLoadContent({\n error,\n loading: !!group.loadingMore,\n hasMore: !!group.hasMore,\n manualFallback,\n retryLabel: this.formatGroupLoadLabel(this.groupRetryLabel, group),\n loadingLabel: this.formatGroupLoadLabel(this.groupLoadingMoreLabel, group),\n loadMoreLabel: this.groupLoadMoreLabel,\n loadMoreAriaLabel: this.formatGroupLoadLabel(this.groupLoadMoreAriaLabel, group),\n onRetry: () => this.groupLoadController.request(group.id, 'retry'),\n onLoadMore: () => this.groupLoadController.request(group.id, 'manual'),\n })}\n </div>\n </td>\n </tr>\n );\n }\n\n private renderStickyGroup(model: TableRenderModel) {\n if (!this.documentStickyHeader || !this.activeStickyGroupId) return null;\n const groupModel = model.groups.find(item => item.group.id === this.activeStickyGroupId);\n if (!groupModel) return null;\n return (\n <div\n class={{\n 'ds-table__sticky-group': true,\n 'ds-table__sticky-group--selectable': model.selectable,\n [groupModel.intentClass ?? '']: !!groupModel.intentClass,\n }}\n data-group-id={groupModel.group.id}\n data-group-intent={groupModel.intent}\n >\n {this.renderGroupContent(groupModel)}\n </div>\n );\n }\n\n private renderDataBodies(model: TableRenderModel, plan: TableVirtualPlan | null) {\n return this.bodyRenderer.render({\n headerRowCount: 1 + Number(this.structure.bands.length > 0),\n model,\n plan,\n rows: this.rows,\n stickyHeader: this.stickyHeader,\n documentStickyHeader: this.documentStickyHeader,\n activeStickyGroupId: this.activeStickyGroupId,\n renderRow: (row, renderModel, ariaRowIndex, variableVirtualSize, rowKey) =>\n this.renderRow(row, renderModel, ariaRowIndex, variableVirtualSize, rowKey),\n renderGroupContent: groupModel => this.renderGroupContent(groupModel),\n renderGroupLoadRow: (group, totalColumns) => this.renderGroupLoadRow(group, totalColumns),\n });\n }\n\n private renderSkeletonBody(model: TableRenderModel) {\n return renderTableSkeletonBody({\n pins: this.structure.pins,\n model,\n visibleColumns: this.visibleColumns,\n skeletonRows: this.skeletonRows,\n renderStickyEdge: (sticky, shadow) => this.renderStickyEdge(sticky, shadow),\n });\n }\n\n private renderStateBody(kind: 'empty' | 'error' | 'virtual-viewport', totalColumns: number) {\n const error = kind !== 'empty';\n const heading =\n kind === 'virtual-viewport'\n ? TABLE_VIRTUAL_VIEWPORT_REQUIRED_HEADING\n : error\n ? this.errorHeading\n : this.emptyHeading;\n const body =\n kind === 'virtual-viewport'\n ? TABLE_VIRTUAL_VIEWPORT_REQUIRED_BODY\n : error\n ? this.errorBody\n : this.emptyBody;\n return (\n <tbody class=\"ds-table__body ds-table__state-body\">\n <tr class=\"ds-table__state-row\">\n <td class=\"ds-table__state-cell\" colSpan={totalColumns}>\n <div class=\"ds-table__viewport-band ds-table__state-band\">\n <ds-empty-state\n icon={error ? 'ExclamationTriangle' : 'Inbox'}\n heading={heading}\n body={body}\n />\n </div>\n </td>\n </tr>\n </tbody>\n );\n }\n\n private renderLazyBody(model: TableRenderModel) {\n if (\n this.dataMode !== 'infinite' ||\n !this.incrementalWindowActive ||\n !model.hasData ||\n model.grouped\n )\n return null;\n const error = this.loadMoreError?.trim();\n const manualFallback =\n this.loadMoreMode === 'manual' || !this.loadController.intersectionSupported;\n\n return (\n <tbody class=\"ds-table__body ds-table__load-body\">\n <tr\n class=\"ds-table__load-row\"\n ref={element => {\n this.sentinelEl = element ?? null;\n }}\n >\n <td class=\"ds-table__load-cell\" colSpan={model.totalColumns}>\n <div class=\"ds-table__viewport-band ds-table__load-band\">\n {renderTableLoadContent({\n error,\n loading: this.loadingMore,\n hasMore: this.hasMore,\n manualFallback,\n retryLabel: this.retryLabel,\n loadingLabel: this.loadingMoreLabel,\n loadMoreLabel: this.loadMoreLabel,\n endOfResultsLabel: this.endOfResultsLabel,\n onRetry: () => this.loadController.request('retry'),\n onLoadMore: () => this.loadController.request('manual'),\n })}\n </div>\n </td>\n </tr>\n </tbody>\n );\n }\n\n private renderDocumentStickyHeader(model: TableRenderModel) {\n if (!this.documentStickyHeader) return null;\n return (\n <div class=\"ds-table__document-sticky-header\">\n <table\n class={{\n 'ds-table__table': true,\n 'ds-table__sticky-header-table': true,\n 'ds-table__table--selectable': model.selectable,\n 'ds-table__table--grouped': model.grouped,\n }}\n style={model.tableStyle}\n role=\"presentation\"\n ref={element => {\n this.stickyHeaderTableEl = element ?? null;\n }}\n >\n {this.renderColgroup(model)}\n {this.renderHeader(model, true, true)}\n </table>\n {this.renderFloatingCollapseAll(model)}\n </div>\n );\n }\n\n private get resultSummary(): string | null {\n if (this.dataMode === 'pagination') return null;\n if (this.dataMode === 'virtual') {\n return formatTableTotalSummary(this.currentLoadedRowCount(), this.resultTotalSummaryLabel);\n }\n return formatTableResultSummary(this.displayedCount, this.totalCount, this.resultSummaryLabel);\n }\n\n private get hasResultFooter(): boolean {\n return (\n (this.dataMode === 'pagination' && !!this.pagination) ||\n !!this.resultSummary ||\n this.footerSlotPresence !== 0\n );\n }\n\n private renderResultFooter() {\n const summary = this.resultSummary;\n const hasLeading = (this.footerSlotPresence & TABLE_FOOTER_SLOT_LEADING) !== 0;\n const hasCopy = (this.footerSlotPresence & TABLE_FOOTER_SLOT_COPY) !== 0;\n const hasTrailing = (this.footerSlotPresence & TABLE_FOOTER_SLOT_TRAILING) !== 0;\n const pagination = this.dataMode === 'pagination' ? this.pagination : null;\n if (!this.hasResultFooter) return null;\n return (\n <div class=\"ds-table__footer ds-table__bar ds-chrome-header ds-control--md\">\n <div class=\"ds-table__bar-copy ds-chrome-header__copy ds-control--md\">\n {hasLeading && (\n <div class=\"ds-table__bar-status ds-chrome-header__heading\">\n <slot name=\"footer-leading\" onSlotchange={this.syncFooterSlotPresence} />\n </div>\n )}\n {hasCopy ? (\n <slot name=\"footer\" onSlotchange={this.syncFooterSlotPresence} />\n ) : summary ? (\n <ds-text\n class=\"ds-table__footer-summary ds-table__bar-text\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n lineTruncation={1}\n >\n {summary}\n </ds-text>\n ) : null}\n </div>\n {(pagination || hasTrailing) && (\n <div class=\"ds-table__bar-trailing ds-chrome-header__trailing\">\n {pagination && (\n <ds-pagination\n pageIndex={pagination.pageIndex}\n pageSize={pagination.pageSize}\n pageSizeMode={pagination.pageSizeMode ?? 'fixed'}\n totalItems={pagination.totalItems}\n pageSizeOptions={pagination.pageSizeOptions ?? [25, 50, 100, 200]}\n fitToPage={pagination.fitToPage ?? false}\n {...{ fitToPageInactive: pagination.fitToPageInactive ?? false }}\n fitPageSize={this.fitPageSize ?? pagination.fitPageSize}\n fitPageSizeLabel={pagination.fitPageSizeLabel ?? 'Fit to page'}\n fitPageSizeTriggerLabel={pagination.fitPageSizeTriggerLabel ?? 'Fit'}\n itemLabel={pagination.itemLabel ?? 'items'}\n pageSizeLabel={pagination.pageSizeLabel ?? 'Items'}\n label={pagination.ariaLabel ?? `${this.caption} pagination`}\n showFirstLastButtons={pagination.showFirstLastButtons ?? false}\n loading={this.loading}\n onDsChange={(event: CustomEvent<PaginationChangeDetail>) =>\n this.dsPaginationChange.emit(event.detail)\n }\n />\n )}\n <slot name=\"footer-trailing\" onSlotchange={this.syncFooterSlotPresence} />\n </div>\n )}\n </div>\n );\n }\n\n private renderCaptionBar() {\n if (this.captionVisibility !== 'visible') return null;\n return (\n <div class=\"ds-table__caption-bar ds-table__bar ds-control--md\">\n <div\n class={{\n 'ds-table__caption-content': true,\n 'ds-table__caption-content--trailing': this.showsCaptionTrailing,\n }}\n >\n <div\n class={{\n 'ds-table__caption-leading': true,\n 'ds-table__caption-leading--toolbar': this.headerUsesToolbar,\n }}\n >\n <slot name=\"header\" onSlotchange={this.syncHeaderSlotPresence} />\n {!this.headerPresent ? (\n <ds-text\n class=\"ds-table__caption-title ds-table__bar-text\"\n as=\"div\"\n variant=\"text-title-small\"\n emphasis={true}\n color=\"primary\"\n aria-hidden=\"true\"\n >\n {this.caption}\n </ds-text>\n ) : null}\n </div>\n {this.renderCaptionTrailing()}\n </div>\n <slot name=\"header-after\" />\n </div>\n );\n }\n\n private renderCaptionTrailing() {\n if (!this.showsCaptionTrailing) return null;\n return (\n <div class=\"ds-table__caption-trailing\">\n {this.renderColumnCustomizerTrigger()}\n {this.showsColumnCustomizer &&\n !this.hideColumnCustomizerTrigger &&\n this.showsDataModeSwitcher ? (\n <ds-divider orientation=\"vertical\" length=\"32px\" />\n ) : null}\n {this.renderDataModeSwitcherTrigger()}\n </div>\n );\n }\n\n private renderDataModeSwitcherTrigger() {\n if (!this.showsDataModeSwitcher) return null;\n if (this.chromeLoading) {\n return <ds-skeleton variant=\"control\" controlSize=\"md\" width=\"var(--dimension-size-400)\" />;\n }\n return (\n <span class=\"ds-table__caption-mode-switcher\">\n <ds-tooltip label={this.dataModeSwitcherLabel} side=\"top\" size=\"sm\">\n <ds-button-unfilled\n hasBorder={!this.captionControlsBorderless}\n id={`${this.dataModeSwitcherElementId}-trigger`}\n variant=\"icon\"\n size=\"md\"\n icon=\"Ellipses\"\n aria-label={this.dataModeSwitcherLabel}\n hasMenu={true}\n expanded={this.dataModeSwitcherOpen}\n surfaceOpen={this.dataModeSwitcherSurfaceOpen}\n controls={this.dataModeSwitcherElementId}\n activeFill={false}\n pressScale={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n this.toggleDataModeSwitcher(event.detail.detail === 0);\n }}\n />\n </ds-tooltip>\n </span>\n );\n }\n\n private renderDataModeSwitcherMenu() {\n if (!this.showsDataModeSwitcher || this.chromeLoading) return null;\n return (\n <ds-menu\n id={this.dataModeSwitcherElementId}\n open={this.dataModeSwitcherOpen}\n anchorId={`${this.dataModeSwitcherElementId}-trigger`}\n align=\"end\"\n side=\"bottom\"\n menuLabel={this.dataModeMenuLabel}\n initialFocusVisible={this.dataModeSwitcherInitialFocusVisible}\n items={tableDataModeMenuItems(this.dataMode, {\n infinite: this.infiniteModeLabel,\n pagination: this.paginationModeLabel,\n virtual: this.virtualModeLabel,\n })}\n onDsClose={() => this.closeDataModeSwitcher()}\n onDsAfterClose={() => {\n if (!this.dataModeSwitcherOpen) this.dataModeSwitcherSurfaceOpen = false;\n }}\n onDsSelect={event => this.handleDataModeSwitcherSelect(event.detail)}\n />\n );\n }\n\n private toggleDataModeSwitcher(fromKeyboard = false): void {\n if (this.dataModeSwitcherOpen) this.closeDataModeSwitcher();\n else this.openDataModeSwitcher(fromKeyboard);\n }\n\n private openDataModeSwitcher(fromKeyboard = false): void {\n if (!this.showsDataModeSwitcher || this.dataModeSwitcherOpen) return;\n this.closeColumnCustomizer();\n this.dataModeSwitcherInitialFocusVisible = fromKeyboard;\n this.dataModeSwitcherOpen = true;\n this.dataModeSwitcherSurfaceOpen = true;\n }\n\n private closeDataModeSwitcher(): void {\n this.dataModeSwitcherOpen = false;\n }\n\n private handleDataModeSwitcherSelect(item: MenuItemData): void {\n const dataMode = tableDataModeFromMenuItem(item);\n if (!dataMode) return;\n this.closeDataModeSwitcher();\n if (dataMode !== this.dataMode) this.dsDataModeChange.emit({ dataMode });\n requestAnimationFrame(() => {\n this.el\n .querySelector<\n HTMLElement & { setFocus?: () => void }\n >(`#${CSS.escape(`${this.dataModeSwitcherElementId}-trigger`)}`)\n ?.setFocus?.();\n });\n }\n\n private renderColumnCustomizerTrigger() {\n if (this.hideColumnCustomizerTrigger) return null;\n if (!this.showsColumnCustomizer) return null;\n return (\n <div\n class={{\n 'ds-table__caption-customizer': true,\n 'ds-table__caption-customizer--loading': this.chromeLoading,\n }}\n aria-hidden={this.chromeLoading ? 'true' : undefined}\n >\n <ds-tooltip label={this.captionCompact ? 'Customize' : ''} side=\"top\" size=\"sm\">\n <ds-button-unfilled\n hasBorder={!this.captionControlsBorderless}\n id={`${this.columnCustomizerElementId}-trigger`}\n variant={this.captionCompact ? 'icon' : 'icon-label'}\n size=\"md\"\n icon=\"Preferences\"\n label=\"Customize\"\n labelEmphasis={false}\n pressScale={false}\n aria-label=\"Customize table\"\n haspopup=\"menu\"\n expanded={this.columnCustomizerOpen}\n surfaceOpen={this.columnCustomizerSurfaceOpen}\n controls={this.columnCustomizerElementId}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n if (this.chromeLoading) return;\n this.toggleColumnCustomizer(event.detail.detail === 0);\n }}\n />\n </ds-tooltip>\n {this.chromeLoading ? (\n <ds-skeleton variant=\"control\" controlSize=\"md\" width=\"100%\" />\n ) : null}\n </div>\n );\n }\n\n private renderColumnCustomizerMenu() {\n if (!this.showsColumnCustomizer || this.chromeLoading) return null;\n return (\n <ds-menu\n id={this.columnCustomizerElementId}\n open={this.columnCustomizerOpen}\n anchorId={`${this.columnCustomizerElementId}-trigger`}\n align=\"end\"\n side=\"bottom\"\n menuLabel=\"Customize table\"\n initialFocusVisible={this.columnCustomizerInitialFocusVisible}\n sections={[\n {\n header: 'Columns',\n items: tableColumnCustomizerMenuItems(\n this.columns,\n this.hiddenFieldIds,\n this.fieldOrder\n ),\n },\n ...(this.customizeOptions.length\n ? [{ header: 'Options', items: this.customizeOptions }]\n : []),\n ]}\n onDsClose={() => this.closeColumnCustomizer()}\n onDsAfterClose={() => {\n if (!this.columnCustomizerOpen) this.columnCustomizerSurfaceOpen = false;\n }}\n onDsSelect={event => {\n if (this.customizeOptions.some(option => option.value === event.detail.value)) {\n this.dsCustomizeOptionChange.emit(event.detail.value!);\n } else this.handleColumnCustomizerSelect(event.detail);\n }}\n onDsReorder={event => this.handleColumnCustomizerReorder(event.detail)}\n />\n );\n }\n\n private toggleColumnCustomizer(fromKeyboard = false): void {\n if (this.columnCustomizerOpen) this.closeColumnCustomizer();\n else this.openColumnCustomizer(fromKeyboard);\n }\n\n private openColumnCustomizer(fromKeyboard = false): void {\n if (!this.showsColumnCustomizer || this.columnCustomizerOpen) return;\n this.closeDataModeSwitcher();\n this.columnCustomizerInitialFocusVisible = fromKeyboard;\n this.columnCustomizerOpen = true;\n this.columnCustomizerSurfaceOpen = true;\n }\n\n private closeColumnCustomizer(): void {\n this.columnCustomizerOpen = false;\n }\n\n private emitColumnsConfigChange(hiddenFieldIds: string[], fieldOrder: string[]): void {\n this.dsFieldsConfigChange.emit({\n hiddenFieldIds: resolveTableHiddenColumnIds(this.columns, hiddenFieldIds),\n fieldOrder: resolveTableColumnOrder(this.columns, fieldOrder),\n });\n }\n\n private handleColumnCustomizerReorder(detail: MenuReorderDetail): void {\n const order = detail.items\n .filter(item => item.reorderable)\n .map(item => item.value)\n .filter((id): id is string => !!id);\n this.emitColumnsConfigChange(this.hiddenFieldIds, order);\n }\n\n private handleColumnCustomizerSelect(item: MenuItemData): void {\n const fieldId = item.value;\n if (!fieldId || item.isInactive) return;\n this.emitColumnsConfigChange(\n toggleTableColumnHidden(this.columns, this.hiddenFieldIds, fieldId),\n resolveTableColumnOrder(this.columns, this.fieldOrder)\n );\n }\n\n render() {\n this.groupSentinelEls.clear();\n const model = this.createRenderModel();\n this.syncVirtualItems(model);\n this.renderedModel = model;\n const regionLabel = this.scrollLabel?.trim() || `${this.caption} scroll area`;\n const initialLoading = this.loading && !model.hasData;\n const initialError = !model.hasData && this.error;\n const hasGroupedStructure = model.grouped && model.groups.length > 0;\n const windowRows = this.shouldWindowRows(model);\n const virtualViewportMissing =\n this.virtualViewportMissing && !initialLoading && (model.hasData || hasGroupedStructure);\n const virtualPlan = windowRows\n ? (this.virtualWindow ??\n resolveTableVirtualPlan({\n items: this.virtualItems,\n sizes: this.virtualItems.map(item => this.virtualController.sizeFor(item)),\n scrollOffset: this.viewportEl?.scrollTop ?? 0,\n viewportSize: this.estimateVirtualViewportSize(),\n pinnedRowIds: this.virtualPinnedRowIds(),\n }))\n : null;\n const groupLoadingMore = hasGroupedStructure && this.groups.some(group => group.loadingMore);\n const viewportStyle =\n !this.boundedComposition && this.resolvedMaxHeight\n ? { '--ds-table-max-block-size': this.resolvedMaxHeight }\n : undefined;\n const hostStyle = this.fixedHeight\n ? { '--_table-fixed-block-size': this.resolvedHeight }\n : undefined;\n\n return (\n <Host\n class={{\n 'table-host': true,\n 'table-host--bounded': this.boundedComposition,\n 'table-host--fixed-height': this.fixedHeight,\n 'table-host--viewport-fit': this.fitViewport,\n }}\n style={hostStyle}\n onKeyDown={(event: KeyboardEvent) => {\n this.handleGridKeyDown(event);\n this.handlePaginationKeyDown(event);\n }}\n onCopy={(event: ClipboardEvent) => this.handleGridClipboard(event)}\n onPaste={(event: ClipboardEvent) => this.handleGridClipboard(event)}\n onFocusin={this.onVirtualFocusIn}\n >\n <div\n class={{\n 'ds-table': true,\n 'ds-table--bounded': this.boundedComposition,\n 'ds-table--contained-scroll': this.containedScroll,\n 'ds-table--fixed-height': this.fixedHeight,\n 'ds-table--viewport-fit': this.fitViewport,\n 'ds-table--viewport-fit-pending': this.fitViewport && !this.viewportFitSettled,\n 'ds-table--viewport-fit-settled': this.fitViewport && this.viewportFitSettled,\n 'ds-table--sticky-header': this.stickyHeader,\n 'ds-table--document-sticky-header': this.documentStickyHeader,\n 'ds-table--contained-sticky-header': this.stickyHeader && !this.documentStickyHeader,\n 'ds-table--caption-visible': this.captionVisibility === 'visible',\n 'ds-table--super-headers': this.structure.bands.length > 0,\n 'ds-table--responsive-cards':\n this.responsiveLayout === 'cards' &&\n this.interactionMode !== 'grid' &&\n this.cellSpans.length === 0,\n 'ds-table--cards-active':\n this.narrowViewport &&\n this.responsiveLayout === 'cards' &&\n this.interactionMode !== 'grid' &&\n this.cellSpans.length === 0,\n 'ds-table--footer-visible': this.hasResultFooter,\n 'ds-table--state-fill':\n !initialLoading &&\n (initialError || virtualViewportMissing || !(model.hasData || hasGroupedStructure)),\n }}\n ref={element => {\n this.rootEl = element ?? null;\n }}\n >\n {this.renderCaptionBar()}\n <div\n class={{\n 'ds-table__frame': true,\n 'ds-table__frame--overflow-start': this.overflowStart,\n 'ds-table__frame--overflow-end': this.overflowEnd,\n }}\n ref={element => {\n this.frameEl = element ?? null;\n }}\n >\n {this.renderDocumentStickyHeader(model)}\n {this.renderStickyGroup(model)}\n {!this.documentStickyHeader && this.renderFloatingCollapseAll(model)}\n <div\n class={{\n 'ds-table__viewport': true,\n 'ds-focus-ring': this.scrollable,\n }}\n style={viewportStyle}\n ref={element => {\n this.viewportEl = element ?? null;\n }}\n role={this.scrollable ? 'region' : undefined}\n aria-label={this.scrollable ? regionLabel : undefined}\n tabIndex={this.scrollable ? 0 : undefined}\n >\n <table\n class={{\n 'ds-table__table': true,\n 'ds-table__table--selectable': model.selectable,\n 'ds-table__table--grouped': model.grouped,\n 'ds-table__table--deferred-rows':\n this.dataMode !== 'virtual' && !this.structure.merges.hasRowSpans,\n 'ds-table__table--windowed': windowRows,\n 'ds-table__table--virtual': this.dataMode === 'virtual' && windowRows,\n 'ds-table__table--native-group-sticky':\n (model.grouped && this.stickyHeader && !this.documentStickyHeader) ||\n windowRows,\n }}\n style={model.tableStyle}\n role={this.gridEnabled ? 'grid' : 'table'}\n aria-multiselectable={this.gridEnabled ? 'true' : undefined}\n aria-rowcount={\n windowRows\n ? 1 + Number(this.structure.bands.length > 0) + this.virtualItems.length\n : undefined\n }\n aria-busy={\n initialLoading ||\n (this.dataMode === 'infinite' && (this.loadingMore || groupLoadingMore))\n ? 'true'\n : undefined\n }\n ref={element => {\n this.tableEl = element ?? null;\n }}\n >\n <caption class=\"ds-table__caption ds-visually-hidden\">\n <ds-text as=\"span\" variant=\"text-title-small\" emphasis={true} color=\"primary\">\n {this.caption}\n </ds-text>\n </caption>\n {this.renderColgroup(model)}\n {this.renderHeader(\n model,\n !this.documentStickyHeader,\n false,\n windowRows ? 1 : undefined\n )}\n {initialLoading\n ? this.renderSkeletonBody(model)\n : initialError\n ? this.renderStateBody('error', model.totalColumns)\n : virtualViewportMissing\n ? this.renderStateBody('virtual-viewport', model.totalColumns)\n : model.hasData || hasGroupedStructure\n ? this.renderDataBodies(model, virtualPlan)\n : this.renderStateBody('empty', model.totalColumns)}\n {!initialLoading &&\n !initialError &&\n this.dataMode !== 'virtual' &&\n this.renderLazyBody(model)}\n </table>\n </div>\n </div>\n {this.renderResultFooter()}\n {this.renderOverflowActionMenu()}\n {this.renderDataModeSwitcherMenu()}\n {this.renderColumnCustomizerMenu()}\n {this.renderTruncateTooltip()}\n <div class=\"ds-visually-hidden\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {this.announcement}\n </div>\n </div>\n </Host>\n );\n }\n}\n",
1681
+ "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Listen,\n Method,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n deriveTableSelectionState,\n formatTableResultSummary,\n formatTableTotalSummary,\n hasOwnedTableFooterSlot,\n isTableCellAction,\n nextDataSortState,\n tableColumnSize,\n tableModelIssues,\n TABLE_GROUP_HERO_SCORE_PRESENTATION,\n tableRows,\n toggleAllLoadedTableRows,\n toggleTableGroupCollapsed,\n toggleTableGroupSelection,\n nextTableGroupsCollapsed,\n toggleTableRowSelection,\n} from './table-model';\nimport { createTableRenderModel, type TableRenderModel } from './table-render-model';\nimport {\n isRenderableTableActionMenu,\n nextTableActionMenuElementId,\n tableActionMenuSections,\n tableActionTriggerId,\n} from './table-action-menu';\nimport {\n nextTableColumnCustomizerElementId,\n reorderTableColumnPartition,\n resolveTableFieldsConfiguration,\n resolveTableVisibleColumns,\n tableColumnCustomizerSections,\n toggleTableColumnHidden,\n toggleTableColumnPinned,\n} from './table-column-customizer';\nimport {\n nextTableDataModeSwitcherElementId,\n tableDataModeFromMenuItem,\n tableDataModeMenuItems,\n} from './table-data-mode-switcher';\nimport { resolveTableTruncateTrack, tableTruncateLabel } from './table-truncate';\nimport { renderTableRow as renderTableRowView } from './table-row-view';\nimport { TableBodyRenderer } from './table-body-renderer';\nimport { tableHeaderBands, tablePinnedColumns, tableSpanModel } from './table-structure';\nimport {\n tableGridModel,\n tableRangeBounds,\n tableEditable,\n tableEditValue,\n tablePasteChanges,\n tableNextEditable,\n} from './table-grid-model';\nimport { renderTableCellEditor } from './table-cell-editor';\nimport type {\n TableCellSpan,\n TableCellRange,\n TableCellAddress,\n TableCellsChangeDetail,\n} from './table-types';\nimport { renderTableSkeletonBody } from './table-skeleton-view';\nimport { renderTableLoadContent } from './table-load-view';\nimport {\n isTableCardViewport,\n TABLE_CARD_VIEWPORT_BREAKPOINT,\n TableLayoutController,\n} from './table-layout-controller';\nimport { TableLoadController } from './table-load-controller';\nimport { TableGroupLoadController } from './table-group-load-controller';\nimport {\n estimateTableRowBlockSize,\n flattenTableVirtualItems,\n resolveTableVirtualPlan,\n TABLE_VIRTUAL_TOTAL_SUMMARY_LABEL,\n TABLE_VIRTUAL_VIEWPORT_REQUIRED_BODY,\n TABLE_VIRTUAL_VIEWPORT_REQUIRED_HEADING,\n type TableVirtualItem,\n type TableVirtualPlan,\n} from './table-virtual-model';\nimport { TableVirtualController } from './table-virtual-controller';\nimport type { PaginationChangeDetail } from '../Pagination/pagination-types';\nimport type { MenuItemData, MenuItemToggleDetail, MenuReorderDetail } from '../Menu/menu-types';\nimport { resolvePaginationState } from '../Pagination/pagination-model';\nimport { resolveCssLengthPx } from '../../utils/resolve-css-length-px';\nimport { isElementTruncated } from '../../utils/is-element-truncated';\nimport { observeTableCaptionCompact } from '../../utils/table-caption-compact';\nimport { resolveTableFitPageSize } from './table-pagination-fit';\nimport { createTableHighlightMatcher, type TableHighlightMatcher } from './table-highlight';\nimport {\n paginationShortcutBlockedByPath,\n shouldHandleContainingPagePaginationShortcut,\n} from './table-pagination-shortcut';\nimport {\n TableViewportFitController,\n type TableViewportFitMetrics,\n} from './table-viewport-fit-controller';\nimport type {\n TableCaptionVisibility,\n TableChromeLayout,\n TableCellActionDetail,\n TableColumn,\n DataFieldsConfigChangeDetail,\n TableDataMode,\n TableDataModeChangeDetail,\n DataGroup,\n TableGroupCollapseChangeDetail,\n TableGroupLoadMoreDetail,\n DataGroupingState,\n TableLoadMoreDetail,\n TableLoadMoreMode,\n TablePaginationState,\n TableRow,\n TableRowActivateDetail,\n TableSelectionChangeDetail,\n TableSelectionMode,\n DataSortChangeDetail,\n DataSortState,\n} from './table-types';\n\nconst TABLE_FOOTER_SLOT_LEADING = 1;\nconst TABLE_FOOTER_SLOT_COPY = 2;\nconst TABLE_FOOTER_SLOT_TRAILING = 4;\nconst TABLE_NON_VIRTUAL_WINDOW_THRESHOLD = 50;\n\n@Component({\n tag: 'ds-table',\n styleUrls: ['../../utils/focus-ring.css', '../../utils/interaction-fill.css', 'Table.css'],\n scoped: true,\n})\nexport class Table {\n @Element() el!: HTMLElement;\n /** Stable column definitions. Assign through JavaScript. */\n @Prop() columns: TableColumn[] = [];\n /** Opt-in native card presentation below the 768px browser viewport breakpoint. */\n @Prop() responsiveLayout: 'scroll' | 'cards' = 'scroll';\n /** Keep all table chrome contained, or let the caption and footer span edge to edge around an inset data frame. */\n @Prop() chromeLayout: TableChromeLayout = 'contained';\n /** Explicit report merges; invalid combinations leave the original cells visible. */\n @Prop() cellSpans: TableCellSpan[] = [];\n /** Table is read-only; edit uses per-cell pencil buttons; grid adds cell selection and keyboard navigation. Grouped tables remain read-only. */\n @Prop() interactionMode: 'table' | 'edit' | 'grid' = 'table';\n /** Controlled rectangular cell selection, independent of row checkbox selection. */\n @Prop() cellRange: TableCellRange | null = null;\n @Event() dsCellRangeChange!: EventEmitter<TableCellRange>;\n @Event() dsCellsChange!: EventEmitter<TableCellsChangeDetail>;\n /** Additional toggle options in the column customizer. */\n @Prop() customizeOptions: MenuItemData[] = [];\n @Event() dsCustomizeOptionChange!: EventEmitter<string>;\n /** Ungrouped row data. Ignored while grouping is active. Assign through JavaScript. */\n @Prop() rows: TableRow[] = [];\n /** One level of application-owned grouped data. Assign through JavaScript. */\n @Prop() groups: DataGroup[] = [];\n /** Controlled grouping column. Applications supply groups in their final fixed order. */\n @Prop() grouping: DataGroupingState | null = null;\n /** Controlled member-row sort state. */\n @Prop() sort: DataSortState | null = null;\n /** Controlled collapsed group identities. Groups not listed remain expanded. */\n @Prop() collapsedGroupIds: string[] = [];\n /** Literal terms to highlight in table-owned text cells. Applications still own filtering. */\n @Prop() highlightTerms: string[] = [];\n /** Optional DataSearch field identities that restrict which data-point tracks are highlighted. */\n @Prop() highlightFieldIds: string[] = [];\n\n /** Required accessible table name, retained as a native caption. */\n @Prop() caption!: string;\n /** Shows a matching presentational title bar above the native table frame. */\n @Prop() captionVisibility: TableCaptionVisibility = 'hidden';\n /**\n * Opt in to the table-owned column customizer. The `columns` prop remains the\n * catalog; hidden and ordered columns are controlled separately. The trigger\n * opens the shared Menu of live show/hide switch rows.\n */\n @Prop() columnCustomizer: boolean = false;\n /** Hide only the built-in Customize trigger when an external preferences surface owns it. */\n /** Remove borders from table-owned caption controls. */\n @Prop() captionControlsBorderless: boolean = false;\n @Prop() hideColumnCustomizerTrigger: boolean = false;\n /** Controlled hidden data-column identities. Action ids are ignored. */\n @Prop() hiddenFieldIds: string[] = [];\n /** Controlled data-column identities in display order. Omitted ids append in catalog order. */\n @Prop() fieldOrder: string[] = [];\n /** Controlled user-pinned data-column identities. Hidden columns retain their pin state. */\n @Prop() pinnedFieldIds: string[] = [];\n /**\n * Optional result summary footer. When both `displayedCount` and `totalCount`\n * are finite numbers, infinite mode shows “Displaying {displayed} of {total}”.\n * Virtual mode ignores `displayedCount` and derives its total from the\n * complete supplied rows; a mismatched `totalCount` emits a warning.\n */\n @Prop() displayedCount: number | undefined;\n @Prop() totalCount: number | undefined;\n /** Supports {displayed} and {total} placeholders. */\n @Prop() resultSummaryLabel: string = 'Displaying {displayed} of {total}';\n /** Supports the {total} placeholder. Used when dataMode is virtual. */\n @Prop() resultTotalSummaryLabel: string = TABLE_VIRTUAL_TOTAL_SUMMARY_LABEL;\n @Prop() stickyHeader: boolean = false;\n /** Maximum scroll-region height. Numbers resolve to CSS pixels. */\n @Prop() maxHeight: string | number | undefined;\n /** Fixed height for the complete header, table frame, and footer composition. */\n @Prop() height: string | number | undefined;\n /** Fit the complete table composition to its nearest vertical scrollport. */\n @Prop() fitViewport: boolean = false;\n /** Reserved space above a viewport-fitted table once surrounding chrome is compact. */\n @Prop() viewportInsetBlockStart: string | number = 0;\n /** Reserved space below a viewport-fitted table. */\n @Prop() viewportInsetBlockEnd: string | number = 0;\n /** Optional explicit label for the horizontal/vertical scroll region. */\n @Prop() scrollLabel: string | undefined;\n\n @Prop() selectionMode: TableSelectionMode = 'none';\n /** Controlled selected row identities. IDs outside the loaded rows are preserved. */\n @Prop() selectedRowIds: string[] = [];\n\n /**\n * Identity of the interactive row whose associated surface is visible.\n * Holds its pressed wash independently of checkbox selection. Keep the ID\n * through exit motion and clear it after close; the application owns toggling\n * and switching the surface. Off-window identities survive virtualization.\n */\n @Prop() surfaceOpenRowId: string | undefined;\n\n /** Initial loading state. Existing rows stay visible; incremental loading uses loadingMore. */\n @Prop() loading: boolean = false;\n /** Replace opted-in table-owned caption controls with same-size visual skeletons. */\n @Prop() chromeLoading: boolean = false;\n /** Initial-loading rows. Defaults to ten so bounded tables retain a useful filled viewport. */\n @Prop() skeletonRows: number = 10;\n @Prop() emptyHeading: string = 'No results';\n @Prop() emptyBody: string = 'No data is available.';\n /** Initial error state. Existing rows stay visible; incremental failures use loadMoreError. */\n @Prop() error: boolean = false;\n @Prop() errorHeading: string = 'Unable to load data';\n @Prop() errorBody: string = 'The data could not be loaded.';\n @Prop() emptyCellLabel: string = 'Not available';\n\n /** Top-level data-window strategy. Virtual mode recycles row DOM only. */\n @Prop() dataMode: TableDataMode = 'infinite';\n /** Opt in to the table-owned control for choosing between supported data modes. */\n @Prop() dataModeSwitcher: boolean = false;\n @Prop() dataModeSwitcherLabel: string = 'Change table variation';\n @Prop() dataModeMenuLabel: string = 'Table variation';\n @Prop() infiniteModeLabel: string = 'Infinite scroll';\n @Prop() paginationModeLabel: string = 'Pagination + Infinite groups';\n @Prop() virtualModeLabel: string = 'Virtual scroll';\n /** Controlled top-level pagination state. Required when dataMode is pagination. */\n @Prop() pagination: TablePaginationState | null = null;\n @Prop() loadMoreMode: TableLoadMoreMode = 'auto';\n @Prop() hasMore: boolean = false;\n @Prop() loadingMore: boolean = false;\n @Prop() loadMoreError: string | undefined;\n /** Reset key for a new query/group/sort dataset. */\n @Prop() loadIdentity: string | number = 'default';\n /** IntersectionObserver root margin in CSS pixels. */\n @Prop() loadMoreThreshold: number = 0;\n @Prop() loadMoreLabel: string = 'Load more';\n @Prop() retryLabel: string = 'Retry';\n @Prop() loadingMoreLabel: string = 'Loading more items';\n @Prop() endOfResultsLabel: string = 'All results loaded';\n /** Supports {count} and {total} placeholders. */\n @Prop() rowsLoadedLabel: string = '{count} more rows loaded. {total} rows loaded.';\n @Prop() groupLoadMoreLabel: string = 'Load more';\n /** Supports the {group} placeholder. */\n @Prop() groupLoadMoreAriaLabel: string = 'Load more {group} results';\n /** Supports the {group} placeholder. */\n @Prop() groupRetryLabel: string = 'Retry loading {group} results';\n /** Supports the {group} placeholder. */\n @Prop() groupLoadingMoreLabel: string = 'Loading more items';\n /** Supports the {group} placeholder. */\n @Prop() groupEndOfResultsLabel: string = 'All {group} results loaded';\n /** Supports {group}, {count}, {loaded}, and {total} placeholders. */\n @Prop() groupRowsLoadedLabel: string =\n '{count} more rows loaded in {group}. {loaded} of {total} rows loaded.';\n\n @Event() dsSortChange!: EventEmitter<DataSortChangeDetail>;\n @Event() dsGroupCollapseChange!: EventEmitter<TableGroupCollapseChangeDetail>;\n @Event() dsSelectionChange!: EventEmitter<TableSelectionChangeDetail>;\n @Event() dsLoadMore!: EventEmitter<TableLoadMoreDetail>;\n @Event() dsGroupLoadMore!: EventEmitter<TableGroupLoadMoreDetail>;\n @Event() dsPaginationChange!: EventEmitter<PaginationChangeDetail>;\n @Event() dsCellAction!: EventEmitter<TableCellActionDetail>;\n @Event() dsRowActivate!: EventEmitter<TableRowActivateDetail>;\n @Event() dsFieldsConfigChange!: EventEmitter<DataFieldsConfigChangeDetail>;\n @Event() dsDataModeChange!: EventEmitter<TableDataModeChangeDetail>;\n\n @State() private overflowStart = false;\n @State() private overflowEnd = false;\n @State() private scrollable = false;\n @State() private announcement = '';\n @State() private activeStickyGroupId: string | null = null;\n @State() private viewportFitSettled = false;\n @State() private headerPresent = false;\n @State() private headerUsesToolbar = false;\n @State() private footerSlotPresence = 0;\n @State() private fitPageSize: number | undefined;\n @State() private actionMenu: { rowId: string; columnId: string } | null = null;\n @State() private actionMenuInitialFocusVisible = false;\n @State() private actionMenuSurface: { rowId: string; columnId: string } | null = null;\n @State() private truncateTooltipLabel = '';\n @State() private captionCompact = false;\n @State() private narrowViewport = false;\n @State() private columnCustomizerOpen = false;\n @State() private columnCustomizerSurfaceOpen = false;\n @State() private columnCustomizerInitialFocusVisible = false;\n @State() private dataModeSwitcherOpen = false;\n @State() private dataModeSwitcherSurfaceOpen = false;\n @State() private dataModeSwitcherInitialFocusVisible = false;\n @State() private virtualWindow: TableVirtualPlan | null = null;\n @State() private activeCell: TableCellAddress | null = null;\n @State() private editingCell: TableCellAddress | null = null;\n private editDraft = '';\n private gridModelCache?: ReturnType<typeof tableGridModel>;\n private get gridEnabled(): boolean {\n return this.interactionMode === 'grid' && !this.grouped;\n }\n private get editingEnabled(): boolean {\n return this.interactionMode !== 'table' && !this.grouped;\n }\n private get gridModel() {\n if (\n !this.gridModelCache ||\n this.gridModelCache.rows !== this.rows ||\n this.gridModelCache.columns !== this.visibleColumns\n ) {\n this.gridModelCache = tableGridModel(this.rows, this.visibleColumns);\n }\n return this.gridModelCache;\n }\n private gridAddress(event: Event): TableCellAddress | null {\n if (\n !this.gridEnabled ||\n !(event.target instanceof HTMLElement) ||\n !event.target.matches('td[role=\"gridcell\"][data-cell-editable=\"true\"]')\n )\n return null;\n const rowId = event.target.closest<HTMLElement>('[data-row-id]')?.dataset.rowId;\n const columnId = event.target.dataset.columnId;\n return rowId && columnId ? { rowId, columnId } : null;\n }\n private selectCell(address: TableCellAddress, extend = false): void {\n if (!this.isEditableAddress(address)) return;\n this.activeCell = address;\n this.dsCellRangeChange.emit({\n anchor: extend ? (this.cellRange?.anchor ?? address) : address,\n focus: address,\n });\n }\n private async focusCell(address: TableCellAddress): Promise<void> {\n if (!this.isEditableAddress(address)) return;\n this.activeCell = address;\n this.focusedRowId = address.rowId;\n await this.scrollRowIntoView(address.rowId);\n await this.nextAnimationFrame();\n const cell = this.findRenderedRow(address.rowId)?.querySelector<HTMLElement>(\n `[data-column-id=\"${CSS.escape(address.columnId)}\"]`\n );\n if (this.gridEnabled) cell?.focus();\n else\n await cell\n ?.querySelector<HTMLDsButtonUnfilledElement>('.ds-table__edit-trigger ds-button-unfilled')\n ?.setFocus();\n }\n private isEditableAddress(address: TableCellAddress): boolean {\n const model = this.gridModel;\n const row = model.rows[model.rowIndices.get(address.rowId) ?? -1];\n const column = model.columns[model.columnIndices.get(address.columnId) ?? -1];\n return this.editingEnabled && !this.loading && !!row && !!column && tableEditable(row, column);\n }\n private beginCellEdit(address: TableCellAddress, initial?: string): void {\n const model = this.gridModel;\n const row = model.rows[model.rowIndices.get(address.rowId) ?? -1];\n const column = model.columns[model.columnIndices.get(address.columnId) ?? -1];\n if (!this.editingEnabled || this.loading || !row || !column || !tableEditable(row, column))\n return;\n this.activeCell = address;\n this.focusedRowId = address.rowId;\n this.editDraft = initial ?? String(row.cells[column.id] ?? '');\n this.editingCell = address;\n requestAnimationFrame(async () => {\n const control = this.el.querySelector<HTMLDsInputElement>('.ds-table__cell-editor > *');\n await control?.componentOnReady?.();\n if (this.editingCell !== address || !control?.isConnected) return;\n await control.setFocus();\n const input = control.querySelector<HTMLInputElement | HTMLTextAreaElement>('input,textarea');\n if (initial == null && input?.type !== 'number') input?.select();\n });\n }\n private handleGridKeyDown(event: KeyboardEvent): void {\n const address = this.gridAddress(event);\n if (!address || event.altKey || event.metaKey || event.ctrlKey || event.isComposing) return;\n if (event.key === 'Enter' || event.key === 'F2' || event.key.length === 1) {\n event.preventDefault();\n this.beginCellEdit(address, event.key.length === 1 ? event.key : undefined);\n return;\n }\n if (!['ArrowDown', 'ArrowUp', 'ArrowRight', 'ArrowLeft', 'Home', 'End'].includes(event.key))\n return;\n event.preventDefault();\n const next = tableNextEditable(this.gridModel, address, event.key);\n if (!next) return;\n this.selectCell(next, event.shiftKey);\n void this.focusCell(next);\n }\n private handleGridClipboard(event: ClipboardEvent): void {\n const address = this.gridAddress(event);\n if (!address || !event.clipboardData) return;\n if (event.type === 'paste') {\n event.preventDefault();\n if (this.loading) return;\n const changes = tablePasteChanges(\n this.gridModel,\n address,\n event.clipboardData.getData('text/plain')\n );\n if (!changes) {\n this.announcement =\n 'Paste rejected. Use a rectangular set of valid values in editable cells.';\n return;\n }\n this.dsCellsChange.emit({ changes, reason: 'paste' });\n this.announcement = `${changes.length} cell changes proposed.`;\n } else {\n const model = this.gridModel;\n const bounds = tableRangeBounds(model, this.cellRange ?? { anchor: address, focus: address });\n if (\n !bounds ||\n (bounds.lastRow - bounds.firstRow + 1) * (bounds.lastColumn - bounds.firstColumn + 1) >\n 10000\n )\n return;\n const text = model.rows\n .slice(bounds.firstRow, bounds.lastRow + 1)\n .map(row =>\n model.columns\n .slice(bounds.firstColumn, bounds.lastColumn + 1)\n .map(column => {\n if (!tableEditable(row, column)) return '';\n const value = row.cells[column.id];\n return typeof value === 'object' ? '' : String(value ?? '').replace(/[\\t\\r\\n]/g, ' ');\n })\n .join('\\t')\n )\n .join('\\n');\n event.preventDefault();\n event.clipboardData.setData('text/plain', text);\n }\n }\n private renderCellEditor(row: TableRow, column: TableColumn) {\n if (\n this.editingCell?.rowId !== row.id ||\n this.editingCell.columnId !== column.id ||\n !tableEditable(row, column) ||\n this.loading\n )\n return null;\n const address = this.editingCell;\n const editor = column.editor!;\n const cell = this.findRenderedRow(row.id)?.querySelector<HTMLElement>(\n `[data-column-id=\"${CSS.escape(column.id)}\"]`\n );\n const commit = (text: string, next = address) => {\n if (this.editingCell !== address || !this.isEditableAddress(address)) return false;\n const value = tableEditValue(text, column);\n if (value === undefined) {\n this.announcement = `Enter a valid value for ${column.label}.`;\n return false;\n }\n this.editingCell = null;\n this.dsCellsChange.emit({ changes: [{ ...address, value }], reason: 'edit' });\n void this.focusCell(next);\n return true;\n };\n return (\n <div\n class=\"ds-table__cell-editor\"\n key={`${row.id}:${column.id}`}\n onClick={event => event.stopPropagation()}\n onFocusout={event => {\n const wrapper = event.currentTarget as HTMLElement;\n requestAnimationFrame(() => {\n const root = wrapper.getRootNode() as Document | ShadowRoot;\n if (this.editingCell === address && !wrapper.contains(root.activeElement))\n this.editingCell = null;\n });\n }}\n onKeyDown={event => {\n event.stopPropagation();\n if (event.isComposing || event.defaultPrevented) return;\n const wrapper = event.currentTarget as HTMLElement;\n if (event.key === 'Escape') {\n event.preventDefault();\n this.editingCell = null;\n void this.focusCell(address);\n return;\n }\n if (event.key !== 'Enter' && event.key !== 'Tab') return;\n // Picker keyboard interactions belong to the picker, including its internal Tab stops.\n if ((event.target as HTMLElement).closest('[popover]')) return;\n if (\n event.key === 'Enter' &&\n editor.type === 'textarea' &&\n !event.ctrlKey &&\n !event.metaKey\n )\n return;\n event.preventDefault();\n const input =\n editor.type === 'select'\n ? null\n : wrapper.querySelector<HTMLInputElement | HTMLTextAreaElement>('input,textarea');\n // Unconstrained numeric columns retain decimal support; explicit steps use the\n // shared control's native step validity as well as the paste validator.\n if (\n input &&\n !input.checkValidity() &&\n !(editor.type === 'number' && editor.step == null && input.validity.stepMismatch)\n ) {\n input.reportValidity();\n return;\n }\n const next =\n event.key === 'Tab'\n ? (tableNextEditable(this.gridModel, address, 'Tab', event.shiftKey) ?? address)\n : address;\n commit(input?.value ?? this.editDraft, next);\n }}\n >\n {renderTableCellEditor(\n editor,\n this.editDraft,\n `Edit ${column.label} for ${row.selectionLabel ?? row.id}`,\n cell ?? undefined,\n value => {\n this.editDraft = value;\n // A single-select choice is a complete edit; let its selection handler finish first.\n if (editor.type === 'select') queueMicrotask(() => commit(value));\n }\n )}\n </div>\n );\n }\n\n private renderCellEditTrigger(row: TableRow, column: TableColumn) {\n if (\n this.interactionMode !== 'edit' ||\n !tableEditable(row, column) ||\n this.loading ||\n (this.editingCell?.rowId === row.id && this.editingCell.columnId === column.id)\n )\n return null;\n return (\n <span class=\"ds-table__edit-trigger\">\n <ds-button-unfilled\n variant=\"icon\"\n icon=\"Pencil\"\n size=\"sm\"\n aria-label={`Edit ${column.label} for ${row.selectionLabel ?? row.id}`}\n pressScale={false}\n onDsClick={event => {\n event.stopPropagation();\n this.beginCellEdit({ rowId: row.id, columnId: column.id });\n }}\n />\n </span>\n );\n }\n\n private gridCellAttributes(row: TableRow, column: TableColumn): Record<string, unknown> {\n const model = this.gridModel;\n const bounds = tableRangeBounds(model, this.cellRange);\n const ri = model.rowIndices.get(row.id)!,\n ci = model.columnIndices.get(column.id)!;\n const editable = tableEditable(row, column) && !this.loading;\n const editing =\n editable && this.editingCell?.rowId === row.id && this.editingCell.columnId === column.id;\n const active =\n this.activeCell && this.isEditableAddress(this.activeCell)\n ? this.activeCell\n : model.firstEditable;\n if (!this.gridEnabled)\n return {\n 'data-cell-editable': String(editable),\n 'data-cell-editing': String(editing),\n 'aria-disabled': editable ? undefined : 'true',\n };\n return {\n role: 'gridcell',\n tabIndex: editable\n ? active?.rowId === row.id && active.columnId === column.id\n ? 0\n : -1\n : undefined,\n 'data-cell-editable': String(editable),\n 'data-cell-editing': String(editing),\n 'aria-readonly': String(!editable),\n 'aria-disabled': editable ? undefined : 'true',\n 'aria-selected': String(\n editable &&\n !!bounds &&\n ri >= bounds.firstRow &&\n ri <= bounds.lastRow &&\n ci >= bounds.firstColumn &&\n ci <= bounds.lastColumn\n ),\n onClick: (event: MouseEvent) => {\n if (\n event.target !== event.currentTarget &&\n (event.target as HTMLElement).closest(\n 'button,a,input,textarea,ds-button-unfilled,.ds-table__cell-editor'\n )\n )\n return;\n event.stopPropagation();\n if (!editable) return;\n this.selectCell({ rowId: row.id, columnId: column.id }, event.shiftKey);\n (event.currentTarget as HTMLElement).focus();\n if (!event.shiftKey) this.beginCellEdit({ rowId: row.id, columnId: column.id });\n },\n };\n }\n\n private readonly actionMenuElementId = nextTableActionMenuElementId();\n private structureCache?: {\n columns: TableColumn[];\n rows: TableRow[];\n spans: TableCellSpan[];\n selectable: boolean;\n grouped: boolean;\n windowed: boolean;\n grid: boolean;\n pins: ReturnType<typeof tablePinnedColumns>;\n bands: ReturnType<typeof tableHeaderBands>;\n merges: ReturnType<typeof tableSpanModel>;\n };\n\n private get structure() {\n const columns = this.visibleColumns;\n const model = this.createRenderModel();\n const cached = this.structureCache;\n const windowed = this.rowWindowingEnabled;\n const grid = this.interactionMode !== 'table';\n if (\n cached &&\n cached.columns === columns &&\n cached.rows === this.rows &&\n cached.spans === this.cellSpans &&\n cached.selectable === model.selectable &&\n cached.grouped === model.grouped &&\n cached.windowed === windowed &&\n cached.grid === grid\n )\n return cached;\n return (this.structureCache = {\n columns,\n rows: this.rows,\n spans: this.cellSpans,\n selectable: model.selectable,\n grouped: model.grouped,\n windowed,\n grid,\n pins: tablePinnedColumns(columns, model.selectable),\n bands: tableHeaderBands(columns, model.selectable, model.elasticSpacerIndex),\n merges: tableSpanModel(this.cellSpans, this.rows, columns, {\n windowed,\n grouped: model.grouped,\n grid,\n selectable: model.selectable,\n spacer: model.elasticSpacerIndex,\n }),\n });\n }\n\n private headerId(columnId: string): string {\n return `${this.actionMenuElementId}-header-${encodeURIComponent(columnId)}`;\n }\n\n private columnBandId(columnId: string): string | undefined {\n const model = this.createRenderModel();\n const index = this.visibleColumns.findIndex(column => column.id === columnId);\n const lane =\n index +\n 1 +\n Number(model.selectable) +\n Number(model.elasticSpacerIndex != null && index >= model.elasticSpacerIndex);\n const band = this.structure.bands.find(\n band => band.label && lane >= band.start && lane < band.start + band.span\n );\n return band ? `${this.actionMenuElementId}-${band.key}` : undefined;\n }\n private readonly columnCustomizerElementId = nextTableColumnCustomizerElementId();\n private readonly dataModeSwitcherElementId = nextTableDataModeSwitcherElementId();\n private truncateTooltipEl?: HTMLDsTooltipElement;\n private truncateAnchor: HTMLElement | null = null;\n private truncateTooltipBound = false;\n private focusedRowId: string | null = null;\n private virtualRenderFocus: HTMLElement | null = null;\n private virtualItems: TableVirtualItem[] = [];\n private visibleColumnsCache: {\n columns: TableColumn[];\n columnCustomizer: boolean;\n hiddenFieldIds: string[];\n fieldOrder: string[];\n pinnedFieldIds: string[];\n value: TableColumn[];\n } | null = null;\n private renderModelCache: {\n columns: TableColumn[];\n rows: TableRow[];\n groups: DataGroup[];\n grouped: boolean;\n selectionMode: TableSelectionMode;\n selectedRowIds: string[];\n collapsedGroupIds: string[];\n virtualCounts: boolean;\n value: TableRenderModel;\n } | null = null;\n private highlightMatcherCache: {\n terms: string[];\n value: TableHighlightMatcher;\n } | null = null;\n private virtualItemsCache: {\n columns: TableColumn[];\n rows: TableRow[];\n groups: DataGroup[];\n grouped: boolean;\n collapsedGroupIds: string[];\n } | null = null;\n private rootEl: HTMLElement | null = null;\n private viewportEl: HTMLElement | null = null;\n private frameEl: HTMLElement | null = null;\n private interactiveHeadEl: HTMLTableSectionElement | null = null;\n private collapseAllOverlayEl: HTMLElement | null = null;\n private stickyHeaderTableEl: HTMLTableElement | null = null;\n private tableEl: HTMLTableElement | null = null;\n private sentinelEl: HTMLElement | null = null;\n private incrementalWindowActive = false;\n private readonly groupSentinelEls = new Map<string, HTMLElement>();\n private previousModelWarning = '';\n private modelWarningQueued = false;\n private initialModelWarningFrame: number | undefined;\n private hasLoaded = false;\n private renderedModel: TableRenderModel | null = null;\n private stickyGroupConnected = false;\n private headerSlotObserver: MutationObserver | null = null;\n private footerSlotObserver: MutationObserver | null = null;\n private captionCompactDisconnect: (() => void) | undefined;\n private fitResizeObserver: ResizeObserver | null = null;\n private fitObservedViewport: HTMLElement | null = null;\n private fitObservedTable: HTMLTableElement | null = null;\n private fitMeasurementPending = false;\n private cardViewportQuery: MediaQueryList | null = null;\n private readonly bodyRenderer = new TableBodyRenderer();\n private readonly layoutController = new TableLayoutController({\n elements: () => ({\n viewport: this.viewportEl,\n contentTable: this.tableEl,\n stickyHeaderTable: this.stickyHeaderTableEl,\n collapseAllOverlay: this.collapseAllOverlayEl,\n frame: this.frameEl,\n interactiveHead: this.interactiveHeadEl,\n }),\n mode: () => ({\n documentStickyHeader: this.documentStickyHeader,\n floatingCollapseAll: this.renderedModel?.collapseAllHost?.mode === 'floating',\n clampVerticalOverscroll: this.fitViewport && this.viewportFitSettled,\n }),\n verticalEdgeWheel: deltaY => this.viewportFitController.scrollOuterBy(deltaY),\n overflowChanged: state => {\n if (state.start !== this.overflowStart) this.overflowStart = state.start;\n if (state.end !== this.overflowEnd) this.overflowEnd = state.end;\n if (state.scrollable !== this.scrollable) this.scrollable = state.scrollable;\n },\n });\n private readonly loadController = new TableLoadController({\n state: () => ({\n lazyLoading: this.dataMode === 'infinite' && !this.grouped,\n loadMoreMode: this.loadMoreMode,\n hasMore: this.hasMore,\n loadingMore: this.loadingMore,\n loadMoreError: this.loadMoreError,\n loadIdentity: this.loadIdentity,\n loadMoreThreshold: this.loadMoreThreshold,\n containedScroll: this.containedScroll,\n loadingMoreLabel: this.loadingMoreLabel,\n endOfResultsLabel: this.endOfResultsLabel,\n rowsLoadedLabel: this.rowsLoadedLabel,\n loadedRowCount: this.currentLoadedRowCount(),\n viewport: this.viewportEl,\n sentinel: this.sentinelEl,\n }),\n announce: message => {\n this.announcement = message;\n },\n request: detail => {\n this.dsLoadMore.emit(detail);\n },\n });\n private readonly groupLoadController = new TableGroupLoadController({\n state: () => ({\n enabled: this.grouped && this.dataMode !== 'virtual',\n loadMoreMode: this.loadMoreMode,\n loadMoreThreshold: this.loadMoreThreshold,\n containedScroll: this.containedScroll,\n groups: this.groups,\n viewport: this.viewportEl,\n sentinels: this.groupSentinelEls,\n loadingMoreLabel: this.groupLoadingMoreLabel,\n endOfResultsLabel: this.groupEndOfResultsLabel,\n rowsLoadedLabel: this.groupRowsLoadedLabel,\n }),\n announce: message => {\n this.announcement = message;\n },\n request: detail => {\n this.dsGroupLoadMore.emit(detail);\n },\n });\n private readonly virtualController = new TableVirtualController({\n state: () => ({\n enabled: this.rowWindowingEnabled,\n items: this.virtualItems,\n pinnedRowIds: this.virtualPinnedRowIds(),\n viewport: this.viewportEl,\n viewportSize: this.estimateVirtualViewportSize(),\n }),\n windowChanged: plan => {\n this.virtualWindow = plan;\n },\n });\n private readonly viewportFitController = new TableViewportFitController({\n enabled: () => this.fitViewport,\n elements: () => ({ host: this.el, surface: this.rootEl }),\n insets: () => ({\n blockStart: this.viewportInsetBlockStart,\n blockEnd: this.viewportInsetBlockEnd,\n }),\n fitChanged: (metrics: TableViewportFitMetrics | null) => {\n const settled = metrics?.settled ?? false;\n if (settled !== this.viewportFitSettled) this.viewportFitSettled = settled;\n },\n });\n\n componentWillLoad(): void {\n this.connectCardViewportQuery();\n // Reserve the final viewport-fit height before the first paint. The fitted\n // surface is connected after render, but the host can already resolve its\n // owning scrollport and prevent an initial auto-height frame.\n this.viewportFitController.connect();\n this.incrementalWindowActive = this.hasIncrementalState;\n this.syncHeaderSlotPresence();\n this.syncFooterSlotPresence();\n this.loadController.initialize();\n this.groupLoadController.initialize();\n }\n\n componentDidLoad(): void {\n this.hasLoaded = true;\n this.layoutController.connect();\n this.loadController.connect();\n this.groupLoadController.connect();\n this.virtualController.connect();\n this.viewportFitController.connect();\n this.syncStickyGroupConnection();\n this.connectHeaderSlotObserver();\n this.connectFooterSlotObserver();\n this.connectFitObserver();\n this.connectCaptionCompactObserver();\n this.syncFitPageSize();\n this.connectTruncateTooltip();\n this.scheduleInitialModelIssueWarning();\n }\n\n componentWillRender(): void {\n const root = this.el.getRootNode() as Document | ShadowRoot;\n const active = root.activeElement;\n this.virtualRenderFocus =\n this.rowWindowingEnabled &&\n active instanceof HTMLElement &&\n this.tableEl?.contains(active) &&\n active.closest('[data-row-id]')\n ? active\n : null;\n }\n\n componentDidRender(): void {\n // Moving a retained pooled row can clear native focus in Firefox. Restore\n // only the same connected node, without scrolling or stealing external focus.\n const retainedFocus = this.virtualRenderFocus;\n this.virtualRenderFocus = null;\n if (retainedFocus?.isConnected) {\n const root = retainedFocus.getRootNode() as Document | ShadowRoot;\n if (!root.activeElement || root.activeElement === this.el.ownerDocument.body) {\n // Resolve the moved rows' scroll geometry before restoring native focus.\n // WebKit otherwise may reveal the caret using the pre-patch layout even\n // with preventScroll. Preserve both axes within the resulting bounds.\n const viewport = this.viewportEl;\n const top = viewport?.scrollTop ?? 0;\n const left = viewport?.scrollLeft ?? 0;\n retainedFocus.focus({ preventScroll: true });\n if (viewport) {\n if (viewport.scrollTop !== top) viewport.scrollTop = top;\n if (viewport.scrollLeft !== left) viewport.scrollLeft = left;\n }\n }\n }\n this.layoutController.refresh(false);\n this.loadController.refresh();\n this.groupLoadController.refresh();\n this.virtualController.refresh();\n this.virtualController.collectMeasurements(this.tableEl);\n if (\n this.rowWindowingEnabled &&\n this.truncateAnchor &&\n this.tableEl &&\n !this.tableEl.contains(this.truncateAnchor)\n ) {\n this.dismissTruncateTooltip();\n }\n this.viewportFitController.refresh(false);\n this.syncStickyGroupConnection();\n this.connectFitObserver();\n this.connectCaptionCompactObserver();\n this.syncFitPageSize();\n if (this.stickyGroupConnected) this.updateStickyGroup();\n }\n\n /**\n * Scroll the nearest vertical owner only enough to reveal a supplied row.\n * Resolves false when the row is absent from the supplied, expanded table model.\n * This method never moves focus.\n */\n @Method()\n async scrollRowIntoView(rowId: string): Promise<boolean> {\n if (!rowId) return false;\n\n if (this.rowWindowingEnabled) {\n const headerSize = this.tableEl?.tHead?.getBoundingClientRect().height ?? 0;\n const revealed = this.virtualController.scrollRowIntoView(\n rowId,\n headerSize,\n this.stickyHeader ? headerSize : 0\n );\n if (!revealed) return false;\n await this.nextAnimationFrame();\n }\n\n const row = this.findRenderedRow(rowId);\n if (!row) return false;\n this.revealRenderedRow(row);\n return true;\n }\n\n connectedCallback(): void {\n if (!this.hasLoaded) return;\n this.connectCardViewportQuery();\n this.layoutController.connect();\n this.loadController.connect();\n this.groupLoadController.connect();\n this.virtualController.connect();\n this.viewportFitController.connect();\n this.syncStickyGroupConnection();\n this.connectHeaderSlotObserver();\n this.connectFooterSlotObserver();\n this.connectFitObserver();\n this.connectCaptionCompactObserver();\n this.connectTruncateTooltip();\n }\n\n disconnectedCallback(): void {\n this.editingCell = null;\n this.layoutController.disconnect();\n this.loadController.disconnect();\n this.groupLoadController.disconnect();\n this.virtualController.disconnect();\n this.viewportFitController.disconnect();\n this.disconnectStickyGroup();\n this.headerSlotObserver?.disconnect();\n this.headerSlotObserver = null;\n this.footerSlotObserver?.disconnect();\n this.footerSlotObserver = null;\n this.disconnectFitObserver();\n this.disconnectCaptionCompactObserver();\n this.disconnectTruncateTooltip();\n this.disconnectCardViewportQuery();\n this.closeColumnCustomizer();\n if (this.initialModelWarningFrame !== undefined) {\n cancelAnimationFrame(this.initialModelWarningFrame);\n this.initialModelWarningFrame = undefined;\n }\n }\n\n private readonly handleCardViewportChange = (event: MediaQueryListEvent): void => {\n if (this.narrowViewport !== event.matches) this.narrowViewport = event.matches;\n };\n\n private connectCardViewportQuery(): void {\n if (\n this.cardViewportQuery ||\n typeof window === 'undefined' ||\n typeof window.matchMedia !== 'function'\n )\n return;\n const query = window.matchMedia(`(max-width: ${TABLE_CARD_VIEWPORT_BREAKPOINT - 1}px)`);\n this.cardViewportQuery = query;\n const narrow = isTableCardViewport(window.innerWidth);\n if (this.narrowViewport !== narrow) this.narrowViewport = narrow;\n query.addEventListener('change', this.handleCardViewportChange);\n }\n\n private disconnectCardViewportQuery(): void {\n this.cardViewportQuery?.removeEventListener('change', this.handleCardViewportChange);\n this.cardViewportQuery = null;\n }\n\n private syncHeaderSlotPresence = () => {\n const header = this.el.querySelector<HTMLElement>('[slot=\"header\"]');\n this.headerPresent = !!header;\n this.headerUsesToolbar = header?.tagName === 'DS-DATA-TOOLBAR';\n };\n\n private connectHeaderSlotObserver(): void {\n if (this.headerSlotObserver || typeof MutationObserver === 'undefined') return;\n this.headerSlotObserver = new MutationObserver(this.syncHeaderSlotPresence);\n this.headerSlotObserver.observe(this.el, { childList: true });\n this.syncHeaderSlotPresence();\n }\n\n private syncFooterSlotPresence = () => {\n let presence = 0;\n if (hasOwnedTableFooterSlot(this.el, 'footer-leading')) presence |= TABLE_FOOTER_SLOT_LEADING;\n if (hasOwnedTableFooterSlot(this.el, 'footer')) presence |= TABLE_FOOTER_SLOT_COPY;\n if (hasOwnedTableFooterSlot(this.el, 'footer-trailing')) presence |= TABLE_FOOTER_SLOT_TRAILING;\n if (presence !== this.footerSlotPresence) this.footerSlotPresence = presence;\n };\n\n private connectFooterSlotObserver(): void {\n if (this.footerSlotObserver || typeof MutationObserver === 'undefined') return;\n this.footerSlotObserver = new MutationObserver(this.syncFooterSlotPresence);\n this.footerSlotObserver.observe(this.el, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['slot'],\n });\n this.syncFooterSlotPresence();\n }\n\n private connectStickyGroup(): void {\n if (this.stickyGroupConnected || typeof window === 'undefined') return;\n this.stickyGroupConnected = true;\n document.addEventListener('scroll', this.updateStickyGroup, {\n capture: true,\n passive: true,\n });\n window.addEventListener('resize', this.updateStickyGroup, { passive: true });\n this.updateStickyGroup();\n }\n\n private syncStickyGroupConnection(): void {\n if (this.documentStickyHeader && this.grouped) {\n this.connectStickyGroup();\n return;\n }\n\n this.disconnectStickyGroup();\n this.frameEl?.style.removeProperty('--_table-sticky-group-top');\n if (this.activeStickyGroupId !== null) {\n this.activeStickyGroupId = null;\n }\n }\n\n private disconnectStickyGroup(): void {\n if (!this.stickyGroupConnected || typeof window === 'undefined') return;\n this.stickyGroupConnected = false;\n document.removeEventListener('scroll', this.updateStickyGroup, true);\n window.removeEventListener('resize', this.updateStickyGroup);\n }\n\n private readonly updateStickyGroup = (): void => {\n if (!this.documentStickyHeader || !this.grouped || !this.frameEl || !this.tableEl) {\n if (this.activeStickyGroupId !== null) this.activeStickyGroupId = null;\n return;\n }\n\n const stickyHeader = this.frameEl.querySelector<HTMLElement>(\n '.ds-table__document-sticky-header'\n );\n if (!stickyHeader) return;\n const threshold = stickyHeader.getBoundingClientRect().bottom;\n let activeGroupId: string | null = null;\n let activeGroupTop = threshold;\n for (const body of this.tableEl.querySelectorAll<HTMLElement>('tbody[data-group-id]')) {\n const source = body.querySelector<HTMLElement>('.ds-table__group-cell');\n if (!source) continue;\n const sourceRect = source.getBoundingClientRect();\n const bodyRect = body.getBoundingClientRect();\n if (sourceRect.top <= threshold && bodyRect.bottom > threshold) {\n activeGroupId = body.dataset.groupId ?? null;\n // A sticky section is bounded by its own row group. As that boundary\n // approaches the sticky lane, move the outgoing section bar with it so\n // the incoming source bar pushes it away instead of rendering beneath it.\n activeGroupTop = Math.min(threshold, bodyRect.bottom - sourceRect.height);\n }\n }\n\n const frameRect = this.frameEl.getBoundingClientRect();\n this.frameEl.style.setProperty(\n '--_table-sticky-group-top',\n `${activeGroupTop - frameRect.top}px`\n );\n\n if (activeGroupId !== this.activeStickyGroupId) {\n this.activeStickyGroupId = activeGroupId;\n return;\n }\n };\n\n @Watch('columns')\n @Watch('grouping')\n @Watch('hiddenFieldIds')\n @Watch('fieldOrder')\n @Watch('pinnedFieldIds')\n @Watch('columnCustomizer')\n @Watch('cellSpans')\n @Watch('interactionMode')\n @Watch('responsiveLayout')\n handleStructureChange(): void {\n this.virtualItemsCache = null;\n this.scheduleModelIssueWarning();\n this.loadController.structureChanged();\n this.groupLoadController.structureChanged();\n this.virtualController.invalidateMeasures();\n }\n\n @Watch('rows')\n @Watch('groups')\n handleDataChange(): void {\n if (this.grouped) this.groupLoadController.dataChanged();\n else this.loadController.dataChanged();\n this.scheduleModelIssueWarning();\n this.syncActionMenu();\n this.virtualController.invalidateMeasures();\n }\n\n @Watch('totalCount')\n handleTotalCountChange(): void {\n this.scheduleModelIssueWarning();\n }\n\n @Watch('caption')\n handleCaptionChange(): void {\n this.scheduleModelIssueWarning();\n }\n\n @Watch('loadIdentity')\n handleLoadIdentityChange(): void {\n this.incrementalWindowActive = this.hasIncrementalState;\n this.loadController.identityChanged();\n this.virtualController.resetToTop();\n }\n\n @Watch('sort')\n @Watch('grouping')\n handleVirtualIndexReset(): void {\n this.virtualController.resetToTop();\n }\n\n @Watch('loadMoreMode')\n @Watch('loadMoreThreshold')\n handleLazyConfigurationChange(): void {\n this.loadController.configurationChanged();\n this.groupLoadController.configurationChanged();\n }\n\n @Watch('dataMode')\n handleDataModeChange(): void {\n this.incrementalWindowActive = this.hasIncrementalState;\n this.scheduleModelIssueWarning();\n this.handleLazyConfigurationChange();\n this.virtualController.resetToTop();\n }\n\n @Watch('pagination')\n handlePaginationChange(\n pagination: TablePaginationState | null,\n previous: TablePaginationState | null\n ): void {\n if (pagination?.pageSizeMode === 'fit' && pagination.fitIdentity !== previous?.fitIdentity) {\n this.fitMeasurementPending = true;\n }\n }\n\n @Watch('loadingMore')\n handleLoadingMoreChange(loading: boolean): void {\n if (loading) this.incrementalWindowActive = true;\n if (!this.grouped) this.loadController.loadingChanged(loading);\n }\n\n @Watch('loadMoreError')\n handleLoadMoreErrorChange(error: string | undefined): void {\n if (error?.trim()) this.incrementalWindowActive = true;\n if (!this.grouped) this.loadController.errorChanged(error);\n }\n\n @Watch('hasMore')\n handleHasMoreChange(hasMore: boolean, hadMore: boolean): void {\n if (hasMore) this.incrementalWindowActive = true;\n if (!this.grouped) this.loadController.hasMoreChanged(hasMore, hadMore);\n }\n\n @Watch('columnCustomizer')\n @Watch('captionVisibility')\n handleColumnCustomizerAvailability(): void {\n if (!this.showsColumnCustomizer) {\n this.closeColumnCustomizer();\n this.columnCustomizerSurfaceOpen = false;\n }\n }\n\n @Watch('dataModeSwitcher')\n @Watch('captionVisibility')\n handleDataModeSwitcherAvailability(): void {\n if (!this.showsDataModeSwitcher) {\n this.closeDataModeSwitcher();\n this.dataModeSwitcherSurfaceOpen = false;\n }\n }\n\n @Watch('chromeLoading')\n handleChromeLoadingChange(loading: boolean): void {\n if (!loading) return;\n this.closeColumnCustomizer();\n this.closeDataModeSwitcher();\n this.columnCustomizerSurfaceOpen = false;\n this.dataModeSwitcherSurfaceOpen = false;\n }\n\n private get grouped(): boolean {\n return this.grouping !== null;\n }\n\n private get hasIncrementalState(): boolean {\n return (\n this.dataMode === 'infinite' &&\n (this.hasMore || this.loadingMore || !!this.loadMoreError?.trim())\n );\n }\n\n private get selectable(): boolean {\n return this.selectionMode === 'multiple';\n }\n\n private get visibleColumns(): TableColumn[] {\n const cached = this.visibleColumnsCache;\n if (\n cached &&\n cached.columns === this.columns &&\n cached.columnCustomizer === this.columnCustomizer &&\n cached.hiddenFieldIds === this.hiddenFieldIds &&\n cached.fieldOrder === this.fieldOrder &&\n cached.pinnedFieldIds === this.pinnedFieldIds\n ) {\n return cached.value;\n }\n const value = resolveTableVisibleColumns(this.columns, {\n columnCustomizer: this.columnCustomizer,\n hiddenFieldIds: this.hiddenFieldIds,\n fieldOrder: this.fieldOrder,\n pinnedFieldIds: this.pinnedFieldIds,\n });\n this.visibleColumnsCache = {\n columns: this.columns,\n columnCustomizer: this.columnCustomizer,\n hiddenFieldIds: this.hiddenFieldIds,\n fieldOrder: this.fieldOrder,\n pinnedFieldIds: this.pinnedFieldIds,\n value,\n };\n return value;\n }\n\n private get showsColumnCustomizer(): boolean {\n return this.columnCustomizer && this.captionVisibility === 'visible';\n }\n\n private get showsDataModeSwitcher(): boolean {\n return this.dataModeSwitcher && this.captionVisibility === 'visible';\n }\n\n private get showsCaptionTrailing(): boolean {\n return (\n this.showsDataModeSwitcher ||\n (this.showsColumnCustomizer && !this.hideColumnCustomizerTrigger)\n );\n }\n\n private get documentStickyHeader(): boolean {\n return this.stickyHeader && !this.containedScroll && this.responsiveLayout !== 'cards';\n }\n\n private get fixedHeight(): boolean {\n return !this.fitViewport && this.resolvedHeight !== undefined;\n }\n\n private get boundedComposition(): boolean {\n return this.fitViewport || this.fixedHeight;\n }\n\n private get containedScroll(): boolean {\n return this.boundedComposition || this.resolvedMaxHeight !== undefined;\n }\n\n private connectFitObserver(): void {\n if (typeof ResizeObserver === 'undefined') return;\n if (this.fitObservedViewport === this.viewportEl && this.fitObservedTable === this.tableEl) {\n return;\n }\n this.disconnectFitObserver();\n this.fitObservedViewport = this.viewportEl;\n this.fitObservedTable = this.tableEl;\n this.fitResizeObserver = new ResizeObserver(this.syncFitPageSize);\n if (this.fitObservedViewport) this.fitResizeObserver.observe(this.fitObservedViewport);\n if (this.fitObservedTable) this.fitResizeObserver.observe(this.fitObservedTable);\n }\n\n private disconnectFitObserver(): void {\n this.fitResizeObserver?.disconnect();\n this.fitResizeObserver = null;\n this.fitObservedViewport = null;\n this.fitObservedTable = null;\n }\n\n private connectCaptionCompactObserver(): void {\n if (this.captionCompactDisconnect) return;\n this.captionCompactDisconnect = observeTableCaptionCompact(this.el, compact => {\n if (this.captionCompact !== compact) this.captionCompact = compact;\n });\n }\n\n private disconnectCaptionCompactObserver(): void {\n this.captionCompactDisconnect?.();\n this.captionCompactDisconnect = undefined;\n }\n\n private readonly syncFitPageSize = (): void => {\n const pagination = this.dataMode === 'pagination' ? this.pagination : null;\n if (\n !pagination?.fitToPage ||\n pagination.fitToPageInactive ||\n !this.containedScroll ||\n !this.viewportEl ||\n !this.tableEl\n ) {\n if (this.fitPageSize !== undefined) this.fitPageSize = undefined;\n this.fitMeasurementPending = false;\n return;\n }\n const header = this.tableEl.querySelector<HTMLElement>('.ds-table__head');\n const item = this.tableEl.querySelector<HTMLElement>(\n this.grouped ? '.ds-table__group-row' : '.ds-table__body .ds-table__row'\n );\n const itemBlockSize =\n item?.getBoundingClientRect().height ||\n resolveCssLengthPx(\n 'var(--ds-table-row-min-block-size, var(--dimension-size-500))',\n 0,\n this.rootEl ?? this.el\n );\n const next = resolveTableFitPageSize({\n viewportBlockSize: this.viewportEl.clientHeight,\n headerBlockSize: header?.getBoundingClientRect().height ?? 0,\n itemBlockSize,\n });\n if (next !== this.fitPageSize) this.fitPageSize = next;\n if (!this.fitMeasurementPending || pagination.pageSizeMode !== 'fit' || next === undefined) {\n return;\n }\n this.fitMeasurementPending = false;\n if (next === pagination.pageSize && pagination.pageIndex === 0) return;\n this.dsPaginationChange.emit({\n pageIndex: 0,\n pageSize: next,\n pageSizeMode: 'fit',\n totalItems: pagination.totalItems,\n pageSizeOptions: pagination.pageSizeOptions ?? [25, 50, 100, 200],\n fitToPage: true,\n fitToPageInactive: false,\n fitPageSize: next,\n fitPageSizeLabel: pagination.fitPageSizeLabel ?? 'Fit to page',\n fitPageSizeTriggerLabel: pagination.fitPageSizeTriggerLabel ?? 'Fit',\n itemLabel: pagination.itemLabel ?? 'items',\n pageSizeLabel: pagination.pageSizeLabel ?? 'Items',\n ariaLabel: pagination.ariaLabel ?? `${this.caption} pagination`,\n showFirstLastButtons: pagination.showFirstLastButtons ?? false,\n previousPageIndex: pagination.pageIndex,\n previousPageSize: pagination.pageSize,\n previousPageSizeMode: 'fit',\n reason: 'fit',\n });\n };\n\n @Listen('keydown', { target: 'window' })\n handleWindowPaginationKeyDown(event: KeyboardEvent): void {\n this.handlePaginationKeyDown(event, true);\n }\n\n private handlePaginationKeyDown(event: KeyboardEvent, fromWindow = false): void {\n const pagination = this.dataMode === 'pagination' ? this.pagination : null;\n if (\n !pagination ||\n this.loading ||\n event.defaultPrevented ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey ||\n event.shiftKey ||\n (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')\n ) {\n return;\n }\n\n if (paginationShortcutBlockedByPath(event.composedPath())) return;\n\n if (fromWindow) {\n const origin = event.composedPath()[0];\n const originNode = origin instanceof Text ? origin.parentNode : origin;\n if (\n !shouldHandleContainingPagePaginationShortcut({\n origin: originNode,\n table: this.el,\n eventPath: event.composedPath(),\n })\n ) {\n return;\n }\n }\n\n const state = resolvePaginationState(pagination);\n const pageIndex = event.key === 'ArrowLeft' ? state.pageIndex - 1 : state.pageIndex + 1;\n if (pageIndex < 0 || pageIndex >= state.totalPages) return;\n\n event.preventDefault();\n this.dsPaginationChange.emit({\n pageIndex,\n pageSize: state.pageSize,\n pageSizeMode: state.pageSizeMode,\n totalItems: state.totalItems,\n pageSizeOptions: state.pageSizeOptions,\n fitToPage: pagination.fitToPage ?? false,\n fitToPageInactive: pagination.fitToPageInactive ?? false,\n fitPageSize: this.fitPageSize ?? pagination.fitPageSize,\n fitPageSizeLabel: pagination.fitPageSizeLabel ?? 'Fit to page',\n fitPageSizeTriggerLabel: pagination.fitPageSizeTriggerLabel ?? 'Fit',\n itemLabel: pagination.itemLabel ?? 'items',\n pageSizeLabel: pagination.pageSizeLabel ?? 'Items',\n ariaLabel: pagination.ariaLabel ?? `${this.caption} pagination`,\n showFirstLastButtons: pagination.showFirstLastButtons ?? false,\n previousPageIndex: state.pageIndex,\n previousPageSize: state.pageSize,\n previousPageSizeMode: state.pageSizeMode,\n reason: 'page',\n });\n }\n\n private currentLoadedRows(): TableRow[] {\n return tableRows(this.rows, this.groups, this.grouped);\n }\n\n private currentLoadedRowCount(): number {\n return this.grouped\n ? this.groups.reduce((count, group) => count + group.rows.length, 0)\n : this.rows.length;\n }\n\n private createRenderModel(): TableRenderModel {\n const columns = this.visibleColumns;\n const grouped = this.grouped;\n const virtualCounts = this.dataMode === 'virtual';\n const cached = this.renderModelCache;\n if (\n cached &&\n cached.columns === columns &&\n cached.rows === this.rows &&\n cached.groups === this.groups &&\n cached.grouped === grouped &&\n cached.selectionMode === this.selectionMode &&\n cached.selectedRowIds === this.selectedRowIds &&\n cached.collapsedGroupIds === this.collapsedGroupIds &&\n cached.virtualCounts === virtualCounts\n ) {\n return cached.value;\n }\n const value = createTableRenderModel({\n columns,\n rows: this.rows,\n groups: this.groups,\n grouped,\n selectionMode: this.selectionMode,\n selectedRowIds: this.selectedRowIds,\n collapsedGroupIds: this.collapsedGroupIds,\n groupCountPresentation: virtualCounts ? 'total' : 'loaded-progress',\n });\n this.renderModelCache = {\n columns,\n rows: this.rows,\n groups: this.groups,\n grouped,\n selectionMode: this.selectionMode,\n selectedRowIds: this.selectedRowIds,\n collapsedGroupIds: this.collapsedGroupIds,\n virtualCounts,\n value,\n };\n return value;\n }\n\n private get highlightMatcher(): TableHighlightMatcher {\n if (this.highlightMatcherCache?.terms === this.highlightTerms) {\n return this.highlightMatcherCache.value;\n }\n const value = createTableHighlightMatcher(this.highlightTerms);\n this.highlightMatcherCache = { terms: this.highlightTerms, value };\n return value;\n }\n\n private get rowWindowingEnabled(): boolean {\n if (!this.hasVirtualViewportBounds) return false;\n if (this.dataMode === 'virtual') return true;\n return !this.grouped && this.currentLoadedRowCount() > TABLE_NON_VIRTUAL_WINDOW_THRESHOLD;\n }\n\n private get virtualViewportMissing(): boolean {\n return this.dataMode === 'virtual' && !this.hasVirtualViewportBounds;\n }\n\n private get hasVirtualViewportBounds(): boolean {\n return this.containedScroll && this.estimateVirtualViewportSize() > 0;\n }\n\n private estimateVirtualViewportSize(): number {\n const measured =\n this.virtualController.currentViewportSize() || this.viewportEl?.clientHeight || 0;\n const chrome =\n (this.captionVisibility === 'visible' ? 48 : 0) + (this.hasResultFooter ? 48 : 0);\n if (this.fixedHeight && this.resolvedHeight) {\n if (measured > 0) return measured;\n return Math.max(\n 0,\n resolveCssLengthPx(this.resolvedHeight, 0, this.rootEl ?? this.el) - chrome\n );\n }\n if (this.fitViewport) return Math.max(0, measured);\n if (this.resolvedMaxHeight) {\n if (this.resolvedMaxHeight.trim().toLowerCase() === 'none') return 0;\n return resolveCssLengthPx(this.resolvedMaxHeight, 0, this.rootEl ?? this.el);\n }\n return 0;\n }\n\n private virtualPinnedRowIds(): Set<string> {\n const pinned = new Set<string>();\n if (this.focusedRowId) pinned.add(this.focusedRowId);\n if (this.actionMenu?.rowId) pinned.add(this.actionMenu.rowId);\n return pinned;\n }\n\n private findRenderedRow(rowId: string): HTMLElement | null {\n return (\n [...this.el.querySelectorAll<HTMLElement>('[data-row-id]')].find(\n candidate => candidate.dataset.rowId === rowId\n ) ?? null\n );\n }\n\n private revealRenderedRow(row: HTMLElement): void {\n if (!this.containedScroll || !this.viewportEl) {\n row.scrollIntoView({ block: 'nearest', inline: 'nearest' });\n return;\n }\n\n const viewportRect = this.viewportEl.getBoundingClientRect();\n const rowRect = row.getBoundingClientRect();\n let visibleStart = viewportRect.top;\n if (this.stickyHeader) {\n visibleStart = Math.max(\n visibleStart,\n this.tableEl?.tHead?.getBoundingClientRect().bottom ?? visibleStart\n );\n }\n const groupRow = row.closest('tbody')?.querySelector<HTMLElement>('.ds-table__group-row');\n if (groupRow && getComputedStyle(groupRow).position === 'sticky') {\n visibleStart = Math.max(visibleStart, groupRow.getBoundingClientRect().bottom);\n }\n\n if (rowRect.top < visibleStart) {\n this.viewportEl.scrollTop += rowRect.top - visibleStart;\n } else if (rowRect.bottom > viewportRect.bottom) {\n this.viewportEl.scrollTop += rowRect.bottom - viewportRect.bottom;\n }\n }\n\n private nextAnimationFrame(): Promise<void> {\n return new Promise(resolve => requestAnimationFrame(() => resolve()));\n }\n\n private syncVirtualItems(model: TableRenderModel): void {\n if (!this.rowWindowingEnabled) {\n this.virtualItems = [];\n this.virtualItemsCache = null;\n this.bodyRenderer.resetVirtualState();\n return;\n }\n const columns = this.visibleColumns;\n const cached = this.virtualItemsCache;\n if (\n cached &&\n cached.columns === columns &&\n cached.rows === this.rows &&\n cached.groups === this.groups &&\n cached.grouped === model.grouped &&\n cached.collapsedGroupIds === this.collapsedGroupIds\n ) {\n return;\n }\n this.virtualItems = flattenTableVirtualItems({\n grouped: model.grouped,\n rows: this.rows,\n groups: this.groups,\n collapsedGroupIds: this.collapsedGroupIds,\n columns,\n });\n if (this.responsiveLayout === 'cards') {\n this.virtualItems = this.virtualItems.map(item => ({ ...item, variableSize: true }));\n }\n this.virtualItemsCache = {\n columns,\n rows: this.rows,\n groups: this.groups,\n grouped: model.grouped,\n collapsedGroupIds: this.collapsedGroupIds,\n };\n }\n\n private shouldWindowRows(model: TableRenderModel): boolean {\n return (\n this.rowWindowingEnabled && (model.hasData || (model.grouped && model.groups.length > 0))\n );\n }\n\n private get resolvedMaxHeight(): string | undefined {\n if (this.maxHeight == null || this.maxHeight === '') return undefined;\n return typeof this.maxHeight === 'number' ? `${Math.max(0, this.maxHeight)}px` : this.maxHeight;\n }\n\n private get resolvedHeight(): string | undefined {\n if (this.height == null || this.height === '') return undefined;\n return typeof this.height === 'number' ? `${Math.max(0, this.height)}px` : this.height;\n }\n\n private warnModelIssues(): void {\n const issues = tableModelIssues(this.columns, this.rows, this.groups, this.grouped);\n const visibleColumns = this.visibleColumns;\n if (!this.caption?.trim()) issues.unshift('A non-empty caption is required.');\n if (this.grouping) {\n const groupingColumn = this.columns.find(column => column.id === this.grouping!.fieldId);\n if (!groupingColumn) {\n issues.push(`Grouping references unknown column id: ${this.grouping.fieldId}`);\n } else if (!visibleColumns.some(column => column.id === groupingColumn.id)) {\n issues.push(`Grouping references hidden column id: ${this.grouping.fieldId}`);\n }\n }\n if (this.sort) {\n const sortColumn = this.columns.find(\n column =>\n column.id === this.sort!.fieldId ||\n column.segments?.some(segment => segment.sortKey === this.sort!.fieldId)\n );\n if (!sortColumn) {\n issues.push(`Sorting references unknown column id: ${this.sort.fieldId}`);\n } else if (!visibleColumns.some(column => column.id === sortColumn.id)) {\n issues.push(`Sorting references hidden column id: ${this.sort.fieldId}`);\n }\n }\n if (\n this.dataMode === 'virtual' &&\n Number.isFinite(this.totalCount) &&\n Math.max(0, Math.trunc(this.totalCount!)) !== this.currentLoadedRowCount()\n ) {\n issues.push('Virtual mode requires totalCount to match the complete supplied row count.');\n }\n const stickyStart = visibleColumns.filter(column => column.sticky === 'start');\n const stickyEnd = visibleColumns.filter(column => column.sticky === 'end');\n issues.push(...this.structure.merges.issues);\n if (this.interactionMode === 'grid' && this.grouped)\n issues.push('Grid interaction requires ungrouped rows.');\n for (const column of [...stickyStart, ...stickyEnd]) {\n if (!tableColumnSize(column))\n issues.push(`Sticky column ${column.id} requires an explicit size.`);\n }\n const message = issues.join(' ');\n if (!message) {\n this.previousModelWarning = '';\n return;\n }\n if (message === this.previousModelWarning) return;\n this.previousModelWarning = message;\n console.warn(`[ds-table] ${message}`);\n }\n\n private scheduleModelIssueWarning(): void {\n if (this.modelWarningQueued) return;\n this.modelWarningQueued = true;\n queueMicrotask(() => {\n this.modelWarningQueued = false;\n this.warnModelIssues();\n });\n }\n\n private scheduleInitialModelIssueWarning(): void {\n if (this.initialModelWarningFrame !== undefined) {\n cancelAnimationFrame(this.initialModelWarningFrame);\n }\n this.initialModelWarningFrame = requestAnimationFrame(() => {\n this.initialModelWarningFrame = undefined;\n this.warnModelIssues();\n });\n }\n\n private emitSort(column: TableColumn, sortKey = column.id): void {\n if (!column.sortable) return;\n this.dsSortChange.emit({ sort: nextDataSortState(this.sort, sortKey) });\n }\n\n private sortButtonLabel(column: TableColumn, sortKey = column.id, label = column.label): string {\n if (this.sort?.fieldId !== sortKey) return `Sort ${label} ascending`;\n if (this.sort.direction === 'asc') return `Sort ${label} descending. Currently ascending.`;\n return `Sort ${label} ascending. Currently descending.`;\n }\n\n private emitRowSelection(row: TableRow): void {\n const next = toggleTableRowSelection(this.selectedRowIds, row);\n const selected = next.includes(row.id);\n this.dsSelectionChange.emit({\n selectedRowIds: next,\n scope: 'row',\n changedRowId: row.id,\n selected,\n });\n }\n\n private emitAllSelection(): void {\n const loadedRows = this.currentLoadedRows();\n const selection = deriveTableSelectionState(loadedRows, this.selectedRowIds);\n this.dsSelectionChange.emit({\n selectedRowIds: toggleAllLoadedTableRows(this.selectedRowIds, loadedRows),\n scope: 'all-loaded',\n selected: !selection.allSelected,\n });\n }\n\n private emitGroupSelection(group: DataGroup): void {\n const state = deriveTableSelectionState(group.rows, this.selectedRowIds);\n this.dsSelectionChange.emit({\n selectedRowIds: toggleTableGroupSelection(this.selectedRowIds, group.rows),\n scope: 'group',\n groupId: group.id,\n selected: !state.allSelected,\n });\n }\n\n private rowEventOwnsActivation(event: Event): boolean {\n const currentTarget = event.currentTarget;\n return !event.composedPath().some(target => {\n if (!(target instanceof HTMLElement) || target === currentTarget) return false;\n return target.matches(\n 'button, a, input, select, textarea, [role=\"button\"], [role=\"checkbox\"], [popover], ds-button-unfilled, ds-menu'\n );\n });\n }\n\n private groupRowEventOwnsCollapse(event: Event): boolean {\n const currentTarget = event.currentTarget;\n return !event.composedPath().some(target => {\n if (!(target instanceof HTMLElement) || target === currentTarget) return false;\n return target.matches(\n 'input, select, textarea, [role=\"checkbox\"], .ds-table__selection-control, .ds-table__group-selection, ds-tooltip'\n );\n });\n }\n\n private handleGroupRowClick(group: DataGroup, event: Event): void {\n if (!this.groupRowEventOwnsCollapse(event)) return;\n this.emitGroupCollapse(group);\n }\n\n private emitRowActivation(row: TableRow, event: Event): void {\n if (this.gridEnabled || row.disabled || !this.rowEventOwnsActivation(event)) return;\n if (this.selectionMode === 'multiple' && this.selectedRowIds.length > 0) {\n if (row.selectable !== false) this.emitRowSelection(row);\n return;\n }\n if (row.interactive) this.dsRowActivate.emit({ rowId: row.id });\n }\n\n private syncActionMenu(): void {\n if (!this.actionMenu) return;\n const row = tableRows(this.rows, this.groups, this.grouped).find(\n candidate => candidate.id === this.actionMenu?.rowId\n );\n const value = row && !row.disabled ? row.cells[this.actionMenu.columnId] : undefined;\n if (!isTableCellAction(value) || !isRenderableTableActionMenu(value)) {\n this.actionMenu = null;\n }\n }\n\n private closeActionMenu(): void {\n this.actionMenu = null;\n }\n\n private handleActionMenuSelect(item: MenuItemData): void {\n const open = this.actionMenu;\n const actionId = item.value;\n if (!open || !actionId || item.isInactive) return;\n this.dsCellAction.emit({\n actionId,\n rowId: open.rowId,\n columnId: open.columnId,\n });\n const triggerId = tableActionTriggerId(this.actionMenuElementId, open.rowId, open.columnId);\n this.closeActionMenu();\n requestAnimationFrame(() => {\n const trigger = this.el.querySelector<HTMLElement & { setFocus?: () => void }>(\n `#${CSS.escape(triggerId)}`\n );\n trigger?.setFocus?.();\n });\n }\n\n private toggleActionMenu(row: TableRow, column: TableColumn, event: MouseEvent): void {\n if (row.disabled) return;\n const open = this.actionMenu?.rowId === row.id && this.actionMenu?.columnId === column.id;\n this.actionMenuInitialFocusVisible = event.detail === 0;\n this.actionMenu = open ? null : { rowId: row.id, columnId: column.id };\n if (this.actionMenu) this.actionMenuSurface = this.actionMenu;\n }\n\n private renderOverflowActionMenu() {\n const open = this.actionMenu ?? this.actionMenuSurface;\n const row = open\n ? tableRows(this.rows, this.groups, this.grouped).find(\n candidate => candidate.id === open.rowId\n )\n : undefined;\n const value = row && open ? row.cells[open.columnId] : undefined;\n const menu = isTableCellAction(value) && isRenderableTableActionMenu(value) ? value : undefined;\n const sections = menu ? tableActionMenuSections(menu.items) : [];\n const triggerId = open\n ? tableActionTriggerId(this.actionMenuElementId, open.rowId, open.columnId)\n : undefined;\n\n return (\n <ds-menu\n id={this.actionMenuElementId}\n open={Boolean(this.actionMenu && sections.length)}\n anchorId={triggerId}\n align=\"end\"\n side=\"bottom\"\n menuLabel={menu?.ariaLabel ?? 'More actions'}\n initialFocusVisible={this.actionMenuInitialFocusVisible}\n sections={sections}\n onDsClose={() => this.closeActionMenu()}\n onDsAfterClose={() => {\n if (!this.actionMenu) this.actionMenuSurface = null;\n }}\n onDsSelect={event => this.handleActionMenuSelect(event.detail)}\n />\n );\n }\n\n private connectTruncateTooltip(): void {\n if (this.truncateTooltipBound) return;\n this.el.addEventListener('pointerover', this.onTruncatePointerOver);\n this.el.addEventListener('pointerout', this.onTruncatePointerOut);\n this.el.addEventListener('focusin', this.onTruncateFocusIn);\n this.el.addEventListener('focusout', this.onTruncateFocusOut);\n this.truncateTooltipBound = true;\n }\n\n private disconnectTruncateTooltip(): void {\n if (!this.truncateTooltipBound) return;\n this.el.removeEventListener('pointerover', this.onTruncatePointerOver);\n this.el.removeEventListener('pointerout', this.onTruncatePointerOut);\n this.el.removeEventListener('focusin', this.onTruncateFocusIn);\n this.el.removeEventListener('focusout', this.onTruncateFocusOut);\n this.truncateTooltipBound = false;\n this.dismissTruncateTooltip();\n }\n\n private onTruncatePointerOver = (event: PointerEvent) => {\n if (event.pointerType === 'touch') return;\n const track = resolveTableTruncateTrack(event.target);\n if (!track) return;\n this.presentOrDismissTruncateTrack(track);\n };\n\n private onTruncatePointerOut = (event: PointerEvent) => {\n if (event.pointerType === 'touch') return;\n if (resolveTableTruncateTrack(event.relatedTarget)) return;\n const next = event.relatedTarget;\n const cell = this.truncateAnchor?.closest('.ds-table__cell');\n if (cell && next instanceof Node && cell.contains(next)) return;\n this.dismissTruncateTooltip();\n };\n\n private onTruncateFocusIn = (event: FocusEvent) => {\n const track = resolveTableTruncateTrack(event.target);\n if (!track) {\n this.dismissTruncateTooltip();\n return;\n }\n this.presentOrDismissTruncateTrack(track);\n };\n\n private onTruncateFocusOut = (event: FocusEvent) => {\n if (resolveTableTruncateTrack(event.relatedTarget)) return;\n this.dismissTruncateTooltip();\n };\n\n private presentOrDismissTruncateTrack(track: HTMLElement): void {\n if (!isElementTruncated(track)) {\n this.dismissTruncateTooltip();\n return;\n }\n const label = tableTruncateLabel(track);\n if (!label) {\n this.dismissTruncateTooltip();\n return;\n }\n this.truncateAnchor = track;\n this.truncateTooltipLabel = label;\n void this.truncateTooltipEl?.presentFrom(track, label);\n }\n\n private dismissTruncateTooltip(): void {\n this.truncateAnchor = null;\n if (!this.truncateTooltipLabel && !this.truncateTooltipEl) return;\n this.truncateTooltipLabel = '';\n void this.truncateTooltipEl?.dismiss();\n }\n\n private renderTruncateTooltip() {\n return (\n <ds-tooltip\n ref={element => {\n this.truncateTooltipEl = element;\n }}\n label={this.truncateTooltipLabel}\n describedBy={false}\n wrapLabel={true}\n size=\"sm\"\n delay={0}\n side=\"top\"\n />\n );\n }\n\n private handleRowKeydown(row: TableRow, event: KeyboardEvent): void {\n if (event.target !== event.currentTarget || (event.key !== 'Enter' && event.key !== ' '))\n return;\n event.preventDefault();\n this.emitRowActivation(row, event);\n }\n\n private onVirtualFocusIn = (event: FocusEvent): void => {\n const target = event.target;\n if (!(target instanceof HTMLElement)) return;\n const row = target.closest('[data-row-id]');\n const id = row?.getAttribute('data-row-id') ?? null;\n if (id === this.focusedRowId) return;\n this.focusedRowId = id;\n if (this.rowWindowingEnabled) this.virtualController.schedule();\n };\n\n private renderSelectionControl(\n label: string,\n checked: boolean,\n indeterminate: boolean,\n disabled: boolean,\n onActivate: () => void\n ) {\n return (\n <button\n class=\"ds-table__selection-control ds-focus-ring ds-interaction-fill__content\"\n type=\"button\"\n role=\"checkbox\"\n aria-label={label}\n aria-checked={indeterminate ? 'mixed' : String(checked)}\n disabled={disabled}\n onClick={onActivate}\n >\n <ds-checkbox\n label=\"\"\n size=\"md\"\n checked={checked}\n indeterminate={indeterminate}\n presentation={true}\n />\n </button>\n );\n }\n\n private renderStickyEdge(sticky: TableColumn['sticky'], shadow = true) {\n if (!sticky) return null;\n return (\n <span\n class={{\n 'ds-table__sticky-edge': true,\n [`ds-table__sticky-edge--${sticky}`]: true,\n 'ds-table__sticky-edge--internal': !shadow,\n }}\n aria-hidden=\"true\"\n />\n );\n }\n\n private renderCollapseAllButton() {\n return (\n <ds-button-unfilled\n class=\"ds-table__collapse-all\"\n variant=\"icon\"\n icon=\"ChevronDownUp\"\n size=\"xs\"\n style={{ width: 'var(--dimension-size-300)' }}\n aria-label=\"Collapse all groups\"\n hasBorder={false}\n activeFill={false}\n pressScale={false}\n onDsClick={event => {\n event.stopPropagation();\n this.emitAllGroupsCollapse();\n }}\n />\n );\n }\n\n private renderFloatingCollapseAll(model: TableRenderModel) {\n if (model.collapseAllHost?.mode !== 'floating') return null;\n return (\n <span\n class=\"ds-table__collapse-all-overlay\"\n ref={element => {\n this.collapseAllOverlayEl = element ?? null;\n }}\n >\n <span class=\"ds-table__collapse-all-surface ds-control-elevation ds-control-elevation--md\">\n {this.renderCollapseAllButton()}\n </span>\n </span>\n );\n }\n\n private renderColumnHeader(\n column: TableColumn,\n model: TableRenderModel,\n interactive = true,\n presentational = false,\n headerLane?: { start: number; spanning: boolean }\n ) {\n const groupedColumn = this.grouping?.fieldId === column.id;\n const previousColumn = headerLane\n ? this.visibleColumns[this.visibleColumns.indexOf(column) - 1]\n : undefined;\n const segments = column.segments?.length\n ? column.segments\n : [{ label: column.label, sortKey: column.id }];\n const activeMemberSegment = segments.find(segment => segment.sortKey === this.sort?.fieldId);\n const activeMemberSort = !!activeMemberSegment;\n const activeSort = activeMemberSort;\n const align = column.align ?? 'start';\n const direction = activeMemberSort ? this.sort!.direction : undefined;\n const memberAriaSort = activeMemberSort\n ? this.sort!.direction === 'asc'\n ? 'ascending'\n : 'descending'\n : undefined;\n\n const help = column.help?.trim();\n const labels = (\n <span\n class=\"ds-table__header-labels\"\n tabIndex={interactive && help && !column.sortable ? 0 : undefined}\n data-header-help={help ? '' : undefined}\n >\n {segments.map((segment, index) => {\n const segmentActive = activeMemberSegment?.sortKey === segment.sortKey;\n const segmentInteractive = interactive && !!column.sortable;\n const label = (\n <ds-text\n class=\"ds-table__header-label-box ds-control-label-box\"\n as=\"span\"\n variant=\"text-caption\"\n emphasis={segmentActive}\n color=\"inherit\"\n decoration={help ? 'dotted-underline' : undefined}\n lineTruncation={1}\n >\n {segment.label}\n </ds-text>\n );\n const control = segmentInteractive ? (\n <button\n class=\"ds-table__header-label ds-table__header-label--interactive ds-focus-ring\"\n type=\"button\"\n aria-label={this.sortButtonLabel(\n column,\n segment.sortKey,\n segment.dataLabel?.trim() || segment.label\n )}\n data-sort-control=\"label\"\n data-sort-key={segment.sortKey}\n data-sort-active={segmentActive ? 'true' : undefined}\n onClick={() => this.emitSort(column, segment.sortKey)}\n >\n {label}\n </button>\n ) : (\n <span class=\"ds-table__header-label ds-table__header-static\">{label}</span>\n );\n\n return (\n <span class=\"ds-table__header-segment\" key={segment.sortKey}>\n {control}\n {index < segments.length - 1 && (\n <ds-text\n class=\"ds-table__header-separator\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"tertiary\"\n aria-hidden=\"true\"\n >\n {segment.separator ?? '/'}\n </ds-text>\n )}\n </span>\n );\n })}\n </span>\n );\n const labelControl =\n interactive && help ? (\n <ds-tooltip label={help} side=\"top\" size=\"sm\" wrapLabel={true}>\n {labels}\n </ds-tooltip>\n ) : (\n labels\n );\n const sortControl = interactive ? (\n <span class=\"ds-table__sort-slot\">\n {activeSort && (\n <ds-button-unfilled\n class=\"ds-table__sort-direction\"\n variant=\"icon\"\n icon={direction === 'asc' ? 'ArrowUp' : 'ArrowDown'}\n size=\"xs\"\n aria-label={this.sortButtonLabel(\n column,\n activeMemberSegment?.sortKey ?? column.id,\n activeMemberSegment?.label ?? column.label\n )}\n hasBorder={false}\n activeFill={false}\n isActive={true}\n pressScale={false}\n data-sort-control=\"direction\"\n onDsClick={() => this.emitSort(column, activeMemberSegment?.sortKey ?? column.id)}\n />\n )}\n </span>\n ) : null;\n const collapseHost = model.collapseAllHost;\n const actionCollapseHost =\n collapseHost?.mode === 'action' && collapseHost.columnId === column.id;\n const blankActionCollapseHost =\n actionCollapseHost && !column.label.trim() && !column.segments?.length;\n const collapseControl =\n interactive && actionCollapseHost ? (\n <span class=\"ds-table__collapse-slot\">{this.renderCollapseAllButton()}</span>\n ) : null;\n\n return (\n <th\n key={column.id}\n id={presentational ? undefined : this.headerId(column.id)}\n headers={presentational ? undefined : this.columnBandId(column.id)}\n rowSpan={headerLane?.spanning ? 2 : undefined}\n style={{\n ...this.structure.pins.get(column.id)?.style,\n ...(headerLane\n ? {\n gridColumn: String(headerLane.start),\n gridRow: headerLane.spanning ? '1 / span 2' : '2',\n }\n : {}),\n }}\n class={{\n 'ds-table__header-cell': true,\n 'ds-table__header-cell--rowspan': !!headerLane?.spanning,\n 'ds-table__header-cell--after-rowspan':\n !!headerLane &&\n !headerLane.spanning &&\n !!previousColumn &&\n !previousColumn.group &&\n !this.structure.pins.has(previousColumn.id),\n [`ds-table__cell--align-${align}`]: true,\n 'ds-table__cell--sticky-start':\n column.sticky === 'start' && this.structure.pins.has(column.id),\n 'ds-table__cell--sticky-end':\n column.sticky === 'end' && this.structure.pins.has(column.id),\n 'ds-table__header-cell--collapse-all': actionCollapseHost,\n }}\n scope={presentational ? undefined : 'col'}\n aria-sort={presentational ? undefined : memberAriaSort}\n data-column-id={column.id}\n data-grouped={groupedColumn ? 'true' : undefined}\n data-sort-active={activeSort ? 'true' : undefined}\n >\n {!column.label.trim() && column.accessibleLabel?.trim() && (\n <span class=\"ds-visually-hidden\">{column.accessibleLabel}</span>\n )}\n {blankActionCollapseHost ? (\n <span class=\"ds-table__header-content ds-table__header-content--collapse-all\">\n {collapseControl}\n </span>\n ) : (\n <span class=\"ds-table__header-content\">\n {align === 'end' && sortControl}\n {align === 'center' && (\n <span\n class=\"ds-table__sort-slot ds-table__sort-slot--balance\"\n data-sort-balance=\"true\"\n aria-hidden=\"true\"\n />\n )}\n {labelControl}\n {align !== 'end' && sortControl}\n {collapseControl}\n </span>\n )}\n {this.renderStickyEdge(\n this.structure.pins.has(column.id) ? column.sticky : undefined,\n !!this.structure.pins.get(column.id)?.edge\n )}\n </th>\n );\n }\n\n private renderColgroup(model: TableRenderModel) {\n const beforeSpacer =\n model.elasticSpacerIndex == null\n ? this.visibleColumns\n : this.visibleColumns.slice(0, model.elasticSpacerIndex);\n const afterSpacer =\n model.elasticSpacerIndex == null ? [] : this.visibleColumns.slice(model.elasticSpacerIndex);\n const renderColumn = (column: TableColumn) => {\n const width = tableColumnSize(column);\n return (\n <col\n key={column.id}\n class={{ 'ds-table__action-column': column.kind === 'action' }}\n style={width ? { width } : undefined}\n />\n );\n };\n return (\n <colgroup>\n {model.selectable && <col class=\"ds-table__selection-column\" />}\n {beforeSpacer.map(renderColumn)}\n {model.elasticSpacerIndex != null && <col class=\"ds-table__elastic-spacer-column\" />}\n {afterSpacer.map(renderColumn)}\n </colgroup>\n );\n }\n\n private renderHeader(\n model: TableRenderModel,\n interactive = true,\n presentational = false,\n ariaRowIndex?: number\n ) {\n const selection = model.selection;\n const bands = this.structure.bands;\n const multirow = bands.length > 0;\n const controls =\n interactive &&\n !(\n this.responsiveLayout === 'cards' &&\n this.narrowViewport &&\n this.interactionMode !== 'grid' &&\n !this.cellSpans.length\n );\n const selectionHeader = model.selectable && (\n <th\n class={{\n 'ds-table__header-cell': true,\n 'ds-table__selection-cell': true,\n 'ds-table__cell--sticky-start': true,\n 'ds-table__header-cell--rowspan': multirow,\n }}\n rowSpan={multirow ? 2 : undefined}\n style={multirow ? { gridColumn: '1', gridRow: '1 / span 2' } : undefined}\n scope={presentational ? undefined : 'col'}\n >\n {controls ? (\n this.renderSelectionControl(\n selection.allSelected ? 'Deselect all loaded rows' : 'Select all loaded rows',\n selection.allSelected,\n selection.indeterminate,\n selection.selectableRowIds.length === 0,\n () => this.emitAllSelection()\n )\n ) : (\n <span class=\"ds-visually-hidden\">Select rows</span>\n )}\n {this.renderStickyEdge(\n 'start',\n ![...this.structure.pins.values()].some(pin => pin.edge === 'start')\n )}\n </th>\n );\n const lanes: (TableColumn | undefined)[] = [...this.visibleColumns];\n if (model.elasticSpacerIndex != null) lanes.splice(model.elasticSpacerIndex, 0, undefined);\n const renderLane = (column: TableColumn | undefined, index: number) => {\n const start = index + 1 + Number(model.selectable);\n if (column)\n return this.renderColumnHeader(\n column,\n model,\n controls,\n presentational,\n multirow ? { start, spanning: !column.group } : undefined\n );\n return (\n <th\n class=\"ds-table__header-cell ds-table__elastic-spacer-cell\"\n rowSpan={multirow ? 2 : undefined}\n style={multirow ? { gridColumn: String(start), gridRow: '1 / span 2' } : undefined}\n aria-hidden=\"true\"\n role=\"presentation\"\n data-elastic-spacer=\"true\"\n />\n );\n };\n return (\n <thead\n class={{\n 'ds-table__head': true,\n 'ds-table__head--multirow': multirow,\n 'ds-table__head--semantic-copy': !interactive,\n }}\n ref={element => {\n if (interactive) this.interactiveHeadEl = element ?? null;\n }}\n >\n {multirow && (\n <tr class=\"ds-table__header-row ds-table__super-header-row\" aria-rowindex={ariaRowIndex}>\n {selectionHeader}\n {bands.map(band =>\n !band.label ? (\n lanes.map((column, index) => {\n const start = index + 1 + Number(model.selectable);\n return start >= band.start && start < band.start + band.span\n ? renderLane(column, index)\n : null;\n })\n ) : (\n <th\n key={band.key}\n id={presentational ? undefined : `${this.actionMenuElementId}-${band.key}`}\n class={{\n 'ds-table__header-cell': true,\n 'ds-table__super-header-cell': true,\n 'ds-table__cell--sticky-start': band.sticky === 'start',\n 'ds-table__cell--sticky-end': band.sticky === 'end',\n }}\n scope={presentational || !band.label ? undefined : 'colgroup'}\n colSpan={band.span}\n style={{\n gridColumn: `${band.start} / span ${band.span}`,\n gridRow: '1',\n ...this.structure.pins.get(\n (band.sticky === 'end' ? band.lastColumnId : band.firstColumnId) ?? ''\n )?.style,\n }}\n aria-hidden={!band.label ? 'true' : undefined}\n >\n <span class=\"ds-table__header-content\">\n <span class=\"ds-table__header-labels\">\n <span class=\"ds-table__header-segment\">\n {/* eslint-disable-next-line compomo/prefer-direct-ds-text -- Match the normal header's structural label canvas and inner text inset. */}\n <span class=\"ds-table__header-label ds-table__header-static\">\n <ds-text\n class=\"ds-table__header-label-box ds-control-label-box\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"inherit\"\n lineTruncation={1}\n >\n {band.label}\n </ds-text>\n </span>\n </span>\n </span>\n </span>\n {this.renderStickyEdge(\n band.sticky,\n !!this.structure.pins.get(\n (band.sticky === 'end' ? band.firstColumnId : band.lastColumnId) ?? ''\n )?.edge\n )}\n </th>\n )\n )}\n </tr>\n )}\n <tr\n class=\"ds-table__header-row\"\n aria-rowindex={\n ariaRowIndex == null\n ? undefined\n : ariaRowIndex + Number(this.structure.bands.length > 0)\n }\n >\n {!multirow && selectionHeader}\n {lanes.map((column, index) => (!multirow || column?.group) && renderLane(column, index))}\n </tr>\n </thead>\n );\n }\n\n private renderRow(\n row: TableRow,\n model: TableRenderModel,\n ariaRowIndex?: number,\n variableVirtualSize = false,\n rowKey = row.id\n ) {\n return renderTableRowView({\n grid: this.gridEnabled,\n cellAttributes: this.editingEnabled\n ? (targetRow, column) => this.gridCellAttributes(targetRow, column)\n : undefined,\n renderEditor: this.editingEnabled\n ? (targetRow, column) => this.renderCellEditor(targetRow, column)\n : undefined,\n renderEditTrigger: this.editingEnabled\n ? (targetRow, column) => this.renderCellEditTrigger(targetRow, column)\n : undefined,\n pins: this.structure.pins,\n spans: this.structure.merges.cells.get(row.id),\n headerId: id => this.headerId(id),\n responsiveCards: this.responsiveLayout === 'cards',\n row,\n model,\n visibleColumns: this.visibleColumns,\n emptyCellLabel: this.emptyCellLabel,\n actionMenuElementId: this.actionMenuElementId,\n actionMenu: this.actionMenu,\n actionMenuSurface: this.actionMenuSurface,\n surfaceOpenRowId: this.surfaceOpenRowId,\n highlightMatcher: this.highlightMatcher,\n highlightFieldIds: this.highlightFieldIds,\n ariaRowIndex:\n ariaRowIndex == null ? undefined : ariaRowIndex + Number(this.structure.bands.length > 0),\n variableVirtualSize,\n intrinsicBlockSize:\n this.dataMode !== 'virtual'\n ? estimateTableRowBlockSize(row, this.visibleColumns)\n : undefined,\n rowKey,\n renderSelectionControl: (label, checked, indeterminate, disabled, onActivate) =>\n this.renderSelectionControl(label, checked, indeterminate, disabled, onActivate),\n renderStickyEdge: (sticky, shadow) => this.renderStickyEdge(sticky, shadow),\n onRowActivate: (targetRow, event) => this.emitRowActivation(targetRow, event),\n onRowKeyDown: (targetRow, event) => this.handleRowKeydown(targetRow, event),\n onRowSelection: targetRow => this.emitRowSelection(targetRow),\n onCellAction: detail => this.dsCellAction.emit(detail),\n onActionMenuToggle: (targetRow, column, event) =>\n this.toggleActionMenu(targetRow, column, event),\n });\n }\n\n private emitGroupCollapse(group: DataGroup) {\n const collapsedGroupIds = toggleTableGroupCollapsed(this.collapsedGroupIds, group.id);\n this.dsGroupCollapseChange.emit({\n scope: 'group',\n groupId: group.id,\n collapsed: collapsedGroupIds.includes(group.id),\n collapsedGroupIds,\n });\n }\n\n private emitAllGroupsCollapse() {\n const groupIds = this.groups.map(group => group.id);\n const collapsedGroupIds = nextTableGroupsCollapsed(this.collapsedGroupIds, groupIds);\n this.dsGroupCollapseChange.emit({\n scope: 'all',\n collapsed: collapsedGroupIds.length > 0,\n collapsedGroupIds,\n });\n }\n\n private renderGroupAccessory(\n item: TableRenderModel['groups'][number]['accessories'][number],\n index: number\n ) {\n const text = (\n <ds-text\n key={`acc-${index}`}\n class=\"ds-table__group-accessory\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"secondary\"\n decoration={item.help ? 'dotted-underline' : undefined}\n lineTruncation={1}\n >\n {item.text}\n </ds-text>\n );\n if (!item.help) return text;\n return (\n <ds-tooltip\n key={`acc-${index}`}\n label={item.help}\n side=\"bottom\"\n align=\"start\"\n size=\"sm\"\n wrapLabel={true}\n >\n <span class=\"ds-table__group-accessory-tip\" tabIndex={0}>\n {text}\n </span>\n </ds-tooltip>\n );\n }\n\n private renderGroupContent(groupModel: TableRenderModel['groups'][number]) {\n const {\n group,\n countLabel,\n collapsed: isCollapsed,\n labelColor,\n selection: groupSelection,\n accessories,\n hero,\n } = groupModel;\n const heroTracks = hero?.kind === 'score' && hero.tracks === 2 ? 2 : 1;\n const heroPresentation = TABLE_GROUP_HERO_SCORE_PRESENTATION[heroTracks];\n return (\n <span\n class={{\n 'ds-table__group-content': true,\n 'ds-table__group-content--multi': accessories.length > 0 || heroTracks === 2,\n 'ds-table__group-content--hero': Boolean(hero),\n 'ds-table__group-content--hero-two-track': Boolean(hero) && heroTracks === 2,\n }}\n onClick={event => this.handleGroupRowClick(group, event)}\n >\n {groupSelection && (\n <span class=\"ds-table__group-selection\">\n {this.renderSelectionControl(\n groupSelection.allSelected\n ? `Deselect loaded rows in ${group.label} group`\n : `Select loaded rows in ${group.label} group`,\n groupSelection.allSelected,\n groupSelection.indeterminate,\n groupSelection.selectableRowIds.length === 0,\n () => this.emitGroupSelection(group)\n )}\n </span>\n )}\n {hero?.kind === 'score' && (\n <span class=\"ds-table__group-hero\">\n <ds-score\n size={heroPresentation.size}\n variant={heroPresentation.variant}\n value={hero.value}\n level={hero.level}\n label={hero.label ?? 'Safety score'}\n isLoading={hero.isLoading === true}\n />\n </span>\n )}\n <span class=\"ds-table__group-copy\">\n <span class=\"ds-table__group-primary\">\n <ds-text\n class=\"ds-table__group-label\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis={true}\n color={labelColor}\n >\n {group.label}\n </ds-text>\n <span class=\"ds-visually-hidden\">{countLabel}</span>\n </span>\n {accessories.length > 0 && (\n <span class=\"ds-table__group-accessories\">\n {accessories.flatMap((item, index) => [\n ...(index > 0\n ? [\n <ds-text\n key={`sep-${index}`}\n class=\"ds-table__group-accessory-separator\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>,\n ]\n : []),\n this.renderGroupAccessory(item, index),\n ])}\n </span>\n )}\n </span>\n <span class=\"ds-table__group-trailing\">\n <ds-text\n class=\"ds-table__group-count\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n {groupModel.visibleCountText}\n </ds-text>\n <span class=\"ds-table__group-action\">\n <ds-button-unfilled\n class=\"ds-table__group-toggle\"\n variant=\"icon\"\n size=\"md\"\n isInset={true}\n insetDepth=\"double\"\n icon={isCollapsed ? 'ChevronDown' : 'ChevronUp'}\n expanded={!isCollapsed}\n aria-label={\n isCollapsed ? `Expand ${group.label} group` : `Collapse ${group.label} group`\n }\n hasBorder={false}\n pressScale={false}\n />\n </span>\n </span>\n </span>\n );\n }\n\n private formatGroupLoadLabel(template: string, group: DataGroup): string {\n return template.split('{group}').join(group.label);\n }\n\n private renderGroupLoadRow(group: DataGroup, totalColumns: number) {\n const error = group.loadMoreError?.trim();\n if (!error && !group.loadingMore && !group.hasMore) return null;\n const manualFallback =\n this.loadMoreMode === 'manual' || !this.groupLoadController.intersectionSupported;\n\n return (\n <tr\n class=\"ds-table__load-row ds-table__group-load-row\"\n data-group-id={group.id}\n ref={element => {\n if (element) this.groupSentinelEls.set(group.id, element);\n else this.groupSentinelEls.delete(group.id);\n }}\n >\n <td class=\"ds-table__load-cell\" colSpan={totalColumns}>\n <div class=\"ds-table__viewport-band ds-table__load-band\">\n {renderTableLoadContent({\n error,\n loading: !!group.loadingMore,\n hasMore: !!group.hasMore,\n manualFallback,\n retryLabel: this.formatGroupLoadLabel(this.groupRetryLabel, group),\n loadingLabel: this.formatGroupLoadLabel(this.groupLoadingMoreLabel, group),\n loadMoreLabel: this.groupLoadMoreLabel,\n loadMoreAriaLabel: this.formatGroupLoadLabel(this.groupLoadMoreAriaLabel, group),\n onRetry: () => this.groupLoadController.request(group.id, 'retry'),\n onLoadMore: () => this.groupLoadController.request(group.id, 'manual'),\n })}\n </div>\n </td>\n </tr>\n );\n }\n\n private renderStickyGroup(model: TableRenderModel) {\n if (!this.documentStickyHeader || !this.activeStickyGroupId) return null;\n const groupModel = model.groups.find(item => item.group.id === this.activeStickyGroupId);\n if (!groupModel) return null;\n return (\n <div\n class={{\n 'ds-table__sticky-group': true,\n 'ds-table__sticky-group--selectable': model.selectable,\n [groupModel.intentClass ?? '']: !!groupModel.intentClass,\n }}\n data-group-id={groupModel.group.id}\n data-group-intent={groupModel.intent}\n >\n {this.renderGroupContent(groupModel)}\n </div>\n );\n }\n\n private renderDataBodies(model: TableRenderModel, plan: TableVirtualPlan | null) {\n return this.bodyRenderer.render({\n headerRowCount: 1 + Number(this.structure.bands.length > 0),\n model,\n plan,\n rows: this.rows,\n stickyHeader: this.stickyHeader,\n documentStickyHeader: this.documentStickyHeader,\n activeStickyGroupId: this.activeStickyGroupId,\n renderRow: (row, renderModel, ariaRowIndex, variableVirtualSize, rowKey) =>\n this.renderRow(row, renderModel, ariaRowIndex, variableVirtualSize, rowKey),\n renderGroupContent: groupModel => this.renderGroupContent(groupModel),\n renderGroupLoadRow: (group, totalColumns) => this.renderGroupLoadRow(group, totalColumns),\n });\n }\n\n private renderSkeletonBody(model: TableRenderModel) {\n return renderTableSkeletonBody({\n pins: this.structure.pins,\n model,\n visibleColumns: this.visibleColumns,\n skeletonRows: this.skeletonRows,\n renderStickyEdge: (sticky, shadow) => this.renderStickyEdge(sticky, shadow),\n });\n }\n\n private renderStateBody(kind: 'empty' | 'error' | 'virtual-viewport', totalColumns: number) {\n const error = kind !== 'empty';\n const heading =\n kind === 'virtual-viewport'\n ? TABLE_VIRTUAL_VIEWPORT_REQUIRED_HEADING\n : error\n ? this.errorHeading\n : this.emptyHeading;\n const body =\n kind === 'virtual-viewport'\n ? TABLE_VIRTUAL_VIEWPORT_REQUIRED_BODY\n : error\n ? this.errorBody\n : this.emptyBody;\n return (\n <tbody class=\"ds-table__body ds-table__state-body\">\n <tr class=\"ds-table__state-row\">\n <td class=\"ds-table__state-cell\" colSpan={totalColumns}>\n <div class=\"ds-table__viewport-band ds-table__state-band\">\n <ds-empty-state\n icon={error ? 'ExclamationTriangle' : 'Inbox'}\n heading={heading}\n body={body}\n />\n </div>\n </td>\n </tr>\n </tbody>\n );\n }\n\n private renderLazyBody(model: TableRenderModel) {\n if (\n this.dataMode !== 'infinite' ||\n !this.incrementalWindowActive ||\n !model.hasData ||\n model.grouped\n )\n return null;\n const error = this.loadMoreError?.trim();\n const manualFallback =\n this.loadMoreMode === 'manual' || !this.loadController.intersectionSupported;\n\n return (\n <tbody class=\"ds-table__body ds-table__load-body\">\n <tr\n class=\"ds-table__load-row\"\n ref={element => {\n this.sentinelEl = element ?? null;\n }}\n >\n <td class=\"ds-table__load-cell\" colSpan={model.totalColumns}>\n <div class=\"ds-table__viewport-band ds-table__load-band\">\n {renderTableLoadContent({\n error,\n loading: this.loadingMore,\n hasMore: this.hasMore,\n manualFallback,\n retryLabel: this.retryLabel,\n loadingLabel: this.loadingMoreLabel,\n loadMoreLabel: this.loadMoreLabel,\n endOfResultsLabel: this.endOfResultsLabel,\n onRetry: () => this.loadController.request('retry'),\n onLoadMore: () => this.loadController.request('manual'),\n })}\n </div>\n </td>\n </tr>\n </tbody>\n );\n }\n\n private renderDocumentStickyHeader(model: TableRenderModel) {\n if (!this.documentStickyHeader) return null;\n return (\n <div class=\"ds-table__document-sticky-header\">\n <table\n class={{\n 'ds-table__table': true,\n 'ds-table__sticky-header-table': true,\n 'ds-table__table--selectable': model.selectable,\n 'ds-table__table--grouped': model.grouped,\n }}\n style={model.tableStyle}\n role=\"presentation\"\n ref={element => {\n this.stickyHeaderTableEl = element ?? null;\n }}\n >\n {this.renderColgroup(model)}\n {this.renderHeader(model, true, true)}\n </table>\n {this.renderFloatingCollapseAll(model)}\n </div>\n );\n }\n\n private get resultSummary(): string | null {\n if (this.dataMode === 'pagination') return null;\n if (this.dataMode === 'virtual') {\n return formatTableTotalSummary(this.currentLoadedRowCount(), this.resultTotalSummaryLabel);\n }\n return formatTableResultSummary(this.displayedCount, this.totalCount, this.resultSummaryLabel);\n }\n\n private get hasResultFooter(): boolean {\n return (\n (this.dataMode === 'pagination' && !!this.pagination) ||\n !!this.resultSummary ||\n this.footerSlotPresence !== 0\n );\n }\n\n private renderResultFooter() {\n const summary = this.resultSummary;\n const hasLeading = (this.footerSlotPresence & TABLE_FOOTER_SLOT_LEADING) !== 0;\n const hasCopy = (this.footerSlotPresence & TABLE_FOOTER_SLOT_COPY) !== 0;\n const hasTrailing = (this.footerSlotPresence & TABLE_FOOTER_SLOT_TRAILING) !== 0;\n const pagination = this.dataMode === 'pagination' ? this.pagination : null;\n if (!this.hasResultFooter) return null;\n return (\n <div class=\"ds-table__footer ds-table__bar ds-chrome-header ds-control--md\">\n <div class=\"ds-table__bar-copy ds-chrome-header__copy ds-control--md\">\n {hasLeading && (\n <div class=\"ds-table__bar-status ds-chrome-header__heading\">\n <slot name=\"footer-leading\" onSlotchange={this.syncFooterSlotPresence} />\n </div>\n )}\n {hasCopy ? (\n <slot name=\"footer\" onSlotchange={this.syncFooterSlotPresence} />\n ) : summary ? (\n <ds-text\n class=\"ds-table__footer-summary ds-table__bar-text\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n lineTruncation={1}\n >\n {summary}\n </ds-text>\n ) : null}\n </div>\n {(pagination || hasTrailing) && (\n <div class=\"ds-table__bar-trailing ds-chrome-header__trailing\">\n {pagination && (\n <ds-pagination\n pageIndex={pagination.pageIndex}\n pageSize={pagination.pageSize}\n pageSizeMode={pagination.pageSizeMode ?? 'fixed'}\n totalItems={pagination.totalItems}\n pageSizeOptions={pagination.pageSizeOptions ?? [25, 50, 100, 200]}\n fitToPage={pagination.fitToPage ?? false}\n {...{ fitToPageInactive: pagination.fitToPageInactive ?? false }}\n fitPageSize={this.fitPageSize ?? pagination.fitPageSize}\n fitPageSizeLabel={pagination.fitPageSizeLabel ?? 'Fit to page'}\n fitPageSizeTriggerLabel={pagination.fitPageSizeTriggerLabel ?? 'Fit'}\n itemLabel={pagination.itemLabel ?? 'items'}\n pageSizeLabel={pagination.pageSizeLabel ?? 'Items'}\n label={pagination.ariaLabel ?? `${this.caption} pagination`}\n showFirstLastButtons={pagination.showFirstLastButtons ?? false}\n loading={this.loading}\n onDsChange={(event: CustomEvent<PaginationChangeDetail>) =>\n this.dsPaginationChange.emit(event.detail)\n }\n />\n )}\n <slot name=\"footer-trailing\" onSlotchange={this.syncFooterSlotPresence} />\n </div>\n )}\n </div>\n );\n }\n\n private renderCaptionBar() {\n if (this.captionVisibility !== 'visible') return null;\n return (\n <div class=\"ds-table__caption-bar ds-table__bar ds-control--md\">\n <div\n class={{\n 'ds-table__caption-content': true,\n 'ds-table__caption-content--trailing': this.showsCaptionTrailing,\n }}\n >\n <div\n class={{\n 'ds-table__caption-leading': true,\n 'ds-table__caption-leading--toolbar': this.headerUsesToolbar,\n }}\n >\n <slot name=\"header\" onSlotchange={this.syncHeaderSlotPresence} />\n {!this.headerPresent ? (\n <ds-text\n class=\"ds-table__caption-title ds-table__bar-text\"\n as=\"div\"\n variant=\"text-title-small\"\n emphasis={true}\n color=\"primary\"\n aria-hidden=\"true\"\n >\n {this.caption}\n </ds-text>\n ) : null}\n </div>\n {this.renderCaptionTrailing()}\n </div>\n <slot name=\"header-after\" />\n </div>\n );\n }\n\n private renderCaptionTrailing() {\n if (!this.showsCaptionTrailing) return null;\n return (\n <div class=\"ds-table__caption-trailing\">\n {this.renderColumnCustomizerTrigger()}\n {this.showsColumnCustomizer &&\n !this.hideColumnCustomizerTrigger &&\n this.showsDataModeSwitcher ? (\n <ds-divider orientation=\"vertical\" length=\"32px\" />\n ) : null}\n {this.renderDataModeSwitcherTrigger()}\n </div>\n );\n }\n\n private renderDataModeSwitcherTrigger() {\n if (!this.showsDataModeSwitcher) return null;\n if (this.chromeLoading) {\n return <ds-skeleton variant=\"control\" controlSize=\"md\" width=\"var(--dimension-size-400)\" />;\n }\n return (\n <span class=\"ds-table__caption-mode-switcher\">\n <ds-tooltip label={this.dataModeSwitcherLabel} side=\"top\" size=\"sm\">\n <ds-button-unfilled\n hasBorder={!this.captionControlsBorderless}\n id={`${this.dataModeSwitcherElementId}-trigger`}\n variant=\"icon\"\n size=\"md\"\n icon=\"Ellipses\"\n aria-label={this.dataModeSwitcherLabel}\n hasMenu={true}\n expanded={this.dataModeSwitcherOpen}\n surfaceOpen={this.dataModeSwitcherSurfaceOpen}\n controls={this.dataModeSwitcherElementId}\n activeFill={false}\n pressScale={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n this.toggleDataModeSwitcher(event.detail.detail === 0);\n }}\n />\n </ds-tooltip>\n </span>\n );\n }\n\n private renderDataModeSwitcherMenu() {\n if (!this.showsDataModeSwitcher || this.chromeLoading) return null;\n return (\n <ds-menu\n id={this.dataModeSwitcherElementId}\n open={this.dataModeSwitcherOpen}\n anchorId={`${this.dataModeSwitcherElementId}-trigger`}\n align=\"end\"\n side=\"bottom\"\n menuLabel={this.dataModeMenuLabel}\n initialFocusVisible={this.dataModeSwitcherInitialFocusVisible}\n items={tableDataModeMenuItems(this.dataMode, {\n infinite: this.infiniteModeLabel,\n pagination: this.paginationModeLabel,\n virtual: this.virtualModeLabel,\n })}\n onDsClose={() => this.closeDataModeSwitcher()}\n onDsAfterClose={() => {\n if (!this.dataModeSwitcherOpen) this.dataModeSwitcherSurfaceOpen = false;\n }}\n onDsSelect={event => this.handleDataModeSwitcherSelect(event.detail)}\n />\n );\n }\n\n private toggleDataModeSwitcher(fromKeyboard = false): void {\n if (this.dataModeSwitcherOpen) this.closeDataModeSwitcher();\n else this.openDataModeSwitcher(fromKeyboard);\n }\n\n private openDataModeSwitcher(fromKeyboard = false): void {\n if (!this.showsDataModeSwitcher || this.dataModeSwitcherOpen) return;\n this.closeColumnCustomizer();\n this.dataModeSwitcherInitialFocusVisible = fromKeyboard;\n this.dataModeSwitcherOpen = true;\n this.dataModeSwitcherSurfaceOpen = true;\n }\n\n private closeDataModeSwitcher(): void {\n this.dataModeSwitcherOpen = false;\n }\n\n private handleDataModeSwitcherSelect(item: MenuItemData): void {\n const dataMode = tableDataModeFromMenuItem(item);\n if (!dataMode) return;\n this.closeDataModeSwitcher();\n if (dataMode !== this.dataMode) this.dsDataModeChange.emit({ dataMode });\n requestAnimationFrame(() => {\n this.el\n .querySelector<\n HTMLElement & { setFocus?: () => void }\n >(`#${CSS.escape(`${this.dataModeSwitcherElementId}-trigger`)}`)\n ?.setFocus?.();\n });\n }\n\n private renderColumnCustomizerTrigger() {\n if (this.hideColumnCustomizerTrigger) return null;\n if (!this.showsColumnCustomizer) return null;\n return (\n <div\n class={{\n 'ds-table__caption-customizer': true,\n 'ds-table__caption-customizer--loading': this.chromeLoading,\n }}\n aria-hidden={this.chromeLoading ? 'true' : undefined}\n >\n <ds-tooltip label={this.captionCompact ? 'Customize' : ''} side=\"top\" size=\"sm\">\n <ds-button-unfilled\n hasBorder={!this.captionControlsBorderless}\n id={`${this.columnCustomizerElementId}-trigger`}\n variant={this.captionCompact ? 'icon' : 'icon-label'}\n size=\"md\"\n icon=\"Preferences\"\n label=\"Customize\"\n labelEmphasis={false}\n pressScale={false}\n aria-label=\"Customize table\"\n haspopup=\"menu\"\n expanded={this.columnCustomizerOpen}\n surfaceOpen={this.columnCustomizerSurfaceOpen}\n controls={this.columnCustomizerElementId}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n if (this.chromeLoading) return;\n this.toggleColumnCustomizer(event.detail.detail === 0);\n }}\n />\n </ds-tooltip>\n {this.chromeLoading ? (\n <ds-skeleton variant=\"control\" controlSize=\"md\" width=\"100%\" />\n ) : null}\n </div>\n );\n }\n\n private renderColumnCustomizerMenu() {\n if (!this.showsColumnCustomizer || this.chromeLoading) return null;\n return (\n <ds-menu\n id={this.columnCustomizerElementId}\n open={this.columnCustomizerOpen}\n anchorId={`${this.columnCustomizerElementId}-trigger`}\n align=\"end\"\n side=\"bottom\"\n menuLabel=\"Customize table\"\n initialFocusVisible={this.columnCustomizerInitialFocusVisible}\n sections={[\n ...tableColumnCustomizerSections(\n this.columns,\n this.hiddenFieldIds,\n this.fieldOrder,\n this.pinnedFieldIds\n ),\n ...(this.customizeOptions.length\n ? [{ header: 'Options', items: this.customizeOptions }]\n : []),\n ]}\n onDsClose={() => this.closeColumnCustomizer()}\n onDsAfterClose={() => {\n if (!this.columnCustomizerOpen) this.columnCustomizerSurfaceOpen = false;\n }}\n onDsSelect={event => {\n if (this.customizeOptions.some(option => option.value === event.detail.value)) {\n this.dsCustomizeOptionChange.emit(event.detail.value!);\n } else this.handleColumnCustomizerSelect(event.detail);\n }}\n onDsItemToggle={(event: CustomEvent<MenuItemToggleDetail>) =>\n this.handleColumnCustomizerAction(event.detail)\n }\n onDsReorder={event => this.handleColumnCustomizerReorder(event.detail)}\n />\n );\n }\n\n private toggleColumnCustomizer(fromKeyboard = false): void {\n if (this.columnCustomizerOpen) this.closeColumnCustomizer();\n else this.openColumnCustomizer(fromKeyboard);\n }\n\n private openColumnCustomizer(fromKeyboard = false): void {\n if (!this.showsColumnCustomizer || this.columnCustomizerOpen) return;\n this.closeDataModeSwitcher();\n this.columnCustomizerInitialFocusVisible = fromKeyboard;\n this.columnCustomizerOpen = true;\n this.columnCustomizerSurfaceOpen = true;\n }\n\n private closeColumnCustomizer(): void {\n this.columnCustomizerOpen = false;\n }\n\n private emitColumnsConfigChange(\n hiddenFieldIds: string[],\n fieldOrder: string[],\n pinnedFieldIds: string[] = this.pinnedFieldIds\n ): void {\n this.dsFieldsConfigChange.emit(\n resolveTableFieldsConfiguration(this.columns, hiddenFieldIds, fieldOrder, pinnedFieldIds)\n );\n }\n\n private handleColumnCustomizerReorder(detail: MenuReorderDetail): void {\n const order = detail.items\n .filter(item => item.reorderable)\n .map(item => item.value)\n .filter((id): id is string => !!id);\n this.dsFieldsConfigChange.emit(\n reorderTableColumnPartition(\n this.columns,\n this.hiddenFieldIds,\n this.fieldOrder,\n this.pinnedFieldIds,\n order\n )\n );\n }\n\n private handleColumnCustomizerAction(detail: MenuItemToggleDetail): void {\n if (detail.action?.id === 'visibility') {\n this.handleColumnCustomizerSelect(detail.item);\n return;\n }\n if (detail.action?.id !== 'pin') return;\n const fieldId = detail.item.value;\n if (!fieldId) return;\n this.dsFieldsConfigChange.emit(\n toggleTableColumnPinned(\n this.columns,\n this.hiddenFieldIds,\n this.fieldOrder,\n this.pinnedFieldIds,\n fieldId\n )\n );\n }\n\n private handleColumnCustomizerSelect(item: MenuItemData): void {\n const fieldId = item.value;\n if (!fieldId || item.isInactive) return;\n this.emitColumnsConfigChange(\n toggleTableColumnHidden(this.columns, this.hiddenFieldIds, fieldId),\n this.fieldOrder\n );\n }\n\n render() {\n this.groupSentinelEls.clear();\n const model = this.createRenderModel();\n this.syncVirtualItems(model);\n this.renderedModel = model;\n const regionLabel = this.scrollLabel?.trim() || `${this.caption} scroll area`;\n const initialLoading = this.loading && !model.hasData;\n const initialError = !model.hasData && this.error;\n const hasGroupedStructure = model.grouped && model.groups.length > 0;\n const windowRows = this.shouldWindowRows(model);\n const virtualViewportMissing =\n this.virtualViewportMissing && !initialLoading && (model.hasData || hasGroupedStructure);\n const virtualPlan = windowRows\n ? (this.virtualWindow ??\n resolveTableVirtualPlan({\n items: this.virtualItems,\n sizes: this.virtualItems.map(item => this.virtualController.sizeFor(item)),\n scrollOffset: this.viewportEl?.scrollTop ?? 0,\n viewportSize: this.estimateVirtualViewportSize(),\n pinnedRowIds: this.virtualPinnedRowIds(),\n }))\n : null;\n const groupLoadingMore = hasGroupedStructure && this.groups.some(group => group.loadingMore);\n const viewportStyle =\n !this.boundedComposition && this.resolvedMaxHeight\n ? { '--ds-table-max-block-size': this.resolvedMaxHeight }\n : undefined;\n const hostStyle = this.fixedHeight\n ? { '--_table-fixed-block-size': this.resolvedHeight }\n : undefined;\n\n return (\n <Host\n class={{\n 'table-host': true,\n 'table-host--bounded': this.boundedComposition,\n 'table-host--fixed-height': this.fixedHeight,\n 'table-host--viewport-fit': this.fitViewport,\n }}\n style={hostStyle}\n onKeyDown={(event: KeyboardEvent) => {\n this.handleGridKeyDown(event);\n this.handlePaginationKeyDown(event);\n }}\n onCopy={(event: ClipboardEvent) => this.handleGridClipboard(event)}\n onPaste={(event: ClipboardEvent) => this.handleGridClipboard(event)}\n onFocusin={this.onVirtualFocusIn}\n >\n <div\n class={{\n 'ds-table': true,\n 'ds-table--bounded': this.boundedComposition,\n 'ds-table--contained-scroll': this.containedScroll,\n 'ds-table--fixed-height': this.fixedHeight,\n 'ds-table--viewport-fit': this.fitViewport,\n 'ds-table--viewport-fit-pending': this.fitViewport && !this.viewportFitSettled,\n 'ds-table--viewport-fit-settled': this.fitViewport && this.viewportFitSettled,\n 'ds-table--sticky-header': this.stickyHeader,\n 'ds-table--document-sticky-header': this.documentStickyHeader,\n 'ds-table--contained-sticky-header': this.stickyHeader && !this.documentStickyHeader,\n 'ds-table--caption-visible': this.captionVisibility === 'visible',\n 'ds-table--chrome-edge-to-edge': this.chromeLayout === 'edge-to-edge',\n 'ds-table--super-headers': this.structure.bands.length > 0,\n 'ds-table--responsive-cards':\n this.responsiveLayout === 'cards' &&\n this.interactionMode !== 'grid' &&\n this.cellSpans.length === 0,\n 'ds-table--cards-active':\n this.narrowViewport &&\n this.responsiveLayout === 'cards' &&\n this.interactionMode !== 'grid' &&\n this.cellSpans.length === 0,\n 'ds-table--footer-visible': this.hasResultFooter,\n 'ds-table--state-fill':\n !initialLoading &&\n (initialError || virtualViewportMissing || !(model.hasData || hasGroupedStructure)),\n }}\n ref={element => {\n this.rootEl = element ?? null;\n }}\n >\n {this.renderCaptionBar()}\n <div\n class={{\n 'ds-table__frame': true,\n 'ds-table__frame--overflow-start': this.overflowStart,\n 'ds-table__frame--overflow-end': this.overflowEnd,\n }}\n data-narrow={String(this.narrowViewport)}\n ref={element => {\n this.frameEl = element ?? null;\n }}\n >\n {this.renderDocumentStickyHeader(model)}\n {this.renderStickyGroup(model)}\n {!this.documentStickyHeader && this.renderFloatingCollapseAll(model)}\n <div\n class={{\n 'ds-table__viewport': true,\n 'ds-focus-ring': this.scrollable,\n }}\n style={viewportStyle}\n ref={element => {\n this.viewportEl = element ?? null;\n }}\n role={this.scrollable ? 'region' : undefined}\n aria-label={this.scrollable ? regionLabel : undefined}\n tabIndex={this.scrollable ? 0 : undefined}\n >\n <table\n class={{\n 'ds-table__table': true,\n 'ds-table__table--selectable': model.selectable,\n 'ds-table__table--grouped': model.grouped,\n 'ds-table__table--deferred-rows':\n this.dataMode !== 'virtual' && !this.structure.merges.hasRowSpans,\n 'ds-table__table--windowed': windowRows,\n 'ds-table__table--virtual': this.dataMode === 'virtual' && windowRows,\n 'ds-table__table--native-group-sticky':\n (model.grouped && this.stickyHeader && !this.documentStickyHeader) ||\n windowRows,\n }}\n style={model.tableStyle}\n role={this.gridEnabled ? 'grid' : 'table'}\n aria-multiselectable={this.gridEnabled ? 'true' : undefined}\n aria-rowcount={\n windowRows\n ? 1 + Number(this.structure.bands.length > 0) + this.virtualItems.length\n : undefined\n }\n aria-busy={\n initialLoading ||\n (this.dataMode === 'infinite' && (this.loadingMore || groupLoadingMore))\n ? 'true'\n : undefined\n }\n ref={element => {\n this.tableEl = element ?? null;\n }}\n >\n <caption class=\"ds-table__caption ds-visually-hidden\">\n <ds-text as=\"span\" variant=\"text-title-small\" emphasis={true} color=\"primary\">\n {this.caption}\n </ds-text>\n </caption>\n {this.renderColgroup(model)}\n {this.renderHeader(\n model,\n !this.documentStickyHeader,\n false,\n windowRows ? 1 : undefined\n )}\n {initialLoading\n ? this.renderSkeletonBody(model)\n : initialError\n ? this.renderStateBody('error', model.totalColumns)\n : virtualViewportMissing\n ? this.renderStateBody('virtual-viewport', model.totalColumns)\n : model.hasData || hasGroupedStructure\n ? this.renderDataBodies(model, virtualPlan)\n : this.renderStateBody('empty', model.totalColumns)}\n {!initialLoading &&\n !initialError &&\n this.dataMode !== 'virtual' &&\n this.renderLazyBody(model)}\n </table>\n </div>\n </div>\n {this.renderResultFooter()}\n {this.renderOverflowActionMenu()}\n {this.renderDataModeSwitcherMenu()}\n {this.renderColumnCustomizerMenu()}\n {this.renderTruncateTooltip()}\n <div class=\"ds-visually-hidden\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {this.announcement}\n </div>\n </div>\n </Host>\n );\n }\n}\n",
1642
1682
  "type": "registry:ui"
1643
1683
  },
1644
1684
  {
@@ -1663,7 +1703,7 @@
1663
1703
  },
1664
1704
  {
1665
1705
  "path": "src/wc/components/Table/table-column-customizer.ts",
1666
- "content": "import type { MenuItemData } from '../Menu/menu-types';\nimport {\n canToggleListHidden,\n listCustomizerMenuItems,\n moveListOrder,\n moveListOrderBy,\n resolveListHiddenIds,\n resolveListOrder,\n toggleListHidden,\n type ListCustomizerOptions,\n} from '../../utils/list-customizer';\nimport type { TableColumn } from './table-types';\n\n/**\n * Table's customizer policy: the last visible data column cannot be hidden, and\n * rows reorder. Everything below is a thin adapter over the domain-neutral\n * helpers in utils/list-customizer so other catalogs can reuse the same state\n * machine with different policy.\n */\nconst TABLE_COLUMN_CUSTOMIZER: ListCustomizerOptions = { minVisible: 1, reorderable: true };\n\nlet columnCustomizerSeq = 0;\n\nexport function nextTableColumnCustomizerElementId(): string {\n columnCustomizerSeq += 1;\n return `ds-table-column-customizer-${columnCustomizerSeq}`;\n}\n\nexport function isTableActionColumn(column: TableColumn): boolean {\n return column.kind === 'action';\n}\n\nexport function tableColumnCustomizerLabel(column: TableColumn): string {\n return column.label.trim() || column.accessibleLabel?.trim() || column.id;\n}\n\nexport function tableDataColumns(columns: TableColumn[]): TableColumn[] {\n return columns.filter(column => !isTableActionColumn(column));\n}\n\n/** Data-column ids in display order. Unknown, duplicate, and action ids are ignored. */\nexport function resolveTableColumnOrder(\n columns: TableColumn[],\n fieldOrder: string[] | undefined\n): string[] {\n return resolveListOrder(\n tableDataColumns(columns).map(column => column.id),\n fieldOrder\n );\n}\n\n/**\n * Hidden data-column ids. Action and unknown ids are dropped. At least one data\n * column remains visible when the catalog has any data columns.\n */\nexport function resolveTableHiddenColumnIds(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined\n): string[] {\n return resolveListHiddenIds(\n tableDataColumns(columns).map(column => column.id),\n hiddenFieldIds,\n TABLE_COLUMN_CUSTOMIZER\n );\n}\n\nexport function resolveTableVisibleColumns(\n columns: TableColumn[],\n options: {\n columnCustomizer?: boolean;\n hiddenFieldIds?: string[];\n fieldOrder?: string[];\n } = {}\n): TableColumn[] {\n if (!options.columnCustomizer) return columns;\n\n const byId = new Map(columns.map(column => [column.id, column]));\n const hidden = new Set(resolveTableHiddenColumnIds(columns, options.hiddenFieldIds));\n const visibleData = resolveTableColumnOrder(columns, options.fieldOrder)\n .map(id => byId.get(id))\n .filter((column): column is TableColumn => !!column && !hidden.has(column.id));\n\n return [...visibleData, ...columns.filter(isTableActionColumn)];\n}\n\nexport function canToggleTableColumnHidden(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined,\n fieldId: string\n): boolean {\n const column = columns.find(candidate => candidate.id === fieldId);\n if (!column || isTableActionColumn(column)) return false;\n\n return canToggleListHidden(\n tableDataColumns(columns).map(candidate => candidate.id),\n hiddenFieldIds,\n fieldId,\n TABLE_COLUMN_CUSTOMIZER\n );\n}\n\nexport function toggleTableColumnHidden(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined,\n fieldId: string\n): string[] {\n const column = columns.find(candidate => candidate.id === fieldId);\n if (!column || isTableActionColumn(column)) {\n return resolveTableHiddenColumnIds(columns, hiddenFieldIds);\n }\n\n return toggleListHidden(\n tableDataColumns(columns).map(candidate => candidate.id),\n hiddenFieldIds,\n fieldId,\n TABLE_COLUMN_CUSTOMIZER\n );\n}\n\nexport function moveTableColumnOrder(order: string[], fromId: string, toId: string): string[] {\n return moveListOrder(order, fromId, toId);\n}\n\nexport function moveTableColumnInOrder(order: string[], fieldId: string, offset: number): string[] {\n return moveListOrderBy(order, fieldId, offset);\n}\n\nexport interface TableColumnCustomizerItem {\n column: TableColumn;\n label: string;\n visible: boolean;\n hideable: boolean;\n reorderable: boolean;\n}\n\nexport function tableColumnCustomizerItems(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined,\n fieldOrder: string[] | undefined\n): TableColumnCustomizerItem[] {\n const byId = new Map(columns.map(column => [column.id, column]));\n const resolvedHidden = resolveTableHiddenColumnIds(columns, hiddenFieldIds);\n const hidden = new Set(resolvedHidden);\n const dataItems = resolveTableColumnOrder(columns, fieldOrder).flatMap(id => {\n const column = byId.get(id);\n if (!column) return [];\n return [\n {\n column,\n label: tableColumnCustomizerLabel(column),\n visible: !hidden.has(id),\n hideable: canToggleTableColumnHidden(columns, resolvedHidden, id),\n reorderable: true,\n },\n ];\n });\n return dataItems;\n}\n\n/** Menu switch rows for the table-owned column customizer. */\nexport function tableColumnCustomizerMenuItems(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined,\n fieldOrder: string[] | undefined\n): MenuItemData[] {\n return listCustomizerMenuItems(\n tableDataColumns(columns).map(column => ({\n id: column.id,\n label: tableColumnCustomizerLabel(column),\n })),\n hiddenFieldIds,\n fieldOrder,\n TABLE_COLUMN_CUSTOMIZER\n );\n}\n",
1706
+ "content": "import type { MenuItemData, MenuItemsSection } from '../Menu/menu-types';\nimport {\n canToggleListHidden,\n moveListOrder,\n moveListOrderBy,\n resolveListHiddenIds,\n resolveListOrder,\n toggleListHidden,\n type ListCustomizerOptions,\n} from '../../utils/list-customizer';\nimport type { TableColumn } from './table-types';\n\n/**\n * Table's customizer policy: the last visible data column cannot be hidden, and\n * rows reorder. Everything below is a thin adapter over the domain-neutral\n * helpers in utils/list-customizer so other catalogs can reuse the same state\n * machine with different policy.\n */\nconst TABLE_COLUMN_CUSTOMIZER: ListCustomizerOptions = { minVisible: 1, reorderable: true };\n\nlet columnCustomizerSeq = 0;\n\nexport function nextTableColumnCustomizerElementId(): string {\n columnCustomizerSeq += 1;\n return `ds-table-column-customizer-${columnCustomizerSeq}`;\n}\n\nexport function isTableActionColumn(column: TableColumn): boolean {\n return column.kind === 'action';\n}\n\nexport function tableColumnCustomizerLabel(column: TableColumn): string {\n return column.label.trim() || column.accessibleLabel?.trim() || column.id;\n}\n\nexport function tableDataColumns(columns: TableColumn[]): TableColumn[] {\n return columns.filter(column => !isTableActionColumn(column));\n}\n\nfunction hasResolvedTableColumnSize(column: TableColumn): boolean {\n return (\n typeof column.size === 'string' ||\n (typeof column.size === 'number' && Number.isFinite(column.size) && column.size > 0) ||\n Boolean(column.imageTracks)\n );\n}\n\nfunction tablePinnableColumns(columns: TableColumn[]): TableColumn[] {\n return tableDataColumns(columns).filter(\n column => !column.sticky && hasResolvedTableColumnSize(column)\n );\n}\n\n/** User-managed pinned ids. Fixed start/end columns remain column-owned. */\nexport function resolveTablePinnedFieldIds(\n columns: TableColumn[],\n pinnedFieldIds: string[] | undefined,\n fieldOrder?: string[]\n): string[] {\n const pinnable = new Set(tablePinnableColumns(columns).map(column => column.id));\n const requested = new Set((pinnedFieldIds ?? []).filter(id => pinnable.has(id)));\n return resolveTableColumnOrder(columns, fieldOrder).filter(id => requested.has(id));\n}\n\n/** Fixed-start, user-pinned, unpinned, then fixed-end data columns. */\nexport function resolveTableConfiguredColumnOrder(\n columns: TableColumn[],\n fieldOrder: string[] | undefined,\n pinnedFieldIds: string[] | undefined\n): string[] {\n const base = resolveTableColumnOrder(columns, fieldOrder);\n const byId = new Map(columns.map(column => [column.id, column]));\n const pinned = new Set(resolveTablePinnedFieldIds(columns, pinnedFieldIds, base));\n return [\n ...base.filter(id => byId.get(id)?.sticky === 'start'),\n ...base.filter(id => pinned.has(id)),\n ...base.filter(id => !pinned.has(id) && !byId.get(id)?.sticky),\n ...base.filter(id => byId.get(id)?.sticky === 'end'),\n ];\n}\n\n/** Data-column ids in display order. Unknown, duplicate, and action ids are ignored. */\nexport function resolveTableColumnOrder(\n columns: TableColumn[],\n fieldOrder: string[] | undefined\n): string[] {\n return resolveListOrder(\n tableDataColumns(columns).map(column => column.id),\n fieldOrder\n );\n}\n\n/**\n * Hidden data-column ids. Action and unknown ids are dropped. At least one data\n * column remains visible when the catalog has any data columns.\n */\nexport function resolveTableHiddenColumnIds(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined\n): string[] {\n return resolveListHiddenIds(\n tableDataColumns(columns).map(column => column.id),\n hiddenFieldIds,\n TABLE_COLUMN_CUSTOMIZER\n );\n}\n\nexport function resolveTableVisibleColumns(\n columns: TableColumn[],\n options: {\n columnCustomizer?: boolean;\n hiddenFieldIds?: string[];\n fieldOrder?: string[];\n pinnedFieldIds?: string[];\n } = {}\n): TableColumn[] {\n if (!options.columnCustomizer) return columns;\n\n const byId = new Map(columns.map(column => [column.id, column]));\n const hidden = new Set(resolveTableHiddenColumnIds(columns, options.hiddenFieldIds));\n const pinned = new Set(\n resolveTablePinnedFieldIds(columns, options.pinnedFieldIds, options.fieldOrder)\n );\n const visibleData = resolveTableConfiguredColumnOrder(\n columns,\n options.fieldOrder,\n options.pinnedFieldIds\n )\n .map(id => byId.get(id))\n .filter((column): column is TableColumn => !!column && !hidden.has(column.id));\n\n return [\n ...visibleData.map(column =>\n pinned.has(column.id) && !column.sticky ? { ...column, sticky: 'start' as const } : column\n ),\n ...columns.filter(isTableActionColumn),\n ];\n}\n\nexport function canToggleTableColumnHidden(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined,\n fieldId: string\n): boolean {\n const column = columns.find(candidate => candidate.id === fieldId);\n if (!column || isTableActionColumn(column)) return false;\n\n return canToggleListHidden(\n tableDataColumns(columns).map(candidate => candidate.id),\n hiddenFieldIds,\n fieldId,\n TABLE_COLUMN_CUSTOMIZER\n );\n}\n\nexport function toggleTableColumnHidden(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined,\n fieldId: string\n): string[] {\n const column = columns.find(candidate => candidate.id === fieldId);\n if (!column || isTableActionColumn(column)) {\n return resolveTableHiddenColumnIds(columns, hiddenFieldIds);\n }\n\n return toggleListHidden(\n tableDataColumns(columns).map(candidate => candidate.id),\n hiddenFieldIds,\n fieldId,\n TABLE_COLUMN_CUSTOMIZER\n );\n}\n\nexport function moveTableColumnOrder(order: string[], fromId: string, toId: string): string[] {\n return moveListOrder(order, fromId, toId);\n}\n\nexport function moveTableColumnInOrder(order: string[], fieldId: string, offset: number): string[] {\n return moveListOrderBy(order, fieldId, offset);\n}\n\nexport interface TableColumnCustomizerItem {\n column: TableColumn;\n label: string;\n visible: boolean;\n hideable: boolean;\n reorderable: boolean;\n pinned: boolean;\n pinnable: boolean;\n}\n\nexport function tableColumnCustomizerItems(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined,\n fieldOrder: string[] | undefined,\n pinnedFieldIds: string[] | undefined = []\n): TableColumnCustomizerItem[] {\n const byId = new Map(columns.map(column => [column.id, column]));\n const resolvedHidden = resolveTableHiddenColumnIds(columns, hiddenFieldIds);\n const hidden = new Set(resolvedHidden);\n const pinned = new Set(resolveTablePinnedFieldIds(columns, pinnedFieldIds, fieldOrder));\n const configuredOrder = resolveTableConfiguredColumnOrder(columns, fieldOrder, pinnedFieldIds);\n const pinnedCount = configuredOrder.filter(id => pinned.has(id)).length;\n const unpinnedCount = configuredOrder.filter(id => !pinned.has(id)).length;\n const dataItems = configuredOrder.flatMap(id => {\n const column = byId.get(id);\n if (!column) return [];\n const isPinned = pinned.has(id);\n const pinnable = !column.sticky && hasResolvedTableColumnSize(column);\n return [\n {\n column,\n label: tableColumnCustomizerLabel(column),\n visible: !hidden.has(id),\n hideable: canToggleTableColumnHidden(columns, resolvedHidden, id),\n reorderable: !column.sticky && (isPinned ? pinnedCount > 1 : unpinnedCount > 1),\n pinned: isPinned,\n pinnable,\n },\n ];\n });\n return dataItems;\n}\n\n/** Menu action rows for the table-owned column customizer. */\nexport function tableColumnCustomizerMenuItems(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined,\n fieldOrder: string[] | undefined,\n pinnedFieldIds: string[] | undefined = []\n): MenuItemData[] {\n return tableColumnCustomizerItems(columns, hiddenFieldIds, fieldOrder, pinnedFieldIds).map(\n item => ({\n label: item.label,\n value: item.column.id,\n reorderable: item.reorderable,\n reorderHandleInactive: item.pinned && !item.reorderable,\n trailingActions: [\n {\n id: 'visibility',\n icon: item.visible ? 'EyeStrikethrough' : 'Eye',\n label: item.visible ? `Hide ${item.label}` : `Show ${item.label}`,\n isInactive: !item.hideable,\n },\n ...(item.pinnable\n ? [\n {\n id: 'pin',\n icon: item.pinned ? 'Cross' : 'Pin',\n label: item.pinned ? `Unpin ${item.label}` : `Pin ${item.label}`,\n },\n ]\n : []),\n ],\n })\n );\n}\n\n/** Column subgroups retain one parent heading while enforcing drag boundaries. */\nexport function tableColumnCustomizerSections(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined,\n fieldOrder: string[] | undefined,\n pinnedFieldIds: string[] | undefined = []\n): MenuItemsSection[] {\n const items = tableColumnCustomizerMenuItems(columns, hiddenFieldIds, fieldOrder, pinnedFieldIds);\n const pinnedIds = new Set(resolveTablePinnedFieldIds(columns, pinnedFieldIds, fieldOrder));\n const pinned = items.filter(item => item.value && pinnedIds.has(item.value));\n const unpinned = items.filter(item => !item.value || !pinnedIds.has(item.value));\n if (!pinned.length) return [{ header: 'Columns', items: unpinned }];\n return [\n { header: 'Columns', subheader: 'Pinned columns', items: pinned },\n ...(unpinned.length\n ? [{ subheader: 'Other columns', insetDividerBefore: true, items: unpinned }]\n : []),\n ];\n}\n\nexport interface TableFieldsConfiguration {\n hiddenFieldIds: string[];\n fieldOrder: string[];\n pinnedFieldIds: string[];\n}\n\nexport function resolveTableFieldsConfiguration(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined,\n fieldOrder: string[] | undefined,\n pinnedFieldIds: string[] | undefined\n): TableFieldsConfiguration {\n const order = resolveTableConfiguredColumnOrder(columns, fieldOrder, pinnedFieldIds);\n return {\n hiddenFieldIds: resolveTableHiddenColumnIds(columns, hiddenFieldIds),\n fieldOrder: order,\n pinnedFieldIds: resolveTablePinnedFieldIds(columns, pinnedFieldIds, order),\n };\n}\n\n/** Pin appends to the pinned block; unpinning inserts at the start of unpinned columns. */\nexport function toggleTableColumnPinned(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined,\n fieldOrder: string[] | undefined,\n pinnedFieldIds: string[] | undefined,\n fieldId: string\n): TableFieldsConfiguration {\n const pinnable = tablePinnableColumns(columns).some(column => column.id === fieldId);\n const current = resolveTableFieldsConfiguration(\n columns,\n hiddenFieldIds,\n fieldOrder,\n pinnedFieldIds\n );\n if (!pinnable) return current;\n\n const pinned = new Set(current.pinnedFieldIds);\n if (pinned.has(fieldId)) pinned.delete(fieldId);\n else pinned.add(fieldId);\n return resolveTableFieldsConfiguration(columns, current.hiddenFieldIds, current.fieldOrder, [\n ...pinned,\n ]);\n}\n\n/** Apply a drag result to only its pinned or unpinned partition. */\nexport function reorderTableColumnPartition(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined,\n fieldOrder: string[] | undefined,\n pinnedFieldIds: string[] | undefined,\n reorderedIds: string[]\n): TableFieldsConfiguration {\n const current = resolveTableFieldsConfiguration(\n columns,\n hiddenFieldIds,\n fieldOrder,\n pinnedFieldIds\n );\n const pinned = new Set(current.pinnedFieldIds);\n const byId = new Map(columns.map(column => [column.id, column]));\n const fixedStart = current.fieldOrder.filter(id => byId.get(id)?.sticky === 'start');\n const fixedEnd = current.fieldOrder.filter(id => byId.get(id)?.sticky === 'end');\n const pinnedOrder = current.fieldOrder.filter(id => pinned.has(id));\n const unpinnedOrder = current.fieldOrder.filter(id => !pinned.has(id) && !byId.get(id)?.sticky);\n const reordersPinned = reorderedIds.some(id => pinned.has(id));\n const nextPinned = reordersPinned ? resolveListOrder(pinnedOrder, reorderedIds) : pinnedOrder;\n const nextUnpinned = reordersPinned\n ? unpinnedOrder\n : resolveListOrder(unpinnedOrder, reorderedIds);\n return resolveTableFieldsConfiguration(\n columns,\n current.hiddenFieldIds,\n [...fixedStart, ...nextPinned, ...nextUnpinned, ...fixedEnd],\n current.pinnedFieldIds\n );\n}\n",
1667
1707
  "type": "registry:ui"
1668
1708
  },
1669
1709
  {
@@ -1688,7 +1728,7 @@
1688
1728
  },
1689
1729
  {
1690
1730
  "path": "src/wc/components/Table/table-layout-controller.ts",
1691
- "content": "export interface TableLayoutElements {\n viewport: HTMLElement | null;\n contentTable: HTMLTableElement | null;\n stickyHeaderTable: HTMLTableElement | null;\n collapseAllOverlay: HTMLElement | null;\n frame: HTMLElement | null;\n interactiveHead: HTMLTableSectionElement | null;\n}\n\nexport interface TableLayoutMode {\n documentStickyHeader: boolean;\n floatingCollapseAll: boolean;\n clampVerticalOverscroll: boolean;\n}\n\nexport interface TableOverflowState {\n start: boolean;\n end: boolean;\n scrollable: boolean;\n}\n\nexport interface TableLayoutControllerOptions {\n elements: () => TableLayoutElements;\n mode: () => TableLayoutMode;\n overflowChanged: (state: TableOverflowState) => void;\n narrowChanged?: (narrow: boolean) => void;\n verticalEdgeWheel?: (deltaY: number) => boolean;\n}\n\nexport interface TableLayoutMetricInput {\n viewportInlineSize: number;\n viewportBlockSize: number;\n scrollInlineSize: number;\n scrollBlockSize: number;\n scrollInlineOffset: number;\n collapseHeadBlockStart?: number;\n collapseFrameBlockStart?: number;\n}\n\nexport interface TableLayoutMetrics {\n visibleInlineSize: number;\n overflow: TableOverflowState;\n inlineOffset: number;\n maxInlineOffset: number;\n collapseBlockOffset: number | null;\n}\n\n/** Pure geometry resolution shared by the controller and focused tests. */\nexport function resolveTableLayoutMetrics(input: TableLayoutMetricInput): TableLayoutMetrics {\n const horizontalOverflow = input.scrollInlineSize - input.viewportInlineSize > 1;\n return {\n visibleInlineSize: input.viewportInlineSize,\n overflow: {\n start: horizontalOverflow && input.scrollInlineOffset > 1,\n end:\n horizontalOverflow &&\n input.scrollInlineOffset + input.viewportInlineSize < input.scrollInlineSize - 1,\n scrollable: horizontalOverflow || input.scrollBlockSize - input.viewportBlockSize > 1,\n },\n inlineOffset: input.scrollInlineOffset,\n maxInlineOffset: Math.max(0, input.scrollInlineSize - input.viewportInlineSize),\n collapseBlockOffset:\n input.collapseHeadBlockStart == null || input.collapseFrameBlockStart == null\n ? null\n : Math.max(0, input.collapseHeadBlockStart - input.collapseFrameBlockStart),\n };\n}\n\n/**\n * Owns table viewport observation. It resolves newly rendered geometry\n * immediately so the first paint has the correct viewport-width chrome, then\n * batches observer-driven scroll and resize reads/writes into one animation frame.\n * The component remains responsible only for refs and reactive overflow state.\n */\nexport class TableLayoutController {\n private connected = false;\n private connectedViewport: HTMLElement | null = null;\n private connectedContentTable: HTMLTableElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private frame: number | null = null;\n private overflow: TableOverflowState | null = null;\n private inlineOffset = 0;\n private maxInlineOffset = 0;\n private maxBlockOffset = 0;\n private lastMode: TableLayoutMode | null = null;\n\n constructor(private readonly options: TableLayoutControllerOptions) {}\n\n connect(): void {\n this.connected = true;\n this.refresh();\n }\n\n disconnect(): void {\n this.connected = false;\n if (this.frame !== null) cancelAnimationFrame(this.frame);\n this.frame = null;\n this.connectedViewport?.removeEventListener('scroll', this.handleScroll);\n this.connectedViewport?.removeEventListener('wheel', this.handleWheel);\n this.connectedViewport = null;\n this.connectedContentTable = null;\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n }\n\n /** Reconcile newly rendered refs, observers, and pending geometry. */\n refresh(forceGeometry = true): void {\n if (!this.connected) return;\n const { viewport, contentTable } = this.options.elements();\n const elementsChanged =\n viewport !== this.connectedViewport || contentTable !== this.connectedContentTable;\n if (elementsChanged) {\n this.connectedViewport?.removeEventListener('scroll', this.handleScroll);\n this.connectedViewport?.removeEventListener('wheel', this.handleWheel);\n this.resizeObserver?.disconnect();\n this.connectedViewport = viewport;\n this.connectedContentTable = contentTable;\n viewport?.addEventListener('scroll', this.handleScroll, { passive: true });\n viewport?.addEventListener('wheel', this.handleWheel, { passive: false });\n\n if (typeof ResizeObserver !== 'undefined' && viewport) {\n this.resizeObserver ??= new ResizeObserver(this.schedule);\n this.resizeObserver.observe(viewport);\n if (contentTable) this.resizeObserver.observe(contentTable);\n }\n }\n const mode = this.options.mode();\n const modeChanged =\n !this.lastMode ||\n mode.documentStickyHeader !== this.lastMode.documentStickyHeader ||\n mode.floatingCollapseAll !== this.lastMode.floatingCollapseAll ||\n mode.clampVerticalOverscroll !== this.lastMode.clampVerticalOverscroll;\n this.lastMode = mode;\n // New refs and explicit refreshes resolve before paint. A recycled virtual\n // window keeps the same scroll geometry, so ResizeObserver owns later size\n // changes without forcing layout after every row patch.\n if (forceGeometry || elementsChanged || modeChanged) this.sync();\n }\n\n private schedule = (): void => {\n if (!this.connected || this.frame !== null) return;\n this.frame = requestAnimationFrame(() => {\n this.frame = null;\n this.sync();\n });\n };\n\n private readonly handleScroll = (): void => {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n const mode = this.options.mode();\n this.clampInlineOffset();\n if (mode.clampVerticalOverscroll) this.clampBlockOffset();\n if (\n viewport.scrollLeft !== this.inlineOffset ||\n (mode.floatingCollapseAll && !mode.documentStickyHeader)\n ) {\n this.schedule();\n }\n };\n\n private readonly handleWheel = (event: WheelEvent): void => {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n\n if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) {\n const atStart = viewport.scrollLeft <= 0 && event.deltaX < 0;\n const atEnd = viewport.scrollLeft >= this.maxInlineOffset && event.deltaX > 0;\n if (atStart || atEnd) event.preventDefault();\n return;\n }\n\n if (!this.options.mode().clampVerticalOverscroll || event.deltaY === 0) return;\n const atStart = viewport.scrollTop <= 0 && event.deltaY < 0;\n const atEnd = viewport.scrollTop >= this.maxBlockOffset && event.deltaY > 0;\n if (!atStart && !atEnd) return;\n\n this.options.verticalEdgeWheel?.(event.deltaY);\n event.preventDefault();\n };\n\n private clampInlineOffset(): void {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n const clamped = Math.min(this.maxInlineOffset, Math.max(0, viewport.scrollLeft));\n if (viewport.scrollLeft !== clamped) viewport.scrollLeft = clamped;\n }\n\n private clampBlockOffset(): void {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n const clamped = Math.min(this.maxBlockOffset, Math.max(0, viewport.scrollTop));\n if (viewport.scrollTop !== clamped) viewport.scrollTop = clamped;\n }\n\n private sync(): void {\n const elements = this.options.elements();\n const viewport = elements.viewport;\n if (!viewport) return;\n\n // Read every layout value before writing styles or reactive state.\n const mode = this.options.mode();\n const measureCollapse =\n mode.floatingCollapseAll &&\n !mode.documentStickyHeader &&\n elements.collapseAllOverlay &&\n elements.frame &&\n elements.interactiveHead;\n const metrics = resolveTableLayoutMetrics({\n viewportInlineSize: viewport.clientWidth,\n viewportBlockSize: viewport.clientHeight,\n scrollInlineSize: viewport.scrollWidth,\n scrollBlockSize: viewport.scrollHeight,\n scrollInlineOffset: viewport.scrollLeft,\n collapseHeadBlockStart: measureCollapse\n ? elements.interactiveHead!.getBoundingClientRect().top\n : undefined,\n collapseFrameBlockStart: measureCollapse\n ? elements.frame!.getBoundingClientRect().top\n : undefined,\n });\n this.inlineOffset = metrics.inlineOffset;\n this.maxInlineOffset = metrics.maxInlineOffset;\n this.maxBlockOffset = Math.max(0, viewport.scrollHeight - viewport.clientHeight);\n // Header-only geometry; independent of the loaded/recycled row count.\n const pinnedCells = elements.interactiveHead?.querySelectorAll?.<HTMLElement>(\n '.ds-table__header-cell:not(.ds-table__super-header-cell).ds-table__cell--sticky-start, .ds-table__header-cell:not(.ds-table__super-header-cell).ds-table__cell--sticky-end'\n );\n const pinnedSize = [...(pinnedCells ?? [])].reduce(\n (size, cell) => size + cell.getBoundingClientRect().width,\n 0\n );\n const crowded = pinnedSize > 0 && pinnedSize >= metrics.visibleInlineSize;\n if (elements.frame?.getAttribute('data-pins-crowded') !== String(crowded))\n elements.frame?.setAttribute('data-pins-crowded', String(crowded));\n // Reuse viewport observation rather than size-contain the recycled body:\n // WebKit resets native scrolling when contained virtual rows are replaced.\n // Responsive mode belongs to the frame, not the scrollable content width:\n // classic scrollbar gutters must not move the card breakpoint.\n const narrow = (elements.frame?.clientWidth ?? metrics.visibleInlineSize) < 768;\n this.options.narrowChanged?.(narrow);\n if (elements.frame?.getAttribute('data-narrow') !== String(narrow))\n elements.frame?.setAttribute('data-narrow', String(narrow));\n\n this.setProperty(viewport, '--ds-table-visible-inline-size', `${metrics.visibleInlineSize}px`);\n if (elements.stickyHeaderTable) {\n this.setProperty(\n elements.stickyHeaderTable,\n '--ds-table-inline-scroll-offset',\n `${metrics.inlineOffset}px`\n );\n this.setProperty(\n elements.stickyHeaderTable,\n '--ds-table-inline-scroll-max-offset',\n `${metrics.maxInlineOffset}px`\n );\n }\n if (metrics.collapseBlockOffset !== null && elements.collapseAllOverlay) {\n this.setProperty(\n elements.collapseAllOverlay,\n '--ds-table-collapse-all-block-offset',\n `${metrics.collapseBlockOffset}px`\n );\n }\n\n if (\n !this.overflow ||\n this.overflow.start !== metrics.overflow.start ||\n this.overflow.end !== metrics.overflow.end ||\n this.overflow.scrollable !== metrics.overflow.scrollable\n ) {\n this.overflow = metrics.overflow;\n this.options.overflowChanged(metrics.overflow);\n }\n }\n\n private setProperty(element: HTMLElement, property: string, value: string): void {\n if (element.style.getPropertyValue(property) !== value) {\n element.style.setProperty(property, value);\n }\n }\n}\n",
1731
+ "content": "export interface TableLayoutElements {\n viewport: HTMLElement | null;\n contentTable: HTMLTableElement | null;\n stickyHeaderTable: HTMLTableElement | null;\n collapseAllOverlay: HTMLElement | null;\n frame: HTMLElement | null;\n interactiveHead: HTMLTableSectionElement | null;\n}\n\nexport interface TableLayoutMode {\n documentStickyHeader: boolean;\n floatingCollapseAll: boolean;\n clampVerticalOverscroll: boolean;\n}\n\nexport interface TableOverflowState {\n start: boolean;\n end: boolean;\n scrollable: boolean;\n}\n\nexport interface TableLayoutControllerOptions {\n elements: () => TableLayoutElements;\n mode: () => TableLayoutMode;\n overflowChanged: (state: TableOverflowState) => void;\n verticalEdgeWheel?: (deltaY: number) => boolean;\n}\n\nexport const TABLE_CARD_VIEWPORT_BREAKPOINT = 768;\n\nexport function isTableCardViewport(inlineSize: number): boolean {\n return inlineSize < TABLE_CARD_VIEWPORT_BREAKPOINT;\n}\n\nexport interface TableLayoutMetricInput {\n viewportInlineSize: number;\n viewportBlockSize: number;\n scrollInlineSize: number;\n scrollBlockSize: number;\n scrollInlineOffset: number;\n collapseHeadBlockStart?: number;\n collapseFrameBlockStart?: number;\n}\n\nexport interface TableLayoutMetrics {\n visibleInlineSize: number;\n overflow: TableOverflowState;\n inlineOffset: number;\n maxInlineOffset: number;\n collapseBlockOffset: number | null;\n}\n\n/** Pure geometry resolution shared by the controller and focused tests. */\nexport function resolveTableLayoutMetrics(input: TableLayoutMetricInput): TableLayoutMetrics {\n const horizontalOverflow = input.scrollInlineSize - input.viewportInlineSize > 1;\n return {\n visibleInlineSize: input.viewportInlineSize,\n overflow: {\n start: horizontalOverflow && input.scrollInlineOffset > 1,\n end:\n horizontalOverflow &&\n input.scrollInlineOffset + input.viewportInlineSize < input.scrollInlineSize - 1,\n scrollable: horizontalOverflow || input.scrollBlockSize - input.viewportBlockSize > 1,\n },\n inlineOffset: input.scrollInlineOffset,\n maxInlineOffset: Math.max(0, input.scrollInlineSize - input.viewportInlineSize),\n collapseBlockOffset:\n input.collapseHeadBlockStart == null || input.collapseFrameBlockStart == null\n ? null\n : Math.max(0, input.collapseHeadBlockStart - input.collapseFrameBlockStart),\n };\n}\n\n/**\n * Owns table viewport observation. It resolves newly rendered geometry\n * immediately so the first paint has the correct viewport-width chrome, then\n * batches observer-driven scroll and resize reads/writes into one animation frame.\n * The component remains responsible only for refs and reactive overflow state.\n */\nexport class TableLayoutController {\n private connected = false;\n private connectedViewport: HTMLElement | null = null;\n private connectedContentTable: HTMLTableElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private frame: number | null = null;\n private overflow: TableOverflowState | null = null;\n private inlineOffset = 0;\n private maxInlineOffset = 0;\n private maxBlockOffset = 0;\n private lastMode: TableLayoutMode | null = null;\n\n constructor(private readonly options: TableLayoutControllerOptions) {}\n\n connect(): void {\n this.connected = true;\n this.refresh();\n }\n\n disconnect(): void {\n this.connected = false;\n if (this.frame !== null) cancelAnimationFrame(this.frame);\n this.frame = null;\n this.connectedViewport?.removeEventListener('scroll', this.handleScroll);\n this.connectedViewport?.removeEventListener('wheel', this.handleWheel);\n this.connectedViewport = null;\n this.connectedContentTable = null;\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n }\n\n /** Reconcile newly rendered refs, observers, and pending geometry. */\n refresh(forceGeometry = true): void {\n if (!this.connected) return;\n const { viewport, contentTable } = this.options.elements();\n const elementsChanged =\n viewport !== this.connectedViewport || contentTable !== this.connectedContentTable;\n if (elementsChanged) {\n this.connectedViewport?.removeEventListener('scroll', this.handleScroll);\n this.connectedViewport?.removeEventListener('wheel', this.handleWheel);\n this.resizeObserver?.disconnect();\n this.connectedViewport = viewport;\n this.connectedContentTable = contentTable;\n viewport?.addEventListener('scroll', this.handleScroll, { passive: true });\n viewport?.addEventListener('wheel', this.handleWheel, { passive: false });\n\n if (typeof ResizeObserver !== 'undefined' && viewport) {\n this.resizeObserver ??= new ResizeObserver(this.schedule);\n this.resizeObserver.observe(viewport);\n if (contentTable) this.resizeObserver.observe(contentTable);\n }\n }\n const mode = this.options.mode();\n const modeChanged =\n !this.lastMode ||\n mode.documentStickyHeader !== this.lastMode.documentStickyHeader ||\n mode.floatingCollapseAll !== this.lastMode.floatingCollapseAll ||\n mode.clampVerticalOverscroll !== this.lastMode.clampVerticalOverscroll;\n this.lastMode = mode;\n // New refs and explicit refreshes resolve before paint. A recycled virtual\n // window keeps the same scroll geometry, so ResizeObserver owns later size\n // changes without forcing layout after every row patch.\n if (forceGeometry || elementsChanged || modeChanged) this.sync();\n }\n\n private schedule = (): void => {\n if (!this.connected || this.frame !== null) return;\n this.frame = requestAnimationFrame(() => {\n this.frame = null;\n this.sync();\n });\n };\n\n private readonly handleScroll = (): void => {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n const mode = this.options.mode();\n this.clampInlineOffset();\n if (mode.clampVerticalOverscroll) this.clampBlockOffset();\n if (\n viewport.scrollLeft !== this.inlineOffset ||\n (mode.floatingCollapseAll && !mode.documentStickyHeader)\n ) {\n this.schedule();\n }\n };\n\n private readonly handleWheel = (event: WheelEvent): void => {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n\n if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) {\n const atStart = viewport.scrollLeft <= 0 && event.deltaX < 0;\n const atEnd = viewport.scrollLeft >= this.maxInlineOffset && event.deltaX > 0;\n if (atStart || atEnd) event.preventDefault();\n return;\n }\n\n if (!this.options.mode().clampVerticalOverscroll || event.deltaY === 0) return;\n const atStart = viewport.scrollTop <= 0 && event.deltaY < 0;\n const atEnd = viewport.scrollTop >= this.maxBlockOffset && event.deltaY > 0;\n if (!atStart && !atEnd) return;\n\n this.options.verticalEdgeWheel?.(event.deltaY);\n event.preventDefault();\n };\n\n private clampInlineOffset(): void {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n const clamped = Math.min(this.maxInlineOffset, Math.max(0, viewport.scrollLeft));\n if (viewport.scrollLeft !== clamped) viewport.scrollLeft = clamped;\n }\n\n private clampBlockOffset(): void {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n const clamped = Math.min(this.maxBlockOffset, Math.max(0, viewport.scrollTop));\n if (viewport.scrollTop !== clamped) viewport.scrollTop = clamped;\n }\n\n private sync(): void {\n const elements = this.options.elements();\n const viewport = elements.viewport;\n if (!viewport) return;\n\n // Read every layout value before writing styles or reactive state.\n const mode = this.options.mode();\n const measureCollapse =\n mode.floatingCollapseAll &&\n !mode.documentStickyHeader &&\n elements.collapseAllOverlay &&\n elements.frame &&\n elements.interactiveHead;\n const metrics = resolveTableLayoutMetrics({\n viewportInlineSize: viewport.clientWidth,\n viewportBlockSize: viewport.clientHeight,\n scrollInlineSize: viewport.scrollWidth,\n scrollBlockSize: viewport.scrollHeight,\n scrollInlineOffset: viewport.scrollLeft,\n collapseHeadBlockStart: measureCollapse\n ? elements.interactiveHead!.getBoundingClientRect().top\n : undefined,\n collapseFrameBlockStart: measureCollapse\n ? elements.frame!.getBoundingClientRect().top\n : undefined,\n });\n this.inlineOffset = metrics.inlineOffset;\n this.maxInlineOffset = metrics.maxInlineOffset;\n this.maxBlockOffset = Math.max(0, viewport.scrollHeight - viewport.clientHeight);\n // Header-only geometry; independent of the loaded/recycled row count.\n const pinnedCells = elements.interactiveHead?.querySelectorAll?.<HTMLElement>(\n '.ds-table__header-cell:not(.ds-table__super-header-cell).ds-table__cell--sticky-start, .ds-table__header-cell:not(.ds-table__super-header-cell).ds-table__cell--sticky-end'\n );\n const pinnedSize = [...(pinnedCells ?? [])].reduce(\n (size, cell) => size + cell.getBoundingClientRect().width,\n 0\n );\n const crowded = pinnedSize > 0 && pinnedSize >= metrics.visibleInlineSize;\n if (elements.frame?.getAttribute('data-pins-crowded') !== String(crowded))\n elements.frame?.setAttribute('data-pins-crowded', String(crowded));\n this.setProperty(viewport, '--ds-table-visible-inline-size', `${metrics.visibleInlineSize}px`);\n if (elements.stickyHeaderTable) {\n this.setProperty(\n elements.stickyHeaderTable,\n '--ds-table-inline-scroll-offset',\n `${metrics.inlineOffset}px`\n );\n this.setProperty(\n elements.stickyHeaderTable,\n '--ds-table-inline-scroll-max-offset',\n `${metrics.maxInlineOffset}px`\n );\n }\n if (metrics.collapseBlockOffset !== null && elements.collapseAllOverlay) {\n this.setProperty(\n elements.collapseAllOverlay,\n '--ds-table-collapse-all-block-offset',\n `${metrics.collapseBlockOffset}px`\n );\n }\n\n if (\n !this.overflow ||\n this.overflow.start !== metrics.overflow.start ||\n this.overflow.end !== metrics.overflow.end ||\n this.overflow.scrollable !== metrics.overflow.scrollable\n ) {\n this.overflow = metrics.overflow;\n this.options.overflowChanged(metrics.overflow);\n }\n }\n\n private setProperty(element: HTMLElement, property: string, value: string): void {\n if (element.style.getPropertyValue(property) !== value) {\n element.style.setProperty(property, value);\n }\n }\n}\n",
1692
1732
  "type": "registry:ui"
1693
1733
  },
1694
1734
  {
@@ -1748,7 +1788,7 @@
1748
1788
  },
1749
1789
  {
1750
1790
  "path": "src/wc/components/Table/table-types.ts",
1751
- "content": "import type { DataField, DataFieldSegment } from '../../utils/data-field';\nimport type { TagContrast, TagIntent } from '../Tag/Tag';\nimport type { IconColor } from '../Icon/Icon';\nimport type { SafetyScoreLevel } from '../Score/score-types';\nimport type { TextColor } from '../Text/text-types';\nimport type { PaginationState } from '../Pagination/pagination-types';\nimport type { SelectOption } from '../Select/Select';\n\nexport type DataSortDirection = 'asc' | 'desc';\nexport type TableSelectionMode = 'none' | 'multiple';\nexport type TableCellAlign = 'start' | 'center' | 'end';\nexport type TableCellLinkTarget = '_self' | '_blank';\nexport type TableColumnSticky = 'start' | 'end';\nexport type TableCaptionVisibility = 'visible' | 'hidden';\nexport type TableLoadMoreMode = 'auto' | 'manual';\nexport type TableDataMode = 'infinite' | 'pagination' | 'virtual';\nexport interface TableDataModeChangeDetail {\n dataMode: TableDataMode;\n}\nexport type TableLoadMoreReason = 'auto' | 'manual' | 'retry';\nexport type TableColumnWidth = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n/** Visible line budget before a text track ellipsizes. */\nexport type TableCellMaxLines = 1 | 2 | 3;\n/** Resolved clamp, including unlimited wrapping. */\nexport type TableCellLineClamp = TableCellMaxLines | 'none';\n/** Semantic color for a group section header (faint surface + bold title). */\nexport type TableGroupIntent =\n | 'brand'\n | 'neutral'\n | 'negative'\n | 'warning'\n | 'caution'\n | 'positive';\n\n/** Independently sortable label within a compound column header. */\nexport interface TableHeaderSegment extends DataFieldSegment {\n /** Visible separator rendered after this label when another segment follows. */\n separator?: string;\n}\n\n/** Controlled member-row sort state. Group order is controlled separately. */\nexport interface DataSortState {\n fieldId: string;\n direction: DataSortDirection;\n}\n\n/** One controlled grouping level. Applications supply groups in their final fixed order. */\nexport interface DataGroupingState {\n fieldId: string;\n direction: DataSortDirection;\n /** Optional application-owned data point used to order the group sections. */\n orderBy?: string;\n}\n\n/** One independently colored run inside a secondary or tertiary track. */\nexport interface TableCellTextRun {\n text: string;\n /** Defaults to the track’s standard color. */\n color?: TextColor;\n /** Optional supplementary copy shown from a dotted-underlined tooltip trigger. */\n help?: string;\n}\n\n/** A secondary or tertiary track: one string, or up to three colorable runs. */\nexport type TableCellTextTrack = string | TableCellTextRun[];\n\n/** Standard two- or three-line cell content owned by the table renderer. */\nexport interface TableCellText {\n primary: string | number;\n secondary?: TableCellTextTrack;\n /** Third subdued track; ignored on dual-primary cells. */\n tertiary?: TableCellTextTrack;\n /** Optional semantic foreground for a string secondary track. */\n secondaryColor?: TextColor;\n /** Optional semantic foreground for a string tertiary track. */\n tertiaryColor?: TextColor;\n /** Application-owned URL for the primary track only. Scalars stay unlinked. */\n href?: string;\n /** Native anchor target for a resolved primary link. */\n target?: TableCellLinkTarget;\n /** Allow this cell to wrap even when its column truncates by default. */\n wrap?: boolean;\n /** Clamp wrapping text after 1, 2, or 3 lines. Overrides column wrap. */\n maxLines?: TableCellMaxLines;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/** Two-line text content where both tracks use primary body text. */\nexport interface TableCellPrimaryText {\n kind: 'primary-text';\n primary: string | number;\n secondary: string | number;\n /** Application-owned URL for the primary track only. */\n href?: string;\n /** Native anchor target for a resolved primary link. */\n target?: TableCellLinkTarget;\n /** Allow this cell to wrap even when its column truncates by default. */\n wrap?: boolean;\n /** Clamp wrapping text after 1, 2, or 3 lines. Overrides column wrap. */\n maxLines?: TableCellMaxLines;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/** Applicable data point whose value is currently unavailable. */\nexport interface TableCellEmpty {\n kind: 'empty';\n}\n\n/** Data point that is intentionally not applicable to this row. */\nexport interface TableCellBlank {\n kind: 'blank';\n}\n\n/** Declarative icon-only content rendered by the table's standard cell primitive. */\nexport interface TableCellIcon {\n kind: 'icon';\n /** Exact canonical IcoMo export name. */\n icon: string;\n color?: IconColor;\n /** Accessible label for informative icons. Omit only when the icon is decorative. */\n label?: string;\n /** Application-owned value used when this icon column participates in sorting. */\n sortValue?: string | number | boolean;\n}\n\n/**\n * Prefix icon beside a text copy stack. Independent of icon-only and plain\n * text cells so those recipes stay unchanged.\n */\nexport interface TableCellIconText {\n kind: 'icon-text';\n /** Exact canonical IcoMo export name. */\n icon: string;\n iconColor?: IconColor;\n /** Accessible name. Omit when the glyph is decorative. */\n iconLabel?: string;\n primary: string | number;\n secondary?: TableCellTextTrack;\n tertiary?: TableCellTextTrack;\n secondaryColor?: TextColor;\n tertiaryColor?: TextColor;\n href?: string;\n target?: TableCellLinkTarget;\n wrap?: boolean;\n maxLines?: TableCellMaxLines;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/**\n * Compact safety-score fill at the top-right of a text copy stack. The score\n * occupies one 24px track while the copy keeps the standard text-cell anatomy.\n */\nexport interface TableCellScoreText {\n kind: 'score-text';\n /** Display-ready safety score. Numeric values infer their semantic level. */\n score: string | number;\n scoreLevel?: SafetyScoreLevel;\n /** Accessible name prefix. Defaults to “Safety score”. */\n scoreLabel?: string;\n /** Replace the score figure with its matching sm skeleton. */\n scoreLoading?: boolean;\n primary: string | number;\n secondary?: TableCellTextTrack;\n tertiary?: TableCellTextTrack;\n secondaryColor?: TextColor;\n tertiaryColor?: TextColor;\n href?: string;\n target?: TableCellLinkTarget;\n wrap?: boolean;\n maxLines?: TableCellMaxLines;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/** Body-row track stack an image preview occupies. */\nexport type TableCellImageTracks = 1 | 2 | 3;\n\n/** Declarative 16:9 image preview rendered by the table's standard cell primitive. */\nexport interface TableCellImage {\n kind: 'image';\n /** Optional image source. Omit it to render the standard bordered placeholder. */\n src?: string;\n /** Accessible description for either the image or its placeholder. */\n alt: string;\n /**\n * Which body-row contract the preview fills. The thumbnail height is that\n * cell height minus 8px padding; width follows 16:9. Defaults to 1.\n */\n tracks?: TableCellImageTracks;\n}\n\ninterface TableCellActionBase {\n kind: 'action';\n /** Add the ButtonUnfilled resting border. Action cells are unbordered by default. */\n hasBorder?: boolean;\n isInactive?: boolean;\n isLoading?: boolean;\n}\n\n/** One command inside an overflow action menu. */\nexport interface TableCellActionMenuItem {\n /** Stable application-owned action identity emitted with dsCellAction. */\n actionId: string;\n label: string;\n isInactive?: boolean;\n isDestructive?: boolean;\n}\n\n/** Visual separator between overflow action-menu commands. */\nexport interface TableCellActionDivider {\n kind: 'divider';\n}\n\nexport type TableCellActionMenuEntry = TableCellActionMenuItem | TableCellActionDivider;\n\n/** Declarative ButtonUnfilled content rendered by the table's standard action cell. */\nexport type TableCellAction = TableCellActionBase &\n (\n | {\n variant?: 'label';\n /** Stable application-owned action identity emitted with dsCellAction. */\n actionId: string;\n label: string;\n icon?: never;\n ariaLabel?: string;\n items?: never;\n }\n | {\n variant: 'icon-label';\n actionId: string;\n label: string;\n icon: string;\n ariaLabel?: string;\n items?: never;\n }\n | {\n variant: 'icon';\n actionId: string;\n label?: never;\n icon: string;\n ariaLabel: string;\n items?: never;\n }\n | {\n variant?: 'icon';\n /** Overflow commands. The Ellipses trigger opens ds-menu instead of emitting on click. */\n items: TableCellActionMenuEntry[];\n ariaLabel: string;\n icon?: string;\n actionId?: never;\n label?: never;\n }\n );\n\n/** Declarative Tag content rendered by the table's standard cell primitive. */\nexport type TableCellTagVariant = 'tag-only' | 'tag-with-text' | 'text-with-tag';\n\n/** One Tag inside a single- or multiple-Tag table cell. */\nexport interface TableCellTagItem {\n label: string;\n intent?: TagIntent;\n contrast?: TagContrast;\n icon?: string;\n rounded?: boolean;\n}\n\nexport type TableCellTag = TableCellTagItem & { kind: 'tag' } & (\n | {\n variant?: 'tag-only';\n text?: never;\n }\n | {\n variant: Exclude<TableCellTagVariant, 'tag-only'>;\n text: string;\n }\n );\n\n/** Multiple Tags in wrapping tracks or a compact slash-separated primary track. */\nexport type TableCellTags = { kind: 'tags' } & (\n | {\n variant?: 'wrap';\n items: TableCellTagItem[];\n /** Expected positive wrapped-line count used for row geometry and virtual estimates. */\n tracks: number;\n text?: never;\n }\n | {\n variant: 'inline';\n /** One to three tags. Runtime inputs beyond three are capped. */\n items:\n | [TableCellTagItem]\n | [TableCellTagItem, TableCellTagItem]\n | [TableCellTagItem, TableCellTagItem, TableCellTagItem];\n /** Optional secondary text beneath the non-wrapping tag track. */\n text?: string;\n tracks?: never;\n }\n);\n\nexport type TableCellValue =\n | string\n | number\n | null\n | undefined\n | TableCellText\n | TableCellPrimaryText\n | TableCellTag\n | TableCellTags\n | TableCellIcon\n | TableCellIconText\n | TableCellScoreText\n | TableCellImage\n | TableCellAction\n | TableCellEmpty\n | TableCellBlank;\n\nexport type TableSkeletonWidth = string | number;\n\n/** Representative loading geometry for one table column. */\nexport type TableCellSkeleton =\n | {\n kind: 'text';\n /** One track for scalar content, two for primary and secondary, three when tertiary is present. */\n lines?: 1 | 2 | 3;\n primaryWidth?: TableSkeletonWidth;\n secondaryWidth?: TableSkeletonWidth;\n tertiaryWidth?: TableSkeletonWidth;\n }\n | {\n kind: 'image';\n /** Match the loaded image cell's track stack. Defaults to 1. */\n tracks?: TableCellImageTracks;\n }\n | {\n kind: 'tag';\n width?: TableSkeletonWidth;\n }\n | {\n kind: 'icon';\n rounded?: boolean;\n }\n | {\n kind: 'icon-text';\n lines?: 1 | 2 | 3;\n primaryWidth?: TableSkeletonWidth;\n secondaryWidth?: TableSkeletonWidth;\n tertiaryWidth?: TableSkeletonWidth;\n }\n | {\n kind: 'score-text';\n lines?: 1 | 2 | 3;\n primaryWidth?: TableSkeletonWidth;\n secondaryWidth?: TableSkeletonWidth;\n tertiaryWidth?: TableSkeletonWidth;\n }\n | {\n kind: 'action';\n /** Icon actions use a square control canvas; labeled actions may provide a wider width. */\n variant?: 'icon' | 'label' | 'icon-label';\n width?: TableSkeletonWidth;\n }\n | {\n kind: 'blank';\n };\n\n/** Scalar editors compose the shared borderless controls; the cell owns interaction chrome. */\nexport type TableCellEditor = { required?: boolean } & (\n | { type: 'text' | 'textarea'; minLength?: number; maxLength?: number }\n | { type: 'number'; min?: number; max?: number; step?: number; showStepper?: boolean }\n | { type: 'select'; options: SelectOption[]; searchable?: boolean }\n | { type: 'date'; min?: string; max?: string }\n | { type: 'time'; min?: string; max?: string; step?: number; hourFormat?: '12' | '24' }\n);\n\nexport interface TableColumn extends DataField {\n /** Optional super-header identity and label. Adjacent matching identities form one band. */\n group?: { id: string; label: string };\n /** Opt-in scalar editor used in edit (pencil) or grid (worksheet) interaction modes. */\n editor?: TableCellEditor;\n segments?: TableHeaderSegment[];\n /** Supplementary header help. Does not replace the visible or accessible column name. */\n help?: string;\n align?: TableCellAlign;\n /** Preferred TokoMo table-column width. Numbers remain available for exceptional custom pixel widths. */\n size?: TableColumnWidth | number;\n /** Derive a fixed image-column width from the matching 1, 2, or 3 track cell geometry. Ignored when size is set. */\n imageTracks?: TableCellImageTracks;\n /** Minimum accepted preferred width in CSS pixels. */\n minSize?: number;\n /** Maximum accepted preferred width in CSS pixels. */\n maxSize?: number;\n /** Wrap cell text instead of truncating it to one line. */\n wrap?: boolean;\n /** Clamp wrapping text after 1, 2, or 3 lines. Omit with wrap for unlimited wrap. */\n maxLines?: TableCellMaxLines;\n /** Pin an explicitly sized column. Visible pinned columns accumulate in display order. */\n sticky?: TableColumnSticky;\n /**\n * Declare a trailing application-action lane. Action columns keep fixed\n * geometry and may host grouped table-level controls in their header.\n */\n kind?: 'data' | 'action';\n /** Representative loading geometry for this column. Defaults to one text line. */\n skeleton?: TableCellSkeleton;\n}\n\nexport interface TableRow {\n /** Stable identity, unique across all groups. */\n id: string;\n cells: Record<string, TableCellValue>;\n /** Presentation is separate from cell values; omitted alignment preserves first-track alignment. */\n cellPresentation?: Record<string, TableCellDecoration>;\n /** Accessible row name used by its selection control. Falls back to the first cell. */\n selectionLabel?: string;\n /** Exclude this row from selection without muting its content. */\n selectable?: boolean;\n /** Keep the row visible while disabling its selection control and styling it as inactive. */\n disabled?: boolean;\n /** Make the complete row a keyboard- and pointer-activatable application target. */\n interactive?: boolean;\n}\n\nexport interface TableCellDecoration {\n verticalAlign?: 'top' | 'middle' | 'bottom';\n /** Left-edge intent outline with an accessible, non-color-only explanation. */\n highlight?: { intent: TableGroupIntent; label: string };\n borders?: Array<'start' | 'end' | 'top' | 'bottom'>;\n flag?: { label: string; icon?: string };\n}\n\n/** A rectangular merge anchored at the first row/column. Covered values remain in application data. */\nexport interface TableCellSpan {\n rowIds: string[];\n columnIds: string[];\n}\n\nexport interface TableCellAddress {\n rowId: string;\n columnId: string;\n}\n\nexport interface TableCellRange {\n anchor: TableCellAddress;\n focus: TableCellAddress;\n}\n\nexport interface TableCellsChangeDetail {\n changes: Array<TableCellAddress & { value: string | number | null }>;\n reason: 'edit' | 'paste';\n}\n\n/** Supporting copy on a group section header's second track. */\nexport interface TableGroupAccessory {\n text: string;\n /** Optional tooltip. Uses the shared dotted underline when present. */\n help?: string;\n}\n\n/** Leading group-header accessory. Score is the first supported hero. */\nexport interface TableGroupHeroScore {\n kind: 'score';\n value: string | number;\n level?: SafetyScoreLevel;\n /** Span one label track by default, or center a larger score across two tracks. */\n tracks?: 1 | 2;\n /** Accessible name prefix, for example “Safety score”. */\n label?: string;\n isLoading?: boolean;\n}\n\nexport type TableGroupHero = TableGroupHeroScore;\n\nexport interface DataGroup {\n /** Stable group identity. */\n id: string;\n label: string;\n value?: string | number;\n /** Total server-side member count; may exceed the currently loaded row count. */\n totalCount?: number;\n /** Optional localized total-count phrase such as “166 events” for accessible loaded progress. */\n countLabel?: string;\n /**\n * Optional semantic intent for the group section header. Applies a faint\n * intent background and bold intent title color.\n */\n intent?: TableGroupIntent;\n /**\n * Optional accessory copy on a second 24px track under the label and count.\n * Middle-dot separated. At most four items are shown.\n */\n accessories?: TableGroupAccessory[];\n /**\n * Optional leading hero before the label and count. A score uses sm on both\n * single-track and two-track headers and sits at the start of the copy stack.\n */\n hero?: TableGroupHero;\n /** Whether this group has more member rows available to load. */\n hasMore?: boolean;\n /** Controlled incremental loading state for this group's member rows. */\n loadingMore?: boolean;\n /** Controlled incremental loading failure for this group's member rows. */\n loadMoreError?: string;\n /** Reset key for this group's query, sort, or filter state. Defaults to the group ID. */\n loadIdentity?: string | number;\n rows: TableRow[];\n}\n\nexport interface DataSortChangeDetail {\n sort: DataSortState | null;\n}\n\nexport interface TableSelectionChangeDetail {\n selectedRowIds: string[];\n scope: 'row' | 'all-loaded' | 'group';\n changedRowId?: string;\n /** Present when scope is `group`. */\n groupId?: string;\n selected?: boolean;\n}\n\nexport interface TableLoadMoreDetail {\n reason: TableLoadMoreReason;\n loadIdentity: string | number;\n loadedRowCount: number;\n}\n\nexport interface TableGroupLoadMoreDetail extends TableLoadMoreDetail {\n groupId: string;\n}\n\nexport interface TableCellActionDetail {\n actionId: string;\n rowId: string;\n columnId: string;\n}\n\nexport interface TableGroupCollapseChangeDetail {\n scope: 'group' | 'all';\n /** Present when a single group was toggled. */\n groupId?: string;\n collapsed: boolean;\n collapsedGroupIds: string[];\n}\n\nexport interface TableRowActivateDetail {\n rowId: string;\n}\n\n/** Controlled show/hide and data-column order for the table-owned customizer. */\nexport interface DataFieldsConfigChangeDetail {\n hiddenFieldIds: string[];\n fieldOrder: string[];\n}\n\n/** Controlled top-level pagination state. Rows or groups contain only the active page. */\nexport interface TablePaginationState extends PaginationState {\n /** Change to request one fresh fitted-capacity snapshot for the current query shape. */\n fitIdentity?: string | number;\n}\n",
1791
+ "content": "import type { DataField, DataFieldSegment } from '../../utils/data-field';\nimport type { TagContrast, TagIntent } from '../Tag/Tag';\nimport type { IconColor } from '../Icon/Icon';\nimport type { SafetyScoreLevel } from '../Score/score-types';\nimport type { TextColor } from '../Text/text-types';\nimport type { PaginationState } from '../Pagination/pagination-types';\nimport type { SelectOption } from '../Select/Select';\n\nexport type DataSortDirection = 'asc' | 'desc';\nexport type TableSelectionMode = 'none' | 'multiple';\nexport type TableCellAlign = 'start' | 'center' | 'end';\nexport type TableCellLinkTarget = '_self' | '_blank';\nexport type TableColumnSticky = 'start' | 'end';\nexport type TableCaptionVisibility = 'visible' | 'hidden';\nexport type TableChromeLayout = 'contained' | 'edge-to-edge';\nexport type TableLoadMoreMode = 'auto' | 'manual';\nexport type TableDataMode = 'infinite' | 'pagination' | 'virtual';\nexport interface TableDataModeChangeDetail {\n dataMode: TableDataMode;\n}\nexport type TableLoadMoreReason = 'auto' | 'manual' | 'retry';\nexport type TableColumnWidth = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n/** Visible line budget before a text track ellipsizes. */\nexport type TableCellMaxLines = 1 | 2 | 3;\n/** Resolved clamp, including unlimited wrapping. */\nexport type TableCellLineClamp = TableCellMaxLines | 'none';\n/** Semantic color for a group section header (faint surface + bold title). */\nexport type TableGroupIntent =\n | 'brand'\n | 'neutral'\n | 'negative'\n | 'warning'\n | 'caution'\n | 'positive';\n\n/** Independently sortable label within a compound column header. */\nexport interface TableHeaderSegment extends DataFieldSegment {\n /** Visible separator rendered after this label when another segment follows. */\n separator?: string;\n}\n\n/** Controlled member-row sort state. Group order is controlled separately. */\nexport interface DataSortState {\n fieldId: string;\n direction: DataSortDirection;\n}\n\n/** One controlled grouping level. Applications supply groups in their final fixed order. */\nexport interface DataGroupingState {\n fieldId: string;\n direction: DataSortDirection;\n /** Optional application-owned data point used to order the group sections. */\n orderBy?: string;\n}\n\n/** One independently colored run inside a secondary or tertiary track. */\nexport interface TableCellTextRun {\n text: string;\n /** Defaults to the track’s standard color. */\n color?: TextColor;\n /** Optional supplementary copy shown from a dotted-underlined tooltip trigger. */\n help?: string;\n}\n\n/** A secondary or tertiary track: one string, or up to three colorable runs. */\nexport type TableCellTextTrack = string | TableCellTextRun[];\n\n/** Standard two- or three-line cell content owned by the table renderer. */\nexport interface TableCellText {\n primary: string | number;\n secondary?: TableCellTextTrack;\n /** Third subdued track; ignored on dual-primary cells. */\n tertiary?: TableCellTextTrack;\n /** Optional semantic foreground for a string secondary track. */\n secondaryColor?: TextColor;\n /** Optional semantic foreground for a string tertiary track. */\n tertiaryColor?: TextColor;\n /** Application-owned URL for the primary track only. Scalars stay unlinked. */\n href?: string;\n /** Native anchor target for a resolved primary link. */\n target?: TableCellLinkTarget;\n /** Allow this cell to wrap even when its column truncates by default. */\n wrap?: boolean;\n /** Clamp wrapping text after 1, 2, or 3 lines. Overrides column wrap. */\n maxLines?: TableCellMaxLines;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/** Two-line text content where both tracks use primary body text. */\nexport interface TableCellPrimaryText {\n kind: 'primary-text';\n primary: string | number;\n secondary: string | number;\n /** Application-owned URL for the primary track only. */\n href?: string;\n /** Native anchor target for a resolved primary link. */\n target?: TableCellLinkTarget;\n /** Allow this cell to wrap even when its column truncates by default. */\n wrap?: boolean;\n /** Clamp wrapping text after 1, 2, or 3 lines. Overrides column wrap. */\n maxLines?: TableCellMaxLines;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/** Applicable data point whose value is currently unavailable. */\nexport interface TableCellEmpty {\n kind: 'empty';\n}\n\n/** Data point that is intentionally not applicable to this row. */\nexport interface TableCellBlank {\n kind: 'blank';\n}\n\n/** Declarative icon-only content rendered by the table's standard cell primitive. */\nexport interface TableCellIcon {\n kind: 'icon';\n /** Exact canonical IcoMo export name. */\n icon: string;\n color?: IconColor;\n /** Accessible label for informative icons. Omit only when the icon is decorative. */\n label?: string;\n /** Application-owned value used when this icon column participates in sorting. */\n sortValue?: string | number | boolean;\n}\n\n/**\n * Prefix icon beside a text copy stack. Independent of icon-only and plain\n * text cells so those recipes stay unchanged.\n */\nexport interface TableCellIconText {\n kind: 'icon-text';\n /** Exact canonical IcoMo export name. */\n icon: string;\n iconColor?: IconColor;\n /** Accessible name. Omit when the glyph is decorative. */\n iconLabel?: string;\n primary: string | number;\n secondary?: TableCellTextTrack;\n tertiary?: TableCellTextTrack;\n secondaryColor?: TextColor;\n tertiaryColor?: TextColor;\n href?: string;\n target?: TableCellLinkTarget;\n wrap?: boolean;\n maxLines?: TableCellMaxLines;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/**\n * Compact safety-score fill at the top-right of a text copy stack. The score\n * occupies one 24px track while the copy keeps the standard text-cell anatomy.\n */\nexport interface TableCellScoreText {\n kind: 'score-text';\n /** Display-ready safety score. Numeric values infer their semantic level. */\n score: string | number;\n scoreLevel?: SafetyScoreLevel;\n /** Accessible name prefix. Defaults to “Safety score”. */\n scoreLabel?: string;\n /** Replace the score figure with its matching sm skeleton. */\n scoreLoading?: boolean;\n primary: string | number;\n secondary?: TableCellTextTrack;\n tertiary?: TableCellTextTrack;\n secondaryColor?: TextColor;\n tertiaryColor?: TextColor;\n href?: string;\n target?: TableCellLinkTarget;\n wrap?: boolean;\n maxLines?: TableCellMaxLines;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/** Body-row track stack an image preview occupies. */\nexport type TableCellImageTracks = 1 | 2 | 3;\n\n/** Declarative 16:9 image preview rendered by the table's standard cell primitive. */\nexport interface TableCellImage {\n kind: 'image';\n /** Optional image source. Omit it to render the standard bordered placeholder. */\n src?: string;\n /** Accessible description for either the image or its placeholder. */\n alt: string;\n /**\n * Which body-row contract the preview fills. The thumbnail height is that\n * cell height minus 8px padding; width follows 16:9. Defaults to 1.\n */\n tracks?: TableCellImageTracks;\n}\n\ninterface TableCellActionBase {\n kind: 'action';\n /** Add the ButtonUnfilled resting border. Action cells are unbordered by default. */\n hasBorder?: boolean;\n isInactive?: boolean;\n isLoading?: boolean;\n}\n\n/** One command inside an overflow action menu. */\nexport interface TableCellActionMenuItem {\n /** Stable application-owned action identity emitted with dsCellAction. */\n actionId: string;\n label: string;\n isInactive?: boolean;\n isDestructive?: boolean;\n}\n\n/** Visual separator between overflow action-menu commands. */\nexport interface TableCellActionDivider {\n kind: 'divider';\n}\n\nexport type TableCellActionMenuEntry = TableCellActionMenuItem | TableCellActionDivider;\n\n/** Declarative ButtonUnfilled content rendered by the table's standard action cell. */\nexport type TableCellAction = TableCellActionBase &\n (\n | {\n variant?: 'label';\n /** Stable application-owned action identity emitted with dsCellAction. */\n actionId: string;\n label: string;\n icon?: never;\n ariaLabel?: string;\n items?: never;\n }\n | {\n variant: 'icon-label';\n actionId: string;\n label: string;\n icon: string;\n ariaLabel?: string;\n items?: never;\n }\n | {\n variant: 'icon';\n actionId: string;\n label?: never;\n icon: string;\n ariaLabel: string;\n items?: never;\n }\n | {\n variant?: 'icon';\n /** Overflow commands. The Ellipses trigger opens ds-menu instead of emitting on click. */\n items: TableCellActionMenuEntry[];\n ariaLabel: string;\n icon?: string;\n actionId?: never;\n label?: never;\n }\n );\n\n/** Declarative Tag content rendered by the table's standard cell primitive. */\nexport type TableCellTagVariant = 'tag-only' | 'tag-with-text' | 'text-with-tag';\n\n/** One Tag inside a single- or multiple-Tag table cell. */\nexport interface TableCellTagItem {\n label: string;\n intent?: TagIntent;\n contrast?: TagContrast;\n icon?: string;\n rounded?: boolean;\n}\n\nexport type TableCellTag = TableCellTagItem & { kind: 'tag' } & (\n | {\n variant?: 'tag-only';\n text?: never;\n }\n | {\n variant: Exclude<TableCellTagVariant, 'tag-only'>;\n text: string;\n }\n );\n\n/** Multiple Tags in wrapping tracks or a compact slash-separated primary track. */\nexport type TableCellTags = { kind: 'tags' } & (\n | {\n variant?: 'wrap';\n items: TableCellTagItem[];\n /** Expected positive wrapped-line count used for row geometry and virtual estimates. */\n tracks: number;\n text?: never;\n }\n | {\n variant: 'inline';\n /** One to three tags. Runtime inputs beyond three are capped. */\n items:\n | [TableCellTagItem]\n | [TableCellTagItem, TableCellTagItem]\n | [TableCellTagItem, TableCellTagItem, TableCellTagItem];\n /** Optional secondary text beneath the non-wrapping tag track. */\n text?: string;\n tracks?: never;\n }\n);\n\nexport type TableCellValue =\n | string\n | number\n | null\n | undefined\n | TableCellText\n | TableCellPrimaryText\n | TableCellTag\n | TableCellTags\n | TableCellIcon\n | TableCellIconText\n | TableCellScoreText\n | TableCellImage\n | TableCellAction\n | TableCellEmpty\n | TableCellBlank;\n\nexport type TableSkeletonWidth = string | number;\n\n/** Representative loading geometry for one table column. */\nexport type TableCellSkeleton =\n | {\n kind: 'text';\n /** One track for scalar content, two for primary and secondary, three when tertiary is present. */\n lines?: 1 | 2 | 3;\n primaryWidth?: TableSkeletonWidth;\n secondaryWidth?: TableSkeletonWidth;\n tertiaryWidth?: TableSkeletonWidth;\n }\n | {\n kind: 'image';\n /** Match the loaded image cell's track stack. Defaults to 1. */\n tracks?: TableCellImageTracks;\n }\n | {\n kind: 'tag';\n width?: TableSkeletonWidth;\n }\n | {\n kind: 'icon';\n rounded?: boolean;\n }\n | {\n kind: 'icon-text';\n lines?: 1 | 2 | 3;\n primaryWidth?: TableSkeletonWidth;\n secondaryWidth?: TableSkeletonWidth;\n tertiaryWidth?: TableSkeletonWidth;\n }\n | {\n kind: 'score-text';\n lines?: 1 | 2 | 3;\n primaryWidth?: TableSkeletonWidth;\n secondaryWidth?: TableSkeletonWidth;\n tertiaryWidth?: TableSkeletonWidth;\n }\n | {\n kind: 'action';\n /** Icon actions use a square control canvas; labeled actions may provide a wider width. */\n variant?: 'icon' | 'label' | 'icon-label';\n width?: TableSkeletonWidth;\n }\n | {\n kind: 'blank';\n };\n\n/** Scalar editors compose the shared borderless controls; the cell owns interaction chrome. */\nexport type TableCellEditor = { required?: boolean } & (\n | { type: 'text' | 'textarea'; minLength?: number; maxLength?: number }\n | { type: 'number'; min?: number; max?: number; step?: number; showStepper?: boolean }\n | { type: 'select'; options: SelectOption[]; searchable?: boolean }\n | { type: 'date'; min?: string; max?: string }\n | { type: 'time'; min?: string; max?: string; step?: number; hourFormat?: '12' | '24' }\n);\n\nexport interface TableColumn extends DataField {\n /** Optional super-header identity and label. Adjacent matching identities form one band. */\n group?: { id: string; label: string };\n /** Opt-in scalar editor used in edit (pencil) or grid (worksheet) interaction modes. */\n editor?: TableCellEditor;\n segments?: TableHeaderSegment[];\n /** Supplementary header help. Does not replace the visible or accessible column name. */\n help?: string;\n align?: TableCellAlign;\n /** Preferred TokoMo table-column width. Numbers remain available for exceptional custom pixel widths. */\n size?: TableColumnWidth | number;\n /** Derive a fixed image-column width from the matching 1, 2, or 3 track cell geometry. Ignored when size is set. */\n imageTracks?: TableCellImageTracks;\n /** Minimum accepted preferred width in CSS pixels. */\n minSize?: number;\n /** Maximum accepted preferred width in CSS pixels. */\n maxSize?: number;\n /** Wrap cell text instead of truncating it to one line. */\n wrap?: boolean;\n /** Clamp wrapping text after 1, 2, or 3 lines. Omit with wrap for unlimited wrap. */\n maxLines?: TableCellMaxLines;\n /** Pin an explicitly sized column. Visible pinned columns accumulate in display order. */\n sticky?: TableColumnSticky;\n /**\n * Declare a trailing application-action lane. Action columns keep fixed\n * geometry and may host grouped table-level controls in their header.\n */\n kind?: 'data' | 'action';\n /** Representative loading geometry for this column. Defaults to one text line. */\n skeleton?: TableCellSkeleton;\n}\n\nexport interface TableRow {\n /** Stable identity, unique across all groups. */\n id: string;\n cells: Record<string, TableCellValue>;\n /** Presentation is separate from cell values; omitted alignment preserves first-track alignment. */\n cellPresentation?: Record<string, TableCellDecoration>;\n /** Accessible row name used by its selection control. Falls back to the first cell. */\n selectionLabel?: string;\n /** Exclude this row from selection without muting its content. */\n selectable?: boolean;\n /** Keep the row visible while disabling its selection control and styling it as inactive. */\n disabled?: boolean;\n /** Make the complete row a keyboard- and pointer-activatable application target. */\n interactive?: boolean;\n}\n\nexport interface TableCellDecoration {\n verticalAlign?: 'top' | 'middle' | 'bottom';\n /** Left-edge intent outline with an accessible, non-color-only explanation. */\n highlight?: { intent: TableGroupIntent; label: string };\n borders?: Array<'start' | 'end' | 'top' | 'bottom'>;\n flag?: { label: string; icon?: string };\n}\n\n/** A rectangular merge anchored at the first row/column. Covered values remain in application data. */\nexport interface TableCellSpan {\n rowIds: string[];\n columnIds: string[];\n}\n\nexport interface TableCellAddress {\n rowId: string;\n columnId: string;\n}\n\nexport interface TableCellRange {\n anchor: TableCellAddress;\n focus: TableCellAddress;\n}\n\nexport interface TableCellsChangeDetail {\n changes: Array<TableCellAddress & { value: string | number | null }>;\n reason: 'edit' | 'paste';\n}\n\n/** Supporting copy on a group section header's second track. */\nexport interface TableGroupAccessory {\n text: string;\n /** Optional tooltip. Uses the shared dotted underline when present. */\n help?: string;\n}\n\n/** Leading group-header accessory. Score is the first supported hero. */\nexport interface TableGroupHeroScore {\n kind: 'score';\n value: string | number;\n level?: SafetyScoreLevel;\n /** Span one label track by default, or center a larger score across two tracks. */\n tracks?: 1 | 2;\n /** Accessible name prefix, for example “Safety score”. */\n label?: string;\n isLoading?: boolean;\n}\n\nexport type TableGroupHero = TableGroupHeroScore;\n\nexport interface DataGroup {\n /** Stable group identity. */\n id: string;\n label: string;\n value?: string | number;\n /** Total server-side member count; may exceed the currently loaded row count. */\n totalCount?: number;\n /** Optional localized total-count phrase such as “166 events” for accessible loaded progress. */\n countLabel?: string;\n /**\n * Optional semantic intent for the group section header. Applies a faint\n * intent background and bold intent title color.\n */\n intent?: TableGroupIntent;\n /**\n * Optional accessory copy on a second 24px track under the label and count.\n * Middle-dot separated. At most four items are shown.\n */\n accessories?: TableGroupAccessory[];\n /**\n * Optional leading hero before the label and count. A score uses sm on both\n * single-track and two-track headers and sits at the start of the copy stack.\n */\n hero?: TableGroupHero;\n /** Whether this group has more member rows available to load. */\n hasMore?: boolean;\n /** Controlled incremental loading state for this group's member rows. */\n loadingMore?: boolean;\n /** Controlled incremental loading failure for this group's member rows. */\n loadMoreError?: string;\n /** Reset key for this group's query, sort, or filter state. Defaults to the group ID. */\n loadIdentity?: string | number;\n rows: TableRow[];\n}\n\nexport interface DataSortChangeDetail {\n sort: DataSortState | null;\n}\n\nexport interface TableSelectionChangeDetail {\n selectedRowIds: string[];\n scope: 'row' | 'all-loaded' | 'group';\n changedRowId?: string;\n /** Present when scope is `group`. */\n groupId?: string;\n selected?: boolean;\n}\n\nexport interface TableLoadMoreDetail {\n reason: TableLoadMoreReason;\n loadIdentity: string | number;\n loadedRowCount: number;\n}\n\nexport interface TableGroupLoadMoreDetail extends TableLoadMoreDetail {\n groupId: string;\n}\n\nexport interface TableCellActionDetail {\n actionId: string;\n rowId: string;\n columnId: string;\n}\n\nexport interface TableGroupCollapseChangeDetail {\n scope: 'group' | 'all';\n /** Present when a single group was toggled. */\n groupId?: string;\n collapsed: boolean;\n collapsedGroupIds: string[];\n}\n\nexport interface TableRowActivateDetail {\n rowId: string;\n}\n\n/** Controlled visibility, display order, and user pinning for the table-owned customizer. */\nexport interface DataFieldsConfigChangeDetail {\n hiddenFieldIds: string[];\n fieldOrder: string[];\n pinnedFieldIds: string[];\n}\n\n/** Controlled top-level pagination state. Rows or groups contain only the active page. */\nexport interface TablePaginationState extends PaginationState {\n /** Change to request one fresh fitted-capacity snapshot for the current query shape. */\n fitIdentity?: string | number;\n}\n",
1752
1792
  "type": "registry:ui"
1753
1793
  },
1754
1794
  {