From d803f56834b62d96e1847b97322665cfb9587d7f Mon Sep 17 00:00:00 2001 From: Ivanova Terzieva Date: Tue, 16 Sep 2025 16:34:16 +0300 Subject: [PATCH] feat(ui5-search): enable fast navigation with F2 Enable focusing of the delete button inside a search item with F2 key press when the search item is focused. --- packages/fiori/cypress/specs/Search.cy.tsx | 116 +++++++++++++++++++++ packages/fiori/src/SearchItem.ts | 37 +++++++ packages/fiori/src/SearchItemTemplate.tsx | 7 +- packages/fiori/test/pages/Search.html | 14 +-- 4 files changed, 162 insertions(+), 12 deletions(-) diff --git a/packages/fiori/cypress/specs/Search.cy.tsx b/packages/fiori/cypress/specs/Search.cy.tsx index 9182c3c65390..0d7616a79cec 100644 --- a/packages/fiori/cypress/specs/Search.cy.tsx +++ b/packages/fiori/cypress/specs/Search.cy.tsx @@ -891,6 +891,122 @@ describe("Events", () => { .should("not.exist") }); + it("delete event is fired on clicking the delete button of a search item", () => { + cy.mount( + + + + ); + + cy.get("[ui5-search]") + .shadow() + .find("input") + .realClick(); + + cy.get("[ui5-search]") + .realPress("I"); + + cy.get("[ui5-search]") + .realPress("ArrowDown"); + + cy.get("[ui5-search-item]").eq(0) + .as("firstSearchItem"); + + cy.get("@firstSearchItem") + .shadow() + .find("[ui5-button]") + .realClick(); + + cy.get("@deleteSpy").should("have.been.calledOnce"); + }); + + it("Fast navigation with F2 key press", () => { + cy.mount( + + + + ); + + cy.get("[ui5-search]") + .shadow() + .find("input") + .realClick(); + + cy.get("[ui5-search]") + .realPress("I"); + + cy.get("[ui5-search]") + .realPress("ArrowDown"); + + cy.get("[ui5-search-item]").eq(0) + .as("firstSearchItem"); + + cy.get("@firstSearchItem") + .should("be.focused"); + + cy.realPress("F2"); + + cy.get("@firstSearchItem") + .shadow() + .find("[ui5-button]") + .should("be.focused"); + + cy.realPress("F2"); + + cy.get("@firstSearchItem") + .should("be.focused"); + }); + + it("delete event is fired on pressing SPACE on the focused delete button of a search item", () => { + cy.mount( + + + + ); + + cy.get("[ui5-search]") + .shadow() + .find("input") + .realClick(); + + cy.get("[ui5-search]") + .realPress("I"); + + cy.get("[ui5-search]") + .realPress("ArrowDown"); + + cy.realPress("F2"); + + cy.realPress("Space"); + + cy.get("@deleteSpy").should("have.been.calledOnce"); + }); + + it("delete event is fired on pressing ENTER on the focused delete button of a search item", () => { + cy.mount( + + + + ); + + cy.get("[ui5-search]") + .shadow() + .find("input") + .realClick(); + + cy.get("[ui5-search]") + .realPress("I"); + + cy.get("[ui5-search]") + .realPress("ArrowDown"); + + cy.realPress("F2"); + + cy.realPress("Enter"); + + cy.get("@deleteSpy").should("have.been.calledOnce"); + }); + it("should deselect items when backspace or delete key is pressed", () => { cy.mount( diff --git a/packages/fiori/src/SearchItem.ts b/packages/fiori/src/SearchItem.ts index a60617910aa1..5ae95c6afa5c 100644 --- a/packages/fiori/src/SearchItem.ts +++ b/packages/fiori/src/SearchItem.ts @@ -8,6 +8,9 @@ import generateHighlightedMarkup from "@ui5/webcomponents-base/dist/util/generat import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; import { SEARCH_ITEM_DELETE_BUTTON } from "./generated/i18n/i18n-defaults.js"; +import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js"; +import { getFirstFocusableElement } from "@ui5/webcomponents-base/dist/util/FocusableElements.js"; +import { isSpace, isEnter, isF2 } from "@ui5/webcomponents-base/dist/Keys.js"; import { i18n } from "@ui5/webcomponents-base/dist/decorators.js"; import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; // @ts-expect-error @@ -129,10 +132,44 @@ class SearchItem extends ListItemBase { this.selected = false; } + async _onkeydown(e: KeyboardEvent) { + super._onkeydown(e); + + if (this.getFocusDomRef()!.matches(":has(:focus-within)")) { + if (isSpace(e) || isEnter(e)) { + e.preventDefault(); + return; + } + } + + if (isF2(e)) { + e.stopImmediatePropagation(); + const activeElement = getActiveElement(); + const focusDomRef = this.getFocusDomRef(); + + if (!focusDomRef) { + return; + } + + if (activeElement === focusDomRef) { + const firstFocusable = await getFirstFocusableElement(focusDomRef); + firstFocusable?.focus(); + } else { + focusDomRef.focus(); + } + } + } + _onDeleteButtonClick() { this.fireDecoratorEvent("delete"); } + _onDeleteButtonKeyDown(e: KeyboardEvent) { + if (isSpace(e) || isEnter(e)) { + this.fireDecoratorEvent("delete"); + } + } + onBeforeRendering(): void { super.onBeforeRendering(); diff --git a/packages/fiori/src/SearchItemTemplate.tsx b/packages/fiori/src/SearchItemTemplate.tsx index 963120818e45..489a516e069d 100644 --- a/packages/fiori/src/SearchItemTemplate.tsx +++ b/packages/fiori/src/SearchItemTemplate.tsx @@ -45,7 +45,12 @@ export default function SearchFieldTemplate(this: SearchItem) { {this.deletable && - + } diff --git a/packages/fiori/test/pages/Search.html b/packages/fiori/test/pages/Search.html index dee7fba812d8..4a857726067c 100644 --- a/packages/fiori/test/pages/Search.html +++ b/packages/fiori/test/pages/Search.html @@ -255,15 +255,6 @@ { name: 'Tomato', category: 'Vegetable' }, ]; - function createItems(parent, data) { - data.forEach(item => { - const searchItem = document.createElement('ui5-search-item'); - searchItem.text = item.name; - searchItem.icon = 'search'; - parent.appendChild(searchItem); - }); - } - const filtering = document.getElementById('filtering'); createItems(filtering, data); filtering.addEventListener('ui5-input', (event) => { @@ -370,10 +361,11 @@ const searchDelete = document.getElementById('delete-search'); - function onDelete(event) { + function onDelete(event, parent) { const item = event.target; if (item) { item.remove(); + parent.focus(); } } @@ -392,7 +384,7 @@ searchItem.text = item.name; searchItem.icon = 'search'; searchItem.deletable = true; - searchItem.addEventListener('ui5-delete', onDelete); + searchItem.addEventListener('ui5-delete', (e) => onDelete(e, parent)); parent.appendChild(searchItem); }); }