@ds-mo/ui 14.17.1 → 14.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/.package-ready.json +1 -1
- package/dist/agent-patterns.json +9 -8
- package/dist/agent.json +103 -45
- package/dist/angular/ds-button-filled.d.ts +1 -1
- package/dist/angular/ds-button-filled.js +3 -3
- package/dist/angular/ds-button-unfilled.d.ts +1 -1
- package/dist/angular/ds-button-unfilled.js +3 -3
- package/dist/angular/ds-filter-menu.d.ts +1 -1
- package/dist/angular/ds-filter-menu.js +3 -3
- package/dist/angular/ds-input.d.ts +1 -1
- package/dist/angular/ds-input.js +3 -3
- package/dist/angular/ds-menu.d.ts +1 -1
- package/dist/angular/ds-menu.js +3 -3
- package/dist/angular/ds-mobile-bar-nav.d.ts +1 -1
- package/dist/angular/ds-mobile-bar-nav.js +3 -3
- package/dist/angular/ds-panel-tool-header.d.ts +3 -8
- package/dist/angular/ds-panel-tools.d.ts +3 -8
- package/dist/angular/ds-select.d.ts +1 -1
- package/dist/angular/ds-select.js +3 -3
- package/dist/angular/ds-tooltip.d.ts +1 -1
- package/dist/angular/ds-tooltip.js +3 -3
- package/dist/components/ds-agent-activity.js +1 -1
- package/dist/components/ds-agent-questionnaire.js +1 -1
- package/dist/components/ds-agent-response.js +1 -1
- package/dist/components/ds-agent-source-list.js +1 -1
- package/dist/components/ds-agent-tool-call.js +1 -1
- package/dist/components/ds-attachment-list.js +1 -1
- package/dist/components/ds-badge.js +1 -1
- package/dist/components/ds-banner.js +1 -1
- package/dist/components/ds-banner.js.map +1 -1
- package/dist/components/ds-bar-action.js +1 -1
- package/dist/components/ds-bar-action.js.map +1 -1
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-title.js +1 -1
- package/dist/components/ds-bar-workflow.js +1 -1
- package/dist/components/ds-breadcrumb.js +1 -1
- package/dist/components/ds-button-filled.js +1 -1
- package/dist/components/ds-button-unfilled.js +1 -1
- package/dist/components/ds-card-chart.js +1 -1
- package/dist/components/ds-card-chart.js.map +1 -1
- package/dist/components/ds-card-overview.js +1 -1
- package/dist/components/ds-card-setting.js +1 -1
- package/dist/components/ds-card-setting.js.map +1 -1
- package/dist/components/ds-chart-legend.js +1 -1
- package/dist/components/ds-chart.js +1 -1
- package/dist/components/ds-checkbox.js +1 -1
- package/dist/components/ds-chip.js +1 -1
- package/dist/components/ds-code-block.js +1 -1
- package/dist/components/ds-conversation-list-item.js +1 -1
- package/dist/components/ds-conversation-list-section.js +1 -1
- package/dist/components/ds-conversation-list.js +1 -1
- package/dist/components/ds-conversation-list.js.map +1 -1
- package/dist/components/ds-empty-state.js +1 -1
- package/dist/components/ds-field.js +1 -1
- package/dist/components/ds-filter-menu.js +1 -1
- package/dist/components/ds-input.js +1 -1
- package/dist/components/ds-markdown.js +1 -1
- package/dist/components/ds-menu.js +1 -1
- package/dist/components/ds-message-actions.js +1 -1
- package/dist/components/ds-message-actions.js.map +1 -1
- package/dist/components/ds-message-composer.js +1 -1
- package/dist/components/ds-message-composer.js.map +1 -1
- package/dist/components/ds-message-scroller.js +1 -1
- package/dist/components/ds-message.js +1 -1
- package/dist/components/ds-mobile-bar-nav.js +1 -1
- package/dist/components/ds-mobile-header.js +1 -1
- package/dist/components/ds-mobile-section-switcher.js +1 -1
- package/dist/components/ds-mobile-sheet-nav.js +1 -1
- package/dist/components/ds-modal.js +1 -1
- package/dist/components/ds-pagination.js +1 -1
- package/dist/components/ds-panel-nav.js +1 -1
- package/dist/components/ds-panel-sub-nav.js +1 -1
- package/dist/components/ds-panel-tool-header.js +1 -1
- package/dist/components/ds-panel-tool-search.js +1 -1
- package/dist/components/ds-panel-tools.js +1 -1
- package/dist/components/ds-paper-texture.js +1 -1
- package/dist/components/ds-radio.js +1 -1
- package/dist/components/ds-select.js +1 -1
- package/dist/components/ds-shell-app.js +1 -1
- package/dist/components/ds-shell-app.js.map +1 -1
- package/dist/components/ds-shell-page.js +1 -1
- package/dist/components/ds-shell-tools.js +1 -1
- package/dist/components/ds-skeleton.js +1 -1
- package/dist/components/ds-slider.js +1 -1
- package/dist/components/ds-swatch-picker.js +1 -1
- package/dist/components/ds-switch.js +1 -1
- package/dist/components/ds-tab-group.js +1 -1
- package/dist/components/ds-table-filter.js +1 -1
- package/dist/components/ds-table-filter.js.map +1 -1
- package/dist/components/ds-table-saved-views.js +1 -1
- package/dist/components/ds-table-sort.js +1 -1
- package/dist/components/ds-table-sort.js.map +1 -1
- package/dist/components/ds-table-toolbar.js +1 -1
- package/dist/components/ds-table-toolbar.js.map +1 -1
- package/dist/components/ds-table.js +1 -1
- package/dist/components/ds-table.js.map +1 -1
- package/dist/components/ds-tag.js +1 -1
- package/dist/components/ds-text.js +1 -1
- package/dist/components/ds-toast.js +1 -1
- package/dist/components/ds-toast.js.map +1 -1
- package/dist/components/ds-tooltip-chart.js +1 -1
- package/dist/components/ds-tooltip.js +1 -1
- package/dist/components/ds-typing-indicator.js +1 -1
- package/dist/components/ds-typing-indicator.js.map +1 -1
- package/dist/components/{p-C0X2zKbB.js → p-1Hu9jlju.js} +2 -2
- package/dist/components/{p-DQqOzDEq.js → p-B00PEE6t.js} +2 -2
- package/dist/components/p-B0Byw6OX.js +2 -0
- package/dist/components/p-B0Byw6OX.js.map +1 -0
- package/dist/components/{p-D1YhYulL.js → p-B1JRCDHS.js} +2 -2
- package/dist/components/p-BBG6Sswb.js +2 -0
- package/dist/components/{p-23u3lIr1.js.map → p-BBG6Sswb.js.map} +1 -1
- package/dist/components/{p-D8Qd6gkf.js → p-BCOl1x0_.js} +2 -2
- package/dist/components/{p-Dg9qiMDa.js → p-BHVOiWz5.js} +2 -2
- package/dist/components/p-BPG0r_mC.js +2 -0
- package/dist/components/p-BPG0r_mC.js.map +1 -0
- package/dist/components/p-BUDAwJVV.js +2 -0
- package/dist/components/p-BUDAwJVV.js.map +1 -0
- package/dist/components/{p-DHuyj-9J.js → p-BXN-e7vF.js} +2 -2
- package/dist/components/{p-Bn8NryHK.js → p-B_jZIC9R.js} +2 -2
- package/dist/components/{p-7tZx4ukz.js → p-Bbb9A3NV.js} +2 -2
- package/dist/components/p-Bece5KH3.js +2 -0
- package/dist/components/p-Bece5KH3.js.map +1 -0
- package/dist/components/{p-ruPTIV6t.js → p-BhYS_Ump.js} +2 -2
- package/dist/components/p-Bkh7scMQ.js +2 -0
- package/dist/components/{p-DsJW5W3W.js.map → p-Bkh7scMQ.js.map} +1 -1
- package/dist/components/{p-B0PEg5RF.js → p-BknXYAEc.js} +2 -2
- package/dist/components/{p-C4TgWJi9.js → p-Bu3dOjpW.js} +2 -2
- package/dist/components/{p-BVlfQ8xZ.js → p-C-Y7gYgz.js} +2 -2
- package/dist/components/p-C-Y7gYgz.js.map +1 -0
- package/dist/components/{p-DAqSoyd4.js → p-C6R5Lft0.js} +2 -2
- package/dist/components/{p-PD1Vfg0t.js → p-C9gC4haA.js} +2 -2
- package/dist/components/{p-BGXqFq2C.js → p-CAnnQL1F.js} +2 -2
- package/dist/components/p-CE2IKOsy.js +2 -0
- package/dist/components/p-CE2IKOsy.js.map +1 -0
- package/dist/components/p-CKVHrOEp.js +2 -0
- package/dist/components/{p-DIEK54fV.js.map → p-CKVHrOEp.js.map} +1 -1
- package/dist/components/{p-BHz78C8h.js → p-CLVxgTme.js} +2 -2
- package/dist/components/p-CY4YFAT3.js +2 -0
- package/dist/components/p-CY4YFAT3.js.map +1 -0
- package/dist/components/{p-y4jhIkRc.js → p-Cg0kimXZ.js} +2 -2
- package/dist/components/p-CqQFowZd.js +2 -0
- package/dist/components/p-CqQFowZd.js.map +1 -0
- package/dist/components/{p-BhYWdbEp.js → p-CsBICYmt.js} +2 -2
- package/dist/components/p-CwjVgUqy.js +2 -0
- package/dist/components/p-CwjVgUqy.js.map +1 -0
- package/dist/components/{p-BHDkJmrC.js → p-D123Jvp-.js} +2 -2
- package/dist/components/p-D1eEgwcP.js +2 -0
- package/dist/components/p-D1eEgwcP.js.map +1 -0
- package/dist/components/{p-CzcWZiH-.js → p-D3fMaUJ3.js} +2 -2
- package/dist/components/p-D7R4yE2C.js +2 -0
- package/dist/components/{p-DEjh3klB.js.map → p-D7R4yE2C.js.map} +1 -1
- package/dist/components/{p-DZ55e3j9.js → p-D7haeL5y.js} +2 -2
- package/dist/components/p-D7haeL5y.js.map +1 -0
- package/dist/components/{p-D2z2DgtX.js → p-DAvh38E4.js} +2 -2
- package/dist/components/{p-CMFvaY05.js → p-DBue3ISk.js} +2 -2
- package/dist/components/{p-rrFMAZkj.js → p-DDgeXGbB.js} +2 -2
- package/dist/components/p-DNpLDbWY.js +2 -0
- package/dist/components/p-DNpLDbWY.js.map +1 -0
- package/dist/components/{p-U8JIZzI2.js → p-DUdOmSFR.js} +2 -2
- package/dist/components/p-DUdOmSFR.js.map +1 -0
- package/dist/components/p-DtndC9f1.js +2 -0
- package/dist/components/p-DtndC9f1.js.map +1 -0
- package/dist/components/{p-Be1sY58w.js → p-H5T258T-.js} +2 -2
- package/dist/components/p-H5T258T-.js.map +1 -0
- package/dist/components/p-HB_cf_aR.js +2 -0
- package/dist/components/{p-DNJTN_s0.js.map → p-HB_cf_aR.js.map} +1 -1
- package/dist/components/p-LK-jpxwq.js +2 -0
- package/dist/components/p-LK-jpxwq.js.map +1 -0
- package/dist/components/p-ag6Io-kL.js +2 -0
- package/dist/components/p-ag6Io-kL.js.map +1 -0
- package/dist/components/p-iMuFgSoM.js +2 -0
- package/dist/components/p-iMuFgSoM.js.map +1 -0
- package/dist/components/{p-DSIOINBK.js → p-uI6z53BE.js} +2 -2
- package/dist/docs/components.json +456 -55
- package/dist/lib/components/ButtonFilled/ButtonFilled.d.ts +2 -0
- package/dist/lib/components/PanelTools/panel-tools-types.d.ts +14 -2
- package/dist/lib/shell/index.d.ts +1 -1
- package/dist/lib/shell/index.js +17 -25
- package/dist/lib/shell/index.js.map +2 -2
- package/dist/lib/shell/shell-responsive.d.ts +6 -1
- package/dist/lib/utils/anchored-position.d.ts +8 -2
- package/dist/lib/utils/button-render.d.ts +1 -0
- package/dist/mcp-data/registry/badge.json +7 -9
- package/dist/mcp-data/registry/banner.json +2 -2
- package/dist/mcp-data/registry/bar-action.json +2 -2
- package/dist/mcp-data/registry/button-filled.json +20 -1
- package/dist/mcp-data/registry/button-unfilled.json +23 -3
- package/dist/mcp-data/registry/conversation-list.json +2 -2
- package/dist/mcp-data/registry/filter-menu.json +22 -7
- package/dist/mcp-data/registry/input.json +24 -4
- package/dist/mcp-data/registry/menu.json +23 -4
- package/dist/mcp-data/registry/message-composer.json +2 -2
- package/dist/mcp-data/registry/mobile-bar-nav.json +57 -6
- package/dist/mcp-data/registry/pagination.json +5 -4
- package/dist/mcp-data/registry/panel-nav.json +1 -1
- package/dist/mcp-data/registry/panel-tool-header.json +3 -3
- package/dist/mcp-data/registry/panel-tools.json +8 -7
- package/dist/mcp-data/registry/registry.json +102 -44
- package/dist/mcp-data/registry/select.json +18 -4
- package/dist/mcp-data/registry/shell-app.json +7 -6
- package/dist/mcp-data/registry/shell-tools.json +4 -4
- package/dist/mcp-data/registry/table-filter.json +3 -3
- package/dist/mcp-data/registry/table-sort.json +5 -4
- package/dist/mcp-data/registry/table-toolbar.json +10 -9
- package/dist/mcp-data/registry/table.json +24 -4
- package/dist/mcp-data/registry/toast.json +2 -2
- package/dist/mcp-data/registry/tooltip.json +20 -3
- package/dist/mcp-data/registry/typing-indicator.json +4 -4
- package/dist/react/ds-panel-tool-header.d.ts +2 -5
- package/dist/react/ds-panel-tools.d.ts +2 -5
- package/dist/styles/table.css +30 -10
- package/dist/types/components/Badge/Badge.d.ts +2 -16
- package/dist/types/components/Banner/Banner.d.ts +0 -1
- package/dist/types/components/BarAction/BarAction.d.ts +0 -7
- package/dist/types/components/ButtonFilled/ButtonFilled.d.ts +2 -0
- package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +2 -0
- package/dist/types/components/ConversationList/ConversationList.d.ts +1 -0
- package/dist/types/components/FilterMenu/FilterMenu.d.ts +5 -5
- package/dist/types/components/Input/Input.d.ts +7 -0
- package/dist/types/components/Menu/Menu.d.ts +4 -4
- package/dist/types/components/MessageComposer/MessageComposer.d.ts +0 -3
- package/dist/types/components/MobileBarNav/MobileBarNav.d.ts +4 -0
- package/dist/types/components/Pagination/Pagination.d.ts +8 -0
- package/dist/types/components/PanelToolHeader/PanelToolHeader.d.ts +2 -5
- package/dist/types/components/PanelTools/PanelTools.d.ts +2 -5
- package/dist/types/components/PanelTools/panel-tools-types.d.ts +14 -2
- package/dist/types/components/Select/Select.d.ts +3 -0
- package/dist/types/components/Table/Table.d.ts +2 -13
- package/dist/types/components/Table/table-body-renderer.d.ts +36 -0
- package/dist/types/components/Table/table-load-view.d.ts +17 -0
- package/dist/types/components/Table/table-row-view.d.ts +26 -0
- package/dist/types/components/Table/table-skeleton-view.d.ts +12 -0
- package/dist/types/components/TableToolbar/TableToolbar.d.ts +5 -0
- package/dist/types/components/Tooltip/Tooltip.d.ts +3 -0
- package/dist/types/components.d.ts +112 -23
- package/dist/types/shell/index.d.ts +1 -1
- package/dist/types/shell/shell-responsive.d.ts +6 -1
- package/dist/types/utils/anchored-overlay-boundary.d.ts +14 -0
- package/dist/types/utils/anchored-overlay-interaction-controller.d.ts +26 -0
- package/dist/types/utils/anchored-position-controller.d.ts +14 -0
- package/dist/types/utils/anchored-position.d.ts +8 -2
- package/dist/types/utils/button-render.d.ts +1 -0
- package/dist/types/utils/select-controller.d.ts +1 -0
- package/dist/vue/ds-button-filled.js +1 -0
- package/dist/vue/ds-button-unfilled.js +1 -0
- package/dist/vue/ds-filter-menu.js +1 -0
- package/dist/vue/ds-input.js +1 -0
- package/dist/vue/ds-menu.js +1 -0
- package/dist/vue/ds-mobile-bar-nav.js +3 -0
- package/dist/vue/ds-select.js +1 -0
- package/dist/vue/ds-tooltip.js +1 -0
- package/package.json +4 -1
- package/dist/components/p-23u3lIr1.js +0 -2
- package/dist/components/p-Asi9h3pa.js +0 -2
- package/dist/components/p-Asi9h3pa.js.map +0 -1
- package/dist/components/p-BBxcMVqk.js +0 -2
- package/dist/components/p-BBxcMVqk.js.map +0 -1
- package/dist/components/p-BEvBJ65v.js +0 -2
- package/dist/components/p-BEvBJ65v.js.map +0 -1
- package/dist/components/p-BFSLT0yc.js +0 -2
- package/dist/components/p-BFSLT0yc.js.map +0 -1
- package/dist/components/p-BVlfQ8xZ.js.map +0 -1
- package/dist/components/p-BX0BMhgz.js +0 -2
- package/dist/components/p-BX0BMhgz.js.map +0 -1
- package/dist/components/p-Be1sY58w.js.map +0 -1
- package/dist/components/p-Bh67XbfB.js +0 -2
- package/dist/components/p-Bh67XbfB.js.map +0 -1
- package/dist/components/p-CKmfuK7k.js +0 -2
- package/dist/components/p-CKmfuK7k.js.map +0 -1
- package/dist/components/p-D4K9CE8a.js +0 -2
- package/dist/components/p-D4K9CE8a.js.map +0 -1
- package/dist/components/p-DEjh3klB.js +0 -2
- package/dist/components/p-DIEK54fV.js +0 -2
- package/dist/components/p-DNJTN_s0.js +0 -2
- package/dist/components/p-DOqnJVos.js +0 -2
- package/dist/components/p-DOqnJVos.js.map +0 -1
- package/dist/components/p-DZ55e3j9.js.map +0 -1
- package/dist/components/p-DhKvqh5G.js +0 -2
- package/dist/components/p-DhKvqh5G.js.map +0 -1
- package/dist/components/p-Dr4AgJbV.js +0 -2
- package/dist/components/p-Dr4AgJbV.js.map +0 -1
- package/dist/components/p-DsJW5W3W.js +0 -2
- package/dist/components/p-NzGRAPcF.js +0 -2
- package/dist/components/p-NzGRAPcF.js.map +0 -1
- package/dist/components/p-U8JIZzI2.js.map +0 -1
- package/dist/components/p-ZZzC52Ea.js +0 -2
- package/dist/components/p-ZZzC52Ea.js.map +0 -1
- package/dist/components/p-__lNIImO.js +0 -2
- package/dist/components/p-__lNIImO.js.map +0 -1
- package/dist/components/p-a6qP60zb.js +0 -2
- package/dist/components/p-a6qP60zb.js.map +0 -1
- package/dist/types/shell/badge-gradient-ring.d.ts +0 -28
- /package/dist/components/{p-C0X2zKbB.js.map → p-1Hu9jlju.js.map} +0 -0
- /package/dist/components/{p-DQqOzDEq.js.map → p-B00PEE6t.js.map} +0 -0
- /package/dist/components/{p-D1YhYulL.js.map → p-B1JRCDHS.js.map} +0 -0
- /package/dist/components/{p-D8Qd6gkf.js.map → p-BCOl1x0_.js.map} +0 -0
- /package/dist/components/{p-Dg9qiMDa.js.map → p-BHVOiWz5.js.map} +0 -0
- /package/dist/components/{p-DHuyj-9J.js.map → p-BXN-e7vF.js.map} +0 -0
- /package/dist/components/{p-Bn8NryHK.js.map → p-B_jZIC9R.js.map} +0 -0
- /package/dist/components/{p-7tZx4ukz.js.map → p-Bbb9A3NV.js.map} +0 -0
- /package/dist/components/{p-ruPTIV6t.js.map → p-BhYS_Ump.js.map} +0 -0
- /package/dist/components/{p-B0PEg5RF.js.map → p-BknXYAEc.js.map} +0 -0
- /package/dist/components/{p-C4TgWJi9.js.map → p-Bu3dOjpW.js.map} +0 -0
- /package/dist/components/{p-DAqSoyd4.js.map → p-C6R5Lft0.js.map} +0 -0
- /package/dist/components/{p-PD1Vfg0t.js.map → p-C9gC4haA.js.map} +0 -0
- /package/dist/components/{p-BGXqFq2C.js.map → p-CAnnQL1F.js.map} +0 -0
- /package/dist/components/{p-BHz78C8h.js.map → p-CLVxgTme.js.map} +0 -0
- /package/dist/components/{p-y4jhIkRc.js.map → p-Cg0kimXZ.js.map} +0 -0
- /package/dist/components/{p-BhYWdbEp.js.map → p-CsBICYmt.js.map} +0 -0
- /package/dist/components/{p-BHDkJmrC.js.map → p-D123Jvp-.js.map} +0 -0
- /package/dist/components/{p-CzcWZiH-.js.map → p-D3fMaUJ3.js.map} +0 -0
- /package/dist/components/{p-D2z2DgtX.js.map → p-DAvh38E4.js.map} +0 -0
- /package/dist/components/{p-CMFvaY05.js.map → p-DBue3ISk.js.map} +0 -0
- /package/dist/components/{p-rrFMAZkj.js.map → p-DDgeXGbB.js.map} +0 -0
- /package/dist/components/{p-DSIOINBK.js.map → p-uI6z53BE.js.map} +0 -0
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"pattern:menu-trigger"
|
|
46
46
|
],
|
|
47
47
|
"accessibility": [
|
|
48
|
-
"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
|
|
48
|
+
"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 Table 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.",
|
|
49
49
|
"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.",
|
|
50
50
|
"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.",
|
|
51
51
|
"Sorting controls remain buttons inside column headers and expose the table's one controlled member-row sort through aria-sort. ds-table-sort in the toolbar trailing cluster is the matching menu of Data and Direction 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; the application supplies groups in its fixed product order. 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.",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
],
|
|
72
72
|
"responsiveBehavior": [
|
|
73
73
|
"The table keeps native semantics at narrow widths and scrolls horizontally instead of transforming into cards.",
|
|
74
|
-
"Inside the named ds-table container, caption 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 Table menu button below 900px and keeps its neutral resting foreground regardless of the controlled column configuration.",
|
|
74
|
+
"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 Table 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.",
|
|
75
75
|
"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.",
|
|
76
76
|
"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.",
|
|
77
77
|
"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.",
|
|
@@ -1290,12 +1290,12 @@
|
|
|
1290
1290
|
"files": [
|
|
1291
1291
|
{
|
|
1292
1292
|
"path": "src/wc/components/Table/Table.css",
|
|
1293
|
-
"content": "@import '../../styles/table.css';\n@import '../../styles/control-elevation.css';\n@import '../../utils/chrome-header.css';\n@import '../../utils/control-density.css';\n@import '../../utils/text-decoration.css';\n\n:host {\n display: block;\n box-sizing: border-box;\n min-width: 0;\n max-width: 100%;\n container-type: inline-size;\n container-name: ds-table;\n}\n\n:host(.table-host--bounded) {\n min-block-size: 0;\n}\n\n:host(.table-host--fixed-height) {\n block-size: var(--_table-fixed-block-size);\n}\n\n:host(.table-host--viewport-fit) {\n block-size: var(--_table-viewport-fit-reserved-block-size, auto);\n min-block-size: var(--_table-viewport-fit-reserved-block-size, auto);\n}\n\n/*\n * The public table recipe stays at zero specificity for product-owned native\n * markup. Inside the component, these classes sit on ds-text hosts whose reset\n * intentionally owns display, padding, and border with higher specificity.\n * Reassert only those collided properties so component geometry cannot depend\n * on whether ds-text or ds-table happens to hydrate first.\n */\nds-text.ds-table__header-label-box {\n padding-inline: var(--dimension-space-025);\n}\n\nds-text.ds-table__cell-track {\n display: flex;\n}\n\n.ds-table__cell--text-wrap ds-text.ds-table__cell-track--text,\n.ds-table__cell--icon-text-wrap ds-text.ds-table__cell-track--text {\n display: block;\n}\n\nds-text.ds-table__cell-track--text {\n padding-inline: var(--_table-cell-track-padding-inline);\n}\n\nds-text.ds-table__cell-primary,\n.ds-table__cell--empty ds-text.ds-table__cell-track {\n padding-block: var(--dimension-space-025);\n}\n\n/*\n * The public :where() wrap recipe has zero specificity, so ds-text's\n * variant line-height would win inside the component. Reassert on wrapping\n * cell layout classes so extra primary and secondary lines occupy later tracks.\n */\n.ds-table__cell--text-wrap.ds-table__cell--text-single .ds-table__cell-primary,\n.ds-table__cell--text-wrap.ds-table__cell--text-multi .ds-table__cell-primary,\n.ds-table__cell--icon-text-wrap.ds-table__cell--icon-text-single .ds-table__cell-primary,\n.ds-table__cell--icon-text-wrap.ds-table__cell--icon-text-multi .ds-table__cell-primary {\n line-height: var(--_table-wrap-primary-line-height);\n padding-block: var(--_table-wrap-primary-padding-block);\n}\n\n.ds-table__cell--text-wrap.ds-table__cell--text-multi .ds-table__cell-copy,\n.ds-table__cell--icon-text-wrap.ds-table__cell--icon-text-multi .ds-table__cell-copy {\n gap: 0;\n}\n\n.ds-table__cell--text-wrap.ds-table__cell--text-multi .ds-table__cell-secondary,\n.ds-table__cell--icon-text-wrap.ds-table__cell--icon-text-multi .ds-table__cell-secondary {\n line-height: var(--_table-wrap-secondary-line-height);\n padding-block: var(--_table-wrap-secondary-padding-block);\n}\n\nds-text.ds-table__cell-secondary,\nds-text.ds-table__cell-tertiary {\n padding-block: var(--dimension-space-025);\n}\n\n.ds-table__cell--primary-text ds-text.ds-table__cell-secondary {\n padding-block: 0;\n}\n\nds-text.ds-table__cell-run,\nds-text.ds-table__cell-run-separator {\n display: flex;\n}\n\n.ds-table__cell-tag-stack--tag-with-text ds-text.ds-table__cell-tag-text {\n padding-inline: var(--_table-cell-track-padding-inline);\n padding-block: var(--dimension-space-025);\n}\n\n.ds-table__cell-tag-stack--text-with-tag ds-text.ds-table__cell-tag-text {\n padding-inline: var(--_table-cell-track-padding-inline);\n padding-block: var(--dimension-space-025);\n}\n\nds-text.ds-table__load-content {\n display: inline-flex;\n}\n\nds-text.ds-table__bar-text {\n display: inline-flex;\n width: auto;\n max-width: 100%;\n padding-inline: var(--ds-control-label-inset, var(--dimension-space-025));\n}\n\nds-text.ds-table__footer-summary {\n margin-inline-start: auto;\n}\n\n.ds-table__bar-trailing ds-pagination {\n max-inline-size: 100%;\n}\n\n.ds-table__bar-status slot::slotted(*) {\n width: 100%;\n max-width: 100%;\n min-width: 0;\n}\n\nds-skeleton.ds-table__skeleton-image-shape {\n display: flex;\n width: 100%;\n height: 100%;\n --ds-control-height: 100%;\n}\n\nds-skeleton.ds-table__skeleton-checkbox {\n --ds-control-height: var(--dimension-iconography-sm);\n}\n\n.ds-table__skeleton-checkbox-canvas {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n inline-size: var(--dimension-size-300);\n block-size: var(--dimension-size-300);\n}\n\n.ds-table__caption-customizer {\n position: relative;\n display: flex;\n align-items: center;\n min-inline-size: 0;\n}\n\n.ds-table__caption-customizer--loading > ds-button-unfilled {\n visibility: hidden;\n}\n\n.ds-table__caption-customizer > ds-skeleton {\n position: absolute;\n inset: 0;\n}\n",
|
|
1293
|
+
"content": "@import '../../styles/table.css';\n@import '../../styles/control-elevation.css';\n@import '../../utils/chrome-header.css';\n@import '../../utils/control-density.css';\n@import '../../utils/text-decoration.css';\n\n:host {\n display: block;\n box-sizing: border-box;\n min-width: 0;\n max-width: 100%;\n container-type: inline-size;\n container-name: ds-table;\n}\n\n:host(.table-host--bounded) {\n min-block-size: 0;\n}\n\n:host(.table-host--fixed-height) {\n block-size: var(--_table-fixed-block-size);\n}\n\n:host(.table-host--viewport-fit) {\n block-size: var(--_table-viewport-fit-reserved-block-size, auto);\n min-block-size: var(--_table-viewport-fit-reserved-block-size, auto);\n}\n\n::slotted([slot='header']) {\n inline-size: 100%;\n min-inline-size: 0;\n}\n\n/*\n * The public table recipe stays at zero specificity for product-owned native\n * markup. Inside the component, these classes sit on ds-text hosts whose reset\n * intentionally owns display, padding, and border with higher specificity.\n * Reassert only those collided properties so component geometry cannot depend\n * on whether ds-text or ds-table happens to hydrate first.\n */\nds-text.ds-table__header-label-box {\n padding-inline: var(--dimension-space-025);\n}\n\nds-text.ds-table__cell-track {\n display: flex;\n}\n\n.ds-table__cell--text-wrap ds-text.ds-table__cell-track--text,\n.ds-table__cell--icon-text-wrap ds-text.ds-table__cell-track--text {\n display: block;\n}\n\nds-text.ds-table__cell-track--text {\n padding-inline: var(--_table-cell-track-padding-inline);\n}\n\nds-text.ds-table__cell-primary,\n.ds-table__cell--empty ds-text.ds-table__cell-track {\n padding-block: var(--dimension-space-025);\n}\n\n/*\n * The public :where() wrap recipe has zero specificity, so ds-text's\n * variant line-height would win inside the component. Reassert on wrapping\n * cell layout classes so extra primary and secondary lines occupy later tracks.\n */\n.ds-table__cell--text-wrap.ds-table__cell--text-single .ds-table__cell-primary,\n.ds-table__cell--text-wrap.ds-table__cell--text-multi .ds-table__cell-primary,\n.ds-table__cell--icon-text-wrap.ds-table__cell--icon-text-single .ds-table__cell-primary,\n.ds-table__cell--icon-text-wrap.ds-table__cell--icon-text-multi .ds-table__cell-primary {\n line-height: var(--_table-wrap-primary-line-height);\n padding-block: var(--_table-wrap-primary-padding-block);\n}\n\n.ds-table__cell--text-wrap.ds-table__cell--text-multi .ds-table__cell-copy,\n.ds-table__cell--icon-text-wrap.ds-table__cell--icon-text-multi .ds-table__cell-copy {\n gap: 0;\n}\n\n.ds-table__cell--text-wrap.ds-table__cell--text-multi .ds-table__cell-secondary,\n.ds-table__cell--icon-text-wrap.ds-table__cell--icon-text-multi .ds-table__cell-secondary {\n line-height: var(--_table-wrap-secondary-line-height);\n padding-block: var(--_table-wrap-secondary-padding-block);\n}\n\nds-text.ds-table__cell-secondary,\nds-text.ds-table__cell-tertiary {\n padding-block: var(--dimension-space-025);\n}\n\n.ds-table__cell--primary-text ds-text.ds-table__cell-secondary {\n padding-block: 0;\n}\n\nds-text.ds-table__cell-run,\nds-text.ds-table__cell-run-separator {\n display: flex;\n}\n\n.ds-table__cell-tag-stack--tag-with-text ds-text.ds-table__cell-tag-text {\n padding-inline: var(--_table-cell-track-padding-inline);\n padding-block: var(--dimension-space-025);\n}\n\n.ds-table__cell-tag-stack--text-with-tag ds-text.ds-table__cell-tag-text {\n padding-inline: var(--_table-cell-track-padding-inline);\n padding-block: var(--dimension-space-025);\n}\n\nds-text.ds-table__load-content {\n display: inline-flex;\n}\n\nds-text.ds-table__bar-text {\n display: inline-flex;\n width: auto;\n max-width: 100%;\n padding-inline: var(--ds-control-label-inset, var(--dimension-space-025));\n}\n\nds-text.ds-table__footer-summary {\n margin-inline-start: auto;\n}\n\n.ds-table__bar-trailing ds-pagination {\n max-inline-size: 100%;\n}\n\n.ds-table__bar-status slot::slotted(*) {\n width: 100%;\n max-width: 100%;\n min-width: 0;\n}\n\nds-skeleton.ds-table__skeleton-image-shape {\n display: flex;\n width: 100%;\n height: 100%;\n --ds-control-height: 100%;\n}\n\nds-skeleton.ds-table__skeleton-checkbox {\n --ds-control-height: var(--dimension-iconography-sm);\n}\n\n.ds-table__skeleton-checkbox-canvas {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n inline-size: var(--dimension-size-300);\n block-size: var(--dimension-size-300);\n}\n\n.ds-table__caption-customizer {\n position: relative;\n display: flex;\n align-items: center;\n min-inline-size: 0;\n}\n\n.ds-table__caption-mode-switcher {\n display: flex;\n align-items: center;\n}\n\n.ds-table__caption-customizer--loading > ds-tooltip ds-button-unfilled {\n visibility: hidden;\n}\n\n.ds-table__caption-customizer > ds-skeleton {\n position: absolute;\n inset: 0;\n}\n",
|
|
1294
1294
|
"type": "registry:ui"
|
|
1295
1295
|
},
|
|
1296
1296
|
{
|
|
1297
1297
|
"path": "src/wc/components/Table/Table.tsx",
|
|
1298
|
-
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Listen,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n deriveTableSelectionState,\n formatTableResultSummary,\n formatTableTotalSummary,\n hasOwnedTableFooterSlot,\n isTableCellAction,\n nextTableSortState,\n tableColumnSize,\n tableModelIssues,\n tableRows,\n tableRowSelectionLabel,\n toggleAllLoadedTableRows,\n toggleTableGroupCollapsed,\n toggleTableGroupSelection,\n nextTableGroupsCollapsed,\n toggleTableRowSelection,\n} from './table-model';\nimport {\n resolveTableCellImageTracks,\n resolveTableCellPresentation,\n tableCellImageVariant,\n tableCellTextOverflowProps,\n type TableCellPresentation,\n} from './table-cell-model';\nimport {\n createTableRenderModel,\n type TableGroupRenderModel,\n type TableRenderModel,\n} from './table-render-model';\nimport {\n isRenderableTableActionMenu,\n isTableCellActionMenu,\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 { TableLayoutController } from './table-layout-controller';\nimport { TableLoadController } from './table-load-controller';\nimport { TableGroupLoadController } from './table-group-load-controller';\nimport {\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 TableVirtualNode,\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 { resolveSafeUrl } from '../../utils/safe-url';\nimport { observeTableCaptionCompact } from '../../utils/table-caption-compact';\nimport { resolveTableFitPageSize } from './table-pagination-fit';\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 TableCellLineClamp,\n TableCellSkeleton,\n TableCellTextRun,\n TableColumn,\n TableColumnsConfigChangeDetail,\n TableDataMode,\n TableDataModeChangeDetail,\n TableGroup,\n TableGroupCollapseChangeDetail,\n TableGroupLoadMoreDetail,\n TableGroupingState,\n TableLoadMoreDetail,\n TableLoadMoreMode,\n TablePaginationState,\n TableRow,\n TableRowActivateDetail,\n TableSelectionChangeDetail,\n TableSelectionMode,\n TableSortChangeDetail,\n TableSortState,\n} from './table-types';\n\nconst TABLE_FOOTER_SLOT_LEADING = 1;\nconst TABLE_FOOTER_SLOT_COPY = 2;\nconst TABLE_FOOTER_SLOT_TRAILING = 4;\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 /** 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: TableGroup[] = [];\n /** Controlled grouping column. Applications supply groups in their final fixed order. */\n @Prop() grouping: TableGroupingState | null = null;\n /** Controlled member-row sort state. */\n @Prop() sort: TableSortState | null = null;\n /** Controlled collapsed group identities. Groups not listed remain expanded. */\n @Prop() collapsedGroupIds: 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 /** Controlled hidden data-column identities. Action ids are ignored. */\n @Prop() hiddenColumnIds: string[] = [];\n /** Controlled data-column identities in display order. Omitted ids append in catalog order. */\n @Prop() columnOrder: 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 /** 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<TableSortChangeDetail>;\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() dsColumnsConfigChange!: EventEmitter<TableColumnsConfigChangeDetail>;\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 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 truncateTooltipLabel = '';\n @State() private captionCompact = false;\n @State() private columnCustomizerOpen = false;\n @State() private columnCustomizerInitialFocusVisible = false;\n @State() private dataModeSwitcherOpen = false;\n @State() private dataModeSwitcherInitialFocusVisible = false;\n @State() private virtualWindow: TableVirtualPlan | null = null;\n\n private readonly actionMenuElementId = nextTableActionMenuElementId();\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 virtualItems: TableVirtualItem[] = [];\n private visibleColumnsCache: {\n columns: TableColumn[];\n columnCustomizer: boolean;\n hiddenColumnIds: string[];\n columnOrder: string[];\n value: TableColumn[];\n } | null = null;\n private renderModelCache: {\n columns: TableColumn[];\n rows: TableRow[];\n groups: TableGroup[];\n grouped: boolean;\n selectionMode: TableSelectionMode;\n selectedRowIds: string[];\n collapsedGroupIds: string[];\n virtualCounts: boolean;\n value: TableRenderModel;\n } | null = null;\n private virtualItemsCache: {\n columns: TableColumn[];\n rows: TableRow[];\n groups: TableGroup[];\n grouped: boolean;\n collapsedGroupIds: string[];\n } | null = null;\n private virtualLookupCache: {\n rows: TableRow[];\n groups: TableGroupRenderModel[];\n rowsById: Map<string, TableRow>;\n groupsById: Map<string, TableGroupRenderModel>;\n } | null = null;\n private virtualRowPoolStates = new Map<\n string,\n {\n slotsByRowId: Map<string, number>;\n nextSlot: number;\n }\n >();\n\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 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 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.virtualizationEnabled,\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 this.warnModelIssues();\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 }\n\n componentDidRender(): void {\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.dataMode === 'virtual' &&\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 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.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 }\n\n private syncHeaderSlotPresence = () => {\n this.headerPresent = !!this.el.querySelector('[slot=\"header\"]');\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('hiddenColumnIds')\n @Watch('columnOrder')\n @Watch('columnCustomizer')\n handleStructureChange(): void {\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('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 if (this.dataMode === 'virtual') 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) this.closeColumnCustomizer();\n }\n\n @Watch('dataModeSwitcher')\n @Watch('captionVisibility')\n handleDataModeSwitcherAvailability(): void {\n if (!this.showsDataModeSwitcher) this.closeDataModeSwitcher();\n }\n\n @Watch('chromeLoading')\n handleChromeLoadingChange(loading: boolean): void {\n if (!loading) return;\n this.closeColumnCustomizer();\n this.closeDataModeSwitcher();\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.hiddenColumnIds === this.hiddenColumnIds &&\n cached.columnOrder === this.columnOrder\n ) {\n return cached.value;\n }\n const value = resolveTableVisibleColumns(this.columns, {\n columnCustomizer: this.columnCustomizer,\n hiddenColumnIds: this.hiddenColumnIds,\n columnOrder: this.columnOrder,\n });\n this.visibleColumnsCache = {\n columns: this.columns,\n columnCustomizer: this.columnCustomizer,\n hiddenColumnIds: this.hiddenColumnIds,\n columnOrder: this.columnOrder,\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 this.showsDataModeSwitcher || this.showsColumnCustomizer;\n }\n\n private get documentStickyHeader(): boolean {\n return this.stickyHeader && !this.containedScroll;\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 .ds-table__header-row');\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 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 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 virtualizationEnabled(): boolean {\n return this.dataMode === 'virtual' && this.hasVirtualViewportBounds;\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 syncVirtualItems(model: TableRenderModel): void {\n if (this.dataMode !== 'virtual') {\n this.virtualItems = [];\n this.virtualItemsCache = null;\n this.virtualRowPoolStates.clear();\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 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 shouldVirtualize(model: TableRenderModel): boolean {\n return (\n this.virtualizationEnabled && (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!.columnId);\n if (!groupingColumn) {\n issues.push(`Grouping references unknown column id: ${this.grouping.columnId}`);\n } else if (!visibleColumns.some(column => column.id === groupingColumn.id)) {\n issues.push(`Grouping references hidden column id: ${this.grouping.columnId}`);\n }\n }\n if (this.sort) {\n const sortColumn = this.columns.find(\n column =>\n column.id === this.sort!.columnId ||\n column.headerSegments?.some(segment => segment.sortKey === this.sort!.columnId)\n );\n if (!sortColumn) {\n issues.push(`Sorting references unknown column id: ${this.sort.columnId}`);\n } else if (!visibleColumns.some(column => column.id === sortColumn.id)) {\n issues.push(`Sorting references hidden column id: ${this.sort.columnId}`);\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 if (stickyStart.length > 1 || (this.selectable && stickyStart.length > 0)) {\n issues.push(\n 'Only one sticky start column is supported, and row selection already owns that lane.'\n );\n }\n if (stickyEnd.length > 1) issues.push('Only one sticky end column is supported.');\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 emitSort(column: TableColumn, sortKey = column.id): void {\n if (!column.sortable) return;\n this.dsSortChange.emit({ sort: nextTableSortState(this.sort, sortKey) });\n }\n\n private sortButtonLabel(column: TableColumn, sortKey = column.id, label = column.header): string {\n if (this.sort?.columnId !== 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: TableGroup): 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 emitRowActivation(row: TableRow, event: Event): void {\n if (!row.interactive || row.disabled || !this.rowEventOwnsActivation(event)) return;\n 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 }\n\n private renderOverflowActionMenu() {\n const open = this.actionMenu;\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(open && 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 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 truncateAttr(lineClamp: TableCellLineClamp) {\n return lineClamp === 'none' ? undefined : '';\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.virtualizationEnabled) 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']) {\n if (!sticky) return null;\n return (\n <span class={`ds-table__sticky-edge ds-table__sticky-edge--${sticky}`} aria-hidden=\"true\" />\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 ) {\n const groupedColumn = this.grouping?.columnId === column.id;\n const headerSegments = column.headerSegments?.length\n ? column.headerSegments\n : [{ label: column.header, sortKey: column.id }];\n const activeMemberSegment = headerSegments.find(\n segment => segment.sortKey === this.sort?.columnId\n );\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 {headerSegments.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(column, segment.sortKey, segment.label)}\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 < headerSegments.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.header\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.header.trim() && !column.headerSegments?.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 class={{\n 'ds-table__header-cell': true,\n [`ds-table__cell--align-${align}`]: true,\n 'ds-table__cell--sticky-start': column.sticky === 'start',\n 'ds-table__cell--sticky-end': column.sticky === 'end',\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.header.trim() && column.headerLabel?.trim() && (\n <span class=\"ds-visually-hidden\">{column.headerLabel}</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(column.sticky)}\n </th>\n );\n }\n\n private renderColgroup(model: TableRenderModel) {\n return (\n <colgroup>\n {model.selectable && <col class=\"ds-table__selection-column\" />}\n {this.visibleColumns.map(column => {\n const width = tableColumnSize(column);\n const flexible = column.id === model.flexibleColumnId;\n return (\n <col\n key={column.id}\n class={{\n 'ds-table__action-column': column.kind === 'action',\n 'ds-table__flexible-column': flexible,\n }}\n style={width && !flexible ? { width } : undefined}\n />\n );\n })}\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 return (\n <thead\n class={{\n 'ds-table__head': true,\n 'ds-table__head--semantic-copy': !interactive,\n }}\n ref={element => {\n if (interactive) this.interactiveHeadEl = element ?? null;\n }}\n >\n <tr class=\"ds-table__header-row\" aria-rowindex={ariaRowIndex}>\n {model.selectable && (\n <th\n class=\"ds-table__header-cell ds-table__selection-cell ds-table__cell--sticky-start\"\n scope={presentational ? undefined : 'col'}\n >\n {interactive ? (\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('start')}\n </th>\n )}\n {this.visibleColumns.map(column =>\n this.renderColumnHeader(column, model, interactive, presentational)\n )}\n </tr>\n </thead>\n );\n }\n\n private renderCellValue(cell: TableCellPresentation, column: TableColumn, row: TableRow) {\n if (cell.kind === 'blank') return null;\n\n if (cell.kind === 'empty') {\n return (\n <ds-text\n class=\"ds-table__cell-track ds-table__cell-track--text\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"tertiary\"\n >\n <span aria-hidden=\"true\">—</span>\n <span class=\"ds-visually-hidden\">{this.emptyCellLabel}</span>\n </ds-text>\n );\n }\n\n if (cell.kind === 'icon') {\n const value = cell.value;\n return (\n <ds-icon\n name={value.icon}\n size=\"md\"\n color={value.color ?? 'secondary'}\n label={value.label}\n />\n );\n }\n\n if (cell.kind === 'icon-text') {\n return (\n <span class=\"ds-table__cell-icon-text\">\n <span class=\"ds-table__cell-icon-text-icon\">\n <ds-icon\n name={cell.icon}\n size=\"md\"\n color={cell.iconColor ?? 'secondary'}\n label={cell.iconLabel}\n />\n </span>\n {this.renderTextCopy(cell)}\n </span>\n );\n }\n\n if (cell.kind === 'image') {\n const value = cell.value;\n return (\n <span class=\"ds-table__cell-image\">\n {value.src ? (\n <img\n class=\"ds-table__cell-image-content\"\n src={value.src}\n alt={value.alt}\n loading=\"lazy\"\n />\n ) : (\n <span class=\"ds-table__cell-image-placeholder\" role=\"img\" aria-label={value.alt} />\n )}\n </span>\n );\n }\n\n if (cell.kind === 'action') {\n const value = cell.value;\n const menu = isRenderableTableActionMenu(value);\n const triggerId = tableActionTriggerId(this.actionMenuElementId, row.id, column.id);\n const expanded =\n menu && this.actionMenu?.rowId === row.id && this.actionMenu?.columnId === column.id;\n return (\n <ds-button-unfilled\n id={menu ? triggerId : undefined}\n variant={menu ? 'icon' : (value.variant ?? 'label')}\n size=\"md\"\n isInset={true}\n insetDepth=\"double\"\n label={value.label ?? ''}\n icon={value.icon ?? (menu ? 'Ellipses' : '')}\n aria-label={value.ariaLabel ?? null}\n hasBorder={value.hasBorder ?? false}\n isInactive={value.isInactive ?? false}\n isLoading={value.isLoading ?? false}\n hasMenu={menu}\n expanded={menu ? expanded : undefined}\n controls={menu ? this.actionMenuElementId : undefined}\n onDsClick={event => {\n event.stopPropagation();\n if (menu) {\n this.toggleActionMenu(row, column, event.detail);\n return;\n }\n if (isTableCellActionMenu(value)) return;\n this.dsCellAction.emit({\n actionId: value.actionId,\n rowId: row.id,\n columnId: column.id,\n });\n }}\n />\n );\n }\n\n if (cell.kind === 'tag') {\n const value = cell.value;\n const variant = cell.variant;\n const tag = (\n <ds-tag\n label={value.label}\n intent={value.intent ?? 'neutral'}\n contrast={value.contrast ?? 'faint'}\n size={variant === 'text-with-tag' ? 'sm' : 'md'}\n icon={value.icon ?? ''}\n rounded={value.rounded ?? false}\n isInset\n insetDepth={variant === 'text-with-tag' ? 'single' : 'double'}\n />\n );\n\n if (variant === 'tag-only') return tag;\n\n return (\n <span class={`ds-table__cell-tag-stack ds-table__cell-tag-stack--${variant}`}>\n {variant === 'tag-with-text' && tag}\n <ds-text\n class=\"ds-table__cell-tag-text ds-table__cell-track\"\n as=\"span\"\n variant={variant === 'tag-with-text' ? 'text-body-small' : 'text-body-medium'}\n color=\"secondary\"\n lineTruncation={1}\n data-table-truncate=\"\"\n >\n {value.text}\n </ds-text>\n {variant === 'text-with-tag' && (\n <span class=\"ds-table__cell-tag-control-track\">{tag}</span>\n )}\n </span>\n );\n }\n\n if (cell.kind !== 'text') return null;\n return this.renderTextCopy(cell);\n }\n\n private renderTextCopy(cell: Extract<TableCellPresentation, { kind: 'text' | 'icon-text' }>) {\n const text = cell.value;\n const wraps = cell.wraps;\n const overflow = tableCellTextOverflowProps(cell.lineClamp);\n const truncate = this.truncateAttr(cell.lineClamp);\n const primaryText = cell.kind === 'text' && cell.primaryText;\n const href = resolveSafeUrl(text.href);\n const primary = (\n <ds-text\n class=\"ds-table__cell-primary ds-table__cell-track ds-table__cell-track--text\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={href ? 'inherit' : 'primary'}\n lineTruncation={overflow.lineTruncation}\n wrap={overflow.wrap}\n fontFeature={text.fontFeature ?? 'normal'}\n data-table-truncate={truncate}\n >\n {text.primary}\n </ds-text>\n );\n\n return (\n <span class={{ 'ds-table__cell-copy': true, 'ds-table__cell-copy--wrap': wraps }}>\n {href ? (\n <a\n class=\"ds-table__cell-link ds-text-action ds-focus-ring\"\n href={href}\n target={text.target === '_blank' ? '_blank' : undefined}\n rel={text.target === '_blank' ? 'noopener noreferrer' : undefined}\n >\n {primary}\n </a>\n ) : (\n primary\n )}\n {this.renderTextTrack(text.secondary, {\n track: 'secondary',\n variant: primaryText ? 'text-body-medium' : 'text-body-small',\n defaultColor: primaryText ? 'primary' : 'secondary',\n wholeColor: text.secondaryColor,\n lineClamp: cell.lineClamp,\n })}\n {this.renderTextTrack(text.tertiary, {\n track: 'tertiary',\n variant: 'text-body-small',\n defaultColor: 'secondary',\n wholeColor: text.tertiaryColor,\n lineClamp: cell.lineClamp,\n })}\n </span>\n );\n }\n\n private renderTextTrack(\n runs: TableCellTextRun[] | undefined,\n options: {\n track: 'secondary' | 'tertiary';\n variant: 'text-body-medium' | 'text-body-small';\n defaultColor: 'primary' | 'secondary';\n wholeColor?: TableCellTextRun['color'];\n lineClamp: TableCellLineClamp;\n }\n ) {\n if (!runs?.length) return null;\n const trackClass = `ds-table__cell-${options.track} ds-table__cell-track ds-table__cell-track--text`;\n const overflow = tableCellTextOverflowProps(options.lineClamp);\n const truncate = this.truncateAttr(options.lineClamp);\n const colorFor = (run: TableCellTextRun) =>\n run.color ?? options.wholeColor ?? options.defaultColor;\n if (runs.length === 1) {\n return (\n <ds-text\n class={trackClass}\n as=\"span\"\n variant={options.variant}\n color={colorFor(runs[0])}\n lineTruncation={overflow.lineTruncation}\n wrap={overflow.wrap}\n data-table-truncate={truncate}\n >\n {runs[0].text}\n </ds-text>\n );\n }\n\n return (\n <span class={`${trackClass} ds-table__cell-track--runs`}>\n {runs.map((run, index) => [\n index > 0 && (\n <ds-text\n key={`${options.track}-sep-${index}`}\n class=\"ds-table__cell-run-separator\"\n as=\"span\"\n variant={options.variant}\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>\n ),\n <ds-text\n key={`${options.track}-run-${index}`}\n class=\"ds-table__cell-run\"\n as=\"span\"\n variant={options.variant}\n color={colorFor(run)}\n lineTruncation={overflow.lineTruncation}\n wrap={overflow.wrap}\n data-table-truncate={truncate}\n >\n {run.text}\n </ds-text>,\n ])}\n </span>\n );\n }\n\n private renderRow(\n row: TableRow,\n model: TableRenderModel,\n ariaRowIndex?: number,\n variableVirtualSize = false,\n rowKey = row.id\n ) {\n const selected = model.selectedRowIds.has(row.id);\n const rowSelectable = row.selectable !== false && !row.disabled;\n return (\n <tr\n key={rowKey}\n class={{\n 'ds-table__row': true,\n 'ds-table__row--selected': selected,\n 'ds-table__row--disabled': !!row.disabled,\n 'ds-table__row--interactive': !!row.interactive && !row.disabled,\n 'ds-focus-ring': !!row.interactive && !row.disabled,\n }}\n data-row-id={row.id}\n data-virtual-id={ariaRowIndex != null ? `row:${row.id}` : undefined}\n data-virtual-pool-key={ariaRowIndex != null ? rowKey : undefined}\n data-virtual-measure={ariaRowIndex != null && variableVirtualSize ? 'true' : undefined}\n data-selected={selected ? 'true' : undefined}\n aria-rowindex={ariaRowIndex}\n aria-disabled={row.disabled ? 'true' : undefined}\n tabIndex={row.interactive && !row.disabled ? 0 : undefined}\n onClick={event => this.emitRowActivation(row, event)}\n onKeyDown={event => this.handleRowKeydown(row, event)}\n >\n {model.selectable && (\n <td\n class={{\n 'ds-table__cell': true,\n 'ds-table__selection-cell': true,\n 'ds-table__cell--sticky-start': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--grouped': true,\n 'ds-interaction-fill--selected': selected,\n }}\n >\n {this.renderSelectionControl(\n `${selected ? 'Deselect' : 'Select'} ${tableRowSelectionLabel(row, this.visibleColumns)}`,\n selected,\n false,\n !rowSelectable,\n () => this.emitRowSelection(row)\n )}\n {this.renderStickyEdge('start')}\n </td>\n )}\n {this.visibleColumns.map(column => {\n const align = column.align ?? 'start';\n const cell = resolveTableCellPresentation(row.cells[column.id], column);\n const tagCell = cell.kind === 'tag';\n const iconCell = cell.kind === 'icon';\n const iconTextCell = cell.kind === 'icon-text';\n const imageCell = cell.kind === 'image';\n const actionCell = cell.kind === 'action';\n const actionMenuCell = actionCell && isRenderableTableActionMenu(cell.value);\n const textCell = cell.kind === 'text';\n const primaryTextCell = textCell && cell.primaryText;\n const singleTextCell = textCell && cell.singleLine;\n const emptyCell = cell.kind === 'empty';\n const blankCell = cell.kind === 'blank';\n const tagVariant = tagCell ? cell.variant : undefined;\n const textVariant = textCell ? cell.variant : undefined;\n const imageVariant = cell.kind === 'image' ? cell.variant : undefined;\n const iconTextVariant = iconTextCell ? cell.variant : undefined;\n const wraps = (cell.kind === 'text' || cell.kind === 'icon-text') && cell.wraps;\n return (\n <td\n key={`${row.id}:${column.id}`}\n class={{\n 'ds-table__cell': true,\n [`ds-table__cell--align-${align}`]: true,\n 'ds-table__cell--tag': tagCell,\n [`ds-table__cell--tag-${tagVariant}`]: tagCell,\n 'ds-table__cell--icon': iconCell,\n 'ds-table__cell--icon-text': iconTextCell,\n [`ds-table__cell--icon-text-${iconTextVariant}`]: iconTextCell,\n 'ds-table__cell--icon-text-wrap': iconTextCell && wraps,\n 'ds-table__cell--image': imageCell,\n [`ds-table__cell--image-${imageVariant}`]: imageCell,\n 'ds-table__cell--action': actionCell,\n 'ds-table__cell--action-menu': actionMenuCell,\n 'ds-table__cell--primary-text': primaryTextCell,\n 'ds-table__cell--text-single': singleTextCell,\n 'ds-table__cell--text-multi':\n textCell && !singleTextCell && textVariant !== 'triple',\n 'ds-table__cell--text-triple': textVariant === 'triple',\n 'ds-table__cell--text-wrap': textCell && wraps,\n 'ds-table__cell--empty': emptyCell,\n 'ds-table__cell--blank': blankCell,\n 'ds-table__cell--sticky-start': column.sticky === 'start',\n 'ds-table__cell--sticky-end': column.sticky === 'end',\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--grouped': true,\n 'ds-interaction-fill--selected': selected,\n }}\n data-column-id={column.id}\n data-cell-type={cell.cellType}\n data-cell-variant={tagVariant ?? textVariant ?? imageVariant ?? iconTextVariant}\n >\n <span class=\"ds-table__cell-content ds-interaction-fill__content\">\n {this.renderCellValue(cell, column, row)}\n </span>\n {this.renderStickyEdge(column.sticky)}\n </td>\n );\n })}\n </tr>\n );\n }\n\n private emitGroupCollapse(group: TableGroup) {\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 renderGroupContent(groupModel: TableRenderModel['groups'][number]) {\n const {\n group,\n countLabel,\n collapsed: isCollapsed,\n labelColor,\n selection: groupSelection,\n } = groupModel;\n return (\n <span class=\"ds-table__group-content\">\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 <span class=\"ds-table__group-copy\">\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 <ds-text\n class=\"ds-table__group-separator\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>\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-visually-hidden\">{countLabel}</span>\n </span>\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={isCollapsed ? `Expand ${group.label} group` : `Collapse ${group.label} group`}\n hasBorder={false}\n onDsClick={event => {\n event.stopPropagation();\n this.emitGroupCollapse(group);\n }}\n />\n </span>\n );\n }\n\n private formatGroupLoadLabel(template: string, group: TableGroup): string {\n return template.split('{group}').join(group.label);\n }\n\n private renderGroupLoadRow(group: TableGroup, 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 {error ? (\n <span class=\"ds-table__load-content ds-table__load-content--error\">\n <span class=\"ds-table__load-copy\">\n <ds-icon name=\"ErrorTriangle\" size=\"md\" color=\"secondary\" aria-hidden=\"true\" />\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"secondary\">\n {error}\n </ds-text>\n </span>\n <ds-button-unfilled\n label={this.formatGroupLoadLabel(this.groupRetryLabel, group)}\n size=\"md\"\n onDsClick={() => this.groupLoadController.request(group.id, 'retry')}\n />\n </span>\n ) : group.loadingMore ? (\n <span class=\"ds-table__load-content\">\n <ds-loader size=\"md\" color=\"secondary\" />\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"secondary\">\n {this.formatGroupLoadLabel(this.groupLoadingMoreLabel, group)}\n </ds-text>\n </span>\n ) : group.hasMore && manualFallback ? (\n <span class=\"ds-table__load-content\">\n <ds-button-unfilled\n label={this.groupLoadMoreLabel}\n aria-label={this.formatGroupLoadLabel(this.groupLoadMoreAriaLabel, group)}\n size=\"md\"\n onDsClick={() => this.groupLoadController.request(group.id, 'manual')}\n />\n </span>\n ) : (\n <span class=\"ds-table__auto-sentinel\" aria-hidden=\"true\" />\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 renderVirtualSpacer(\n node: Extract<TableVirtualNode, { kind: 'spacer' }>,\n totalColumns: number\n ) {\n if (node.size <= 0) return null;\n return (\n <tr class=\"ds-table__virtual-spacer-row\" key={node.key} aria-hidden=\"true\">\n <td\n class=\"ds-table__virtual-spacer-cell\"\n colSpan={totalColumns}\n style={\n { '--_table-virtual-spacer-block-size': `${node.size}px` } as Record<string, string>\n }\n />\n </tr>\n );\n }\n\n private renderVirtualRow(\n node: Extract<TableVirtualNode, { kind: 'row' }>,\n model: TableRenderModel,\n rowsById: Map<string, TableRow>,\n rowKey: string\n ) {\n const row = node.item.rowId ? rowsById.get(node.item.rowId) : undefined;\n if (!row) return null;\n return this.renderRow(row, model, node.index + 2, node.item.variableSize, rowKey);\n }\n\n private virtualRowPoolKeys(\n nodes: readonly TableVirtualNode[],\n scope: string\n ): Map<number, string> {\n const rows = nodes.filter(\n (node): node is Extract<TableVirtualNode, { kind: 'row' }> => node.kind === 'row'\n );\n const state = this.virtualRowPoolStates.get(scope) ?? {\n slotsByRowId: new Map<string, number>(),\n nextSlot: 0,\n };\n this.virtualRowPoolStates.set(scope, state);\n const desiredIds = new Set(rows.map(node => node.item.rowId ?? node.item.id));\n const freeSlots: number[] = [];\n for (const [rowId, slot] of state.slotsByRowId) {\n if (desiredIds.has(rowId)) continue;\n state.slotsByRowId.delete(rowId);\n freeSlots.push(slot);\n }\n freeSlots.sort((left, right) => left - right);\n\n const keys = new Map<number, string>();\n for (const node of rows) {\n const rowId = node.item.rowId ?? node.item.id;\n let slot = state.slotsByRowId.get(rowId);\n if (slot == null) {\n slot = freeSlots.shift() ?? state.nextSlot++;\n state.slotsByRowId.set(rowId, slot);\n }\n keys.set(node.index, `virtual-row-slot-${slot}`);\n }\n return keys;\n }\n\n private renderVirtualGroup(\n node: Extract<TableVirtualNode, { kind: 'group' }>,\n model: TableRenderModel,\n rowsById: Map<string, TableRow>,\n groupsById: Map<string, TableGroupRenderModel>\n ) {\n const groupModel = groupsById.get(node.groupId);\n if (!groupModel) return null;\n const { group, collapsed: isCollapsed, intent, intentClass } = groupModel;\n const rowKeys = this.virtualRowPoolKeys(node.nodes, `group:${group.id}`);\n return (\n <tbody\n class=\"ds-table__body ds-table__group\"\n role=\"rowgroup\"\n data-group-id={group.id}\n data-group-intent={intent}\n data-collapsed={isCollapsed ? 'true' : undefined}\n key={group.id}\n >\n <tr\n role=\"row\"\n data-virtual-id={`group:${group.id}`}\n aria-rowindex={node.headerIndex + 2}\n class={{\n 'ds-table__group-row': true,\n 'ds-table__group-row--native-sticky': this.stickyHeader && !this.documentStickyHeader,\n }}\n >\n <th\n class={{\n 'ds-table__group-cell': true,\n [intentClass ?? '']: !!intentClass,\n }}\n role=\"rowheader\"\n scope=\"rowgroup\"\n colSpan={model.totalColumns}\n >\n {this.renderGroupContent(groupModel)}\n </th>\n </tr>\n {node.nodes.map(child =>\n child.kind === 'spacer'\n ? this.renderVirtualSpacer(child, model.totalColumns)\n : this.renderVirtualRow(\n child,\n model,\n rowsById,\n rowKeys.get(child.index) ?? `virtual-row-${child.index}`\n )\n )}\n </tbody>\n );\n }\n\n private renderVirtualBodies(model: TableRenderModel, plan: TableVirtualPlan) {\n let lookup = this.virtualLookupCache;\n if (!lookup || lookup.rows !== model.loadedRows || lookup.groups !== model.groups) {\n lookup = {\n rows: model.loadedRows,\n groups: model.groups,\n rowsById: new Map(model.loadedRows.map(row => [row.id, row])),\n groupsById: new Map(model.groups.map(groupModel => [groupModel.group.id, groupModel])),\n };\n this.virtualLookupCache = lookup;\n }\n const { rowsById, groupsById } = lookup;\n if (!model.grouped) {\n for (const scope of this.virtualRowPoolStates.keys()) {\n if (scope !== 'flat') this.virtualRowPoolStates.delete(scope);\n }\n const rowKeys = this.virtualRowPoolKeys(plan.nodes, 'flat');\n return (\n <tbody class=\"ds-table__body\">\n {plan.nodes.map(node =>\n node.kind === 'spacer'\n ? this.renderVirtualSpacer(node, model.totalColumns)\n : node.kind === 'row'\n ? this.renderVirtualRow(\n node,\n model,\n rowsById,\n rowKeys.get(node.index) ?? `virtual-row-${node.index}`\n )\n : null\n )}\n </tbody>\n );\n }\n\n const activeGroupScopes = new Set(\n plan.nodes\n .filter(\n (node): node is Extract<TableVirtualNode, { kind: 'group' }> => node.kind === 'group'\n )\n .map(node => `group:${node.groupId}`)\n );\n for (const scope of this.virtualRowPoolStates.keys()) {\n if (!activeGroupScopes.has(scope)) this.virtualRowPoolStates.delete(scope);\n }\n return plan.nodes.map(node => {\n if (node.kind === 'spacer') {\n return (\n <tbody class=\"ds-table__body ds-table__virtual-pad\" aria-hidden=\"true\" key={node.key}>\n {this.renderVirtualSpacer(node, model.totalColumns)}\n </tbody>\n );\n }\n if (node.kind === 'group') {\n return this.renderVirtualGroup(node, model, rowsById, groupsById);\n }\n return null;\n });\n }\n\n private renderDataBodies(model: TableRenderModel, plan: TableVirtualPlan | null) {\n if (plan) return this.renderVirtualBodies(model, plan);\n\n if (!model.grouped) {\n return (\n <tbody class=\"ds-table__body\">{this.rows.map(row => this.renderRow(row, model))}</tbody>\n );\n }\n\n return model.groups.map(groupModel => {\n const { group, collapsed: isCollapsed, intent, intentClass } = groupModel;\n const stickySourceHidden = this.documentStickyHeader && this.activeStickyGroupId === group.id;\n return (\n <tbody\n class=\"ds-table__body ds-table__group\"\n role=\"rowgroup\"\n data-group-id={group.id}\n data-group-intent={intent}\n data-collapsed={isCollapsed ? 'true' : undefined}\n aria-busy={!isCollapsed && group.loadingMore ? 'true' : undefined}\n key={group.id}\n >\n <tr\n role=\"row\"\n aria-hidden={stickySourceHidden ? 'true' : undefined}\n class={{\n 'ds-table__group-row': true,\n 'ds-table__group-row--native-sticky': this.stickyHeader && !this.documentStickyHeader,\n }}\n >\n <th\n class={{\n 'ds-table__group-cell': true,\n 'ds-table__group-cell--sticky-source-hidden': stickySourceHidden,\n [intentClass ?? '']: !!intentClass,\n }}\n role=\"rowheader\"\n scope=\"rowgroup\"\n colSpan={model.totalColumns}\n >\n {this.renderGroupContent(groupModel)}\n </th>\n </tr>\n {!isCollapsed && group.rows.map(row => this.renderRow(row, model))}\n {!isCollapsed && this.renderGroupLoadRow(group, model.totalColumns)}\n </tbody>\n );\n });\n }\n\n private renderSkeletonBody(model: TableRenderModel) {\n const count = Math.min(20, Math.max(1, Math.round(this.skeletonRows) || 1));\n return (\n <tbody class=\"ds-table__body ds-table__skeleton-body\">\n {Array.from({ length: count }, (_, index) => (\n <tr class=\"ds-table__row ds-table__skeleton-row\" key={`skeleton-${index}`}>\n {model.selectable && (\n <td\n class=\"ds-table__cell ds-table__selection-cell ds-table__cell--sticky-start ds-table__skeleton-cell ds-interaction-fill ds-interaction-fill--grouped\"\n data-skeleton-kind=\"checkbox\"\n >\n <span class=\"ds-table__skeleton-checkbox-canvas ds-interaction-fill__content\">\n <ds-skeleton\n class=\"ds-table__skeleton-checkbox\"\n variant=\"control\"\n controlSize=\"xs\"\n width=\"var(--dimension-iconography-sm)\"\n />\n </span>\n {this.renderStickyEdge('start')}\n </td>\n )}\n {this.visibleColumns.map(column => this.renderSkeletonCell(column, index))}\n </tr>\n ))}\n </tbody>\n );\n }\n\n private renderSkeletonCell(column: TableColumn, rowIndex: number) {\n const skeleton =\n column.skeleton ??\n ((column.kind === 'action'\n ? { kind: 'action', variant: 'icon' }\n : { kind: 'text', lines: 1 }) satisfies TableCellSkeleton);\n const align = column.align ?? 'start';\n const text = skeleton.kind === 'text';\n const iconText = skeleton.kind === 'icon-text';\n const lines = text || iconText ? (skeleton.lines ?? 1) : 1;\n const tag = skeleton.kind === 'tag';\n const icon = skeleton.kind === 'icon';\n const image = skeleton.kind === 'image';\n const imageVariant = image\n ? tableCellImageVariant(resolveTableCellImageTracks(skeleton.tracks))\n : undefined;\n const iconTextVariant = iconText\n ? lines === 3\n ? 'triple'\n : lines === 2\n ? 'multi'\n : 'single'\n : undefined;\n const action = skeleton.kind === 'action';\n const blank = skeleton.kind === 'blank';\n\n return (\n <td\n class={{\n 'ds-table__cell': true,\n [`ds-table__cell--align-${align}`]: true,\n 'ds-table__skeleton-cell': true,\n 'ds-table__cell--text-single': text && lines === 1,\n 'ds-table__cell--text-multi': text && lines === 2,\n 'ds-table__cell--text-triple': text && lines === 3,\n 'ds-table__cell--tag': tag,\n 'ds-table__cell--tag-tag-only': tag,\n 'ds-table__cell--icon': icon,\n 'ds-table__cell--icon-text': iconText,\n [`ds-table__cell--icon-text-${iconTextVariant}`]: iconText,\n 'ds-table__cell--image': image,\n [`ds-table__cell--image-${imageVariant}`]: image,\n 'ds-table__cell--action': action,\n 'ds-table__cell--blank': blank,\n 'ds-table__cell--sticky-start': column.sticky === 'start',\n 'ds-table__cell--sticky-end': column.sticky === 'end',\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--grouped': true,\n }}\n data-column-id={column.id}\n data-skeleton-kind={skeleton.kind}\n data-cell-variant={imageVariant ?? iconTextVariant}\n key={`skeleton-${rowIndex}:${column.id}`}\n >\n <span class=\"ds-table__cell-content ds-interaction-fill__content\">\n {this.renderSkeletonCellContent(skeleton)}\n </span>\n {this.renderStickyEdge(column.sticky)}\n </td>\n );\n }\n\n private renderSkeletonCellContent(skeleton: TableCellSkeleton) {\n if (skeleton.kind === 'blank') return null;\n\n if (skeleton.kind === 'image') {\n return (\n <span class=\"ds-table__cell-image ds-table__skeleton-image\">\n <ds-skeleton\n class=\"ds-table__skeleton-image-shape\"\n variant=\"control\"\n controlSize=\"md\"\n width=\"100%\"\n />\n </span>\n );\n }\n\n if (skeleton.kind === 'icon') {\n return <ds-skeleton variant=\"icon\" iconSize=\"md\" rounded={skeleton.rounded ?? false} />;\n }\n\n if (skeleton.kind === 'tag') {\n return <ds-skeleton variant=\"control\" controlSize=\"sm\" width={skeleton.width ?? '64%'} />;\n }\n\n if (skeleton.kind === 'action') {\n const iconOnly = (skeleton.variant ?? 'icon') === 'icon';\n return (\n <ds-skeleton\n variant=\"control\"\n controlSize=\"sm\"\n width={skeleton.width ?? (iconOnly ? '24px' : '72%')}\n />\n );\n }\n\n const lines = skeleton.lines ?? 1;\n const copy = (\n <span class=\"ds-table__cell-copy\">\n <span class=\"ds-table__cell-primary ds-table__cell-track ds-table__cell-track--text\">\n <ds-skeleton\n variant=\"text\"\n textVariant=\"text-body-medium\"\n width={skeleton.primaryWidth ?? '100%'}\n />\n </span>\n {lines >= 2 && (\n <span class=\"ds-table__cell-secondary ds-table__cell-track ds-table__cell-track--text\">\n <ds-skeleton\n variant=\"text\"\n textVariant=\"text-body-small\"\n width={skeleton.secondaryWidth ?? '72%'}\n />\n </span>\n )}\n {lines === 3 && (\n <span class=\"ds-table__cell-tertiary ds-table__cell-track ds-table__cell-track--text\">\n <ds-skeleton\n variant=\"text\"\n textVariant=\"text-body-small\"\n width={skeleton.tertiaryWidth ?? '56%'}\n />\n </span>\n )}\n </span>\n );\n if (skeleton.kind === 'icon-text') {\n return (\n <span class=\"ds-table__cell-icon-text\">\n <span class=\"ds-table__cell-icon-text-icon\">\n <ds-skeleton variant=\"icon\" iconSize=\"md\" />\n </span>\n {copy}\n </span>\n );\n }\n return copy;\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 ? 'ErrorTriangle' : '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 {error ? (\n <span class=\"ds-table__load-content ds-table__load-content--error\">\n <span class=\"ds-table__load-copy\">\n <ds-icon name=\"ErrorTriangle\" size=\"md\" color=\"secondary\" aria-hidden=\"true\" />\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"secondary\">\n {error}\n </ds-text>\n </span>\n <ds-button-unfilled\n label={this.retryLabel}\n size=\"md\"\n onDsClick={() => this.loadController.request('retry')}\n />\n </span>\n ) : this.loadingMore ? (\n <span class=\"ds-table__load-content\">\n <ds-loader size=\"md\" color=\"secondary\" />\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"secondary\">\n {this.loadingMoreLabel}\n </ds-text>\n </span>\n ) : this.hasMore && manualFallback ? (\n <span class=\"ds-table__load-content\">\n <ds-button-unfilled\n label={this.loadMoreLabel}\n size=\"md\"\n onDsClick={() => this.loadController.request('manual')}\n />\n </span>\n ) : this.hasMore ? (\n <span class=\"ds-table__auto-sentinel\" aria-hidden=\"true\" />\n ) : (\n <ds-text\n class=\"ds-table__load-content\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n >\n {this.endOfResultsLabel}\n </ds-text>\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 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 class=\"ds-table__caption-leading\">\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 </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 && 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 <ds-button-unfilled\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 controls={this.dataModeSwitcherElementId}\n activeFill={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n this.toggleDataModeSwitcher(event.detail.detail === 0);\n }}\n />\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 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 }\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<HTMLElement & { setFocus?: () => void }>(\n `#${CSS.escape(`${this.dataModeSwitcherElementId}-trigger`)}`\n )\n ?.setFocus?.();\n });\n }\n\n private renderColumnCustomizerTrigger() {\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-button-unfilled\n id={`${this.columnCustomizerElementId}-trigger`}\n variant={this.captionCompact ? 'icon' : 'icon-label'}\n size=\"md\"\n icon=\"Table\"\n label=\"Customize\"\n aria-label=\"Customize table\"\n hasMenu={true}\n expanded={this.columnCustomizerOpen}\n controls={this.columnCustomizerElementId}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n if (this.chromeLoading) return;\n this.toggleColumnCustomizer(event.detail.detail === 0);\n }}\n />\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 items={tableColumnCustomizerMenuItems(this.columns, this.hiddenColumnIds, this.columnOrder)}\n onDsClose={() => this.closeColumnCustomizer()}\n onDsSelect={event => this.handleColumnCustomizerSelect(event.detail)}\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 }\n\n private closeColumnCustomizer(): void {\n this.columnCustomizerOpen = false;\n }\n\n private emitColumnsConfigChange(hiddenColumnIds: string[], columnOrder: string[]): void {\n this.dsColumnsConfigChange.emit({\n hiddenColumnIds: resolveTableHiddenColumnIds(this.columns, hiddenColumnIds),\n columnOrder: resolveTableColumnOrder(this.columns, columnOrder),\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.hiddenColumnIds, order);\n }\n\n private handleColumnCustomizerSelect(item: MenuItemData): void {\n const columnId = item.value;\n if (!columnId || item.isInactive) return;\n this.emitColumnsConfigChange(\n toggleTableColumnHidden(this.columns, this.hiddenColumnIds, columnId),\n resolveTableColumnOrder(this.columns, this.columnOrder)\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 virtualize = this.shouldVirtualize(model);\n const virtualViewportMissing =\n this.virtualViewportMissing && !initialLoading && (model.hasData || hasGroupedStructure);\n const virtualPlan = virtualize\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) => this.handlePaginationKeyDown(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--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--virtual': virtualize,\n 'ds-table__table--native-group-sticky':\n (model.grouped && this.stickyHeader && !this.documentStickyHeader) ||\n virtualize,\n }}\n style={model.tableStyle}\n aria-rowcount={virtualize ? 1 + this.virtualItems.length : undefined}\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 virtualize ? 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",
|
|
1298
|
+
"content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Listen,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n deriveTableSelectionState,\n formatTableResultSummary,\n formatTableTotalSummary,\n hasOwnedTableFooterSlot,\n isTableCellAction,\n nextTableSortState,\n tableColumnSize,\n tableModelIssues,\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 { 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 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 {\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 TableColumnsConfigChangeDetail,\n TableDataMode,\n TableDataModeChangeDetail,\n TableGroup,\n TableGroupCollapseChangeDetail,\n TableGroupLoadMoreDetail,\n TableGroupingState,\n TableLoadMoreDetail,\n TableLoadMoreMode,\n TablePaginationState,\n TableRow,\n TableRowActivateDetail,\n TableSelectionChangeDetail,\n TableSelectionMode,\n TableSortChangeDetail,\n TableSortState,\n} from './table-types';\n\nconst TABLE_FOOTER_SLOT_LEADING = 1;\nconst TABLE_FOOTER_SLOT_COPY = 2;\nconst TABLE_FOOTER_SLOT_TRAILING = 4;\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 /** 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: TableGroup[] = [];\n /** Controlled grouping column. Applications supply groups in their final fixed order. */\n @Prop() grouping: TableGroupingState | null = null;\n /** Controlled member-row sort state. */\n @Prop() sort: TableSortState | null = null;\n /** Controlled collapsed group identities. Groups not listed remain expanded. */\n @Prop() collapsedGroupIds: 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 /** Controlled hidden data-column identities. Action ids are ignored. */\n @Prop() hiddenColumnIds: string[] = [];\n /** Controlled data-column identities in display order. Omitted ids append in catalog order. */\n @Prop() columnOrder: 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 /** 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<TableSortChangeDetail>;\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() dsColumnsConfigChange!: EventEmitter<TableColumnsConfigChangeDetail>;\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 truncateTooltipLabel = '';\n @State() private captionCompact = false;\n @State() private columnCustomizerOpen = false;\n @State() private columnCustomizerInitialFocusVisible = false;\n @State() private dataModeSwitcherOpen = false;\n @State() private dataModeSwitcherInitialFocusVisible = false;\n @State() private virtualWindow: TableVirtualPlan | null = null;\n\n private readonly actionMenuElementId = nextTableActionMenuElementId();\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 virtualItems: TableVirtualItem[] = [];\n private visibleColumnsCache: {\n columns: TableColumn[];\n columnCustomizer: boolean;\n hiddenColumnIds: string[];\n columnOrder: string[];\n value: TableColumn[];\n } | null = null;\n private renderModelCache: {\n columns: TableColumn[];\n rows: TableRow[];\n groups: TableGroup[];\n grouped: boolean;\n selectionMode: TableSelectionMode;\n selectedRowIds: string[];\n collapsedGroupIds: string[];\n virtualCounts: boolean;\n value: TableRenderModel;\n } | null = null;\n private virtualItemsCache: {\n columns: TableColumn[];\n rows: TableRow[];\n groups: TableGroup[];\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 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 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.virtualizationEnabled,\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 this.warnModelIssues();\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 }\n\n componentDidRender(): void {\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.dataMode === 'virtual' &&\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 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.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 }\n\n private syncHeaderSlotPresence = () => {\n const header = this.el.querySelector<HTMLElement>('[slot=\"header\"]');\n this.headerPresent = !!header;\n this.headerUsesToolbar = header?.tagName === 'DS-TABLE-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('hiddenColumnIds')\n @Watch('columnOrder')\n @Watch('columnCustomizer')\n handleStructureChange(): void {\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('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 if (this.dataMode === 'virtual') 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) this.closeColumnCustomizer();\n }\n\n @Watch('dataModeSwitcher')\n @Watch('captionVisibility')\n handleDataModeSwitcherAvailability(): void {\n if (!this.showsDataModeSwitcher) this.closeDataModeSwitcher();\n }\n\n @Watch('chromeLoading')\n handleChromeLoadingChange(loading: boolean): void {\n if (!loading) return;\n this.closeColumnCustomizer();\n this.closeDataModeSwitcher();\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.hiddenColumnIds === this.hiddenColumnIds &&\n cached.columnOrder === this.columnOrder\n ) {\n return cached.value;\n }\n const value = resolveTableVisibleColumns(this.columns, {\n columnCustomizer: this.columnCustomizer,\n hiddenColumnIds: this.hiddenColumnIds,\n columnOrder: this.columnOrder,\n });\n this.visibleColumnsCache = {\n columns: this.columns,\n columnCustomizer: this.columnCustomizer,\n hiddenColumnIds: this.hiddenColumnIds,\n columnOrder: this.columnOrder,\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 this.showsDataModeSwitcher || this.showsColumnCustomizer;\n }\n\n private get documentStickyHeader(): boolean {\n return this.stickyHeader && !this.containedScroll;\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 .ds-table__header-row');\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 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 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 virtualizationEnabled(): boolean {\n return this.dataMode === 'virtual' && this.hasVirtualViewportBounds;\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 syncVirtualItems(model: TableRenderModel): void {\n if (this.dataMode !== 'virtual') {\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 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 shouldVirtualize(model: TableRenderModel): boolean {\n return (\n this.virtualizationEnabled && (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!.columnId);\n if (!groupingColumn) {\n issues.push(`Grouping references unknown column id: ${this.grouping.columnId}`);\n } else if (!visibleColumns.some(column => column.id === groupingColumn.id)) {\n issues.push(`Grouping references hidden column id: ${this.grouping.columnId}`);\n }\n }\n if (this.sort) {\n const sortColumn = this.columns.find(\n column =>\n column.id === this.sort!.columnId ||\n column.headerSegments?.some(segment => segment.sortKey === this.sort!.columnId)\n );\n if (!sortColumn) {\n issues.push(`Sorting references unknown column id: ${this.sort.columnId}`);\n } else if (!visibleColumns.some(column => column.id === sortColumn.id)) {\n issues.push(`Sorting references hidden column id: ${this.sort.columnId}`);\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 if (stickyStart.length > 1 || (this.selectable && stickyStart.length > 0)) {\n issues.push(\n 'Only one sticky start column is supported, and row selection already owns that lane.'\n );\n }\n if (stickyEnd.length > 1) issues.push('Only one sticky end column is supported.');\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 emitSort(column: TableColumn, sortKey = column.id): void {\n if (!column.sortable) return;\n this.dsSortChange.emit({ sort: nextTableSortState(this.sort, sortKey) });\n }\n\n private sortButtonLabel(column: TableColumn, sortKey = column.id, label = column.header): string {\n if (this.sort?.columnId !== 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: TableGroup): 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 emitRowActivation(row: TableRow, event: Event): void {\n if (!row.interactive || row.disabled || !this.rowEventOwnsActivation(event)) return;\n 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 }\n\n private renderOverflowActionMenu() {\n const open = this.actionMenu;\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(open && 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 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.virtualizationEnabled) 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']) {\n if (!sticky) return null;\n return (\n <span class={`ds-table__sticky-edge ds-table__sticky-edge--${sticky}`} aria-hidden=\"true\" />\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 ) {\n const groupedColumn = this.grouping?.columnId === column.id;\n const headerSegments = column.headerSegments?.length\n ? column.headerSegments\n : [{ label: column.header, sortKey: column.id }];\n const activeMemberSegment = headerSegments.find(\n segment => segment.sortKey === this.sort?.columnId\n );\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 {headerSegments.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(column, segment.sortKey, segment.label)}\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 < headerSegments.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.header\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.header.trim() && !column.headerSegments?.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 class={{\n 'ds-table__header-cell': true,\n [`ds-table__cell--align-${align}`]: true,\n 'ds-table__cell--sticky-start': column.sticky === 'start',\n 'ds-table__cell--sticky-end': column.sticky === 'end',\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.header.trim() && column.headerLabel?.trim() && (\n <span class=\"ds-visually-hidden\">{column.headerLabel}</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(column.sticky)}\n </th>\n );\n }\n\n private renderColgroup(model: TableRenderModel) {\n return (\n <colgroup>\n {model.selectable && <col class=\"ds-table__selection-column\" />}\n {this.visibleColumns.map(column => {\n const width = tableColumnSize(column);\n const flexible = column.id === model.flexibleColumnId;\n return (\n <col\n key={column.id}\n class={{\n 'ds-table__action-column': column.kind === 'action',\n 'ds-table__flexible-column': flexible,\n }}\n style={width && !flexible ? { width } : undefined}\n />\n );\n })}\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 return (\n <thead\n class={{\n 'ds-table__head': true,\n 'ds-table__head--semantic-copy': !interactive,\n }}\n ref={element => {\n if (interactive) this.interactiveHeadEl = element ?? null;\n }}\n >\n <tr class=\"ds-table__header-row\" aria-rowindex={ariaRowIndex}>\n {model.selectable && (\n <th\n class=\"ds-table__header-cell ds-table__selection-cell ds-table__cell--sticky-start\"\n scope={presentational ? undefined : 'col'}\n >\n {interactive ? (\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('start')}\n </th>\n )}\n {this.visibleColumns.map(column =>\n this.renderColumnHeader(column, model, interactive, presentational)\n )}\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 row,\n model,\n visibleColumns: this.visibleColumns,\n emptyCellLabel: this.emptyCellLabel,\n actionMenuElementId: this.actionMenuElementId,\n actionMenu: this.actionMenu,\n ariaRowIndex,\n variableVirtualSize,\n rowKey,\n renderSelectionControl: (label, checked, indeterminate, disabled, onActivate) =>\n this.renderSelectionControl(label, checked, indeterminate, disabled, onActivate),\n renderStickyEdge: sticky => this.renderStickyEdge(sticky),\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\n private emitGroupCollapse(group: TableGroup) {\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 renderGroupContent(groupModel: TableRenderModel['groups'][number]) {\n const {\n group,\n countLabel,\n collapsed: isCollapsed,\n labelColor,\n selection: groupSelection,\n } = groupModel;\n return (\n <span class=\"ds-table__group-content\">\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 <span class=\"ds-table__group-copy\">\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 <ds-text\n class=\"ds-table__group-separator\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>\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-visually-hidden\">{countLabel}</span>\n </span>\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={isCollapsed ? `Expand ${group.label} group` : `Collapse ${group.label} group`}\n hasBorder={false}\n onDsClick={event => {\n event.stopPropagation();\n this.emitGroupCollapse(group);\n }}\n />\n </span>\n );\n }\n\n private formatGroupLoadLabel(template: string, group: TableGroup): string {\n return template.split('{group}').join(group.label);\n }\n\n private renderGroupLoadRow(group: TableGroup, 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\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 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) =>\n this.renderGroupLoadRow(group, totalColumns),\n });\n }\n\n\n private renderSkeletonBody(model: TableRenderModel) {\n return renderTableSkeletonBody({\n model,\n visibleColumns: this.visibleColumns,\n skeletonRows: this.skeletonRows,\n renderStickyEdge: sticky => this.renderStickyEdge(sticky),\n });\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 ? 'ErrorTriangle' : '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\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 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 </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 && 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 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 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 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 }\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<HTMLElement & { setFocus?: () => void }>(\n `#${CSS.escape(`${this.dataModeSwitcherElementId}-trigger`)}`\n )\n ?.setFocus?.();\n });\n }\n\n private renderColumnCustomizerTrigger() {\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 id={`${this.columnCustomizerElementId}-trigger`}\n variant={this.captionCompact ? 'icon' : 'icon-label'}\n size=\"md\"\n icon=\"Table\"\n label=\"Customize\"\n labelEmphasis={false}\n pressScale={false}\n aria-label=\"Customize table\"\n hasMenu={true}\n expanded={this.columnCustomizerOpen}\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 items={tableColumnCustomizerMenuItems(this.columns, this.hiddenColumnIds, this.columnOrder)}\n onDsClose={() => this.closeColumnCustomizer()}\n onDsSelect={event => this.handleColumnCustomizerSelect(event.detail)}\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 }\n\n private closeColumnCustomizer(): void {\n this.columnCustomizerOpen = false;\n }\n\n private emitColumnsConfigChange(hiddenColumnIds: string[], columnOrder: string[]): void {\n this.dsColumnsConfigChange.emit({\n hiddenColumnIds: resolveTableHiddenColumnIds(this.columns, hiddenColumnIds),\n columnOrder: resolveTableColumnOrder(this.columns, columnOrder),\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.hiddenColumnIds, order);\n }\n\n private handleColumnCustomizerSelect(item: MenuItemData): void {\n const columnId = item.value;\n if (!columnId || item.isInactive) return;\n this.emitColumnsConfigChange(\n toggleTableColumnHidden(this.columns, this.hiddenColumnIds, columnId),\n resolveTableColumnOrder(this.columns, this.columnOrder)\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 virtualize = this.shouldVirtualize(model);\n const virtualViewportMissing =\n this.virtualViewportMissing && !initialLoading && (model.hasData || hasGroupedStructure);\n const virtualPlan = virtualize\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) => this.handlePaginationKeyDown(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--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--virtual': virtualize,\n 'ds-table__table--native-group-sticky':\n (model.grouped && this.stickyHeader && !this.documentStickyHeader) ||\n virtualize,\n }}\n style={model.tableStyle}\n aria-rowcount={virtualize ? 1 + this.virtualItems.length : undefined}\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 virtualize ? 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",
|
|
1299
1299
|
"type": "registry:ui"
|
|
1300
1300
|
},
|
|
1301
1301
|
{
|
|
@@ -1303,6 +1303,11 @@
|
|
|
1303
1303
|
"content": "import type { MenuItemData, MenuItemsSection } from '../Menu/menu-types';\nimport type { TableCellAction, TableCellActionMenuEntry } from './table-types';\n\nlet actionMenuSeq = 0;\n\nexport function nextTableActionMenuElementId(): string {\n actionMenuSeq += 1;\n return `ds-table-action-menu-${actionMenuSeq}`;\n}\n\nexport function isTableCellActionMenu(\n value: TableCellAction,\n): value is TableCellAction & { items: TableCellActionMenuEntry[]; ariaLabel: string } {\n return Array.isArray(value.items);\n}\n\nexport function isRenderableTableActionMenu(\n value: TableCellAction,\n): value is TableCellAction & { items: TableCellActionMenuEntry[]; ariaLabel: string } {\n return isTableCellActionMenu(value) && tableActionMenuSections(value.items).length > 0;\n}\n\n/** Split divider entries into Menu sections. Empty runs (leading/repeated dividers) are dropped. */\nexport function tableActionMenuSections(\n items: TableCellActionMenuEntry[],\n): MenuItemsSection[] {\n const sections: MenuItemsSection[] = [];\n let current: MenuItemData[] = [];\n const flush = () => {\n if (!current.length) return;\n sections.push({ items: current });\n current = [];\n };\n\n for (const entry of items) {\n if ('kind' in entry) {\n flush();\n continue;\n }\n current.push({\n value: entry.actionId,\n label: entry.label,\n ...(entry.isInactive ? { isInactive: true } : {}),\n ...(entry.isDestructive ? { isDestructive: true } : {}),\n });\n }\n flush();\n return sections;\n}\n\nexport function tableActionTriggerId(menuId: string, rowId: string, columnId: string): string {\n return `${menuId}-${sanitizeId(rowId)}-${sanitizeId(columnId)}`;\n}\n\nfunction sanitizeId(value: string): string {\n return value.replace(/[^A-Za-z0-9_-]+/g, '-');\n}\n",
|
|
1304
1304
|
"type": "registry:ui"
|
|
1305
1305
|
},
|
|
1306
|
+
{
|
|
1307
|
+
"path": "src/wc/components/Table/table-body-renderer.tsx",
|
|
1308
|
+
"content": "import { h } from '@stencil/core';\nimport type { TableGroupRenderModel, TableRenderModel } from './table-render-model';\nimport type { TableGroup, TableRow } from './table-types';\nimport type { TableVirtualNode, TableVirtualPlan } from './table-virtual-model';\n\nexport interface TableVirtualRowPoolState {\n slotsByRowId: Map<string, number>;\n nextSlot: number;\n}\n\nexport interface TableBodyRenderOptions {\n model: TableRenderModel;\n plan: TableVirtualPlan | null;\n rows: TableRow[];\n stickyHeader: boolean;\n documentStickyHeader: boolean;\n activeStickyGroupId: string | null;\n renderRow: (\n row: TableRow,\n model: TableRenderModel,\n ariaRowIndex?: number,\n variableVirtualSize?: boolean,\n rowKey?: string,\n ) => unknown;\n renderGroupContent: (group: TableGroupRenderModel) => unknown;\n renderGroupLoadRow: (group: TableGroup, totalColumns: number) => unknown;\n}\n\n/**\n * Assign stable, bounded DOM keys to the rows currently occupying one virtual scope.\n * Departed rows release their slots so long scrolls recycle DOM instead of growing it.\n */\nexport function assignTableVirtualRowPoolKeys(\n nodes: readonly TableVirtualNode[],\n state: TableVirtualRowPoolState,\n): Map<number, string> {\n const rows = nodes.filter(\n (node): node is Extract<TableVirtualNode, { kind: 'row' }> => node.kind === 'row',\n );\n const desiredIds = new Set(rows.map(node => node.item.rowId ?? node.item.id));\n const freeSlots: number[] = [];\n for (const [rowId, slot] of state.slotsByRowId) {\n if (desiredIds.has(rowId)) continue;\n state.slotsByRowId.delete(rowId);\n freeSlots.push(slot);\n }\n freeSlots.sort((left, right) => left - right);\n\n const keys = new Map<number, string>();\n for (const node of rows) {\n const rowId = node.item.rowId ?? node.item.id;\n let slot = state.slotsByRowId.get(rowId);\n if (slot == null) {\n slot = freeSlots.shift() ?? state.nextSlot++;\n state.slotsByRowId.set(rowId, slot);\n }\n keys.set(node.index, `virtual-row-slot-${slot}`);\n }\n return keys;\n}\n\n/** Owns grouped and virtual tbody composition while Table remains the behavior orchestrator. */\nexport class TableBodyRenderer {\n private virtualLookupCache: {\n rows: TableRow[];\n groups: TableGroupRenderModel[];\n rowsById: Map<string, TableRow>;\n groupsById: Map<string, TableGroupRenderModel>;\n } | null = null;\n\n private readonly virtualRowPoolStates = new Map<string, TableVirtualRowPoolState>();\n\n resetVirtualState(): void {\n this.virtualLookupCache = null;\n this.virtualRowPoolStates.clear();\n }\n\n render(options: TableBodyRenderOptions) {\n if (options.plan) return this.renderVirtualBodies(options);\n\n const { model } = options;\n if (!model.grouped) {\n return (\n <tbody class=\"ds-table__body\">\n {options.rows.map(row => options.renderRow(row, model))}\n </tbody>\n );\n }\n\n return model.groups.map(groupModel => {\n const { group, collapsed: isCollapsed, intent, intentClass } = groupModel;\n const stickySourceHidden =\n options.documentStickyHeader && options.activeStickyGroupId === group.id;\n return (\n <tbody\n class=\"ds-table__body ds-table__group\"\n role=\"rowgroup\"\n data-group-id={group.id}\n data-group-intent={intent}\n data-collapsed={isCollapsed ? 'true' : undefined}\n aria-busy={!isCollapsed && group.loadingMore ? 'true' : undefined}\n key={group.id}\n >\n <tr\n role=\"row\"\n aria-hidden={stickySourceHidden ? 'true' : undefined}\n class={{\n 'ds-table__group-row': true,\n 'ds-table__group-row--native-sticky':\n options.stickyHeader && !options.documentStickyHeader,\n }}\n >\n <th\n class={{\n 'ds-table__group-cell': true,\n 'ds-table__group-cell--sticky-source-hidden': stickySourceHidden,\n [intentClass ?? '']: !!intentClass,\n }}\n role=\"rowheader\"\n scope=\"rowgroup\"\n colSpan={model.totalColumns}\n >\n {options.renderGroupContent(groupModel)}\n </th>\n </tr>\n {!isCollapsed && group.rows.map(row => options.renderRow(row, model))}\n {!isCollapsed && options.renderGroupLoadRow(group, model.totalColumns)}\n </tbody>\n );\n });\n }\n\n private renderVirtualSpacer(\n node: Extract<TableVirtualNode, { kind: 'spacer' }>,\n totalColumns: number,\n ) {\n if (node.size <= 0) return null;\n return (\n <tr class=\"ds-table__virtual-spacer-row\" key={node.key} aria-hidden=\"true\">\n <td\n class=\"ds-table__virtual-spacer-cell\"\n colSpan={totalColumns}\n style={\n { '--_table-virtual-spacer-block-size': `${node.size}px` } as Record<string, string>\n }\n />\n </tr>\n );\n }\n\n private renderVirtualRow(\n node: Extract<TableVirtualNode, { kind: 'row' }>,\n options: TableBodyRenderOptions,\n rowsById: Map<string, TableRow>,\n rowKey: string,\n ) {\n const row = node.item.rowId ? rowsById.get(node.item.rowId) : undefined;\n if (!row) return null;\n return options.renderRow(\n row,\n options.model,\n node.index + 2,\n node.item.variableSize,\n rowKey,\n );\n }\n\n private virtualRowPoolKeys(nodes: readonly TableVirtualNode[], scope: string) {\n const state = this.virtualRowPoolStates.get(scope) ?? {\n slotsByRowId: new Map<string, number>(),\n nextSlot: 0,\n };\n this.virtualRowPoolStates.set(scope, state);\n return assignTableVirtualRowPoolKeys(nodes, state);\n }\n\n private renderVirtualGroup(\n node: Extract<TableVirtualNode, { kind: 'group' }>,\n options: TableBodyRenderOptions,\n rowsById: Map<string, TableRow>,\n groupsById: Map<string, TableGroupRenderModel>,\n ) {\n const groupModel = groupsById.get(node.groupId);\n if (!groupModel) return null;\n const { group, collapsed: isCollapsed, intent, intentClass } = groupModel;\n const rowKeys = this.virtualRowPoolKeys(node.nodes, `group:${group.id}`);\n return (\n <tbody\n class=\"ds-table__body ds-table__group\"\n role=\"rowgroup\"\n data-group-id={group.id}\n data-group-intent={intent}\n data-collapsed={isCollapsed ? 'true' : undefined}\n key={group.id}\n >\n <tr\n role=\"row\"\n data-virtual-id={`group:${group.id}`}\n aria-rowindex={node.headerIndex + 2}\n class={{\n 'ds-table__group-row': true,\n 'ds-table__group-row--native-sticky':\n options.stickyHeader && !options.documentStickyHeader,\n }}\n >\n <th\n class={{\n 'ds-table__group-cell': true,\n [intentClass ?? '']: !!intentClass,\n }}\n role=\"rowheader\"\n scope=\"rowgroup\"\n colSpan={options.model.totalColumns}\n >\n {options.renderGroupContent(groupModel)}\n </th>\n </tr>\n {node.nodes.map(child =>\n child.kind === 'spacer'\n ? this.renderVirtualSpacer(child, options.model.totalColumns)\n : this.renderVirtualRow(\n child,\n options,\n rowsById,\n rowKeys.get(child.index) ?? `virtual-row-${child.index}`,\n ),\n )}\n </tbody>\n );\n }\n\n private renderVirtualBodies(options: TableBodyRenderOptions) {\n const { model, plan } = options;\n if (!plan) return null;\n let lookup = this.virtualLookupCache;\n if (!lookup || lookup.rows !== model.loadedRows || lookup.groups !== model.groups) {\n lookup = {\n rows: model.loadedRows,\n groups: model.groups,\n rowsById: new Map(model.loadedRows.map(row => [row.id, row])),\n groupsById: new Map(model.groups.map(groupModel => [groupModel.group.id, groupModel])),\n };\n this.virtualLookupCache = lookup;\n }\n const { rowsById, groupsById } = lookup;\n if (!model.grouped) {\n for (const scope of this.virtualRowPoolStates.keys()) {\n if (scope !== 'flat') this.virtualRowPoolStates.delete(scope);\n }\n const rowKeys = this.virtualRowPoolKeys(plan.nodes, 'flat');\n return (\n <tbody class=\"ds-table__body\">\n {plan.nodes.map(node =>\n node.kind === 'spacer'\n ? this.renderVirtualSpacer(node, model.totalColumns)\n : node.kind === 'row'\n ? this.renderVirtualRow(\n node,\n options,\n rowsById,\n rowKeys.get(node.index) ?? `virtual-row-${node.index}`,\n )\n : null,\n )}\n </tbody>\n );\n }\n\n const activeGroupScopes = new Set(\n plan.nodes\n .filter(\n (node): node is Extract<TableVirtualNode, { kind: 'group' }> => node.kind === 'group',\n )\n .map(node => `group:${node.groupId}`),\n );\n for (const scope of this.virtualRowPoolStates.keys()) {\n if (!activeGroupScopes.has(scope)) this.virtualRowPoolStates.delete(scope);\n }\n return plan.nodes.map(node => {\n if (node.kind === 'spacer') {\n return (\n <tbody class=\"ds-table__body ds-table__virtual-pad\" aria-hidden=\"true\" key={node.key}>\n {this.renderVirtualSpacer(node, model.totalColumns)}\n </tbody>\n );\n }\n if (node.kind === 'group') {\n return this.renderVirtualGroup(node, options, rowsById, groupsById);\n }\n return null;\n });\n }\n}\n",
|
|
1309
|
+
"type": "registry:ui"
|
|
1310
|
+
},
|
|
1306
1311
|
{
|
|
1307
1312
|
"path": "src/wc/components/Table/table-cell-model.ts",
|
|
1308
1313
|
"content": "import {\n isTableCellAction,\n isTableCellBlank,\n isTableCellEmpty,\n isTableCellIcon,\n isTableCellIconText,\n isTableCellImage,\n isTableCellPrimaryText,\n isTableCellTag,\n isTableCellText,\n} from './table-model';\nimport type {\n TableCellAction,\n TableCellBlank,\n TableCellEmpty,\n TableCellIcon,\n TableCellImage,\n TableCellImageTracks,\n TableCellTag,\n TableCellTagVariant,\n TableCellTextRun,\n TableCellTextTrack,\n TableCellValue,\n TableCellLinkTarget,\n TableCellLineClamp,\n TableCellMaxLines,\n TableColumn,\n} from './table-types';\nimport type { IconColor } from '../Icon/Icon';\nimport type { TextColor } from '../Text/text-types';\n\n/** Maximum independently colored runs on one secondary or tertiary track. */\nexport const TABLE_CELL_TEXT_RUN_LIMIT = 3;\n\nexport interface ResolvedTableCellText {\n primary: string | number;\n secondary?: TableCellTextRun[];\n tertiary?: TableCellTextRun[];\n secondaryColor?: TextColor;\n tertiaryColor?: TextColor;\n href?: string;\n target?: TableCellLinkTarget;\n wrap?: boolean;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\nexport type TableCellPresentation =\n | { kind: 'blank'; cellType: 'blank'; value: TableCellBlank }\n | { kind: 'empty'; cellType: 'empty'; value: TableCellEmpty | null | undefined }\n | { kind: 'icon'; cellType: 'icon'; value: TableCellIcon }\n | {\n kind: 'icon-text';\n cellType: 'icon-text';\n icon: string;\n iconColor?: IconColor;\n iconLabel?: string;\n value: ResolvedTableCellText;\n variant: 'single' | 'multi' | 'triple';\n wraps: boolean;\n lineClamp: TableCellLineClamp;\n }\n | {\n kind: 'image';\n cellType: 'image';\n value: TableCellImage;\n variant: 'single' | 'multi' | 'triple';\n }\n | { kind: 'action'; cellType: 'action'; value: TableCellAction }\n | {\n kind: 'tag';\n cellType: 'tag';\n value: TableCellTag;\n variant: TableCellTagVariant;\n }\n | {\n kind: 'text';\n cellType: 'text' | 'primary-text';\n value: ResolvedTableCellText;\n primaryText: boolean;\n singleLine: boolean;\n variant: 'single' | 'multi' | 'triple' | 'primary-pair';\n wraps: boolean;\n lineClamp: TableCellLineClamp;\n };\n\nfunction trackText(value: string | number | TableCellTextRun): string | undefined {\n if (typeof value === 'string' || typeof value === 'number') {\n const text = String(value);\n return text.trim() === '' ? undefined : text;\n }\n const text = value.text?.trim();\n return text ? value.text : undefined;\n}\n\n/** Resolve an image cell's track stack; unknown values fall back to one track. */\nexport function resolveTableCellImageTracks(\n tracks: TableCellImage['tracks'],\n): TableCellImageTracks {\n return tracks === 2 || tracks === 3 ? tracks : 1;\n}\n\n/** Map an image track stack onto the shared single/multi/triple cell variant. */\nexport function tableCellImageVariant(\n tracks: TableCellImageTracks,\n): 'single' | 'multi' | 'triple' {\n return tracks === 3 ? 'triple' : tracks === 2 ? 'multi' : 'single';\n}\n\n/** Collapse a consumer track into at most three non-empty runs. */\nexport function normalizeTableCellTextTrack(\n track: TableCellTextTrack | number | undefined,\n): TableCellTextRun[] | undefined {\n if (track == null) return undefined;\n const items = Array.isArray(track) ? track : [track];\n const runs: TableCellTextRun[] = [];\n for (const item of items) {\n if (runs.length >= TABLE_CELL_TEXT_RUN_LIMIT) break;\n const text = trackText(item);\n if (text === undefined) continue;\n runs.push(\n typeof item === 'object' && item !== null && 'color' in item && item.color\n ? { text, color: item.color }\n : { text },\n );\n }\n return runs.length ? runs : undefined;\n}\n\n/** Map wrap / maxLines onto wrap-to-track geometry and a line clamp. */\nexport function resolveTableCellTextOverflow(\n source: { wrap?: boolean; maxLines?: TableCellMaxLines },\n column: TableColumn,\n): { wraps: boolean; lineClamp: TableCellLineClamp } {\n const wrap = source.wrap ?? column.wrap ?? false;\n const maxLines = source.maxLines ?? (source.wrap === true ? undefined : column.maxLines);\n if (maxLines != null) {\n return { wraps: maxLines > 1, lineClamp: maxLines };\n }\n if (wrap) return { wraps: true, lineClamp: 'none' };\n return { wraps: false, lineClamp: 1 };\n}\n\n/** ds-text overflow props for a resolved clamp. */\nexport function tableCellTextOverflowProps(lineClamp: TableCellLineClamp): {\n lineTruncation: TableCellLineClamp;\n wrap: 'wrap' | 'nowrap';\n} {\n if (lineClamp === 'none') return { lineTruncation: 'none', wrap: 'wrap' };\n if (lineClamp === 1) return { lineTruncation: 1, wrap: 'nowrap' };\n return { lineTruncation: lineClamp, wrap: 'wrap' };\n}\n\nfunction resolveTextPresentation(\n source: {\n primary: string | number;\n secondary?: TableCellTextTrack | number;\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 column: TableColumn,\n options: { primaryText: boolean; allowTertiary: boolean },\n): {\n value: ResolvedTableCellText;\n singleLine: boolean;\n variant: 'single' | 'multi' | 'triple' | 'primary-pair';\n wraps: boolean;\n lineClamp: TableCellLineClamp;\n} {\n const secondary = normalizeTableCellTextTrack(source.secondary);\n const tertiary = options.allowTertiary\n ? normalizeTableCellTextTrack(source.tertiary)\n : undefined;\n const value: ResolvedTableCellText = {\n primary: source.primary,\n ...(secondary ? { secondary } : {}),\n ...(tertiary ? { tertiary } : {}),\n ...(source.secondaryColor ? { secondaryColor: source.secondaryColor } : {}),\n ...(source.tertiaryColor ? { tertiaryColor: source.tertiaryColor } : {}),\n ...(source.href ? { href: source.href } : {}),\n ...(source.target ? { target: source.target } : {}),\n ...(source.wrap ? { wrap: source.wrap } : {}),\n ...(source.fontFeature ? { fontFeature: source.fontFeature } : {}),\n };\n const singleLine = !options.primaryText && !secondary && !tertiary;\n const overflow = resolveTableCellTextOverflow(source, column);\n return {\n value,\n singleLine,\n variant: options.primaryText\n ? 'primary-pair'\n : tertiary\n ? 'triple'\n : singleLine\n ? 'single'\n : 'multi',\n wraps: overflow.wraps,\n lineClamp: overflow.lineClamp,\n };\n}\n\n/** Resolve a cell's semantic and visual recipe once for both markup and classes. */\nexport function resolveTableCellPresentation(\n value: TableCellValue,\n column: TableColumn,\n): TableCellPresentation {\n if (isTableCellBlank(value)) return { kind: 'blank', cellType: 'blank', value };\n if (value == null || isTableCellEmpty(value)) {\n return { kind: 'empty', cellType: 'empty', value };\n }\n if (isTableCellIcon(value)) return { kind: 'icon', cellType: 'icon', value };\n if (isTableCellIconText(value)) {\n const text = resolveTextPresentation(value, column, {\n primaryText: false,\n allowTertiary: true,\n });\n return {\n kind: 'icon-text',\n cellType: 'icon-text',\n icon: value.icon,\n ...(value.iconColor ? { iconColor: value.iconColor } : {}),\n ...(value.iconLabel ? { iconLabel: value.iconLabel } : {}),\n value: text.value,\n variant: text.variant === 'primary-pair' ? 'single' : text.variant,\n wraps: text.wraps,\n lineClamp: text.lineClamp,\n };\n }\n if (isTableCellImage(value)) {\n return {\n kind: 'image',\n cellType: 'image',\n value,\n variant: tableCellImageVariant(resolveTableCellImageTracks(value.tracks)),\n };\n }\n if (isTableCellAction(value)) return { kind: 'action', cellType: 'action', value };\n if (isTableCellTag(value)) {\n return {\n kind: 'tag',\n cellType: 'tag',\n value,\n variant: value.variant ?? 'tag-only',\n };\n }\n\n const primaryText = isTableCellPrimaryText(value);\n const source = isTableCellText(value) || primaryText\n ? value\n : {\n primary: value,\n fontFeature: typeof value === 'number' ? 'tabular-nums' as const : 'normal' as const,\n };\n const text = resolveTextPresentation(\n isTableCellText(value)\n ? value\n : source,\n column,\n { primaryText, allowTertiary: isTableCellText(value) },\n );\n return {\n kind: 'text',\n cellType: primaryText ? 'primary-text' : 'text',\n value: text.value,\n primaryText,\n singleLine: text.singleLine,\n variant: text.variant,\n wraps: text.wraps,\n lineClamp: text.lineClamp,\n };\n}\n",
|
|
@@ -1333,6 +1338,11 @@
|
|
|
1333
1338
|
"content": "import type {\n TableLoadMoreDetail,\n TableLoadMoreMode,\n TableLoadMoreReason,\n} from './table-types';\n\nexport interface TableLoadControllerState {\n lazyLoading: boolean;\n loadMoreMode: TableLoadMoreMode;\n hasMore: boolean;\n loadingMore: boolean;\n loadMoreError: string | undefined;\n loadIdentity: string | number;\n loadMoreThreshold: number;\n containedScroll: boolean;\n loadingMoreLabel: string;\n endOfResultsLabel: string;\n rowsLoadedLabel: string;\n loadedRowCount: number;\n viewport: HTMLElement | null;\n sentinel: HTMLElement | null;\n}\n\nexport interface TableLoadControllerOptions {\n state: () => TableLoadControllerState;\n announce: (message: string) => void;\n request: (detail: TableLoadMoreDetail) => void;\n}\n\n/** Owns incremental-loading request guards, announcements, and observation. */\nexport class TableLoadController {\n readonly intersectionSupported = typeof IntersectionObserver !== 'undefined';\n\n private connected = false;\n private intersectionObserver: IntersectionObserver | null = null;\n private observedSentinel: HTMLElement | null = null;\n private requestPending = false;\n private requestedRowCount = 0;\n private previousLoadedRowCount = 0;\n\n constructor(private readonly options: TableLoadControllerOptions) {}\n\n initialize(): void {\n this.previousLoadedRowCount = this.options.state().loadedRowCount;\n }\n\n connect(): void {\n this.connected = true;\n this.refresh();\n }\n\n disconnect(): void {\n this.connected = false;\n this.disconnectObserver();\n }\n\n refresh(): void {\n if (!this.connected) return;\n const state = this.options.state();\n if (\n !state.lazyLoading ||\n state.loadMoreMode !== 'auto' ||\n !state.hasMore ||\n !!state.loadMoreError?.trim() ||\n !this.intersectionSupported ||\n !state.viewport ||\n !state.sentinel\n ) {\n this.disconnectObserver();\n return;\n }\n\n if (this.intersectionObserver && this.observedSentinel === state.sentinel) return;\n this.disconnectObserver();\n const threshold = Number.isFinite(state.loadMoreThreshold)\n ? Math.max(0, state.loadMoreThreshold)\n : 0;\n this.intersectionObserver = new IntersectionObserver(\n entries => {\n if (entries.some(entry => entry.isIntersecting)) this.request('auto');\n },\n {\n root: state.containedScroll ? state.viewport : null,\n rootMargin: `0px 0px ${threshold}px 0px`,\n },\n );\n this.observedSentinel = state.sentinel;\n this.intersectionObserver.observe(state.sentinel);\n }\n\n structureChanged(): void {\n this.resetRequest();\n }\n\n dataChanged(): void {\n const state = this.options.state();\n const nextCount = state.loadedRowCount;\n if (!state.lazyLoading) {\n this.previousLoadedRowCount = nextCount;\n this.resetRequest();\n return;\n }\n if (nextCount > this.previousLoadedRowCount) {\n const added = nextCount - this.previousLoadedRowCount;\n this.options.announce(\n state.rowsLoadedLabel\n .replace('{count}', String(added))\n .replace('{total}', String(nextCount)),\n );\n }\n if (nextCount > this.requestedRowCount) this.requestPending = false;\n this.previousLoadedRowCount = nextCount;\n this.disconnectObserver();\n }\n\n identityChanged(): void {\n this.previousLoadedRowCount = this.options.state().loadedRowCount;\n this.resetRequest();\n }\n\n configurationChanged(): void {\n this.disconnectObserver();\n }\n\n loadingChanged(loading: boolean): void {\n const state = this.options.state();\n if (!state.lazyLoading) {\n this.resetRequest();\n return;\n }\n if (loading) {\n this.requestPending = true;\n this.options.announce(state.loadingMoreLabel);\n } else if (state.loadMoreMode === 'manual') {\n // A completed manual request may append no rows; permit another explicit activation.\n this.requestPending = false;\n }\n this.disconnectObserver();\n }\n\n errorChanged(error: string | undefined): void {\n if (!this.options.state().lazyLoading) {\n this.resetRequest();\n return;\n }\n if (error?.trim()) {\n this.requestPending = false;\n this.options.announce(error);\n }\n this.disconnectObserver();\n }\n\n hasMoreChanged(hasMore: boolean, hadMore: boolean): void {\n if (!this.options.state().lazyLoading) {\n this.resetRequest();\n return;\n }\n if (!hasMore) {\n this.requestPending = false;\n if (hadMore) this.options.announce(this.options.state().endOfResultsLabel);\n }\n this.disconnectObserver();\n }\n\n request(reason: TableLoadMoreReason): void {\n const state = this.options.state();\n if (\n !state.lazyLoading ||\n !state.hasMore ||\n state.loadingMore ||\n (reason !== 'retry' && !!state.loadMoreError?.trim()) ||\n this.requestPending\n ) return;\n\n this.requestPending = true;\n this.requestedRowCount = state.loadedRowCount;\n this.options.announce(state.loadingMoreLabel);\n this.options.request({\n reason,\n loadIdentity: state.loadIdentity,\n loadedRowCount: this.requestedRowCount,\n });\n }\n\n private resetRequest(): void {\n this.requestPending = false;\n this.requestedRowCount = this.options.state().loadedRowCount;\n this.disconnectObserver();\n }\n\n private disconnectObserver(): void {\n this.intersectionObserver?.disconnect();\n this.intersectionObserver = null;\n this.observedSentinel = null;\n }\n}\n",
|
|
1334
1339
|
"type": "registry:ui"
|
|
1335
1340
|
},
|
|
1341
|
+
{
|
|
1342
|
+
"path": "src/wc/components/Table/table-load-view.tsx",
|
|
1343
|
+
"content": "import { h } from '@stencil/core';\n\ninterface TableLoadContentOptions {\n error?: string;\n loading: boolean;\n hasMore: boolean;\n manualFallback: boolean;\n retryLabel: string;\n loadingLabel: string;\n loadMoreLabel: string;\n loadMoreAriaLabel?: string;\n endOfResultsLabel?: string;\n onRetry: () => void;\n onLoadMore: () => void;\n}\n\n/** Shared visible state machine for top-level and per-group incremental loading bands. */\nexport function renderTableLoadContent(options: TableLoadContentOptions) {\n if (options.error) {\n return (\n <span class=\"ds-table__load-content ds-table__load-content--error\">\n <span class=\"ds-table__load-copy\">\n <ds-icon name=\"ErrorTriangle\" size=\"md\" color=\"secondary\" aria-hidden=\"true\" />\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"secondary\">\n {options.error}\n </ds-text>\n </span>\n <ds-button-unfilled label={options.retryLabel} size=\"md\" onDsClick={options.onRetry} />\n </span>\n );\n }\n\n if (options.loading) {\n return (\n <span class=\"ds-table__load-content\">\n <ds-loader size=\"md\" color=\"secondary\" />\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"secondary\">\n {options.loadingLabel}\n </ds-text>\n </span>\n );\n }\n\n if (options.hasMore && options.manualFallback) {\n return (\n <span class=\"ds-table__load-content\">\n <ds-button-unfilled\n label={options.loadMoreLabel}\n aria-label={options.loadMoreAriaLabel}\n size=\"md\"\n onDsClick={options.onLoadMore}\n />\n </span>\n );\n }\n\n if (options.hasMore) return <span class=\"ds-table__auto-sentinel\" aria-hidden=\"true\" />;\n if (!options.endOfResultsLabel) return null;\n return (\n <ds-text\n class=\"ds-table__load-content\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n >\n {options.endOfResultsLabel}\n </ds-text>\n );\n}\n",
|
|
1344
|
+
"type": "registry:ui"
|
|
1345
|
+
},
|
|
1336
1346
|
{
|
|
1337
1347
|
"path": "src/wc/components/Table/table-model.ts",
|
|
1338
1348
|
"content": "import type {\n TableCellBlank,\n TableCellAction,\n TableCellEmpty,\n TableCellIcon,\n TableCellIconText,\n TableCellImage,\n TableCellPrimaryText,\n TableCellText,\n TableCellTag,\n TableCellValue,\n TableColumn,\n TableGroup,\n TableRow,\n TableSortState,\n} from './table-types';\n\nexport interface TableSelectionState {\n selectableRowIds: string[];\n selectedLoadedCount: number;\n allSelected: boolean;\n indeterminate: boolean;\n}\n\nconst TABLE_COLUMN_WIDTH_TOKENS = {\n xs: '--dimension-table-column-width-xs',\n sm: '--dimension-table-column-width-sm',\n md: '--dimension-table-column-width-md',\n lg: '--dimension-table-column-width-lg',\n xl: '--dimension-table-column-width-xl',\n} as const;\n\nconst TABLE_IMAGE_COLUMN_WIDTH_VARIABLES = {\n 1: '--_table-image-column-inline-size',\n 2: '--_table-image-column-inline-size-multi',\n 3: '--_table-image-column-inline-size-triple',\n} as const;\n\nexport function isTableCellText(value: TableCellValue): value is TableCellText {\n return typeof value === 'object' && value !== null && 'primary' in value && !('kind' in value);\n}\n\nexport function isTableCellPrimaryText(value: TableCellValue): value is TableCellPrimaryText {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'primary-text';\n}\n\nexport function isTableCellTag(value: TableCellValue): value is TableCellTag {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'tag';\n}\n\nexport function isTableCellIcon(value: TableCellValue): value is TableCellIcon {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'icon';\n}\n\nexport function isTableCellIconText(value: TableCellValue): value is TableCellIconText {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'icon-text';\n}\n\nexport function isTableCellImage(value: TableCellValue): value is TableCellImage {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'image';\n}\n\nexport function isTableCellAction(value: TableCellValue): value is TableCellAction {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'action';\n}\n\nexport function isTableCellEmpty(value: TableCellValue): value is TableCellEmpty {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'empty';\n}\n\nexport function isTableCellBlank(value: TableCellValue): value is TableCellBlank {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'blank';\n}\n\nexport function tableCellPrimary(value: TableCellValue): string | number | null {\n if (value == null) return null;\n if (isTableCellTag(value)) return value.label;\n if (isTableCellIcon(value)) return null;\n if (isTableCellIconText(value)) return value.primary;\n if (isTableCellImage(value)) return value.alt;\n if (isTableCellAction(value)) return value.label ?? value.ariaLabel;\n if (isTableCellPrimaryText(value)) return value.primary;\n if (isTableCellEmpty(value) || isTableCellBlank(value)) return null;\n return isTableCellText(value) ? value.primary : value;\n}\n\nexport function tableRowSelectionLabel(row: TableRow, columns: TableColumn[]): string {\n const explicit = row.selectionLabel?.trim();\n if (explicit) return explicit;\n\n for (const column of columns) {\n const primary = tableCellPrimary(row.cells[column.id]);\n if (primary != null && String(primary).trim()) return String(primary);\n }\n\n return row.id;\n}\n\nexport function nextTableSortState(\n current: TableSortState | null | undefined,\n columnId: string,\n): TableSortState {\n if (current?.columnId !== columnId) return { columnId, direction: 'asc' };\n if (current.direction === 'asc') return { columnId, direction: 'desc' };\n return { columnId, direction: 'asc' };\n}\n\nexport function toggleTableGroupCollapsed(\n collapsedGroupIds: string[],\n groupId: string,\n): string[] {\n const next = new Set(collapsedGroupIds);\n if (next.has(groupId)) next.delete(groupId);\n else next.add(groupId);\n return [...next];\n}\n\nexport function nextTableGroupsCollapsed(\n collapsedGroupIds: string[],\n groupIds: string[],\n): string[] {\n if (groupIds.length === 0) return [];\n const collapsed = new Set(collapsedGroupIds);\n const allCollapsed = groupIds.every(id => collapsed.has(id));\n return allCollapsed ? [] : [...groupIds];\n}\n\n/** Host for grouped collapse-all: the trailing action lane or a scrollport-owned overlay. */\nexport type TableCollapseAllHost =\n | { columnId: string; mode: 'action' }\n | { mode: 'floating' };\n\nexport function tableCollapseAllHost(\n columns: TableColumn[],\n): TableCollapseAllHost | undefined {\n if (columns.length === 0) return undefined;\n for (let index = columns.length - 1; index >= 0; index -= 1) {\n const column = columns[index]!;\n if (column.kind === 'action') return { columnId: column.id, mode: 'action' };\n }\n return { mode: 'floating' };\n}\n\nexport function clampTableColumnSize(column: TableColumn): number | undefined {\n if (typeof column.size !== 'number' || !Number.isFinite(column.size) || column.size <= 0) {\n return undefined;\n }\n\n const minimum = Number.isFinite(column.minSize) && (column.minSize ?? 0) > 0\n ? column.minSize!\n : 0;\n const maximum = Number.isFinite(column.maxSize) && (column.maxSize ?? 0) > 0\n ? Math.max(column.maxSize!, minimum)\n : Number.POSITIVE_INFINITY;\n\n return Math.min(Math.max(column.size!, minimum), maximum);\n}\n\nexport function tableColumnSize(column: TableColumn): string | undefined {\n if (typeof column.size === 'string') {\n return `var(${TABLE_COLUMN_WIDTH_TOKENS[column.size]})`;\n }\n const width = clampTableColumnSize(column);\n if (width != null) return `${width}px`;\n if (column.imageTracks) {\n return `var(${TABLE_IMAGE_COLUMN_WIDTH_VARIABLES[column.imageTracks]})`;\n }\n return column.kind === 'action' ? 'var(--dimension-size-500)' : undefined;\n}\n\n/**\n * When every column has an explicit width, one ordinary lane must absorb spare\n * table width so fixed selection and action lanes are not expanded by the\n * native fixed-table layout algorithm.\n */\nexport function tableFlexibleColumnId(columns: TableColumn[]): string | undefined {\n if (columns.some(column => !tableColumnSize(column))) return undefined;\n for (let index = columns.length - 1; index >= 0; index -= 1) {\n const column = columns[index]!;\n if (column.kind !== 'action' && !column.sticky) return column.id;\n }\n for (let index = columns.length - 1; index >= 0; index -= 1) {\n const column = columns[index]!;\n if (column.kind !== 'action') return column.id;\n }\n return columns[columns.length - 1]?.id;\n}\n\nexport function tableExplicitMinWidth(columns: TableColumn[]): string | undefined {\n const widths = columns.map(tableColumnSize).filter((width): width is string => !!width);\n if (widths.length === 0) return undefined;\n return widths.length === 1 ? widths[0] : `calc(${widths.join(' + ')})`;\n}\n\nexport function tableRows(rows: TableRow[], groups: TableGroup[], grouped: boolean): TableRow[] {\n return grouped ? groups.flatMap(group => group.rows) : rows;\n}\n\nfunction canSelectRow(row: TableRow): boolean {\n return row.selectable !== false && !row.disabled;\n}\n\nexport function deriveTableSelectionState(\n rows: TableRow[],\n selectedRowIds: readonly string[],\n): TableSelectionState {\n const selected = new Set(selectedRowIds);\n const selectableRowIds = rows.filter(canSelectRow).map(row => row.id);\n const selectedLoadedCount = selectableRowIds.reduce(\n (count, id) => count + (selected.has(id) ? 1 : 0),\n 0,\n );\n\n return {\n selectableRowIds,\n selectedLoadedCount,\n allSelected: selectableRowIds.length > 0 && selectedLoadedCount === selectableRowIds.length,\n indeterminate: selectedLoadedCount > 0 && selectedLoadedCount < selectableRowIds.length,\n };\n}\n\nexport function toggleTableRowSelection(\n selectedRowIds: readonly string[],\n row: TableRow,\n): string[] {\n if (!canSelectRow(row)) return [...selectedRowIds];\n const selected = new Set(selectedRowIds);\n if (selected.has(row.id)) selected.delete(row.id);\n else selected.add(row.id);\n return [...selected];\n}\n\nexport function toggleAllLoadedTableRows(\n selectedRowIds: readonly string[],\n loadedRows: TableRow[],\n): string[] {\n const selected = new Set(selectedRowIds);\n const state = deriveTableSelectionState(loadedRows, selectedRowIds);\n\n for (const id of state.selectableRowIds) {\n if (state.allSelected) selected.delete(id);\n else selected.add(id);\n }\n\n return [...selected];\n}\n\n/** Toggle every selectable loaded row that belongs to a group. */\nexport function toggleTableGroupSelection(\n selectedRowIds: readonly string[],\n groupRows: TableRow[],\n): string[] {\n return toggleAllLoadedTableRows(selectedRowIds, groupRows);\n}\n\nexport function resolvedTableGroupCount(group: TableGroup): number {\n const total = Number.isFinite(group.totalCount) ? Math.max(0, group.totalCount!) : 0;\n return Math.max(total, group.rows.length);\n}\n\nexport const TABLE_GROUP_INTENTS = [\n 'brand',\n 'neutral',\n 'negative',\n 'warning',\n 'caution',\n 'positive',\n] as const;\n\nexport function isTableGroupIntent(\n value: unknown,\n): value is (typeof TABLE_GROUP_INTENTS)[number] {\n return (\n typeof value === 'string' &&\n (TABLE_GROUP_INTENTS as readonly string[]).includes(value)\n );\n}\n\nexport function tableGroupIntentClass(\n intent: (typeof TABLE_GROUP_INTENTS)[number] | undefined,\n): string | undefined {\n return isTableGroupIntent(intent) ? `ds-table__group-cell--intent-${intent}` : undefined;\n}\n\n/** Title color for an intentful group label; defaults to primary when unset. */\nexport function tableGroupLabelColor(\n intent: (typeof TABLE_GROUP_INTENTS)[number] | undefined,\n): 'primary' | 'brand' | 'negative' | 'warning' | 'caution' | 'positive' | `var(--${string})` {\n if (!isTableGroupIntent(intent)) return 'primary';\n if (intent === 'neutral') return 'var(--color-foreground-bold-neutral)';\n return intent;\n}\n\n/** Formats the optional table result footer. Returns null when either count is missing. */\nexport function formatTableResultSummary(\n displayed: number | null | undefined,\n total: number | null | undefined,\n label = 'Displaying {displayed} of {total}',\n locale?: string,\n): string | null {\n if (!Number.isFinite(displayed) || !Number.isFinite(total)) return null;\n const normalizedTotal = Math.max(0, Math.trunc(total as number));\n const normalizedDisplayed = Math.min(\n normalizedTotal,\n Math.max(0, Math.trunc(displayed as number)),\n );\n const formatter = new Intl.NumberFormat(locale);\n return label\n .replace('{displayed}', formatter.format(normalizedDisplayed))\n .replace('{total}', formatter.format(normalizedTotal));\n}\n\n/** Formats the virtual-mode total-only result footer. */\nexport function formatTableTotalSummary(\n total: number | null | undefined,\n label = '{total} items',\n locale?: string,\n): string | null {\n if (!Number.isFinite(total)) return null;\n const normalizedTotal = Math.max(0, Math.trunc(total as number));\n return label.replace('{total}', new Intl.NumberFormat(locale).format(normalizedTotal));\n}\n\n/**\n * True when a footer slot belongs to the table itself: a light-DOM child of the\n * host, or a top-level node Stencil relocated into this table's footer. A\n * slotted ancestor marks a nested component boundary, so dialog action slots\n * inside footer-leading/footer-trailing content cannot replace the table copy.\n */\nexport function isOwnedTableFooterSlot(node: Element, host: Element): boolean {\n if (node.parentElement === host) return true;\n let insideTableFooter = false;\n for (let ancestor = node.parentElement; ancestor && ancestor !== host; ancestor = ancestor.parentElement) {\n if (ancestor.hasAttribute('slot')) return false;\n if (ancestor.classList.contains('ds-table__footer')) insideTableFooter = true;\n }\n return insideTableFooter;\n}\n\nexport function hasOwnedTableFooterSlot(host: Element, slotName: string): boolean {\n return [...host.querySelectorAll(`[slot=\"${slotName}\"]`)].some(node =>\n isOwnedTableFooterSlot(node, host),\n );\n}\n\nexport function tableModelIssues(\n columns: TableColumn[],\n rows: TableRow[],\n groups: TableGroup[],\n grouped: boolean,\n): string[] {\n const issues: string[] = [];\n const columnIds = new Set<string>();\n for (const column of columns) {\n if (!column.id.trim()) issues.push('Every column requires a non-empty id.');\n else if (columnIds.has(column.id)) issues.push(`Duplicate column id: ${column.id}`);\n if (!column.header.trim() && !column.headerLabel?.trim()) {\n issues.push(`Column ${column.id || '(missing id)'} requires a visible header or headerLabel.`);\n }\n columnIds.add(column.id);\n }\n\n const groupIds = new Set<string>();\n if (grouped) {\n for (const group of groups) {\n if (!group.id.trim()) issues.push('Every group requires a non-empty id.');\n else if (groupIds.has(group.id)) issues.push(`Duplicate group id: ${group.id}`);\n groupIds.add(group.id);\n if (group.totalCount != null && group.totalCount < group.rows.length) {\n issues.push(`Group ${group.id} totalCount is smaller than its loaded row count.`);\n }\n if (group.intent != null && !isTableGroupIntent(group.intent)) {\n issues.push(`Group ${group.id} has an unsupported intent.`);\n }\n }\n }\n\n const rowIds = new Set<string>();\n for (const row of tableRows(rows, groups, grouped)) {\n if (!row.id.trim()) issues.push('Every row requires a non-empty id.');\n else if (rowIds.has(row.id)) issues.push(`Duplicate row id: ${row.id}`);\n rowIds.add(row.id);\n }\n\n return [...new Set(issues)];\n}\n",
|
|
@@ -1353,6 +1363,16 @@
|
|
|
1353
1363
|
"content": "import {\n deriveTableSelectionState,\n isTableGroupIntent,\n resolvedTableGroupCount,\n tableCollapseAllHost,\n tableColumnSize,\n tableExplicitMinWidth,\n tableFlexibleColumnId,\n tableGroupIntentClass,\n tableGroupLabelColor,\n tableRows,\n type TableCollapseAllHost,\n type TableSelectionState,\n} from './table-model';\nimport type {\n TableColumn,\n TableGroup,\n TableGroupIntent,\n TableRow,\n TableSelectionMode,\n} from './table-types';\n\nexport type TableGroupCountPresentation = 'loaded-progress' | 'total';\n\nexport interface TableGroupRenderModel {\n group: TableGroup;\n count: number;\n loadedCount: number;\n visibleCountText: string;\n countLabel: string;\n intent: TableGroupIntent | undefined;\n intentClass: string | undefined;\n labelColor: ReturnType<typeof tableGroupLabelColor>;\n collapsed: boolean;\n selection: TableSelectionState | null;\n}\n\nexport interface TableRenderModel {\n grouped: boolean;\n selectable: boolean;\n loadedRows: TableRow[];\n hasData: boolean;\n selectedRowIds: ReadonlySet<string>;\n selection: TableSelectionState;\n collapsedGroupIds: ReadonlySet<string>;\n groups: TableGroupRenderModel[];\n totalColumns: number;\n allGroupsCollapsed: boolean;\n showCollapseAll: boolean;\n collapseAllHost: TableCollapseAllHost | undefined;\n flexibleColumnId: string | undefined;\n tableStyle: Record<string, string> | undefined;\n}\n\nexport interface TableRenderModelInput {\n columns: TableColumn[];\n rows: TableRow[];\n groups: TableGroup[];\n grouped: boolean;\n selectionMode: TableSelectionMode;\n selectedRowIds: string[];\n collapsedGroupIds: string[];\n groupCountPresentation?: TableGroupCountPresentation;\n}\n\n/** Create one immutable snapshot of all derived state consumed during a render. */\nexport function createTableRenderModel(input: TableRenderModelInput): TableRenderModel {\n const selectable = input.selectionMode === 'multiple';\n const loadedRows = tableRows(input.rows, input.groups, input.grouped);\n const selectedRowIds = new Set(input.selectedRowIds);\n const collapsedGroupIds = new Set(input.collapsedGroupIds);\n const allGroupsCollapsed = input.grouped && input.groups.length > 0 &&\n input.groups.every(group => collapsedGroupIds.has(group.id));\n const showCollapseAll = input.grouped && input.groups.length > 0 && !allGroupsCollapsed;\n const explicitMinWidth = tableExplicitMinWidth(input.columns);\n const flexibleColumnId = tableFlexibleColumnId(input.columns);\n const gridTracks = [\n ...(selectable ? ['var(--_table-selection-column-inline-size)'] : []),\n ...input.columns.map(column => {\n const width = tableColumnSize(column);\n if (!width) return 'minmax(0, 1fr)';\n return column.id === flexibleColumnId ? `minmax(${width}, 1fr)` : width;\n }),\n ].join(' ');\n const tableStyle: Record<string, string> = {\n '--_table-grid-template-columns': gridTracks || 'minmax(0, 1fr)',\n };\n if (explicitMinWidth != null) {\n tableStyle['--ds-table-explicit-min-inline-size'] = explicitMinWidth;\n }\n\n return {\n grouped: input.grouped,\n selectable,\n loadedRows,\n hasData: loadedRows.length > 0,\n selectedRowIds,\n selection: deriveTableSelectionState(loadedRows, input.selectedRowIds),\n collapsedGroupIds,\n groups: input.groups.map(group => {\n const loadedCount = group.rows.length;\n const totalPresentation = input.groupCountPresentation === 'total';\n const count = resolvedTableGroupCount(group);\n const totalLabel = group.countLabel ?? `${count} ${count === 1 ? 'item' : 'items'}`;\n const intent = isTableGroupIntent(group.intent) ? group.intent : undefined;\n return {\n group,\n count,\n loadedCount,\n visibleCountText: totalPresentation ? String(count) : `${loadedCount} of ${count}`,\n countLabel: totalPresentation ? totalLabel : `${loadedCount} of ${totalLabel} loaded`,\n intent,\n intentClass: tableGroupIntentClass(intent),\n labelColor: tableGroupLabelColor(intent),\n collapsed: collapsedGroupIds.has(group.id),\n selection: selectable\n ? deriveTableSelectionState(group.rows, input.selectedRowIds)\n : null,\n };\n }),\n totalColumns: input.columns.length + (selectable ? 1 : 0),\n allGroupsCollapsed,\n showCollapseAll,\n collapseAllHost: showCollapseAll ? tableCollapseAllHost(input.columns) : undefined,\n flexibleColumnId,\n tableStyle,\n };\n}\n",
|
|
1354
1364
|
"type": "registry:ui"
|
|
1355
1365
|
},
|
|
1366
|
+
{
|
|
1367
|
+
"path": "src/wc/components/Table/table-row-view.tsx",
|
|
1368
|
+
"content": "import { h } from '@stencil/core';\nimport {\n resolveTableCellPresentation,\n tableCellTextOverflowProps,\n type TableCellPresentation,\n} from './table-cell-model';\nimport { isRenderableTableActionMenu, isTableCellActionMenu, tableActionTriggerId } from './table-action-menu';\nimport { tableRowSelectionLabel } from './table-model';\nimport type { TableRenderModel } from './table-render-model';\nimport type {\n TableCellActionDetail,\n TableCellTextRun,\n TableColumn,\n TableRow,\n} from './table-types';\nimport { resolveSafeUrl } from '../../utils/safe-url';\n\nexport interface TableRowViewOptions {\n row: TableRow;\n model: TableRenderModel;\n visibleColumns: TableColumn[];\n emptyCellLabel: string;\n actionMenuElementId: string;\n actionMenu: { rowId: string; columnId: string } | null;\n ariaRowIndex?: number;\n variableVirtualSize?: boolean;\n rowKey?: string;\n renderSelectionControl: (\n label: string,\n checked: boolean,\n indeterminate: boolean,\n disabled: boolean,\n onActivate: () => void,\n ) => unknown;\n renderStickyEdge: (sticky: TableColumn['sticky']) => unknown;\n onRowActivate: (row: TableRow, event: Event) => void;\n onRowKeyDown: (row: TableRow, event: KeyboardEvent) => void;\n onRowSelection: (row: TableRow) => void;\n onCellAction: (detail: TableCellActionDetail) => void;\n onActionMenuToggle: (row: TableRow, column: TableColumn, event: MouseEvent) => void;\n}\n\n/** Render one semantic body row and the complete declarative table-cell vocabulary. */\nexport function renderTableRow({\n row,\n model,\n visibleColumns,\n emptyCellLabel,\n actionMenuElementId,\n actionMenu,\n ariaRowIndex,\n variableVirtualSize = false,\n rowKey = row.id,\n renderSelectionControl,\n renderStickyEdge,\n onRowActivate,\n onRowKeyDown,\n onRowSelection,\n onCellAction,\n onActionMenuToggle,\n}: TableRowViewOptions) {\n const selected = model.selectedRowIds.has(row.id);\n const rowSelectable = row.selectable !== false && !row.disabled;\n\n return (\n <tr\n key={rowKey}\n class={{\n 'ds-table__row': true,\n 'ds-table__row--selected': selected,\n 'ds-table__row--disabled': !!row.disabled,\n 'ds-table__row--interactive': !!row.interactive && !row.disabled,\n 'ds-focus-ring': !!row.interactive && !row.disabled,\n }}\n data-row-id={row.id}\n data-virtual-id={ariaRowIndex != null ? `row:${row.id}` : undefined}\n data-virtual-pool-key={ariaRowIndex != null ? rowKey : undefined}\n data-virtual-measure={ariaRowIndex != null && variableVirtualSize ? 'true' : undefined}\n data-selected={selected ? 'true' : undefined}\n aria-rowindex={ariaRowIndex}\n aria-disabled={row.disabled ? 'true' : undefined}\n tabIndex={row.interactive && !row.disabled ? 0 : undefined}\n onClick={event => onRowActivate(row, event)}\n onKeyDown={event => onRowKeyDown(row, event)}\n >\n {model.selectable && (\n <td\n class={{\n 'ds-table__cell': true,\n 'ds-table__selection-cell': true,\n 'ds-table__cell--sticky-start': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--grouped': true,\n 'ds-interaction-fill--selected': selected,\n }}\n >\n {renderSelectionControl(\n `${selected ? 'Deselect' : 'Select'} ${tableRowSelectionLabel(row, visibleColumns)}`,\n selected,\n false,\n !rowSelectable,\n () => onRowSelection(row),\n )}\n {renderStickyEdge('start')}\n </td>\n )}\n {visibleColumns.map(column =>\n renderTableCell({\n row,\n column,\n selected,\n emptyCellLabel,\n actionMenuElementId,\n actionMenu,\n renderStickyEdge,\n onCellAction,\n onActionMenuToggle,\n }),\n )}\n </tr>\n );\n}\n\ninterface TableCellViewOptions {\n row: TableRow;\n column: TableColumn;\n selected: boolean;\n emptyCellLabel: string;\n actionMenuElementId: string;\n actionMenu: { rowId: string; columnId: string } | null;\n renderStickyEdge: (sticky: TableColumn['sticky']) => unknown;\n onCellAction: (detail: TableCellActionDetail) => void;\n onActionMenuToggle: (row: TableRow, column: TableColumn, event: MouseEvent) => void;\n}\n\nfunction renderTableCell(options: TableCellViewOptions) {\n const { row, column, selected, renderStickyEdge } = options;\n const align = column.align ?? 'start';\n const cell = resolveTableCellPresentation(row.cells[column.id], column);\n const tagCell = cell.kind === 'tag';\n const iconCell = cell.kind === 'icon';\n const iconTextCell = cell.kind === 'icon-text';\n const imageCell = cell.kind === 'image';\n const actionCell = cell.kind === 'action';\n const actionMenuCell = actionCell && isRenderableTableActionMenu(cell.value);\n const textCell = cell.kind === 'text';\n const primaryTextCell = textCell && cell.primaryText;\n const singleTextCell = textCell && cell.singleLine;\n const emptyCell = cell.kind === 'empty';\n const blankCell = cell.kind === 'blank';\n const tagVariant = tagCell ? cell.variant : undefined;\n const textVariant = textCell ? cell.variant : undefined;\n const imageVariant = imageCell ? cell.variant : undefined;\n const iconTextVariant = iconTextCell ? cell.variant : undefined;\n const wraps = (textCell || iconTextCell) && cell.wraps;\n\n return (\n <td\n key={`${row.id}:${column.id}`}\n class={{\n 'ds-table__cell': true,\n [`ds-table__cell--align-${align}`]: true,\n 'ds-table__cell--tag': tagCell,\n [`ds-table__cell--tag-${tagVariant}`]: tagCell,\n 'ds-table__cell--icon': iconCell,\n 'ds-table__cell--icon-text': iconTextCell,\n [`ds-table__cell--icon-text-${iconTextVariant}`]: iconTextCell,\n 'ds-table__cell--icon-text-wrap': iconTextCell && wraps,\n 'ds-table__cell--image': imageCell,\n [`ds-table__cell--image-${imageVariant}`]: imageCell,\n 'ds-table__cell--action': actionCell,\n 'ds-table__cell--action-menu': actionMenuCell,\n 'ds-table__cell--primary-text': primaryTextCell,\n 'ds-table__cell--text-single': singleTextCell,\n 'ds-table__cell--text-multi': textCell && !singleTextCell && textVariant !== 'triple',\n 'ds-table__cell--text-triple': textVariant === 'triple',\n 'ds-table__cell--text-wrap': textCell && wraps,\n 'ds-table__cell--empty': emptyCell,\n 'ds-table__cell--blank': blankCell,\n 'ds-table__cell--sticky-start': column.sticky === 'start',\n 'ds-table__cell--sticky-end': column.sticky === 'end',\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--grouped': true,\n 'ds-interaction-fill--selected': selected,\n }}\n data-column-id={column.id}\n data-cell-type={cell.cellType}\n data-cell-variant={tagVariant ?? textVariant ?? imageVariant ?? iconTextVariant}\n >\n <span class=\"ds-table__cell-content ds-interaction-fill__content\">\n {renderTableCellValue(cell, options)}\n </span>\n {renderStickyEdge(column.sticky)}\n </td>\n );\n}\n\nfunction renderTableCellValue(cell: TableCellPresentation, options: TableCellViewOptions) {\n const {\n row,\n column,\n emptyCellLabel,\n actionMenuElementId,\n actionMenu,\n onCellAction,\n onActionMenuToggle,\n } = options;\n if (cell.kind === 'blank') return null;\n\n if (cell.kind === 'empty') {\n return (\n <ds-text\n class=\"ds-table__cell-track ds-table__cell-track--text\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"tertiary\"\n >\n <span aria-hidden=\"true\">—</span>\n <span class=\"ds-visually-hidden\">{emptyCellLabel}</span>\n </ds-text>\n );\n }\n\n if (cell.kind === 'icon') {\n const value = cell.value;\n return (\n <ds-icon\n name={value.icon}\n size=\"md\"\n color={value.color ?? 'secondary'}\n label={value.label}\n />\n );\n }\n\n if (cell.kind === 'icon-text') {\n return (\n <span class=\"ds-table__cell-icon-text\">\n <span class=\"ds-table__cell-icon-text-icon\">\n <ds-icon\n name={cell.icon}\n size=\"md\"\n color={cell.iconColor ?? 'secondary'}\n label={cell.iconLabel}\n />\n </span>\n {renderTextCopy(cell)}\n </span>\n );\n }\n\n if (cell.kind === 'image') {\n const value = cell.value;\n return (\n <span class=\"ds-table__cell-image\">\n {value.src ? (\n <img class=\"ds-table__cell-image-content\" src={value.src} alt={value.alt} loading=\"lazy\" />\n ) : (\n <span class=\"ds-table__cell-image-placeholder\" role=\"img\" aria-label={value.alt} />\n )}\n </span>\n );\n }\n\n if (cell.kind === 'action') {\n const value = cell.value;\n const menu = isRenderableTableActionMenu(value);\n const triggerId = tableActionTriggerId(actionMenuElementId, row.id, column.id);\n const expanded = menu && actionMenu?.rowId === row.id && actionMenu?.columnId === column.id;\n return (\n <ds-button-unfilled\n id={menu ? triggerId : undefined}\n variant={menu ? 'icon' : (value.variant ?? 'label')}\n size=\"md\"\n isInset={true}\n insetDepth=\"double\"\n label={value.label ?? ''}\n icon={value.icon ?? (menu ? 'Ellipses' : '')}\n aria-label={value.ariaLabel ?? null}\n hasBorder={value.hasBorder ?? false}\n isInactive={value.isInactive ?? false}\n isLoading={value.isLoading ?? false}\n hasMenu={menu}\n expanded={menu ? expanded : undefined}\n controls={menu ? actionMenuElementId : undefined}\n onDsClick={event => {\n event.stopPropagation();\n if (menu) {\n onActionMenuToggle(row, column, event.detail);\n return;\n }\n if (isTableCellActionMenu(value)) return;\n onCellAction({ actionId: value.actionId, rowId: row.id, columnId: column.id });\n }}\n />\n );\n }\n\n if (cell.kind === 'tag') {\n const value = cell.value;\n const variant = cell.variant;\n const tag = (\n <ds-tag\n label={value.label}\n intent={value.intent ?? 'neutral'}\n contrast={value.contrast ?? 'faint'}\n size={variant === 'text-with-tag' ? 'sm' : 'md'}\n icon={value.icon ?? ''}\n rounded={value.rounded ?? false}\n isInset\n insetDepth={variant === 'text-with-tag' ? 'single' : 'double'}\n />\n );\n if (variant === 'tag-only') return tag;\n return (\n <span class={`ds-table__cell-tag-stack ds-table__cell-tag-stack--${variant}`}>\n {variant === 'tag-with-text' && tag}\n <ds-text\n class=\"ds-table__cell-tag-text ds-table__cell-track\"\n as=\"span\"\n variant={variant === 'tag-with-text' ? 'text-body-small' : 'text-body-medium'}\n color=\"secondary\"\n lineTruncation={1}\n data-table-truncate=\"\"\n >\n {value.text}\n </ds-text>\n {variant === 'text-with-tag' && <span class=\"ds-table__cell-tag-control-track\">{tag}</span>}\n </span>\n );\n }\n\n if (cell.kind !== 'text') return null;\n return renderTextCopy(cell);\n}\n\nfunction renderTextCopy(cell: Extract<TableCellPresentation, { kind: 'text' | 'icon-text' }>) {\n const text = cell.value;\n const overflow = tableCellTextOverflowProps(cell.lineClamp);\n const primaryText = cell.kind === 'text' && cell.primaryText;\n const href = resolveSafeUrl(text.href);\n const primary = (\n <ds-text\n class=\"ds-table__cell-primary ds-table__cell-track ds-table__cell-track--text\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={href ? 'inherit' : 'primary'}\n lineTruncation={overflow.lineTruncation}\n wrap={overflow.wrap}\n fontFeature={text.fontFeature ?? 'normal'}\n data-table-truncate={truncateAttr(cell.lineClamp)}\n >\n {text.primary}\n </ds-text>\n );\n\n return (\n <span class={{ 'ds-table__cell-copy': true, 'ds-table__cell-copy--wrap': cell.wraps }}>\n {href ? (\n <a\n class=\"ds-table__cell-link ds-text-action ds-focus-ring\"\n href={href}\n target={text.target === '_blank' ? '_blank' : undefined}\n rel={text.target === '_blank' ? 'noopener noreferrer' : undefined}\n >\n {primary}\n </a>\n ) : (\n primary\n )}\n {renderTextTrack(text.secondary, {\n track: 'secondary',\n variant: primaryText ? 'text-body-medium' : 'text-body-small',\n defaultColor: primaryText ? 'primary' : 'secondary',\n wholeColor: text.secondaryColor,\n lineClamp: cell.lineClamp,\n })}\n {renderTextTrack(text.tertiary, {\n track: 'tertiary',\n variant: 'text-body-small',\n defaultColor: 'secondary',\n wholeColor: text.tertiaryColor,\n lineClamp: cell.lineClamp,\n })}\n </span>\n );\n}\n\nfunction renderTextTrack(\n runs: TableCellTextRun[] | undefined,\n options: {\n track: 'secondary' | 'tertiary';\n variant: 'text-body-medium' | 'text-body-small';\n defaultColor: 'primary' | 'secondary';\n wholeColor?: TableCellTextRun['color'];\n lineClamp: Extract<TableCellPresentation, { kind: 'text' | 'icon-text' }>['lineClamp'];\n },\n) {\n if (!runs?.length) return null;\n const trackClass = `ds-table__cell-${options.track} ds-table__cell-track ds-table__cell-track--text`;\n const overflow = tableCellTextOverflowProps(options.lineClamp);\n const colorFor = (run: TableCellTextRun) =>\n run.color ?? options.wholeColor ?? options.defaultColor;\n if (runs.length === 1) {\n return (\n <ds-text\n class={trackClass}\n as=\"span\"\n variant={options.variant}\n color={colorFor(runs[0])}\n lineTruncation={overflow.lineTruncation}\n wrap={overflow.wrap}\n data-table-truncate={truncateAttr(options.lineClamp)}\n >\n {runs[0].text}\n </ds-text>\n );\n }\n\n return (\n <span class={`${trackClass} ds-table__cell-track--runs`}>\n {runs.map((run, index) => [\n index > 0 && (\n <ds-text\n key={`${options.track}-sep-${index}`}\n class=\"ds-table__cell-run-separator\"\n as=\"span\"\n variant={options.variant}\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>\n ),\n <ds-text\n key={`${options.track}-run-${index}`}\n class=\"ds-table__cell-run\"\n as=\"span\"\n variant={options.variant}\n color={colorFor(run)}\n lineTruncation={overflow.lineTruncation}\n wrap={overflow.wrap}\n data-table-truncate={truncateAttr(options.lineClamp)}\n >\n {run.text}\n </ds-text>,\n ])}\n </span>\n );\n}\n\nfunction truncateAttr(lineClamp: Extract<TableCellPresentation, { kind: 'text' | 'icon-text' }>['lineClamp']) {\n return lineClamp === 'none' ? undefined : '';\n}\n",
|
|
1369
|
+
"type": "registry:ui"
|
|
1370
|
+
},
|
|
1371
|
+
{
|
|
1372
|
+
"path": "src/wc/components/Table/table-skeleton-view.tsx",
|
|
1373
|
+
"content": "import { h } from '@stencil/core';\nimport { resolveTableCellImageTracks, tableCellImageVariant } from './table-cell-model';\nimport type { TableRenderModel } from './table-render-model';\nimport type { TableCellSkeleton, TableColumn } from './table-types';\n\ninterface TableSkeletonViewOptions {\n model: TableRenderModel;\n visibleColumns: TableColumn[];\n skeletonRows: number;\n renderStickyEdge: (sticky: TableColumn['sticky']) => unknown;\n}\n\n/** Render the loading body from the same cell recipes used by loaded rows. */\nexport function renderTableSkeletonBody(options: TableSkeletonViewOptions) {\n const { model, visibleColumns, renderStickyEdge } = options;\n const count = Math.min(20, Math.max(1, Math.round(options.skeletonRows) || 1));\n return (\n <tbody class=\"ds-table__body ds-table__skeleton-body\">\n {Array.from({ length: count }, (_, index) => (\n <tr class=\"ds-table__row ds-table__skeleton-row\" key={`skeleton-${index}`}>\n {model.selectable && (\n <td\n class=\"ds-table__cell ds-table__selection-cell ds-table__cell--sticky-start ds-table__skeleton-cell ds-interaction-fill ds-interaction-fill--grouped\"\n data-skeleton-kind=\"checkbox\"\n >\n <span class=\"ds-table__skeleton-checkbox-canvas ds-interaction-fill__content\">\n <ds-skeleton\n class=\"ds-table__skeleton-checkbox\"\n variant=\"control\"\n controlSize=\"xs\"\n width=\"var(--dimension-iconography-sm)\"\n />\n </span>\n {renderStickyEdge('start')}\n </td>\n )}\n {visibleColumns.map(column => renderSkeletonCell(column, index, renderStickyEdge))}\n </tr>\n ))}\n </tbody>\n );\n}\n\nfunction renderSkeletonCell(\n column: TableColumn,\n rowIndex: number,\n renderStickyEdge: TableSkeletonViewOptions['renderStickyEdge'],\n) {\n const skeleton =\n column.skeleton ??\n ((column.kind === 'action'\n ? { kind: 'action', variant: 'icon' }\n : { kind: 'text', lines: 1 }) satisfies TableCellSkeleton);\n const align = column.align ?? 'start';\n const text = skeleton.kind === 'text';\n const iconText = skeleton.kind === 'icon-text';\n const lines = text || iconText ? (skeleton.lines ?? 1) : 1;\n const tag = skeleton.kind === 'tag';\n const icon = skeleton.kind === 'icon';\n const image = skeleton.kind === 'image';\n const imageVariant = image\n ? tableCellImageVariant(resolveTableCellImageTracks(skeleton.tracks))\n : undefined;\n const iconTextVariant = iconText\n ? lines === 3\n ? 'triple'\n : lines === 2\n ? 'multi'\n : 'single'\n : undefined;\n const action = skeleton.kind === 'action';\n const blank = skeleton.kind === 'blank';\n\n return (\n <td\n class={{\n 'ds-table__cell': true,\n [`ds-table__cell--align-${align}`]: true,\n 'ds-table__skeleton-cell': true,\n 'ds-table__cell--text-single': text && lines === 1,\n 'ds-table__cell--text-multi': text && lines === 2,\n 'ds-table__cell--text-triple': text && lines === 3,\n 'ds-table__cell--tag': tag,\n 'ds-table__cell--tag-tag-only': tag,\n 'ds-table__cell--icon': icon,\n 'ds-table__cell--icon-text': iconText,\n [`ds-table__cell--icon-text-${iconTextVariant}`]: iconText,\n 'ds-table__cell--image': image,\n [`ds-table__cell--image-${imageVariant}`]: image,\n 'ds-table__cell--action': action,\n 'ds-table__cell--blank': blank,\n 'ds-table__cell--sticky-start': column.sticky === 'start',\n 'ds-table__cell--sticky-end': column.sticky === 'end',\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--grouped': true,\n }}\n data-column-id={column.id}\n data-skeleton-kind={skeleton.kind}\n data-cell-variant={imageVariant ?? iconTextVariant}\n key={`skeleton-${rowIndex}:${column.id}`}\n >\n <span class=\"ds-table__cell-content ds-interaction-fill__content\">\n {renderSkeletonCellContent(skeleton)}\n </span>\n {renderStickyEdge(column.sticky)}\n </td>\n );\n}\n\nfunction renderSkeletonCellContent(skeleton: TableCellSkeleton) {\n if (skeleton.kind === 'blank') return null;\n\n if (skeleton.kind === 'image') {\n return (\n <span class=\"ds-table__cell-image ds-table__skeleton-image\">\n <ds-skeleton\n class=\"ds-table__skeleton-image-shape\"\n variant=\"control\"\n controlSize=\"md\"\n width=\"100%\"\n />\n </span>\n );\n }\n\n if (skeleton.kind === 'icon') {\n return <ds-skeleton variant=\"icon\" iconSize=\"md\" rounded={skeleton.rounded ?? false} />;\n }\n\n if (skeleton.kind === 'tag') {\n return <ds-skeleton variant=\"control\" controlSize=\"sm\" width={skeleton.width ?? '64%'} />;\n }\n\n if (skeleton.kind === 'action') {\n const iconOnly = (skeleton.variant ?? 'icon') === 'icon';\n return (\n <ds-skeleton\n variant=\"control\"\n controlSize=\"sm\"\n width={skeleton.width ?? (iconOnly ? '24px' : '72%')}\n />\n );\n }\n\n const lines = skeleton.lines ?? 1;\n const copy = (\n <span class=\"ds-table__cell-copy\">\n <span class=\"ds-table__cell-primary ds-table__cell-track ds-table__cell-track--text\">\n <ds-skeleton\n variant=\"text\"\n textVariant=\"text-body-medium\"\n width={skeleton.primaryWidth ?? '100%'}\n />\n </span>\n {lines >= 2 && (\n <span class=\"ds-table__cell-secondary ds-table__cell-track ds-table__cell-track--text\">\n <ds-skeleton\n variant=\"text\"\n textVariant=\"text-body-small\"\n width={skeleton.secondaryWidth ?? '72%'}\n />\n </span>\n )}\n {lines === 3 && (\n <span class=\"ds-table__cell-tertiary ds-table__cell-track ds-table__cell-track--text\">\n <ds-skeleton\n variant=\"text\"\n textVariant=\"text-body-small\"\n width={skeleton.tertiaryWidth ?? '56%'}\n />\n </span>\n )}\n </span>\n );\n if (skeleton.kind === 'icon-text') {\n return (\n <span class=\"ds-table__cell-icon-text\">\n <span class=\"ds-table__cell-icon-text-icon\">\n <ds-skeleton variant=\"icon\" iconSize=\"md\" />\n </span>\n {copy}\n </span>\n );\n }\n return copy;\n}\n",
|
|
1374
|
+
"type": "registry:ui"
|
|
1375
|
+
},
|
|
1356
1376
|
{
|
|
1357
1377
|
"path": "src/wc/components/Table/table-truncate.ts",
|
|
1358
1378
|
"content": "/** Marks text tracks that may present the table-owned truncation tooltip. */\nexport const TABLE_TRUNCATE_ATTR = 'data-table-truncate';\n\nfunction isValidTruncateTrack(track: HTMLElement): boolean {\n return !track.closest('.ds-table__row--disabled, thead, .ds-table__header-row');\n}\n\n/** Resolve the overflowing text track under a pointer or focus target. */\nexport function resolveTableTruncateTrack(target: EventTarget | null): HTMLElement | null {\n const element =\n target instanceof Element\n ? target\n : target instanceof Node\n ? target.parentElement\n : null;\n if (!element) return null;\n\n const fromAncestor = element.closest<HTMLElement>(`[${TABLE_TRUNCATE_ATTR}]`);\n if (fromAncestor && isValidTruncateTrack(fromAncestor)) return fromAncestor;\n\n const link = element.closest('a.ds-table__cell-link');\n const nested = link?.querySelector<HTMLElement>(`[${TABLE_TRUNCATE_ATTR}]`);\n if (nested && isValidTruncateTrack(nested)) return nested;\n return null;\n}\n\nexport function tableTruncateLabel(track: HTMLElement): string {\n return (track.textContent ?? '').replace(/\\s+/g, ' ').trim();\n}\n",
|