From 7f23cf5346a3fdff75f540928b213c6988b2ac6d Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Fri, 21 Aug 2026 11:21:59 +0300 Subject: [PATCH 1/6] feat(ui5-list): add InactiveSelectable list item type Introduces a new `ListItemType.InactiveSelectable` enum value. Items of this type behave like `Inactive` (no active press feedback, no `item-click` event) but still allow selection (checkbox/radio) to be toggled when the list has a selection mode set. --- packages/main/src/ListItem.ts | 17 +++++++++++++++-- packages/main/src/types/ListItemType.ts | 9 +++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/main/src/ListItem.ts b/packages/main/src/ListItem.ts index 668619999151e..15195b9173aac 100644 --- a/packages/main/src/ListItem.ts +++ b/packages/main/src/ListItem.ts @@ -107,10 +107,13 @@ abstract class ListItem extends ListItemBase { } /** * Defines the visual indication and behavior of the list items. - * Available options are `Active` (by default), `Inactive`, `Detail` and `Navigation`. + * Available options are `Active` (by default), `Inactive`, `InactiveSelectable`, `Detail` and `Navigation`. * * **Note:** When set to `Active` or `Navigation`, the item will provide visual response upon press and hover, - * while with type `Inactive` and `Detail` - will not. + * while with type `Inactive`, `InactiveSelectable` and `Detail` - will not. + * + * **Note:** `InactiveSelectable` behaves like `Inactive` but allows selection (checkbox/radio) to be toggled + * when the list has a selection mode. The `item-click` event is not fired for this type. * @default "Active" * @public */ @@ -401,6 +404,12 @@ abstract class ListItem extends ListItemBase { if (this.isInactive) { return; } + if (this.isInactiveSelectable) { + if (this._selectionMode !== ListSelectionMode.None && this._selectionMode !== ListSelectionMode.Delete) { + this.fireDecoratorEvent("selection-requested", { item: this, selected: !this.selected, selectionComponentPressed: false }); + } + return; + } super.fireItemPress(e); if (document.activeElement !== this) { this.focus(); @@ -411,6 +420,10 @@ abstract class ListItem extends ListItemBase { return this.type === ListItemType.Inactive || this.type === ListItemType.Detail; } + get isInactiveSelectable() { + return this.type === ListItemType.InactiveSelectable; + } + get placeSelectionElementBefore() { return this._selectionMode === ListSelectionMode.Multiple || this._selectionMode === ListSelectionMode.SingleStart; diff --git a/packages/main/src/types/ListItemType.ts b/packages/main/src/types/ListItemType.ts index 1baae23ff25b3..bc891ade93b6e 100644 --- a/packages/main/src/types/ListItemType.ts +++ b/packages/main/src/types/ListItemType.ts @@ -9,6 +9,15 @@ enum ListItemType { */ Inactive = "Inactive", + /** + * Indicates the list item does not have any active feedback when item is pressed, + * but selection (checkbox/radio) is still possible when a selection mode is active. + * The `item-click` event is not fired for items of this type. + * @public + * @since 2.x.0 + */ + InactiveSelectable = "InactiveSelectable", + /** * Indicates that the item is clickable via active feedback when item is pressed. * @public From 650ca5165e1c5ec54ae951440d5593636ec31b3f Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Fri, 21 Aug 2026 11:41:55 +0300 Subject: [PATCH 2/6] chore(ui5-list): fix @since tag for InactiveSelectable to 2.26.0 --- packages/main/src/types/ListItemType.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/main/src/types/ListItemType.ts b/packages/main/src/types/ListItemType.ts index bc891ade93b6e..a63fa608bf345 100644 --- a/packages/main/src/types/ListItemType.ts +++ b/packages/main/src/types/ListItemType.ts @@ -14,7 +14,7 @@ enum ListItemType { * but selection (checkbox/radio) is still possible when a selection mode is active. * The `item-click` event is not fired for items of this type. * @public - * @since 2.x.0 + * @since 2.26.0 */ InactiveSelectable = "InactiveSelectable", From 964dcff4e6c470e484b429759e179105926e9121 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Fri, 21 Aug 2026 11:42:17 +0300 Subject: [PATCH 3/6] test(ui5-list): add Cypress tests for InactiveSelectable type --- packages/main/cypress/specs/List.cy.tsx | 227 ++++++++++++++++++++++++ 1 file changed, 227 insertions(+) diff --git a/packages/main/cypress/specs/List.cy.tsx b/packages/main/cypress/specs/List.cy.tsx index 42a3372fbc8d6..e51b8780954cf 100644 --- a/packages/main/cypress/specs/List.cy.tsx +++ b/packages/main/cypress/specs/List.cy.tsx @@ -3874,4 +3874,231 @@ describe("List - ListItem accessible role inheritance", () => { .find("li") .should("have.attr", "role", "listitem"); }); +}); + +describe("List - InactiveSelectable type", () => { + it("type='InactiveSelectable' + selectionMode='Multiple' — clicking item toggles checkbox selection", () => { + cy.mount( + + Option A + Option B + + ); + + // Initially not selected + cy.get("#item1").should("not.have.attr", "selected"); + + // Click item — should become selected (checkbox toggled) + cy.get("#item1").click(); + cy.get("#item1").should("have.attr", "selected"); + + // Click again — should become deselected + cy.get("#item1").click(); + cy.get("#item1").should("not.have.attr", "selected"); + + // Second item is independent + cy.get("#item2").click(); + cy.get("#item2").should("have.attr", "selected"); + cy.get("#item1").should("not.have.attr", "selected"); + }); + + it("type='InactiveSelectable' + selectionMode='Multiple' — pressing Space toggles selection", () => { + cy.mount( + + Option A + + ); + + cy.get("#item1").should("not.have.attr", "selected"); + + // Focus item and press Space + cy.get("#item1").shadow().find("li").focus(); + cy.realPress("Space"); + cy.get("#item1").should("have.attr", "selected"); + + // Press Space again — should deselect + cy.realPress("Space"); + cy.get("#item1").should("not.have.attr", "selected"); + }); + + it("type='InactiveSelectable' — item-click event is NOT fired on click", () => { + cy.mount( + + Option A + + ); + + cy.get("[ui5-list]").then(($list) => { + $list[0].addEventListener("ui5-item-click", cy.stub().as("itemClickStub")); + }); + + cy.get("#item1").click(); + + cy.get("@itemClickStub").should("not.have.been.called"); + }); + + it("type='InactiveSelectable' — item-click event is NOT fired on Space key", () => { + cy.mount( + + Option A + + ); + + cy.get("[ui5-list]").then(($list) => { + $list[0].addEventListener("ui5-item-click", cy.stub().as("itemClickStub")); + }); + + cy.get("#item1").shadow().find("li").focus(); + cy.realPress("Space"); + + cy.get("@itemClickStub").should("not.have.been.called"); + }); + + it("type='Inactive' — clicking item does NOT toggle selection (regression guard)", () => { + cy.mount( + + Option A + + ); + + cy.get("#item1").should("not.have.attr", "selected"); + + cy.get("#item1").click(); + + cy.get("#item1").should("not.have.attr", "selected"); + }); + + it("type='Inactive' + selectionMode='Single' — clicking item does NOT select it (regression guard)", () => { + cy.mount( + + Option A + + ); + + cy.get("#item1").should("not.have.attr", "selected"); + + cy.get("#item1").click(); + + cy.get("#item1").should("not.have.attr", "selected"); + }); + + it("type='InactiveSelectable' + selectionMode='Single' — clicking item selects it", () => { + cy.mount( + + Option A + Option B + + ); + + cy.get("#item1").should("not.have.attr", "selected"); + + cy.get("#item1").click(); + cy.get("#item1").should("have.attr", "selected"); + + // Clicking second item moves selection + cy.get("#item2").click(); + cy.get("#item2").should("have.attr", "selected"); + cy.get("#item1").should("not.have.attr", "selected"); + }); + + it("type='InactiveSelectable' + selectionMode='Single' — pressing Space selects item", () => { + cy.mount( + + Option A + + ); + + cy.get("#item1").should("not.have.attr", "selected"); + + cy.get("#item1").shadow().find("li").focus(); + cy.realPress("Space"); + + cy.get("#item1").should("have.attr", "selected"); + }); + + it("type='InactiveSelectable' + selectionMode='None' — clicking item does nothing", () => { + cy.mount( + + Option A + + ); + + cy.get("[ui5-list]").then(($list) => { + $list[0].addEventListener("ui5-item-click", cy.stub().as("itemClickStub")); + $list[0].addEventListener("ui5-selection-change", cy.stub().as("selectionChangeStub")); + }); + + cy.get("#item1").click(); + + cy.get("#item1").should("not.have.attr", "selected"); + cy.get("@itemClickStub").should("not.have.been.called"); + cy.get("@selectionChangeStub").should("not.have.been.called"); + }); + + it("type='InactiveSelectable' + selectionMode='Delete' — clicking item does nothing", () => { + cy.mount( + + Option A + + ); + + cy.get("[ui5-list]").then(($list) => { + $list[0].addEventListener("ui5-item-click", cy.stub().as("itemClickStub")); + $list[0].addEventListener("ui5-selection-change", cy.stub().as("selectionChangeStub")); + }); + + cy.get("#item1").click(); + + cy.get("#item1").should("not.have.attr", "selected"); + cy.get("@itemClickStub").should("not.have.been.called"); + cy.get("@selectionChangeStub").should("not.have.been.called"); + }); + + it("type='InactiveSelectable' + selectionMode='Multiple' — selection-change event is fired", () => { + cy.mount( + + Option A + + ); + + cy.get("[ui5-list]").then(($list) => { + $list[0].addEventListener("ui5-selection-change", cy.stub().as("selectionChangeStub")); + }); + + cy.get("#item1").click(); + + cy.get("@selectionChangeStub").should("have.been.calledOnce"); + }); + + it("type='InactiveSelectable' + selectionMode='SingleStart' — clicking item selects it", () => { + cy.mount( + + Option A + Option B + + ); + + cy.get("#item1").click(); + cy.get("#item1").should("have.attr", "selected"); + + cy.get("#item2").click(); + cy.get("#item2").should("have.attr", "selected"); + cy.get("#item1").should("not.have.attr", "selected"); + }); + + it("type='InactiveSelectable' + selectionMode='SingleEnd' — clicking item selects it", () => { + cy.mount( + + Option A + Option B + + ); + + cy.get("#item1").click(); + cy.get("#item1").should("have.attr", "selected"); + + cy.get("#item2").click(); + cy.get("#item2").should("have.attr", "selected"); + cy.get("#item1").should("not.have.attr", "selected"); + }); }); \ No newline at end of file From 8d0c7f40f4e09c279fbf00ed2f1ff1d31ee12dd2 Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Fri, 21 Aug 2026 13:18:50 +0300 Subject: [PATCH 4/6] test(ui5-list): add Enter key tests for InactiveSelectable type Cover the Enter keydown path through fireItemPress, which is a distinct code path from Space (keyup) and click. Adds: - Multiple + Enter toggles selection - Single + Enter selects item - item-click is NOT fired on Enter --- packages/main/cypress/specs/List.cy.tsx | 49 +++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/main/cypress/specs/List.cy.tsx b/packages/main/cypress/specs/List.cy.tsx index e51b8780954cf..1f74bdab741bc 100644 --- a/packages/main/cypress/specs/List.cy.tsx +++ b/packages/main/cypress/specs/List.cy.tsx @@ -4101,4 +4101,53 @@ describe("List - InactiveSelectable type", () => { cy.get("#item2").should("have.attr", "selected"); cy.get("#item1").should("not.have.attr", "selected"); }); + + it("type='InactiveSelectable' + selectionMode='Multiple' — pressing Enter toggles selection", () => { + cy.mount( + + Option A + + ); + + cy.get("#item1").should("not.have.attr", "selected"); + + cy.get("#item1").shadow().find("li").focus(); + cy.realPress("Enter"); + cy.get("#item1").should("have.attr", "selected"); + + cy.realPress("Enter"); + cy.get("#item1").should("not.have.attr", "selected"); + }); + + it("type='InactiveSelectable' + selectionMode='Single' — pressing Enter selects item", () => { + cy.mount( + + Option A + + ); + + cy.get("#item1").should("not.have.attr", "selected"); + + cy.get("#item1").shadow().find("li").focus(); + cy.realPress("Enter"); + + cy.get("#item1").should("have.attr", "selected"); + }); + + it("type='InactiveSelectable' — item-click event is NOT fired on Enter key", () => { + cy.mount( + + Option A + + ); + + cy.get("[ui5-list]").then(($list) => { + $list[0].addEventListener("ui5-item-click", cy.stub().as("itemClickStub")); + }); + + cy.get("#item1").shadow().find("li").focus(); + cy.realPress("Enter"); + + cy.get("@itemClickStub").should("not.have.been.called"); + }); }); \ No newline at end of file From 1d077df94130244d1f4d63e5e6220c71c648e26d Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Mon, 24 Aug 2026 10:23:05 +0300 Subject: [PATCH 5/6] fix(ui5-list): exclude SingleAuto from InactiveSelectable selection-requested SingleAuto drives selection by focus, not by click. The inverse guard (!None && !Delete) would fire selection-requested on click/Space/Enter for SingleAuto, causing a double-fire with the focus-driven path. Replace with the intent-revealing positive check (modeSingleSelect || modeMultiple), which is consistent with how the rest of ListItem reasons about modes that render visible selection controls. Also adds tests for direct checkbox/radio click and SingleAuto to lock in the expected behavior. --- packages/main/cypress/specs/List.cy.tsx | 52 +++++++++++++++++++++++++ packages/main/src/ListItem.ts | 2 +- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/packages/main/cypress/specs/List.cy.tsx b/packages/main/cypress/specs/List.cy.tsx index 1f74bdab741bc..ba28f67beabf2 100644 --- a/packages/main/cypress/specs/List.cy.tsx +++ b/packages/main/cypress/specs/List.cy.tsx @@ -4150,4 +4150,56 @@ describe("List - InactiveSelectable type", () => { cy.get("@itemClickStub").should("not.have.been.called"); }); + + it("type='InactiveSelectable' + selectionMode='Multiple' — clicking checkbox directly toggles selection", () => { + cy.mount( + + Option A + + ); + + cy.get("[ui5-list]").then(($list) => { + $list[0].addEventListener("ui5-selection-change", cy.stub().as("selectionChangeStub")); + }); + + cy.get("#item1").shadow().find("ui5-checkbox").click(); + + cy.get("#item1").should("have.attr", "selected"); + cy.get("@selectionChangeStub").should("have.been.calledOnce"); + }); + + it("type='InactiveSelectable' + selectionMode='SingleStart' — clicking radio directly selects item", () => { + cy.mount( + + Option A + + ); + + cy.get("[ui5-list]").then(($list) => { + $list[0].addEventListener("ui5-selection-change", cy.stub().as("selectionChangeStub")); + }); + + cy.get("#item1").shadow().find("ui5-radio-button").click(); + + cy.get("#item1").should("have.attr", "selected"); + cy.get("@selectionChangeStub").should("have.been.calledOnce"); + }); + + it("type='InactiveSelectable' + selectionMode='SingleAuto' — clicking item does nothing", () => { + cy.mount( + + Option A + + ); + + cy.get("[ui5-list]").then(($list) => { + $list[0].addEventListener("ui5-item-click", cy.stub().as("itemClickStub")); + $list[0].addEventListener("ui5-selection-change", cy.stub().as("selectionChangeStub")); + }); + + cy.get("#item1").click(); + + cy.get("@itemClickStub").should("not.have.been.called"); + cy.get("@selectionChangeStub").should("have.been.calledOnce"); + }); }); \ No newline at end of file diff --git a/packages/main/src/ListItem.ts b/packages/main/src/ListItem.ts index 15195b9173aac..7004bba656e0d 100644 --- a/packages/main/src/ListItem.ts +++ b/packages/main/src/ListItem.ts @@ -405,7 +405,7 @@ abstract class ListItem extends ListItemBase { return; } if (this.isInactiveSelectable) { - if (this._selectionMode !== ListSelectionMode.None && this._selectionMode !== ListSelectionMode.Delete) { + if (this.modeSingleSelect || this.modeMultiple) { this.fireDecoratorEvent("selection-requested", { item: this, selected: !this.selected, selectionComponentPressed: false }); } return; From fe83c93a072161e1ebcf534d9514104630d6674c Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Mon, 24 Aug 2026 14:19:21 +0300 Subject: [PATCH 6/6] test(ui5-list): add Inactive Space/Enter regression guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing regression tests only covered click. This extends them to Space and Enter — the two keyboard paths through fireItemPress — and asserts that neither item-click nor selection-change fires for Inactive. --- packages/main/cypress/specs/List.cy.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/main/cypress/specs/List.cy.tsx b/packages/main/cypress/specs/List.cy.tsx index ba28f67beabf2..3b58c5dfd7e14 100644 --- a/packages/main/cypress/specs/List.cy.tsx +++ b/packages/main/cypress/specs/List.cy.tsx @@ -3982,6 +3982,29 @@ describe("List - InactiveSelectable type", () => { cy.get("#item1").should("not.have.attr", "selected"); }); + it("type='Inactive' — Space and Enter do NOT toggle selection (regression guard)", () => { + cy.mount( + + Option A + + ); + + cy.get("[ui5-list]").then(($list) => { + $list[0].addEventListener("ui5-item-click", cy.stub().as("itemClickStub")); + $list[0].addEventListener("ui5-selection-change", cy.stub().as("selectionChangeStub")); + }); + + cy.get("#item1").shadow().find("li").focus(); + cy.realPress("Space"); + cy.get("#item1").should("not.have.attr", "selected"); + + cy.realPress("Enter"); + cy.get("#item1").should("not.have.attr", "selected"); + + cy.get("@itemClickStub").should("not.have.been.called"); + cy.get("@selectionChangeStub").should("not.have.been.called"); + }); + it("type='InactiveSelectable' + selectionMode='Single' — clicking item selects it", () => { cy.mount(