Skip to content

fix(ui5-shellbar): arrow key navigation inputs support #11684

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
70 changes: 70 additions & 0 deletions packages/fiori/cypress/specs/ShellBar.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ShellBar showSearchField={true}>
<Button id="button" slot="content">Test Button</Button>
<ShellBarSearch slot="searchField" value="test value"></ShellBarSearch>
<ShellBarItem icon={activities} text="Action 1"></ShellBarItem>
</ShellBar>
);
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");
});
});
78 changes: 46 additions & 32 deletions packages/fiori/src/ShellBar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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)) {
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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<ListItemClickEventDetail>) {
const shouldContinue = this.fireDecoratorEvent("menu-item-click", {
item: e.detail.item,
Expand Down
Loading