From b49802632e31ec5ef1082fc34541ba2e10948051 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Thu, 16 Apr 2026 11:58:47 +0300 Subject: [PATCH 1/6] feat: add semantic selection change event on item --- .../main/cypress/specs/SegmentedButton.cy.tsx | 79 ++++++++++++++++++- packages/main/src/SegmentedButtonItem.ts | 38 +++++++++ packages/main/test/pages/SegmentedButton.html | 49 ++++++++++++ 3 files changed, 165 insertions(+), 1 deletion(-) diff --git a/packages/main/cypress/specs/SegmentedButton.cy.tsx b/packages/main/cypress/specs/SegmentedButton.cy.tsx index e3623bde5457..f34dc0fefacf 100644 --- a/packages/main/cypress/specs/SegmentedButton.cy.tsx +++ b/packages/main/cypress/specs/SegmentedButton.cy.tsx @@ -566,4 +566,81 @@ describe("SegmentedButtonItem Accessibility", () => { .trigger("mouseover") .should("have.attr", "title", TOOLTIP_TEXT); }); -}); \ No newline at end of file +}); + +describe("SegmentedButtonItem: selection-change event", () => { + it("should fire selection-change event when item is clicked", () => { + const selectionChangeSpy = cy.spy().as("selectionChangeSpy"); + + cy.mount( + + First + Second + + ); + + cy.get("#item2") + .then($el => { + $el[0].addEventListener("selection-change", selectionChangeSpy); + }); + + cy.get("#item2") + .ui5SegmentedButtonItemToggleSelect(); + + cy.get("@selectionChangeSpy").should("have.been.calledOnce"); + }); + + it("should prevent selection when preventDefault is called", () => { + cy.mount( + + First + Second + + ); + + cy.get("#item2") + .then($el => { + $el[0].addEventListener("selection-change", (e: Event) => { + e.preventDefault(); + }); + }); + + // Item 1 should be selected initially + cy.get("#item1").should("have.attr", "selected"); + cy.get("#item2").should("not.have.attr", "selected"); + + // Click item 2 + 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"); + }); + + it("should not fire selection-change event when disabled item is clicked", () => { + const selectionChangeSpy = cy.spy().as("selectionChangeSpy"); + + cy.mount( + + First + Second + + ); + + cy.get("#item2") + .then($el => { + $el[0].addEventListener("selection-change", selectionChangeSpy); + }); + + // Click the disabled item directly + cy.get("#item2") + .shadow() + .find("li") + .click({ force: true }); + + cy.get("@selectionChangeSpy").should("not.have.been.called"); + cy.get("#item2").should("not.have.attr", "selected"); + }); + +}); + \ No newline at end of file diff --git a/packages/main/src/SegmentedButtonItem.ts b/packages/main/src/SegmentedButtonItem.ts index 1bcb0e005c1a..e6173bc02759 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 = { + originalEvent: Event; + item: SegmentedButtonItem; +}; + /** * @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`. + * + * @since 2.22.0 + * @public + * @param {MouseEvent} originalEvent The original mouse event that triggered the selection + * @param {SegmentedButtonItem} item The segmented button item that was clicked + */ +@event("selection-change", { + bubbles: true, + cancelable: true, +}) + class SegmentedButtonItem extends UI5Element implements IButton, ISegmentedButtonItem { + eventDetails!: { + "selection-change": SegmentedButtonItemClickEventDetail, + } /** * Defines whether the component is disabled. * A disabled component can't be selected or @@ -196,6 +222,18 @@ class SegmentedButtonItem extends UI5Element implements IButton, ISegmentedButto return; } + const prevented = !this.fireDecoratorEvent("selection-change", { + originalEvent: e, + item: this, + }); + + if (prevented) { + // Stop the native MouseEvent from reaching the parent + e.stopPropagation(); + e.preventDefault(); + return; + } + this.selected = !this.selected; } diff --git a/packages/main/test/pages/SegmentedButton.html b/packages/main/test/pages/SegmentedButton.html index c271de06f12a..60a228c80b34 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 + +

+ +
+ From 9a92e68fa89bcbca13db1f98b690a854e17f9757 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Thu, 16 Apr 2026 14:21:25 +0300 Subject: [PATCH 2/6] fix: export type --- packages/main/src/SegmentedButtonItem.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/main/src/SegmentedButtonItem.ts b/packages/main/src/SegmentedButtonItem.ts index e6173bc02759..106e431f54ea 100644 --- a/packages/main/src/SegmentedButtonItem.ts +++ b/packages/main/src/SegmentedButtonItem.ts @@ -291,3 +291,4 @@ class SegmentedButtonItem extends UI5Element implements IButton, ISegmentedButto SegmentedButtonItem.define(); export default SegmentedButtonItem; +export type { SegmentedButtonItemClickEventDetail }; From 4efd7aeada452317a519f51c9050b90519859bee Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Fri, 17 Apr 2026 10:31:28 +0300 Subject: [PATCH 3/6] feat: change custom event --- .../main/cypress/specs/SegmentedButton.cy.tsx | 1249 +++++++++-------- packages/main/src/SegmentedButton.ts | 10 +- packages/main/src/SegmentedButtonItem.ts | 16 +- packages/main/test/pages/SegmentedButton.html | 12 +- 4 files changed, 659 insertions(+), 628 deletions(-) diff --git a/packages/main/cypress/specs/SegmentedButton.cy.tsx b/packages/main/cypress/specs/SegmentedButton.cy.tsx index f34dc0fefacf..19d80d450cd2 100644 --- a/packages/main/cypress/specs/SegmentedButton.cy.tsx +++ b/packages/main/cypress/specs/SegmentedButton.cy.tsx @@ -5,642 +5,663 @@ import type UI5Element from "@ui5/webcomponents-base"; import { SEGMENTEDBUTTON_ARIA_DESCRIBEDBY } from "../../src/generated/i18n/i18n-defaults.js"; import { Key } from "@ui5/webcomponents-base/dist/thirdparty/preact/preact.module.js"; -function testSelectItem(shouldPreventSelect: boolean, pressedKeys: Key[]) { - cy.mount( - - First - Second - - ); - - cy.get("[ui5-segmented-button]") - .as("segmentedButton"); - - cy.get("@segmentedButton") - .find("[ui5-segmented-button-item]") - .as("items"); - - cy.get("@items") - .ui5SegmentedButtonFocusFirstItem(); - - cy.realPress("ArrowRight"); - - cy.get("@items") - .eq(1) - .as("secondItem"); - - cy.get("@secondItem") - .should("be.focused"); - - cy.get("@secondItem") - .should("not.have.attr", "selected"); - - cy.realPress(pressedKeys); - - cy.get("@secondItem") - .should(shouldPreventSelect ? "not.have.attr" : "have.attr", "selected"); -} - -describe("SegmentedButton general interaction tests", () => { - it("should have first item selected by default", () => { - cy.mount( - - First - Second - - ); - - cy.get("[ui5-segmented-button]") - .as("segmentedButton"); - - cy.get("@segmentedButton") - .find("[ui5-segmented-button-item]") - .first() - .should("have.attr", "selected"); - }); - - it("should select second item with enter", () => { - testSelectItem(false, ["Enter"]); - }); - - it("should select second item with space", () => { - testSelectItem(false, ["Space"]); - }); - - it("should not select second item on space when shift is pressed", () => { - testSelectItem(true, ["Space", "Shift"]); - }); - - it("should not select second item on space when escape is pressed", () => { - testSelectItem(true, ["Space", "Escape"]); - }); - - it("should select last item with mouse", () => { - cy.mount( - - First - Second - Third - - ); - - cy.get("[ui5-segmented-button]") - .as("segmentedButton"); - - cy.get("@segmentedButton") - .find("[ui5-segmented-button-item]") - .as("items"); - - cy.get("@items") - .last() - .as("lastItem"); - - cy.get("@lastItem") - .ui5SegmentedButtonItemToggleSelect(); - }); - - it("should be able to select multple items in multiple selection mode", () => { - cy.mount( - - First - Second - - ); - - cy.get("[ui5-segmented-button]") - .as("segmentedButton"); - - cy.get("@segmentedButton") - .find("[ui5-segmented-button-item]") - .as("items"); - - cy.get("@items") - .first() - .as("firstItem"); - - cy.get("@items") - .last() - .as("lastItem"); - - // Select second item - cy.get("@lastItem") - .ui5SegmentedButtonItemToggleSelect(); - - // First item should still be selected - cy.get("@firstItem") - .should("have.attr", "selected"); - }); - - it("should be able to deselect items in multiple selection mode", () => { - cy.mount( - - First - Second - - ); - - cy.get("[ui5-segmented-button]") - .as("segmentedButton"); - - cy.get("@segmentedButton") - .find("[ui5-segmented-button-item]") - .as("items"); - - cy.get("@items") - .first() - .as("firstItem"); - - cy.get("@items") - .last() - .as("lastItem"); - - const deselect = true; - // Deselect first item - cy.get("@firstItem") - .ui5SegmentedButtonItemToggleSelect(deselect); - - // Second item is still selected - cy.get("@lastItem") - .should("have.attr", "selected"); - }); -}); - -describe("SegmentedButton - getFocusDomRef", () => { - it("should return undefined when the SegmentedButton is empty", () => { - cy.mount(); - - cy.get("[ui5-segmented-button]") - .then(($el) => { - expect($el[0].getFocusDomRef()).to.be.undefined; - }); - }); - - it("should return first item if no item was focused before", () => { - cy.mount( - - - - - ); - - cy.get("[ui5-segmented-button], #button1") - .then(($el) => { - const wrapper = $el[0], - firstButton = $el[1]; - - expect(wrapper.getFocusDomRef()).to.equal(firstButton.getFocusDomRef()); - }); - }); - - it("should return last focused item in the SegmentedButton", () => { - cy.mount( - - - - - ); - - cy.get('#button2').realClick(); - cy.get('#button2').should("be.focused"); - - cy.get("[ui5-segmented-button], #button2") - .then(($el) => { - const wrapper = $el[0], - secondButton = $el[1]; - - expect(wrapper.getFocusDomRef()).to.equal(secondButton.getFocusDomRef()); - }); - }); -}); - -describe("SegmentedButtonItems appearance", () => { - it("should not render items with hidden property and set correct aria-posinset and aria-setsize", () => { - cy.mount( - - First - - Third - - ); - - cy.get("[ui5-segmented-button]") - .as("segmentedButton"); - - cy.get("@segmentedButton") - .find("[ui5-segmented-button-item]:not([hidden])") - .as("visibleItems"); - - cy.get("@segmentedButton") - .find("[ui5-segmented-button-item][hidden]") - .as("hiddenItems"); - - // Only visible items should be counted by SegmentedButton - cy.get("@segmentedButton") - .invoke("attr", "style") - .should("satisfy", style => style.slice(-9) === "count: 2;"); - - // Assert aria-posinset and aria-setsize for visible items - cy.get("@visibleItems") - .shadow() - .find("li") - .each(($el, index, $list) => { - cy.wrap($el).should("have.attr", "aria-posinset", `${index + 1}`); - cy.wrap($el).should("have.attr", "aria-setsize", `${$list.length}`); - }); - - // Assert hidden item does not have aria-posinset and aria-setsize - cy.get("@hiddenItems") - .shadow() - .find("li") - .each(($el) => { - cy.wrap($el).should("not.have.attr", "aria-posinset"); - cy.wrap($el).should("not.have.attr", "aria-setsize"); - }); - }); -}); - -describe("SegmentedButton: fitContent", () => { - it("should have items with width which fits item content when itemsFitContent is true", () => { - cy.mount( - - Short - Much longer text - Medium - - ); - - cy.get("#item1") - .invoke("outerWidth") - .then(shortWidth => { - cy.get("#item2") - .invoke("outerWidth") - .should("be.gt", shortWidth); - cy.get("#item3") - .invoke("outerWidth") - .should("be.gt", shortWidth); - }); - }); - - it("should have items with equal width when itemsFitContent is false (default)", () => { - cy.mount( - - Short - Much longer text - Medium - - ); - - cy.get("#item1") - .invoke("outerWidth") - .then(width1 => { - cy.get("#item2") - .invoke("outerWidth") - .should("eq", width1); - cy.get("#item3") - .invoke("outerWidth") - .should("eq", width1); - }); - }); -}); - -describe("SegmentedButton Accessibility", () => { - it("segmented button should have correct aria label when accessibleName is set", () => { - const LABEL = "Label"; - cy.mount( - - First - Second - - ); - - cy.get("[ui5-segmented-button]") - .shadow() - .find(".ui5-segmented-button-root") - .should("have.attr", "aria-label", LABEL); - }); - - it("segmented button should have correct aria label when external label is set", () => { - const LABEL = "External label"; - cy.mount( - <> - - - First - Second - - - ); - - cy.get("[ui5-segmented-button]") - .shadow() - .find(".ui5-segmented-button-root") - .should("have.attr", "aria-label", LABEL); - }); - - it("segmented button should have correct aria label when accessibleNameRef is set", () => { - const LABEL = "External label"; - cy.mount( - <> -

{LABEL}

- - First - Second - - - ); - - cy.get("[ui5-segmented-button]") - .shadow() - .find(".ui5-segmented-button-root") - .should("have.attr", "aria-label", LABEL); - }); - - it("segmented button should have correct aria description when neither accessibleDescription nor accessibleDescriptionRef are set", () => { - cy.mount( - - First - Second - - ); - - cy.get("[ui5-segmented-button]") - .shadow() - .find(".ui5-segmented-button-root") - .should("have.attr", "aria-description", SegmentedButton.i18nBundle.getText(SEGMENTEDBUTTON_ARIA_DESCRIBEDBY)); - }); - - it("segmented button should have correct aria description when accessibleDescription is set", () => { - const DESCRIPTION = "Description"; - cy.mount( - - First - Second - - ); - - cy.get("[ui5-segmented-button]") - .shadow() - .find(".ui5-segmented-button-root") - .should("have.attr", "aria-description", `${DESCRIPTION} ${SegmentedButton.i18nBundle.getText(SEGMENTEDBUTTON_ARIA_DESCRIBEDBY)}`); - }); - - it("segmented button should have correct aria description when accessibleDescriptionRef is set", () => { - const DESCRIPTION = "External description"; - cy.mount( - <> -

{DESCRIPTION}

- - First - Second - - - ); - - cy.get("[ui5-segmented-button]") - .shadow() - .find(".ui5-segmented-button-root") - .should("have.attr", "aria-description", `${DESCRIPTION} ${SegmentedButton.i18nBundle.getText(SEGMENTEDBUTTON_ARIA_DESCRIBEDBY)}`); - }); - - it("segmented button should have correct aria-multiselectable", () => { - cy.mount( - <> - - First - Second - - - First - Second - - - ); - - // Test Single mode (default) should have aria-multiselectable="false" - cy.get("[ui5-segmented-button]") - .first() - .shadow() - .find("ul[role='listbox']") - .should("have.attr", "aria-multiselectable", "false") - .should("have.attr", "aria-orientation", "horizontal"); - - // Test Multiple mode should have aria-multiselectable="true" - cy.get("[ui5-segmented-button]") - .last() - .shadow() - .find("ul[role='listbox']") - .should("have.attr", "aria-multiselectable", "true") - .should("have.attr", "aria-orientation", "horizontal"); - }); +// function testSelectItem(shouldPreventSelect: boolean, pressedKeys: Key[]) { +// cy.mount( +// +// First +// Second +// +// ); + +// cy.get("[ui5-segmented-button]") +// .as("segmentedButton"); + +// cy.get("@segmentedButton") +// .find("[ui5-segmented-button-item]") +// .as("items"); + +// cy.get("@items") +// .ui5SegmentedButtonFocusFirstItem(); + +// cy.realPress("ArrowRight"); + +// cy.get("@items") +// .eq(1) +// .as("secondItem"); + +// cy.get("@secondItem") +// .should("be.focused"); + +// cy.get("@secondItem") +// .should("not.have.attr", "selected"); + +// cy.realPress(pressedKeys); + +// cy.get("@secondItem") +// .should(shouldPreventSelect ? "not.have.attr" : "have.attr", "selected"); +// } + +// describe("SegmentedButton general interaction tests", () => { +// it("should have first item selected by default", () => { +// cy.mount( +// +// First +// Second +// +// ); + +// cy.get("[ui5-segmented-button]") +// .as("segmentedButton"); + +// cy.get("@segmentedButton") +// .find("[ui5-segmented-button-item]") +// .first() +// .should("have.attr", "selected"); +// }); + +// it("should select second item with enter", () => { +// testSelectItem(false, ["Enter"]); +// }); + +// it("should select second item with space", () => { +// testSelectItem(false, ["Space"]); +// }); + +// it("should not select second item on space when shift is pressed", () => { +// testSelectItem(true, ["Space", "Shift"]); +// }); + +// it("should not select second item on space when escape is pressed", () => { +// testSelectItem(true, ["Space", "Escape"]); +// }); + +// it("should select last item with mouse", () => { +// cy.mount( +// +// First +// Second +// Third +// +// ); + +// cy.get("[ui5-segmented-button]") +// .as("segmentedButton"); + +// cy.get("@segmentedButton") +// .find("[ui5-segmented-button-item]") +// .as("items"); + +// cy.get("@items") +// .last() +// .as("lastItem"); + +// cy.get("@lastItem") +// .ui5SegmentedButtonItemToggleSelect(); +// }); + +// it("should be able to select multple items in multiple selection mode", () => { +// cy.mount( +// +// First +// Second +// +// ); + +// cy.get("[ui5-segmented-button]") +// .as("segmentedButton"); + +// cy.get("@segmentedButton") +// .find("[ui5-segmented-button-item]") +// .as("items"); + +// cy.get("@items") +// .first() +// .as("firstItem"); + +// cy.get("@items") +// .last() +// .as("lastItem"); + +// // Select second item +// cy.get("@lastItem") +// .ui5SegmentedButtonItemToggleSelect(); + +// // First item should still be selected +// cy.get("@firstItem") +// .should("have.attr", "selected"); +// }); + +// it("should be able to deselect items in multiple selection mode", () => { +// cy.mount( +// +// First +// Second +// +// ); + +// cy.get("[ui5-segmented-button]") +// .as("segmentedButton"); + +// cy.get("@segmentedButton") +// .find("[ui5-segmented-button-item]") +// .as("items"); + +// cy.get("@items") +// .first() +// .as("firstItem"); + +// cy.get("@items") +// .last() +// .as("lastItem"); + +// const deselect = true; +// // Deselect first item +// cy.get("@firstItem") +// .ui5SegmentedButtonItemToggleSelect(deselect); + +// // Second item is still selected +// cy.get("@lastItem") +// .should("have.attr", "selected"); +// }); +// }); + +// describe("SegmentedButton - getFocusDomRef", () => { +// it("should return undefined when the SegmentedButton is empty", () => { +// cy.mount(); + +// cy.get("[ui5-segmented-button]") +// .then(($el) => { +// expect($el[0].getFocusDomRef()).to.be.undefined; +// }); +// }); + +// it("should return first item if no item was focused before", () => { +// cy.mount( +// +// +// +// +// ); + +// cy.get("[ui5-segmented-button], #button1") +// .then(($el) => { +// const wrapper = $el[0], +// firstButton = $el[1]; + +// expect(wrapper.getFocusDomRef()).to.equal(firstButton.getFocusDomRef()); +// }); +// }); + +// it("should return last focused item in the SegmentedButton", () => { +// cy.mount( +// +// +// +// +// ); + +// cy.get('#button2').realClick(); +// cy.get('#button2').should("be.focused"); + +// cy.get("[ui5-segmented-button], #button2") +// .then(($el) => { +// const wrapper = $el[0], +// secondButton = $el[1]; + +// expect(wrapper.getFocusDomRef()).to.equal(secondButton.getFocusDomRef()); +// }); +// }); +// }); + +// describe("SegmentedButtonItems appearance", () => { +// it("should not render items with hidden property and set correct aria-posinset and aria-setsize", () => { +// cy.mount( +// +// First +// +// Third +// +// ); + +// cy.get("[ui5-segmented-button]") +// .as("segmentedButton"); + +// cy.get("@segmentedButton") +// .find("[ui5-segmented-button-item]:not([hidden])") +// .as("visibleItems"); + +// cy.get("@segmentedButton") +// .find("[ui5-segmented-button-item][hidden]") +// .as("hiddenItems"); + +// // Only visible items should be counted by SegmentedButton +// cy.get("@segmentedButton") +// .invoke("attr", "style") +// .should("satisfy", style => style.slice(-9) === "count: 2;"); + +// // Assert aria-posinset and aria-setsize for visible items +// cy.get("@visibleItems") +// .shadow() +// .find("li") +// .each(($el, index, $list) => { +// cy.wrap($el).should("have.attr", "aria-posinset", `${index + 1}`); +// cy.wrap($el).should("have.attr", "aria-setsize", `${$list.length}`); +// }); + +// // Assert hidden item does not have aria-posinset and aria-setsize +// cy.get("@hiddenItems") +// .shadow() +// .find("li") +// .each(($el) => { +// cy.wrap($el).should("not.have.attr", "aria-posinset"); +// cy.wrap($el).should("not.have.attr", "aria-setsize"); +// }); +// }); +// }); + +// describe("SegmentedButton: fitContent", () => { +// it("should have items with width which fits item content when itemsFitContent is true", () => { +// cy.mount( +// +// Short +// Much longer text +// Medium +// +// ); + +// cy.get("#item1") +// .invoke("outerWidth") +// .then(shortWidth => { +// cy.get("#item2") +// .invoke("outerWidth") +// .should("be.gt", shortWidth); +// cy.get("#item3") +// .invoke("outerWidth") +// .should("be.gt", shortWidth); +// }); +// }); + +// it("should have items with equal width when itemsFitContent is false (default)", () => { +// cy.mount( +// +// Short +// Much longer text +// Medium +// +// ); + +// cy.get("#item1") +// .invoke("outerWidth") +// .then(width1 => { +// cy.get("#item2") +// .invoke("outerWidth") +// .should("eq", width1); +// cy.get("#item3") +// .invoke("outerWidth") +// .should("eq", width1); +// }); +// }); +// }); + +// describe("SegmentedButton Accessibility", () => { +// it("segmented button should have correct aria label when accessibleName is set", () => { +// const LABEL = "Label"; +// cy.mount( +// +// First +// Second +// +// ); + +// cy.get("[ui5-segmented-button]") +// .shadow() +// .find(".ui5-segmented-button-root") +// .should("have.attr", "aria-label", LABEL); +// }); + +// it("segmented button should have correct aria label when external label is set", () => { +// const LABEL = "External label"; +// cy.mount( +// <> +// +// +// First +// Second +// +// +// ); + +// cy.get("[ui5-segmented-button]") +// .shadow() +// .find(".ui5-segmented-button-root") +// .should("have.attr", "aria-label", LABEL); +// }); + +// it("segmented button should have correct aria label when accessibleNameRef is set", () => { +// const LABEL = "External label"; +// cy.mount( +// <> +//

{LABEL}

+// +// First +// Second +// +// +// ); + +// cy.get("[ui5-segmented-button]") +// .shadow() +// .find(".ui5-segmented-button-root") +// .should("have.attr", "aria-label", LABEL); +// }); + +// it("segmented button should have correct aria description when neither accessibleDescription nor accessibleDescriptionRef are set", () => { +// cy.mount( +// +// First +// Second +// +// ); + +// cy.get("[ui5-segmented-button]") +// .shadow() +// .find(".ui5-segmented-button-root") +// .should("have.attr", "aria-description", SegmentedButton.i18nBundle.getText(SEGMENTEDBUTTON_ARIA_DESCRIBEDBY)); +// }); + +// it("segmented button should have correct aria description when accessibleDescription is set", () => { +// const DESCRIPTION = "Description"; +// cy.mount( +// +// First +// Second +// +// ); + +// cy.get("[ui5-segmented-button]") +// .shadow() +// .find(".ui5-segmented-button-root") +// .should("have.attr", "aria-description", `${DESCRIPTION} ${SegmentedButton.i18nBundle.getText(SEGMENTEDBUTTON_ARIA_DESCRIBEDBY)}`); +// }); + +// it("segmented button should have correct aria description when accessibleDescriptionRef is set", () => { +// const DESCRIPTION = "External description"; +// cy.mount( +// <> +//

{DESCRIPTION}

+// +// First +// Second +// +// +// ); + +// cy.get("[ui5-segmented-button]") +// .shadow() +// .find(".ui5-segmented-button-root") +// .should("have.attr", "aria-description", `${DESCRIPTION} ${SegmentedButton.i18nBundle.getText(SEGMENTEDBUTTON_ARIA_DESCRIBEDBY)}`); +// }); + +// it("segmented button should have correct aria-multiselectable", () => { +// cy.mount( +// <> +// +// First +// Second +// +// +// First +// Second +// +// +// ); + +// // Test Single mode (default) should have aria-multiselectable="false" +// cy.get("[ui5-segmented-button]") +// .first() +// .shadow() +// .find("ul[role='listbox']") +// .should("have.attr", "aria-multiselectable", "false") +// .should("have.attr", "aria-orientation", "horizontal"); + +// // Test Multiple mode should have aria-multiselectable="true" +// cy.get("[ui5-segmented-button]") +// .last() +// .shadow() +// .find("ul[role='listbox']") +// .should("have.attr", "aria-multiselectable", "true") +// .should("have.attr", "aria-orientation", "horizontal"); +// }); -}); - - -describe("SegmentedButtonItem Accessibility", () => { - it("segmented button items should have correct aria labels", () => { - const LABEL = "Text Label"; - const REF_LABEL = "Ref Label"; - const FOR_LABEL = "For Label"; - cy.mount( - <> - - - First - Second - Third - - {REF_LABEL} - - ); - - cy.get("[ui5-segmented-button]") - .as("segmentedButton"); - - cy.get("@segmentedButton") - .find("[ui5-segmented-button-item]") - .as("items"); - - cy.get("@items") - .eq(0) - .shadow() - .find("li") - .should("have.attr", "aria-label", LABEL); - - cy.get("@items") - .eq(1) - .shadow() - .find("li") - .should("have.attr", "aria-label", REF_LABEL); - - cy.get("@items") - .eq(2) - .shadow() - .find("li") - .should("have.attr", "aria-label", FOR_LABEL); - }); - - it("segmented button item should have correct aria descriptions", () => { - const DESCRIPTION = "Text Description"; - const REF_DESCRIPTION = "Ref Description"; - cy.mount( - <> -

{REF_DESCRIPTION}

- - First - Second - - - ); - - cy.get("[ui5-segmented-button]") - .as("segmentedButton"); - - cy.get("@segmentedButton") - .find("[ui5-segmented-button-item]") - .as("items"); - - cy.get("@items") - .eq(0) - .shadow() - .find("li") - .should("have.attr", "aria-description", DESCRIPTION); - - cy.get("@items") - .eq(1) - .shadow() - .find("li") - .should("have.attr", "aria-description", REF_DESCRIPTION); - }); - - it("should set title attribute to slot text when tooltip is not provided", () => { - cy.mount( - - Segmented Item Text - - ); - - cy.get("#item1") - .shadow() - .find("li") - .should("have.attr", "title", "Segmented Item Text"); - }); - - it("should show tooltip for disabled items", () => { - const TOOLTIP_TEXT = "Disabled item tooltip"; - cy.mount( - - Disabled Item - Enabled Item - - ); - - // Check that disabled item has tooltip - cy.get("[ui5-segmented-button-item][disabled]") - .shadow() - .find("li") - .should("have.attr", "title", TOOLTIP_TEXT); - }); - - it("should show tooltip on hover for disabled items", () => { - const TOOLTIP_TEXT = "This is a disabled item"; - cy.mount( - - - Disabled - - - ); - - // Verify the disabled item has the correct title attribute - cy.get("#disabledItem") - .shadow() - .find("li") - .should("have.attr", "title", TOOLTIP_TEXT) - .should("have.attr", "aria-disabled", "true"); - - // Test that hovering shows tooltip (this is browser behavior, but we can verify the title is present) - cy.get("#disabledItem") - .shadow() - .find("li") - .trigger("mouseover") - .should("have.attr", "title", TOOLTIP_TEXT); - }); -}); - -describe("SegmentedButtonItem: selection-change event", () => { - it("should fire selection-change event when item is clicked", () => { +// }); + + +// describe("SegmentedButtonItem Accessibility", () => { +// it("segmented button items should have correct aria labels", () => { +// const LABEL = "Text Label"; +// const REF_LABEL = "Ref Label"; +// const FOR_LABEL = "For Label"; +// cy.mount( +// <> +// +// +// First +// Second +// Third +// +// {REF_LABEL} +// +// ); + +// cy.get("[ui5-segmented-button]") +// .as("segmentedButton"); + +// cy.get("@segmentedButton") +// .find("[ui5-segmented-button-item]") +// .as("items"); + +// cy.get("@items") +// .eq(0) +// .shadow() +// .find("li") +// .should("have.attr", "aria-label", LABEL); + +// cy.get("@items") +// .eq(1) +// .shadow() +// .find("li") +// .should("have.attr", "aria-label", REF_LABEL); + +// cy.get("@items") +// .eq(2) +// .shadow() +// .find("li") +// .should("have.attr", "aria-label", FOR_LABEL); +// }); + +// it("segmented button item should have correct aria descriptions", () => { +// const DESCRIPTION = "Text Description"; +// const REF_DESCRIPTION = "Ref Description"; +// cy.mount( +// <> +//

{REF_DESCRIPTION}

+// +// First +// Second +// +// +// ); + +// cy.get("[ui5-segmented-button]") +// .as("segmentedButton"); + +// cy.get("@segmentedButton") +// .find("[ui5-segmented-button-item]") +// .as("items"); + +// cy.get("@items") +// .eq(0) +// .shadow() +// .find("li") +// .should("have.attr", "aria-description", DESCRIPTION); + +// cy.get("@items") +// .eq(1) +// .shadow() +// .find("li") +// .should("have.attr", "aria-description", REF_DESCRIPTION); +// }); + +// it("should set title attribute to slot text when tooltip is not provided", () => { +// cy.mount( +// +// Segmented Item Text +// +// ); + +// cy.get("#item1") +// .shadow() +// .find("li") +// .should("have.attr", "title", "Segmented Item Text"); +// }); + +// it("should show tooltip for disabled items", () => { +// const TOOLTIP_TEXT = "Disabled item tooltip"; +// cy.mount( +// +// Disabled Item +// Enabled Item +// +// ); + +// // Check that disabled item has tooltip +// cy.get("[ui5-segmented-button-item][disabled]") +// .shadow() +// .find("li") +// .should("have.attr", "title", TOOLTIP_TEXT); +// }); + +// it("should show tooltip on hover for disabled items", () => { +// const TOOLTIP_TEXT = "This is a disabled item"; +// cy.mount( +// +// +// Disabled +// +// +// ); + +// // Verify the disabled item has the correct title attribute +// cy.get("#disabledItem") +// .shadow() +// .find("li") +// .should("have.attr", "title", TOOLTIP_TEXT) +// .should("have.attr", "aria-disabled", "true"); + +// // Test that hovering shows tooltip (this is browser behavior, but we can verify the title is present) +// cy.get("#disabledItem") +// .shadow() +// .find("li") +// .trigger("mouseover") +// .should("have.attr", "title", TOOLTIP_TEXT); +// }); +// }); + +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 + First Second ); - cy.get("#item2") + cy.get("[ui5-segmented-button]") .then($el => { $el[0].addEventListener("selection-change", selectionChangeSpy); }); - cy.get("#item2") - .ui5SegmentedButtonItemToggleSelect(); - - cy.get("@selectionChangeSpy").should("have.been.calledOnce"); - }); - - it("should prevent selection when preventDefault is called", () => { - cy.mount( - - First - Second - - ); - cy.get("#item2") .then($el => { - $el[0].addEventListener("selection-change", (e: Event) => { - e.preventDefault(); - }); + $el[0].addEventListener("click", clickSpy); }); - // Item 1 should be selected initially - cy.get("#item1").should("have.attr", "selected"); - cy.get("#item2").should("not.have.attr", "selected"); - - // Click item 2 - 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"); - }); - - it("should not fire selection-change event when disabled item is clicked", () => { - const selectionChangeSpy = cy.spy().as("selectionChangeSpy"); - - cy.mount( - - First - Second - - ); - cy.get("#item2") - .then($el => { - $el[0].addEventListener("selection-change", selectionChangeSpy); - }); + .realClick(); - // Click the disabled item directly - cy.get("#item2") - .shadow() - .find("li") - .click({ force: true }); + cy.get("@clickSpy") + .should("have.been.calledOnce"); - cy.get("@selectionChangeSpy").should("not.have.been.called"); - cy.get("#item2").should("not.have.attr", "selected"); + 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"); + // }); }); \ No newline at end of file diff --git a/packages/main/src/SegmentedButton.ts b/packages/main/src/SegmentedButton.ts index 1dc3fa61cb00..4ce03ca9e052 100644 --- a/packages/main/src/SegmentedButton.ts +++ b/packages/main/src/SegmentedButton.ts @@ -223,6 +223,11 @@ 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) { @@ -230,12 +235,13 @@ class SegmentedButton extends UI5Element { } break; default: - this._applySingleSelection(target); + this._applySingleSelection(target as unknown as ISegmentedButtonItem); } this.fireDecoratorEvent("selection-change", { selectedItems: this.selectedItems, }); + console.log("parent click"); this._itemNavigation.setCurrentItem(target); @@ -256,7 +262,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 106e431f54ea..e61bea548772 100644 --- a/packages/main/src/SegmentedButtonItem.ts +++ b/packages/main/src/SegmentedButtonItem.ts @@ -66,14 +66,14 @@ type SegmentedButtonItemClickEventDetail = { * @param {MouseEvent} originalEvent The original mouse event that triggered the selection * @param {SegmentedButtonItem} item The segmented button item that was clicked */ -@event("selection-change", { +@event("click", { bubbles: true, cancelable: true, }) class SegmentedButtonItem extends UI5Element implements IButton, ISegmentedButtonItem { eventDetails!: { - "selection-change": SegmentedButtonItemClickEventDetail, + "click": SegmentedButtonItemClickEventDetail, } /** * Defines whether the component is disabled. @@ -221,19 +221,23 @@ class SegmentedButtonItem extends UI5Element implements IButton, ISegmentedButto e.stopPropagation(); return; } - - const prevented = !this.fireDecoratorEvent("selection-change", { + console.log("item click"); + // Fire semantic click event (CustomEvent that bubbles) + const prevented = !this.fireDecoratorEvent("click", { originalEvent: e, item: this, }); + console.log("item click event fired, prevented:", prevented); + if (prevented) { - // Stop the native MouseEvent from reaching the parent - e.stopPropagation(); e.preventDefault(); + e.stopPropagation(); return; } + + this.selected = !this.selected; } diff --git a/packages/main/test/pages/SegmentedButton.html b/packages/main/test/pages/SegmentedButton.html index 60a228c80b34..ecc36b38aa2f 100644 --- a/packages/main/test/pages/SegmentedButton.html +++ b/packages/main/test/pages/SegmentedButton.html @@ -288,29 +288,29 @@

selection-change Event Demo

}); // Normal behavior - events fire, selection works - normalItem1.addEventListener("selection-change", function(e) { + normalItem1.addEventListener("click", function(e) { normalEventLog.value = "Item 1 clicked - selection allowed"; }); - normalItem2.addEventListener("selection-change", function(e) { + normalItem2.addEventListener("click", function(e) { normalEventLog.value = "Item 2 clicked - selection allowed"; }); - normalItem3.addEventListener("selection-change", function(e) { + normalItem3.addEventListener("click", function(e) { normalEventLog.value = "Item 3 clicked - selection allowed"; }); // Blocked behavior - "Second" item prevents selection - blockedItem1.addEventListener("selection-change", function(e) { + blockedItem1.addEventListener("click", function(e) { blockedEventLog.value = "Item 1 clicked - selection allowed"; }); - blockedItem2.addEventListener("selection-change", function(e) { + blockedItem2.addEventListener("click", function(e) { e.preventDefault(); // Block selection for this item blockedEventLog.value = "Item 2 clicked - SELECTION BLOCKED!"; }); - blockedItem3.addEventListener("selection-change", function(e) { + blockedItem3.addEventListener("click", function(e) { blockedEventLog.value = "Item 3 clicked - selection allowed"; }); From dc744d3a042dcb57022931c1382937836643044c Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Fri, 17 Apr 2026 12:09:03 +0300 Subject: [PATCH 4/6] test: add tests --- .../main/cypress/specs/SegmentedButton.cy.tsx | 1412 +++++++++-------- packages/main/src/SegmentedButton.ts | 5 +- packages/main/src/SegmentedButtonItem.ts | 36 +- 3 files changed, 813 insertions(+), 640 deletions(-) diff --git a/packages/main/cypress/specs/SegmentedButton.cy.tsx b/packages/main/cypress/specs/SegmentedButton.cy.tsx index 19d80d450cd2..0a935fbcce14 100644 --- a/packages/main/cypress/specs/SegmentedButton.cy.tsx +++ b/packages/main/cypress/specs/SegmentedButton.cy.tsx @@ -5,568 +5,568 @@ import type UI5Element from "@ui5/webcomponents-base"; import { SEGMENTEDBUTTON_ARIA_DESCRIBEDBY } from "../../src/generated/i18n/i18n-defaults.js"; import { Key } from "@ui5/webcomponents-base/dist/thirdparty/preact/preact.module.js"; -// function testSelectItem(shouldPreventSelect: boolean, pressedKeys: Key[]) { -// cy.mount( -// -// First -// Second -// -// ); - -// cy.get("[ui5-segmented-button]") -// .as("segmentedButton"); - -// cy.get("@segmentedButton") -// .find("[ui5-segmented-button-item]") -// .as("items"); - -// cy.get("@items") -// .ui5SegmentedButtonFocusFirstItem(); - -// cy.realPress("ArrowRight"); - -// cy.get("@items") -// .eq(1) -// .as("secondItem"); - -// cy.get("@secondItem") -// .should("be.focused"); - -// cy.get("@secondItem") -// .should("not.have.attr", "selected"); - -// cy.realPress(pressedKeys); - -// cy.get("@secondItem") -// .should(shouldPreventSelect ? "not.have.attr" : "have.attr", "selected"); -// } - -// describe("SegmentedButton general interaction tests", () => { -// it("should have first item selected by default", () => { -// cy.mount( -// -// First -// Second -// -// ); - -// cy.get("[ui5-segmented-button]") -// .as("segmentedButton"); - -// cy.get("@segmentedButton") -// .find("[ui5-segmented-button-item]") -// .first() -// .should("have.attr", "selected"); -// }); - -// it("should select second item with enter", () => { -// testSelectItem(false, ["Enter"]); -// }); - -// it("should select second item with space", () => { -// testSelectItem(false, ["Space"]); -// }); - -// it("should not select second item on space when shift is pressed", () => { -// testSelectItem(true, ["Space", "Shift"]); -// }); - -// it("should not select second item on space when escape is pressed", () => { -// testSelectItem(true, ["Space", "Escape"]); -// }); - -// it("should select last item with mouse", () => { -// cy.mount( -// -// First -// Second -// Third -// -// ); - -// cy.get("[ui5-segmented-button]") -// .as("segmentedButton"); - -// cy.get("@segmentedButton") -// .find("[ui5-segmented-button-item]") -// .as("items"); - -// cy.get("@items") -// .last() -// .as("lastItem"); - -// cy.get("@lastItem") -// .ui5SegmentedButtonItemToggleSelect(); -// }); - -// it("should be able to select multple items in multiple selection mode", () => { -// cy.mount( -// -// First -// Second -// -// ); - -// cy.get("[ui5-segmented-button]") -// .as("segmentedButton"); - -// cy.get("@segmentedButton") -// .find("[ui5-segmented-button-item]") -// .as("items"); - -// cy.get("@items") -// .first() -// .as("firstItem"); - -// cy.get("@items") -// .last() -// .as("lastItem"); - -// // Select second item -// cy.get("@lastItem") -// .ui5SegmentedButtonItemToggleSelect(); - -// // First item should still be selected -// cy.get("@firstItem") -// .should("have.attr", "selected"); -// }); - -// it("should be able to deselect items in multiple selection mode", () => { -// cy.mount( -// -// First -// Second -// -// ); - -// cy.get("[ui5-segmented-button]") -// .as("segmentedButton"); - -// cy.get("@segmentedButton") -// .find("[ui5-segmented-button-item]") -// .as("items"); - -// cy.get("@items") -// .first() -// .as("firstItem"); - -// cy.get("@items") -// .last() -// .as("lastItem"); - -// const deselect = true; -// // Deselect first item -// cy.get("@firstItem") -// .ui5SegmentedButtonItemToggleSelect(deselect); - -// // Second item is still selected -// cy.get("@lastItem") -// .should("have.attr", "selected"); -// }); -// }); - -// describe("SegmentedButton - getFocusDomRef", () => { -// it("should return undefined when the SegmentedButton is empty", () => { -// cy.mount(); - -// cy.get("[ui5-segmented-button]") -// .then(($el) => { -// expect($el[0].getFocusDomRef()).to.be.undefined; -// }); -// }); - -// it("should return first item if no item was focused before", () => { -// cy.mount( -// -// -// -// -// ); - -// cy.get("[ui5-segmented-button], #button1") -// .then(($el) => { -// const wrapper = $el[0], -// firstButton = $el[1]; - -// expect(wrapper.getFocusDomRef()).to.equal(firstButton.getFocusDomRef()); -// }); -// }); - -// it("should return last focused item in the SegmentedButton", () => { -// cy.mount( -// -// -// -// -// ); - -// cy.get('#button2').realClick(); -// cy.get('#button2').should("be.focused"); - -// cy.get("[ui5-segmented-button], #button2") -// .then(($el) => { -// const wrapper = $el[0], -// secondButton = $el[1]; - -// expect(wrapper.getFocusDomRef()).to.equal(secondButton.getFocusDomRef()); -// }); -// }); -// }); - -// describe("SegmentedButtonItems appearance", () => { -// it("should not render items with hidden property and set correct aria-posinset and aria-setsize", () => { -// cy.mount( -// -// First -// -// Third -// -// ); - -// cy.get("[ui5-segmented-button]") -// .as("segmentedButton"); - -// cy.get("@segmentedButton") -// .find("[ui5-segmented-button-item]:not([hidden])") -// .as("visibleItems"); - -// cy.get("@segmentedButton") -// .find("[ui5-segmented-button-item][hidden]") -// .as("hiddenItems"); - -// // Only visible items should be counted by SegmentedButton -// cy.get("@segmentedButton") -// .invoke("attr", "style") -// .should("satisfy", style => style.slice(-9) === "count: 2;"); - -// // Assert aria-posinset and aria-setsize for visible items -// cy.get("@visibleItems") -// .shadow() -// .find("li") -// .each(($el, index, $list) => { -// cy.wrap($el).should("have.attr", "aria-posinset", `${index + 1}`); -// cy.wrap($el).should("have.attr", "aria-setsize", `${$list.length}`); -// }); - -// // Assert hidden item does not have aria-posinset and aria-setsize -// cy.get("@hiddenItems") -// .shadow() -// .find("li") -// .each(($el) => { -// cy.wrap($el).should("not.have.attr", "aria-posinset"); -// cy.wrap($el).should("not.have.attr", "aria-setsize"); -// }); -// }); -// }); - -// describe("SegmentedButton: fitContent", () => { -// it("should have items with width which fits item content when itemsFitContent is true", () => { -// cy.mount( -// -// Short -// Much longer text -// Medium -// -// ); - -// cy.get("#item1") -// .invoke("outerWidth") -// .then(shortWidth => { -// cy.get("#item2") -// .invoke("outerWidth") -// .should("be.gt", shortWidth); -// cy.get("#item3") -// .invoke("outerWidth") -// .should("be.gt", shortWidth); -// }); -// }); - -// it("should have items with equal width when itemsFitContent is false (default)", () => { -// cy.mount( -// -// Short -// Much longer text -// Medium -// -// ); - -// cy.get("#item1") -// .invoke("outerWidth") -// .then(width1 => { -// cy.get("#item2") -// .invoke("outerWidth") -// .should("eq", width1); -// cy.get("#item3") -// .invoke("outerWidth") -// .should("eq", width1); -// }); -// }); -// }); - -// describe("SegmentedButton Accessibility", () => { -// it("segmented button should have correct aria label when accessibleName is set", () => { -// const LABEL = "Label"; -// cy.mount( -// -// First -// Second -// -// ); - -// cy.get("[ui5-segmented-button]") -// .shadow() -// .find(".ui5-segmented-button-root") -// .should("have.attr", "aria-label", LABEL); -// }); - -// it("segmented button should have correct aria label when external label is set", () => { -// const LABEL = "External label"; -// cy.mount( -// <> -// -// -// First -// Second -// -// -// ); - -// cy.get("[ui5-segmented-button]") -// .shadow() -// .find(".ui5-segmented-button-root") -// .should("have.attr", "aria-label", LABEL); -// }); - -// it("segmented button should have correct aria label when accessibleNameRef is set", () => { -// const LABEL = "External label"; -// cy.mount( -// <> -//

{LABEL}

-// -// First -// Second -// -// -// ); - -// cy.get("[ui5-segmented-button]") -// .shadow() -// .find(".ui5-segmented-button-root") -// .should("have.attr", "aria-label", LABEL); -// }); - -// it("segmented button should have correct aria description when neither accessibleDescription nor accessibleDescriptionRef are set", () => { -// cy.mount( -// -// First -// Second -// -// ); - -// cy.get("[ui5-segmented-button]") -// .shadow() -// .find(".ui5-segmented-button-root") -// .should("have.attr", "aria-description", SegmentedButton.i18nBundle.getText(SEGMENTEDBUTTON_ARIA_DESCRIBEDBY)); -// }); - -// it("segmented button should have correct aria description when accessibleDescription is set", () => { -// const DESCRIPTION = "Description"; -// cy.mount( -// -// First -// Second -// -// ); - -// cy.get("[ui5-segmented-button]") -// .shadow() -// .find(".ui5-segmented-button-root") -// .should("have.attr", "aria-description", `${DESCRIPTION} ${SegmentedButton.i18nBundle.getText(SEGMENTEDBUTTON_ARIA_DESCRIBEDBY)}`); -// }); - -// it("segmented button should have correct aria description when accessibleDescriptionRef is set", () => { -// const DESCRIPTION = "External description"; -// cy.mount( -// <> -//

{DESCRIPTION}

-// -// First -// Second -// -// -// ); - -// cy.get("[ui5-segmented-button]") -// .shadow() -// .find(".ui5-segmented-button-root") -// .should("have.attr", "aria-description", `${DESCRIPTION} ${SegmentedButton.i18nBundle.getText(SEGMENTEDBUTTON_ARIA_DESCRIBEDBY)}`); -// }); - -// it("segmented button should have correct aria-multiselectable", () => { -// cy.mount( -// <> -// -// First -// Second -// -// -// First -// Second -// -// -// ); - -// // Test Single mode (default) should have aria-multiselectable="false" -// cy.get("[ui5-segmented-button]") -// .first() -// .shadow() -// .find("ul[role='listbox']") -// .should("have.attr", "aria-multiselectable", "false") -// .should("have.attr", "aria-orientation", "horizontal"); - -// // Test Multiple mode should have aria-multiselectable="true" -// cy.get("[ui5-segmented-button]") -// .last() -// .shadow() -// .find("ul[role='listbox']") -// .should("have.attr", "aria-multiselectable", "true") -// .should("have.attr", "aria-orientation", "horizontal"); -// }); +function testSelectItem(shouldPreventSelect: boolean, pressedKeys: Key[]) { + cy.mount( + + First + Second + + ); + + cy.get("[ui5-segmented-button]") + .as("segmentedButton"); + + cy.get("@segmentedButton") + .find("[ui5-segmented-button-item]") + .as("items"); + + cy.get("@items") + .ui5SegmentedButtonFocusFirstItem(); + + cy.realPress("ArrowRight"); + + cy.get("@items") + .eq(1) + .as("secondItem"); + + cy.get("@secondItem") + .should("be.focused"); + + cy.get("@secondItem") + .should("not.have.attr", "selected"); + + cy.realPress(pressedKeys); + + cy.get("@secondItem") + .should(shouldPreventSelect ? "not.have.attr" : "have.attr", "selected"); +} + +describe("SegmentedButton general interaction tests", () => { + it("should have first item selected by default", () => { + cy.mount( + + First + Second + + ); + + cy.get("[ui5-segmented-button]") + .as("segmentedButton"); + + cy.get("@segmentedButton") + .find("[ui5-segmented-button-item]") + .first() + .should("have.attr", "selected"); + }); + + it("should select second item with enter", () => { + testSelectItem(false, ["Enter"]); + }); + + it("should select second item with space", () => { + testSelectItem(false, ["Space"]); + }); + + it("should not select second item on space when shift is pressed", () => { + testSelectItem(true, ["Space", "Shift"]); + }); + + it("should not select second item on space when escape is pressed", () => { + testSelectItem(true, ["Space", "Escape"]); + }); + + it("should select last item with mouse", () => { + cy.mount( + + First + Second + Third + + ); + + cy.get("[ui5-segmented-button]") + .as("segmentedButton"); + + cy.get("@segmentedButton") + .find("[ui5-segmented-button-item]") + .as("items"); + + cy.get("@items") + .last() + .as("lastItem"); + + cy.get("@lastItem") + .ui5SegmentedButtonItemToggleSelect(); + }); + + it("should be able to select multple items in multiple selection mode", () => { + cy.mount( + + First + Second + + ); + + cy.get("[ui5-segmented-button]") + .as("segmentedButton"); + + cy.get("@segmentedButton") + .find("[ui5-segmented-button-item]") + .as("items"); + + cy.get("@items") + .first() + .as("firstItem"); + + cy.get("@items") + .last() + .as("lastItem"); + + // Select second item + cy.get("@lastItem") + .ui5SegmentedButtonItemToggleSelect(); + + // First item should still be selected + cy.get("@firstItem") + .should("have.attr", "selected"); + }); + + it("should be able to deselect items in multiple selection mode", () => { + cy.mount( + + First + Second + + ); + + cy.get("[ui5-segmented-button]") + .as("segmentedButton"); + + cy.get("@segmentedButton") + .find("[ui5-segmented-button-item]") + .as("items"); + + cy.get("@items") + .first() + .as("firstItem"); + + cy.get("@items") + .last() + .as("lastItem"); + + const deselect = true; + // Deselect first item + cy.get("@firstItem") + .ui5SegmentedButtonItemToggleSelect(deselect); + + // Second item is still selected + cy.get("@lastItem") + .should("have.attr", "selected"); + }); +}); + +describe("SegmentedButton - getFocusDomRef", () => { + it("should return undefined when the SegmentedButton is empty", () => { + cy.mount(); + + cy.get("[ui5-segmented-button]") + .then(($el) => { + expect($el[0].getFocusDomRef()).to.be.undefined; + }); + }); + + it("should return first item if no item was focused before", () => { + cy.mount( + + + + + ); + + cy.get("[ui5-segmented-button], #button1") + .then(($el) => { + const wrapper = $el[0], + firstButton = $el[1]; + + expect(wrapper.getFocusDomRef()).to.equal(firstButton.getFocusDomRef()); + }); + }); + + it("should return last focused item in the SegmentedButton", () => { + cy.mount( + + + + + ); + + cy.get('#button2').realClick(); + cy.get('#button2').should("be.focused"); + + cy.get("[ui5-segmented-button], #button2") + .then(($el) => { + const wrapper = $el[0], + secondButton = $el[1]; + + expect(wrapper.getFocusDomRef()).to.equal(secondButton.getFocusDomRef()); + }); + }); +}); + +describe("SegmentedButtonItems appearance", () => { + it("should not render items with hidden property and set correct aria-posinset and aria-setsize", () => { + cy.mount( + + First + + Third + + ); + + cy.get("[ui5-segmented-button]") + .as("segmentedButton"); + + cy.get("@segmentedButton") + .find("[ui5-segmented-button-item]:not([hidden])") + .as("visibleItems"); + + cy.get("@segmentedButton") + .find("[ui5-segmented-button-item][hidden]") + .as("hiddenItems"); + + // Only visible items should be counted by SegmentedButton + cy.get("@segmentedButton") + .invoke("attr", "style") + .should("satisfy", style => style.slice(-9) === "count: 2;"); + + // Assert aria-posinset and aria-setsize for visible items + cy.get("@visibleItems") + .shadow() + .find("li") + .each(($el, index, $list) => { + cy.wrap($el).should("have.attr", "aria-posinset", `${index + 1}`); + cy.wrap($el).should("have.attr", "aria-setsize", `${$list.length}`); + }); + + // Assert hidden item does not have aria-posinset and aria-setsize + cy.get("@hiddenItems") + .shadow() + .find("li") + .each(($el) => { + cy.wrap($el).should("not.have.attr", "aria-posinset"); + cy.wrap($el).should("not.have.attr", "aria-setsize"); + }); + }); +}); + +describe("SegmentedButton: fitContent", () => { + it("should have items with width which fits item content when itemsFitContent is true", () => { + cy.mount( + + Short + Much longer text + Medium + + ); + + cy.get("#item1") + .invoke("outerWidth") + .then(shortWidth => { + cy.get("#item2") + .invoke("outerWidth") + .should("be.gt", shortWidth); + cy.get("#item3") + .invoke("outerWidth") + .should("be.gt", shortWidth); + }); + }); + + it("should have items with equal width when itemsFitContent is false (default)", () => { + cy.mount( + + Short + Much longer text + Medium + + ); + + cy.get("#item1") + .invoke("outerWidth") + .then(width1 => { + cy.get("#item2") + .invoke("outerWidth") + .should("eq", width1); + cy.get("#item3") + .invoke("outerWidth") + .should("eq", width1); + }); + }); +}); + +describe("SegmentedButton Accessibility", () => { + it("segmented button should have correct aria label when accessibleName is set", () => { + const LABEL = "Label"; + cy.mount( + + First + Second + + ); + + cy.get("[ui5-segmented-button]") + .shadow() + .find(".ui5-segmented-button-root") + .should("have.attr", "aria-label", LABEL); + }); + + it("segmented button should have correct aria label when external label is set", () => { + const LABEL = "External label"; + cy.mount( + <> + + + First + Second + + + ); + + cy.get("[ui5-segmented-button]") + .shadow() + .find(".ui5-segmented-button-root") + .should("have.attr", "aria-label", LABEL); + }); + + it("segmented button should have correct aria label when accessibleNameRef is set", () => { + const LABEL = "External label"; + cy.mount( + <> +

{LABEL}

+ + First + Second + + + ); + + cy.get("[ui5-segmented-button]") + .shadow() + .find(".ui5-segmented-button-root") + .should("have.attr", "aria-label", LABEL); + }); + + it("segmented button should have correct aria description when neither accessibleDescription nor accessibleDescriptionRef are set", () => { + cy.mount( + + First + Second + + ); + + cy.get("[ui5-segmented-button]") + .shadow() + .find(".ui5-segmented-button-root") + .should("have.attr", "aria-description", SegmentedButton.i18nBundle.getText(SEGMENTEDBUTTON_ARIA_DESCRIBEDBY)); + }); + + it("segmented button should have correct aria description when accessibleDescription is set", () => { + const DESCRIPTION = "Description"; + cy.mount( + + First + Second + + ); + + cy.get("[ui5-segmented-button]") + .shadow() + .find(".ui5-segmented-button-root") + .should("have.attr", "aria-description", `${DESCRIPTION} ${SegmentedButton.i18nBundle.getText(SEGMENTEDBUTTON_ARIA_DESCRIBEDBY)}`); + }); + + it("segmented button should have correct aria description when accessibleDescriptionRef is set", () => { + const DESCRIPTION = "External description"; + cy.mount( + <> +

{DESCRIPTION}

+ + First + Second + + + ); + + cy.get("[ui5-segmented-button]") + .shadow() + .find(".ui5-segmented-button-root") + .should("have.attr", "aria-description", `${DESCRIPTION} ${SegmentedButton.i18nBundle.getText(SEGMENTEDBUTTON_ARIA_DESCRIBEDBY)}`); + }); + + it("segmented button should have correct aria-multiselectable", () => { + cy.mount( + <> + + First + Second + + + First + Second + + + ); + + // Test Single mode (default) should have aria-multiselectable="false" + cy.get("[ui5-segmented-button]") + .first() + .shadow() + .find("ul[role='listbox']") + .should("have.attr", "aria-multiselectable", "false") + .should("have.attr", "aria-orientation", "horizontal"); + + // Test Multiple mode should have aria-multiselectable="true" + cy.get("[ui5-segmented-button]") + .last() + .shadow() + .find("ul[role='listbox']") + .should("have.attr", "aria-multiselectable", "true") + .should("have.attr", "aria-orientation", "horizontal"); + }); -// }); - - -// describe("SegmentedButtonItem Accessibility", () => { -// it("segmented button items should have correct aria labels", () => { -// const LABEL = "Text Label"; -// const REF_LABEL = "Ref Label"; -// const FOR_LABEL = "For Label"; -// cy.mount( -// <> -// -// -// First -// Second -// Third -// -// {REF_LABEL} -// -// ); - -// cy.get("[ui5-segmented-button]") -// .as("segmentedButton"); - -// cy.get("@segmentedButton") -// .find("[ui5-segmented-button-item]") -// .as("items"); - -// cy.get("@items") -// .eq(0) -// .shadow() -// .find("li") -// .should("have.attr", "aria-label", LABEL); - -// cy.get("@items") -// .eq(1) -// .shadow() -// .find("li") -// .should("have.attr", "aria-label", REF_LABEL); - -// cy.get("@items") -// .eq(2) -// .shadow() -// .find("li") -// .should("have.attr", "aria-label", FOR_LABEL); -// }); - -// it("segmented button item should have correct aria descriptions", () => { -// const DESCRIPTION = "Text Description"; -// const REF_DESCRIPTION = "Ref Description"; -// cy.mount( -// <> -//

{REF_DESCRIPTION}

-// -// First -// Second -// -// -// ); - -// cy.get("[ui5-segmented-button]") -// .as("segmentedButton"); - -// cy.get("@segmentedButton") -// .find("[ui5-segmented-button-item]") -// .as("items"); - -// cy.get("@items") -// .eq(0) -// .shadow() -// .find("li") -// .should("have.attr", "aria-description", DESCRIPTION); - -// cy.get("@items") -// .eq(1) -// .shadow() -// .find("li") -// .should("have.attr", "aria-description", REF_DESCRIPTION); -// }); - -// it("should set title attribute to slot text when tooltip is not provided", () => { -// cy.mount( -// -// Segmented Item Text -// -// ); - -// cy.get("#item1") -// .shadow() -// .find("li") -// .should("have.attr", "title", "Segmented Item Text"); -// }); - -// it("should show tooltip for disabled items", () => { -// const TOOLTIP_TEXT = "Disabled item tooltip"; -// cy.mount( -// -// Disabled Item -// Enabled Item -// -// ); - -// // Check that disabled item has tooltip -// cy.get("[ui5-segmented-button-item][disabled]") -// .shadow() -// .find("li") -// .should("have.attr", "title", TOOLTIP_TEXT); -// }); - -// it("should show tooltip on hover for disabled items", () => { -// const TOOLTIP_TEXT = "This is a disabled item"; -// cy.mount( -// -// -// Disabled -// -// -// ); - -// // Verify the disabled item has the correct title attribute -// cy.get("#disabledItem") -// .shadow() -// .find("li") -// .should("have.attr", "title", TOOLTIP_TEXT) -// .should("have.attr", "aria-disabled", "true"); - -// // Test that hovering shows tooltip (this is browser behavior, but we can verify the title is present) -// cy.get("#disabledItem") -// .shadow() -// .find("li") -// .trigger("mouseover") -// .should("have.attr", "title", TOOLTIP_TEXT); -// }); -// }); +}); + + +describe("SegmentedButtonItem Accessibility", () => { + it("segmented button items should have correct aria labels", () => { + const LABEL = "Text Label"; + const REF_LABEL = "Ref Label"; + const FOR_LABEL = "For Label"; + cy.mount( + <> + + + First + Second + Third + + {REF_LABEL} + + ); + + cy.get("[ui5-segmented-button]") + .as("segmentedButton"); + + cy.get("@segmentedButton") + .find("[ui5-segmented-button-item]") + .as("items"); + + cy.get("@items") + .eq(0) + .shadow() + .find("li") + .should("have.attr", "aria-label", LABEL); + + cy.get("@items") + .eq(1) + .shadow() + .find("li") + .should("have.attr", "aria-label", REF_LABEL); + + cy.get("@items") + .eq(2) + .shadow() + .find("li") + .should("have.attr", "aria-label", FOR_LABEL); + }); + + it("segmented button item should have correct aria descriptions", () => { + const DESCRIPTION = "Text Description"; + const REF_DESCRIPTION = "Ref Description"; + cy.mount( + <> +

{REF_DESCRIPTION}

+ + First + Second + + + ); + + cy.get("[ui5-segmented-button]") + .as("segmentedButton"); + + cy.get("@segmentedButton") + .find("[ui5-segmented-button-item]") + .as("items"); + + cy.get("@items") + .eq(0) + .shadow() + .find("li") + .should("have.attr", "aria-description", DESCRIPTION); + + cy.get("@items") + .eq(1) + .shadow() + .find("li") + .should("have.attr", "aria-description", REF_DESCRIPTION); + }); + + it("should set title attribute to slot text when tooltip is not provided", () => { + cy.mount( + + Segmented Item Text + + ); + + cy.get("#item1") + .shadow() + .find("li") + .should("have.attr", "title", "Segmented Item Text"); + }); + + it("should show tooltip for disabled items", () => { + const TOOLTIP_TEXT = "Disabled item tooltip"; + cy.mount( + + Disabled Item + Enabled Item + + ); + + // Check that disabled item has tooltip + cy.get("[ui5-segmented-button-item][disabled]") + .shadow() + .find("li") + .should("have.attr", "title", TOOLTIP_TEXT); + }); + + it("should show tooltip on hover for disabled items", () => { + const TOOLTIP_TEXT = "This is a disabled item"; + cy.mount( + + + Disabled + + + ); + + // Verify the disabled item has the correct title attribute + cy.get("#disabledItem") + .shadow() + .find("li") + .should("have.attr", "title", TOOLTIP_TEXT) + .should("have.attr", "aria-disabled", "true"); + + // Test that hovering shows tooltip (this is browser behavior, but we can verify the title is present) + cy.get("#disabledItem") + .shadow() + .find("li") + .trigger("mouseover") + .should("have.attr", "title", TOOLTIP_TEXT); + }); +}); describe("SegmentedButtonItem: click event", () => { it("should fire selection change event when item is clicked", () => { @@ -600,68 +600,236 @@ describe("SegmentedButtonItem: click event", () => { .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"); + 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"); - // }); + 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 correct modifier keys 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 modifier keys + expect(e.detail).to.have.property("altKey"); + expect(e.detail).to.have.property("ctrlKey"); + expect(e.detail).to.have.property("metaKey"); + expect(e.detail).to.have.property("shiftKey"); + + // Initially all should be false + expect(e.detail.altKey).to.be.false; + expect(e.detail.ctrlKey).to.be.false; + expect(e.detail.metaKey).to.be.false; + expect(e.detail.shiftKey).to.be.false; + }).as("clickSpy")); + }); + + cy.get("#item2") + .realClick(); + + cy.get("@clickSpy") + .should("have.been.calledOnce"); + }); + + it("should provide correct modifier keys when Ctrl is pressed", () => { + let eventDetail: any; + + cy.mount( + + First + Second + + ); + + cy.get("#item2") + .then($el => { + $el[0].addEventListener("click", (e: Event) => { + eventDetail = (e as CustomEvent).detail; + }); + }); + + // Manually dispatch a MouseEvent with ctrlKey + cy.get("#item2") + .shadow() + .find("li") + .then($li => { + const mouseEvent = new MouseEvent('click', { + bubbles: true, + cancelable: true, + ctrlKey: true, + altKey: false, + metaKey: false, + shiftKey: false + }); + $li[0].dispatchEvent(mouseEvent); + }); + + cy.then(() => eventDetail) + .then((detail) => { + expect(detail, "event detail should exist").to.exist; + expect(detail.ctrlKey, "ctrlKey should be true").to.be.true; + expect(detail.altKey, "altKey should be false").to.be.false; + expect(detail.metaKey, "metaKey should be false").to.be.false; + expect(detail.shiftKey, "shiftKey should be false").to.be.false; + }); + }); + + it("should provide correct modifier keys when Alt is pressed", () => { + cy.mount( + + First + Second + + ); + + cy.get("#item2") + .then($el => { + $el[0].addEventListener("click", cy.spy((e: CustomEvent) => { + expect(e.detail.altKey).to.be.true; + expect(e.detail.ctrlKey).to.be.false; + expect(e.detail.metaKey).to.be.false; + expect(e.detail.shiftKey).to.be.false; + }).as("clickSpyAlt")); + }); + + cy.get("#item2") + .realClick({ altKey: true }); + + cy.get("@clickSpyAlt") + .should("have.been.calledOnce"); + }); + + it("should provide correct modifier keys when Shift is pressed", () => { + cy.mount( + + First + Second + + ); + + cy.get("#item2") + .then($el => { + $el[0].addEventListener("click", cy.spy((e: CustomEvent) => { + expect(e.detail.shiftKey).to.be.true; + expect(e.detail.altKey).to.be.false; + expect(e.detail.ctrlKey).to.be.false; + expect(e.detail.metaKey).to.be.false; + }).as("clickSpyShift")); + }); + + cy.get("#item2") + .realClick({ shiftKey: true }); + + cy.get("@clickSpyShift") + .should("have.been.calledOnce"); + }); + + it("should provide correct modifier keys when multiple modifiers are pressed", () => { + let eventDetail: any; + + cy.mount( + + First + Second + + ); + + cy.get("#item2") + .then($el => { + $el[0].addEventListener("click", (e: Event) => { + eventDetail = (e as CustomEvent).detail; + }); + }); + + // Manually dispatch a MouseEvent with multiple modifier keys + cy.get("#item2") + .shadow() + .find("li") + .then($li => { + const mouseEvent = new MouseEvent('click', { + bubbles: true, + cancelable: true, + ctrlKey: true, + altKey: false, + metaKey: false, + shiftKey: true + }); + $li[0].dispatchEvent(mouseEvent); + }); + + cy.then(() => eventDetail) + .then((detail) => { + expect(detail, "event detail should exist").to.exist; + expect(detail.ctrlKey, "ctrlKey should be true").to.be.true; + expect(detail.shiftKey, "shiftKey should be true").to.be.true; + expect(detail.altKey, "altKey should be false").to.be.false; + expect(detail.metaKey, "metaKey should be false").to.be.false; + }); + }); }); \ No newline at end of file diff --git a/packages/main/src/SegmentedButton.ts b/packages/main/src/SegmentedButton.ts index 4ce03ca9e052..35e0c629d566 100644 --- a/packages/main/src/SegmentedButton.ts +++ b/packages/main/src/SegmentedButton.ts @@ -230,9 +230,7 @@ class SegmentedButton extends UI5Element { switch (this.selectionMode) { case SegmentedButtonSelectionMode.Multiple: - if (e instanceof KeyboardEvent) { - target.selected = !target.selected; - } + target.selected = !target.selected; break; default: this._applySingleSelection(target as unknown as ISegmentedButtonItem); @@ -241,7 +239,6 @@ class SegmentedButton extends UI5Element { this.fireDecoratorEvent("selection-change", { selectedItems: this.selectedItems, }); - console.log("parent click"); this._itemNavigation.setCurrentItem(target); diff --git a/packages/main/src/SegmentedButtonItem.ts b/packages/main/src/SegmentedButtonItem.ts index e61bea548772..13286e1a4350 100644 --- a/packages/main/src/SegmentedButtonItem.ts +++ b/packages/main/src/SegmentedButtonItem.ts @@ -24,8 +24,10 @@ import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; import segmentedButtonItemCss from "./generated/themes/SegmentedButtonItem.css.js"; type SegmentedButtonItemClickEventDetail = { - originalEvent: Event; - item: SegmentedButtonItem; + altKey: boolean; + ctrlKey: boolean; + metaKey: boolean; + shiftKey: boolean; }; /** @@ -61,10 +63,12 @@ type SegmentedButtonItemClickEventDetail = { * * **Note:** The event will not be fired if the `disabled` property is set to `true`. * + * @param {boolean} altKey Returns whether the "ALT" key was pressed when the event was triggered. + * @param {boolean} ctrlKey Returns whether the "CTRL" key was pressed when the event was triggered. + * @param {boolean} metaKey Returns whether the "META" key was pressed when the event was triggered. + * @param {boolean} shiftKey Returns whether the "SHIFT" key was pressed when the event was triggered. * @since 2.22.0 * @public - * @param {MouseEvent} originalEvent The original mouse event that triggered the selection - * @param {SegmentedButtonItem} item The segmented button item that was clicked */ @event("click", { bubbles: true, @@ -221,24 +225,28 @@ class SegmentedButtonItem extends UI5Element implements IButton, ISegmentedButto e.stopPropagation(); return; } - console.log("item click"); + + e.stopImmediatePropagation(); + + const { + altKey, + ctrlKey, + metaKey, + shiftKey, + } = e; + // Fire semantic click event (CustomEvent that bubbles) const prevented = !this.fireDecoratorEvent("click", { - originalEvent: e, - item: this, + altKey, + ctrlKey, + metaKey, + shiftKey, }); - console.log("item click event fired, prevented:", prevented); - if (prevented) { e.preventDefault(); e.stopPropagation(); - return; } - - - - this.selected = !this.selected; } onEnterDOM() { From 1260e28e16b53df57b42a16f97240a6d30b9d5d5 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Mon, 20 Apr 2026 12:30:16 +0300 Subject: [PATCH 5/6] fix: add casting --- packages/website/docs/_samples/main/Table/Popin/sample.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ( From 73fa116f3c88d9dec68556dd0fba4a3f1c1e0342 Mon Sep 17 00:00:00 2001 From: Georgi Damyanov Date: Thu, 23 Apr 2026 15:41:31 +0300 Subject: [PATCH 6/6] refactor: update event details --- .../main/cypress/specs/SegmentedButton.cy.tsx | 160 ++---------------- packages/main/src/SegmentedButtonItem.ts | 25 +-- 2 files changed, 18 insertions(+), 167 deletions(-) diff --git a/packages/main/cypress/specs/SegmentedButton.cy.tsx b/packages/main/cypress/specs/SegmentedButton.cy.tsx index 0a935fbcce14..5abad9e2da3b 100644 --- a/packages/main/cypress/specs/SegmentedButton.cy.tsx +++ b/packages/main/cypress/specs/SegmentedButton.cy.tsx @@ -664,7 +664,7 @@ describe("SegmentedButtonItem: click event", () => { cy.get("#item2").should("not.have.attr", "selected"); }); - it("should provide correct modifier keys in click event detail", () => { + it("should provide item and originalEvent in click event detail", () => { cy.mount( First @@ -675,17 +675,17 @@ describe("SegmentedButtonItem: click event", () => { cy.get("#item2") .then($el => { $el[0].addEventListener("click", cy.spy((e: CustomEvent) => { - // Check that event detail contains modifier keys - expect(e.detail).to.have.property("altKey"); - expect(e.detail).to.have.property("ctrlKey"); - expect(e.detail).to.have.property("metaKey"); - expect(e.detail).to.have.property("shiftKey"); - - // Initially all should be false - expect(e.detail.altKey).to.be.false; - expect(e.detail.ctrlKey).to.be.false; - expect(e.detail.metaKey).to.be.false; - expect(e.detail.shiftKey).to.be.false; + // 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")); }); @@ -695,141 +695,5 @@ describe("SegmentedButtonItem: click event", () => { cy.get("@clickSpy") .should("have.been.calledOnce"); }); - - it("should provide correct modifier keys when Ctrl is pressed", () => { - let eventDetail: any; - - cy.mount( - - First - Second - - ); - - cy.get("#item2") - .then($el => { - $el[0].addEventListener("click", (e: Event) => { - eventDetail = (e as CustomEvent).detail; - }); - }); - - // Manually dispatch a MouseEvent with ctrlKey - cy.get("#item2") - .shadow() - .find("li") - .then($li => { - const mouseEvent = new MouseEvent('click', { - bubbles: true, - cancelable: true, - ctrlKey: true, - altKey: false, - metaKey: false, - shiftKey: false - }); - $li[0].dispatchEvent(mouseEvent); - }); - - cy.then(() => eventDetail) - .then((detail) => { - expect(detail, "event detail should exist").to.exist; - expect(detail.ctrlKey, "ctrlKey should be true").to.be.true; - expect(detail.altKey, "altKey should be false").to.be.false; - expect(detail.metaKey, "metaKey should be false").to.be.false; - expect(detail.shiftKey, "shiftKey should be false").to.be.false; - }); - }); - - it("should provide correct modifier keys when Alt is pressed", () => { - cy.mount( - - First - Second - - ); - - cy.get("#item2") - .then($el => { - $el[0].addEventListener("click", cy.spy((e: CustomEvent) => { - expect(e.detail.altKey).to.be.true; - expect(e.detail.ctrlKey).to.be.false; - expect(e.detail.metaKey).to.be.false; - expect(e.detail.shiftKey).to.be.false; - }).as("clickSpyAlt")); - }); - - cy.get("#item2") - .realClick({ altKey: true }); - - cy.get("@clickSpyAlt") - .should("have.been.calledOnce"); - }); - - it("should provide correct modifier keys when Shift is pressed", () => { - cy.mount( - - First - Second - - ); - - cy.get("#item2") - .then($el => { - $el[0].addEventListener("click", cy.spy((e: CustomEvent) => { - expect(e.detail.shiftKey).to.be.true; - expect(e.detail.altKey).to.be.false; - expect(e.detail.ctrlKey).to.be.false; - expect(e.detail.metaKey).to.be.false; - }).as("clickSpyShift")); - }); - - cy.get("#item2") - .realClick({ shiftKey: true }); - - cy.get("@clickSpyShift") - .should("have.been.calledOnce"); - }); - - it("should provide correct modifier keys when multiple modifiers are pressed", () => { - let eventDetail: any; - - cy.mount( - - First - Second - - ); - - cy.get("#item2") - .then($el => { - $el[0].addEventListener("click", (e: Event) => { - eventDetail = (e as CustomEvent).detail; - }); - }); - - // Manually dispatch a MouseEvent with multiple modifier keys - cy.get("#item2") - .shadow() - .find("li") - .then($li => { - const mouseEvent = new MouseEvent('click', { - bubbles: true, - cancelable: true, - ctrlKey: true, - altKey: false, - metaKey: false, - shiftKey: true - }); - $li[0].dispatchEvent(mouseEvent); - }); - - cy.then(() => eventDetail) - .then((detail) => { - expect(detail, "event detail should exist").to.exist; - expect(detail.ctrlKey, "ctrlKey should be true").to.be.true; - expect(detail.shiftKey, "shiftKey should be true").to.be.true; - expect(detail.altKey, "altKey should be false").to.be.false; - expect(detail.metaKey, "metaKey should be false").to.be.false; - }); - }); }); \ No newline at end of file diff --git a/packages/main/src/SegmentedButtonItem.ts b/packages/main/src/SegmentedButtonItem.ts index 13286e1a4350..c4b4ba0f7211 100644 --- a/packages/main/src/SegmentedButtonItem.ts +++ b/packages/main/src/SegmentedButtonItem.ts @@ -24,10 +24,8 @@ import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; import segmentedButtonItemCss from "./generated/themes/SegmentedButtonItem.css.js"; type SegmentedButtonItemClickEventDetail = { - altKey: boolean; - ctrlKey: boolean; - metaKey: boolean; - shiftKey: boolean; + item: SegmentedButtonItem, + originalEvent: Event, }; /** @@ -63,10 +61,8 @@ type SegmentedButtonItemClickEventDetail = { * * **Note:** The event will not be fired if the `disabled` property is set to `true`. * - * @param {boolean} altKey Returns whether the "ALT" key was pressed when the event was triggered. - * @param {boolean} ctrlKey Returns whether the "CTRL" key was pressed when the event was triggered. - * @param {boolean} metaKey Returns whether the "META" key was pressed when the event was triggered. - * @param {boolean} shiftKey Returns whether the "SHIFT" key was pressed when the event was triggered. + * @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 */ @@ -228,19 +224,10 @@ class SegmentedButtonItem extends UI5Element implements IButton, ISegmentedButto e.stopImmediatePropagation(); - const { - altKey, - ctrlKey, - metaKey, - shiftKey, - } = e; - // Fire semantic click event (CustomEvent that bubbles) const prevented = !this.fireDecoratorEvent("click", { - altKey, - ctrlKey, - metaKey, - shiftKey, + item: this, + originalEvent: e, }); if (prevented) {