Skip to content

Commit

Permalink
feat: apply modifier keys in pointer events (#751)
Browse files Browse the repository at this point in the history
  • Loading branch information
ph-fritsche committed Nov 28, 2021
1 parent c12ee44 commit e33eb86
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 54 deletions.
25 changes: 25 additions & 0 deletions src/__tests__/pointer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,3 +387,28 @@ test('asynchronous pointer', async () => {
mousemove - button=0; buttons=0; detail=0
`)
})

test('apply modifiers from keyboardstate', async () => {
const {element, getEvents} = setup(`<input/>`)

element.focus()
let keyboardState = userEvent.keyboard('[ShiftLeft>]')
userEvent.pointer({keys: '[MouseLeft]', target: element}, {keyboardState})
keyboardState = userEvent.keyboard('[/ShiftLeft][ControlRight>]', {
keyboardState,
})
userEvent.pointer({keys: '[MouseLeft]', target: element}, {keyboardState})
keyboardState = userEvent.keyboard('[/ControlRight][AltLeft>]', {
keyboardState,
})
userEvent.pointer({keys: '[MouseLeft]', target: element}, {keyboardState})
keyboardState = userEvent.keyboard('[/AltLeft][MetaLeft>]', {keyboardState})
userEvent.pointer({keys: '[MouseLeft]', target: element}, {keyboardState})

expect(getEvents('click')).toEqual([
expect.objectContaining({shiftKey: true}),
expect.objectContaining({ctrlKey: true}),
expect.objectContaining({altKey: true}),
expect.objectContaining({metaKey: true}),
])
})
26 changes: 13 additions & 13 deletions src/pointer/index.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,37 @@
import {getConfig as getDOMTestingLibraryConfig} from '@testing-library/dom'
import {createKeyboardState} from '../keyboard'
import {parseKeyDef} from './parseKeyDef'
import {defaultKeyMap} from './keyMap'
import {
pointerAction,
PointerAction,
PointerActionTarget,
} from './pointerAction'
import {pointerOptions, pointerState} from './types'
import type {inputDeviceState, pointerOptions, pointerState} from './types'

export function pointer(
input: PointerInput,
options?: Partial<pointerOptions & {pointerState: pointerState; delay: 0}>,
options?: Partial<pointerOptions & {delay: 0} & inputDeviceState>,
): pointerState
export function pointer(
input: PointerInput,
options: Partial<
pointerOptions & {pointerState: pointerState; delay: number}
>,
options: Partial<pointerOptions & {delay: number} & inputDeviceState>,
): Promise<pointerState>
export function pointer(
input: PointerInput,
options: Partial<pointerOptions & {pointerState: pointerState}> = {},
options: Partial<pointerOptions & inputDeviceState> = {},
) {
const {promise, state} = pointerImplementationWrapper(input, options)
const {promise, pointerState} = pointerImplementationWrapper(input, options)

if ((options.delay ?? 0) > 0) {
return getDOMTestingLibraryConfig().asyncWrapper(() =>
promise.then(() => state),
promise.then(() => pointerState),
)
} else {
// prevent users from dealing with UnhandledPromiseRejectionWarning in sync call
promise.catch(console.error)

return state
return pointerState
}
}

Expand All @@ -44,10 +43,11 @@ type PointerInput = PointerActionInput | Array<PointerActionInput>

export function pointerImplementationWrapper(
input: PointerInput,
config: Partial<pointerOptions & {pointerState: pointerState}>,
config: Partial<pointerOptions & inputDeviceState>,
) {
const {
pointerState: state = createPointerState(),
pointerState = createPointerState(),
keyboardState = createKeyboardState(),
delay = 0,
pointerMap = defaultKeyMap,
} = config
Expand All @@ -73,8 +73,8 @@ export function pointerImplementationWrapper(
})

return {
promise: pointerAction(actions, options, state),
state,
promise: pointerAction(actions, options, {pointerState, keyboardState}),
pointerState,
}
}

Expand Down
13 changes: 7 additions & 6 deletions src/pointer/pointerAction.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Coords, wait} from '../utils'
import {pointerMove, PointerMoveAction} from './pointerMove'
import {pointerPress, PointerPressAction} from './pointerPress'
import {pointerOptions, pointerState} from './types'
import {inputDeviceState, pointerOptions, pointerState} from './types'

export type PointerActionTarget = {
target?: Element
Expand All @@ -17,7 +17,7 @@ export type PointerAction = PointerActionTarget &
export async function pointerAction(
actions: PointerAction[],
options: pointerOptions,
state: pointerState,
state: inputDeviceState,
): Promise<unknown[]> {
const ret: Array<Promise<void>> = []

Expand All @@ -32,10 +32,11 @@ export async function pointerAction(
: action.keyDef.pointerType
: 'mouse'

const target = action.target ?? getPrevTarget(pointerName, state)
const target =
action.target ?? getPrevTarget(pointerName, state.pointerState)
const coords = completeCoords({
...(pointerName in state.position
? state.position[pointerName].coords
...(pointerName in state.pointerState.position
? state.pointerState.position[pointerName].coords
: undefined),
...action.coords,
})
Expand All @@ -55,7 +56,7 @@ export async function pointerAction(
}
}

delete state.activeClickCount
delete state.pointerState.activeClickCount

return Promise.all(ret)
}
Expand Down
15 changes: 7 additions & 8 deletions src/pointer/pointerMove.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import {Coords, firePointerEvent, isDescendantOrSelf} from '../utils'
import {pointerState, PointerTarget} from './types'
import {inputDeviceState, PointerTarget} from './types'

export interface PointerMoveAction extends PointerTarget {
pointerName?: string
}

export async function pointerMove(
{pointerName = 'mouse', target, coords}: PointerMoveAction,
state: pointerState,
{pointerState, keyboardState}: inputDeviceState,
): Promise<void> {
if (!(pointerName in state.position)) {
if (!(pointerName in pointerState.position)) {
throw new Error(
`Trying to move pointer "${pointerName}" which does not exist.`,
)
Expand All @@ -20,7 +20,7 @@ export async function pointerMove(
pointerType,
target: prevTarget,
coords: prevCoords,
} = state.position[pointerName]
} = pointerState.position[pointerName]

if (prevTarget && prevTarget !== target) {
// Here we could probably calculate a few coords to a fake boundary(?)
Expand All @@ -42,7 +42,7 @@ export async function pointerMove(
// Here we could probably calculate a few coords leading up to the final position
fireMove(target, coords)

state.position[pointerName] = {pointerId, pointerType, target, coords}
pointerState.position[pointerName] = {pointerId, pointerType, target, coords}

function fireMove(eventTarget: Element, eventCoords: Coords) {
fire(eventTarget, 'pointermove', eventCoords)
Expand Down Expand Up @@ -71,9 +71,8 @@ export async function pointerMove(

function fire(eventTarget: Element, type: string, eventCoords: Coords) {
return firePointerEvent(eventTarget, type, {
buttons: state.pressed
.filter(p => p.keyDef.pointerType === pointerType)
.map(p => p.keyDef.button ?? 0),
pointerState,
keyboardState,
coords: eventCoords,
pointerId,
pointerType,
Expand Down
50 changes: 27 additions & 23 deletions src/pointer/pointerPress.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import {Coords, firePointerEvent} from '../utils'
import type {pointerKey, pointerState, PointerTarget} from './types'
import type {
inputDeviceState,
pointerKey,
pointerState,
PointerTarget,
} from './types'

export interface PointerPressAction extends PointerTarget {
keyDef: pointerKey
Expand All @@ -9,9 +14,9 @@ export interface PointerPressAction extends PointerTarget {

export async function pointerPress(
{keyDef, releasePrevious, releaseSelf, target, coords}: PointerPressAction,
state: pointerState,
state: inputDeviceState,
): Promise<void> {
const previous = state.pressed.find(p => p.keyDef === keyDef)
const previous = state.pointerState.pressed.find(p => p.keyDef === keyDef)

const pointerName =
keyDef.pointerType === 'touch' ? keyDef.name : keyDef.pointerType
Expand Down Expand Up @@ -39,12 +44,12 @@ function down(
keyDef: pointerKey,
target: Element,
coords: Coords,
state: pointerState,
{pointerState, keyboardState}: inputDeviceState,
) {
const {name, pointerType, button} = keyDef
const pointerId = pointerType === 'mouse' ? 1 : getNextPointerId(state)
const pointerId = pointerType === 'mouse' ? 1 : getNextPointerId(pointerState)

state.position[pointerName] = {
pointerState.position[pointerName] = {
pointerId,
pointerType,
target,
Expand All @@ -54,7 +59,7 @@ function down(
let isMultiTouch = false
let isPrimary = true
if (pointerType !== 'mouse') {
for (const obj of state.pressed) {
for (const obj of pointerState.pressed) {
// TODO: test multi device input across browsers
// istanbul ignore else
if (obj.keyDef.pointerType === pointerType) {
Expand All @@ -65,11 +70,11 @@ function down(
}
}

if (state.activeClickCount?.[0] !== name) {
delete state.activeClickCount
if (pointerState.activeClickCount?.[0] !== name) {
delete pointerState.activeClickCount
}
const clickCount = Number(state.activeClickCount?.[1] ?? 0) + 1
state.activeClickCount = [name, clickCount]
const clickCount = Number(pointerState.activeClickCount?.[1] ?? 0) + 1
pointerState.activeClickCount = [name, clickCount]

const pressObj = {
keyDef,
Expand All @@ -80,15 +85,15 @@ function down(
isPrimary,
clickCount,
}
state.pressed.push(pressObj)
pointerState.pressed.push(pressObj)

if (pointerType !== 'mouse') {
fire('pointerover')
fire('pointerenter')
}
if (
pointerType !== 'mouse' ||
!state.pressed.some(
!pointerState.pressed.some(
p => p.keyDef !== keyDef && p.keyDef.pointerType === pointerType,
)
) {
Expand All @@ -104,10 +109,9 @@ function down(

function fire(type: string) {
return firePointerEvent(target, type, {
pointerState,
keyboardState,
button,
buttons: state.pressed
.filter(p => p.keyDef.pointerType === pointerType)
.map(p => p.keyDef.button ?? 0),
clickCount,
coords,
isPrimary,
Expand All @@ -122,15 +126,15 @@ function up(
{pointerType, button}: pointerKey,
target: Element,
coords: Coords,
state: pointerState,
{pointerState, keyboardState}: inputDeviceState,
pressed: pointerState['pressed'][number],
) {
state.pressed = state.pressed.filter(p => p !== pressed)
pointerState.pressed = pointerState.pressed.filter(p => p !== pressed)

const {isMultiTouch, isPrimary, pointerId, clickCount} = pressed
let {unpreventedDefault} = pressed

state.position[pointerName] = {
pointerState.position[pointerName] = {
pointerId,
pointerType,
target,
Expand All @@ -141,7 +145,8 @@ function up(

if (
pointerType !== 'mouse' ||
!state.pressed.filter(p => p.keyDef.pointerType === pointerType).length
!pointerState.pressed.filter(p => p.keyDef.pointerType === pointerType)
.length
) {
fire('pointerup')
}
Expand Down Expand Up @@ -169,10 +174,9 @@ function up(

function fire(type: string) {
return firePointerEvent(target, type, {
pointerState,
keyboardState,
button,
buttons: state.pressed
.filter(p => p.keyDef.pointerType === pointerType)
.map(p => p.keyDef.button ?? 0),
clickCount,
coords,
isPrimary,
Expand Down
6 changes: 6 additions & 0 deletions src/pointer/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {keyboardState} from 'keyboard/types'
import {Coords, MouseButton} from '../utils'

/**
Expand Down Expand Up @@ -61,3 +62,8 @@ export interface PointerTarget {
target: Element
coords: Coords
}

export interface inputDeviceState {
pointerState: pointerState
keyboardState: keyboardState
}
2 changes: 1 addition & 1 deletion src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ function _setup(

// pointer needs typecasting because of the overloading
pointer: ((...args: Parameters<typeof pointer>) => {
args[1] = {...pointerApiDefaults, ...args[1], pointerState}
args[1] = {...pointerApiDefaults, ...args[1], pointerState, keyboardState}
const ret = pointer(...args) as pointerState | Promise<pointerState>
if (ret instanceof Promise) {
return ret.then(() => undefined)
Expand Down
Loading

0 comments on commit e33eb86

Please sign in to comment.