Skip to content

Commit f681f7b

Browse files
snowystingerph-fritsche
andauthoredJan 21, 2025··
fix(pointer): dispatch mouse events if pointerdown is defaultPrevented (#1121)
Co-authored-by: Philipp Fritsche <ph.fritsche@gmail.com>
1 parent 3e471d1 commit f681f7b

File tree

5 files changed

+112
-51
lines changed

5 files changed

+112
-51
lines changed
 

‎src/system/pointer/index.ts

+19-19
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ export class PointerHost {
101101
this.buttons.down(keyDef)
102102
pointer.down(instance, keyDef)
103103

104-
if (pointer.pointerType !== 'touch' && !pointer.isPrevented) {
105-
this.mouse.down(instance, keyDef, pointer)
104+
if (pointer.pointerType !== 'touch') {
105+
this.mouse.down(instance, keyDef, pointer.isPrevented)
106106
}
107107
}
108108

@@ -119,9 +119,9 @@ export class PointerHost {
119119
// the order in which they interweave/follow on a user interaction depends on the implementation.
120120
const pointermove = pointer.move(instance, position)
121121
const mousemove =
122-
pointer.pointerType === 'touch' || (pointer.isPrevented && pointer.isDown)
122+
pointer.pointerType === 'touch'
123123
? undefined
124-
: this.mouse.move(instance, position)
124+
: this.mouse.move(instance, position, pointer.isPrevented)
125125

126126
pointermove?.leave()
127127
mousemove?.leave()
@@ -143,6 +143,8 @@ export class PointerHost {
143143

144144
const pointer = this.pointers.get(this.getPointerName(keyDef))
145145

146+
const isPrevented = pointer.isPrevented
147+
146148
// TODO: deprecate the following implicit setting of position
147149
pointer.position = position
148150
if (pointer.pointerType !== 'touch') {
@@ -157,23 +159,21 @@ export class PointerHost {
157159
pointer.release(instance)
158160
}
159161

160-
if (!pointer.isPrevented) {
161-
if (pointer.pointerType === 'touch' && !pointer.isMultitouch) {
162-
const mousemove = this.mouse.move(instance, pointer.position)
163-
mousemove?.leave()
164-
mousemove?.enter()
165-
mousemove?.move()
162+
if (pointer.pointerType === 'touch' && !pointer.isMultitouch) {
163+
const mousemove = this.mouse.move(instance, position, isPrevented)
164+
mousemove?.leave()
165+
mousemove?.enter()
166+
mousemove?.move()
166167

167-
this.mouse.down(instance, keyDef, pointer)
168-
}
169-
if (!pointer.isMultitouch) {
170-
const mousemove = this.mouse.move(instance, pointer.position)
171-
mousemove?.leave()
172-
mousemove?.enter()
173-
mousemove?.move()
168+
this.mouse.down(instance, keyDef, isPrevented)
169+
}
170+
if (!pointer.isMultitouch) {
171+
const mousemove = this.mouse.move(instance, position, isPrevented)
172+
mousemove?.leave()
173+
mousemove?.enter()
174+
mousemove?.move()
174175

175-
this.mouse.up(instance, keyDef, pointer)
176-
}
176+
this.mouse.up(instance, keyDef, isPrevented)
177177
}
178178
}
179179

‎src/system/pointer/mouse.ts

+36-26
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
import {type Instance} from '../../setup'
99
import {getTreeDiff, isDisabled} from '../../utils'
1010
import {Buttons, getMouseEventButton, MouseButton} from './buttons'
11-
import {type Pointer} from './pointer'
1211
import {isDifferentPointerPosition, pointerKey, PointerPosition} from './shared'
1312

1413
/**
@@ -66,7 +65,12 @@ export class Mouse {
6665
}
6766
})()
6867

69-
move(instance: Instance, position: PointerPosition) {
68+
move(
69+
instance: Instance,
70+
position: PointerPosition,
71+
/** Whether `preventDefault()` has been called on the `pointerdown` event */
72+
isPrevented: boolean,
73+
) {
7074
const prevPosition = this.position
7175
const prevTarget = this.getTarget(instance)
7276

@@ -96,14 +100,23 @@ export class Mouse {
96100
}
97101
},
98102
move: () => {
103+
if (isPrevented) {
104+
return
105+
}
106+
99107
instance.dispatchUIEvent(nextTarget, 'mousemove', init)
100108

101109
this.modifySelecting(instance)
102110
},
103111
}
104112
}
105113

106-
down(instance: Instance, keyDef: pointerKey, pointer: Pointer) {
114+
down(
115+
instance: Instance,
116+
keyDef: pointerKey,
117+
/** Whether `preventDefault()` has been called on the `pointerdown` event */
118+
isPrevented: boolean,
119+
) {
107120
const button = this.buttons.down(keyDef)
108121

109122
if (button === undefined) {
@@ -112,42 +125,49 @@ export class Mouse {
112125

113126
const target = this.getTarget(instance)
114127
this.buttonDownTarget[button] = target
115-
const disabled = isDisabled(target)
116128
const init = this.getEventInit('mousedown', keyDef.button)
117-
if (disabled || instance.dispatchUIEvent(target, 'mousedown', init)) {
129+
const disabled = isDisabled(target)
130+
if (
131+
!isPrevented &&
132+
(disabled || instance.dispatchUIEvent(target, 'mousedown', init))
133+
) {
118134
this.startSelecting(instance, init.detail as number)
119135
focusElement(target)
120136
}
121137
if (!disabled && getMouseEventButton(keyDef.button) === 2) {
122138
instance.dispatchUIEvent(
123139
target,
124140
'contextmenu',
125-
this.getEventInit('contextmenu', keyDef.button, pointer),
141+
this.getEventInit('contextmenu', keyDef.button),
126142
)
127143
}
128144
}
129145

130-
up(instance: Instance, keyDef: pointerKey, pointer: Pointer) {
146+
up(
147+
instance: Instance,
148+
keyDef: pointerKey,
149+
/** Whether `preventDefault()` has been called on the `pointerdown` event */
150+
isPrevented: boolean,
151+
) {
131152
const button = this.buttons.up(keyDef)
132153

133154
if (button === undefined) {
134155
return
135156
}
136157
const target = this.getTarget(instance)
137158
if (!isDisabled(target)) {
138-
instance.dispatchUIEvent(
139-
target,
140-
'mouseup',
141-
this.getEventInit('mouseup', keyDef.button),
142-
)
143-
this.endSelecting()
159+
if (!isPrevented) {
160+
const mouseUpInit = this.getEventInit('mouseup', keyDef.button)
161+
instance.dispatchUIEvent(target, 'mouseup', mouseUpInit)
162+
this.endSelecting()
163+
}
144164

145165
const clickTarget = getTreeDiff(
146166
this.buttonDownTarget[button],
147167
target,
148168
)[2][0] as Element | undefined
149169
if (clickTarget) {
150-
const init = this.getEventInit('click', keyDef.button, pointer)
170+
const init = this.getEventInit('click', keyDef.button)
151171
if (init.detail) {
152172
instance.dispatchUIEvent(
153173
clickTarget,
@@ -169,21 +189,11 @@ export class Mouse {
169189
this.clickCount.reset()
170190
}
171191

172-
private getEventInit(
173-
type: EventType,
174-
button?: MouseButton,
175-
pointer?: Pointer,
176-
) {
177-
const init: PointerEventInit = {
192+
private getEventInit(type: EventType, button?: MouseButton) {
193+
const init: MouseEventInit = {
178194
...this.position.coords,
179195
}
180196

181-
if (pointer) {
182-
init.pointerId = pointer.pointerId
183-
init.pointerType = pointer.pointerType
184-
init.isPrimary = pointer.isPrimary
185-
}
186-
187197
init.button = getMouseEventButton(button)
188198
init.buttons = this.buttons.getButtons()
189199

‎src/system/pointer/pointer.ts

+1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export class Pointer {
108108

109109
assertPointerEvents(instance, target)
110110

111+
this.isPrevented = false
111112
this.isDown = false
112113
instance.dispatchUIEvent(target, 'pointerup', this.getEventInit())
113114
}

‎tests/convenience/click.ts

+17
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@ describe.each([
1313

1414
expect(getEvents('mouseover')).toHaveLength(1)
1515
expect(getEvents('mousedown')).toHaveLength(clickCount)
16+
expect(getEvents('pointerdown')).toHaveLength(clickCount)
17+
expect(getEvents('click')).toHaveLength(clickCount)
18+
expect(getEvents('dblclick')).toHaveLength(clickCount >= 2 ? 1 : 0)
19+
})
20+
21+
test('preventDefault on pointer down prevents compatibility events', async () => {
22+
const {element, getEvents, user} = setup(`<div></div>`, {
23+
eventHandlers: {pointerdown: e => e.preventDefault()},
24+
})
25+
26+
await user[method](element)
27+
28+
expect(getEvents('mouseover')).toHaveLength(1)
29+
expect(getEvents('mousedown')).toHaveLength(0)
30+
expect(getEvents('mouseup')).toHaveLength(0)
31+
expect(getEvents('pointerdown')).toHaveLength(clickCount)
32+
expect(getEvents('pointerup')).toHaveLength(clickCount)
1633
expect(getEvents('click')).toHaveLength(clickCount)
1734
expect(getEvents('dblclick')).toHaveLength(clickCount >= 2 ? 1 : 0)
1835
})

‎tests/pointer/click.ts

+39-6
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,31 @@ test('double click', async () => {
5050
expect(getEvents('click')).toHaveLength(2)
5151

5252
// detail reflects the click count
53-
expect(getEvents('mousedown')[1]).toHaveProperty('detail', 2)
53+
expect(getEvents('dblclick')[0]).toHaveProperty('detail', 2)
54+
})
55+
56+
test('double click with prevent compatibility', async () => {
57+
const {element, getClickEventsSnapshot, getEvents, user} = setup(
58+
`<div></div>`,
59+
{eventHandlers: {pointerdown: e => e.preventDefault()}},
60+
)
61+
62+
await user.pointer({keys: '[MouseLeft][MouseLeft]', target: element})
63+
64+
expect(getClickEventsSnapshot()).toMatchInlineSnapshot(`
65+
pointerdown - pointerId=1; pointerType=mouse; isPrimary=true
66+
pointerup - pointerId=1; pointerType=mouse; isPrimary=true
67+
click - button=0; buttons=0; detail=1
68+
pointerdown - pointerId=1; pointerType=mouse; isPrimary=true
69+
pointerup - pointerId=1; pointerType=mouse; isPrimary=true
70+
click - button=0; buttons=0; detail=2
71+
dblclick - button=0; buttons=0; detail=2
72+
`)
73+
74+
expect(getEvents('dblclick')).toHaveLength(1)
75+
expect(getEvents('click')).toHaveLength(2)
76+
77+
// detail reflects the click count
5478
expect(getEvents('dblclick')[0]).toHaveProperty('detail', 2)
5579
})
5680

@@ -138,9 +162,7 @@ test('click per touch device', async () => {
138162
click - button=0; buttons=0; detail=1
139163
`)
140164

141-
// mouse is pointerId=1, every other pointer gets a new id
142165
expect(getEvents('click')).toHaveLength(1)
143-
expect(getEvents('click')[0]).toHaveProperty('pointerId', 2)
144166
})
145167

146168
test('double click per touch device', async () => {
@@ -176,10 +198,7 @@ test('double click per touch device', async () => {
176198

177199
// mouse is pointerId=1, every other pointer gets a new id
178200
expect(getEvents('click')).toHaveLength(2)
179-
expect(getEvents('click')[0]).toHaveProperty('pointerId', 2)
180-
expect(getEvents('click')[1]).toHaveProperty('pointerId', 3)
181201
expect(getEvents('dblclick')).toHaveLength(1)
182-
expect(getEvents('dblclick')[0]).not.toHaveProperty('pointerId')
183202
})
184203

185204
test('multi touch does not click', async () => {
@@ -340,3 +359,17 @@ test('click closest common ancestor of pointerdown/pointerup', async () => {
340359
expect(getEvents('mouseup')).toHaveLength(1)
341360
expect(getEvents('click')).toHaveLength(0)
342361
})
362+
363+
test('preventDefault on pointer down prevents compatibility events works with pointer', async () => {
364+
const {element, getClickEventsSnapshot, getEvents, user} = setup('<div />', {
365+
eventHandlers: {pointerdown: e => e.preventDefault()},
366+
})
367+
await user.pointer({keys: '[MouseLeft]', target: element})
368+
369+
expect(getClickEventsSnapshot()).toMatchInlineSnapshot(`
370+
pointerdown - pointerId=1; pointerType=mouse; isPrimary=true
371+
pointerup - pointerId=1; pointerType=mouse; isPrimary=true
372+
click - button=0; buttons=0; detail=1
373+
`)
374+
expect(getEvents('click')).toHaveLength(1)
375+
})

0 commit comments

Comments
 (0)
Please sign in to comment.