diff --git a/packages/main/cypress/specs/List.cy.tsx b/packages/main/cypress/specs/List.cy.tsx index 42a3372fbc8d6..3b58c5dfd7e14 100644 --- a/packages/main/cypress/specs/List.cy.tsx +++ b/packages/main/cypress/specs/List.cy.tsx @@ -3874,4 +3874,355 @@ 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='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( + + 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"); + }); + + 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"); + }); + + 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 668619999151e..7004bba656e0d 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.modeSingleSelect || this.modeMultiple) { + 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..a63fa608bf345 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.26.0 + */ + InactiveSelectable = "InactiveSelectable", + /** * Indicates that the item is clickable via active feedback when item is pressed. * @public