From dbc6ab0ff32b2d41358e260c8ee2f9aa1f53531c Mon Sep 17 00:00:00 2001 From: Cahit Guerguec Date: Wed, 2 Sep 2026 00:31:14 +0300 Subject: [PATCH] refactor(ui5-table): resolve row selector/action/sort components lazily via features Move ownership of the concrete controls rendered inside table rows and header cells from the templates to the owning slotted feature, child element, or the header cell itself, so a plain table only pulls in the components it uses. Previously the row templates and the header-cell template statically imported concrete components (CheckBox, RadioButton, Button, Icon) plus their enum/icon modules. Because a component's `.ts` value-imports its template `.tsx`, and the template value-imported those components, importing TableRow / TableHeaderCell always dragged all of them into the bundle even for a read-only, unsorted table with no selection or row actions. Instead of hard-coding the components, the templates now ask the owning feature/element for the component class to render at render time: - TableSelection(Base/Single/Multi) expose getSelectionComponent(), getClearAllComponent() and getClearAllIcon(). Single returns RadioButton, Multi returns CheckBox plus the ClearAll Icon; the base returns undefined. - TableRowActionBase exposes overflowButtonComponent / overflowButtonIcon and owns the Button + overflow icon imports. - TableHeaderCell exposes _sortIconComponent / _sortIcon, returning the Icon class and sort-ascending/descending icon only while the column is sorted, so an unsorted table no longer bundles Icon or the sort icon modules. - TableRowBase/TableRow/TableHeaderRow surface these via _selectionComponent, _clearAllComponent, _overflowButtonComponent getters that delegate to the active feature/action. The templates render the resolved components (, , , ), guarded so nothing renders when the feature/action/sort state is absent. Enum values are passed as string literals now that the concrete component owns its types. Net effect: CheckBox, RadioButton, Button, Icon and their icons are imported by the selection features, row actions and header cell rather than by the row, so a plain read-only table no longer bundles selection/action/sort controls. --- packages/main/src/TableHeaderCell.ts | 18 +++++++++++ packages/main/src/TableHeaderCellTemplate.tsx | 20 +++---------- packages/main/src/TableHeaderRow.ts | 8 +++++ packages/main/src/TableHeaderRowTemplate.tsx | 30 +++++++++---------- packages/main/src/TableRow.ts | 8 +++++ packages/main/src/TableRowActionBase.ts | 11 ++++++- packages/main/src/TableRowBase.ts | 4 +++ packages/main/src/TableRowTemplate.tsx | 30 +++++++------------ packages/main/src/TableSelection.ts | 14 +++++++++ packages/main/src/TableSelectionBase.ts | 22 ++++++++++++++ packages/main/src/TableSelectionMulti.ts | 15 ++++++++++ packages/main/src/TableSelectionSingle.ts | 5 ++++ 12 files changed, 133 insertions(+), 52 deletions(-) diff --git a/packages/main/src/TableHeaderCell.ts b/packages/main/src/TableHeaderCell.ts index 5107c46ffadfc..46dcb4dd5fe1c 100644 --- a/packages/main/src/TableHeaderCell.ts +++ b/packages/main/src/TableHeaderCell.ts @@ -4,6 +4,9 @@ import TableCellBase from "./TableCellBase.js"; import TableHeaderCellTemplate from "./TableHeaderCellTemplate.js"; import TableHeaderCellStyles from "./generated/themes/TableHeaderCell.css.js"; import SortOrder from "@ui5/webcomponents-base/dist/types/SortOrder.js"; +import Icon from "./Icon.js"; +import SortAscending from "@ui5/webcomponents-icons/dist/sort-ascending.js"; +import SortDescending from "@ui5/webcomponents-icons/dist/sort-descending.js"; import query from "@ui5/webcomponents-base/dist/decorators/query.js"; import type TableHeaderCellActionBase from "./TableHeaderCellActionBase.js"; import type { Slot } from "@ui5/webcomponents-base/dist/UI5Element.js"; @@ -132,6 +135,21 @@ class TableHeaderCell extends TableCellBase { ariaRole: string = "columnheader"; _popinWidth: number = 0; + get _sortIconComponent(): typeof Icon | undefined { + return this.sortIndicator === SortOrder.None ? undefined : Icon; + } + + get _sortIcon(): string | undefined { + switch (this.sortIndicator) { + case SortOrder.Ascending: + return SortAscending; + case SortOrder.Descending: + return SortDescending; + default: + return undefined; + } + } + onBeforeRendering() { super.onBeforeRendering(); this.style.textAlign = this.horizontalAlign || ""; diff --git a/packages/main/src/TableHeaderCellTemplate.tsx b/packages/main/src/TableHeaderCellTemplate.tsx index 9f05d0ad74f14..2e1c7d5706fda 100644 --- a/packages/main/src/TableHeaderCellTemplate.tsx +++ b/packages/main/src/TableHeaderCellTemplate.tsx @@ -1,26 +1,14 @@ -import Icon from "./Icon.js"; -import SortOrder from "@ui5/webcomponents-base/dist/types/SortOrder.js"; -import SortAscending from "@ui5/webcomponents-icons/dist/sort-ascending.js"; -import SortDescending from "@ui5/webcomponents-icons/dist/sort-descending.js"; import type TableHeaderCell from "./TableHeaderCell.js"; export default function TableHeaderCellTemplate(this: TableHeaderCell) { + const SortIconComponent = this._sortIconComponent; return ( <> - { sortIcon.call(this) } + { SortIconComponent && + + } ); } - -function sortIcon(this: TableHeaderCell) { - switch (this.sortIndicator) { - case SortOrder.Ascending: - return ; - case SortOrder.Descending: - return ; - default: - return <>; - } -} diff --git a/packages/main/src/TableHeaderRow.ts b/packages/main/src/TableHeaderRow.ts index b74ed87b799a6..9352f77d52297 100644 --- a/packages/main/src/TableHeaderRow.ts +++ b/packages/main/src/TableHeaderRow.ts @@ -105,6 +105,14 @@ class TableHeaderRow extends TableRowBase { return (this._tableSelection as TableSelectionMulti).headerSelector === "ClearAll"; } + get _clearAllComponent() { + return this._tableSelection?.getClearAllComponent(); + } + + get _clearAllIcon() { + return this._tableSelection?.getClearAllIcon(); + } + get _selectionCellAriaDescription() { return this._tableSelection?.getAriaDescriptionForColumnHeader(); } diff --git a/packages/main/src/TableHeaderRowTemplate.tsx b/packages/main/src/TableHeaderRowTemplate.tsx index 68b4e4595eeb6..c41ea3d9559d5 100644 --- a/packages/main/src/TableHeaderRowTemplate.tsx +++ b/packages/main/src/TableHeaderRowTemplate.tsx @@ -1,12 +1,9 @@ -import CheckBox from "./CheckBox.js"; import TableHeaderCell from "./TableHeaderCell.js"; -import Icon from "./Icon.js"; -import IconMode from "./types/IconMode.js"; -import ClearAll from "@ui5/webcomponents-icons/dist/clear-all.js"; -import IconDesign from "./types/IconDesign.js"; import type TableHeaderRow from "./TableHeaderRow.js"; export default function TableHeaderRowTemplate(this: TableHeaderRow, ariaColIndex: number = 1) { + const SelectionComponent = this._selectionComponent; + const ClearAllComponent = this._clearAllComponent; return ( <> { this._hasSelector && @@ -22,22 +19,25 @@ export default function TableHeaderRowTemplate(this: TableHeaderRow, ariaColInde <> : this._shouldRenderClearAll ? - + (ClearAllComponent && + + ) : - + > } } diff --git a/packages/main/src/TableRow.ts b/packages/main/src/TableRow.ts index 58d58ca40639c..0fcfdbeb854a9 100644 --- a/packages/main/src/TableRow.ts +++ b/packages/main/src/TableRow.ts @@ -212,6 +212,14 @@ class TableRow extends TableRowBase { return TableRowBase.i18nBundle.getText(TABLE_ROW_OVERFLOW_BUTTON); } + get _overflowButtonComponent(): typeof Button | undefined { + return this.actions.at(0)?.overflowButtonComponent; + } + + get _overflowButtonIcon(): string | undefined { + return this.actions.at(0)?.overflowButtonIcon; + } + get _flexibleActions() { const flexibleActions = this.actions.filter(action => !action.isFixedAction()); const fixedActionsCount = this.actions.length - flexibleActions.length; diff --git a/packages/main/src/TableRowActionBase.ts b/packages/main/src/TableRowActionBase.ts index 4c83b15aacf7d..561b34148518e 100644 --- a/packages/main/src/TableRowActionBase.ts +++ b/packages/main/src/TableRowActionBase.ts @@ -5,12 +5,13 @@ import type { UI5CustomEvent } from "@ui5/webcomponents-base"; import TableRowActionBaseTemplate from "./TableRowActionBaseTemplate.js"; import TableRowActionBaseStyles from "./generated/themes/TableRowActionBase.css.js"; +import Button from "./Button.js"; +import iconOverflow from "@ui5/webcomponents-icons/dist/overflow.js"; import type Menu from "./Menu.js"; import type MenuItem from "./MenuItem.js"; import type Table from "./Table.js"; import type TableRow from "./TableRow.js"; import type TableRowAction from "./TableRowAction.js"; -import type Button from "./Button.js"; let MenuConstructor: new () => Menu; let MenuItemConstructor: new () => MenuItem; @@ -56,6 +57,14 @@ abstract class TableRowActionBase extends UI5Element { @property({ type: Boolean }) invisible = false; + get overflowButtonComponent(): typeof Button { + return Button; + } + + get overflowButtonIcon(): string { + return iconOverflow; + } + private static _menu: Menu; private static _menuItems = new WeakMap(); static async showMenu(actions: TableRowActionBase[], opener: HTMLElement) { diff --git a/packages/main/src/TableRowBase.ts b/packages/main/src/TableRowBase.ts index 0186d191132d6..4b021af3ea21d 100644 --- a/packages/main/src/TableRowBase.ts +++ b/packages/main/src/TableRowBase.ts @@ -144,6 +144,10 @@ abstract class TableRowBase extends return !!this._tableSelection?.isMultiSelectable(); } + get _selectionComponent() { + return this._tableSelection?.getSelectionComponent(); + } + get _hasSelector() { return this._table?._isRowSelectorRequired; } diff --git a/packages/main/src/TableRowTemplate.tsx b/packages/main/src/TableRowTemplate.tsx index 17a0850f863b5..f2f3b17f3a5ce 100644 --- a/packages/main/src/TableRowTemplate.tsx +++ b/packages/main/src/TableRowTemplate.tsx @@ -1,12 +1,9 @@ import TableCell from "./TableCell.js"; -import CheckBox from "./CheckBox.js"; -import RadioButton from "./RadioButton.js"; -import Button from "./Button.js"; -import ButtonDesign from "./types/ButtonDesign.js"; -import iconOverflow from "@ui5/webcomponents-icons/dist/overflow.js"; import type TableRow from "./TableRow.js"; export default function TableRowTemplate(this: TableRow, ariaColIndex: number = 1) { + const SelectionComponent = this._selectionComponent; + const OverflowButton = this._overflowButtonComponent; return ( <> { this._hasSelector && @@ -17,20 +14,13 @@ export default function TableRowTemplate(this: TableRow, ariaColIndex: number = data-ui5-table-selection-cell data-ui5-acc-text="" > - { this._isMultiSelect ? - - : - + > } } @@ -62,13 +52,13 @@ export default function TableRowTemplate(this: TableRow, ariaColIndex: number = ))} - { this._hasOverflowActions && - + > } { this._fixedActions.map(action => ( diff --git a/packages/main/src/TableSelection.ts b/packages/main/src/TableSelection.ts index e65c3d2392f47..33d2068ef227a 100644 --- a/packages/main/src/TableSelection.ts +++ b/packages/main/src/TableSelection.ts @@ -7,6 +7,8 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; import TableSelectionMode from "./types/TableSelectionMode.js"; +import CheckBox from "./CheckBox.js"; +import RadioButton from "./RadioButton.js"; import { isSelectionCell, isHeaderSelectionCell, findRowInPath } from "./TableUtils.js"; import type Table from "./Table.js"; import type { ITableFeature } from "./Table.js"; @@ -135,6 +137,18 @@ class TableSelection extends UI5Element implements ITableFeature { return this.mode !== TableSelectionMode.None; } + getSelectionComponent(): typeof CheckBox | typeof RadioButton { + return this.isMultiSelectable() ? CheckBox : RadioButton; + } + + getClearAllComponent(): undefined { + return undefined; + } + + getClearAllIcon(): undefined { + return undefined; + } + getAriaDescriptionForTable(): string | undefined { return undefined; } diff --git a/packages/main/src/TableSelectionBase.ts b/packages/main/src/TableSelectionBase.ts index 7578210a1c2d5..168e5de018257 100644 --- a/packages/main/src/TableSelectionBase.ts +++ b/packages/main/src/TableSelectionBase.ts @@ -1,6 +1,7 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; import { property, eventStrict } from "@ui5/webcomponents-base/dist/decorators.js"; import { isInstanceOfTable } from "./TableUtils.js"; +import type Icon from "./Icon.js"; import type Table from "./Table.js"; import type TableRowBase from "./TableRowBase.js"; import type TableRow from "./TableRow.js"; @@ -106,6 +107,27 @@ abstract class TableSelectionBase extends UI5Element implements ITableFeature { return this.behavior === TableSelectionBehavior.RowSelector; } + /** + * Returns the component used to render the row selector (for example, `CheckBox` or `RadioButton`). + */ + getSelectionComponent(): typeof UI5Element | undefined { + return undefined; + } + + /** + * Returns the component used to render the "Clear All" selector in the column header. + */ + getClearAllComponent(): typeof Icon | undefined { + return undefined; + } + + /** + * Returns the icon name used for the "Clear All" selector in the column header. + */ + getClearAllIcon(): string | undefined { + return undefined; + } + /** * Returns the ARIA description of the Table as an alternative to aria-multiselectable. */ diff --git a/packages/main/src/TableSelectionMulti.ts b/packages/main/src/TableSelectionMulti.ts index 34e46b5377d82..36a95db2d23d4 100644 --- a/packages/main/src/TableSelectionMulti.ts +++ b/packages/main/src/TableSelectionMulti.ts @@ -1,5 +1,8 @@ import { customElement, property } from "@ui5/webcomponents-base/dist/decorators.js"; import TableSelectionBase from "./TableSelectionBase.js"; +import CheckBox from "./CheckBox.js"; +import Icon from "./Icon.js"; +import ClearAll from "@ui5/webcomponents-icons/dist/clear-all.js"; import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js"; import { isSelectionCell, isHeaderSelectionCell, findRowInPath } from "./TableUtils.js"; import { isUpShift } from "@ui5/webcomponents-base/dist/Keys.js"; @@ -94,6 +97,18 @@ class TableSelectionMulti extends TableSelectionBase { return true; } + getSelectionComponent(): typeof CheckBox { + return CheckBox; + } + + getClearAllComponent(): typeof Icon { + return Icon; + } + + getClearAllIcon(): string { + return ClearAll; + } + isSelected(row: TableRowBase): boolean { if (row.isHeaderRow()) { return this.headerSelector === "ClearAll" ? true : this.areAllRowsSelected(); diff --git a/packages/main/src/TableSelectionSingle.ts b/packages/main/src/TableSelectionSingle.ts index 85332ec173ecc..782befbb3b0ed 100644 --- a/packages/main/src/TableSelectionSingle.ts +++ b/packages/main/src/TableSelectionSingle.ts @@ -1,5 +1,6 @@ import { customElement, property } from "@ui5/webcomponents-base/dist/decorators.js"; import TableSelectionBase from "./TableSelectionBase.js"; +import RadioButton from "./RadioButton.js"; import type TableRow from "./TableRow.js"; /** @@ -48,6 +49,10 @@ class TableSelectionSingle extends TableSelectionBase { return rowKey ? this.selected === rowKey : false; } + getSelectionComponent(): typeof RadioButton { + return RadioButton; + } + setSelected(row: TableRow, selected: boolean, fireEvent: boolean = false) { const rowKey = this.getRowKey(row); if (rowKey) {