Skip to content

Commit 567c8eb

Browse files
committed
feat(ui5-li-custom): implement F7 keyboard navigation
F7 key enables navigation between list item and internal focusable elements: - If focus is on item level, moves focus to previously focused internal element (or first if none) - If focus is on internal element, saves focus position and moves back to item level - Add Cypress tests for F7 functionality - Add test page for manual F7 validation Jira: BGSOFUIPIRIN-6942 Related: #11987
1 parent 7df4b03 commit 567c8eb

File tree

4 files changed

+212
-13
lines changed

4 files changed

+212
-13
lines changed

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,73 @@ describe("List Tests", () => {
12861286
cy.get("[ui5-li-custom]").first().should("be.focused");
12871287
});
12881288

1289+
it("keyboard handling on F7", () => {
1290+
cy.mount(
1291+
<List>
1292+
<ListItemCustom>
1293+
<Button>First</Button>
1294+
<Button>Second</Button>
1295+
</ListItemCustom>
1296+
</List>
1297+
);
1298+
1299+
cy.get("[ui5-li-custom]").click();
1300+
cy.get("[ui5-li-custom]").should("be.focused");
1301+
1302+
// F7 goes to first focusable element
1303+
cy.realPress("F7");
1304+
cy.get("[ui5-button]").first().should("be.focused");
1305+
1306+
// Tab to second button
1307+
cy.realPress("Tab");
1308+
cy.get("[ui5-button]").last().should("be.focused");
1309+
1310+
// F7 returns to list item
1311+
cy.realPress("F7");
1312+
cy.get("[ui5-li-custom]").should("be.focused");
1313+
1314+
// F7 remembers last focused element (second button)
1315+
cy.realPress("F7");
1316+
cy.get("[ui5-button]").last().should("be.focused");
1317+
});
1318+
1319+
it("keyboard handling on F7 after TAB navigation", () => {
1320+
cy.mount(
1321+
<div>
1322+
<button>Before</button>
1323+
<List>
1324+
<ListItemCustom>
1325+
<Button>First</Button>
1326+
<Button>Second</Button>
1327+
</ListItemCustom>
1328+
</List>
1329+
</div>
1330+
);
1331+
1332+
cy.get("button").click();
1333+
cy.get("button").should("be.focused");
1334+
1335+
// Tab into list item
1336+
cy.realPress("Tab");
1337+
cy.get("[ui5-li-custom]").should("be.focused");
1338+
1339+
// Tab into internal elements (goes to first button)
1340+
cy.realPress("Tab");
1341+
cy.get("[ui5-button]").first().should("be.focused");
1342+
1343+
// Tab to second button
1344+
cy.realPress("Tab");
1345+
cy.get("[ui5-button]").last().should("be.focused");
1346+
1347+
// F7 should store current element and return to list item
1348+
cy.realPress("F7");
1349+
cy.get("[ui5-li-custom]").should("be.focused");
1350+
1351+
// F7 should remember the second button (not go to first)
1352+
cy.realPress("F7");
1353+
cy.get("[ui5-button]").last().should("be.focused");
1354+
});
1355+
12891356
it("keyboard handling on TAB when 2 level nested UI5Element is focused", () => {
12901357
cy.mount(
12911358
<div>

packages/main/src/ListItem.ts

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
22
import {
3-
isSpace, isEnter, isDelete, isF2,
3+
isSpace, isEnter, isDelete, isF2, isF7,
44
} from "@ui5/webcomponents-base/dist/Keys.js";
55
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
66
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
@@ -200,6 +200,12 @@ abstract class ListItem extends ListItemBase {
200200
@property()
201201
mediaRange = "S";
202202

203+
/**
204+
* Stores the last focused element within the list item when navigating with F7.
205+
* @private
206+
*/
207+
_lastInnerFocusedElement?: HTMLElement;
208+
203209
/**
204210
* Defines the delete button, displayed in "Delete" mode.
205211
* **Note:** While the slot allows custom buttons, to match
@@ -255,7 +261,7 @@ abstract class ListItem extends ListItemBase {
255261
document.removeEventListener("touchend", this.deactivate);
256262
}
257263

258-
async _onkeydown(e: KeyboardEvent) {
264+
_onkeydown(e: KeyboardEvent) {
259265
if ((isSpace(e) || isEnter(e)) && this._isTargetSelfFocusDomRef(e)) {
260266
return;
261267
}
@@ -270,15 +276,11 @@ abstract class ListItem extends ListItemBase {
270276
}
271277

272278
if (isF2(e)) {
273-
const activeElement = getActiveElement();
274-
const focusDomRef = this.getFocusDomRef()!;
279+
this._handleF2();
280+
}
275281

276-
if (activeElement === focusDomRef) {
277-
const firstFocusable = await getFirstFocusableElement(focusDomRef);
278-
firstFocusable?.focus();
279-
} else {
280-
focusDomRef.focus();
281-
}
282+
if (isF7(e)) {
283+
this._handleF7(e);
282284
}
283285
}
284286

@@ -518,6 +520,42 @@ abstract class ListItem extends ListItemBase {
518520
get _listItem() {
519521
return this.shadowRoot!.querySelector("li");
520522
}
523+
524+
async _handleF7(e: KeyboardEvent) {
525+
e.preventDefault(); // Prevent browser default behavior (F7 = Caret Browsing toggle)
526+
527+
const focusDomRef = this.getFocusDomRef()!;
528+
const activeElement = getActiveElement();
529+
530+
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+
}
539+
} else {
540+
// On internal element - store it and go back to list item
541+
this._lastInnerFocusedElement = activeElement as HTMLElement;
542+
focusDomRef.focus();
543+
}
544+
}
545+
546+
async _handleF2() {
547+
const focusDomRef = this.getFocusDomRef()!;
548+
const activeElement = getActiveElement();
549+
550+
if (activeElement === focusDomRef) {
551+
// On list item - always go to first focusable (no memory)
552+
const firstFocusable = await getFirstFocusableElement(focusDomRef);
553+
firstFocusable?.focus();
554+
} else {
555+
// On internal element - go back to list item
556+
focusDomRef.focus();
557+
}
558+
}
521559
}
522560

523561
export default ListItem;

packages/main/src/ListItemCustom.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { isTabNext, isTabPrevious, isF2 } from "@ui5/webcomponents-base/dist/Keys.js";
1+
import {
2+
isTabNext, isTabPrevious, isF2, isF7,
3+
} from "@ui5/webcomponents-base/dist/Keys.js";
24
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
35
import type { ClassMap } from "@ui5/webcomponents-base/dist/types.js";
46
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
@@ -58,7 +60,7 @@ class ListItemCustom extends ListItem {
5860
const isTab = isTabNext(e) || isTabPrevious(e);
5961
const isFocused = this.matches(":focus");
6062

61-
if (!isTab && !isFocused && !isF2(e)) {
63+
if (!isTab && !isFocused && !isF2(e) && !isF7(e)) {
6264
return;
6365
}
6466

@@ -69,7 +71,7 @@ class ListItemCustom extends ListItem {
6971
const isTab = isTabNext(e) || isTabPrevious(e);
7072
const isFocused = this.matches(":focus");
7173

72-
if (!isTab && !isFocused && !isF2(e)) {
74+
if (!isTab && !isFocused && !isF2(e) && !isF7(e)) {
7375
return;
7476
}
7577

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>F7/F2 Key Test</title>
8+
<script src="%VITE_BUNDLE_PATH%" type="module"></script>
9+
<style>
10+
body {
11+
margin: 0;
12+
padding: 2rem;
13+
font-family: "72", "72full", Arial, sans-serif;
14+
}
15+
16+
.instructions {
17+
padding: 1rem;
18+
margin-bottom: 2rem;
19+
border: 1px solid #ccc;
20+
background: #f9f9f9;
21+
}
22+
23+
.buttons {
24+
display: flex;
25+
gap: 1rem;
26+
align-items: center;
27+
}
28+
29+
#test-list {
30+
width: 400px;
31+
}
32+
</style>
33+
</head>
34+
35+
<body>
36+
<div class="instructions">
37+
<h2>F7/F2 Key Test</h2>
38+
<p><strong>F7 vs F2 Behavior:</strong></p>
39+
<ul>
40+
<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>
42+
</ul>
43+
44+
<p><strong>Test Steps:</strong></p>
45+
<ol>
46+
<li>Click on a list item</li>
47+
<li>Press <strong>F7</strong> → should go to first button</li>
48+
<li>Press <strong>TAB</strong> to move to second button</li>
49+
<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>
51+
<li>Test <strong>F2</strong> → should always go to first button (no memory)</li>
52+
</ol>
53+
</div>
54+
55+
<ui5-list id="test-list">
56+
<ui5-li-custom>
57+
<div class="buttons">
58+
<ui5-button>First Button</ui5-button>
59+
<ui5-button>Second Button</ui5-button>
60+
<ui5-input placeholder="Input Field" style="width: 120px;"></ui5-input>
61+
</div>
62+
</ui5-li-custom>
63+
<ui5-li-custom>
64+
<div class="buttons">
65+
<ui5-button>Button A</ui5-button>
66+
<ui5-button>Button B</ui5-button>
67+
<ui5-input placeholder="Text Input" style="width: 120px;"></ui5-input>
68+
</div>
69+
</ui5-li-custom>
70+
</ui5-list>
71+
72+
<script>
73+
document.addEventListener('focusin', function (e) {
74+
const tag = e.target.tagName.toLowerCase();
75+
let label = e.target.textContent?.trim() || e.target.placeholder || tag;
76+
77+
if (tag === 'ui5-li-custom') {
78+
const index = Array.from(document.querySelectorAll('ui5-li-custom')).indexOf(e.target);
79+
label = `Item ${index + 1}`;
80+
}
81+
82+
console.log('Focus:', label);
83+
});
84+
85+
document.addEventListener('keydown', function (e) {
86+
if (e.key === 'F7') console.log('F7 pressed');
87+
if (e.key === 'F2') console.log('F2 pressed');
88+
});
89+
</script>
90+
</body>
91+
92+
</html>

0 commit comments

Comments
 (0)