From 32e971226e41c8bb458d6f56982f3e603152cffc Mon Sep 17 00:00:00 2001 From: Mohit Date: Tue, 30 Mar 2021 17:14:01 +0800 Subject: [PATCH] feat: support CSS pointer-events property (#631) Co-authored-by: Philipp Fritsche --- src/__tests__/click.js | 10 ++++ src/__tests__/dblclick.js | 10 ++++ src/__tests__/helpers/utils.js | 2 + src/__tests__/hover.js | 10 ++++ src/__tests__/select-options.js | 41 +++++++++++++ src/__tests__/unhover.js | 10 ++++ src/__tests__/utils/misc/hasPointerEvents.ts | 17 ++++++ src/click.ts | 3 + src/hover.ts | 3 + src/select-options.ts | 63 +++++++++++++------- src/utils/index.ts | 1 + src/utils/misc/hasPointerEvents.ts | 18 ++++++ 12 files changed, 166 insertions(+), 22 deletions(-) create mode 100644 src/__tests__/utils/misc/hasPointerEvents.ts create mode 100644 src/utils/misc/hasPointerEvents.ts diff --git a/src/__tests__/click.js b/src/__tests__/click.js index bdbf2bf2..9800ba24 100644 --- a/src/__tests__/click.js +++ b/src/__tests__/click.js @@ -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( + `
`, + ) + userEvent.click(element) + expect(getEventSnapshot()).toMatchInlineSnapshot( + `No events were fired on: div`, + ) +}) diff --git a/src/__tests__/dblclick.js b/src/__tests__/dblclick.js index d449193d..75053538 100644 --- a/src/__tests__/dblclick.js +++ b/src/__tests__/dblclick.js @@ -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( + `
`, + ) + userEvent.dblClick(element) + expect(getEventSnapshot()).toMatchInlineSnapshot( + `No events were fired on: div`, + ) +}) diff --git a/src/__tests__/helpers/utils.js b/src/__tests__/helpers/utils.js index dc051a4a..7a18fd4a 100644 --- a/src/__tests__/helpers/utils.js +++ b/src/__tests__/helpers/utils.js @@ -32,11 +32,13 @@ function setupSelect({ disabled = false, disabledOptions = false, multiple = false, + pointerEvents = 'auto', } = {}) { const form = document.createElement('form') form.innerHTML = ` + + + + `) + + 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) +}) diff --git a/src/click.ts b/src/click.ts index 04af1cd7..da797497 100644 --- a/src/click.ts +++ b/src/click.ts @@ -5,6 +5,7 @@ import { isFocusable, isDisabled, isElementType, + hasPointerEvents, } from './utils' import {hover} from './hover' import {blur} from './blur' @@ -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')) { @@ -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}) diff --git a/src/hover.ts b/src/hover.ts index 12199684..29b1fd74 100644 --- a/src/hover.ts +++ b/src/hover.ts @@ -3,6 +3,7 @@ import { isLabelWithInternallyDisabledControl, getMouseEventOptions, isDisabled, + hasPointerEvents, } from './utils' // includes `element` @@ -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() @@ -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) diff --git a/src/select-options.ts b/src/select-options.ts index c9922242..47e4d76b 100644 --- a/src/select-options.ts +++ b/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' @@ -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`, diff --git a/src/utils/index.ts b/src/utils/index.ts index f87571fc..5341a95a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -23,3 +23,4 @@ export * from './misc/isLabelWithInternallyDisabledControl' export * from './misc/isVisible' export * from './misc/isDisabled' export * from './misc/wait' +export * from './misc/hasPointerEvents' diff --git a/src/utils/misc/hasPointerEvents.ts b/src/utils/misc/hasPointerEvents.ts new file mode 100644 index 00000000..32c8f83a --- /dev/null +++ b/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 +}