Skip to content

Commit c428f77

Browse files
authored
fix(ui5-search): improve arrow navigation with grouping (#12083)
Problem: Arrow keys don't work when navigating through suggestion groups without group headers. What changed: - Updated search arrow up/down focus logic to use actual focusable items from the list (flat items, without groups), using the list's `listItems` getter - Move getFocusDomRef method to ListItemGroup and return first item if no header is set - Includes Cypress tests to verify the navigation behavior works as expected.
1 parent c22bbc9 commit c428f77

File tree

5 files changed

+126
-10
lines changed

5 files changed

+126
-10
lines changed

packages/fiori/cypress/specs/Search.cy.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,76 @@ describe("Properties", () => {
8888
.should("be.focused");
8989
});
9090

91+
it("items slot arrow navigation with groups and headerText", () => {
92+
cy.mount(
93+
<Search>
94+
<SearchItemGroup headerText="Group Header 1">
95+
<SearchItem text="List Item" icon={history}></SearchItem>
96+
<SearchItem text="List Item" icon={searchIcon}></SearchItem>
97+
</SearchItemGroup>
98+
</Search>
99+
);
100+
101+
cy.get("[ui5-search]")
102+
.shadow()
103+
.find("input")
104+
.realClick();
105+
106+
cy.get("[ui5-search]")
107+
.realPress("L");
108+
109+
cy.get("[ui5-search]")
110+
.should("be.focused");
111+
112+
cy.get("[ui5-search]")
113+
.realPress("ArrowDown");
114+
115+
cy.get("ui5-search-item-group")
116+
.shadow()
117+
.find("[ui5-li-group-header]")
118+
.should("be.focused");
119+
120+
cy.get("[ui5-search]")
121+
.realPress("ArrowUp");
122+
123+
cy.get("[ui5-search]")
124+
.should("be.focused");
125+
});
126+
127+
it("items slot arrow navigation with groups and no headerText", () => {
128+
cy.mount(
129+
<Search>
130+
<SearchItemGroup>
131+
<SearchItem text="List Item" icon={history}></SearchItem>
132+
<SearchItem text="List Item" icon={searchIcon}></SearchItem>
133+
</SearchItemGroup>
134+
</Search>
135+
);
136+
137+
cy.get("[ui5-search]")
138+
.shadow()
139+
.find("input")
140+
.realClick();
141+
142+
cy.get("[ui5-search]")
143+
.realPress("L");
144+
145+
cy.get("[ui5-search]")
146+
.should("be.focused");
147+
148+
cy.get("[ui5-search]")
149+
.realPress("ArrowDown");
150+
151+
cy.get("ui5-search-item").eq(0)
152+
.should("be.focused");
153+
154+
cy.get("[ui5-search]")
155+
.realPress("ArrowUp");
156+
157+
cy.get("[ui5-search]")
158+
.should("be.focused");
159+
})
160+
91161
it("items should be shown instead of illustration of both present ", () => {
92162
cy.mount(
93163
<Search>

packages/fiori/src/Search.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ class Search extends SearchField {
336336
return StartsWithPerTerm(str, this._flattenItems.filter(item => !this._isGroupItem(item)), "text");
337337
}
338338

339-
_isGroupItem(item: ISearchSuggestionItem) {
339+
_isGroupItem(item: HTMLElement): item is SearchItemGroup {
340340
return item.hasAttribute("ui5-search-item-group");
341341
}
342342

@@ -354,7 +354,8 @@ class Search extends SearchField {
354354
}
355355

356356
_handleArrowDown() {
357-
const firstListItem = this._getItemsList()?.getSlottedNodes<ISearchSuggestionItem>("items")[0];
357+
const focusableItems = this._getItemsList().listItems;
358+
const firstListItem = focusableItems.at(0);
358359

359360
if (this.open) {
360361
this._deselectItems();
@@ -449,8 +450,13 @@ class Search extends SearchField {
449450
}
450451

451452
_onItemKeydown(e: KeyboardEvent) {
452-
const isFirstItem = this._flattenItems[0] === e.target;
453-
const isLastItem = this._flattenItems[this._flattenItems.length - 1] === e.target;
453+
const target = e.target as HTMLElement;
454+
// if focus is on the group header (in group's shadow dom) the target is the group itself,
455+
// if so using getFocusDomRef ensures the actual focused element is used
456+
const focusedItem = this._isGroupItem(target) ? target?.getFocusDomRef() : target;
457+
const focusableItems = this._getItemsList().listItems;
458+
const isFirstItem = focusableItems.at(0) === focusedItem;
459+
const isLastItem = focusableItems.at(-1) === focusedItem;
454460
const isArrowUp = isUp(e);
455461
const isArrowDown = isDown(e);
456462
const isTab = isTabNext(e);
@@ -602,7 +608,7 @@ class Search extends SearchField {
602608

603609
get _flattenItems(): Array<ISearchSuggestionItem> {
604610
return this.getSlottedNodes<ISearchSuggestionItem>("items").flatMap(item => {
605-
return this._isGroupItem(item) ? [item, ...item.items!] : [item];
611+
return this._isGroupItem(item) ? [item, ...item.items] : [item];
606612
});
607613
}
608614

packages/fiori/src/SearchItemGroup.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
22
import ListItemGroup from "@ui5/webcomponents/dist/ListItemGroup.js";
3-
import type ListItemGroupHeader from "@ui5/webcomponents/dist/ListItemGroupHeader.js";
43
import SearchItemGroupCss from "./generated/themes/SearchItemGroup.css.js";
54
import ListBoxItemGroupTemplate from "@ui5/webcomponents/dist/ListBoxItemGroupTemplate.js";
65

@@ -26,10 +25,6 @@ class SearchItemGroup extends ListItemGroup {
2625
get isGroupItem(): boolean {
2726
return true;
2827
}
29-
30-
getFocusDomRef() {
31-
return this.shadowRoot!.querySelector("[ui5-li-group-header]") as ListItemGroupHeader;
32-
}
3328
}
3429

3530
SearchItemGroup.define();

packages/main/cypress/specs/ListItemGroup.cy.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import List from "../../src/List.js";
2+
import ListItemStandard from "../../src/ListItemStandard.js";
13
import ListItemGroup from "../../src/ListItemGroup.js";
24

35
describe("ListItemGroup Tests", () => {
@@ -308,4 +310,43 @@ describe("List drag and drop tests", () => {
308310
cy.get("@list2").children().should("have.length", 4);
309311
cy.get("@list2").find("a").should("exist").and("contain.text", "http://sap.com");
310312
});
313+
});
314+
315+
describe("Focus", () => {
316+
it("getFocusDomRef should return header element if available", () => {
317+
cy.mount(
318+
<List>
319+
<ListItemGroup headerText="Group Header">
320+
<ListItemStandard>Item 1</ListItemStandard>
321+
<ListItemStandard>Item 2</ListItemStandard>
322+
<ListItemStandard>Item 3</ListItemStandard>
323+
</ListItemGroup>
324+
</List>
325+
);
326+
327+
cy.get<ListItemGroup>("[ui5-li-group]")
328+
.then(($el) => {
329+
const group = $el[0];
330+
expect(group.getFocusDomRef()).to.have.attr("ui5-li-group-header");
331+
});
332+
333+
});
334+
335+
it("getFocusDomRef should return list item when header is not available", () => {
336+
cy.mount(
337+
<List>
338+
<ListItemGroup>
339+
<ListItemStandard>Item 1</ListItemStandard>
340+
<ListItemStandard>Item 2</ListItemStandard>
341+
<ListItemStandard>Item 3</ListItemStandard>
342+
</ListItemGroup>
343+
</List>
344+
);
345+
346+
cy.get<ListItemGroup>("[ui5-li-group]")
347+
.then(($el) => {
348+
const group = $el[0];
349+
expect(group.getFocusDomRef()).to.have.attr("ui5-li");
350+
});
351+
});
311352
});

packages/main/src/ListItemGroup.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ class ListItemGroup extends UI5Element {
212212
}
213213
return placements;
214214
}
215+
216+
getFocusDomRef() {
217+
return this.groupHeaderItem || this.items.at(0);
218+
}
215219
}
216220

217221
ListItemGroup.define();

0 commit comments

Comments
 (0)