diff --git a/packages/main/cypress/specs/SegmentedButton.cy.tsx b/packages/main/cypress/specs/SegmentedButton.cy.tsx index e3623bde5457..5abad9e2da3b 100644 --- a/packages/main/cypress/specs/SegmentedButton.cy.tsx +++ b/packages/main/cypress/specs/SegmentedButton.cy.tsx @@ -566,4 +566,134 @@ describe("SegmentedButtonItem Accessibility", () => { .trigger("mouseover") .should("have.attr", "title", TOOLTIP_TEXT); }); -}); \ No newline at end of file +}); + +describe("SegmentedButtonItem: click event", () => { + it("should fire selection change event when item is clicked", () => { + const clickSpy = cy.spy().as("clickSpy"); + const selectionChangeSpy = cy.spy().as("selectionChangeSpy"); + + cy.mount( + + First + Second + + ); + + cy.get("[ui5-segmented-button]") + .then($el => { + $el[0].addEventListener("selection-change", selectionChangeSpy); + }); + + cy.get("#item2") + .then($el => { + $el[0].addEventListener("click", clickSpy); + }); + + cy.get("#item2") + .realClick(); + + cy.get("@clickSpy") + .should("have.been.calledOnce"); + + cy.get("@selectionChangeSpy") + .should("have.been.calledOnce"); + }); + + it("should prevent selection when preventDefault is called", () => { + cy.mount( + + First + Second + + ); + + cy.get("[ui5-segmented-button]") + .then($el => { + $el[0].addEventListener("selection-change", cy.spy().as("selectionChangeSpy")); + }) + + + cy.get("#item2") + .then($el => { + $el[0].addEventListener("click", (e: Event) => { + e.preventDefault(); + }); + }); + + cy.get("#item1") + .should("have.attr", "selected"); + cy.get("#item2") + .should("not.have.attr", "selected"); + + cy.get("#item2") + .realClick(); + + // Item 2 should NOT be selected because we called preventDefault + cy.get("#item1") + .should("have.attr", "selected"); + cy.get("#item2") + .should("not.have.attr", "selected"); + + cy.get("@selectionChangeSpy") + .should("not.have.been.called"); + }); + + it("should not fire click event when disabled item is clicked", () => { + const clickSpy = cy.spy().as("clickSpy"); + + cy.mount( + + First + Second + + ); + + cy.get("#item2") + .then($el => { + $el[0].addEventListener("click", clickSpy); + }); + + // Click the disabled item directly + cy.get("#item2") + .shadow() + .find("li") + .click({ force: true }); + + cy.get("@clickSpy").should("not.have.been.called"); + cy.get("#item2").should("not.have.attr", "selected"); + }); + + it("should provide item and originalEvent in click event detail", () => { + cy.mount( + + First + Second + + ); + + cy.get("#item2") + .then($el => { + $el[0].addEventListener("click", cy.spy((e: CustomEvent) => { + // Check that event detail contains item and originalEvent + expect(e.detail).to.have.property("item"); + expect(e.detail).to.have.property("originalEvent"); + + // Check item reference and properties + expect(e.detail.item).to.equal($el[0]); + expect(e.detail.item.id).to.equal("item2"); + expect(e.detail.item.slotTextContent).to.equal("Second"); + + // Check originalEvent is a MouseEvent + expect(e.detail.originalEvent).to.be.instanceOf(MouseEvent); + }).as("clickSpy")); + }); + + cy.get("#item2") + .realClick(); + + cy.get("@clickSpy") + .should("have.been.calledOnce"); + }); +}); + \ No newline at end of file diff --git a/packages/main/src/SegmentedButton.ts b/packages/main/src/SegmentedButton.ts index 1dc3fa61cb00..35e0c629d566 100644 --- a/packages/main/src/SegmentedButton.ts +++ b/packages/main/src/SegmentedButton.ts @@ -223,14 +223,17 @@ class SegmentedButton extends UI5Element { return; } + // Check if preventDefault was called on the native event (e.g., by item's semantic click handler) + if (e.defaultPrevented) { + return; + } + switch (this.selectionMode) { case SegmentedButtonSelectionMode.Multiple: - if (e instanceof KeyboardEvent) { - target.selected = !target.selected; - } + target.selected = !target.selected; break; default: - this._applySingleSelection(target); + this._applySingleSelection(target as unknown as ISegmentedButtonItem); } this.fireDecoratorEvent("selection-change", { @@ -256,7 +259,7 @@ class SegmentedButton extends UI5Element { _onkeydown(e: KeyboardEvent) { if (isEnter(e)) { - this._selectItem(e); // Enter key behavior remains unaffected + this._selectItem(e); } else if (isSpace(e)) { e.preventDefault(); // Prevent scrolling this._isSpacePressed = true; diff --git a/packages/main/src/SegmentedButtonItem.ts b/packages/main/src/SegmentedButtonItem.ts index 1bcb0e005c1a..c4b4ba0f7211 100644 --- a/packages/main/src/SegmentedButtonItem.ts +++ b/packages/main/src/SegmentedButtonItem.ts @@ -20,7 +20,14 @@ import type { ISegmentedButtonItem } from "./SegmentedButton.js"; import SegmentedButtonItemTemplate from "./SegmentedButtonItemTemplate.js"; import type { IButton } from "./Button.js"; +import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; import segmentedButtonItemCss from "./generated/themes/SegmentedButtonItem.css.js"; + +type SegmentedButtonItemClickEventDetail = { + item: SegmentedButtonItem, + originalEvent: Event, +}; + /** * @class * @@ -48,7 +55,26 @@ import segmentedButtonItemCss from "./generated/themes/SegmentedButtonItem.css.j template: SegmentedButtonItemTemplate, styles: segmentedButtonItemCss, }) + +/** + * Fired when the component is activated either with a mouse/tap or by using the Enter or Space key. + * + * **Note:** The event will not be fired if the `disabled` property is set to `true`. + * + * @param {SegmentedButtonItem} item The segmented button item that was clicked. + * @param {Event} originalEvent The original DOM event that triggered the click. Use this to access modifier keys (altKey, ctrlKey, metaKey, shiftKey) and other native event properties. + * @since 2.22.0 + * @public + */ +@event("click", { + bubbles: true, + cancelable: true, +}) + class SegmentedButtonItem extends UI5Element implements IButton, ISegmentedButtonItem { + eventDetails!: { + "click": SegmentedButtonItemClickEventDetail, + } /** * Defines whether the component is disabled. * A disabled component can't be selected or @@ -196,7 +222,18 @@ class SegmentedButtonItem extends UI5Element implements IButton, ISegmentedButto return; } - this.selected = !this.selected; + e.stopImmediatePropagation(); + + // Fire semantic click event (CustomEvent that bubbles) + const prevented = !this.fireDecoratorEvent("click", { + item: this, + originalEvent: e, + }); + + if (prevented) { + e.preventDefault(); + e.stopPropagation(); + } } onEnterDOM() { @@ -253,3 +290,4 @@ class SegmentedButtonItem extends UI5Element implements IButton, ISegmentedButto SegmentedButtonItem.define(); export default SegmentedButtonItem; +export type { SegmentedButtonItemClickEventDetail }; diff --git a/packages/main/test/pages/SegmentedButton.html b/packages/main/test/pages/SegmentedButton.html index c271de06f12a..ecc36b38aa2f 100644 --- a/packages/main/test/pages/SegmentedButton.html +++ b/packages/main/test/pages/SegmentedButton.html @@ -245,6 +245,28 @@

Accessibility

accessible ref text +
+

selection-change Event Demo

+

Normal behavior - selection works:

+ + First + Second + Third + +

+ +

+ +

With preventDefault - "Second" item blocks selection:

+ + First + Second (Blocked) + Third + +

+ +
+ diff --git a/packages/website/docs/_samples/main/Table/Popin/sample.tsx b/packages/website/docs/_samples/main/Table/Popin/sample.tsx index 7cc86b60c508..43f3531d388c 100644 --- a/packages/website/docs/_samples/main/Table/Popin/sample.tsx +++ b/packages/website/docs/_samples/main/Table/Popin/sample.tsx @@ -50,7 +50,7 @@ function App() { e: UI5CustomEvent, ) => { const selectedItem = e.detail.selectedItems[0]; - setPopinState((selectedItem as SegmentedButtonItemClass).tooltip === "Hide Details"); + setPopinState((selectedItem as unknown as SegmentedButtonItemClass).tooltip === "Hide Details"); }; return (