diff --git a/packages/main/cypress/specs/ColorPalette.cy.tsx b/packages/main/cypress/specs/ColorPalette.cy.tsx index aacd8c6e1c4b..910bfd656da6 100644 --- a/packages/main/cypress/specs/ColorPalette.cy.tsx +++ b/packages/main/cypress/specs/ColorPalette.cy.tsx @@ -312,3 +312,257 @@ describe("Color Palette Item - tooltip", () => { .and("not.contain", "#d60d5a"); }); }); + +describe("Color Palette Item: click event", () => { + it("should fire item-click event when item is clicked", () => { + const clickSpy = cy.spy().as("clickSpy"); + const itemClickSpy = cy.spy().as("itemClickSpy"); + + cy.mount( + + + + + ); + + cy.get("[ui5-color-palette]") + .then($el => { + $el[0].addEventListener("item-click", itemClickSpy); + }); + + cy.get("#item2") + .then($el => { + $el[0].addEventListener("click", clickSpy); + }); + + cy.get("#item2") + .realClick(); + + cy.get("@clickSpy") + .should("have.been.calledOnce"); + + cy.get("@itemClickSpy") + .should("have.been.calledOnce"); + }); + + it("should prevent selection when preventDefault is called", () => { + cy.mount( + + + + + ); + + cy.get("[ui5-color-palette]") + .then($el => { + $el[0].addEventListener("item-click", cy.spy().as("itemClickSpy")); + }); + + // First, select item1 + cy.get("#item1") + .realClick(); + + cy.get("#item1") + .should("have.attr", "selected"); + cy.get("#item2") + .should("not.have.attr", "selected"); + + // Now add preventDefault to item2 + cy.get("#item2") + .then($el => { + $el[0].addEventListener("click", (e: Event) => { + e.preventDefault(); + }); + }); + + // Try to click item2 with preventDefault + cy.get("#item2") + .realClick(); + + // Item1 should still be selected because we called preventDefault on item2 + cy.get("#item1") + .should("have.attr", "selected"); + cy.get("#item2") + .should("not.have.attr", "selected"); + + // The item-click event on ColorPalette should only have been called once (for item1) + cy.get("@itemClickSpy") + .should("have.been.calledOnce"); + }); + + it("should provide correct modifier keys in click event detail", () => { + cy.mount( + + + + + ); + + 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 properties + expect(e.detail.item.value).to.equal("blue"); + + // Check modifier keys from originalEvent + const originalEvent = e.detail.originalEvent; + expect(originalEvent.altKey).to.be.false; + expect(originalEvent.ctrlKey).to.be.false; + expect(originalEvent.metaKey).to.be.false; + expect(originalEvent.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( + + + + + ); + + 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(".ui5-cp-item") + .then($item => { + const mouseEvent = new MouseEvent('click', { + bubbles: true, + cancelable: true, + ctrlKey: true, + altKey: false, + metaKey: false, + shiftKey: false + }); + $item[0].dispatchEvent(mouseEvent); + }); + + cy.then(() => eventDetail) + .then((detail) => { + expect(detail, "event detail should exist").to.exist; + expect(detail.item.value, "item value should be blue").to.equal("blue"); + const originalEvent = detail.originalEvent; + expect(originalEvent.ctrlKey, "ctrlKey should be true").to.be.true; + expect(originalEvent.altKey, "altKey should be false").to.be.false; + expect(originalEvent.metaKey, "metaKey should be false").to.be.false; + expect(originalEvent.shiftKey, "shiftKey should be false").to.be.false; + }); + }); + + it("should provide correct modifier keys when Alt is pressed", () => { + cy.mount( + + + + + ); + + cy.get("#item2") + .then($el => { + $el[0].addEventListener("click", cy.spy((e: CustomEvent) => { + const originalEvent = e.detail.originalEvent; + expect(originalEvent.altKey).to.be.true; + expect(originalEvent.ctrlKey).to.be.false; + expect(originalEvent.metaKey).to.be.false; + expect(originalEvent.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( + + + + + ); + + cy.get("#item2") + .then($el => { + $el[0].addEventListener("click", cy.spy((e: CustomEvent) => { + const originalEvent = e.detail.originalEvent; + expect(originalEvent.shiftKey).to.be.true; + expect(originalEvent.altKey).to.be.false; + expect(originalEvent.ctrlKey).to.be.false; + expect(originalEvent.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( + + + + + ); + + 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(".ui5-cp-item") + .then($item => { + const mouseEvent = new MouseEvent('click', { + bubbles: true, + cancelable: true, + ctrlKey: true, + altKey: false, + metaKey: false, + shiftKey: true + }); + $item[0].dispatchEvent(mouseEvent); + }); + + cy.then(() => eventDetail) + .then((detail) => { + expect(detail, "event detail should exist").to.exist; + expect(detail.item.value, "item value should be blue").to.equal("blue"); + const originalEvent = detail.originalEvent; + expect(originalEvent.ctrlKey, "ctrlKey should be true").to.be.true; + expect(originalEvent.shiftKey, "shiftKey should be true").to.be.true; + expect(originalEvent.altKey, "altKey should be false").to.be.false; + expect(originalEvent.metaKey, "metaKey should be false").to.be.false; + }); + }); +}); diff --git a/packages/main/src/ColorPalette.ts b/packages/main/src/ColorPalette.ts index 30787ef8aa1b..474a9607f710 100644 --- a/packages/main/src/ColorPalette.ts +++ b/packages/main/src/ColorPalette.ts @@ -54,7 +54,7 @@ interface IColorPaletteItem extends UI5Element, ITabbable { selected?: boolean, } -type ColorPaletteNavigationItem = IColorPaletteItem | Button; +type ColorPaletteNavigationItem = ColorPaletteItem | Button; type ColorPaletteItemClickEventDetail = { color: string, @@ -203,7 +203,7 @@ class ColorPalette extends UI5Element { invalidateOnChildChange: true, individualSlots: true, }) - colors!: DefaultSlot; + colors!: DefaultSlot; _itemNavigation: ItemNavigation; _itemNavigationRecentColors: ItemNavigation; @@ -308,14 +308,11 @@ class ColorPalette extends UI5Element { }); } - get effectiveColorItems() { - let colorItems: IColorPaletteItem[] = this.colors; - + get effectiveColorItems(): ColorPaletteItem[] { if (this.popupMode) { - colorItems = this.getSlottedNodes("colors"); + return this.getSlottedNodes("colors"); } - - return colorItems; + return this.colors; } /** @@ -323,7 +320,7 @@ class ColorPalette extends UI5Element { * @private */ _ensureSingleSelectionOrDeselectAll() { - let lastSelectedItem: IColorPaletteItem; + let lastSelectedItem: ColorPaletteItem | undefined; this.allColorsInPalette.forEach(item => { if (item.selected) { @@ -336,6 +333,10 @@ class ColorPalette extends UI5Element { } _onclick(e: MouseEvent) { + if (e.defaultPrevented) { + return; + } + this.handleSelection(e.target as ColorPaletteItem); } @@ -628,12 +629,12 @@ class ColorPalette extends UI5Element { return isDown(e) || isRight(e); } - _isFirstSwatch(target: ColorPaletteItem, swatches: Array): boolean { - return swatches && Boolean(swatches.length) && swatches[0] === target; + _isFirstSwatch(target: ColorPaletteItem, swatches: Array): boolean { + return swatches && Boolean(swatches.length) && swatches[0] === (target); } - _isLastSwatch(target: ColorPaletteItem, swatches: Array): boolean { - return swatches && Boolean(swatches.length) && swatches[swatches.length - 1] === target; + _isLastSwatch(target: ColorPaletteItem, swatches: Array): boolean { + return swatches && Boolean(swatches.length) && swatches[swatches.length - 1] === (target); } /** @@ -896,8 +897,8 @@ class ColorPalette extends UI5Element { return this._selectedColor; } - get displayedColors(): Array { - const colors = this.getSlottedNodes("colors"); + get displayedColors(): Array { + const colors = this.getSlottedNodes("colors"); return colors.filter(item => item.value).slice(0, 15); } diff --git a/packages/main/src/ColorPaletteItem.ts b/packages/main/src/ColorPaletteItem.ts index 486b727adf3b..272a5527d1fe 100644 --- a/packages/main/src/ColorPaletteItem.ts +++ b/packages/main/src/ColorPaletteItem.ts @@ -10,10 +10,16 @@ import ColorPaletteItemTemplate from "./ColorPaletteItemTemplate.js"; import { COLORPALETTE_COLOR_LABEL, } from "./generated/i18n/i18n-defaults.js"; +import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; // Styles import ColorPaletteItemCss from "./generated/themes/ColorPaletteItem.css.js"; +type ColorPaletteItemNativeClickEventDetail = { + item: ColorPaletteItem, + originalEvent: Event; +}; + /** * @class * @@ -33,7 +39,25 @@ import ColorPaletteItemCss from "./generated/themes/ColorPaletteItem.css.js"; template: ColorPaletteItemTemplate, shadowRootOptions: { delegatesFocus: true }, }) + +/** + * 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 {ColorPaletteItem} item The color palette 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 ColorPaletteItem extends UI5Element implements IColorPaletteItem { + eventDetails!: { + "click": ColorPaletteItemNativeClickEventDetail, + } /** * Defines the colour of the component. * @@ -129,8 +153,30 @@ class ColorPaletteItem extends UI5Element implements IColorPaletteItem { }, }; } + + _onClick(e: MouseEvent) { + if (this._disabled) { + e.preventDefault(); + e.stopPropagation(); + return; + } + + e.stopImmediatePropagation(); + + // Fire semantic click event (CustomEvent that bubbles) + const prevented = !this.fireDecoratorEvent("click", { + item: this, + originalEvent: e, + }); + + if (prevented) { + e.preventDefault(); + e.stopPropagation(); + } + } } ColorPaletteItem.define(); export default ColorPaletteItem; +export type { ColorPaletteItemNativeClickEventDetail }; diff --git a/packages/main/src/ColorPaletteItemTemplate.tsx b/packages/main/src/ColorPaletteItemTemplate.tsx index f8dae34c0604..f1ab9a326f9d 100644 --- a/packages/main/src/ColorPaletteItemTemplate.tsx +++ b/packages/main/src/ColorPaletteItemTemplate.tsx @@ -9,6 +9,7 @@ export default function ColorPaletteItemTemplate(this: ColorPaletteItem) { aria-label={this.getLabelText} aria-pressed={this.selected} title={this.getLabelText} + onClick={this._onClick} > ); } diff --git a/packages/main/src/ColorPalettePopover.ts b/packages/main/src/ColorPalettePopover.ts index b9a8d3042eb5..212e4fcefd74 100644 --- a/packages/main/src/ColorPalettePopover.ts +++ b/packages/main/src/ColorPalettePopover.ts @@ -204,7 +204,7 @@ class ColorPalettePopover extends UI5Element { } // since height is dynamically determined by padding-block-start - colorPalette.allColorsInPalette.forEach((item: IColorPaletteItem) => { + (colorPalette.allColorsInPalette).forEach((item: ColorPaletteItem) => { const itemHeight = item.offsetHeight + 4; // adding 4px for the offsets on top and bottom item.style.setProperty("--_ui5_color_palette_item_height", `${itemHeight}px`); }); diff --git a/packages/main/test/pages/ColorPalette.html b/packages/main/test/pages/ColorPalette.html index b961217011f9..7e83135bdd60 100644 --- a/packages/main/test/pages/ColorPalette.html +++ b/packages/main/test/pages/ColorPalette.html @@ -127,6 +127,35 @@ + +
+
+
+

ColorPaletteItem Click Event Demo

+

Sample 1: Listen to click event on individual items (with modifier keys)

+ +
+ + + + + + + + +
+
+
+

Sample 2: preventDefault on item click prevents parent's item-click event

+ +
+ + + + + +

The middle item (brown) calls preventDefault and parent's item-click will not fire

+