Skip to content

Commit 2e72928

Browse files
committed
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.
1 parent bafb11a commit 2e72928

File tree

2 files changed

+94
-106
lines changed

2 files changed

+94
-106
lines changed

packages/main/src/List.ts

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
1111
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
1212
import type { ClassMap } from "@ui5/webcomponents-base/dist/types.js";
1313
import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js";
14+
import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js";
1415
import {
1516
isTabNext,
1617
isSpace,
@@ -21,6 +22,7 @@ import {
2122
isHome,
2223
isDown,
2324
isUp,
25+
isF7,
2426
} from "@ui5/webcomponents-base/dist/Keys.js";
2527
import DragAndDropHandler from "./delegate/DragAndDropHandler.js";
2628
import type { MoveEventDetail } from "@ui5/webcomponents-base/dist/util/dragAndDrop/DragRegistry.js";
@@ -42,11 +44,11 @@ import ListSelectionMode from "./types/ListSelectionMode.js";
4244
import ListGrowingMode from "./types/ListGrowingMode.js";
4345
import ListAccessibleRole from "./types/ListAccessibleRole.js";
4446
import type ListItemBase from "./ListItemBase.js";
47+
import type ListItem from "./ListItem.js";
4548
import type {
4649
ListItemBasePressEventDetail,
4750
} from "./ListItemBase.js";
4851
import type DropIndicator from "./DropIndicator.js";
49-
import type ListItem from "./ListItem.js";
5052
import type {
5153
SelectionRequestEventDetail,
5254
} from "./ListItem.js";
@@ -988,10 +990,19 @@ class List extends UI5Element {
988990
return;
989991
}
990992

991-
if (isDown(e)) {
992-
if (this._handleDown()) {
993+
// Handle Arrow Up/Down navigation between internal elements
994+
const isArrowKey = isUp(e) || isDown(e);
995+
const listItem = this._getClosestListItem(e.target as HTMLElement);
996+
if (listItem?._isFocusOnInternalElement() && isArrowKey) {
997+
const offset = isUp(e) ? -1 : 1;
998+
if (this._navigateToAdjacentItem(listItem, offset)) {
993999
e.preventDefault();
1000+
return;
9941001
}
1002+
}
1003+
1004+
if (isDown(e)) {
1005+
this._handleDown(e);
9951006
return;
9961007
}
9971008

@@ -1003,6 +1014,35 @@ class List extends UI5Element {
10031014
if (isTabNext(e)) {
10041015
this._handleTabNext(e);
10051016
}
1017+
1018+
if (isF7(e)) {
1019+
this._handleF7(e);
1020+
}
1021+
}
1022+
1023+
_handleF7(e: KeyboardEvent) {
1024+
const listItem = this._getClosestListItem(e.target as HTMLElement);
1025+
if (!listItem || !listItem._hasFocusableElements()) {
1026+
return;
1027+
}
1028+
1029+
const listItemDomRef = listItem.getFocusDomRef()!;
1030+
const activeElement = getActiveElement();
1031+
1032+
e.preventDefault();
1033+
1034+
if (activeElement === listItemDomRef) {
1035+
listItem._focusInternalElement(this._lastFocusedElementIndex ?? 0);
1036+
this._lastFocusedElementIndex = listItem._getFocusedElementIndex();
1037+
} else {
1038+
this._lastFocusedElementIndex = listItem._getFocusedElementIndex();
1039+
listItemDomRef.focus();
1040+
}
1041+
}
1042+
1043+
_getClosestListItem(element: HTMLElement): ListItem | null {
1044+
const listItem = element.closest<ListItem>("[ui5-li], [ui5-li-custom]");
1045+
return listItem;
10061046
}
10071047

10081048
_moveItem(item: ListItemBase, e: KeyboardEvent) {
@@ -1165,15 +1205,41 @@ class List extends UI5Element {
11651205
return;
11661206
}
11671207

1168-
this._shouldFocusGrowingButton();
1208+
if (this._shouldFocusGrowingButton()) {
1209+
this.focusGrowingButton();
1210+
}
11691211
}
11701212

1171-
_handleDown() {
1172-
if (!this.growsWithButton) {
1213+
_handleDown(e: KeyboardEvent) {
1214+
if (this._shouldFocusGrowingButton()) {
1215+
this.focusGrowingButton();
1216+
e.preventDefault();
1217+
}
1218+
}
1219+
1220+
_navigateToAdjacentItem(listItem: ListItem, offset: -1 | 1): boolean {
1221+
const targetInternalElementIndex = listItem?._getFocusedElementIndex();
1222+
if (targetInternalElementIndex === undefined || targetInternalElementIndex === -1) {
1223+
return false;
1224+
}
1225+
1226+
const allItems = this.getItems().filter(node => {
1227+
return "hasConfigurableMode" in node && node.hasConfigurableMode
1228+
&& (node as ListItem)._hasFocusableElements();
1229+
}) as ListItem[];
1230+
1231+
const itemIndex = allItems.indexOf(listItem) + offset;
1232+
const nextNode = allItems[itemIndex];
1233+
1234+
if (!nextNode) {
11731235
return false;
11741236
}
11751237

1176-
return this._shouldFocusGrowingButton();
1238+
const focusedIndex = nextNode._focusInternalElement(targetInternalElementIndex);
1239+
if (focusedIndex !== undefined) {
1240+
this._lastFocusedElementIndex = focusedIndex;
1241+
}
1242+
return true;
11771243
}
11781244

11791245
_onfocusin(e: FocusEvent) {
@@ -1346,12 +1412,14 @@ class List extends UI5Element {
13461412
}
13471413

13481414
_shouldFocusGrowingButton() {
1415+
if (!this.growsWithButton) {
1416+
return false;
1417+
}
13491418
const items = this.getItems();
13501419
const lastIndex = items.length - 1;
13511420
const currentIndex = this._itemNavigation._currentIndex;
13521421

13531422
if (currentIndex !== -1 && currentIndex === lastIndex) {
1354-
this.focusGrowingButton();
13551423
return true;
13561424
}
13571425
return false;

packages/main/src/ListItem.ts

Lines changed: 18 additions & 98 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, isUp, isDown,
3+
isSpace, isEnter, isDelete, isF2,
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";
@@ -22,7 +22,6 @@ import ListItemBase from "./ListItemBase.js";
2222
import type RadioButton from "./RadioButton.js";
2323
import type CheckBox from "./CheckBox.js";
2424
import type { IButton } from "./Button.js";
25-
import type List from "./List.js";
2625
import {
2726
DELETE,
2827
ARIA_LABEL_LIST_ITEM_CHECKBOX,
@@ -258,23 +257,12 @@ abstract class ListItem extends ListItemBase {
258257
}
259258

260259
_onkeydown(e: KeyboardEvent) {
261-
const isInternalElementFocused = this._isTargetSelfFocusDomRef(e);
260+
const isInternalElementFocused = e.target !== this.getFocusDomRef();
262261

263262
if ((isSpace(e) || isEnter(e)) && isInternalElementFocused) {
264263
return;
265264
}
266265

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-
278266
super._onkeydown(e);
279267

280268
const itemActive = this.type === ListItemType.Active,
@@ -287,10 +275,6 @@ abstract class ListItem extends ListItemBase {
287275
if (isF2(e)) {
288276
this._handleF2();
289277
}
290-
291-
if (isF7(e)) {
292-
this._handleF7(e);
293-
}
294278
}
295279

296280
_onkeyup(e: KeyboardEvent) {
@@ -356,13 +340,6 @@ abstract class ListItem extends ListItemBase {
356340
}
357341
}
358342

359-
_isTargetSelfFocusDomRef(e: KeyboardEvent): boolean {
360-
const target = e.target as HTMLElement,
361-
focusDomRef = this.getFocusDomRef();
362-
363-
return target !== focusDomRef;
364-
}
365-
366343
/**
367344
* Called when selection components in Single (ui5-radio-button)
368345
* and Multi (ui5-checkbox) selection modes are used.
@@ -530,32 +507,6 @@ abstract class ListItem extends ListItemBase {
530507
return this.shadowRoot!.querySelector("li");
531508
}
532509

533-
_getList(): List | null {
534-
return this.closest("[ui5-list]");
535-
}
536-
537-
_handleF7(e: KeyboardEvent) {
538-
const focusDomRef = this.getFocusDomRef()!;
539-
const activeElement = getActiveElement();
540-
const list = this._getList();
541-
542-
const focusables = this._getFocusableElements().length > 0;
543-
if (!focusables) {
544-
return;
545-
}
546-
547-
e.preventDefault();
548-
549-
if (activeElement === focusDomRef) {
550-
this._focusInternalElement(list);
551-
} else {
552-
if (activeElement) {
553-
this._updateStoredFocusIndex(list, activeElement as HTMLElement);
554-
}
555-
focusDomRef.focus();
556-
}
557-
}
558-
559510
async _handleF2() {
560511
const focusDomRef = this.getFocusDomRef()!;
561512
const activeElement = getActiveElement();
@@ -578,65 +529,34 @@ abstract class ListItem extends ListItemBase {
578529
return getTabbableElements(focusDomRef);
579530
}
580531

581-
_focusInternalElement(list: List | null) {
532+
_getFocusedElementIndex(): number {
582533
const focusables = this._getFocusableElements();
583-
if (!focusables.length) {
584-
return;
585-
}
586-
587-
const targetIndex = list?._lastFocusedElementIndex ?? 0;
588-
const safeIndex = Math.min(targetIndex, focusables.length - 1);
589-
const elementToFocus = focusables[safeIndex];
590-
591-
elementToFocus.focus();
592-
593-
if (list) {
594-
list._lastFocusedElementIndex = safeIndex;
595-
}
534+
const activeElement = getActiveElement() as HTMLElement;
535+
return focusables.indexOf(activeElement);
596536
}
597537

598-
_updateStoredFocusIndex(list: List | null, activeElement: HTMLElement) {
599-
if (!list) {
600-
return;
601-
}
538+
_hasFocusableElements(): boolean {
539+
return this._getFocusableElements().length > 0;
540+
}
602541

542+
_isFocusOnInternalElement(): boolean {
603543
const focusables = this._getFocusableElements();
604-
const currentIndex = focusables.indexOf(activeElement);
605-
606-
if (currentIndex !== -1) {
607-
list._lastFocusedElementIndex = currentIndex;
608-
}
544+
const currentElementIndex = focusables.indexOf(getActiveElement() as HTMLElement);
545+
return currentElementIndex !== -1;
609546
}
610547

611-
_navigateToAdjacentItem(offset: -1 | 1): boolean {
612-
const list = this._getList();
613-
if (!list) {
614-
return false;
615-
}
616-
548+
_focusInternalElement(targetIndex: number) {
617549
const focusables = this._getFocusableElements();
618-
const currentElementIndex = focusables.indexOf(getActiveElement() as HTMLElement);
619-
if (currentElementIndex === -1) {
620-
return false;
550+
if (!focusables.length) {
551+
return;
621552
}
622553

623-
const allItems = list.getItems().filter(item => "hasConfigurableMode" in item && item.hasConfigurableMode) as ListItem[];
624-
let itemIndex = allItems.indexOf(this as ListItem) + offset;
625-
626-
while (itemIndex >= 0 && itemIndex < allItems.length) {
627-
const targetFocusables = allItems[itemIndex]._getFocusableElements();
628-
629-
if (targetFocusables.length > 0) {
630-
const elementIndex = Math.min(currentElementIndex, targetFocusables.length - 1);
631-
targetFocusables[elementIndex].focus();
632-
list._lastFocusedElementIndex = elementIndex;
633-
return true;
634-
}
554+
const safeIndex = Math.min(targetIndex, focusables.length - 1);
555+
const elementToFocus = focusables[safeIndex];
635556

636-
itemIndex += offset;
637-
}
557+
elementToFocus.focus();
638558

639-
return false;
559+
return safeIndex;
640560
}
641561
}
642562

0 commit comments

Comments
 (0)