Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: apply modifier keys in pointer events #751

Merged
merged 1 commit into from
Oct 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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