Skip to content

Commit f672e13

Browse files
authored
feat(ui5-li-custom): enhance arrow key navigation (#12700)
* 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 * fix: remove unnecessary async/await from _onkeydown methods Fixes TypeScript linter errors for awaiting non-Promise parent calls * 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. * feat(ui5-li-custom): implement arrow key navigation for internal elements Arrow Up/Down keys now navigate between same-index focusable elements across list items when focus is inside the internal content of list items. This allows users to navigate column-wise through structured list items. Key features: - Navigate between corresponding elements across items (e.g., first button to first button in next/previous item) - Automatically skip items without focusable elements - Works across ui5-li-group boundaries - Preserves existing F2/F7/Tab navigation - Only prevents default scroll when List handles the event (growing button) * test(ui5-li-custom): add arrow navigation test page Add comprehensive test page for arrow key navigation between internal elements across list items. Includes 8 test examples covering basic navigation, mixed items, groups, boundary cases, selection modes, and nested lists. * test(ui5-list): add arrow navigation tests for custom items Add cypress tests for arrow key navigation between internal elements across list items. Tests cover basic navigation, skipping standard items, group boundaries, varying element counts, and boundary conditions. * fix(ui5-li): only handle F7/F2 when tabbable elements exist F7 and F2 navigation now checks if there are tabbable elements inside the list item before handling the event. If no tabbable elements exist (e.g., delete button with tabindex="-1"), the event propagates to allow parent components like Tokenizer to handle it with their custom logic. This fixes interference with Tokenizer's F7 behavior while maintaining correct navigation for custom list items. * refactor(ui5-list): move internal element navigation logic to List component Move F7 and Arrow Up/Down keyboard navigation logic from ListItem to List to centralize list-level navigation behavior and improve maintainability. Changes: - Move _handleF7 navigation logic from ListItem to List - Move _navigateToAdjacentItem logic from ListItem to List - Add _getClosestListItem helper in List using scoping-safe attribute selector - ListItem now only provides utility methods: _getFocusedElementIndex, _hasFocusableElements, _isFocusOnInternalElement, _focusInternalElement - Remove List reference from ListItem (_getList method removed) This refactoring ensures navigation logic is in the appropriate component and follows UI5 Web Components architectural patterns. * refactor(ui5-list): simplify _shouldFocusGrowingButton logic
1 parent 288fd1a commit f672e13

File tree

8 files changed

+844
-45
lines changed

8 files changed

+844
-45
lines changed

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

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,291 @@ 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]").realClick();
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").realClick();
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+
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().realClick();
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+
1397+
it("arrow down navigates to same-index element in next custom item", () => {
1398+
cy.mount(
1399+
<List>
1400+
<ListItemCustom>
1401+
<Button>Item 1 - First</Button>
1402+
<Button>Item 1 - Second</Button>
1403+
</ListItemCustom>
1404+
<ListItemCustom>
1405+
<Button>Item 2 - First</Button>
1406+
<Button>Item 2 - Second</Button>
1407+
</ListItemCustom>
1408+
<ListItemCustom>
1409+
<Button>Item 3 - First</Button>
1410+
<Button>Item 3 - Second</Button>
1411+
</ListItemCustom>
1412+
</List>
1413+
);
1414+
1415+
// Focus first button in first item
1416+
cy.get("[ui5-button]").first().realClick();
1417+
cy.get("[ui5-button]").first().should("be.focused");
1418+
1419+
// Arrow down should move to first button in second item
1420+
cy.realPress("ArrowDown");
1421+
cy.get("[ui5-button]").eq(2).should("be.focused").and("contain", "Item 2 - First");
1422+
1423+
// Arrow down again should move to first button in third item
1424+
cy.realPress("ArrowDown");
1425+
cy.get("[ui5-button]").eq(4).should("be.focused").and("contain", "Item 3 - First");
1426+
});
1427+
1428+
it("arrow up navigates to same-index element in previous custom item", () => {
1429+
cy.mount(
1430+
<List>
1431+
<ListItemCustom>
1432+
<Button>Item 1 - First</Button>
1433+
<Button>Item 1 - Second</Button>
1434+
</ListItemCustom>
1435+
<ListItemCustom>
1436+
<Button>Item 2 - First</Button>
1437+
<Button>Item 2 - Second</Button>
1438+
</ListItemCustom>
1439+
<ListItemCustom>
1440+
<Button>Item 3 - First</Button>
1441+
<Button>Item 3 - Second</Button>
1442+
</ListItemCustom>
1443+
</List>
1444+
);
1445+
1446+
// Focus second button in last item
1447+
cy.get("[ui5-button]").eq(5).realClick();
1448+
cy.get("[ui5-button]").eq(5).should("be.focused");
1449+
1450+
// Arrow up should move to second button in second item
1451+
cy.realPress("ArrowUp");
1452+
cy.get("[ui5-button]").eq(3).should("be.focused").and("contain", "Item 2 - Second");
1453+
1454+
// Arrow up again should move to second button in first item
1455+
cy.realPress("ArrowUp");
1456+
cy.get("[ui5-button]").eq(1).should("be.focused").and("contain", "Item 1 - Second");
1457+
});
1458+
1459+
it("arrow navigation skips standard list items", () => {
1460+
cy.mount(
1461+
<List>
1462+
<ListItemCustom>
1463+
<Button>Custom 1</Button>
1464+
</ListItemCustom>
1465+
<ListItemStandard>Standard Item</ListItemStandard>
1466+
<ListItemStandard>Another Standard</ListItemStandard>
1467+
<ListItemCustom>
1468+
<Button>Custom 2</Button>
1469+
</ListItemCustom>
1470+
</List>
1471+
);
1472+
1473+
// Focus button in first custom item
1474+
cy.get("[ui5-button]").first().realClick();
1475+
cy.get("[ui5-button]").first().should("be.focused");
1476+
1477+
// Arrow down should skip standard items and focus button in second custom item
1478+
cy.realPress("ArrowDown");
1479+
cy.get("[ui5-button]").last().should("be.focused").and("contain", "Custom 2");
1480+
1481+
// Arrow up should skip standard items and return to first custom item
1482+
cy.realPress("ArrowUp");
1483+
cy.get("[ui5-button]").first().should("be.focused").and("contain", "Custom 1");
1484+
});
1485+
1486+
it("arrow navigation works across groups", () => {
1487+
cy.mount(
1488+
<List>
1489+
<ListItemCustom>
1490+
<Button>Before Group</Button>
1491+
</ListItemCustom>
1492+
<ListItemGroup headerText="Group 1">
1493+
<ListItemCustom>
1494+
<Button>In Group 1</Button>
1495+
</ListItemCustom>
1496+
</ListItemGroup>
1497+
<ListItemGroup headerText="Group 2">
1498+
<ListItemCustom>
1499+
<Button>In Group 2</Button>
1500+
</ListItemCustom>
1501+
</ListItemGroup>
1502+
<ListItemCustom>
1503+
<Button>After Group</Button>
1504+
</ListItemCustom>
1505+
</List>
1506+
);
1507+
1508+
// Focus button before groups
1509+
cy.get("[ui5-button]").first().realClick();
1510+
1511+
// Navigate down through groups
1512+
cy.realPress("ArrowDown");
1513+
cy.get("[ui5-button]").eq(1).should("be.focused").and("contain", "In Group 1");
1514+
1515+
cy.realPress("ArrowDown");
1516+
cy.get("[ui5-button]").eq(2).should("be.focused").and("contain", "In Group 2");
1517+
1518+
cy.realPress("ArrowDown");
1519+
cy.get("[ui5-button]").last().should("be.focused").and("contain", "After Group");
1520+
});
1521+
1522+
it("arrow navigation handles items with different element counts", () => {
1523+
cy.mount(
1524+
<List>
1525+
<ListItemCustom>
1526+
<Button>Item 1 - A</Button>
1527+
<Button>Item 1 - B</Button>
1528+
<Button>Item 1 - C</Button>
1529+
<Button>Item 1 - D</Button>
1530+
</ListItemCustom>
1531+
<ListItemCustom>
1532+
<Button>Item 2 - A</Button>
1533+
<Button>Item 2 - B</Button>
1534+
</ListItemCustom>
1535+
</List>
1536+
);
1537+
1538+
// Focus fourth button (index 3) in first item
1539+
cy.get("[ui5-button]").eq(3).realClick();
1540+
cy.get("[ui5-button]").eq(3).should("be.focused");
1541+
1542+
// Arrow down should focus last button in second item (index clamped to 1)
1543+
cy.realPress("ArrowDown");
1544+
cy.get("[ui5-button]").eq(5).should("be.focused").and("contain", "Item 2 - B");
1545+
});
1546+
1547+
it("arrow navigation does nothing at list boundaries", () => {
1548+
cy.mount(
1549+
<List>
1550+
<ListItemCustom>
1551+
<Button>First Item</Button>
1552+
</ListItemCustom>
1553+
<ListItemCustom>
1554+
<Button>Last Item</Button>
1555+
</ListItemCustom>
1556+
</List>
1557+
);
1558+
1559+
// Focus first button
1560+
cy.get("[ui5-button]").first().realClick();
1561+
1562+
// Arrow up should do nothing (at top boundary)
1563+
cy.realPress("ArrowUp");
1564+
cy.get("[ui5-button]").first().should("be.focused");
1565+
1566+
// Focus last button
1567+
cy.get("[ui5-button]").last().realClick();
1568+
1569+
// Arrow down should do nothing (at bottom boundary)
1570+
cy.realPress("ArrowDown");
1571+
cy.get("[ui5-button]").last().should("be.focused");
1572+
});
1573+
12891574
it("keyboard handling on TAB when 2 level nested UI5Element is focused", () => {
12901575
cy.mount(
12911576
<div>

0 commit comments

Comments
 (0)