Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions packages/main/cypress/specs/ComboBox.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ComboBox>
<ComboBoxItem text="Argentina"></ComboBoxItem>
<ComboBoxItem text="Bulgaria"></ComboBoxItem>
<ComboBoxItem text="Canada"></ComboBoxItem>
</ComboBox>
);

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(
<ComboBox>
<ComboBoxItem text="Argentina"></ComboBoxItem>
<ComboBoxItem text="Bulgaria"></ComboBoxItem>
<ComboBoxItem text="Canada"></ComboBoxItem>
</ComboBox>
);

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(
<ComboBox>
<ComboBoxItem text="Argentina"></ComboBoxItem>
<ComboBoxItem text="Bulgaria"></ComboBoxItem>
<ComboBoxItem text="Canada"></ComboBoxItem>
</ComboBox>
);

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", () => {
Expand Down
22 changes: 18 additions & 4 deletions packages/main/src/ComboBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -124,6 +125,7 @@ enum ValueStateIconMapping {

type ComboBoxSelectionChangeEventDetail = {
item: ComboBoxItem | null,
trigger: `${ComboBoxSelectionChangeTrigger}`,
};

/**
Expand Down Expand Up @@ -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", {
Expand Down Expand Up @@ -506,6 +509,7 @@ class ComboBox extends UI5Element implements IFormInputElement {
_autocomplete = false;
_isKeyNavigation = false;
_selectionPerformed = false;
_selectionTrigger?: `${ComboBoxSelectionChangeTrigger}`;
_lastValue: string;
_selectedItemText = "";
_userTypedValue = "";
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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 */);
}

Expand All @@ -1047,6 +1057,7 @@ class ComboBox extends UI5Element implements IFormInputElement {
}

if (isEnter(e)) {
this._selectionPerformed = true;
let focusedItem: IComboBoxItem | undefined;

this._filteredItems.forEach(item => {
Expand Down Expand Up @@ -1351,22 +1362,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,
});
}
}
Expand Down Expand Up @@ -1427,6 +1439,7 @@ class ComboBox extends UI5Element implements IFormInputElement {
if (!item.selected) {
this.fireDecoratorEvent("selection-change", {
item,
trigger: ComboBoxSelectionChangeTrigger.Click,
});
}

Expand Down Expand Up @@ -1753,6 +1766,7 @@ class ComboBox extends UI5Element implements IFormInputElement {
ComboBox.define();

export default ComboBox;
export { ComboBoxSelectionChangeTrigger };

export type {
ComboBoxSelectionChangeEventDetail,
Expand Down
26 changes: 26 additions & 0 deletions packages/main/src/types/ComboBoxSelectionChangeTrigger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* 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, Enter).
Comment thread
ivoplashkov marked this conversation as resolved.
Outdated
* @public
*/
Keyboard = "Keyboard",
}

export default ComboBoxSelectionChangeTrigger;
96 changes: 96 additions & 0 deletions packages/main/test/pages/ComboBox.html
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,102 @@ <h3>ComboBox Composition</h3>
</ui5-combobox>
</div>

<div class="demo-section" style="padding: 20px; background: #f0f8ff; border-radius: 8px;">
<ui5-title>Selection Trigger Flag Demo</ui5-title>
<p style="margin: 10px 0;">The selection-change event includes a <code>trigger</code> property: "typeahead", "enter", "click", or "keyboard"</p>
Comment thread
ivoplashkov marked this conversation as resolved.
Outdated

<ui5-combobox id="trigger-flag-cb" placeholder="Try typing, arrow keys, Enter, or clicking...">
<ui5-cb-item text="Apple" value="apple"></ui5-cb-item>
<ui5-cb-item text="Apricot" value="apricot"></ui5-cb-item>
<ui5-cb-item text="Banana" value="banana"></ui5-cb-item>
<ui5-cb-item text="Blueberry" value="blueberry"></ui5-cb-item>
<ui5-cb-item text="Cherry" value="cherry"></ui5-cb-item>
</ui5-combobox>

<div style="display: flex; gap: 15px; margin-top: 15px;">
<div style="flex: 1; padding: 15px; background: white; border: 2px solid #107e3e; border-radius: 4px; min-height: 100px;">
<div style="font-weight: bold; margin-bottom: 8px;">selection-change events (with trigger):</div>
<div id="trigger-flag-events" style="font-family: monospace; font-size: 13px;">(none yet)</div>
</div>
<div style="flex: 1; padding: 15px; background: white; border: 2px solid #e78c07; border-radius: 4px; min-height: 100px;">
<div style="font-weight: bold; margin-bottom: 8px;">change events:</div>
<div id="trigger-flag-change-events" style="font-family: monospace; font-size: 13px;">(none yet)</div>
</div>
</div>

<div style="margin-top: 10px; padding: 10px; background: #fffbe6; border-radius: 4px; font-size: 13px;">
<strong>Trigger types:</strong>
<span style="margin-left: 10px; padding: 2px 8px; background: #d4edda; border-radius: 3px;">typeahead</span>
<span style="margin-left: 5px; padding: 2px 8px; background: #cce5ff; border-radius: 3px;">keyboard</span>
<span style="margin-left: 5px; padding: 2px 8px; background: #e2d5f1; border-radius: 3px;">click</span>
</div>

<ui5-button id="trigger-flag-clear" style="margin-top: 10px;">Clear Log</ui5-button>

<script>
(function() {
const cb = document.getElementById("trigger-flag-cb");
const selectionEventsEl = document.getElementById("trigger-flag-events");
const changeEventsEl = document.getElementById("trigger-flag-change-events");
const clearBtn = document.getElementById("trigger-flag-clear");
let selectionCount = 0;
let changeCount = 0;

const triggerColors = {
Comment thread
ivoplashkov marked this conversation as resolved.
typeahead: "#d4edda",
keyboard: "#cce5ff",
click: "#e2d5f1"
};

cb.addEventListener("ui5-selection-change", function(e) {
selectionCount++;
const item = e.detail.item;
const trigger = e.detail.trigger;
const timestamp = new Date().toLocaleTimeString();
const entry = document.createElement("div");
const bgColor = triggerColors[trigger] || "#f5f5f5";
entry.style.cssText = `padding: 5px 10px; margin: 3px 0; background: ${bgColor}; border-left: 3px solid #107e3e; animation: flashGreen 0.5s;`;
entry.innerHTML = `<strong>#${selectionCount}</strong> [${timestamp}] <span style="background: ${bgColor}; padding: 1px 6px; border-radius: 3px; font-weight: bold;">${trigger}</span> → ${item ? item.text : 'null'}`;

if (selectionEventsEl.textContent === "(none yet)") {
selectionEventsEl.textContent = "";
}
selectionEventsEl.insertBefore(entry, selectionEventsEl.firstChild);
});

cb.addEventListener("ui5-change", function(e) {
changeCount++;
const timestamp = new Date().toLocaleTimeString();
const entry = document.createElement("div");
entry.style.cssText = "padding: 5px 10px; margin: 3px 0; background: #fff4e8; border-left: 3px solid #e78c07; animation: flashOrange2 0.5s;";
entry.innerHTML = `<strong>#${changeCount}</strong> [${timestamp}] value: <strong>"${e.target.value}"</strong>`;

if (changeEventsEl.textContent === "(none yet)") {
changeEventsEl.textContent = "";
}
changeEventsEl.insertBefore(entry, changeEventsEl.firstChild);
});

clearBtn.addEventListener("click", function() {
selectionEventsEl.innerHTML = "(none yet)";
changeEventsEl.innerHTML = "(none yet)";
selectionCount = 0;
changeCount = 0;
});
})();
</script>
<style>
@keyframes flashGreen {
0% { background: #107e3e; color: white; }
100% { background: inherit; color: inherit; }
}
@keyframes flashOrange2 {
0% { background: #e78c07; color: white; }
100% { background: #fff4e8; color: inherit; }
}
</style>
</div>

<script type="module">
document.getElementById("lazy").addEventListener("ui5-input", async event => {
const { value } = event.target;
Expand Down
Loading