Skip to content

Commit e2a5f43

Browse files
authored
feat(pointer)!: introduce pointerEventsCheck option (#823)
BREAKING CHANGE: `skipPointerEvents` has been removed. Use `pointerEventsCheck: PointerEventsCheckLevel.Never` instead.
1 parent b83b259 commit e2a5f43

30 files changed

+377
-176
lines changed

src/clipboard/copy.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {fireEvent} from '@testing-library/dom'
2-
import {Config, UserEvent} from '../setup'
2+
import {Config, Instance} from '../setup'
33
import {copySelection, writeDataTransferToClipboard} from '../utils'
44

5-
export async function copy(this: UserEvent) {
5+
export async function copy(this: Instance) {
66
const doc = this[Config].document
77
const target = doc.activeElement ?? /* istanbul ignore next */ doc.body
88

src/clipboard/cut.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import {fireEvent} from '@testing-library/dom'
2-
import {Config, UserEvent} from '../setup'
2+
import {Config, Instance} from '../setup'
33
import {
44
copySelection,
55
isEditable,
66
prepareInput,
77
writeDataTransferToClipboard,
88
} from '../utils'
99

10-
export async function cut(this: UserEvent) {
10+
export async function cut(this: Instance) {
1111
const doc = this[Config].document
1212
const target = doc.activeElement ?? /* istanbul ignore next */ doc.body
1313

src/clipboard/paste.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {fireEvent} from '@testing-library/dom'
2-
import {Config, UserEvent} from '../setup'
2+
import {Config, Instance} from '../setup'
33
import {
44
createDataTransfer,
55
getSpaceUntilMaxLength,
@@ -9,7 +9,7 @@ import {
99
} from '../utils'
1010

1111
export async function paste(
12-
this: UserEvent,
12+
this: Instance,
1313
clipboardData?: DataTransfer | string,
1414
) {
1515
const doc = this[Config].document

src/convenience/click.ts

+4-23
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
import type {PointerInput} from '../pointer'
2-
import {hasPointerEvents} from '../utils'
3-
import {Config, UserEvent} from '../setup'
4-
5-
export async function click(this: UserEvent, element: Element): Promise<void> {
6-
if (!this[Config].skipPointerEventsCheck && !hasPointerEvents(element)) {
7-
throw new Error(
8-
'unable to click element as it has or inherits pointer-events set to "none".',
9-
)
10-
}
2+
import {Config, Instance} from '../setup'
113

4+
export async function click(this: Instance, element: Element): Promise<void> {
125
const pointerIn: PointerInput = []
136
if (!this[Config].skipHover) {
147
pointerIn.push({target: element})
@@ -19,27 +12,15 @@ export async function click(this: UserEvent, element: Element): Promise<void> {
1912
}
2013

2114
export async function dblClick(
22-
this: UserEvent,
15+
this: Instance,
2316
element: Element,
2417
): Promise<void> {
25-
if (!this[Config].skipPointerEventsCheck && !hasPointerEvents(element)) {
26-
throw new Error(
27-
'unable to double-click element as it has or inherits pointer-events set to "none".',
28-
)
29-
}
30-
3118
return this.pointer([{target: element}, '[MouseLeft][MouseLeft]'])
3219
}
3320

3421
export async function tripleClick(
35-
this: UserEvent,
22+
this: Instance,
3623
element: Element,
3724
): Promise<void> {
38-
if (!this[Config].skipPointerEventsCheck && !hasPointerEvents(element)) {
39-
throw new Error(
40-
'unable to triple-click element as it has or inherits pointer-events set to "none".',
41-
)
42-
}
43-
4425
return this.pointer([{target: element}, '[MouseLeft][MouseLeft][MouseLeft]'])
4526
}

src/convenience/hover.ts

+3-16
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,9 @@
1-
import {Config, UserEvent} from '../setup'
2-
import {hasPointerEvents} from '../utils'
3-
4-
export async function hover(this: UserEvent, element: Element) {
5-
if (!this[Config].skipPointerEventsCheck && !hasPointerEvents(element)) {
6-
throw new Error(
7-
'unable to hover element as it has or inherits pointer-events set to "none".',
8-
)
9-
}
1+
import {Instance} from '../setup'
102

3+
export async function hover(this: Instance, element: Element) {
114
return this.pointer({target: element})
125
}
136

14-
export async function unhover(this: UserEvent, element: Element) {
15-
if (!this[Config].skipPointerEventsCheck && !hasPointerEvents(element)) {
16-
throw new Error(
17-
'unable to unhover element as it has or inherits pointer-events set to "none".',
18-
)
19-
}
20-
7+
export async function unhover(this: Instance, element: Element) {
218
return this.pointer({target: element.ownerDocument.body})
229
}

src/convenience/tab.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type {UserEvent} from '../setup'
1+
import type {Instance} from '../setup'
22

33
export async function tab(
4-
this: UserEvent,
4+
this: Instance,
55
{
66
shift,
77
}: {

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export {userEvent as default} from './setup'
22
export type {keyboardKey} from './keyboard'
33
export type {pointerKey} from './pointer'
4+
export {PointerEventsCheckLevel} from './options'

src/keyboard/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import {Config, UserEvent} from '../setup'
1+
import {Config, Instance} from '../setup'
22
import {keyboardAction, KeyboardAction, releaseAllKeys} from './keyboardAction'
33
import {parseKeyDef} from './parseKeyDef'
44
import type {keyboardState, keyboardKey} from './types'
55

66
export {releaseAllKeys}
77
export type {keyboardKey, keyboardState}
88

9-
export async function keyboard(this: UserEvent, text: string): Promise<void> {
9+
export async function keyboard(this: Instance, text: string): Promise<void> {
1010
const {keyboardMap} = this[Config]
1111

1212
const actions: KeyboardAction[] = parseKeyDef(keyboardMap, text)

src/options.ts

+26-10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,20 @@ import type {pointerKey} from './pointer/types'
33
import {defaultKeyMap as defaultKeyboardMap} from './keyboard/keyMap'
44
import {defaultKeyMap as defaultPointerMap} from './pointer/keyMap'
55

6+
export enum PointerEventsCheckLevel {
7+
/**
8+
* Check pointer events on every user interaction that triggers a bunch of events.
9+
* E.g. once for releasing a mouse button even though this triggers `pointerup`, `mouseup`, `click`, etc...
10+
*/
11+
EachTrigger = 4,
12+
/** Check each target once per call to pointer (related) API */
13+
EachApiCall = 2,
14+
/** Check each event target once */
15+
EachTarget = 1,
16+
/** No pointer events check */
17+
Never = 0,
18+
}
19+
620
export interface Options {
721
/**
822
* When using `userEvent.upload`, automatically discard files
@@ -61,6 +75,17 @@ export interface Options {
6175
*/
6276
pointerMap?: pointerKey[]
6377

78+
/**
79+
* The pointer API includes a check if an element has or inherits `pointer-events: none`.
80+
* This check is known to be expensive and very expensive when checking deeply nested nodes.
81+
* This option determines how often the pointer related APIs perform the check.
82+
*
83+
* This is a binary flag option. You can combine multiple Levels.
84+
*
85+
* @default PointerEventsCheckLevel.EachCall
86+
*/
87+
pointerEventsCheck?: PointerEventsCheckLevel | number
88+
6489
/**
6590
* `userEvent.type` automatically releases any keys still pressed at the end of the call.
6691
* This option allows to opt out of this feature.
@@ -85,15 +110,6 @@ export interface Options {
85110
*/
86111
skipHover?: boolean
87112

88-
/**
89-
* Calling pointer related APIs on an element triggers a check if that element can receive pointer events.
90-
* This check is known to be expensive.
91-
* This option allows to skip the check.
92-
*
93-
* @default false
94-
*/
95-
skipPointerEventsCheck?: boolean
96-
97113
/**
98114
* Write selected data to Clipboard API when a `cut` or `copy` is triggered.
99115
*
@@ -116,10 +132,10 @@ export const defaultOptionsDirect: Required<Options> = {
116132
document: global.document,
117133
keyboardMap: defaultKeyboardMap,
118134
pointerMap: defaultPointerMap,
135+
pointerEventsCheck: PointerEventsCheckLevel.EachApiCall,
119136
skipAutoClose: false,
120137
skipClick: false,
121138
skipHover: false,
122-
skipPointerEventsCheck: false,
123139
writeToClipboard: false,
124140
}
125141

src/pointer/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Config, UserEvent} from '../setup'
1+
import {Config, Instance} from '../setup'
22
import {parseKeyDef} from './parseKeyDef'
33
import {
44
pointerAction,
@@ -16,7 +16,7 @@ type PointerActionInput =
1616
export type PointerInput = PointerActionInput | Array<PointerActionInput>
1717

1818
export async function pointer(
19-
this: UserEvent,
19+
this: Instance,
2020
input: PointerInput,
2121
): Promise<void> {
2222
const {pointerMap} = this[Config]

src/pointer/pointerAction.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ export async function pointerAction(config: Config, actions: PointerAction[]) {
3434
: undefined)
3535

3636
await ('keyDef' in action
37-
? pointerPress({...action, target, coords}, config)
38-
: pointerMove({...action, target, coords}, config))
37+
? pointerPress(config, {...action, target, coords})
38+
: pointerMove(config, {...action, target, coords}))
3939

4040
if (typeof config.delay === 'number') {
4141
if (i < actions.length - 1) {

src/pointer/pointerMove.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import {setUISelection} from '../document'
2-
import {inputDeviceState} from '../setup'
2+
import {Config} from '../setup'
33
import {
44
PointerCoords,
55
firePointerEvent,
66
isDescendantOrSelf,
77
isDisabled,
8+
assertPointerEvents,
9+
setLevelRef,
10+
ApiLevel,
811
} from '../utils'
912
import {resolveSelectionTarget} from './resolveSelectionTarget'
1013
import {PointerTarget, SelectionTarget} from './types'
@@ -14,9 +17,10 @@ export interface PointerMoveAction extends PointerTarget, SelectionTarget {
1417
}
1518

1619
export async function pointerMove(
20+
config: Config,
1721
{pointerName = 'mouse', target, coords, node, offset}: PointerMoveAction,
18-
{pointerState, keyboardState}: inputDeviceState,
1922
): Promise<void> {
23+
const {pointerState, keyboardState} = config
2024
if (!(pointerName in pointerState.position)) {
2125
throw new Error(
2226
`Trying to move pointer "${pointerName}" which does not exist.`,
@@ -32,6 +36,9 @@ export async function pointerMove(
3236
} = pointerState.position[pointerName]
3337

3438
if (prevTarget && prevTarget !== target) {
39+
setLevelRef(config, ApiLevel.Trigger)
40+
assertPointerEvents(config, prevTarget)
41+
3542
// Here we could probably calculate a few coords to a fake boundary(?)
3643
fireMove(prevTarget, prevCoords)
3744

@@ -40,6 +47,9 @@ export async function pointerMove(
4047
}
4148
}
4249

50+
setLevelRef(config, ApiLevel.Trigger)
51+
assertPointerEvents(config, target)
52+
4353
pointerState.position[pointerName] = {
4454
...pointerState.position[pointerName],
4555
target,

src/pointer/pointerPress.ts

+19-8
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
/* eslint-disable complexity */
22

33
import {
4+
ApiLevel,
5+
assertPointerEvents,
46
findClosest,
57
firePointerEvent,
68
focus,
79
isDisabled,
810
isElementType,
911
isFocusable,
12+
setLevelRef,
1013
} from '../utils'
1114
import {getUIValue, setUISelection} from '../document'
12-
import {inputDeviceState} from '../setup'
15+
import {Config} from '../setup'
1316
import type {
1417
pointerKey,
1518
pointerState,
@@ -25,26 +28,26 @@ export interface PointerPressAction extends PointerTarget, SelectionTarget {
2528
}
2629

2730
export async function pointerPress(
31+
config: Config,
2832
action: PointerPressAction,
29-
state: inputDeviceState,
3033
): Promise<void> {
3134
const {keyDef, target, releasePrevious, releaseSelf} = action
32-
const previous = state.pointerState.pressed.find(p => p.keyDef === keyDef)
35+
const previous = config.pointerState.pressed.find(p => p.keyDef === keyDef)
3336

3437
const pointerName =
3538
keyDef.pointerType === 'touch' ? keyDef.name : keyDef.pointerType
3639

3740
const targetIsDisabled = isDisabled(target)
3841

3942
if (previous) {
40-
up(state, pointerName, action, previous, targetIsDisabled)
43+
up(config, pointerName, action, previous, targetIsDisabled)
4144
}
4245

4346
if (!releasePrevious) {
44-
const press = down(state, pointerName, action, targetIsDisabled)
47+
const press = down(config, pointerName, action, targetIsDisabled)
4548

4649
if (releaseSelf) {
47-
up(state, pointerName, action, press, targetIsDisabled)
50+
up(config, pointerName, action, press, targetIsDisabled)
4851
}
4952
}
5053
}
@@ -55,11 +58,15 @@ function getNextPointerId(state: pointerState) {
5558
}
5659

5760
function down(
58-
{pointerState, keyboardState}: inputDeviceState,
61+
config: Config,
5962
pointerName: string,
6063
{keyDef, node, offset, target, coords}: PointerPressAction,
6164
targetIsDisabled: boolean,
6265
) {
66+
setLevelRef(config, ApiLevel.Trigger)
67+
assertPointerEvents(config, target)
68+
69+
const {pointerState, keyboardState} = config
6370
const {name, pointerType, button} = keyDef
6471
const pointerId = pointerType === 'mouse' ? 1 : getNextPointerId(pointerState)
6572

@@ -155,7 +162,7 @@ function down(
155162
}
156163

157164
function up(
158-
{pointerState, keyboardState}: inputDeviceState,
165+
config: Config,
159166
pointerName: string,
160167
{
161168
keyDef: {pointerType, button},
@@ -167,6 +174,10 @@ function up(
167174
pressed: pointerState['pressed'][number],
168175
targetIsDisabled: boolean,
169176
) {
177+
setLevelRef(config, ApiLevel.Trigger)
178+
assertPointerEvents(config, target)
179+
180+
const {pointerState, keyboardState} = config
170181
pointerState.pressed = pointerState.pressed.filter(p => p !== pressed)
171182

172183
const {isMultiTouch, isPrimary, pointerId, clickCount} = pressed

0 commit comments

Comments
 (0)