Skip to content

Commit ff6d77d

Browse files
authored
feat(ui5-table): action header cell is added (#10698)
* feat(ui5-table): action header cell is added * feat(ui5-table): add label component for the header cell
1 parent 92da37d commit ff6d77d

25 files changed

+503
-40
lines changed

packages/main/cypress/specs/Table.cy.tsx

+100
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import TableCell from "../../src/TableCell.js";
44
import TableRow from "../../src/TableRow.js";
55
import TableSelection from "../../src/TableSelection.js";
66
import TableHeaderCell from "../../src/TableHeaderCell.js";
7+
import TableHeaderCellActionAI from "../../src/TableHeaderCellActionAI.js";
78
import Label from "../../src/Label.js";
89
import Input from "../../src/Input.js";
910
import Bar from "../../src/Bar.js";
@@ -663,3 +664,102 @@ describe("Table - Navigated Rows", () => {
663664
});
664665
});
665666
});
667+
668+
describe("Table - HeaderCell", () => {
669+
beforeEach(() => {
670+
cy.mount(
671+
<Table overflow-mode="Popin">
672+
<TableHeaderRow slot="headerRow">
673+
<TableHeaderCell min-width="300px">Column A</TableHeaderCell>
674+
<TableHeaderCell min-width="200px" sort-indicator="Ascending">
675+
<Label required wrappingType="None">Column B</Label>
676+
<TableHeaderCellActionAI slot="action"></TableHeaderCellActionAI>
677+
</TableHeaderCell>
678+
<TableHeaderCell min-width="150px" popin-text="Popin Text">
679+
<Label required>Column C</Label>
680+
</TableHeaderCell>
681+
</TableHeaderRow>
682+
<TableRow>
683+
<TableCell>Cell A</TableCell>
684+
<TableCell>Cell B</TableCell>
685+
<TableCell>Cell C</TableCell>
686+
</TableRow>
687+
<TableRow>
688+
<TableCell>Cell A</TableCell>
689+
<TableCell>Cell B</TableCell>
690+
<TableCell>Cell C</TableCell>
691+
</TableRow>
692+
</Table>
693+
);
694+
cy.get("[ui5-table]").as("table").children("ui5-table-row").as("rows");
695+
cy.get("@table").children("ui5-table-header-row").first().as("headerRow");
696+
cy.get("@headerRow").get("ui5-table-header-cell").each(($headerCell, index) => {
697+
cy.wrap($headerCell).as(`headerCell${index + 1}`);
698+
});
699+
cy.get("@rows").each(($row, index) => {
700+
cy.wrap($row).as(`row${index + 1}`);
701+
});
702+
});
703+
704+
it("should render header-cell correctly", () => {
705+
cy.get("@headerCell1").contains("Column A");
706+
cy.get("@headerCell2").should("have.attr", "aria-sort", "ascending");
707+
cy.get("@headerCell2").find("ui5-table-header-cell-action-ai").as("actionB");
708+
cy.get("@actionB").shadow().find("ui5-button").as("actionBbutton");
709+
cy.get("@actionBbutton").should("have.attr", "icon", "ai");
710+
cy.get("@actionBbutton").should("have.attr", "tooltip", "Generated by AI");
711+
cy.get("@actionB").invoke("on", "click", cy.stub().as("actionBclick"));
712+
cy.get("@actionBbutton").realClick();
713+
cy.get("@actionBclick").should("have.been.calledOnce");
714+
cy.get("@headerCell2").shadow().find("ui5-icon").as("actionBicon");
715+
cy.get("@actionBicon").should("have.attr", "name", "sort-ascending");
716+
717+
cy.get("@headerCell2").invoke("attr", "sort-indicator", "Descending");
718+
cy.get("@headerCell2").shadow().find("ui5-icon").should("have.attr", "name", "sort-descending");
719+
cy.get("@actionBicon").should("have.attr", "name", "sort-descending");
720+
cy.get("@headerCell2").should("have.attr", "aria-sort", "descending");
721+
722+
cy.get("@headerCell2").invoke("attr", "sort-indicator", "None");
723+
cy.get("@headerCell2").shadow().find("ui5-icon").should("not.exist");
724+
cy.get("@headerCell2").should("not.have.attr", "aria-sort");
725+
726+
cy.get("@table").invoke("css", "width", "250px");
727+
// eslint-disable-next-line cypress/no-unnecessary-waiting
728+
cy.wait(50);
729+
730+
cy.get("@row1").find("ui5-table-cell[_popin]").as("row1popins");
731+
cy.get("@row1popins").first().as("row1popinB");
732+
cy.get("@row1popinB").shadow().find("ui5-table-header-cell-action-ai").as("row1popinBaction");
733+
cy.get("@row1popinBaction").shadow().find("ui5-button").as("row1popinBbutton");
734+
cy.get("@row1popinBbutton").should("have.attr", "icon", "ai");
735+
cy.get("@row1popinBbutton").should("have.attr", "design", "Transparent");
736+
cy.get("@row1popinBbutton").should("have.attr", "tooltip", "Generated by AI");
737+
cy.get("@row1popinBbutton").realClick();
738+
cy.get("@actionBclick").invoke("getCall", 1).its("args.0.detail.targetRef").as("actionBclickTarget");
739+
cy.get("@actionBclickTarget").should("have.attr", "icon", "ai");
740+
cy.get("@actionBclickTarget").should("have.attr", "design", "Transparent");
741+
cy.get("@actionBclickTarget").should("have.attr", "tooltip", "Generated by AI");
742+
743+
cy.get("@row1popinB").shadow().find("ui5-label").as("row1popinBlabel");
744+
cy.get("@row1popinBlabel").contains("Column B");
745+
cy.get("@row1popinBlabel").should("have.attr", "wrapping-type", "None");
746+
cy.get("@row1popinBlabel").should("have.attr", "required");
747+
748+
cy.get("@row1popins").last().as("row1popinC");
749+
cy.get("@row1popinC").shadow().find("ui5-label").should("not.exist");
750+
cy.get("@row1popinC").shadow().should("have.text", "Popin Text:");
751+
cy.get("@row1popinC").should("have.text", "Cell C");
752+
753+
cy.get("@row2").find("ui5-table-cell[_popin]").as("row2popins");
754+
cy.get("@row2popins").first().as("row2popinB");
755+
cy.get("@row2popinB").shadow().find("ui5-table-header-cell-action-ai").as("row2popinBaction");
756+
cy.get("@row2popinBaction").shadow().find("ui5-button").as("row2popinBbutton");
757+
cy.get("@row2popinBbutton").should("have.attr", "icon", "ai");
758+
cy.get("@row2popinBbutton").should("have.attr", "tooltip", "Generated by AI");
759+
cy.get("@row2popinBbutton").realClick();
760+
cy.get("@actionBclick").invoke("getCall", 2).its("args.0.detail.targetRef").as("actionBclickTarget");
761+
cy.get("@actionBclickTarget").should("have.attr", "icon", "ai");
762+
cy.get("@actionBclickTarget").should("have.attr", "design", "Transparent");
763+
cy.get("@actionBclickTarget").should("have.attr", "tooltip", "Generated by AI");
764+
});
765+
});

packages/main/src/Table.ts

+1
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ type TableRowActionClickEventDetail = {
237237
* Fired when a row action is clicked.
238238
*
239239
* @param {TableRowActionBase} action The row action instance
240+
* @param {TableRow} row The row instance
240241
* @since 2.6.0
241242
* @public
242243
*/

packages/main/src/TableCell.hbs

+6-11
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
{{#if _popin}}
2-
{{#if _popinText}}
3-
{{_popinText}}
4-
<span class="popin-colon">{{_i18nPopinColon}}</span>
5-
{{else if _popinHeader}}
6-
{{_popinHeader}}
7-
<span class="popin-colon">{{_i18nPopinColon}}</span>
8-
{{/if}}
9-
<slot></slot>
10-
{{else}}
11-
<slot></slot>
12-
{{/if}}
2+
{{#each _popinHeaderNodes}}
3+
{{this}}
4+
{{/each}}
5+
<span class="popin-colon">{{_i18nPopinColon}}</span>
6+
{{/if}}
7+
<slot></slot>

packages/main/src/TableCell.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,18 @@ class TableCell extends TableCellBase {
4646
return table.headerRow[0].cells[index];
4747
}
4848

49-
get _popinText() {
50-
return this._headerCell?.popinText;
51-
}
52-
53-
get _popinHeader() {
54-
return this._headerCell?.content[0]?.cloneNode(true);
49+
get _popinHeaderNodes() {
50+
const nodes = [];
51+
const headerCell = this._headerCell;
52+
if (headerCell.popinText) {
53+
nodes.push(headerCell.popinText);
54+
} else {
55+
nodes.push(...this._headerCell.content.map(node => node.cloneNode(true)));
56+
}
57+
if (headerCell.action[0]) {
58+
nodes.push(headerCell.action[0].cloneNode(true));
59+
}
60+
return nodes;
5561
}
5662

5763
get _i18nPopinColon() {

packages/main/src/TableHeaderCell.hbs

+5-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
<slot></slot>
1+
<slot name="action"></slot>
2+
<slot></slot>
3+
{{#if _sortIcon}}
4+
<ui5-icon name="{{_sortIcon}}"></ui5-icon>
5+
{{/if}}

packages/main/src/TableHeaderCell.ts

+39-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
2-
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
1+
import { customElement, property, slot } from "@ui5/webcomponents-base/dist/decorators.js";
32
import TableCellBase from "./TableCellBase.js";
43
import TableHeaderCellTemplate from "./generated/templates/TableHeaderCellTemplate.lit.js";
54
import TableHeaderCellStyles from "./generated/themes/TableHeaderCell.css.js";
5+
import Icon from "./Icon.js";
6+
import TableSortOrder from "./types/TableSortOrder.js";
7+
import type TableHeaderCellActionBase from "./TableHeaderCellActionBase.js";
8+
import "@ui5/webcomponents-icons/dist/sort-ascending.js";
9+
import "@ui5/webcomponents-icons/dist/sort-descending.js";
610

711
/**
812
* @class
@@ -28,6 +32,7 @@ import TableHeaderCellStyles from "./generated/themes/TableHeaderCell.css.js";
2832
tag: "ui5-table-header-cell",
2933
styles: [TableCellBase.styles, TableHeaderCellStyles],
3034
template: TableHeaderCellTemplate,
35+
dependencies: [Icon],
3136
})
3237
class TableHeaderCell extends TableCellBase {
3338
/**
@@ -85,6 +90,27 @@ class TableHeaderCell extends TableCellBase {
8590
@property()
8691
popinText?: string;
8792

93+
/**
94+
* Defines the sort indicator of the column.
95+
*
96+
* @default "None"
97+
* @since 2.8.0
98+
* @public
99+
*/
100+
@property()
101+
sortIndicator: `${TableSortOrder}` = "None";
102+
103+
/**
104+
* Defines the action of the column.
105+
*
106+
* **Note:** While multiple actions are technically possible, this is not supported.
107+
*
108+
* @public
109+
* @since 2.8.0
110+
*/
111+
@slot()
112+
action!: Array<TableHeaderCellActionBase>;
113+
88114
@property({ type: Boolean, noAttribute: true })
89115
_popin = false;
90116

@@ -104,6 +130,17 @@ class TableHeaderCell extends TableCellBase {
104130
// overwrite setting of TableCellBase so that the TableHeaderCell always uses the slot variable
105131
this.style.justifyContent = `var(--horizontal-align-${this._individualSlot})`;
106132
}
133+
if (this.sortIndicator !== TableSortOrder.None) {
134+
this.setAttribute("aria-sort", this.sortIndicator.toLowerCase());
135+
} else if (this.hasAttribute("aria-sort")) {
136+
this.removeAttribute("aria-sort");
137+
}
138+
}
139+
140+
get _sortIcon() {
141+
if (this.sortIndicator !== TableSortOrder.None) {
142+
return `sort-${this.sortIndicator.toLowerCase()}`;
143+
}
107144
}
108145
}
109146

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { customElement, i18n } from "@ui5/webcomponents-base/dist/decorators.js";
2+
import TableHeaderCellActionBase from "./TableHeaderCellActionBase.js";
3+
import { TABLE_GENERATED_BY_AI } from "./generated/i18n/i18n-defaults.js";
4+
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
5+
import "@ui5/webcomponents-icons/dist/ai.js";
6+
7+
/**
8+
* @class
9+
*
10+
* ### Overview
11+
*
12+
* The `ui5-table-header-cell-action-ai` component defines a dedicated AI action for the table column.
13+
*
14+
* ### ES6 Module Import
15+
*
16+
* `import "@ui5/webcomponents/dist/TableHeaderCellActionAI.js";`
17+
*
18+
* @constructor
19+
* @extends TableHeaderCellActionBase
20+
* @since 2.8.0
21+
* @public
22+
*/
23+
@customElement({ tag: "ui5-table-header-cell-action-ai" })
24+
25+
class TableHeaderCellActionAI extends TableHeaderCellActionBase {
26+
@i18n("@ui5/webcomponents")
27+
static i18nBundle: I18nBundle;
28+
29+
getRenderInfo() {
30+
return {
31+
icon: "ai",
32+
tooltip: TableHeaderCellActionAI.i18nBundle.getText(TABLE_GENERATED_BY_AI),
33+
};
34+
}
35+
}
36+
37+
TableHeaderCellActionAI.define();
38+
39+
export default TableHeaderCellActionAI;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<ui5-button
2+
icon="{{_icon}}"
3+
tooltip="{{_tooltip}}"
4+
@click={{_onClick}}
5+
design="Transparent">
6+
</ui5-button>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
2+
import { customElement, eventStrict } from "@ui5/webcomponents-base/dist/decorators.js";
3+
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
4+
import TableHeaderCellActionBaseTemplate from "./generated/templates/TableHeaderCellActionBaseTemplate.lit.js";
5+
import TableHeaderCellActionBaseStyles from "./generated/themes/TableHeaderCellActionBase.css.js";
6+
import Button from "./Button.js";
7+
import type TableCell from "./TableCell.js";
8+
9+
/**
10+
* Fired when a header cell action is clicked.
11+
*
12+
* @param {HTMLElement} targetRef The reference to the element that triggered the event
13+
* @public
14+
* @since 2.8.0
15+
*/
16+
type TableHeaderCellActionClickEventDetail = {
17+
targetRef: HTMLElement;
18+
};
19+
20+
/**
21+
* Fired when a header cell action is clicked.
22+
*
23+
* @param {HTMLElement} targetRef The reference to the element that triggered the event
24+
* @public
25+
* @since 2.8.0
26+
*/
27+
@eventStrict("click", {
28+
bubbles: false,
29+
})
30+
31+
/**
32+
* @class
33+
* The `TableHeaderCellActionBase` class serves as a foundation for table header cell actions.
34+
* @constructor
35+
* @abstract
36+
* @extends UI5Element
37+
* @since 2.8.0
38+
* @public
39+
*/
40+
@customElement({
41+
renderer: litRender,
42+
styles: TableHeaderCellActionBaseStyles,
43+
template: TableHeaderCellActionBaseTemplate,
44+
dependencies: [Button],
45+
})
46+
abstract class TableHeaderCellActionBase extends UI5Element {
47+
eventDetails!: {
48+
"click": TableHeaderCellActionClickEventDetail,
49+
}
50+
51+
abstract getRenderInfo(): {
52+
icon: string;
53+
tooltip: string;
54+
};
55+
56+
onBeforeRendering() {
57+
this.toggleAttribute("_popin", !this.parentElement);
58+
}
59+
60+
_onClick(e: MouseEvent) {
61+
const action = this.parentElement ? this : ((this.getRootNode() as ShadowRoot).host as TableCell)._headerCell.action[0] as this;
62+
action.fireDecoratorEvent("click", { targetRef: e.target as HTMLElement });
63+
e.stopPropagation();
64+
}
65+
66+
get _tooltip() {
67+
return this.getRenderInfo().tooltip;
68+
}
69+
70+
get _icon() {
71+
return this.getRenderInfo().icon;
72+
}
73+
}
74+
75+
export default TableHeaderCellActionBase;
76+
77+
export type {
78+
TableHeaderCellActionClickEventDetail,
79+
};

packages/main/src/TableHeaderRow.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
2-
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
3-
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
1+
import { customElement, slot, property } from "@ui5/webcomponents-base/dist/decorators.js";
42
import TableRowBase from "./TableRowBase.js";
53
import TableHeaderRowTemplate from "./generated/templates/TableHeaderRowTemplate.lit.js";
64
import TableHeaderRowStyles from "./generated/themes/TableHeaderRow.css.js";

packages/main/src/TableRowActionBase.hbs

+1-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
{{else if _isInteractive}}
44
<ui5-button
55
icon="{{_icon}}"
6-
text="{{_text}}"
76
tooltip="{{_text}}"
87
@click={{_onActionClick}}
98
design="Transparent">
@@ -12,6 +11,6 @@
1211
<ui5-icon
1312
name="{{_icon}}"
1413
tooltip="{{_text}}"
15-
design="Transparent">
14+
design="NonInteractive">
1615
</ui5-icon>
1716
{{/if}}

0 commit comments

Comments
 (0)