diff --git a/packages/fiori/cypress/specs/Search.cy.tsx b/packages/fiori/cypress/specs/Search.cy.tsx index bd20b91d2dec..dc2ab16399a0 100644 --- a/packages/fiori/cypress/specs/Search.cy.tsx +++ b/packages/fiori/cypress/specs/Search.cy.tsx @@ -88,6 +88,76 @@ describe("Properties", () => { .should("be.focused"); }); + it("items slot arrow navigation with groups and headerText", () => { + cy.mount( + + + + + + + ); + + cy.get("[ui5-search]") + .shadow() + .find("input") + .realClick(); + + cy.get("[ui5-search]") + .realPress("L"); + + cy.get("[ui5-search]") + .should("be.focused"); + + cy.get("[ui5-search]") + .realPress("ArrowDown"); + + cy.get("ui5-search-item-group") + .shadow() + .find("[ui5-li-group-header]") + .should("be.focused"); + + cy.get("[ui5-search]") + .realPress("ArrowUp"); + + cy.get("[ui5-search]") + .should("be.focused"); + }); + + it("items slot arrow navigation with groups and no headerText", () => { + cy.mount( + + + + + + + ); + + cy.get("[ui5-search]") + .shadow() + .find("input") + .realClick(); + + cy.get("[ui5-search]") + .realPress("L"); + + cy.get("[ui5-search]") + .should("be.focused"); + + cy.get("[ui5-search]") + .realPress("ArrowDown"); + + cy.get("ui5-search-item").eq(0) + .should("be.focused"); + + cy.get("[ui5-search]") + .realPress("ArrowUp"); + + cy.get("[ui5-search]") + .should("be.focused"); + }) + it("items should be shown instead of illustration of both present ", () => { cy.mount( diff --git a/packages/fiori/src/Search.ts b/packages/fiori/src/Search.ts index 2c62d802f753..b9c877efb1a4 100644 --- a/packages/fiori/src/Search.ts +++ b/packages/fiori/src/Search.ts @@ -336,7 +336,7 @@ class Search extends SearchField { return StartsWithPerTerm(str, this._flattenItems.filter(item => !this._isGroupItem(item)), "text"); } - _isGroupItem(item: ISearchSuggestionItem) { + _isGroupItem(item: HTMLElement): item is SearchItemGroup { return item.hasAttribute("ui5-search-item-group"); } @@ -354,7 +354,8 @@ class Search extends SearchField { } _handleArrowDown() { - const firstListItem = this._getItemsList()?.getSlottedNodes("items")[0]; + const focusableItems = this._getItemsList().listItems; + const firstListItem = focusableItems.at(0); if (this.open) { this._deselectItems(); @@ -449,8 +450,13 @@ class Search extends SearchField { } _onItemKeydown(e: KeyboardEvent) { - const isFirstItem = this._flattenItems[0] === e.target; - const isLastItem = this._flattenItems[this._flattenItems.length - 1] === e.target; + const target = e.target as HTMLElement; + // if focus is on the group header (in group's shadow dom) the target is the group itself, + // if so using getFocusDomRef ensures the actual focused element is used + const focusedItem = this._isGroupItem(target) ? target?.getFocusDomRef() : target; + const focusableItems = this._getItemsList().listItems; + const isFirstItem = focusableItems.at(0) === focusedItem; + const isLastItem = focusableItems.at(-1) === focusedItem; const isArrowUp = isUp(e); const isArrowDown = isDown(e); const isTab = isTabNext(e); @@ -602,7 +608,7 @@ class Search extends SearchField { get _flattenItems(): Array { return this.getSlottedNodes("items").flatMap(item => { - return this._isGroupItem(item) ? [item, ...item.items!] : [item]; + return this._isGroupItem(item) ? [item, ...item.items] : [item]; }); } diff --git a/packages/fiori/src/SearchItemGroup.ts b/packages/fiori/src/SearchItemGroup.ts index fcc9c162f6fe..96ab8f717d6b 100644 --- a/packages/fiori/src/SearchItemGroup.ts +++ b/packages/fiori/src/SearchItemGroup.ts @@ -1,6 +1,5 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import ListItemGroup from "@ui5/webcomponents/dist/ListItemGroup.js"; -import type ListItemGroupHeader from "@ui5/webcomponents/dist/ListItemGroupHeader.js"; import SearchItemGroupCss from "./generated/themes/SearchItemGroup.css.js"; import ListBoxItemGroupTemplate from "@ui5/webcomponents/dist/ListBoxItemGroupTemplate.js"; @@ -26,10 +25,6 @@ class SearchItemGroup extends ListItemGroup { get isGroupItem(): boolean { return true; } - - getFocusDomRef() { - return this.shadowRoot!.querySelector("[ui5-li-group-header]") as ListItemGroupHeader; - } } SearchItemGroup.define(); diff --git a/packages/main/cypress/specs/ListItemGroup.cy.tsx b/packages/main/cypress/specs/ListItemGroup.cy.tsx index d06c3937b620..009c18e8dd9e 100644 --- a/packages/main/cypress/specs/ListItemGroup.cy.tsx +++ b/packages/main/cypress/specs/ListItemGroup.cy.tsx @@ -1,3 +1,5 @@ +import List from "../../src/List.js"; +import ListItemStandard from "../../src/ListItemStandard.js"; import ListItemGroup from "../../src/ListItemGroup.js"; describe("ListItemGroup Tests", () => { @@ -308,4 +310,43 @@ describe("List drag and drop tests", () => { cy.get("@list2").children().should("have.length", 4); cy.get("@list2").find("a").should("exist").and("contain.text", "http://sap.com"); }); +}); + +describe("Focus", () => { + it("getFocusDomRef should return header element if available", () => { + cy.mount( + + + Item 1 + Item 2 + Item 3 + + + ); + + cy.get("[ui5-li-group]") + .then(($el) => { + const group = $el[0]; + expect(group.getFocusDomRef()).to.have.attr("ui5-li-group-header"); + }); + + }); + + it("getFocusDomRef should return list item when header is not available", () => { + cy.mount( + + + Item 1 + Item 2 + Item 3 + + + ); + + cy.get("[ui5-li-group]") + .then(($el) => { + const group = $el[0]; + expect(group.getFocusDomRef()).to.have.attr("ui5-li"); + }); + }); }); \ No newline at end of file diff --git a/packages/main/src/ListItemGroup.ts b/packages/main/src/ListItemGroup.ts index b97829abb761..9ddfa567fb3d 100644 --- a/packages/main/src/ListItemGroup.ts +++ b/packages/main/src/ListItemGroup.ts @@ -212,6 +212,10 @@ class ListItemGroup extends UI5Element { } return placements; } + + getFocusDomRef() { + return this.groupHeaderItem || this.items.at(0); + } } ListItemGroup.define();