Skip to content

Commit

Permalink
feat(ui5-table): action header cell is added
Browse files Browse the repository at this point in the history
  • Loading branch information
aborjinik committed Feb 2, 2025
1 parent 77096c1 commit 3a8ade2
Show file tree
Hide file tree
Showing 20 changed files with 374 additions and 41 deletions.
96 changes: 95 additions & 1 deletion packages/main/cypress/specs/Table.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { html } from "lit";

import "../../src/Table.js";
import "../../src/TableHeaderRow.js";
import "../../src/TableCell.js";
import "../../src/TableHeaderCell.js";
import "../../src/TableHeaderCellActionAI.js";
import "../../src/TableRow.js";
import "../../src/TableCell.js";
import "../../src/TableSelection.js";
import type Table from "../../src/Table.js";

Expand Down Expand Up @@ -634,3 +636,95 @@ describe("Table - Navigated Rows", () => {
});
});
});

describe("Table - HeaderCell", () => {
beforeEach(() => {
cy.mount(html`
<ui5-table overflow-mode="Popin">
<ui5-table-header-row slot="headerRow">
<ui5-table-header-cell min-width="300px">Column A</ui5-table-header-cell>
<ui5-table-header-cell min-width="200px" sort-indicator="Ascending">
<ui5-label required wrapping-type="None">Column B</ui5-label>
<ui5-table-header-cell-action-ai slot="action""></ui5-table-header-cell-action-ai>
</ui5-table-header-cell>
<ui5-table-header-cell min-width="150px" popin-text="Popin Text">
<ui5-label required>Column C</ui5-label>
</ui5-table-header-cell>
</ui5-table-header-row>
<ui5-table-row>
<ui5-table-cell>Cell A</ui5-table-cell>
<ui5-table-cell>Cell B</ui5-table-cell>
<ui5-table-cell>Cell C</ui5-table-cell>
</ui5-table-row>
<ui5-table-row>
<ui5-table-cell>Cell A</ui5-table-cell>
<ui5-table-cell>Cell B</ui5-table-cell>
<ui5-table-cell>Cell C</ui5-table-cell>
</ui5-table-row>
</ui5-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", "tooltip", "Generated by AI");
cy.get("@row1popinBbutton").realClick();
cy.get("@actionBclick").should("have.been.calledTwice");

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").should("have.been.calledThrice");
});
});
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.7.0
* @public
*/
@property()
sortIndicator: `${TableSortOrder}` = "None";

/**
* Defines the action of the column.
*
* **Note:** Only one `action` is allowed.
*
* @public
* @since 2.7.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.7.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>
64 changes: 64 additions & 0 deletions packages/main/src/TableHeaderCellActionBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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";

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

/**
* Fired when an action is clicked.
*
* @public
* @since 2.7.0
*/
@eventStrict("click", {
bubbles: false,
})

abstract class TableHeaderCellActionBase extends UI5Element {
eventDetails!: {
"click": void
}

abstract getRenderInfo(): {
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");
e.stopPropagation();
}

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

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

export default TableHeaderCellActionBase;
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}}
5 changes: 0 additions & 5 deletions packages/main/src/TableRowActionBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ import { customElement, property } from "@ui5/webcomponents-base/dist/decorators
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
import TableRowActionBaseTemplate from "./generated/templates/TableRowActionBaseTemplate.lit.js";
import TableRowActionBaseStyles from "./generated/themes/TableRowActionBase.css.js";
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
import Icon from "./Icon.js";
import Button from "./Button.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 I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";

let MenuConstructor: new () => Menu;
let MenuItemConstructor: new () => MenuItem;
Expand Down Expand Up @@ -44,9 +42,6 @@ abstract class TableRowActionBase extends UI5Element {
@property({ type: Boolean })
invisible = false;

@i18n("@ui5/webcomponents")
static i18nBundle: I18nBundle;

private static _menu: Menu;
private static _menuItems = new WeakMap();
static async showMenu(actions: TableRowActionBase[], opener: HTMLElement) {
Expand Down
Loading

0 comments on commit 3a8ade2

Please sign in to comment.