From eca9e436bffd72bdd7d357920a40378ee826732a Mon Sep 17 00:00:00 2001 From: Dobrin Dimchev Date: Thu, 12 Jun 2025 22:39:17 +0300 Subject: [PATCH] fix(ui5-shellbar): smart arrow nabigation respecting inputs --- packages/fiori/cypress/specs/ShellBar.cy.tsx | 70 ++++++++++++++++++ packages/fiori/src/ShellBar.ts | 78 ++++++++++++-------- 2 files changed, 116 insertions(+), 32 deletions(-) diff --git a/packages/fiori/cypress/specs/ShellBar.cy.tsx b/packages/fiori/cypress/specs/ShellBar.cy.tsx index 4ac34c011211..86402d5712c7 100644 --- a/packages/fiori/cypress/specs/ShellBar.cy.tsx +++ b/packages/fiori/cypress/specs/ShellBar.cy.tsx @@ -684,4 +684,74 @@ describe("Keyboard Navigation", () => { .find(".ui5-shellbar-logo-area") .should("not.exist"); }); + + it("Test arrow navigation within search input respects cursor position", () => { + cy.mount( + + + + + + ); + cy.wait(RESIZE_THROTTLE_RATE); + + function placeAtStartOfInput() { + cy.get("[ui5-shellbar] [slot='searchField']") + .shadow() + .find("input") + .then($input => { + $input[0].setSelectionRange(0, 0); + }); + } + function placeAtEndOfInput() { + cy.get("[ui5-shellbar] [slot='searchField']") + .shadow() + .find("input") + .then($input => { + const inputLength = $input.val().toString().length; + $input[0].setSelectionRange(inputLength, inputLength); + }); + } + function placeInMiddleOfInput() { + cy.get("[ui5-shellbar] [slot='searchField']") + .shadow() + .find("input") + .then($input => { + const inputLength = $input.val().toString().length; + const middlePosition = Math.floor(inputLength / 2); + $input[0].setSelectionRange(middlePosition, middlePosition); + }); + } + + // Focus the search input + cy.get("[ui5-shellbar] [slot='searchField']") + .realClick() + .shadow() + .find("input") + .as("nativeInput"); + + placeAtStartOfInput(); + // Press left arrow - should move focus away from input since cursor is at start + cy.get("@nativeInput").type("{leftArrow}"); + // Verify focus is now on the button + cy.get("[ui5-shellbar] [ui5-button]").should("be.focused"); + + + placeAtEndOfInput(); + // Press right arrow - should move focus away from input since cursor is at end + cy.get("@nativeInput").type("{rightArrow}"); + // Verify focus is now on the ShellBarItem + cy.get("[ui5-shellbar]") + .shadow() + .find(".ui5-shellbar-custom-item") + .should("be.focused"); + + placeInMiddleOfInput(); + // Press left arrow - should stay focused on input since cursor is in the middle + cy.get("@nativeInput").type("{leftArrow}"); + cy.get("@nativeInput").should("be.focused"); + // Press right arrow - should stay focused on input since cursor is in the middle + cy.get("@nativeInput").type("{rightArrow}"); + cy.get("@nativeInput").should("be.focused"); + }); }); diff --git a/packages/fiori/src/ShellBar.ts b/packages/fiori/src/ShellBar.ts index e225caa12c60..e846e84160ac 100644 --- a/packages/fiori/src/ShellBar.ts +++ b/packages/fiori/src/ShellBar.ts @@ -14,6 +14,7 @@ import { isRight, } from "@ui5/webcomponents-base/dist/Keys.js"; import { getEffectiveAriaLabelText } from "@ui5/webcomponents-base/dist/util/AccessibilityTextsHelper.js"; +import { getTabbableElements } from "@ui5/webcomponents-base/dist/util/TabbableElements.js"; import ListItemStandard from "@ui5/webcomponents/dist/ListItemStandard.js"; import List from "@ui5/webcomponents/dist/List.js"; import type { ListItemClickEventDetail } from "@ui5/webcomponents/dist/List.js"; @@ -647,12 +648,32 @@ class ShellBar extends UI5Element { } _onKeyDown(e: KeyboardEvent) { - const items = this._getVisibleAndInteractiveItems(); + if (!isLeft(e) && !isRight(e)) { + return; + } + + const domRef = this.getDomRef(); + if (!domRef) { + // If the component is not rendered yet, we should not handle the keydown event + return; + } + const activeElement = getActiveElement(); + if (!activeElement) { + return; + } + + // Check if the active elements should "steal" the navigation + if (this._allowChildNavigation(activeElement as HTMLElement, e)) { + return; + } + + const items = getTabbableElements(domRef).filter(el => this._isVisible(el)); const currentIndex = items.findIndex(el => el === activeElement); - if (isLeft(e) || isRight(e)) { - e.preventDefault();// Prevent the default behavior to avoid any further automatic focus movemen + // Only handle arrow navigation if the focus is on a ShellBar item + if (currentIndex !== -1) { + e.preventDefault(); // Focus navigation based on the key pressed if (isLeft(e)) { @@ -663,6 +684,28 @@ class ShellBar extends UI5Element { } } + private _allowChildNavigation(activeElement: HTMLElement, e: KeyboardEvent): boolean { + if (activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA") { + return this._allowInputNavigation(activeElement as HTMLInputElement | HTMLTextAreaElement, e); + } + + return false; // Default to false for other elements + } + + private _allowInputNavigation(inputElement: HTMLInputElement | HTMLTextAreaElement, e: KeyboardEvent): boolean { + const cursorPosition = inputElement.selectionStart || 0; + const textLength = inputElement.value.length; + + // Allow internal navigation if cursor is not at the boundaries + if ((isLeft(e) && cursorPosition > 0) + || (isRight(e) && cursorPosition < textLength)) { + return true; + } + + // Let ShellBar handle navigation if at boundaries + return false; + } + _focusNextItem(items: HTMLElement[], currentIndex: number) { if (currentIndex < items.length - 1) { (items[currentIndex + 1]).focus(); // Focus the next element @@ -681,26 +724,6 @@ class ShellBar extends UI5Element { return style.display !== "none" && style.visibility !== "hidden" && element.offsetWidth > 0 && element.offsetHeight > 0; } - _getNavigableContent() { - const elements = [ - ...this.startButton, - ...this.logo, - ...this.shadowRoot!.querySelectorAll(".ui5-shellbar-logo"), - ...this.shadowRoot!.querySelectorAll(".ui5-shellbar-logo-area"), - ...this.shadowRoot!.querySelectorAll(".ui5-shellbar-menu-button"), - ...this.contentItems, - ...this._getRightChildItems(), - ] as HTMLElement[]; - - return elements.map((element: HTMLElement) => { - const component = element as UI5Element; - if (component.isUI5Element) { - return component.getFocusDomRef(); - } - return element; - }).filter(el => !!el); - } - _getRightChildItems() { return [ ...this.searchField, @@ -710,15 +733,6 @@ class ShellBar extends UI5Element { ] as HTMLElement[]; } - _getVisibleAndInteractiveItems() { - const items = this._getNavigableContent(); - const visibleAndInteractiveItems = items.filter(item => { - return this._isVisible(item) && item.tabIndex === 0; - }); - - return visibleAndInteractiveItems; - } - _menuItemPress(e: CustomEvent) { const shouldContinue = this.fireDecoratorEvent("menu-item-click", { item: e.detail.item,