Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui5-table): action header cell is added #10698

Merged
merged 2 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions packages/main/cypress/specs/Table.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import TableCell from "../../src/TableCell.js";
import TableRow from "../../src/TableRow.js";
import TableSelection from "../../src/TableSelection.js";
import TableHeaderCell from "../../src/TableHeaderCell.js";
import TableHeaderCellActionAI from "../../src/TableHeaderCellActionAI.js";
import Label from "../../src/Label.js";
import Input from "../../src/Input.js";
import Bar from "../../src/Bar.js";
Expand Down Expand Up @@ -663,3 +664,102 @@ describe("Table - Navigated Rows", () => {
});
});
});

describe("Table - HeaderCell", () => {
beforeEach(() => {
cy.mount(
<Table overflow-mode="Popin">
<TableHeaderRow slot="headerRow">
<TableHeaderCell min-width="300px">Column A</TableHeaderCell>
<TableHeaderCell min-width="200px" sort-indicator="Ascending">
<Label required wrappingType="None">Column B</Label>
<TableHeaderCellActionAI slot="action"></TableHeaderCellActionAI>
</TableHeaderCell>
<TableHeaderCell min-width="150px" popin-text="Popin Text">
<Label required>Column C</Label>
</TableHeaderCell>
</TableHeaderRow>
<TableRow>
<TableCell>Cell A</TableCell>
<TableCell>Cell B</TableCell>
<TableCell>Cell C</TableCell>
</TableRow>
<TableRow>
<TableCell>Cell A</TableCell>
<TableCell>Cell B</TableCell>
<TableCell>Cell C</TableCell>
</TableRow>
</Table>
);
cy.get("[ui5-table]").as("table").children("ui5-table-row").as("rows");
cy.get("@table").children("ui5-table-header-row").first().as("headerRow");
cy.get("@headerRow").get("ui5-table-header-cell").each(($headerCell, index) => {
cy.wrap($headerCell).as(`headerCell${index + 1}`);
});
cy.get("@rows").each(($row, index) => {
cy.wrap($row).as(`row${index + 1}`);
});
});

it("should render header-cell correctly", () => {
cy.get("@headerCell1").contains("Column A");
cy.get("@headerCell2").should("have.attr", "aria-sort", "ascending");
cy.get("@headerCell2").find("ui5-table-header-cell-action-ai").as("actionB");
cy.get("@actionB").shadow().find("ui5-button").as("actionBbutton");
cy.get("@actionBbutton").should("have.attr", "icon", "ai");
cy.get("@actionBbutton").should("have.attr", "tooltip", "Generated by AI");
cy.get("@actionB").invoke("on", "click", cy.stub().as("actionBclick"));
cy.get("@actionBbutton").realClick();
cy.get("@actionBclick").should("have.been.calledOnce");
cy.get("@headerCell2").shadow().find("ui5-icon").as("actionBicon");
cy.get("@actionBicon").should("have.attr", "name", "sort-ascending");

cy.get("@headerCell2").invoke("attr", "sort-indicator", "Descending");
cy.get("@headerCell2").shadow().find("ui5-icon").should("have.attr", "name", "sort-descending");
cy.get("@actionBicon").should("have.attr", "name", "sort-descending");
cy.get("@headerCell2").should("have.attr", "aria-sort", "descending");

cy.get("@headerCell2").invoke("attr", "sort-indicator", "None");
cy.get("@headerCell2").shadow().find("ui5-icon").should("not.exist");
cy.get("@headerCell2").should("not.have.attr", "aria-sort");

cy.get("@table").invoke("css", "width", "250px");
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(50);

cy.get("@row1").find("ui5-table-cell[_popin]").as("row1popins");
cy.get("@row1popins").first().as("row1popinB");
cy.get("@row1popinB").shadow().find("ui5-table-header-cell-action-ai").as("row1popinBaction");
cy.get("@row1popinBaction").shadow().find("ui5-button").as("row1popinBbutton");
cy.get("@row1popinBbutton").should("have.attr", "icon", "ai");
cy.get("@row1popinBbutton").should("have.attr", "design", "Transparent");
cy.get("@row1popinBbutton").should("have.attr", "tooltip", "Generated by AI");
cy.get("@row1popinBbutton").realClick();
cy.get("@actionBclick").invoke("getCall", 1).its("args.0.detail.targetRef").as("actionBclickTarget");
cy.get("@actionBclickTarget").should("have.attr", "icon", "ai");
cy.get("@actionBclickTarget").should("have.attr", "design", "Transparent");
cy.get("@actionBclickTarget").should("have.attr", "tooltip", "Generated by AI");

cy.get("@row1popinB").shadow().find("ui5-label").as("row1popinBlabel");
cy.get("@row1popinBlabel").contains("Column B");
cy.get("@row1popinBlabel").should("have.attr", "wrapping-type", "None");
cy.get("@row1popinBlabel").should("have.attr", "required");

cy.get("@row1popins").last().as("row1popinC");
cy.get("@row1popinC").shadow().find("ui5-label").should("not.exist");
cy.get("@row1popinC").shadow().should("have.text", "Popin Text:");
cy.get("@row1popinC").should("have.text", "Cell C");

cy.get("@row2").find("ui5-table-cell[_popin]").as("row2popins");
cy.get("@row2popins").first().as("row2popinB");
cy.get("@row2popinB").shadow().find("ui5-table-header-cell-action-ai").as("row2popinBaction");
cy.get("@row2popinBaction").shadow().find("ui5-button").as("row2popinBbutton");
cy.get("@row2popinBbutton").should("have.attr", "icon", "ai");
cy.get("@row2popinBbutton").should("have.attr", "tooltip", "Generated by AI");
cy.get("@row2popinBbutton").realClick();
cy.get("@actionBclick").invoke("getCall", 2).its("args.0.detail.targetRef").as("actionBclickTarget");
cy.get("@actionBclickTarget").should("have.attr", "icon", "ai");
cy.get("@actionBclickTarget").should("have.attr", "design", "Transparent");
cy.get("@actionBclickTarget").should("have.attr", "tooltip", "Generated by AI");
});
});
1 change: 1 addition & 0 deletions packages/main/src/Table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ type TableRowActionClickEventDetail = {
* Fired when a row action is clicked.
*
* @param {TableRowActionBase} action The row action instance
* @param {TableRow} row The row instance
* @since 2.6.0
* @public
*/
Expand Down
17 changes: 6 additions & 11 deletions packages/main/src/TableCell.hbs
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
{{#if _popin}}
{{#if _popinText}}
{{_popinText}}
<span class="popin-colon">{{_i18nPopinColon}}</span>
{{else if _popinHeader}}
{{_popinHeader}}
<span class="popin-colon">{{_i18nPopinColon}}</span>
{{/if}}
<slot></slot>
{{else}}
<slot></slot>
{{/if}}
{{#each _popinHeaderNodes}}
{{this}}
{{/each}}
<span class="popin-colon">{{_i18nPopinColon}}</span>
{{/if}}
<slot></slot>
18 changes: 12 additions & 6 deletions packages/main/src/TableCell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,18 @@ class TableCell extends TableCellBase {
return table.headerRow[0].cells[index];
}

get _popinText() {
return this._headerCell?.popinText;
}

get _popinHeader() {
return this._headerCell?.content[0]?.cloneNode(true);
get _popinHeaderNodes() {
const nodes = [];
const headerCell = this._headerCell;
if (headerCell.popinText) {
nodes.push(headerCell.popinText);
} else {
nodes.push(...this._headerCell.content.map(node => node.cloneNode(true)));
}
if (headerCell.action[0]) {
nodes.push(headerCell.action[0].cloneNode(true));
}
return nodes;
}

get _i18nPopinColon() {
Expand Down
6 changes: 5 additions & 1 deletion packages/main/src/TableHeaderCell.hbs
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
<slot></slot>
<slot name="action"></slot>
<slot></slot>
{{#if _sortIcon}}
<ui5-icon name="{{_sortIcon}}"></ui5-icon>
{{/if}}
41 changes: 39 additions & 2 deletions packages/main/src/TableHeaderCell.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import { customElement, property, slot } from "@ui5/webcomponents-base/dist/decorators.js";
import TableCellBase from "./TableCellBase.js";
import TableHeaderCellTemplate from "./generated/templates/TableHeaderCellTemplate.lit.js";
import TableHeaderCellStyles from "./generated/themes/TableHeaderCell.css.js";
import Icon from "./Icon.js";
import TableSortOrder from "./types/TableSortOrder.js";
import type TableHeaderCellActionBase from "./TableHeaderCellActionBase.js";
import "@ui5/webcomponents-icons/dist/sort-ascending.js";
import "@ui5/webcomponents-icons/dist/sort-descending.js";

/**
* @class
Expand All @@ -28,6 +32,7 @@ import TableHeaderCellStyles from "./generated/themes/TableHeaderCell.css.js";
tag: "ui5-table-header-cell",
styles: [TableCellBase.styles, TableHeaderCellStyles],
template: TableHeaderCellTemplate,
dependencies: [Icon],
})
class TableHeaderCell extends TableCellBase {
/**
Expand Down Expand Up @@ -85,6 +90,27 @@ class TableHeaderCell extends TableCellBase {
@property()
popinText?: string;

/**
* Defines the sort indicator of the column.
*
* @default "None"
* @since 2.8.0
* @public
*/
@property()
sortIndicator: `${TableSortOrder}` = "None";

/**
* Defines the action of the column.
*
* **Note:** While multiple actions are technically possible, this is not supported.
*
* @public
* @since 2.8.0
*/
@slot()
action!: Array<TableHeaderCellActionBase>;

@property({ type: Boolean, noAttribute: true })
_popin = false;

Expand All @@ -104,6 +130,17 @@ class TableHeaderCell extends TableCellBase {
// overwrite setting of TableCellBase so that the TableHeaderCell always uses the slot variable
this.style.justifyContent = `var(--horizontal-align-${this._individualSlot})`;
}
if (this.sortIndicator !== TableSortOrder.None) {
this.setAttribute("aria-sort", this.sortIndicator.toLowerCase());
} else if (this.hasAttribute("aria-sort")) {
this.removeAttribute("aria-sort");
}
}

get _sortIcon() {
if (this.sortIndicator !== TableSortOrder.None) {
return `sort-${this.sortIndicator.toLowerCase()}`;
}
}
}

Expand Down
39 changes: 39 additions & 0 deletions packages/main/src/TableHeaderCellActionAI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { customElement, i18n } from "@ui5/webcomponents-base/dist/decorators.js";
import TableHeaderCellActionBase from "./TableHeaderCellActionBase.js";
import { TABLE_GENERATED_BY_AI } from "./generated/i18n/i18n-defaults.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import "@ui5/webcomponents-icons/dist/ai.js";

/**
* @class
*
* ### Overview
*
* The `ui5-table-header-cell-action-ai` component defines a dedicated AI action for the table column.
*
* ### ES6 Module Import
*
* `import "@ui5/webcomponents/dist/TableHeaderCellActionAI.js";`
*
* @constructor
* @extends TableHeaderCellActionBase
* @since 2.8.0
* @public
*/
@customElement({ tag: "ui5-table-header-cell-action-ai" })

class TableHeaderCellActionAI extends TableHeaderCellActionBase {
@i18n("@ui5/webcomponents")
static i18nBundle: I18nBundle;

getRenderInfo() {
return {
icon: "ai",
tooltip: TableHeaderCellActionAI.i18nBundle.getText(TABLE_GENERATED_BY_AI),
};
}
}

TableHeaderCellActionAI.define();

export default TableHeaderCellActionAI;
6 changes: 6 additions & 0 deletions packages/main/src/TableHeaderCellActionBase.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<ui5-button
icon="{{_icon}}"
tooltip="{{_tooltip}}"
@click={{_onClick}}
design="Transparent">
</ui5-button>
79 changes: 79 additions & 0 deletions packages/main/src/TableHeaderCellActionBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import { customElement, eventStrict } from "@ui5/webcomponents-base/dist/decorators.js";
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
import TableHeaderCellActionBaseTemplate from "./generated/templates/TableHeaderCellActionBaseTemplate.lit.js";
import TableHeaderCellActionBaseStyles from "./generated/themes/TableHeaderCellActionBase.css.js";
import Button from "./Button.js";
import type TableCell from "./TableCell.js";

/**
* Fired when a header cell action is clicked.
*
* @param {HTMLElement} targetRef The reference to the element that triggered the event
* @public
* @since 2.8.0
*/
type TableHeaderCellActionClickEventDetail = {
targetRef: HTMLElement;
};

/**
* Fired when a header cell action is clicked.
*
* @param {HTMLElement} targetRef The reference to the element that triggered the event
* @public
* @since 2.8.0
*/
@eventStrict("click", {
bubbles: false,
})

/**
* @class
* The `TableHeaderCellActionBase` class serves as a foundation for table header cell actions.
* @constructor
* @abstract
* @extends UI5Element
* @since 2.8.0
* @public
*/
@customElement({
renderer: litRender,
styles: TableHeaderCellActionBaseStyles,
template: TableHeaderCellActionBaseTemplate,
dependencies: [Button],
})
abstract class TableHeaderCellActionBase extends UI5Element {
eventDetails!: {
"click": TableHeaderCellActionClickEventDetail,
}

abstract getRenderInfo(): {
aborjinik marked this conversation as resolved.
Show resolved Hide resolved
icon: string;
tooltip: string;
};

onBeforeRendering() {
this.toggleAttribute("_popin", !this.parentElement);
}

_onClick(e: MouseEvent) {
const action = this.parentElement ? this : ((this.getRootNode() as ShadowRoot).host as TableCell)._headerCell.action[0] as this;
action.fireDecoratorEvent("click", { targetRef: e.target as HTMLElement });
e.stopPropagation();
}

get _tooltip() {
return this.getRenderInfo().tooltip;
}

get _icon() {
return this.getRenderInfo().icon;
}
}

export default TableHeaderCellActionBase;

export type {
TableHeaderCellActionClickEventDetail,
};
4 changes: 1 addition & 3 deletions packages/main/src/TableHeaderRow.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import { customElement, slot, property } from "@ui5/webcomponents-base/dist/decorators.js";
import TableRowBase from "./TableRowBase.js";
import TableHeaderRowTemplate from "./generated/templates/TableHeaderRowTemplate.lit.js";
import TableHeaderRowStyles from "./generated/themes/TableHeaderRow.css.js";
Expand Down
3 changes: 1 addition & 2 deletions packages/main/src/TableRowActionBase.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
{{else if _isInteractive}}
<ui5-button
icon="{{_icon}}"
text="{{_text}}"
tooltip="{{_text}}"
@click={{_onActionClick}}
design="Transparent">
Expand All @@ -12,6 +11,6 @@
<ui5-icon
name="{{_icon}}"
tooltip="{{_text}}"
design="Transparent">
design="NonInteractive">
</ui5-icon>
{{/if}}
Loading