diff --git a/packages/main/cypress/specs/ComboBox.cy.tsx b/packages/main/cypress/specs/ComboBox.cy.tsx index 0cd2803c939b..ec75f3a15e0b 100644 --- a/packages/main/cypress/specs/ComboBox.cy.tsx +++ b/packages/main/cypress/specs/ComboBox.cy.tsx @@ -2911,6 +2911,67 @@ describe("Event firing", () => { cy.get("@selectionChangeSpy") .should("have.been.calledWith", Cypress.sinon.match.has("detail", Cypress.sinon.match.has("item"))); }); + + it("selection-change trigger is 'Typeahead' when text is auto-completed", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-combobox]") + .as("combo") + .invoke('on', 'ui5-selection-change', cy.spy().as('selectionChangeSpy')); + + cy.get("@combo").shadow().find("input").focus().realType("Bul"); + + cy.get("@selectionChangeSpy").should("have.been.calledWithMatch", Cypress.sinon.match(event => { + return event.detail.item?.text === "Bulgaria" && event.detail.trigger === "Typeahead"; + })); + }); + + it("selection-change trigger is 'Click' when an item is clicked in the dropdown", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-combobox]") + .as("combo") + .invoke('on', 'ui5-selection-change', cy.spy().as('selectionChangeSpy')); + + cy.get("@combo").shadow().find(".inputIcon").realClick(); + cy.get("@combo").find("ui5-cb-item").eq(2).realClick(); + + cy.get("@selectionChangeSpy").should("have.been.calledWithMatch", Cypress.sinon.match(event => { + return event.detail.item?.text === "Canada" && event.detail.trigger === "Click"; + })); + }); + + it("selection-change trigger is 'Keyboard' when navigating with arrow keys", () => { + cy.mount( + + + + + + ); + + cy.get("[ui5-combobox]") + .as("combo") + .invoke('on', 'ui5-selection-change', cy.spy().as('selectionChangeSpy')); + + cy.get("@combo").shadow().find("input").focus().realPress("F4").realPress("ArrowDown"); + + cy.get("@selectionChangeSpy").should("have.been.calledWithMatch", Cypress.sinon.match(event => { + return event.detail.trigger === "Keyboard"; + })); + }); }); describe("Scrolling", () => { diff --git a/packages/main/src/ComboBox.ts b/packages/main/src/ComboBox.ts index c7bc268c62c0..2eed3a0ec0c2 100644 --- a/packages/main/src/ComboBox.ts +++ b/packages/main/src/ComboBox.ts @@ -88,6 +88,7 @@ import "./ComboBoxItemGroup.js"; // eslint-disable-next-line import { isInstanceOfComboBoxItemGroup } from "./ComboBoxItemGroup.js"; import type ComboBoxFilter from "./types/ComboBoxFilter.js"; +import ComboBoxSelectionChangeTrigger from "./types/ComboBoxSelectionChangeTrigger.js"; import type Input from "./Input.js"; import type { InputEventDetail } from "./Input.js"; import type { ListItemBaseClickEventDetail } from "./ListItemBase.js"; @@ -124,6 +125,7 @@ enum ValueStateIconMapping { type ComboBoxSelectionChangeEventDetail = { item: ComboBoxItem | null, + trigger: `${ComboBoxSelectionChangeTrigger}`, }; /** @@ -241,6 +243,7 @@ type ComboBoxSelectionChangeEventDetail = { /** * Fired when selection is changed by user interaction * @param {IComboBoxItem} item item to be selected. + * @param {ComboBoxSelectionChangeTrigger} trigger source of the selection change - typeahead, click or keyboard navigation. * @public */ @event("selection-change", { @@ -506,6 +509,7 @@ class ComboBox extends UI5Element implements IFormInputElement { _autocomplete = false; _isKeyNavigation = false; _selectionPerformed = false; + _selectionTrigger?: `${ComboBoxSelectionChangeTrigger}`; _lastValue: string; _selectedItemText = ""; _userTypedValue = ""; @@ -962,6 +966,7 @@ class ComboBox extends UI5Element implements IFormInputElement { } _handleArrowDown(e: KeyboardEvent, indexOfItem: number) { + this._selectionTrigger = ComboBoxSelectionChangeTrigger.Keyboard; const isOpen = this.open; if (this.focused && indexOfItem === -1 && isOpen) { @@ -983,6 +988,7 @@ class ComboBox extends UI5Element implements IFormInputElement { } _handleArrowUp(e: KeyboardEvent, indexOfItem: number) { + this._selectionTrigger = ComboBoxSelectionChangeTrigger.Keyboard; const isOpen = this.open; if (indexOfItem === 0) { @@ -1002,6 +1008,7 @@ class ComboBox extends UI5Element implements IFormInputElement { } _handlePageUp(e: KeyboardEvent, indexOfItem: number) { + this._selectionTrigger = ComboBoxSelectionChangeTrigger.Keyboard; const allItems = this._getItems(); const isProposedIndexValid = indexOfItem - SKIP_ITEMS_SIZE > -1; indexOfItem = isProposedIndexValid ? indexOfItem - SKIP_ITEMS_SIZE : 0; @@ -1011,6 +1018,7 @@ class ComboBox extends UI5Element implements IFormInputElement { } _handlePageDown(e: KeyboardEvent, indexOfItem: number) { + this._selectionTrigger = ComboBoxSelectionChangeTrigger.Keyboard; const allItems = this._getItems(); const itemsLength = allItems.length; const isProposedIndexValid = indexOfItem + SKIP_ITEMS_SIZE < itemsLength; @@ -1022,12 +1030,14 @@ class ComboBox extends UI5Element implements IFormInputElement { } _handleHome(e: KeyboardEvent) { + this._selectionTrigger = ComboBoxSelectionChangeTrigger.Keyboard; const shouldMoveForward = isInstanceOfComboBoxItemGroup(this._filteredItems[0]) && !this.open; this._handleItemNavigation(e, 0, shouldMoveForward); } _handleEnd(e: KeyboardEvent) { + this._selectionTrigger = ComboBoxSelectionChangeTrigger.Keyboard; this._handleItemNavigation(e, this._getItems().length - 1, true /* isForward */); } @@ -1351,22 +1361,23 @@ class ComboBox extends UI5Element implements IFormInputElement { } const noUserInteraction = !this.focused && !this._isKeyNavigation && !this._selectionPerformed && !this._iconPressed; - // Skip firing "selection-change" event if this is initial rendering or if there has been no user interaction yet if (this._initialRendering || noUserInteraction) { return; } - // Fire selection-change event only when selection actually changes if (previouslySelectedItem !== itemToBeSelected) { + const trigger = this._selectionTrigger || ComboBoxSelectionChangeTrigger.Typeahead; + this._selectionTrigger = undefined; + if (itemToBeSelected) { - // New item selected this.fireDecoratorEvent("selection-change", { item: itemToBeSelected as ComboBoxItem, + trigger, }); } else if (previouslySelectedItem) { - // Selection cleared - fire event with 'null' this.fireDecoratorEvent("selection-change", { item: null, + trigger, }); } } @@ -1427,6 +1438,7 @@ class ComboBox extends UI5Element implements IFormInputElement { if (!item.selected) { this.fireDecoratorEvent("selection-change", { item, + trigger: ComboBoxSelectionChangeTrigger.Click, }); } @@ -1753,6 +1765,7 @@ class ComboBox extends UI5Element implements IFormInputElement { ComboBox.define(); export default ComboBox; +export { ComboBoxSelectionChangeTrigger }; export type { ComboBoxSelectionChangeEventDetail, diff --git a/packages/main/src/types/ComboBoxSelectionChangeTrigger.ts b/packages/main/src/types/ComboBoxSelectionChangeTrigger.ts new file mode 100644 index 000000000000..394c8d764dea --- /dev/null +++ b/packages/main/src/types/ComboBoxSelectionChangeTrigger.ts @@ -0,0 +1,28 @@ +/** + * Describes the source of a `selection-change` event fired by the `ui5-combobox`. + * @public + * @since 2.24.0 + */ +enum ComboBoxSelectionChangeTrigger { + /** + * Selection caused by typeahead (auto-complete while typing). + * @public + */ + Typeahead = "Typeahead", + + /** + * Selection caused by clicking or tapping an item in the dropdown. + * @public + */ + Click = "Click", + + /** + * Selection caused by keyboard navigation (Arrow keys, Home, End, Page Up/Down). + * + * Note: pressing Enter does not fire `selection-change` - it fires the `change` event instead. + * @public + */ + Keyboard = "Keyboard", +} + +export default ComboBoxSelectionChangeTrigger; diff --git a/packages/main/test/pages/ComboBox.html b/packages/main/test/pages/ComboBox.html index 0d2e49ee54cf..0c6afc080384 100644 --- a/packages/main/test/pages/ComboBox.html +++ b/packages/main/test/pages/ComboBox.html @@ -517,6 +517,102 @@

ComboBox Composition

+
+ Selection Trigger Flag Demo +

The selection-change event includes a trigger property: "Typeahead", "Click", or "Keyboard"

+ + + + + + + + + +
+
+
selection-change events (with trigger):
+
(none yet)
+
+
+
change events:
+
(none yet)
+
+
+ +
+ Trigger types: + Typeahead + Keyboard + Click +
+ + Clear Log + + + +
+