From 4d181eb6f813608eb68a54a2a1fb94b5f3d14920 Mon Sep 17 00:00:00 2001 From: Petar Skelin Date: Thu, 19 Mar 2026 08:50:48 -0700 Subject: [PATCH 1/3] feat(ui5-table-row): add semantic click event on TableRow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TableRow now fires its own custom `click` event when an interactive row is activated via mouse click or Enter key. The native click is intercepted and suppressed, replaced by a CustomEvent that covers both mouse and keyboard activation — matching the established pattern used by Button, Link, Icon, and other components. This enables React consumers using `createReactComponent` to attach `onClick` handlers directly on ``. The existing Table-level `row-click` event is preserved for backward compatibility. --- packages/main/cypress/specs/Table.cy.tsx | 35 +++++++++ packages/main/src/TableRow.ts | 46 +++++++++++- packages/main/src/TableRowBase.ts | 4 ++ .../_components_pages/main/Table/Table.mdx | 8 +++ .../_components_pages/main/Table/TableRow.mdx | 8 +++ .../_samples/main/Table/RowClick/RowClick.md | 5 ++ .../docs/_samples/main/Table/RowClick/main.js | 17 +++++ .../_samples/main/Table/RowClick/sample.html | 38 ++++++++++ .../_samples/main/Table/RowClick/sample.tsx | 71 +++++++++++++++++++ 9 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 packages/website/docs/_samples/main/Table/RowClick/RowClick.md create mode 100644 packages/website/docs/_samples/main/Table/RowClick/main.js create mode 100644 packages/website/docs/_samples/main/Table/RowClick/sample.html create mode 100644 packages/website/docs/_samples/main/Table/RowClick/sample.tsx diff --git a/packages/main/cypress/specs/Table.cy.tsx b/packages/main/cypress/specs/Table.cy.tsx index f41b55637967..245c92d5a517 100644 --- a/packages/main/cypress/specs/Table.cy.tsx +++ b/packages/main/cypress/specs/Table.cy.tsx @@ -982,6 +982,41 @@ describe("Table - Interactive Rows", () => { cy.get("@buttonClickHandler").should("have.been.calledThrice"); cy.get("@rowClickHandler").should("have.been.calledThrice"); }); + + it("fires click event on the row element", () => { + cy.mount( + + + ColumnA + ColumnB + + + + + + + + + +
+ ); + + cy.get("#row1").invoke("on", "click", cy.stub().as("row1ClickSpy")); + cy.get("#row2").invoke("on", "click", cy.stub().as("row2ClickSpy")); + + // Non-interactive row should not fire click + cy.get("#row1").realClick(); + cy.get("@row1ClickSpy").should("not.have.been.called"); + + // Interactive row fires click on mouse click + cy.get("#row2").realClick(); + cy.get("@row2ClickSpy").should("have.been.calledOnce"); + cy.get("@row2ClickSpy").invoke("getCall", 0).its("args.0").should("have.property", "detail"); + + // Interactive row fires click on Enter key + cy.get("#row2").realPress("Enter"); + cy.get("@row2ClickSpy").should("have.been.calledTwice"); + }); }); describe("Table - HeaderCell", () => { diff --git a/packages/main/src/TableRow.ts b/packages/main/src/TableRow.ts index e60c521d5194..319dae1ce38d 100644 --- a/packages/main/src/TableRow.ts +++ b/packages/main/src/TableRow.ts @@ -1,4 +1,6 @@ -import { customElement, slotStrict as slot, property } from "@ui5/webcomponents-base/dist/decorators.js"; +import { + customElement, slotStrict as slot, property, eventStrict, +} from "@ui5/webcomponents-base/dist/decorators.js"; import { isEnter } from "@ui5/webcomponents-base/dist/Keys.js"; import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js"; import query from "@ui5/webcomponents-base/dist/decorators/query.js"; @@ -31,12 +33,33 @@ import { * @since 2.0.0 * @public */ +/** + * Fired when the row is activated by the user via click or Enter key. + * + * **Note:** This event is not fired when the row has `behavior="RowOnly"` selection. + * In that case, use the selection component's `change` event instead. + * + * @public + * @since 2.9.0 + */ +@eventStrict("click", { + bubbles: true, +}) @customElement({ tag: "ui5-table-row", styles: [TableRowBase.styles, TableRowCss], template: TableRowTemplate, }) class TableRow extends TableRowBase { + eventDetails!: TableRowBase["eventDetails"] & { + click: void + } + + constructor() { + super(); + this.addEventListener("click", this._interceptClick); + } + /** * Defines the cells of the component. * @@ -124,6 +147,14 @@ class TableRow extends TableRowBase { @query("#actions-cell") _actionsCell?: TableCell; + _interceptClick = (e: Event) => { + if (e instanceof CustomEvent) { + return; + } + e.stopImmediatePropagation(); + this._table?._onEvent(e); + }; + onBeforeRendering() { super.onBeforeRendering(); this.ariaRowIndex = (this.role === "row") ? `${this._rowIndex + 2}` : null; @@ -160,15 +191,24 @@ class TableRow extends TableRowBase { if (eventOrigin === this && this._isInteractive && isEnter(e)) { this._setActive("keyup"); - this._onclick(); + this._handleClick(); } } - _onclick() { + _onclick(e: Event) { + if (e instanceof CustomEvent) { + return; + } + + this._handleClick(); + } + + _handleClick() { if (this === getActiveElement()) { if (this._isSelectable && !this._hasSelector) { this._onSelectionChange(); } else if (this.interactive || this._isNavigable) { + this.fireDecoratorEvent("click"); this._table?._onRowClick(this); } } diff --git a/packages/main/src/TableRowBase.ts b/packages/main/src/TableRowBase.ts index 0798cdad6dfd..fedf563abc73 100644 --- a/packages/main/src/TableRowBase.ts +++ b/packages/main/src/TableRowBase.ts @@ -26,6 +26,10 @@ import { styles: TableRowBaseCss, }) abstract class TableRowBase extends UI5Element { + eventDetails!: { + click: void + } + cells!: Array; @property({ type: Number, noAttribute: true }) diff --git a/packages/website/docs/_components_pages/main/Table/Table.mdx b/packages/website/docs/_components_pages/main/Table/Table.mdx index dd2d159d020d..b2800099847d 100644 --- a/packages/website/docs/_components_pages/main/Table/Table.mdx +++ b/packages/website/docs/_components_pages/main/Table/Table.mdx @@ -9,6 +9,7 @@ import StickyHeader from "../../../_samples/main/Table/StickyHeader/StickyHeader import StickyHeaderContainer from "../../../_samples/main/Table/StickyHeaderContainer/StickyHeaderContainer.md"; import NoDataSlot from "../../../_samples/main/Table/NoDataSlot/NoDataSlot.md"; import Interactive from "../../../_samples/main/Table/Interactive/Interactive.md"; +import RowClick from "../../../_samples/main/Table/RowClick/RowClick.md"; import DragAndDrop from "../../../_samples/main/Table/DragAndDrop/DragAndDrop.md"; <%COMPONENT_OVERVIEW%> @@ -57,6 +58,13 @@ will fire the `row-click` event. +### Row Click Event + +The `click` event is fired directly on `ui5-table-row` when an interactive row is activated via mouse click or keyboard Enter. +This allows attaching click handlers directly on row elements, which is particularly useful for framework wrappers like React. + + + ### Drag and Drop Enable Drag and Drop by using the `move-over` and `move` event in combination with the `movable` property on the diff --git a/packages/website/docs/_components_pages/main/Table/TableRow.mdx b/packages/website/docs/_components_pages/main/Table/TableRow.mdx index 9b7821ceb777..c741295a695d 100644 --- a/packages/website/docs/_components_pages/main/Table/TableRow.mdx +++ b/packages/website/docs/_components_pages/main/Table/TableRow.mdx @@ -3,6 +3,7 @@ slug: ../../TableRow --- import Interactive from "../../../_samples/main/Table/Interactive/Interactive.md"; +import RowClick from "../../../_samples/main/Table/RowClick/RowClick.md"; import DragAndDrop from "../../../_samples/main/Table/DragAndDrop/DragAndDrop.md"; <%COMPONENT_OVERVIEW%> @@ -16,6 +17,13 @@ will fire the `row-click` event. +## Row Click Event + +The `click` event is fired directly on `ui5-table-row` when an interactive row is activated via mouse click or keyboard Enter. +This allows attaching click handlers directly on row elements, which is particularly useful for framework wrappers like React. + + + ## Movable Rows Adding the `movable` property enables the `ui5-table-row` for drag and drop operations. diff --git a/packages/website/docs/_samples/main/Table/RowClick/RowClick.md b/packages/website/docs/_samples/main/Table/RowClick/RowClick.md new file mode 100644 index 000000000000..0c062a836e84 --- /dev/null +++ b/packages/website/docs/_samples/main/Table/RowClick/RowClick.md @@ -0,0 +1,5 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; +import react from '!!raw-loader!./sample.tsx'; + + diff --git a/packages/website/docs/_samples/main/Table/RowClick/main.js b/packages/website/docs/_samples/main/Table/RowClick/main.js new file mode 100644 index 000000000000..d1e22690a2df --- /dev/null +++ b/packages/website/docs/_samples/main/Table/RowClick/main.js @@ -0,0 +1,17 @@ +import "@ui5/webcomponents/dist/Table.js"; +import "@ui5/webcomponents/dist/TableHeaderRow.js"; +import "@ui5/webcomponents/dist/TableHeaderCell.js"; +import "@ui5/webcomponents/dist/Label.js"; +import "@ui5/webcomponents/dist/Toast.js"; + +const toast = document.getElementById("message"); + +document.getElementById("row-a").addEventListener("click", () => { + toast.textContent = "Row A clicked!"; + toast.open = true; +}); + +document.getElementById("row-b").addEventListener("click", () => { + toast.textContent = "Row B clicked!"; + toast.open = true; +}); diff --git a/packages/website/docs/_samples/main/Table/RowClick/sample.html b/packages/website/docs/_samples/main/Table/RowClick/sample.html new file mode 100644 index 000000000000..f13f850b8a25 --- /dev/null +++ b/packages/website/docs/_samples/main/Table/RowClick/sample.html @@ -0,0 +1,38 @@ + + + + + + + + Sample + + + + + + + + + Product + Supplier + Price + + + + Notebook Basic 15 + Very Best Screens + 956 EUR + + + Notebook Basic 17 + Smartcards + 1249 EUR + + + + + + + + diff --git a/packages/website/docs/_samples/main/Table/RowClick/sample.tsx b/packages/website/docs/_samples/main/Table/RowClick/sample.tsx new file mode 100644 index 000000000000..fb44a8716ebc --- /dev/null +++ b/packages/website/docs/_samples/main/Table/RowClick/sample.tsx @@ -0,0 +1,71 @@ +import { useRef } from "react"; +import createReactComponent from "@ui5/webcomponents-base/dist/createReactComponent.js"; +import LabelClass from "@ui5/webcomponents/dist/Label.js"; +import TableClass from "@ui5/webcomponents/dist/Table.js"; +import TableCellClass from "@ui5/webcomponents/dist/TableCell.js"; +import TableHeaderCellClass from "@ui5/webcomponents/dist/TableHeaderCell.js"; +import TableHeaderRowClass from "@ui5/webcomponents/dist/TableHeaderRow.js"; +import TableRowClass from "@ui5/webcomponents/dist/TableRow.js"; +import ToastClass from "@ui5/webcomponents/dist/Toast.js"; + +const Label = createReactComponent(LabelClass); +const Table = createReactComponent(TableClass); +const TableCell = createReactComponent(TableCellClass); +const TableHeaderCell = createReactComponent(TableHeaderCellClass); +const TableHeaderRow = createReactComponent(TableHeaderRowClass); +const TableRow = createReactComponent(TableRowClass); +const Toast = createReactComponent(ToastClass); + +function App() { + const toastRef = useRef(null); + + const showToast = (message: string) => { + if (toastRef.current) { + toastRef.current!.textContent = message; + toastRef.current!.open = true; + } + }; + + return ( + <> + + + {/* playground-fold */} + + Product + Supplier + Price + + {/* playground-fold-end */} + showToast("Row A clicked!")}> + + + + + + + + + + + showToast("Row B clicked!")}> + + + + + + + + + + +
+ + ); +} + +export default App; From eee72daef19779ebc7e6ff6ade80c09975727850 Mon Sep 17 00:00:00 2001 From: Petar Skelin Date: Thu, 19 Mar 2026 09:20:22 -0700 Subject: [PATCH 2/3] fix(ui5-table-row): correct JSDoc order for click event decorator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move @customElement decorator right after the @class JSDoc block, and place the event JSDoc + @eventStrict after it — matching the established ordering used by Button and other components. --- packages/main/src/TableRow.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/main/src/TableRow.ts b/packages/main/src/TableRow.ts index 319dae1ce38d..77dfa7a670ac 100644 --- a/packages/main/src/TableRow.ts +++ b/packages/main/src/TableRow.ts @@ -33,6 +33,11 @@ import { * @since 2.0.0 * @public */ +@customElement({ + tag: "ui5-table-row", + styles: [TableRowBase.styles, TableRowCss], + template: TableRowTemplate, +}) /** * Fired when the row is activated by the user via click or Enter key. * @@ -45,11 +50,6 @@ import { @eventStrict("click", { bubbles: true, }) -@customElement({ - tag: "ui5-table-row", - styles: [TableRowBase.styles, TableRowCss], - template: TableRowTemplate, -}) class TableRow extends TableRowBase { eventDetails!: TableRowBase["eventDetails"] & { click: void From 5d4c58a8d6b750e752d519ffb6995e3b3dde606d Mon Sep 17 00:00:00 2001 From: Petar Skelin Date: Mon, 23 Mar 2026 16:19:44 +0200 Subject: [PATCH 3/3] refactor(ui5-table-row): only fire custom click on Enter key Remove the native click interceptor so mouse clicks bubble normally with their original target preserved. The custom click event now only fires on Enter key activation, filling the keyboard gap without altering existing mouse click behavior. --- packages/main/cypress/specs/Table.cy.tsx | 23 ++++++++++++++++------- packages/main/src/TableRow.ts | 24 ++++++------------------ 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/packages/main/cypress/specs/Table.cy.tsx b/packages/main/cypress/specs/Table.cy.tsx index 245c92d5a517..a59c4c8b6c35 100644 --- a/packages/main/cypress/specs/Table.cy.tsx +++ b/packages/main/cypress/specs/Table.cy.tsx @@ -1004,18 +1004,27 @@ describe("Table - Interactive Rows", () => { cy.get("#row1").invoke("on", "click", cy.stub().as("row1ClickSpy")); cy.get("#row2").invoke("on", "click", cy.stub().as("row2ClickSpy")); - // Non-interactive row should not fire click + // Non-interactive row should not fire custom click cy.get("#row1").realClick(); - cy.get("@row1ClickSpy").should("not.have.been.called"); + cy.get("@row1ClickSpy").then(stub => { + const customClicks = (stub as unknown as Cypress.Agent).getCalls().filter(call => call.args[0].originalEvent instanceof CustomEvent); + expect(customClicks).to.have.length(0); + }); - // Interactive row fires click on mouse click + // Interactive row does not fire custom click on mouse click (native click bubbles normally) cy.get("#row2").realClick(); - cy.get("@row2ClickSpy").should("have.been.calledOnce"); - cy.get("@row2ClickSpy").invoke("getCall", 0).its("args.0").should("have.property", "detail"); + cy.get("@row2ClickSpy").then(stub => { + const customClicks = (stub as unknown as Cypress.Agent).getCalls().filter(call => call.args[0].originalEvent instanceof CustomEvent); + expect(customClicks).to.have.length(0); + }); - // Interactive row fires click on Enter key + // Interactive row fires custom click on Enter key cy.get("#row2").realPress("Enter"); - cy.get("@row2ClickSpy").should("have.been.calledTwice"); + cy.get("@row2ClickSpy").then(stub => { + const customClicks = (stub as unknown as Cypress.Agent).getCalls().filter(call => call.args[0].originalEvent instanceof CustomEvent); + expect(customClicks).to.have.length(1); + expect(customClicks[0].args[0].originalEvent).to.have.property("detail"); + }); }); }); diff --git a/packages/main/src/TableRow.ts b/packages/main/src/TableRow.ts index 77dfa7a670ac..04eb8499bc4b 100644 --- a/packages/main/src/TableRow.ts +++ b/packages/main/src/TableRow.ts @@ -55,11 +55,6 @@ class TableRow extends TableRowBase { click: void } - constructor() { - super(); - this.addEventListener("click", this._interceptClick); - } - /** * Defines the cells of the component. * @@ -147,14 +142,6 @@ class TableRow extends TableRowBase { @query("#actions-cell") _actionsCell?: TableCell; - _interceptClick = (e: Event) => { - if (e instanceof CustomEvent) { - return; - } - e.stopImmediatePropagation(); - this._table?._onEvent(e); - }; - onBeforeRendering() { super.onBeforeRendering(); this.ariaRowIndex = (this.role === "row") ? `${this._rowIndex + 2}` : null; @@ -191,7 +178,7 @@ class TableRow extends TableRowBase { if (eventOrigin === this && this._isInteractive && isEnter(e)) { this._setActive("keyup"); - this._handleClick(); + this._handleClick(true); } } @@ -199,16 +186,17 @@ class TableRow extends TableRowBase { if (e instanceof CustomEvent) { return; } - - this._handleClick(); + this._handleClick(false); } - _handleClick() { + _handleClick(fireClick = false) { if (this === getActiveElement()) { if (this._isSelectable && !this._hasSelector) { this._onSelectionChange(); } else if (this.interactive || this._isNavigable) { - this.fireDecoratorEvent("click"); + if (fireClick) { + this.fireDecoratorEvent("click"); + } this._table?._onRowClick(this); } }