Skip to content

Commit 3f3a761

Browse files
committed
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)
1 parent 9ec7101 commit 3f3a761

File tree

3 files changed

+66
-17
lines changed

3 files changed

+66
-17
lines changed

packages/main/src/List.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -989,8 +989,9 @@ class List extends UI5Element {
989989
}
990990

991991
if (isDown(e)) {
992-
this._handleDown();
993-
e.preventDefault();
992+
if (this._handleDown()) {
993+
e.preventDefault();
994+
}
994995
return;
995996
}
996997

@@ -1169,10 +1170,10 @@ class List extends UI5Element {
11691170

11701171
_handleDown() {
11711172
if (!this.growsWithButton) {
1172-
return;
1173+
return false;
11731174
}
11741175

1175-
this._shouldFocusGrowingButton();
1176+
return this._shouldFocusGrowingButton();
11761177
}
11771178

11781179
_onfocusin(e: FocusEvent) {
@@ -1351,7 +1352,9 @@ class List extends UI5Element {
13511352

13521353
if (currentIndex !== -1 && currentIndex === lastIndex) {
13531354
this.focusGrowingButton();
1355+
return true;
13541356
}
1357+
return false;
13551358
}
13561359

13571360
getGrowingButton() {

packages/main/src/ListItem.ts

Lines changed: 46 additions & 2 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, isF7,
3+
isSpace, isEnter, isDelete, isF2, isF7, isUp, isDown,
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";
@@ -258,10 +258,23 @@ abstract class ListItem extends ListItemBase {
258258
}
259259

260260
_onkeydown(e: KeyboardEvent) {
261-
if ((isSpace(e) || isEnter(e)) && this._isTargetSelfFocusDomRef(e)) {
261+
const isInternalElementFocused = this._isTargetSelfFocusDomRef(e);
262+
263+
if ((isSpace(e) || isEnter(e)) && isInternalElementFocused) {
262264
return;
263265
}
264266

267+
// Handle Arrow Up/Down navigation between internal elements
268+
const isArrowKey = isUp(e) || isDown(e);
269+
270+
if (isInternalElementFocused && isArrowKey) {
271+
const offset = isUp(e) ? -1 : 1;
272+
if (this._navigateToAdjacentItem(offset)) {
273+
e.preventDefault();
274+
return;
275+
}
276+
}
277+
265278
super._onkeydown(e);
266279

267280
const itemActive = this.type === ListItemType.Active,
@@ -584,6 +597,37 @@ abstract class ListItem extends ListItemBase {
584597
list._lastFocusedElementIndex = currentIndex;
585598
}
586599
}
600+
601+
_navigateToAdjacentItem(offset: -1 | 1): boolean {
602+
const list = this._getList();
603+
if (!list) {
604+
return false;
605+
}
606+
607+
const focusables = this._getFocusableElements();
608+
const currentElementIndex = focusables.indexOf(getActiveElement() as HTMLElement);
609+
if (currentElementIndex === -1) {
610+
return false;
611+
}
612+
613+
const allItems = list.getItems().filter(item => "hasConfigurableMode" in item && item.hasConfigurableMode) as ListItem[];
614+
let itemIndex = allItems.indexOf(this as ListItem) + offset;
615+
616+
while (itemIndex >= 0 && itemIndex < allItems.length) {
617+
const targetFocusables = allItems[itemIndex]._getFocusableElements();
618+
619+
if (targetFocusables.length > 0) {
620+
const elementIndex = Math.min(currentElementIndex, targetFocusables.length - 1);
621+
targetFocusables[elementIndex].focus();
622+
list._lastFocusedElementIndex = elementIndex;
623+
return true;
624+
}
625+
626+
itemIndex += offset;
627+
}
628+
629+
return false;
630+
}
587631
}
588632

589633
export default ListItem;

packages/main/src/ListItemCustom.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
isTabNext, isTabPrevious, isF2, isF7,
2+
isTabNext, isTabPrevious, isF2, isF7, isUp, isDown,
33
} from "@ui5/webcomponents-base/dist/Keys.js";
44
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
55
import type { ClassMap } from "@ui5/webcomponents-base/dist/types.js";
@@ -57,25 +57,27 @@ class ListItemCustom extends ListItem {
5757
declare accessibleName?: string;
5858

5959
_onkeydown(e: KeyboardEvent) {
60-
const isTab = isTabNext(e) || isTabPrevious(e);
6160
const isFocused = this.matches(":focus");
61+
const shouldHandle = isFocused
62+
|| isTabNext(e) || isTabPrevious(e)
63+
|| isF2(e) || isF7(e)
64+
|| isUp(e) || isDown(e);
6265

63-
if (!isTab && !isFocused && !isF2(e) && !isF7(e)) {
64-
return;
66+
if (shouldHandle) {
67+
super._onkeydown(e);
6568
}
66-
67-
super._onkeydown(e);
6869
}
6970

7071
_onkeyup(e: KeyboardEvent) {
71-
const isTab = isTabNext(e) || isTabPrevious(e);
7272
const isFocused = this.matches(":focus");
73+
const shouldHandle = isFocused
74+
|| isTabNext(e) || isTabPrevious(e)
75+
|| isF2(e) || isF7(e)
76+
|| isUp(e) || isDown(e);
7377

74-
if (!isTab && !isFocused && !isF2(e) && !isF7(e)) {
75-
return;
78+
if (shouldHandle) {
79+
super._onkeyup(e);
7680
}
77-
78-
super._onkeyup(e);
7981
}
8082

8183
get classes(): ClassMap {

0 commit comments

Comments
 (0)