Skip to content

Commit 1d92472

Browse files
committed
feat(utils): Added isFocusable util
1 parent 725d1c9 commit 1d92472

File tree

3 files changed

+125
-9
lines changed

3 files changed

+125
-9
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { isFocusable } from "../isFocusable";
2+
3+
describe("isFocusable", () => {
4+
it("should default to programmatic focus type", () => {
5+
const element = document.createElement("div");
6+
element.tabIndex = -1;
7+
8+
expect(isFocusable(element)).toBe(true);
9+
expect(isFocusable(element, "tab")).toBe(false);
10+
expect(isFocusable(element, "programmatic")).toBe(true);
11+
});
12+
13+
it("should work correctly for anchors and areas", () => {
14+
const anchor = document.createElement("a");
15+
const area = document.createElement("area");
16+
17+
expect(isFocusable(anchor, "tab")).toBe(false);
18+
expect(isFocusable(area, "tab")).toBe(false);
19+
expect(isFocusable(anchor, "programmatic")).toBe(false);
20+
expect(isFocusable(area, "programmatic")).toBe(false);
21+
22+
anchor.href = "#";
23+
area.href = "#";
24+
expect(isFocusable(anchor, "tab")).toBe(true);
25+
expect(isFocusable(area, "tab")).toBe(true);
26+
expect(isFocusable(anchor, "programmatic")).toBe(true);
27+
expect(isFocusable(area, "programmatic")).toBe(true);
28+
});
29+
30+
it("should work correctly for disablable elements", () => {
31+
const button = document.createElement("button");
32+
const select = document.createElement("select");
33+
const textarea = document.createElement("textarea");
34+
const input = document.createElement("input");
35+
input.type = "text";
36+
37+
expect(isFocusable(button, "tab")).toBe(true);
38+
expect(isFocusable(select, "tab")).toBe(true);
39+
expect(isFocusable(textarea, "tab")).toBe(true);
40+
expect(isFocusable(input, "tab")).toBe(true);
41+
expect(isFocusable(button, "programmatic")).toBe(true);
42+
expect(isFocusable(select, "programmatic")).toBe(true);
43+
expect(isFocusable(textarea, "programmatic")).toBe(true);
44+
expect(isFocusable(input, "programmatic")).toBe(true);
45+
46+
button.disabled = true;
47+
select.disabled = true;
48+
textarea.disabled = true;
49+
input.disabled = true;
50+
expect(isFocusable(button, "tab")).toBe(false);
51+
expect(isFocusable(select, "tab")).toBe(false);
52+
expect(isFocusable(textarea, "tab")).toBe(false);
53+
expect(isFocusable(input, "tab")).toBe(false);
54+
expect(isFocusable(button, "programmatic")).toBe(false);
55+
expect(isFocusable(select, "programmatic")).toBe(false);
56+
expect(isFocusable(textarea, "programmatic")).toBe(false);
57+
expect(isFocusable(input, "programmatic")).toBe(false);
58+
});
59+
60+
it("should never include hidden inputs", () => {
61+
const hidden = document.createElement("input");
62+
hidden.type = "hidden";
63+
64+
expect(isFocusable(hidden, "tab")).toBe(false);
65+
expect(isFocusable(hidden, "programmatic")).toBe(false);
66+
});
67+
68+
it("should allow for elements with a tab index to be focusable", () => {
69+
const div = document.createElement("div");
70+
71+
expect(isFocusable(div, "tab")).toBe(false);
72+
expect(isFocusable(div, "programmatic")).toBe(false);
73+
74+
div.tabIndex = -1;
75+
expect(isFocusable(div, "tab")).toBe(false);
76+
expect(isFocusable(div, "programmatic")).toBe(true);
77+
78+
div.tabIndex = 0;
79+
expect(isFocusable(div, "tab")).toBe(true);
80+
expect(isFocusable(div, "programmatic")).toBe(true);
81+
});
82+
});
Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1+
export * from "./extractTextContent";
12
export * from "./FocusContainer";
2-
3+
export * from "./focusElementWithin";
4+
export * from "./getFocusableElements";
5+
export * from "./isFocusable";
36
export * from "./movement";
47
export * from "./radio";
5-
6-
export * from "./useScrollLock";
8+
export * from "./tryToSubmitRelatedForm";
9+
export * from "./useCloseOnEscape";
710
export * from "./useFocusOnMount";
811
export * from "./usePreviousFocus";
9-
export * from "./useCloseOnEscape";
10-
11-
export * from "./getFocusableElements";
12-
export * from "./focusElementWithin";
13-
export * from "./extractTextContent";
14-
export * from "./tryToSubmitRelatedForm";
12+
export * from "./useScrollLock";
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { PROGRAMATICALLY_FOCUSABLE, TAB_FOCUSABLE } from "./constants";
2+
3+
/**
4+
* An element can be tab focused if it is:
5+
* - an anchor or area with an `href`
6+
* - a non-disabled `input` element that is not `type="hidden"`
7+
* - a non-disabled `button`, `textarea`, or `select` element
8+
* - an element with a `tabIndex >= 0`
9+
*
10+
* An element can be noted as "programmatically focusable only" has the above
11+
* rules, but the `tabIndex` will be set to `-1`.
12+
*
13+
* @remarks \@since 2.8.0
14+
*/
15+
export type ElementFocusType = "tab" | "programmatic";
16+
17+
/**
18+
* Checks if an element is focusable.
19+
*
20+
* @see {@link ElementFocusType}
21+
* @remarks \@since 2.8.0
22+
* @param element - The element to check
23+
* @param type - The focus type to compare against
24+
* @returns true if the element is focusable
25+
*/
26+
export function isFocusable(
27+
element: HTMLElement | Document | Window,
28+
type: ElementFocusType = "programmatic"
29+
): element is HTMLElement {
30+
return (
31+
"matches" in element &&
32+
element.matches(
33+
type === "programmatic" ? PROGRAMATICALLY_FOCUSABLE : TAB_FOCUSABLE
34+
)
35+
);
36+
}

0 commit comments

Comments
 (0)