Skip to content

Commit

Permalink
feat: separate input devices from pointer/keyboard API (#1003)
Browse files Browse the repository at this point in the history
feat:
* **pointer**: dispatch `auxclick` events

fix:
* **keyboard**: switch modifier state of lock keys on the correct event
* **keyboard**: remove platform-specific additional key events for `Control` on `AltGraph`
* **pointer**: dispatch `contextmenu` events with `detail: 0`
* **pointer**: always set `PointerEvent.isPrimary`
* **pointer**: set `button` property on pointer events separately from legacy mouse events
* **pointer**: click closest common ancestor if `mousedown` and `mouseup` happen on different elements
* **pointer**: omit click event on release if another button is released first
* **pointer**: dispatch `mouseover`, `mouseenter` and `mousemove` on disabled elements
* **pointer**: prevent `mouse*` events per `pointerdown` event handler
* **pointer**: dispatch `*out` and `*over` events when moving into / out of nested elements
* **pointer**: dispatch `*enter` and `*leave` events on ancestors
  • Loading branch information
ph-fritsche committed Aug 2, 2022
1 parent c6aafb7 commit 2852509
Show file tree
Hide file tree
Showing 57 changed files with 1,562 additions and 1,304 deletions.
2 changes: 1 addition & 1 deletion src/convenience/hover.ts
Expand Up @@ -8,7 +8,7 @@ export async function hover(this: Instance, element: Element) {
export async function unhover(this: Instance, element: Element) {
assertPointerEvents(
this[Config],
this[Config].pointerState.position.mouse.target as Element,
this[Config].system.pointer.getMouseTarget(this[Config]),
)
return this.pointer({target: element.ownerDocument.body})
}
9 changes: 6 additions & 3 deletions src/event/behavior/keydown.ts
Expand Up @@ -102,9 +102,12 @@ const keydownBehavior: {
}
}
},
Tab: (event, target, {keyboardState}) => {
Tab: (event, target, config) => {
return () => {
const dest = getTabDestination(target, keyboardState.modifiers.Shift)
const dest = getTabDestination(
target,
config.system.keyboard.modifiers.Shift,
)
focus(dest)
if (hasOwnSelection(dest)) {
setUISelection(dest, {
Expand All @@ -121,7 +124,7 @@ const combinationBehavior: BehaviorPlugin<'keydown'> = (
target,
config,
) => {
if (event.code === 'KeyA' && config.keyboardState.modifiers.Control) {
if (event.code === 'KeyA' && config.system.keyboard.modifiers.Control) {
return () => selectAll(target)
}
}
2 changes: 1 addition & 1 deletion src/event/behavior/keypress.ts
Expand Up @@ -37,7 +37,7 @@ behavior.keypress = (event, target, config) => {
if (isEditable(target)) {
const inputType =
event.key === 'Enter'
? isContentEditable(target) && !config.keyboardState.modifiers.Shift
? isContentEditable(target) && !config.system.keyboard.modifiers.Shift
? 'insertParagraph'
: 'insertLineBreak'
: 'insertText'
Expand Down
3 changes: 1 addition & 2 deletions src/event/createEvent.ts
@@ -1,6 +1,5 @@
import {createEvent as createEventBase} from '@testing-library/dom'
import {eventMap, eventMapKeys} from './eventMap'
import {isMouseEvent} from './eventTypes'
import {eventMap, eventMapKeys, isMouseEvent} from './eventMap'
import {EventType, PointerCoords} from './types'

export type EventTypeInit<K extends EventType> = SpecificEventInit<
Expand Down
21 changes: 21 additions & 0 deletions src/event/eventMap.ts
@@ -1,8 +1,15 @@
import {eventMap as baseEventMap} from '@testing-library/dom/dist/event-map.js'
import {EventType} from './types'

export const eventMap = {
...baseEventMap,

auxclick: {
// like other events this should be PointerEvent, but this is missing in Jsdom
// see https://github.com/jsdom/jsdom/issues/2527
EventType: 'MouseEvent',
defaultInit: {bubbles: true, cancelable: true, composed: true},
},
beforeInput: {
EventType: 'InputEvent',
defaultInit: {bubbles: true, cancelable: true, composed: true},
Expand All @@ -12,3 +19,17 @@ export const eventMap = {
export const eventMapKeys: {
[k in keyof DocumentEventMap]?: keyof typeof eventMap
} = Object.fromEntries(Object.keys(eventMap).map(k => [k.toLowerCase(), k]))

function getEventClass(type: EventType) {
const k = eventMapKeys[type]
return k && eventMap[k].EventType
}

const mouseEvents = ['MouseEvent', 'PointerEvent']
export function isMouseEvent(type: EventType) {
return mouseEvents.includes(getEventClass(type) as string)
}

export function isKeyboardEvent(type: EventType) {
return getEventClass(type) === 'KeyboardEvent'
}
22 changes: 0 additions & 22 deletions src/event/eventTypes.ts

This file was deleted.

5 changes: 2 additions & 3 deletions src/event/index.ts
@@ -1,8 +1,7 @@
import {Config} from '../setup'
import {getUIEventModifiers} from '../utils'
import {createEvent, EventTypeInit} from './createEvent'
import {dispatchEvent} from './dispatchEvent'
import {isKeyboardEvent, isMouseEvent} from './eventTypes'
import {isKeyboardEvent, isMouseEvent} from './eventMap'
import {EventType, PointerCoords} from './types'

export type {EventType, PointerCoords}
Expand All @@ -17,7 +16,7 @@ export function dispatchUIEvent<K extends EventType>(
if (isMouseEvent(type) || isKeyboardEvent(type)) {
init = {
...init,
...getUIEventModifiers(config.keyboardState),
...config.system.getUIEventModifiers(),
} as EventTypeInit<K>
}

Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
@@ -1,4 +1,4 @@
export {userEvent as default} from './setup'
export type {keyboardKey} from './keyboard'
export type {pointerKey} from './pointer'
export type {keyboardKey} from './system/keyboard'
export type {pointerKey} from './system/pointer'
export {PointerEventsCheckLevel} from './options'
68 changes: 43 additions & 25 deletions src/keyboard/index.ts
@@ -1,36 +1,54 @@
import {Config, Instance} from '../setup'
import {keyboardAction, KeyboardAction, releaseAllKeys} from './keyboardAction'
import {keyboardKey} from '../system/keyboard'
import {wait} from '../utils'
import {parseKeyDef} from './parseKeyDef'
import type {keyboardState, keyboardKey} from './types'

export {releaseAllKeys}
export type {keyboardKey, keyboardState}
interface KeyboardAction {
keyDef: keyboardKey
releasePrevious: boolean
releaseSelf: boolean
repeat: number
}

export async function keyboard(this: Instance, text: string): Promise<void> {
const actions: KeyboardAction[] = parseKeyDef(this[Config].keyboardMap, text)

return keyboardAction(this[Config], actions)
for (let i = 0; i < actions.length; i++) {
await wait(this[Config])

await keyboardAction(this[Config], actions[i])
}
}

async function keyboardAction(
config: Config,
{keyDef, releasePrevious, releaseSelf, repeat}: KeyboardAction,
) {
const {system} = config

// Release the key automatically if it was pressed before.
if (system.keyboard.isKeyPressed(keyDef)) {
await system.keyboard.keyup(config, keyDef)
}

if (!releasePrevious) {
for (let i = 1; i <= repeat; i++) {
await system.keyboard.keydown(config, keyDef)

if (i < repeat) {
await wait(config)
}
}

// Release the key only on the last iteration on `state.repeatKey`.
if (releaseSelf) {
await system.keyboard.keyup(config, keyDef)
}
}
}

export function createKeyboardState(): keyboardState {
return {
activeElement: null,
pressed: [],
carryChar: '',
modifiers: {
Alt: false,
AltGraph: false,
Control: false,
CapsLock: false,
Fn: false,
FnLock: false,
Meta: false,
NumLock: false,
ScrollLock: false,
Shift: false,
Symbol: false,
SymbolLock: false,
},
modifierPhase: {},
export async function releaseAllKeys(config: Config) {
for (const k of config.system.keyboard.getPressedKeys()) {
await config.system.keyboard.keyup(config, k)
}
}
2 changes: 1 addition & 1 deletion src/keyboard/keyMap.ts
@@ -1,4 +1,4 @@
import {DOM_KEY_LOCATION, keyboardKey} from './types'
import {DOM_KEY_LOCATION, keyboardKey} from '../system/keyboard'

/**
* Mapping for a default US-104-QWERTY keyboard
Expand Down
148 changes: 0 additions & 148 deletions src/keyboard/keyboardAction.ts

This file was deleted.

0 comments on commit 2852509

Please sign in to comment.