diff --git a/packages/main/cypress/specs/List.cy.tsx b/packages/main/cypress/specs/List.cy.tsx index 7b50bb97b3bc..8effeab5314c 100644 --- a/packages/main/cypress/specs/List.cy.tsx +++ b/packages/main/cypress/specs/List.cy.tsx @@ -1286,6 +1286,114 @@ describe("List Tests", () => { cy.get("[ui5-li-custom]").first().should("be.focused"); }); + it("keyboard handling on F7", () => { + cy.mount( + + + + + + + ); + + cy.get("[ui5-li-custom]").click(); + cy.get("[ui5-li-custom]").should("be.focused"); + + // F7 goes to first focusable element + cy.realPress("F7"); + cy.get("[ui5-button]").first().should("be.focused"); + + // Tab to second button + cy.realPress("Tab"); + cy.get("[ui5-button]").last().should("be.focused"); + + // F7 returns to list item + cy.realPress("F7"); + cy.get("[ui5-li-custom]").should("be.focused"); + + // F7 remembers last focused element (second button) + cy.realPress("F7"); + cy.get("[ui5-button]").last().should("be.focused"); + }); + + it("keyboard handling on F7 after TAB navigation", () => { + cy.mount( +
+ + + + + + + +
+ ); + + cy.get("button").click(); + cy.get("button").should("be.focused"); + + // Tab into list item + cy.realPress("Tab"); + cy.get("[ui5-li-custom]").should("be.focused"); + + // Tab into internal elements (goes to first button) + cy.realPress("Tab"); + cy.get("[ui5-button]").first().should("be.focused"); + + // Tab to second button + cy.realPress("Tab"); + cy.get("[ui5-button]").last().should("be.focused"); + + // F7 should store current element and return to list item + cy.realPress("F7"); + cy.get("[ui5-li-custom]").should("be.focused"); + + // F7 should remember the second button (not go to first) + cy.realPress("F7"); + cy.get("[ui5-button]").last().should("be.focused"); + }); + + it("keyboard handling on F7 maintains focus position across list items", () => { + cy.mount( + + + + + + + + + + + + + ); + + // Focus first list item + cy.get("[ui5-li-custom]").first().click(); + cy.get("[ui5-li-custom]").first().should("be.focused"); + + // F7 to enter (should go to first button) + cy.realPress("F7"); + cy.get("[ui5-button]").eq(0).should("be.focused"); + + // Tab to second button + cy.realPress("Tab"); + cy.get("[ui5-button]").eq(1).should("be.focused"); + + // F7 to exit back to list item + cy.realPress("F7"); + cy.get("[ui5-li-custom]").first().should("be.focused"); + + // Navigate to second list item with ArrowDown + cy.realPress("ArrowDown"); + cy.get("[ui5-li-custom]").last().should("be.focused"); + + // F7 should focus the second button (same index as previous item) + cy.realPress("F7"); + cy.get("[ui5-button]").eq(4).should("be.focused").and("contain", "Item 2 - Second"); + }); + it("keyboard handling on TAB when 2 level nested UI5Element is focused", () => { cy.mount(
diff --git a/packages/main/src/List.ts b/packages/main/src/List.ts index 9ef8a7e49ebd..e419bbf8f230 100644 --- a/packages/main/src/List.ts +++ b/packages/main/src/List.ts @@ -534,6 +534,7 @@ class List extends UI5Element { _beforeElement?: HTMLElement | null; _afterElement?: HTMLElement | null; _startMarkerOutOfView: boolean = false; + _lastFocusedElementIndex?: number; handleResizeCallback: ResizeObserverCallback; onItemFocusedBound: (e: CustomEvent) => void; diff --git a/packages/main/src/ListItem.ts b/packages/main/src/ListItem.ts index ee24d52adc80..995f3f7c7027 100644 --- a/packages/main/src/ListItem.ts +++ b/packages/main/src/ListItem.ts @@ -1,11 +1,12 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; import { - isSpace, isEnter, isDelete, isF2, + isSpace, isEnter, isDelete, isF2, isF7, } from "@ui5/webcomponents-base/dist/Keys.js"; import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js"; import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js"; import { getFirstFocusableElement } from "@ui5/webcomponents-base/dist/util/FocusableElements.js"; +import { getTabbableElements } from "@ui5/webcomponents-base/dist/util/TabbableElements.js"; import type { AccessibilityAttributes, AriaRole, AriaHasPopup } from "@ui5/webcomponents-base"; import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js"; @@ -21,6 +22,7 @@ import ListItemBase from "./ListItemBase.js"; import type RadioButton from "./RadioButton.js"; import type CheckBox from "./CheckBox.js"; import type { IButton } from "./Button.js"; +import type List from "./List.js"; import { DELETE, ARIA_LABEL_LIST_ITEM_CHECKBOX, @@ -255,7 +257,7 @@ abstract class ListItem extends ListItemBase { document.removeEventListener("touchend", this.deactivate); } - async _onkeydown(e: KeyboardEvent) { + _onkeydown(e: KeyboardEvent) { if ((isSpace(e) || isEnter(e)) && this._isTargetSelfFocusDomRef(e)) { return; } @@ -270,15 +272,11 @@ abstract class ListItem extends ListItemBase { } if (isF2(e)) { - const activeElement = getActiveElement(); - const focusDomRef = this.getFocusDomRef()!; - - if (activeElement === focusDomRef) { - const firstFocusable = await getFirstFocusableElement(focusDomRef); - firstFocusable?.focus(); - } else { - focusDomRef.focus(); - } + this._handleF2(); + } + + if (isF7(e)) { + this._handleF7(e); } } @@ -518,6 +516,74 @@ abstract class ListItem extends ListItemBase { get _listItem() { return this.shadowRoot!.querySelector("li"); } + + _getList(): List | null { + return this.closest("[ui5-list]"); + } + + _handleF7(e: KeyboardEvent) { + e.preventDefault(); + + const focusDomRef = this.getFocusDomRef()!; + const activeElement = getActiveElement(); + const list = this._getList(); + + if (activeElement === focusDomRef) { + this._focusInternalElement(list); + } else { + if (activeElement) { + this._updateStoredFocusIndex(list, activeElement as HTMLElement); + } + focusDomRef.focus(); + } + } + + async _handleF2() { + const focusDomRef = this.getFocusDomRef()!; + const activeElement = getActiveElement(); + + if (activeElement === focusDomRef) { + const firstFocusable = await getFirstFocusableElement(focusDomRef); + firstFocusable?.focus(); + } else { + focusDomRef.focus(); + } + } + + _getFocusableElements(): HTMLElement[] { + const focusDomRef = this.getFocusDomRef()!; + return getTabbableElements(focusDomRef); + } + + _focusInternalElement(list: List | null) { + const focusables = this._getFocusableElements(); + if (!focusables.length) { + return; + } + + const targetIndex = list?._lastFocusedElementIndex ?? 0; + const safeIndex = Math.min(targetIndex, focusables.length - 1); + const elementToFocus = focusables[safeIndex]; + + elementToFocus.focus(); + + if (list) { + list._lastFocusedElementIndex = safeIndex; + } + } + + _updateStoredFocusIndex(list: List | null, activeElement: HTMLElement) { + if (!list) { + return; + } + + const focusables = this._getFocusableElements(); + const currentIndex = focusables.indexOf(activeElement); + + if (currentIndex !== -1) { + list._lastFocusedElementIndex = currentIndex; + } + } } export default ListItem; diff --git a/packages/main/src/ListItemCustom.ts b/packages/main/src/ListItemCustom.ts index 55a3d929ce60..f581290f16c3 100644 --- a/packages/main/src/ListItemCustom.ts +++ b/packages/main/src/ListItemCustom.ts @@ -1,4 +1,6 @@ -import { isTabNext, isTabPrevious, isF2 } from "@ui5/webcomponents-base/dist/Keys.js"; +import { + isTabNext, isTabPrevious, isF2, isF7, +} from "@ui5/webcomponents-base/dist/Keys.js"; import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js"; import type { ClassMap } from "@ui5/webcomponents-base/dist/types.js"; import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js"; @@ -54,22 +56,22 @@ class ListItemCustom extends ListItem { @property() declare accessibleName?: string; - async _onkeydown(e: KeyboardEvent) { + _onkeydown(e: KeyboardEvent) { const isTab = isTabNext(e) || isTabPrevious(e); const isFocused = this.matches(":focus"); - if (!isTab && !isFocused && !isF2(e)) { + if (!isTab && !isFocused && !isF2(e) && !isF7(e)) { return; } - await super._onkeydown(e); + super._onkeydown(e); } _onkeyup(e: KeyboardEvent) { const isTab = isTabNext(e) || isTabPrevious(e); const isFocused = this.matches(":focus"); - if (!isTab && !isFocused && !isF2(e)) { + if (!isTab && !isFocused && !isF2(e) && !isF7(e)) { return; } diff --git a/packages/main/src/TreeItemBase.ts b/packages/main/src/TreeItemBase.ts index 01f888782388..274dae32e5fb 100644 --- a/packages/main/src/TreeItemBase.ts +++ b/packages/main/src/TreeItemBase.ts @@ -313,8 +313,8 @@ class TreeItemBase extends ListItem { this.fireDecoratorEvent("toggle", { item: this }); } - async _onkeydown(e: KeyboardEvent) { - await super._onkeydown(e); + _onkeydown(e: KeyboardEvent) { + super._onkeydown(e); if (!this._fixed && this.showToggleButton && isRight(e)) { if (!this.expanded) { diff --git a/packages/main/src/TreeItemCustom.ts b/packages/main/src/TreeItemCustom.ts index 68d5b65b7191..c24d2dff598d 100644 --- a/packages/main/src/TreeItemCustom.ts +++ b/packages/main/src/TreeItemCustom.ts @@ -57,7 +57,7 @@ class TreeItemCustom extends TreeItemBase { @slot() content!: Array; - async _onkeydown(e: KeyboardEvent) { + _onkeydown(e: KeyboardEvent) { if (isDown(e) && this.content?.some(el => el.contains(e.target as Node))) { e.stopPropagation(); return; @@ -69,7 +69,7 @@ class TreeItemCustom extends TreeItemBase { return; } - await super._onkeydown(e); + super._onkeydown(e); } _onkeyup(e: KeyboardEvent) { diff --git a/packages/main/test/pages/ListItemCustomF7.html b/packages/main/test/pages/ListItemCustomF7.html new file mode 100644 index 000000000000..a15d6f950b9f --- /dev/null +++ b/packages/main/test/pages/ListItemCustomF7.html @@ -0,0 +1,93 @@ + + + + + + + F7/F2 Key Test + + + + + +
+

F7/F2 Key Test

+

F7 vs F2 Behavior:

+
    +
  • F2: Simple navigation - always goes to first focusable element
  • +
  • F7: Smart navigation - remembers last focused element position across items
  • +
+ +

Test Steps:

+
    +
  1. Click on first list item
  2. +
  3. Press F7 → should go to first button
  4. +
  5. Press TAB to move to second button
  6. +
  7. Press F7 → should return to list item
  8. +
  9. Press ArrowDown → should go to second list item
  10. +
  11. Press F7 → should go to SECOND button (maintains position!)
  12. +
  13. Test F2 → should always go to first button (no memory)
  14. +
+
+ + + +
+ First Button + Second Button + +
+
+ +
+ Button A + Button B + +
+
+
+ + + + + \ No newline at end of file