Skip to content
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
116 changes: 113 additions & 3 deletions packages/main/cypress/specs/Table.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import Title from "../../src/Title.js";
import Slider from "../../src/Slider.js";
import Button from "../../src/Button.js";

// Porting Table.spec.js (wdio tests) to cypress tests
const ROLE_COLUMN_HEADER = "columnheader";
const TRANSPARENT = "rgba(0, 0, 0, 0)";

describe("Table - Rendering", () => {
function checkWidth(id: string, expectedWidth: number) {
Expand Down Expand Up @@ -302,7 +301,7 @@ describe("Table - Popin Mode", () => {
const roleCondition = shouldBePoppedIn || shouldBeHidden ? "not.have.attr" : "have.attr";

cy.wrap($cell)
.should(roleCondition, "role", ROLE_COLUMN_HEADER);
.should(roleCondition, "role", "columnheader");
cy.get("ui5-table-header-row")
.shadow()
.find(`slot[name=default-${index + 1}]`)
Expand Down Expand Up @@ -1083,3 +1082,114 @@ describe("Table - HeaderCell", () => {
cy.get("@actionBclickTarget").should("have.attr", "tooltip", "Generated by AI");
});
});

describe("Table - Cell Merging", () => {
function mountMergedTable(overflowMode: "Scroll" | "Popin" = "Scroll") {
cy.mount(
<Table id="table" overflowMode={overflowMode}>
<TableSelectionMulti id="selection" slot="features"></TableSelectionMulti>
<TableHeaderRow slot="headerRow">
<TableHeaderCell id="colA" minWidth="200px">Column A</TableHeaderCell>
<TableHeaderCell id="colB" minWidth="200px">Column B</TableHeaderCell>
<TableHeaderCell id="colC" minWidth="150px">Column C</TableHeaderCell>
</TableHeaderRow>
<TableRow id="row1">
<TableCell id="r1cA"><Label>SAP</Label></TableCell>
Comment thread
aborjinik marked this conversation as resolved.
<TableCell id="r1cB"><Label>100</Label></TableCell>
<TableCell id="r1cC"><Label>X</Label></TableCell>
</TableRow>
<TableRow id="row2">
<TableCell id="r2cA" merged><Label>SAP</Label></TableCell>
<TableCell id="r2cB"><Label>200</Label></TableCell>
<TableCell id="r2cC" merged><Label>X</Label></TableCell>
</TableRow>
<TableRow id="row3">
<TableCell id="r3cA" merged><Label>SAP</Label></TableCell>
<TableCell id="r3cB"><Label>300</Label></TableCell>
<TableCell id="r3cC"><Label>Y</Label></TableCell>
</TableRow>
</Table>
);
}

it("should have transparent border on merged cells and selection cell", () => {
mountMergedTable();

// Merged cell should have transparent top border
cy.get("#r2cA").should("have.css", "border-top-color", TRANSPARENT);
cy.get("#r2cA").find("ui5-label").should("have.css", "opacity", "0");

// Non-merged cell should not have transparent border
cy.get("#r2cB").should("not.have.css", "border-top-color", TRANSPARENT);
cy.get("#r2cB").find("ui5-label").should("have.css", "opacity", "1");

// Selection cell should have transparent border when first cell is merged
cy.get("#row2").shadow().find("#selection-cell").should("have.attr", "data-border-merged");
cy.get("#row2").shadow().find("#selection-cell").should("have.css", "border-top-color", TRANSPARENT);

// Selection cell should NOT have transparent border when first cell is not merged
cy.get("#row1").shadow().find("#selection-cell").should("not.have.attr", "data-border-merged");
cy.get("#row1").shadow().find("#selection-cell").should("not.have.css", "border-top-color", TRANSPARENT);
});

it("should disable merged styles when row has popin", () => {
mountMergedTable("Popin");

// At full width, merged styles should be active
cy.get("ui5-table").invoke("css", "width", "600px");
cy.get("#r2cA").find("ui5-label").should("have.css", "opacity", "0");
cy.get("#r2cA").should("have.css", "border-top-color", TRANSPARENT);
cy.get("#row2").shadow().find("#selection-cell").should("have.css", "border-top-color", TRANSPARENT);

// Shrink table to trigger popin
cy.get("ui5-table").invoke("css", "width", "250px");
cy.wait(50);

// Merged cell border should fall back to normal border color (not transparent)
cy.get("#row2").should("have.attr", "_haspopin");
cy.get("#r2cA").should("not.have.css", "border-top-color", TRANSPARENT);
cy.get("#row2").shadow().find("#selection-cell").should("not.have.css", "border-top-color", TRANSPARENT);

// Merged cell content should be fully visible (opacity back to 1)
cy.get("#r2cA").find("ui5-label").should("have.css", "opacity", "1");

// Expand table again, merged styles should re-activate
cy.get("ui5-table").invoke("css", "width", "600px");
cy.wait(50);

cy.get("#r2cA").find("ui5-label").should("have.css", "opacity", "0");
cy.get("#r2cA").should("have.css", "border-top-color", TRANSPARENT);
cy.get("#row2").shadow().find("#selection-cell").should("have.css", "border-top-color", TRANSPARENT);
});

it("should toggle merged styles at runtime", () => {
mountMergedTable();

// Initially merged
cy.get("#r3cA").find("ui5-label").should("have.css", "opacity", "0");

// Remove merged attribute
cy.get("#r3cA").invoke("removeAttr", "merged");
cy.get("#r3cA").find("ui5-label").should("have.css", "opacity", "1");
cy.get("#r3cA").should("not.have.css", "border-top-color", TRANSPARENT);

// Re-add merged attribute
cy.get("#r3cA").invoke("prop", "merged", true);
cy.get("#r3cA").find("ui5-label").should("have.css", "opacity", "0");
cy.get("#r3cA").should("have.css", "border-top-color", TRANSPARENT);
});

it("should disable merged styles on focus", () => {
mountMergedTable();

// Before hover: merged styles active
cy.get("#r2cA").find("ui5-label").should("have.css", "opacity", "0");
cy.get("#r2cA").should("have.css", "border-top-color", TRANSPARENT);

// On focus: merged cell content should become visible but border should remain transparent
cy.get("#row2").realClick();
cy.get("#r2cA").find("ui5-label").should("not.have.css", "opacity", "0");
cy.get("#r2cA").should("have.css", "border-top-color", TRANSPARENT);
cy.get("#row2").shadow().find("#selection-cell").should("have.css", "border-top-color", TRANSPARENT);
});
});
15 changes: 15 additions & 0 deletions packages/main/src/TableCell.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import query from "@ui5/webcomponents-base/dist/decorators/query.js";
import TableCellTemplate from "./TableCellTemplate.js";
import TableCellStyles from "./generated/themes/TableCell.css.js";
Expand Down Expand Up @@ -30,6 +31,20 @@ import { LABEL_COLON } from "./generated/i18n/i18n-defaults.js";
template: TableCellTemplate,
})
class TableCell extends TableCellBase {
/**
* Defines whether the cell is visually merged with the cell directly above it.
*
* This is useful when consecutive cells in a column have the same value and should visually appear as a single merged cell.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* This is useful when consecutive cells in a column have the same value and should visually appear as a single merged cell.
* This is useful if consecutive cells in a column have the same value and should visually appear as a single merged cell.

* Although the cell is visually merged with the previous one, its content must still be provided for accessibility purposes.
* **Note:** This feature is disabled when cells are rendered as popin, and should remain `false` for interactive cell content.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should or must?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the new design we show the merged content on focus and hover so I think "should" is fine.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* **Note:** This feature is disabled when cells are rendered as popin, and should remain `false` for interactive cell content.
* **Note:** This feature is disabled when cells are rendered as a popin, and should remain `false` for interactive cell content.

*
* @default false
* @since 2.21.0
* @public
*/
@property({ type: Boolean })
merged = false;

@query("#popin-header")
_popinHeader?: HTMLElement;

Expand Down
1 change: 0 additions & 1 deletion packages/main/src/TableHeaderRowTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export default function TableHeaderRowTemplate(this: TableHeaderRow, ariaColInde
aria-description={this._selectionCellAriaDescription}
aria-colindex={ariaColIndex++}
data-ui5-table-selection-cell
data-ui5-table-cell-fixed
data-ui5-acc-text=""
>
{ !this._isMultiSelect ?
Expand Down
7 changes: 6 additions & 1 deletion packages/main/src/TableRow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class TableRow extends TableRowBase<TableCell> {
"default": true,
individualSlots: true,
invalidateOnChildChange: {
properties: ["_popin", "_popinHidden"],
properties: ["merged", "_popin", "_popinHidden"],
slots: false,
},
})
Expand Down Expand Up @@ -130,6 +130,7 @@ class TableRow extends TableRowBase<TableCell> {
toggleAttribute(this, "draggable", this.movable, "true");
toggleAttribute(this, "_interactive", this._isInteractive);
toggleAttribute(this, "_alternate", this._alternate);
toggleAttribute(this, "_haspopin", this._hasPopin);
}

async focus(focusOptions?: FocusOptions | undefined): Promise<void> {
Expand Down Expand Up @@ -197,6 +198,10 @@ class TableRow extends TableRowBase<TableCell> {
}) !== undefined;
}

get _hasPopin() {
return this.cells.some(c => c._popin && !c._popinHidden);
}

get _rowIndex() {
if (this.position !== undefined) {
return this.position;
Expand Down
4 changes: 4 additions & 0 deletions packages/main/src/TableRowBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ abstract class TableRowBase<TCell extends TableCellBase = TableCellBase> extends
return this.cells.filter(c => !c._popin);
}

get _firstVisibleCell() {
return this.cells.find(c => !c._popin);
}

get _popinCells() {
return this.cells.filter(c => c._popin && !c._popinHidden);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/main/src/TableRowTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export default function TableRowTemplate(this: TableRow, ariaColIndex: number =
<TableCell id="selection-cell"
aria-selected={this._isSelected}
aria-colindex={ariaColIndex++}
data-border-merged={this._firstVisibleCell?.merged ? "" : null}
data-ui5-table-selection-cell
data-ui5-table-cell-fixed
data-ui5-acc-text=""
>
{ this._isMultiSelect ?
Expand Down
17 changes: 17 additions & 0 deletions packages/main/src/themes/TableCell.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
:host {
border-top: var(--sapList_BorderWidth) solid var(--sapList_BorderColor);
}

:host([merged]),
:host([data-border-merged]) {
--_ui5_table_cell_merged_border_color: var(--_ui5_table_cell_border_merged) transparent;
border-top-color: var(--_ui5_table_cell_merged_border_color, var(--sapList_BorderColor));
}

:host([merged]) ::slotted(*) {
--_ui5_table_cell_merged_content_opacity: var(--_ui5_table_cell_content_merged) 0;
opacity: var(--_ui5_table_cell_merged_content_opacity, 1);
transition: opacity 300ms ease;
}

:host([_popin]) {
padding-inline-start: 0;
padding-inline-end: 0;
align-items: center;
border-top: none;
}

:host([_popin]) #popin-header {
Expand Down
11 changes: 0 additions & 11 deletions packages/main/src/themes/TableCellBase.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,4 @@
:host([tabindex]:focus) {
outline: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--sapContent_FocusColor);
outline-offset: calc(-1 * var(--sapContent_FocusWidth));
}

:host(#selection-cell) {
width: auto;
min-width: auto;
background-color: inherit;
}

:host([data-ui5-table-cell-fixed]) {
position: sticky;
z-index: 1;
}
2 changes: 1 addition & 1 deletion packages/main/src/themes/TableHeaderRow.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
:host {
background: var(--sapList_HeaderBackground);
border-top: var(--sapList_BorderWidth) solid var(--sapList_BorderColor);
border-top: var(--sapList_BorderWidth) solid var(--sapList_HeaderBorderColor);
border-bottom: var(--sapList_BorderWidth) solid var(--sapList_HeaderBorderColor);
grid-template-rows: auto 0px;
}
Expand Down
60 changes: 39 additions & 21 deletions packages/main/src/themes/TableRow.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,30 @@
background: var(--_ui5_table_row_alternating_background);
}

:host([aria-selected=true]) {
:host(:first-of-type) > [ui5-table-cell],
:host(:first-of-type) > ::slotted([ui5-table-cell]) {
border-top: none;
}

:host(:last-of-type) {
border-bottom: var(--sapList_BorderWidth) solid var(--sapList_TableFooterBorder);
}

:host([aria-selected="true"]) {
background: var(--sapList_SelectionBackgroundColor);
border-bottom: var(--sapList_BorderWidth) solid var(--sapList_SelectionBorderColor);
box-shadow: inset 0 calc(-1 * var(--sapList_BorderWidth)) 0 0 var(--sapList_SelectionBorderColor);
}

:host(:not([_haspopin])) {
/* Use CSS Space Toggles until if() or container style queries are widely supported */
--_ui5_table_cell_border_merged: ;
--_ui5_table_cell_content_merged: ;
}

:host(:not([_haspopin]):active),
:host(:not([_haspopin]):focus-within) {
/* Provide a valid CSS value to intentionally invalidate the TableCell variable-based rules and disable visual merging. */
--_ui5_table_cell_content_merged: initial;
}

@media (hover: hover) {
Expand All @@ -18,10 +39,14 @@
:host([_interactive][aria-selected=true]:hover) {
background: var(--sapList_Hover_SelectionBackground);
}
:host(:not([_haspopin]):hover) {
/* Provide a valid CSS value to intentionally invalidate the TableCell variable-based rules and disable visual merging. */
--_ui5_table_cell_content_merged: initial;
}
}

:host([_interactive][_active]),
:host([_interactive][aria-selected=true][_active]) {
:host([_interactive][aria-selected="true"][_active]) {
background: var(--sapList_Active_Background);
}

Expand All @@ -34,35 +59,34 @@
}

#popin-cell {
padding-inline-start: var(--_ui5_first_table_cell_horizontal_padding);
align-content: initial;
flex-direction: column;
grid-column: 1 / -1;
border-top: none;
}

#navigated-cell {
position: sticky;
right: 0;
position: sticky;
inset-inline-end: 0;
z-index: 1;
background-color: inherit;
overflow: visible;
grid-row: span 2;
min-width: 0;
padding: 0;
background: inherit;
}

:dir(rtl)#navigated-cell {
left: 0;
}

:host([navigated]) #navigated {
position: absolute;
inset: 0;
inset: -1px 0px 0px 1px;
background: var(--sapList_SelectionBorderColor);
}

:host([tabindex]:focus) #navigated {
transform: translateX(calc(var(--_ui5_table_navigated_cell_width) * -1));
bottom: 2px;
top: 3px;
bottom: 3px;
top: 2px;
}

:host([tabindex]:focus) #navigated:dir(rtl) {
Expand All @@ -83,19 +107,13 @@

#selection-cell ~ #popin-cell {
grid-column-start: 2;
padding-inline-start: var(--_ui5_table_cell_horizontal_padding);
}

#actions-cell {
display: flex;
align-items: center;
gap: var(--_ui5_table_row_actions_gap);
}

#actions-cell:has(+ #navigated-cell) {
right: var(--_ui5_table_navigated_cell_width);
overflow: auto;
}

:dir(rtl)#actions-cell:has(+ #navigated-cell) {
left: var(--_ui5_table_navigated_cell_width);
inset-inline-end: var(--_ui5_table_navigated_cell_width);
}
Loading
Loading