Skip to content

Commit

Permalink
feat(utils): Added isFocusable util
Browse files Browse the repository at this point in the history
  • Loading branch information
mlaursen committed Apr 18, 2021
1 parent 725d1c9 commit 1d92472
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 9 deletions.
82 changes: 82 additions & 0 deletions packages/utils/src/wia-aria/__tests__/isFocusable.ts
@@ -0,0 +1,82 @@
import { isFocusable } from "../isFocusable";

describe("isFocusable", () => {
it("should default to programmatic focus type", () => {
const element = document.createElement("div");
element.tabIndex = -1;

expect(isFocusable(element)).toBe(true);
expect(isFocusable(element, "tab")).toBe(false);
expect(isFocusable(element, "programmatic")).toBe(true);
});

it("should work correctly for anchors and areas", () => {
const anchor = document.createElement("a");
const area = document.createElement("area");

expect(isFocusable(anchor, "tab")).toBe(false);
expect(isFocusable(area, "tab")).toBe(false);
expect(isFocusable(anchor, "programmatic")).toBe(false);
expect(isFocusable(area, "programmatic")).toBe(false);

anchor.href = "#";
area.href = "#";
expect(isFocusable(anchor, "tab")).toBe(true);
expect(isFocusable(area, "tab")).toBe(true);
expect(isFocusable(anchor, "programmatic")).toBe(true);
expect(isFocusable(area, "programmatic")).toBe(true);
});

it("should work correctly for disablable elements", () => {
const button = document.createElement("button");
const select = document.createElement("select");
const textarea = document.createElement("textarea");
const input = document.createElement("input");
input.type = "text";

expect(isFocusable(button, "tab")).toBe(true);
expect(isFocusable(select, "tab")).toBe(true);
expect(isFocusable(textarea, "tab")).toBe(true);
expect(isFocusable(input, "tab")).toBe(true);
expect(isFocusable(button, "programmatic")).toBe(true);
expect(isFocusable(select, "programmatic")).toBe(true);
expect(isFocusable(textarea, "programmatic")).toBe(true);
expect(isFocusable(input, "programmatic")).toBe(true);

button.disabled = true;
select.disabled = true;
textarea.disabled = true;
input.disabled = true;
expect(isFocusable(button, "tab")).toBe(false);
expect(isFocusable(select, "tab")).toBe(false);
expect(isFocusable(textarea, "tab")).toBe(false);
expect(isFocusable(input, "tab")).toBe(false);
expect(isFocusable(button, "programmatic")).toBe(false);
expect(isFocusable(select, "programmatic")).toBe(false);
expect(isFocusable(textarea, "programmatic")).toBe(false);
expect(isFocusable(input, "programmatic")).toBe(false);
});

it("should never include hidden inputs", () => {
const hidden = document.createElement("input");
hidden.type = "hidden";

expect(isFocusable(hidden, "tab")).toBe(false);
expect(isFocusable(hidden, "programmatic")).toBe(false);
});

it("should allow for elements with a tab index to be focusable", () => {
const div = document.createElement("div");

expect(isFocusable(div, "tab")).toBe(false);
expect(isFocusable(div, "programmatic")).toBe(false);

div.tabIndex = -1;
expect(isFocusable(div, "tab")).toBe(false);
expect(isFocusable(div, "programmatic")).toBe(true);

div.tabIndex = 0;
expect(isFocusable(div, "tab")).toBe(true);
expect(isFocusable(div, "programmatic")).toBe(true);
});
});
16 changes: 7 additions & 9 deletions packages/utils/src/wia-aria/index.ts
@@ -1,14 +1,12 @@
export * from "./extractTextContent";
export * from "./FocusContainer";

export * from "./focusElementWithin";
export * from "./getFocusableElements";
export * from "./isFocusable";
export * from "./movement";
export * from "./radio";

export * from "./useScrollLock";
export * from "./tryToSubmitRelatedForm";
export * from "./useCloseOnEscape";
export * from "./useFocusOnMount";
export * from "./usePreviousFocus";
export * from "./useCloseOnEscape";

export * from "./getFocusableElements";
export * from "./focusElementWithin";
export * from "./extractTextContent";
export * from "./tryToSubmitRelatedForm";
export * from "./useScrollLock";
36 changes: 36 additions & 0 deletions packages/utils/src/wia-aria/isFocusable.ts
@@ -0,0 +1,36 @@
import { PROGRAMATICALLY_FOCUSABLE, TAB_FOCUSABLE } from "./constants";

/**
* An element can be tab focused if it is:
* - an anchor or area with an `href`
* - a non-disabled `input` element that is not `type="hidden"`
* - a non-disabled `button`, `textarea`, or `select` element
* - an element with a `tabIndex >= 0`
*
* An element can be noted as "programmatically focusable only" has the above
* rules, but the `tabIndex` will be set to `-1`.
*
* @remarks \@since 2.8.0
*/
export type ElementFocusType = "tab" | "programmatic";

/**
* Checks if an element is focusable.
*
* @see {@link ElementFocusType}
* @remarks \@since 2.8.0
* @param element - The element to check
* @param type - The focus type to compare against
* @returns true if the element is focusable
*/
export function isFocusable(
element: HTMLElement | Document | Window,
type: ElementFocusType = "programmatic"
): element is HTMLElement {
return (
"matches" in element &&
element.matches(
type === "programmatic" ? PROGRAMATICALLY_FOCUSABLE : TAB_FOCUSABLE
)
);
}

0 comments on commit 1d92472

Please sign in to comment.