Skip to content

Commit

Permalink
feat: support CSS pointer-events property (#631)
Browse files Browse the repository at this point in the history
Co-authored-by: Philipp Fritsche <ph.fritsche@gmail.com>
  • Loading branch information
MohitPopli and ph-fritsche committed Mar 30, 2021
1 parent f633a52 commit 32e9712
Show file tree
Hide file tree
Showing 12 changed files with 166 additions and 22 deletions.
10 changes: 10 additions & 0 deletions src/__tests__/click.js
Expand Up @@ -473,3 +473,13 @@ test('right click fires `contextmenu` instead of `click', () => {
expect(getEvents('contextmenu')).toHaveLength(1)
expect(getEvents('click')).toHaveLength(0)
})

test('fires no events when clicking element with pointer-events set to none', () => {
const {element, getEventSnapshot} = setup(
`<div style="pointer-events: none"></div>`,
)
userEvent.click(element)
expect(getEventSnapshot()).toMatchInlineSnapshot(
`No events were fired on: div`,
)
})
10 changes: 10 additions & 0 deletions src/__tests__/dblclick.js
Expand Up @@ -280,3 +280,13 @@ test('fires mouse events with custom buttons property', () => {
dblclick - button=1; buttons=4; detail=2
`)
})

test('fires no events when dblClick element with pointer-events set to none', () => {
const {element, getEventSnapshot} = setup(
`<div style="pointer-events: none"></div>`,
)
userEvent.dblClick(element)
expect(getEventSnapshot()).toMatchInlineSnapshot(
`No events were fired on: div`,
)
})
2 changes: 2 additions & 0 deletions src/__tests__/helpers/utils.js
Expand Up @@ -32,11 +32,13 @@ function setupSelect({
disabled = false,
disabledOptions = false,
multiple = false,
pointerEvents = 'auto',
} = {}) {
const form = document.createElement('form')
form.innerHTML = `
<select
name="select"
style="pointer-events: ${pointerEvents}"
${disabled ? 'disabled' : ''}
${multiple ? 'multiple' : ''}
>
Expand Down
10 changes: 10 additions & 0 deletions src/__tests__/hover.js
Expand Up @@ -122,3 +122,13 @@ test('fires non-bubbling events on parents for unhover', () => {
DIV: mouseleave"
`)
})

test('fires no events when hovering element with pointer-events set to none', () => {
const {element, getEventSnapshot} = setup(
`<div style="pointer-events: none"></div>`,
)
userEvent.hover(element)
expect(getEventSnapshot()).toMatchInlineSnapshot(
`No events were fired on: div`,
)
})
41 changes: 41 additions & 0 deletions src/__tests__/select-options.js
Expand Up @@ -218,3 +218,44 @@ test('should call onChange/input bubbling up the event when a new option is sele
expect(onInputSelect).toHaveBeenCalledTimes(1)
expect(onInputForm).toHaveBeenCalledTimes(1)
})

test('fire no pointer events when select has disabled pointer events', () => {
const {select, options, getEventSnapshot} = setupSelect({
pointerEvents: 'none',
})
userEvent.selectOptions(select, '2')
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: select[name="select"][value="2"]
select[name="select"][value="1"] - focus
select[name="select"][value="1"] - focusin
select[name="select"][value="2"] - input
select[name="select"][value="2"] - change
`)
const [o1, o2, o3] = options
expect(o1.selected).toBe(false)
expect(o2.selected).toBe(true)
expect(o3.selected).toBe(false)
})

test('fire no pointer events when multiple select has disabled pointer events', () => {
const {select, options, getEventSnapshot} = setupSelect({
multiple: true,
pointerEvents: 'none',
})
userEvent.selectOptions(select, ['2', '3'])
expect(getEventSnapshot()).toMatchInlineSnapshot(`
Events fired on: select[name="select"][value=["2","3"]]
select[name="select"][value=[]] - focus
select[name="select"][value=[]] - focusin
select[name="select"][value=["2"]] - input
select[name="select"][value=["2"]] - change
select[name="select"][value=["2","3"]] - input
select[name="select"][value=["2","3"]] - change
`)
const [o1, o2, o3] = options
expect(o1.selected).toBe(false)
expect(o2.selected).toBe(true)
expect(o3.selected).toBe(true)
})
10 changes: 10 additions & 0 deletions src/__tests__/unhover.js
Expand Up @@ -38,3 +38,13 @@ test('no events fired on labels that contain disabled controls', () => {
`No events were fired on: label`,
)
})

test('fires no events when unhover element with pointer-events set to none', () => {
const {element, getEventSnapshot} = setup(
`<div style="pointer-events: none"></div>`,
)
userEvent.unhover(element)
expect(getEventSnapshot()).toMatchInlineSnapshot(
`No events were fired on: div`,
)
})
17 changes: 17 additions & 0 deletions src/__tests__/utils/misc/hasPointerEvents.ts
@@ -0,0 +1,17 @@
import {hasPointerEvents} from 'utils'
import {setup} from '__tests__/helpers/utils'

test('get pointer-events from element or ancestor', () => {
const {element} = setup(`
<div style="pointer-events: none">
<input style="pointer-events: initial"/>
<input style="pointer-events: inherit"/>
<input/>
</div>
`)

expect(hasPointerEvents(element as HTMLDivElement)).toBe(false)
expect(hasPointerEvents((element as HTMLDivElement).children[0])).toBe(true)
expect(hasPointerEvents((element as HTMLDivElement).children[1])).toBe(false)
expect(hasPointerEvents((element as HTMLDivElement).children[2])).toBe(false)
})
3 changes: 3 additions & 0 deletions src/click.ts
Expand Up @@ -5,6 +5,7 @@ import {
isFocusable,
isDisabled,
isElementType,
hasPointerEvents,
} from './utils'
import {hover} from './hover'
import {blur} from './blur'
Expand Down Expand Up @@ -117,6 +118,7 @@ function click(
init?: MouseEventInit,
{skipHover = false, clickCount = 0}: clickOptions = {},
) {
if (!hasPointerEvents(element)) return
if (!skipHover) hover(element, init)

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

function dblClick(element: Element, init?: MouseEventInit) {
if (!hasPointerEvents(element)) return
hover(element, init)
click(element, init, {skipHover: true, clickCount: 0})
click(element, init, {skipHover: true, clickCount: 1})
Expand Down
3 changes: 3 additions & 0 deletions src/hover.ts
Expand Up @@ -3,6 +3,7 @@ import {
isLabelWithInternallyDisabledControl,
getMouseEventOptions,
isDisabled,
hasPointerEvents,
} from './utils'

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

function hover(element: Element, init?: MouseEventInit) {
if (!hasPointerEvents(element)) return
if (isLabelWithInternallyDisabledControl(element)) return

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

function unhover(element: Element, init?: MouseEventInit) {
if (!hasPointerEvents(element)) return
if (isLabelWithInternallyDisabledControl(element)) return

const parentElements = getParentElements(element)
Expand Down
63 changes: 41 additions & 22 deletions src/select-options.ts
@@ -1,5 +1,5 @@
import {createEvent, getConfig, fireEvent} from '@testing-library/dom'
import {isDisabled, isElementType} from './utils'
import {hasPointerEvents, isDisabled, isElementType} from './utils'
import {click} from './click'
import {focus} from './focus'
import {hover, unhover} from './hover'
Expand Down Expand Up @@ -47,36 +47,55 @@ function selectOptionsBase(
if (isElementType(select, 'select')) {
if (select.multiple) {
for (const option of selectedOptions) {
const withPointerEvents = hasPointerEvents(option)

// events fired for multiple select are weird. Can't use hover...
fireEvent.pointerOver(option, init)
fireEvent.pointerEnter(select, init)
fireEvent.mouseOver(option)
fireEvent.mouseEnter(select)
fireEvent.pointerMove(option, init)
fireEvent.mouseMove(option, init)
fireEvent.pointerDown(option, init)
fireEvent.mouseDown(option, init)
if (withPointerEvents) {
fireEvent.pointerOver(option, init)
fireEvent.pointerEnter(select, init)
fireEvent.mouseOver(option)
fireEvent.mouseEnter(select)
fireEvent.pointerMove(option, init)
fireEvent.mouseMove(option, init)
fireEvent.pointerDown(option, init)
fireEvent.mouseDown(option, init)
}

focus(select)
fireEvent.pointerUp(option, init)
fireEvent.mouseUp(option, init)

if (withPointerEvents) {
fireEvent.pointerUp(option, init)
fireEvent.mouseUp(option, init)
}

selectOption(option as HTMLOptionElement)
fireEvent.click(option, init)

if (withPointerEvents) {
fireEvent.click(option, init)
}
}
} else if (selectedOptions.length === 1) {
const withPointerEvents = hasPointerEvents(select)
// the click to open the select options
click(select, init)
if (withPointerEvents) {
click(select, init)
} else {
focus(select)
}

selectOption(selectedOptions[0] as HTMLOptionElement)

// the browser triggers another click event on the select for the click on the option
// this second click has no 'down' phase
fireEvent.pointerOver(select, init)
fireEvent.pointerEnter(select, init)
fireEvent.mouseOver(select)
fireEvent.mouseEnter(select)
fireEvent.pointerUp(select, init)
fireEvent.mouseUp(select, init)
fireEvent.click(select, init)
if (withPointerEvents) {
// the browser triggers another click event on the select for the click on the option
// this second click has no 'down' phase
fireEvent.pointerOver(select, init)
fireEvent.pointerEnter(select, init)
fireEvent.mouseOver(select)
fireEvent.mouseEnter(select)
fireEvent.pointerUp(select, init)
fireEvent.mouseUp(select, init)
fireEvent.click(select, init)
}
} else {
throw getConfig().getElementError(
`Cannot select multiple options on a non-multiple select`,
Expand Down
1 change: 1 addition & 0 deletions src/utils/index.ts
Expand Up @@ -23,3 +23,4 @@ export * from './misc/isLabelWithInternallyDisabledControl'
export * from './misc/isVisible'
export * from './misc/isDisabled'
export * from './misc/wait'
export * from './misc/hasPointerEvents'
18 changes: 18 additions & 0 deletions src/utils/misc/hasPointerEvents.ts
@@ -0,0 +1,18 @@
import {getWindowFromNode} from '@testing-library/dom/dist/helpers'

export function hasPointerEvents(element: Element): boolean {
const window = getWindowFromNode(element)

for (
let el: Element | null = element;
el?.ownerDocument;
el = el.parentElement
) {
const pointerEvents = window.getComputedStyle(el).pointerEvents
if (pointerEvents && !['inherit', 'unset'].includes(pointerEvents)) {
return pointerEvents !== 'none'
}
}

return true
}

0 comments on commit 32e9712

Please sign in to comment.