Skip to content

Commit 40303d5

Browse files
fix(framework): fix find first focusable element (#10984)
* fix(framework): fix first focusable element
1 parent 43fe1e0 commit 40303d5

File tree

3 files changed

+103
-2
lines changed

3 files changed

+103
-2
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { getFirstFocusableElement } from "@ui5/webcomponents-base/dist/util/FocusableElements.js";
2+
import SideNavigation from "@ui5/webcomponents-fiori/dist/SideNavigation.js";
3+
import SideNavigationItem from "@ui5/webcomponents-fiori/dist/SideNavigationItem.js";
4+
import SideNavigationSubItem from "@ui5/webcomponents-fiori/dist/SideNavigationSubItem.js";
5+
import Button from "@ui5/webcomponents/dist/Button.js";
6+
import Input from "@ui5/webcomponents/dist/Input.js";
7+
8+
describe("FocusableElements", () => {
9+
it("Tests first focusable element", () => {
10+
cy.mount(
11+
<>
12+
<div id="container">
13+
<Input tabindex="-1"></Input>
14+
<br/>
15+
<SideNavigation>
16+
<SideNavigationItem
17+
text="Home"
18+
icon="home"
19+
href="#home"
20+
title="Home tooltip"></SideNavigationItem>
21+
<SideNavigationItem text="People" href="#people" expanded icon="group">
22+
<SideNavigationSubItem
23+
id="subItem1"
24+
selected
25+
text="Should be Focused When Open"></SideNavigationSubItem>
26+
<SideNavigationSubItem text="Sub Item 2"></SideNavigationSubItem>
27+
</SideNavigationItem>
28+
</SideNavigation>
29+
<br/>
30+
<Button id="buttonId">Close</Button>
31+
</div>
32+
</>
33+
);
34+
35+
cy.get("#subItem1")
36+
.shadow()
37+
.find(".ui5-sn-item")
38+
.should("have.attr", "tabindex", "0");
39+
40+
cy.get("#container").then( async ($container) => {
41+
const firstFocusable = await getFirstFocusableElement($container.get(0));
42+
await firstFocusable?.focus();
43+
});
44+
45+
cy.get("#subItem1")
46+
.should("have.focus");
47+
});
48+
});

packages/base/src/util/FocusableElements.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ const isElemFocusable = (el: HTMLElement) => {
3636
return el.hasAttribute("data-ui5-focus-redirect") || !isElementHidden(el);
3737
};
3838

39+
const isUI5ElementWithNegativeTabIndex = (el: HTMLElement) => {
40+
if (instanceOfUI5Element(el)) {
41+
const tabIndex = el.getAttribute("tabindex");
42+
if (tabIndex !== null && parseInt(tabIndex) < 0) {
43+
return true;
44+
}
45+
}
46+
47+
return false;
48+
};
49+
3950
const findFocusableElement = async (container: HTMLElement, forward: boolean, startFromContainer?: boolean): FocusableElementPromise => {
4051
let child: HTMLElement | undefined;
4152
let assignedElements;
@@ -60,9 +71,16 @@ const findFocusableElement = async (container: HTMLElement, forward: boolean, st
6071
while (child) {
6172
const originalChild: HTMLElement | undefined = child;
6273

63-
if (!isElementHidden(originalChild)) {
74+
if (!isElementHidden(originalChild) && !isUI5ElementWithNegativeTabIndex(originalChild)) {
6475
if (instanceOfUI5Element(child)) {
65-
child = await child.getFocusDomRefAsync();
76+
// getDomRef is used because some components mark their focusable ref in an inner
77+
// html but there might also be focusable targets outside of it
78+
// as an example - TreeItemBase
79+
// div - root of the component returned by getDomRef()
80+
// li.ui5-li-tree - returned by getFocusDomRef() and may not be focusable (ItemNavigation manages tabindex)
81+
// ul.subtree - may still contain focusable targets (sub nodes of the tree item)
82+
await child._waitForDomRef();
83+
child = child.getDomRef();
6684
}
6785

6886
if (!child || isElementHidden(child)) {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<meta charset="utf-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
7+
<title>Focusable Elements</title>
8+
<script src="%VITE_BUNDLE_PATH%" type="module"></script>
9+
</head>
10+
11+
<body class="card1auto">
12+
<ui5-button id="buttonId">Open Dialog</ui5-button>
13+
14+
<ui5-dialog id="dialogId" header-text="Dialog Header">
15+
<ui5-input tabindex="-1"></ui5-input>
16+
<br />
17+
<ui5-side-navigation>
18+
<!-- Items -->
19+
<ui5-side-navigation-item text="Home"
20+
icon="home"
21+
title="Home tooltip"></ui5-side-navigation-item>
22+
<ui5-side-navigation-item text="People" expanded icon="group">
23+
<ui5-side-navigation-sub-item selected text="Should be Focused When Open"></ui5-side-navigation-sub-item>
24+
<ui5-side-navigation-sub-item text="Sub Item 2"></ui5-side-navigation-sub-item>
25+
</ui5-side-navigation-item>
26+
</ui5-side-navigation>
27+
<br />
28+
<ui5-button>Close</ui5-button>
29+
</ui5-dialog>
30+
<script>
31+
buttonId.onclick = () => {
32+
dialogId.open = true;
33+
};
34+
</script>
35+
</body>

0 commit comments

Comments
 (0)