Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions packages/main/cypress/specs/List.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,114 @@ describe("List Tests", () => {
cy.get("[ui5-li-custom]").first().should("be.focused");
});

it("keyboard handling on F7", () => {
cy.mount(
<List>
<ListItemCustom>
<Button>First</Button>
<Button>Second</Button>
</ListItemCustom>
</List>
);

cy.get("[ui5-li-custom]").click();
cy.get("[ui5-li-custom]").should("be.focused");

// F7 goes to first focusable element
cy.realPress("F7");
cy.get("[ui5-button]").first().should("be.focused");

// Tab to second button
cy.realPress("Tab");
cy.get("[ui5-button]").last().should("be.focused");

// F7 returns to list item
cy.realPress("F7");
cy.get("[ui5-li-custom]").should("be.focused");

// F7 remembers last focused element (second button)
cy.realPress("F7");
cy.get("[ui5-button]").last().should("be.focused");
});

it("keyboard handling on F7 after TAB navigation", () => {
cy.mount(
<div>
<button>Before</button>
<List>
<ListItemCustom>
<Button>First</Button>
<Button>Second</Button>
</ListItemCustom>
</List>
</div>
);

cy.get("button").click();
cy.get("button").should("be.focused");

// Tab into list item
cy.realPress("Tab");
cy.get("[ui5-li-custom]").should("be.focused");

// Tab into internal elements (goes to first button)
cy.realPress("Tab");
cy.get("[ui5-button]").first().should("be.focused");

// Tab to second button
cy.realPress("Tab");
cy.get("[ui5-button]").last().should("be.focused");

// F7 should store current element and return to list item
cy.realPress("F7");
cy.get("[ui5-li-custom]").should("be.focused");

// F7 should remember the second button (not go to first)
cy.realPress("F7");
cy.get("[ui5-button]").last().should("be.focused");
});

it("keyboard handling on F7 maintains focus position across list items", () => {
cy.mount(
<List>
<ListItemCustom>
<Button>Item 1 - First</Button>
<Button>Item 1 - Second</Button>
<Button>Item 1 - Third</Button>
</ListItemCustom>
<ListItemCustom>
<Button>Item 2 - First</Button>
<Button>Item 2 - Second</Button>
<Button>Item 2 - Third</Button>
</ListItemCustom>
</List>
);

// Focus first list item
cy.get("[ui5-li-custom]").first().click();
cy.get("[ui5-li-custom]").first().should("be.focused");

// F7 to enter (should go to first button)
cy.realPress("F7");
cy.get("[ui5-button]").eq(0).should("be.focused");

// Tab to second button
cy.realPress("Tab");
cy.get("[ui5-button]").eq(1).should("be.focused");

// F7 to exit back to list item
cy.realPress("F7");
cy.get("[ui5-li-custom]").first().should("be.focused");

// Navigate to second list item with ArrowDown
cy.realPress("ArrowDown");
cy.get("[ui5-li-custom]").last().should("be.focused");

// F7 should focus the second button (same index as previous item)
cy.realPress("F7");
cy.get("[ui5-button]").eq(4).should("be.focused").and("contain", "Item 2 - Second");
});

it("keyboard handling on TAB when 2 level nested UI5Element is focused", () => {
cy.mount(
<div>
Expand Down
1 change: 1 addition & 0 deletions packages/main/src/List.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ class List extends UI5Element {
_beforeElement?: HTMLElement | null;
_afterElement?: HTMLElement | null;
_startMarkerOutOfView: boolean = false;
_lastFocusedElementIndex?: number;

handleResizeCallback: ResizeObserverCallback;
onItemFocusedBound: (e: CustomEvent) => void;
Expand Down
88 changes: 77 additions & 11 deletions packages/main/src/ListItem.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import {
isSpace, isEnter, isDelete, isF2,
isSpace, isEnter, isDelete, isF2, isF7,
} from "@ui5/webcomponents-base/dist/Keys.js";
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js";
import { getFirstFocusableElement } from "@ui5/webcomponents-base/dist/util/FocusableElements.js";
import { getTabbableElements } from "@ui5/webcomponents-base/dist/util/TabbableElements.js";
import type { AccessibilityAttributes, AriaRole, AriaHasPopup } from "@ui5/webcomponents-base";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import event from "@ui5/webcomponents-base/dist/decorators/event-strict.js";
Expand All @@ -21,6 +22,7 @@ import ListItemBase from "./ListItemBase.js";
import type RadioButton from "./RadioButton.js";
import type CheckBox from "./CheckBox.js";
import type { IButton } from "./Button.js";
import type List from "./List.js";
import {
DELETE,
ARIA_LABEL_LIST_ITEM_CHECKBOX,
Expand Down Expand Up @@ -255,7 +257,7 @@ abstract class ListItem extends ListItemBase {
document.removeEventListener("touchend", this.deactivate);
}

async _onkeydown(e: KeyboardEvent) {
_onkeydown(e: KeyboardEvent) {
if ((isSpace(e) || isEnter(e)) && this._isTargetSelfFocusDomRef(e)) {
return;
}
Expand All @@ -270,15 +272,11 @@ abstract class ListItem extends ListItemBase {
}

if (isF2(e)) {
const activeElement = getActiveElement();
const focusDomRef = this.getFocusDomRef()!;

if (activeElement === focusDomRef) {
const firstFocusable = await getFirstFocusableElement(focusDomRef);
firstFocusable?.focus();
} else {
focusDomRef.focus();
}
this._handleF2();
}

if (isF7(e)) {
this._handleF7(e);
}
}

Expand Down Expand Up @@ -518,6 +516,74 @@ abstract class ListItem extends ListItemBase {
get _listItem() {
return this.shadowRoot!.querySelector("li");
}

_getList(): List | null {
return this.closest("[ui5-list]");
}

_handleF7(e: KeyboardEvent) {
e.preventDefault();

const focusDomRef = this.getFocusDomRef()!;
const activeElement = getActiveElement();
const list = this._getList();

if (activeElement === focusDomRef) {
this._focusInternalElement(list);
} else {
if (activeElement) {
this._updateStoredFocusIndex(list, activeElement as HTMLElement);
}
focusDomRef.focus();
}
}

async _handleF2() {
const focusDomRef = this.getFocusDomRef()!;
const activeElement = getActiveElement();

if (activeElement === focusDomRef) {
const firstFocusable = await getFirstFocusableElement(focusDomRef);
firstFocusable?.focus();
} else {
focusDomRef.focus();
}
}

_getFocusableElements(): HTMLElement[] {
const focusDomRef = this.getFocusDomRef()!;
return getTabbableElements(focusDomRef);
}

_focusInternalElement(list: List | null) {
const focusables = this._getFocusableElements();
if (!focusables.length) {
return;
}

const targetIndex = list?._lastFocusedElementIndex ?? 0;
const safeIndex = Math.min(targetIndex, focusables.length - 1);
const elementToFocus = focusables[safeIndex];

elementToFocus.focus();

if (list) {
list._lastFocusedElementIndex = safeIndex;
}
}

_updateStoredFocusIndex(list: List | null, activeElement: HTMLElement) {
if (!list) {
return;
}

const focusables = this._getFocusableElements();
const currentIndex = focusables.indexOf(activeElement);

if (currentIndex !== -1) {
list._lastFocusedElementIndex = currentIndex;
}
}
}

export default ListItem;
Expand Down
12 changes: 7 additions & 5 deletions packages/main/src/ListItemCustom.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { isTabNext, isTabPrevious, isF2 } from "@ui5/webcomponents-base/dist/Keys.js";
import {
isTabNext, isTabPrevious, isF2, isF7,
} from "@ui5/webcomponents-base/dist/Keys.js";
import jsxRenderer from "@ui5/webcomponents-base/dist/renderer/JsxRenderer.js";
import type { ClassMap } from "@ui5/webcomponents-base/dist/types.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
Expand Down Expand Up @@ -54,22 +56,22 @@ class ListItemCustom extends ListItem {
@property()
declare accessibleName?: string;

async _onkeydown(e: KeyboardEvent) {
_onkeydown(e: KeyboardEvent) {
const isTab = isTabNext(e) || isTabPrevious(e);
const isFocused = this.matches(":focus");

if (!isTab && !isFocused && !isF2(e)) {
if (!isTab && !isFocused && !isF2(e) && !isF7(e)) {
return;
}

await super._onkeydown(e);
super._onkeydown(e);
}

_onkeyup(e: KeyboardEvent) {
const isTab = isTabNext(e) || isTabPrevious(e);
const isFocused = this.matches(":focus");

if (!isTab && !isFocused && !isF2(e)) {
if (!isTab && !isFocused && !isF2(e) && !isF7(e)) {
return;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/main/src/TreeItemBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,8 @@ class TreeItemBase extends ListItem {
this.fireDecoratorEvent("toggle", { item: this });
}

async _onkeydown(e: KeyboardEvent) {
await super._onkeydown(e);
_onkeydown(e: KeyboardEvent) {
super._onkeydown(e);

if (!this._fixed && this.showToggleButton && isRight(e)) {
if (!this.expanded) {
Expand Down
4 changes: 2 additions & 2 deletions packages/main/src/TreeItemCustom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class TreeItemCustom extends TreeItemBase {
@slot()
content!: Array<HTMLElement>;

async _onkeydown(e: KeyboardEvent) {
_onkeydown(e: KeyboardEvent) {
if (isDown(e) && this.content?.some(el => el.contains(e.target as Node))) {
e.stopPropagation();
return;
Expand All @@ -69,7 +69,7 @@ class TreeItemCustom extends TreeItemBase {
return;
}

await super._onkeydown(e);
super._onkeydown(e);
}

_onkeyup(e: KeyboardEvent) {
Expand Down
Loading
Loading