Skip to content

Commit 32e9712

Browse files
feat: support CSS pointer-events property (#631)
Co-authored-by: Philipp Fritsche <ph.fritsche@gmail.com>
1 parent f633a52 commit 32e9712

File tree

12 files changed

+166
-22
lines changed

12 files changed

+166
-22
lines changed

src/__tests__/click.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,3 +473,13 @@ test('right click fires `contextmenu` instead of `click', () => {
473473
expect(getEvents('contextmenu')).toHaveLength(1)
474474
expect(getEvents('click')).toHaveLength(0)
475475
})
476+
477+
test('fires no events when clicking element with pointer-events set to none', () => {
478+
const {element, getEventSnapshot} = setup(
479+
`<div style="pointer-events: none"></div>`,
480+
)
481+
userEvent.click(element)
482+
expect(getEventSnapshot()).toMatchInlineSnapshot(
483+
`No events were fired on: div`,
484+
)
485+
})

src/__tests__/dblclick.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,13 @@ test('fires mouse events with custom buttons property', () => {
280280
dblclick - button=1; buttons=4; detail=2
281281
`)
282282
})
283+
284+
test('fires no events when dblClick element with pointer-events set to none', () => {
285+
const {element, getEventSnapshot} = setup(
286+
`<div style="pointer-events: none"></div>`,
287+
)
288+
userEvent.dblClick(element)
289+
expect(getEventSnapshot()).toMatchInlineSnapshot(
290+
`No events were fired on: div`,
291+
)
292+
})

src/__tests__/helpers/utils.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ function setupSelect({
3232
disabled = false,
3333
disabledOptions = false,
3434
multiple = false,
35+
pointerEvents = 'auto',
3536
} = {}) {
3637
const form = document.createElement('form')
3738
form.innerHTML = `
3839
<select
3940
name="select"
41+
style="pointer-events: ${pointerEvents}"
4042
${disabled ? 'disabled' : ''}
4143
${multiple ? 'multiple' : ''}
4244
>

src/__tests__/hover.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,13 @@ test('fires non-bubbling events on parents for unhover', () => {
122122
DIV: mouseleave"
123123
`)
124124
})
125+
126+
test('fires no events when hovering element with pointer-events set to none', () => {
127+
const {element, getEventSnapshot} = setup(
128+
`<div style="pointer-events: none"></div>`,
129+
)
130+
userEvent.hover(element)
131+
expect(getEventSnapshot()).toMatchInlineSnapshot(
132+
`No events were fired on: div`,
133+
)
134+
})

src/__tests__/select-options.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,44 @@ test('should call onChange/input bubbling up the event when a new option is sele
218218
expect(onInputSelect).toHaveBeenCalledTimes(1)
219219
expect(onInputForm).toHaveBeenCalledTimes(1)
220220
})
221+
222+
test('fire no pointer events when select has disabled pointer events', () => {
223+
const {select, options, getEventSnapshot} = setupSelect({
224+
pointerEvents: 'none',
225+
})
226+
userEvent.selectOptions(select, '2')
227+
expect(getEventSnapshot()).toMatchInlineSnapshot(`
228+
Events fired on: select[name="select"][value="2"]
229+
230+
select[name="select"][value="1"] - focus
231+
select[name="select"][value="1"] - focusin
232+
select[name="select"][value="2"] - input
233+
select[name="select"][value="2"] - change
234+
`)
235+
const [o1, o2, o3] = options
236+
expect(o1.selected).toBe(false)
237+
expect(o2.selected).toBe(true)
238+
expect(o3.selected).toBe(false)
239+
})
240+
241+
test('fire no pointer events when multiple select has disabled pointer events', () => {
242+
const {select, options, getEventSnapshot} = setupSelect({
243+
multiple: true,
244+
pointerEvents: 'none',
245+
})
246+
userEvent.selectOptions(select, ['2', '3'])
247+
expect(getEventSnapshot()).toMatchInlineSnapshot(`
248+
Events fired on: select[name="select"][value=["2","3"]]
249+
250+
select[name="select"][value=[]] - focus
251+
select[name="select"][value=[]] - focusin
252+
select[name="select"][value=["2"]] - input
253+
select[name="select"][value=["2"]] - change
254+
select[name="select"][value=["2","3"]] - input
255+
select[name="select"][value=["2","3"]] - change
256+
`)
257+
const [o1, o2, o3] = options
258+
expect(o1.selected).toBe(false)
259+
expect(o2.selected).toBe(true)
260+
expect(o3.selected).toBe(true)
261+
})

src/__tests__/unhover.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,13 @@ test('no events fired on labels that contain disabled controls', () => {
3838
`No events were fired on: label`,
3939
)
4040
})
41+
42+
test('fires no events when unhover element with pointer-events set to none', () => {
43+
const {element, getEventSnapshot} = setup(
44+
`<div style="pointer-events: none"></div>`,
45+
)
46+
userEvent.unhover(element)
47+
expect(getEventSnapshot()).toMatchInlineSnapshot(
48+
`No events were fired on: div`,
49+
)
50+
})
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {hasPointerEvents} from 'utils'
2+
import {setup} from '__tests__/helpers/utils'
3+
4+
test('get pointer-events from element or ancestor', () => {
5+
const {element} = setup(`
6+
<div style="pointer-events: none">
7+
<input style="pointer-events: initial"/>
8+
<input style="pointer-events: inherit"/>
9+
<input/>
10+
</div>
11+
`)
12+
13+
expect(hasPointerEvents(element as HTMLDivElement)).toBe(false)
14+
expect(hasPointerEvents((element as HTMLDivElement).children[0])).toBe(true)
15+
expect(hasPointerEvents((element as HTMLDivElement).children[1])).toBe(false)
16+
expect(hasPointerEvents((element as HTMLDivElement).children[2])).toBe(false)
17+
})

src/click.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
isFocusable,
66
isDisabled,
77
isElementType,
8+
hasPointerEvents,
89
} from './utils'
910
import {hover} from './hover'
1011
import {blur} from './blur'
@@ -117,6 +118,7 @@ function click(
117118
init?: MouseEventInit,
118119
{skipHover = false, clickCount = 0}: clickOptions = {},
119120
) {
121+
if (!hasPointerEvents(element)) return
120122
if (!skipHover) hover(element, init)
121123

122124
if (isElementType(element, 'label')) {
@@ -141,6 +143,7 @@ function fireClick(element: Element, mouseEventOptions: MouseEventInit) {
141143
}
142144

143145
function dblClick(element: Element, init?: MouseEventInit) {
146+
if (!hasPointerEvents(element)) return
144147
hover(element, init)
145148
click(element, init, {skipHover: true, clickCount: 0})
146149
click(element, init, {skipHover: true, clickCount: 1})

src/hover.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
isLabelWithInternallyDisabledControl,
44
getMouseEventOptions,
55
isDisabled,
6+
hasPointerEvents,
67
} from './utils'
78

89
// includes `element`
@@ -16,6 +17,7 @@ function getParentElements(element: Element) {
1617
}
1718

1819
function hover(element: Element, init?: MouseEventInit) {
20+
if (!hasPointerEvents(element)) return
1921
if (isLabelWithInternallyDisabledControl(element)) return
2022

2123
const parentElements = getParentElements(element).reverse()
@@ -37,6 +39,7 @@ function hover(element: Element, init?: MouseEventInit) {
3739
}
3840

3941
function unhover(element: Element, init?: MouseEventInit) {
42+
if (!hasPointerEvents(element)) return
4043
if (isLabelWithInternallyDisabledControl(element)) return
4144

4245
const parentElements = getParentElements(element)

src/select-options.ts

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {createEvent, getConfig, fireEvent} from '@testing-library/dom'
2-
import {isDisabled, isElementType} from './utils'
2+
import {hasPointerEvents, isDisabled, isElementType} from './utils'
33
import {click} from './click'
44
import {focus} from './focus'
55
import {hover, unhover} from './hover'
@@ -47,36 +47,55 @@ function selectOptionsBase(
4747
if (isElementType(select, 'select')) {
4848
if (select.multiple) {
4949
for (const option of selectedOptions) {
50+
const withPointerEvents = hasPointerEvents(option)
51+
5052
// events fired for multiple select are weird. Can't use hover...
51-
fireEvent.pointerOver(option, init)
52-
fireEvent.pointerEnter(select, init)
53-
fireEvent.mouseOver(option)
54-
fireEvent.mouseEnter(select)
55-
fireEvent.pointerMove(option, init)
56-
fireEvent.mouseMove(option, init)
57-
fireEvent.pointerDown(option, init)
58-
fireEvent.mouseDown(option, init)
53+
if (withPointerEvents) {
54+
fireEvent.pointerOver(option, init)
55+
fireEvent.pointerEnter(select, init)
56+
fireEvent.mouseOver(option)
57+
fireEvent.mouseEnter(select)
58+
fireEvent.pointerMove(option, init)
59+
fireEvent.mouseMove(option, init)
60+
fireEvent.pointerDown(option, init)
61+
fireEvent.mouseDown(option, init)
62+
}
63+
5964
focus(select)
60-
fireEvent.pointerUp(option, init)
61-
fireEvent.mouseUp(option, init)
65+
66+
if (withPointerEvents) {
67+
fireEvent.pointerUp(option, init)
68+
fireEvent.mouseUp(option, init)
69+
}
70+
6271
selectOption(option as HTMLOptionElement)
63-
fireEvent.click(option, init)
72+
73+
if (withPointerEvents) {
74+
fireEvent.click(option, init)
75+
}
6476
}
6577
} else if (selectedOptions.length === 1) {
78+
const withPointerEvents = hasPointerEvents(select)
6679
// the click to open the select options
67-
click(select, init)
80+
if (withPointerEvents) {
81+
click(select, init)
82+
} else {
83+
focus(select)
84+
}
6885

6986
selectOption(selectedOptions[0] as HTMLOptionElement)
7087

71-
// the browser triggers another click event on the select for the click on the option
72-
// this second click has no 'down' phase
73-
fireEvent.pointerOver(select, init)
74-
fireEvent.pointerEnter(select, init)
75-
fireEvent.mouseOver(select)
76-
fireEvent.mouseEnter(select)
77-
fireEvent.pointerUp(select, init)
78-
fireEvent.mouseUp(select, init)
79-
fireEvent.click(select, init)
88+
if (withPointerEvents) {
89+
// the browser triggers another click event on the select for the click on the option
90+
// this second click has no 'down' phase
91+
fireEvent.pointerOver(select, init)
92+
fireEvent.pointerEnter(select, init)
93+
fireEvent.mouseOver(select)
94+
fireEvent.mouseEnter(select)
95+
fireEvent.pointerUp(select, init)
96+
fireEvent.mouseUp(select, init)
97+
fireEvent.click(select, init)
98+
}
8099
} else {
81100
throw getConfig().getElementError(
82101
`Cannot select multiple options on a non-multiple select`,

0 commit comments

Comments
 (0)