From c2650eda3f641da93935661137a6bf1cec761e3a Mon Sep 17 00:00:00 2001 From: Philipp Fritsche Date: Wed, 20 Oct 2021 16:16:37 +0000 Subject: [PATCH] feat: apply modifier keys in pointer events --- src/__tests__/pointer/index.ts | 25 +++++++++++++ src/pointer/index.ts | 26 +++++++------- src/pointer/pointerAction.ts | 13 +++---- src/pointer/pointerMove.ts | 15 ++++---- src/pointer/pointerPress.ts | 50 ++++++++++++++------------ src/pointer/types.ts | 6 ++++ src/setup.ts | 2 +- src/utils/pointer/firePointerEvents.ts | 18 ++++++++-- 8 files changed, 101 insertions(+), 54 deletions(-) diff --git a/src/__tests__/pointer/index.ts b/src/__tests__/pointer/index.ts index c9a0bcdb..c8e73394 100644 --- a/src/__tests__/pointer/index.ts +++ b/src/__tests__/pointer/index.ts @@ -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(``) + + 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}), + ]) +}) diff --git a/src/pointer/index.ts b/src/pointer/index.ts index ee5cd753..75da2eeb 100644 --- a/src/pointer/index.ts +++ b/src/pointer/index.ts @@ -1,4 +1,5 @@ import {getConfig as getDOMTestingLibraryConfig} from '@testing-library/dom' +import {createKeyboardState} from '../keyboard' import {parseKeyDef} from './parseKeyDef' import {defaultKeyMap} from './keyMap' import { @@ -6,33 +7,31 @@ import { PointerAction, PointerActionTarget, } from './pointerAction' -import {pointerOptions, pointerState} from './types' +import type {inputDeviceState, pointerOptions, pointerState} from './types' export function pointer( input: PointerInput, - options?: Partial, + options?: Partial, ): pointerState export function pointer( input: PointerInput, - options: Partial< - pointerOptions & {pointerState: pointerState; delay: number} - >, + options: Partial, ): Promise export function pointer( input: PointerInput, - options: Partial = {}, + options: Partial = {}, ) { - 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 } } @@ -44,10 +43,11 @@ type PointerInput = PointerActionInput | Array export function pointerImplementationWrapper( input: PointerInput, - config: Partial, + config: Partial, ) { const { - pointerState: state = createPointerState(), + pointerState = createPointerState(), + keyboardState = createKeyboardState(), delay = 0, pointerMap = defaultKeyMap, } = config @@ -73,8 +73,8 @@ export function pointerImplementationWrapper( }) return { - promise: pointerAction(actions, options, state), - state, + promise: pointerAction(actions, options, {pointerState, keyboardState}), + pointerState, } } diff --git a/src/pointer/pointerAction.ts b/src/pointer/pointerAction.ts index 9af8dc5a..f4df5e4d 100644 --- a/src/pointer/pointerAction.ts +++ b/src/pointer/pointerAction.ts @@ -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 @@ -17,7 +17,7 @@ export type PointerAction = PointerActionTarget & export async function pointerAction( actions: PointerAction[], options: pointerOptions, - state: pointerState, + state: inputDeviceState, ): Promise { const ret: Array> = [] @@ -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, }) @@ -55,7 +56,7 @@ export async function pointerAction( } } - delete state.activeClickCount + delete state.pointerState.activeClickCount return Promise.all(ret) } diff --git a/src/pointer/pointerMove.ts b/src/pointer/pointerMove.ts index 3ecd32f7..8a27539e 100644 --- a/src/pointer/pointerMove.ts +++ b/src/pointer/pointerMove.ts @@ -1,5 +1,5 @@ import {Coords, firePointerEvent, isDescendantOrSelf} from '../utils' -import {pointerState, PointerTarget} from './types' +import {inputDeviceState, PointerTarget} from './types' export interface PointerMoveAction extends PointerTarget { pointerName?: string @@ -7,9 +7,9 @@ export interface PointerMoveAction extends PointerTarget { export async function pointerMove( {pointerName = 'mouse', target, coords}: PointerMoveAction, - state: pointerState, + {pointerState, keyboardState}: inputDeviceState, ): Promise { - if (!(pointerName in state.position)) { + if (!(pointerName in pointerState.position)) { throw new Error( `Trying to move pointer "${pointerName}" which does not exist.`, ) @@ -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(?) @@ -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) @@ -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, diff --git a/src/pointer/pointerPress.ts b/src/pointer/pointerPress.ts index d3421bec..5237522a 100644 --- a/src/pointer/pointerPress.ts +++ b/src/pointer/pointerPress.ts @@ -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 @@ -9,9 +14,9 @@ export interface PointerPressAction extends PointerTarget { export async function pointerPress( {keyDef, releasePrevious, releaseSelf, target, coords}: PointerPressAction, - state: pointerState, + state: inputDeviceState, ): Promise { - 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 @@ -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, @@ -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) { @@ -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, @@ -80,7 +85,7 @@ function down( isPrimary, clickCount, } - state.pressed.push(pressObj) + pointerState.pressed.push(pressObj) if (pointerType !== 'mouse') { fire('pointerover') @@ -88,7 +93,7 @@ function down( } if ( pointerType !== 'mouse' || - !state.pressed.some( + !pointerState.pressed.some( p => p.keyDef !== keyDef && p.keyDef.pointerType === pointerType, ) ) { @@ -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, @@ -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, @@ -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') } @@ -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, diff --git a/src/pointer/types.ts b/src/pointer/types.ts index ddf52725..70e336b7 100644 --- a/src/pointer/types.ts +++ b/src/pointer/types.ts @@ -1,3 +1,4 @@ +import {keyboardState} from 'keyboard/types' import {Coords, MouseButton} from '../utils' /** @@ -61,3 +62,8 @@ export interface PointerTarget { target: Element coords: Coords } + +export interface inputDeviceState { + pointerState: pointerState + keyboardState: keyboardState +} diff --git a/src/setup.ts b/src/setup.ts index 29d712b0..2d43c4ad 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -160,7 +160,7 @@ function _setup( // pointer needs typecasting because of the overloading pointer: ((...args: Parameters) => { - args[1] = {...pointerApiDefaults, ...args[1], pointerState} + args[1] = {...pointerApiDefaults, ...args[1], pointerState, keyboardState} const ret = pointer(...args) as pointerState | Promise if (ret instanceof Promise) { return ret.then(() => undefined) diff --git a/src/utils/pointer/firePointerEvents.ts b/src/utils/pointer/firePointerEvents.ts index 6c2f5c8a..69d367ba 100644 --- a/src/utils/pointer/firePointerEvents.ts +++ b/src/utils/pointer/firePointerEvents.ts @@ -1,4 +1,6 @@ import {fireEvent} from '@testing-library/dom' +import type {pointerState} from 'pointer/types' +import type {keyboardState} from 'keyboard/types' import {FakeEventInit, FakeMouseEvent, FakePointerEvent} from './fakeEvent' import {getMouseButton, getMouseButtons, MouseButton} from './mouseButtons' @@ -17,17 +19,19 @@ export function firePointerEvent( target: Element, type: string, { + pointerState, + keyboardState, pointerType, button, - buttons, coords, pointerId, isPrimary, clickCount, }: { + pointerState: pointerState + keyboardState: keyboardState pointerType?: 'mouse' | 'pen' | 'touch' button?: MouseButton - buttons: MouseButton[] coords: Coords pointerId?: number isPrimary?: boolean @@ -41,6 +45,10 @@ export function firePointerEvent( let init: FakeEventInit = { ...coords, + altKey: keyboardState.modifiers.alt, + ctrlKey: keyboardState.modifiers.ctrl, + metaKey: keyboardState.modifiers.meta, + shiftKey: keyboardState.modifiers.shift, } if (Event === FakePointerEvent) { init = {...init, pointerId, pointerType} @@ -52,7 +60,11 @@ export function firePointerEvent( ['mousedown', 'mouseup', 'pointerdown', 'pointerup', 'click'].includes(type) ) { init.button = getMouseButton(button ?? 0) - init.buttons = getMouseButtons(...buttons) + init.buttons = getMouseButtons( + ...pointerState.pressed + .filter(p => p.keyDef.pointerType === pointerType) + .map(p => p.keyDef.button ?? 0), + ) } if (['mousedown', 'mouseup', 'click'].includes(type)) { init.detail = clickCount