Skip to content

Commit 366a0b3

Browse files
authored
refactor: improve tabbable elements detection (#865)
FIXES: #858 Refactor FocusHelper.js (used to export a class previously) as follows: (1) All logic, related to finding focusable elements is just split (without changes) to: util/FocusableElements.js util/isNodeClickable.js util/isNodeHidden.js (2) The logic, related to finding tabbable elements is refactored, considering the issues we have that not all child elements have been traversed and slotted nodes have not been traversed via the shadow DOM order: util/TabbableElements.js util/isNodeTabbable.js Add test for tab handling in the ListItem
1 parent d305959 commit 366a0b3

13 files changed

+246
-245
lines changed

packages/base/src/FocusHelper.js

-226
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import UI5Element from "../UI5Element.js";
2+
import isNodeHidden from "./isNodeHidden.js";
3+
import isNodeClickable from "./isNodeClickable.js";
4+
5+
const getFirstFocusableElement = container => {
6+
if (!container || isNodeHidden(container)) {
7+
return null;
8+
}
9+
10+
return findFocusableElement(container, true);
11+
};
12+
13+
const getLastFocusableElement = container => {
14+
if (!container || isNodeHidden(container)) {
15+
return null;
16+
}
17+
18+
return findFocusableElement(container, false);
19+
};
20+
21+
const findFocusableElement = (container, forward) => {
22+
let child;
23+
if (container.assignedNodes && container.assignedNodes()) {
24+
const assignedElements = container.assignedNodes();
25+
child = forward ? assignedElements[0] : assignedElements[assignedElements.length - 1];
26+
} else {
27+
child = forward ? container.firstChild : container.lastChild;
28+
}
29+
30+
let focusableDescendant;
31+
32+
while (child) {
33+
const originalChild = child;
34+
35+
child = child instanceof UI5Element ? child.getFocusDomRef() : child;
36+
if (!child) {
37+
return null;
38+
}
39+
40+
if (child.nodeType === 1 && !isNodeHidden(child)) {
41+
if (isNodeClickable(child)) {
42+
return (child && typeof child.focus === "function") ? child : null;
43+
}
44+
45+
focusableDescendant = findFocusableElement(child, forward);
46+
if (focusableDescendant) {
47+
return (focusableDescendant && typeof focusableDescendant.focus === "function") ? focusableDescendant : null;
48+
}
49+
}
50+
51+
child = forward ? originalChild.nextSibling : originalChild.previousSibling;
52+
}
53+
54+
return null;
55+
};
56+
57+
export {
58+
getFirstFocusableElement,
59+
getLastFocusableElement,
60+
};
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
2+
import isNodeTabbable from "./isNodeTabbable.js";
3+
4+
const getTabbableElements = node => {
5+
return getTabbables(node.children);
6+
};
7+
8+
const getLastTabbableElement = node => {
9+
const tabbables = getTabbables(node.children);
10+
return tabbables.length ? tabbables[tabbables.length - 1] : null;
11+
};
12+
13+
const getTabbables = (nodes, tabbables) => {
14+
const tabbablesNodes = tabbables || [];
15+
16+
Array.from(nodes).forEach(currentNode => {
17+
if (currentNode.nodeType === Node.TEXT_NODE) {
18+
return;
19+
}
20+
21+
if (currentNode.shadowRoot) {
22+
// get the root node of the ShadowDom (1st none style tag)
23+
const children = currentNode.shadowRoot.children;
24+
currentNode = Array.from(children).filter(node => node.tagName !== "STYLE")[0];
25+
}
26+
27+
if (isNodeTabbable(currentNode)) {
28+
tabbablesNodes.push(currentNode);
29+
}
30+
31+
if (currentNode.tagName === "SLOT") {
32+
getTabbables(currentNode.assignedNodes(), tabbablesNodes);
33+
} else {
34+
getTabbables(currentNode.children, tabbablesNodes);
35+
}
36+
});
37+
38+
return tabbablesNodes;
39+
};
40+
41+
export {
42+
getTabbableElements,
43+
getLastTabbableElement,
44+
};
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const rClickable = /^(?:a|area)$/i;
2+
const rFocusable = /^(?:input|select|textarea|button)$/i;
3+
4+
const isNodeClickable = node => {
5+
if (node.disabled) {
6+
return false;
7+
}
8+
9+
const tabIndex = node.getAttribute("tabindex");
10+
if (tabIndex !== null && tabIndex !== undefined) {
11+
return parseInt(tabIndex) >= 0;
12+
}
13+
14+
return rFocusable.test(node.nodeName)
15+
|| (rClickable.test(node.nodeName)
16+
&& node.href);
17+
};
18+
19+
export default isNodeClickable;
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const isNodeHidden = node => {
2+
if (node.nodeName === "SLOT") {
3+
return false;
4+
}
5+
6+
const rect = node.getBoundingClientRect();
7+
8+
return (node.offsetWidth <= 0 && node.offsetHeight <= 0)
9+
|| node.style.visibility === "hidden"
10+
|| (rect.width === 0 && 0 && rect.height === 0);
11+
};
12+
13+
export default isNodeHidden;
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import isNodeHidden from "./isNodeHidden";
2+
3+
const isNodeTabbable = node => {
4+
if (!node) {
5+
return false;
6+
}
7+
8+
const nodeName = node.nodeName.toLowerCase();
9+
10+
if (node.hasAttribute("data-sap-no-tab-ref")) {
11+
return false;
12+
}
13+
14+
if (isNodeHidden(node)) {
15+
return false;
16+
}
17+
18+
if (nodeName === "a" || /input|select|textarea|button|object/.test(nodeName)) {
19+
return !node.disabled;
20+
}
21+
22+
const tabIndex = node.getAttribute("tabindex");
23+
if (tabIndex !== null && tabIndex !== undefined) {
24+
return parseInt(tabIndex) >= 0;
25+
}
26+
};
27+
28+
export default isNodeTabbable;

0 commit comments

Comments
 (0)