Skip to content

Commit

Permalink
feat: focus trap on tab (#193)
Browse files Browse the repository at this point in the history
feat: focus trap on tab

Co-authored-by: Ben Monro <benjamin.monro@walmartlabs.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
  • Loading branch information
3 people authored and Kent C. Dodds committed Dec 17, 2019
1 parent 9ab5b2b commit bcb3c5c
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 105 deletions.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,57 @@ expect(getByTestId("val3").selected).toBe(true);
The `values` parameter can be either an array of values or a singular scalar
value.

### `tab({shift, focusTrap})`

Fires a tab event changing the document.activeElement in the same way the
browser does.

Options:

- `shift` (default `false`) can be true or false to invert tab direction.
- `focusTrap` (default `document`) a container element to restrict the tabbing
within.

> **A note about tab**: [jsdom does not support tabbing](https://github.com/jsdom/jsdom/issues/2102), so this feature is a way to enable tests to verify tabbing from the end user's perspective. However, this limitation in jsdom will mean that components like [focus-trap-react](https://github.com/davidtheclark/focus-trap-react) will not work with `userEvent.tab()` or jsdom. For that reason, the `focusTrap` option is available to let you ensure your user is restricted within a focus-trap.
```jsx
import React from "react";
import { render } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import userEvent from "@testing-library/user-event";

it("should cycle elements in document tab order", () => {
const { getAllByTestId } = render(
<div>
<input data-testid="element" type="checkbox" />
<input data-testid="element" type="radio" />
<input data-testid="element" type="number" />
</div>
);

const [checkbox, radio, number] = getAllByTestId("element");

expect(document.body).toHaveFocus();

userEvent.tab();

expect(checkbox).toHaveFocus();

userEvent.tab();

expect(radio).toHaveFocus();

userEvent.tab();

expect(number).toHaveFocus();

userEvent.tab();

// cycle goes back to first element
expect(checkbox).toHaveFocus();
});
```

## Contributors

Thanks goes to these wonderful people
Expand Down Expand Up @@ -197,6 +248,7 @@ Thanks goes to these wonderful people

<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the
Expand Down
265 changes: 164 additions & 101 deletions __tests__/react/tab.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,156 +4,219 @@ import "@testing-library/jest-dom/extend-expect";
import userEvent from "../../src";

describe("userEvent.tab", () => {
it("should cycle elements in document tab order", () => {
const { getAllByTestId } = render(
<div>
<input data-testid="element" type="checkbox" />
<input data-testid="element" type="radio" />
<input data-testid="element" type="number" />
</div>
);
it("should cycle elements in document tab order", () => {
const { getAllByTestId } = render(
<div>
<input data-testid="element" type="checkbox" />
<input data-testid="element" type="radio" />
<input data-testid="element" type="number" />
</div>
);

const [checkbox, radio, number] = getAllByTestId("element");
const [checkbox, radio, number] = getAllByTestId("element");

expect(document.activeElement).toBe(document.body);
expect(document.body).toHaveFocus();

userEvent.tab();
userEvent.tab();

expect(document.activeElement).toBe(checkbox);
expect(checkbox).toHaveFocus();

userEvent.tab();
userEvent.tab();

expect(document.activeElement).toBe(radio);
expect(radio).toHaveFocus();

userEvent.tab();
userEvent.tab();

expect(document.activeElement).toBe(number);
expect(number).toHaveFocus();

userEvent.tab();
userEvent.tab();

// cycle goes back to first element
expect(document.activeElement).toBe(checkbox);
});
// cycle goes back to first element
expect(checkbox).toHaveFocus();
});

it("should go backwards when shift = true", () => {
const { getAllByTestId } = render(
<div>
<input data-testid="element" type="checkbox" />
<input data-testid="element" type="radio" />
<input data-testid="element" type="number" />
</div>
);
it("should go backwards when shift = true", () => {
const { getAllByTestId } = render(
<div>
<input data-testid="element" type="checkbox" />
<input data-testid="element" type="radio" />
<input data-testid="element" type="number" />
</div>
);

const [checkbox, radio, number] = getAllByTestId("element");
const [checkbox, radio, number] = getAllByTestId("element");

radio.focus();
radio.focus();

userEvent.tab({ shift: true });
userEvent.tab({ shift: true });

expect(document.activeElement).toBe(checkbox);
expect(checkbox).toHaveFocus();

userEvent.tab({ shift: true });
userEvent.tab({ shift: true });

expect(document.activeElement).toBe(number);
});
expect(number).toHaveFocus();
});

it("should respect tabindex, regardless of dom position", () => {
const { getAllByTestId } = render(
<div>
<input data-testid="element" tabIndex={2} type="checkbox" />
<input data-testid="element" tabIndex={1} type="radio" />
<input data-testid="element" tabIndex={3} type="number" />
</div>
);
it("should respect tabindex, regardless of dom position", () => {
const { getAllByTestId } = render(
<div>
<input data-testid="element" tabIndex={2} type="checkbox" />
<input data-testid="element" tabIndex={1} type="radio" />
<input data-testid="element" tabIndex={3} type="number" />
</div>
);

const [checkbox, radio, number] = getAllByTestId("element");
const [checkbox, radio, number] = getAllByTestId("element");

userEvent.tab();
userEvent.tab();

expect(document.activeElement).toBe(radio);
expect(radio).toHaveFocus();

userEvent.tab();
userEvent.tab();

expect(document.activeElement).toBe(checkbox);
expect(checkbox).toHaveFocus();

userEvent.tab();
userEvent.tab();

expect(document.activeElement).toBe(number);
expect(number).toHaveFocus();

userEvent.tab();
userEvent.tab();

expect(document.activeElement).toBe(radio);
});
expect(radio).toHaveFocus();
});

it('should respect dom order when tabindex are all the same', () => {
const { getAllByTestId } = render(
<div>
<input data-testid="element" tabIndex={0} type="checkbox" />
<input data-testid="element" tabIndex={1} type="radio" />
<input data-testid="element" tabIndex={0} type="number" />
</div>
);
it("should respect dom order when tabindex are all the same", () => {
const { getAllByTestId } = render(
<div>
<input data-testid="element" tabIndex={0} type="checkbox" />
<input data-testid="element" tabIndex={1} type="radio" />
<input data-testid="element" tabIndex={0} type="number" />
</div>
);

const [checkbox, radio, number] = getAllByTestId("element");
const [checkbox, radio, number] = getAllByTestId("element");

userEvent.tab();
userEvent.tab();

expect(document.activeElement).toBe(checkbox);
expect(checkbox).toHaveFocus();

userEvent.tab();
userEvent.tab();

expect(document.activeElement).toBe(number);
expect(number).toHaveFocus();

userEvent.tab();
userEvent.tab();

expect(document.activeElement).toBe(radio);
expect(radio).toHaveFocus();

userEvent.tab();
userEvent.tab();

expect(document.activeElement).toBe(checkbox);
});
expect(checkbox).toHaveFocus();
});

it('should suport a mix of elements with/without tab index', () => {
const { getAllByTestId } = render(
<div>
<input data-testid="element" tabIndex={0} type="checkbox" />
<input data-testid="element" tabIndex={1} type="radio" />
<input data-testid="element" type="number" />
</div>
);
it("should suport a mix of elements with/without tab index", () => {
const { getAllByTestId } = render(
<div>
<input data-testid="element" tabIndex={0} type="checkbox" />
<input data-testid="element" tabIndex={1} type="radio" />
<input data-testid="element" type="number" />
</div>
);

const [checkbox, radio, number] = getAllByTestId("element");
const [checkbox, radio, number] = getAllByTestId("element");

userEvent.tab();
userEvent.tab();

expect(document.activeElement).toBe(checkbox);
userEvent.tab();
expect(checkbox).toHaveFocus();
userEvent.tab();

expect(document.activeElement).toBe(number);
userEvent.tab();
expect(number).toHaveFocus();
userEvent.tab();

expect(document.activeElement).toBe(radio);
expect(radio).toHaveFocus();
});

});
it("should not tab to <a> with no href", () => {
const { getAllByTestId } = render(
<div>
<input data-testid="element" tabIndex={0} type="checkbox" />
<a>ignore this</a>
<a data-testid="element" href="http://www.testingjavascript.com">
a link
</a>
</div>
);

it('should not tab to <a> with no href', () => {
const { getAllByTestId } = render(
<div>
<input data-testid="element" tabIndex={0} type="checkbox" />
<a>ignore this</a>
<a data-testid="element" href="http://www.testingjavascript.com">a link</a>
</div>
);
const [checkbox, link] = getAllByTestId("element");

const [checkbox, link] = getAllByTestId("element");
userEvent.tab();

userEvent.tab();
expect(checkbox).toHaveFocus();

expect(document.activeElement).toBe(checkbox);
userEvent.tab();

userEvent.tab();
expect(link).toHaveFocus();
});

expect(document.activeElement).toBe(link);
});
it("should stay within a focus trab", () => {
const { getAllByTestId, getByTestId } = render(
<>
<div data-testid="div1">
<input data-testid="element" type="checkbox" />
<input data-testid="element" type="radio" />
<input data-testid="element" type="number" />
</div>
<div data-testid="div2">
<input data-testid="element" foo="bar" type="checkbox" />
<input data-testid="element" foo="bar" type="radio" />
<input data-testid="element" foo="bar" type="number" />
</div>
</>
);

const [div1, div2] = [getByTestId("div1"), getByTestId("div2")];
const [
checkbox1,
radio1,
number1,
checkbox2,
radio2,
number2
] = getAllByTestId("element");

expect(document.body).toHaveFocus();

userEvent.tab({ focusTrap: div1 });

expect(checkbox1).toHaveFocus();

userEvent.tab({ focusTrap: div1 });

expect(radio1).toHaveFocus();

userEvent.tab({ focusTrap: div1 });

expect(number1).toHaveFocus();

userEvent.tab({ focusTrap: div1 });

// cycle goes back to first element
expect(checkbox1).toHaveFocus();

userEvent.tab({ focusTrap: div2 });

expect(checkbox2).toHaveFocus();

userEvent.tab({ focusTrap: div2 });

expect(radio2).toHaveFocus();

userEvent.tab({ focusTrap: div2 });

expect(number2).toHaveFocus();

userEvent.tab({ focusTrap: div2 });

// cycle goes back to first element
expect(checkbox2).toHaveFocus();
});
});
Loading

0 comments on commit bcb3c5c

Please sign in to comment.