Skip to content

Commit 65cb4aa

Browse files
committed
feat(ui5-li-custom): maintain focus position with F7 across list items
F7 navigation now remembers the focused element position when moving between list items. Pressing F7 focuses the element at the same index that was previously focused in another item. The List component stores a shared _lastFocusedElementIndex property, and ListItem uses getTabbableElements to reliably find focusable elements. Helper methods handle focusing by index and updating the stored position.
1 parent fad9332 commit 65cb4aa

File tree

4 files changed

+97
-22
lines changed

4 files changed

+97
-22
lines changed

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,6 +1353,47 @@ describe("List Tests", () => {
13531353
cy.get("[ui5-button]").last().should("be.focused");
13541354
});
13551355

1356+
it("keyboard handling on F7 maintains focus position across list items", () => {
1357+
cy.mount(
1358+
<List>
1359+
<ListItemCustom>
1360+
<Button>Item 1 - First</Button>
1361+
<Button>Item 1 - Second</Button>
1362+
<Button>Item 1 - Third</Button>
1363+
</ListItemCustom>
1364+
<ListItemCustom>
1365+
<Button>Item 2 - First</Button>
1366+
<Button>Item 2 - Second</Button>
1367+
<Button>Item 2 - Third</Button>
1368+
</ListItemCustom>
1369+
</List>
1370+
);
1371+
1372+
// Focus first list item
1373+
cy.get("[ui5-li-custom]").first().click();
1374+
cy.get("[ui5-li-custom]").first().should("be.focused");
1375+
1376+
// F7 to enter (should go to first button)
1377+
cy.realPress("F7");
1378+
cy.get("[ui5-button]").eq(0).should("be.focused");
1379+
1380+
// Tab to second button
1381+
cy.realPress("Tab");
1382+
cy.get("[ui5-button]").eq(1).should("be.focused");
1383+
1384+
// F7 to exit back to list item
1385+
cy.realPress("F7");
1386+
cy.get("[ui5-li-custom]").first().should("be.focused");
1387+
1388+
// Navigate to second list item with ArrowDown
1389+
cy.realPress("ArrowDown");
1390+
cy.get("[ui5-li-custom]").last().should("be.focused");
1391+
1392+
// F7 should focus the second button (same index as previous item)
1393+
cy.realPress("F7");
1394+
cy.get("[ui5-button]").eq(4).should("be.focused").and("contain", "Item 2 - Second");
1395+
});
1396+
13561397
it("keyboard handling on TAB when 2 level nested UI5Element is focused", () => {
13571398
cy.mount(
13581399
<div>

packages/main/src/List.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,7 @@ class List extends UI5Element {
534534
_beforeElement?: HTMLElement | null;
535535
_afterElement?: HTMLElement | null;
536536
_startMarkerOutOfView: boolean = false;
537+
_lastFocusedElementIndex?: number;
537538

538539
handleResizeCallback: ResizeObserverCallback;
539540
onItemFocusedBound: (e: CustomEvent) => void;

packages/main/src/ListItem.ts

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
66
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
77
import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js";
88
import { getFirstFocusableElement } from "@ui5/webcomponents-base/dist/util/FocusableElements.js";
9+
import { getTabbableElements } from "@ui5/webcomponents-base/dist/util/TabbableElements.js";
910
import type { AccessibilityAttributes, AriaRole, AriaHasPopup } from "@ui5/webcomponents-base";
1011
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
1112
import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
@@ -21,6 +22,7 @@ import ListItemBase from "./ListItemBase.js";
2122
import type RadioButton from "./RadioButton.js";
2223
import type CheckBox from "./CheckBox.js";
2324
import type { IButton } from "./Button.js";
25+
import type List from "./List.js";
2426
import {
2527
DELETE,
2628
ARIA_LABEL_LIST_ITEM_CHECKBOX,
@@ -200,12 +202,6 @@ abstract class ListItem extends ListItemBase {
200202
@property()
201203
mediaRange = "S";
202204

203-
/**
204-
* Stores the last focused element within the list item when navigating with F7.
205-
* @private
206-
*/
207-
_lastInnerFocusedElement?: HTMLElement;
208-
209205
/**
210206
* Defines the delete button, displayed in "Delete" mode.
211207
* **Note:** While the slot allows custom buttons, to match
@@ -521,24 +517,21 @@ abstract class ListItem extends ListItemBase {
521517
return this.shadowRoot!.querySelector("li");
522518
}
523519

524-
async _handleF7(e: KeyboardEvent) {
525-
e.preventDefault(); // Prevent browser default behavior (F7 = Caret Browsing toggle)
520+
_handleF7(e: KeyboardEvent) {
521+
e.preventDefault();
526522

527523
const focusDomRef = this.getFocusDomRef()!;
528524
const activeElement = getActiveElement();
525+
const list = this._getList();
529526

530527
if (activeElement === focusDomRef) {
531-
// On list item - restore to stored element or go to first focusable
532-
if (this._lastInnerFocusedElement) {
533-
this._lastInnerFocusedElement.focus();
534-
} else {
535-
const firstFocusable = await getFirstFocusableElement(focusDomRef);
536-
firstFocusable?.focus();
537-
this._lastInnerFocusedElement = firstFocusable || undefined;
538-
}
528+
// Navigate from list item into content
529+
this._focusInternalElement(list);
539530
} else {
540-
// On internal element - store it and go back to list item
541-
this._lastInnerFocusedElement = activeElement as HTMLElement;
531+
// Navigate from content back to list item
532+
if (activeElement) {
533+
this._updateStoredFocusIndex(list, activeElement as HTMLElement);
534+
}
542535
focusDomRef.focus();
543536
}
544537
}
@@ -556,6 +549,45 @@ abstract class ListItem extends ListItemBase {
556549
focusDomRef.focus();
557550
}
558551
}
552+
553+
_getList(): List | null {
554+
return this.closest("[ui5-list]");
555+
}
556+
557+
_getFocusableElements(): HTMLElement[] {
558+
const focusDomRef = this.getFocusDomRef()!;
559+
return getTabbableElements(focusDomRef);
560+
}
561+
562+
_focusInternalElement(list: List | null) {
563+
const focusables = this._getFocusableElements();
564+
if (!focusables.length) {
565+
return;
566+
}
567+
568+
const targetIndex = list?._lastFocusedElementIndex ?? 0;
569+
const safeIndex = Math.min(targetIndex, focusables.length - 1);
570+
const elementToFocus = focusables[safeIndex];
571+
572+
elementToFocus.focus();
573+
574+
if (list) {
575+
list._lastFocusedElementIndex = safeIndex;
576+
}
577+
}
578+
579+
_updateStoredFocusIndex(list: List | null, activeElement: HTMLElement) {
580+
if (!list) {
581+
return;
582+
}
583+
584+
const focusables = this._getFocusableElements();
585+
const currentIndex = focusables.indexOf(activeElement);
586+
587+
if (currentIndex !== -1) {
588+
list._lastFocusedElementIndex = currentIndex;
589+
}
590+
}
559591
}
560592

561593
export default ListItem;

packages/main/test/pages/ListItemCustomF7.html

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,22 @@ <h2>F7/F2 Key Test</h2>
3838
<p><strong>F7 vs F2 Behavior:</strong></p>
3939
<ul>
4040
<li><strong>F2:</strong> Simple navigation - always goes to first focusable element</li>
41-
<li><strong>F7:</strong> Smart navigation - remembers last focused element</li>
41+
<li><strong>F7:</strong> Smart navigation - remembers last focused element position across items</li>
4242
</ul>
4343

4444
<p><strong>Test Steps:</strong></p>
4545
<ol>
46-
<li>Click on a list item</li>
46+
<li>Click on first list item</li>
4747
<li>Press <strong>F7</strong> → should go to first button</li>
4848
<li>Press <strong>TAB</strong> to move to second button</li>
4949
<li>Press <strong>F7</strong> → should return to list item</li>
50-
<li>Press <strong>F7</strong> again → should return to second button (memory working)</li>
50+
<li>Press <strong>ArrowDown</strong> → should go to second list item</li>
51+
<li>Press <strong>F7</strong> → should go to SECOND button (maintains position!)</li>
5152
<li>Test <strong>F2</strong> → should always go to first button (no memory)</li>
5253
</ol>
5354
</div>
5455

55-
<ui5-list id="test-list">
56+
<ui5-list id="test-list" header-text="List with Custom Items">
5657
<ui5-li-custom>
5758
<div class="buttons">
5859
<ui5-button>First Button</ui5-button>

0 commit comments

Comments
 (0)