Files
headlessui/packages/@headlessui-react/src/test-utils/interactions.test.tsx
T
Robin Malfait 4459689beb handle keyboard interactions in a more robust way
Browsers. Are. Crazy.

In JSDOM, when you fire an event, you only get that specific event. You
don't get all the magic that the browser gives you. For example, when
you are focused on a button and press to "Tab" then in JSDOM you would
only get a keydown event. However in the browser you get this chain of
events:

1. `keydown` on the current element
2. `blur` on the current element
3. `focus` on the new element
4. `keyup` on the new element

I implemented this "magic", for the `Tab`, `Enter` and `Space` key for
now. Those are the most important currently. `Enter` and `Space` also
trigger `click` events for example.

I also have a "generic" implementation, where a normal press results in:

1. `keydown`
2. `keypress` (in case it has a `charCode` and is "printable", so `alt`
   is ignored)
3. `keyup`

I also ensured that the cancelation when you use an
`event.preventDefault()` happens correctly.

Here is a fun summary: https://twitter.com/malfaitrobin/status/1354472678128820234

Press "Enter" on a button
  -> keydown, keypress, click, keyup

Press "Space" on a button
  -> keydown, keypress, keyup, click

Press "Enter" or "Space" on a button, with event.preventDefault() in the keydown listener
  -> keydown, keyup

Press "Enter" on a button, with event.preventDefault() in the keypress listener
  -> keydown, keypress, keyup

Press "Space" on a button, with event.preventDefault() in the keypress listener
  -> keydown, keypress, keyup, click
2021-01-29 12:23:14 +01:00

191 lines
5.4 KiB
TypeScript

import React from 'react'
import { render } from '@testing-library/react'
import { type, shift, Keys } from './interactions'
type Events = 'onKeyDown' | 'onKeyUp' | 'onKeyPress' | 'onClick' | 'onBlur' | 'onFocus'
let events: Events[] = ['onKeyDown', 'onKeyUp', 'onKeyPress', 'onClick', 'onBlur', 'onFocus']
type Args = [
string | Partial<KeyboardEvent>,
(string | Partial<KeyboardEvent | MouseEvent>)[],
Set<Events>
]
function key(input: string | Partial<KeyboardEvent>): Partial<KeyboardEvent> {
if (typeof input === 'string') return { key: input }
return input
}
function event(
input: string | Partial<KeyboardEvent | MouseEvent>,
target?: string
): Partial<KeyboardEvent | MouseEvent> {
let e = typeof input === 'string' ? { type: input } : input
if (target) {
Object.defineProperty(e, 'target', {
configurable: false,
enumerable: true,
get() {
return document.getElementById(target!)
},
})
}
return e
}
describe('Keyboard', () => {
describe('type', () => {
it.each<Args>([
// Default - no cancellation
['a', ['keydown', 'keypress', 'keyup'], new Set()],
[Keys.Space, ['keydown', 'keypress', 'keyup', 'click'], new Set()],
[Keys.Enter, ['keydown', 'keypress', 'click', 'keyup'], new Set()],
[
Keys.Tab,
[
event('keydown', 'trigger'),
event('blur', 'trigger'),
event('focus', 'after'),
event('keyup', 'after'),
],
new Set(),
],
[
shift(Keys.Tab),
[
event('keydown', 'trigger'),
event('blur', 'trigger'),
event('focus', 'before'),
event('keyup', 'before'),
],
new Set(),
],
// Canceling keydown
['a', ['keydown', 'keyup'], new Set<Events>(['onKeyDown'])],
[Keys.Space, ['keydown', 'keyup'], new Set<Events>(['onKeyDown'])],
[Keys.Enter, ['keydown', 'keyup'], new Set<Events>(['onKeyDown'])],
[Keys.Tab, ['keydown', 'keyup'], new Set<Events>(['onKeyDown'])],
[shift(Keys.Tab), ['keydown', 'keyup'], new Set<Events>(['onKeyDown'])],
// Canceling keypress
['a', ['keydown', 'keypress', 'keyup'], new Set<Events>(['onKeyPress'])],
[Keys.Space, ['keydown', 'keypress', 'keyup', 'click'], new Set<Events>(['onKeyPress'])],
[Keys.Enter, ['keydown', 'keypress', 'keyup'], new Set<Events>(['onKeyPress'])],
[
Keys.Tab,
[
event('keydown', 'trigger'),
event('blur', 'trigger'),
event('focus', 'after'),
event('keyup', 'after'),
],
new Set<Events>(['onKeyPress']),
],
[
shift(Keys.Tab),
[
event('keydown', 'trigger'),
event('blur', 'trigger'),
event('focus', 'before'),
event('keyup', 'before'),
],
new Set<Events>(['onKeyPress']),
],
// Canceling keyup
['a', ['keydown', 'keypress', 'keyup'], new Set<Events>(['onKeyUp'])],
[Keys.Space, ['keydown', 'keypress', 'keyup'], new Set<Events>(['onKeyUp'])],
[Keys.Enter, ['keydown', 'keypress', 'click', 'keyup'], new Set<Events>(['onKeyUp'])],
[
Keys.Tab,
[
event('keydown', 'trigger'),
event('blur', 'trigger'),
event('focus', 'after'),
event('keyup', 'after'),
],
new Set<Events>(['onKeyUp']),
],
[
shift(Keys.Tab),
[
event('keydown', 'trigger'),
event('blur', 'trigger'),
event('focus', 'before'),
event('keyup', 'before'),
],
new Set<Events>(['onKeyUp']),
],
// Cancelling blur
[
Keys.Tab,
[
event('keydown', 'trigger'),
event('blur', 'trigger'),
event('focus', 'after'),
event('keyup', 'after'),
],
new Set<Events>(['onBlur']),
],
[
shift(Keys.Tab),
[
event('keydown', 'trigger'),
event('blur', 'trigger'),
event('focus', 'before'),
event('keyup', 'before'),
],
new Set<Events>(['onBlur']),
],
])('should fire the correct events %#', async (input, result, prevents) => {
let fired: (KeyboardEvent | MouseEvent)[] = []
let state = { readyToCapture: false }
function createProps(id: string) {
return events.reduce(
(props: React.ComponentProps<'button'>, name) => {
props[name] = (event: any) => {
if (!state.readyToCapture) return
if (prevents.has(name)) event.preventDefault()
fired.push(event.nativeEvent)
}
return props
},
{ id }
)
}
render(
<>
<button {...createProps('before')}>Before</button>
<button {...createProps('trigger')}>Trigger</button>
<button {...createProps('after')}>After</button>
</>
)
let trigger = document.getElementById('trigger')
trigger?.focus()
state.readyToCapture = true
await type([key(input)])
let expected = result.map(e => event(e))
expect(fired.length).toEqual(result.length)
for (let [idx, event] of fired.entries()) {
for (let key in expected[idx]) {
let _key = key as keyof (KeyboardEvent | MouseEvent)
expect(event[_key]).toBe(expected[idx][_key])
}
}
})
})
})