Skip to content

Commit

Permalink
fix(tab)!: remove focusTrap option (#772)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: The `focusTrap` option has been removed from `userEvent.tab()`.
  • Loading branch information
ph-fritsche committed Nov 28, 2021
1 parent 87470ff commit a0412c0
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 139 deletions.
6 changes: 1 addition & 5 deletions src/keyboard/plugins/functional.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,7 @@ export const keydownBehavior: behaviorPlugin[] = [
{
matches: keyDef => keyDef.key === 'Tab',
handle: (keyDef, element, options, state) => {
const dest = getTabDestination(
element,
state.modifiers.shift,
element.ownerDocument,
)
const dest = getTabDestination(element, state.modifiers.shift)
if (dest === element.ownerDocument.body) {
blur(element)
} else {
Expand Down
5 changes: 0 additions & 5 deletions src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ function _setup(
autoModify,
delay = 0,
document,
focusTrap,
keyboardMap,
pointerMap,
skipAutoClose,
Expand Down Expand Up @@ -114,9 +113,6 @@ function _setup(
const clickDefaults: clickOptions = {
skipHover,
}
const tabDefaults: TabOptions = {
focusTrap,
}
const typeDefaults: TypeOptions = {
delay,
skipAutoClose,
Expand Down Expand Up @@ -194,7 +190,6 @@ function _setup(
},

tab: (...args: Parameters<typeof tab>) => {
args[0] = {...tabDefaults, ...args[0]}
return tab.call(userEvent, ...args)
},

Expand Down
62 changes: 8 additions & 54 deletions src/tab.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,15 @@
import {fireEvent} from '@testing-library/dom'
import {blur, focus, getActiveElement, getTabDestination} from './utils'
import type {UserEvent} from './setup'

export interface tabOptions {
shift?: boolean
focusTrap?: Document | Element
}

export function tab(
this: UserEvent,
{shift = false, focusTrap}: tabOptions = {},
) {
const doc = focusTrap?.ownerDocument ?? document
const previousElement = getActiveElement(doc)

// Never the case in our test environment
// Only happens if the activeElement is inside a shadowRoot that is not part of `doc`.
/* istanbul ignore next */
if (!previousElement) {
return
}

if (!focusTrap) {
focusTrap = doc
}

const nextElement = getTabDestination(previousElement, shift, focusTrap)

const shiftKeyInit = {
key: 'Shift',
keyCode: 16,
shiftKey: true,
}
const tabKeyInit = {
key: 'Tab',
keyCode: 9,
shiftKey: shift,
}

let continueToTab = true

if (shift) fireEvent.keyDown(previousElement, {...shiftKeyInit})
continueToTab = fireEvent.keyDown(previousElement, {...tabKeyInit})

const keyUpTarget = continueToTab ? nextElement : previousElement

if (continueToTab) {
if (nextElement === doc.body) {
blur(previousElement)
} else {
focus(nextElement)
}
}

fireEvent.keyUp(keyUpTarget, {...tabKeyInit})

if (shift) {
fireEvent.keyUp(keyUpTarget, {...shiftKeyInit, shiftKey: false})
}
export function tab(this: UserEvent, {shift}: tabOptions = {}) {
this.keyboard(
shift === true
? '{Shift>}{Tab}{/Shift}'
: shift === false
? '[/ShiftLeft][/ShiftRight]{Tab}'
: '{Tab}',
)
}
12 changes: 4 additions & 8 deletions src/utils/focus/getTabDestination.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import {isDisabled} from '../misc/isDisabled'
import {isDocument} from '../misc/isDocument'
import {isElementType} from '../misc/isElementType'
import {isVisible} from '../misc/isVisible'
import {FOCUSABLE_SELECTOR} from './selector'

export function getTabDestination(
activeElement: Element,
shift: boolean,
focusTrap: Document | Element,
) {
const focusableElements = focusTrap.querySelectorAll(FOCUSABLE_SELECTOR)
export function getTabDestination(activeElement: Element, shift: boolean) {
const document = activeElement.ownerDocument
const focusableElements = document.querySelectorAll(FOCUSABLE_SELECTOR)

const enabledElements = Array.from(focusableElements).filter(
el =>
Expand All @@ -29,7 +25,7 @@ export function getTabDestination(
}

const checkedRadio: Record<string, HTMLInputElement> = {}
let prunedElements: Element[] = isDocument(focusTrap) ? [focusTrap.body] : []
let prunedElements = [document.body]
const activeRadioGroup = isElementType(activeElement, 'input', {
type: 'radio',
})
Expand Down
1 change: 0 additions & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export * from './misc/isDescendantOrSelf'
export * from './misc/isElementType'
export * from './misc/isVisible'
export * from './misc/isDisabled'
export * from './misc/isDocument'
export * from './misc/wait'
export * from './misc/hasPointerEvents'
export * from './misc/hasFormSubmit'
Expand Down
3 changes: 0 additions & 3 deletions src/utils/misc/isDocument.ts

This file was deleted.

4 changes: 0 additions & 4 deletions tests/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,6 @@ cases<APICase>(
},
tab: {
api: 'tab',
optionsArg: 0,
options: {
focusTrap: document.querySelector('body'),
},
},
type: {
api: 'type',
Expand Down
135 changes: 76 additions & 59 deletions tests/tab.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,82 @@ test('does not change focus if default prevented on keydown', () => {
expect(bListeners.eventWasFired('focus')).toBe(false)
})

test('tabs backward if shift is already pressed', () => {
const {
elements: [elA, elB],
getEventSnapshot,
clearEventCalls,
} = setup(`<input/><input/><input/>`)
const user = userEvent.setup()
user.keyboard('[ShiftLeft>]')
elB.focus()
clearEventCalls()

user.tab()

expect(elA).toHaveFocus()
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: div
input[value=""] - keydown: Tab (9) {shift}
input[value=""] - focusout
input[value=""] - focusin
input[value=""] - keyup: Tab (9) {shift}
`)
})

test('shift option lifts pressed shift key', () => {
const {
elements: [, elB, elC],
getEventSnapshot,
clearEventCalls,
} = setup(`<input/><input/><input/>`)
const user = userEvent.setup()
user.keyboard('[ShiftRight>]')
elB.focus()
clearEventCalls()

user.tab({shift: false})

expect(elC).toHaveFocus()
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: div
input[value=""] - keyup: Shift (16)
input[value=""] - keydown: Tab (9)
input[value=""] - focusout
input[value=""] - focusin
input[value=""] - keyup: Tab (9)
`)
})

test('shift option presses shift key', () => {
const {
elements: [elA, elB],
getEventSnapshot,
clearEventCalls,
} = setup(`<input/><input/><input/>`)
const user = userEvent.setup()
user.keyboard('[ShiftLeft>]')
elB.focus()
clearEventCalls()

user.tab({shift: true})

expect(elA).toHaveFocus()
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: div
input[value=""] - keyup: Shift (16)
input[value=""] - keydown: Shift (16) {shift}
input[value=""] - keydown: Tab (9) {shift}
input[value=""] - focusout
input[value=""] - focusin
input[value=""] - keyup: Tab (9) {shift}
input[value=""] - keyup: Shift (16)
`)
})

test('fires correct events with shift key', () => {
const {element, getEventSnapshot, clearEventCalls} = setup(
`<div><input id="a" /><input id="b" /></div>`,
Expand Down Expand Up @@ -305,65 +381,6 @@ test('should not tab to <input> with type="hidden"', () => {
expect(text).toHaveFocus()
})

test('should stay within a focus trap', () => {
setup(`
<>
<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] = [
document.querySelector('[data-testid="div1"]'),
document.querySelector('[data-testid="div2"]'),
]
const [checkbox1, radio1, number1, checkbox2, radio2, number2] =
document.querySelectorAll('[data-testid="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()
})

// prior to node 11, Array.sort was unstable for arrays w/ length > 10.
// https://twitter.com/mathias/status/1036626116654637057
// for this reason, the tab() function needs to account for this in it's sorting.
Expand Down

0 comments on commit a0412c0

Please sign in to comment.