Skip to content

Commit

Permalink
feat: report element with declaration in pointerEventsCheck (#950)
Browse files Browse the repository at this point in the history
  • Loading branch information
ph-fritsche committed May 12, 2022
1 parent 7ea7a77 commit 31b7091
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 26 deletions.
88 changes: 81 additions & 7 deletions src/utils/pointer/cssPointerEvents.ts
Expand Up @@ -2,22 +2,33 @@ import {PointerEventsCheckLevel} from '../../options'
import {Config} from '../../setup'
import {ApiLevel, getLevelRef} from '..'
import {getWindow} from '../misc/getWindow'
import {isElementType} from '../misc/isElementType'

export function hasPointerEvents(element: Element): boolean {
return closestPointerEventsDeclaration(element)?.pointerEvents !== 'none'
}

function closestPointerEventsDeclaration(element: Element):
| {
pointerEvents: string
tree: Element[]
}
| undefined {
const window = getWindow(element)

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

return true
return undefined
}

const PointerEventsCheck = Symbol('Last check for pointer-events')
Expand Down Expand Up @@ -52,21 +63,84 @@ export function assertPointerEvents(config: Config, element: Element) {
return
}

const result = hasPointerEvents(element)
const declaration = closestPointerEventsDeclaration(element)

element[PointerEventsCheck] = {
[ApiLevel.Call]: getLevelRef(config, ApiLevel.Call),
[ApiLevel.Trigger]: getLevelRef(config, ApiLevel.Trigger),
result,
result: declaration?.pointerEvents !== 'none',
}

if (!result) {
if (declaration?.pointerEvents === 'none') {
throw new Error(
'Unable to perform pointer interaction as the element has or inherits pointer-events set to "none".',
[
`Unable to perform pointer interaction as the element ${
declaration.tree.length > 1 ? 'inherits' : 'has'
} \`pointer-events: none\`:`,
'',
printTree(declaration.tree),
].join('\n'),
)
}
}

function printTree(tree: Element[]) {
return tree
.reverse()
.map((el, i) =>
[
''.padEnd(i),
el.tagName,
el.id && `#${el.id}`,
el.hasAttribute('data-testid') &&
`(testId=${el.getAttribute('data-testid')})`,
getLabelDescr(el),
tree.length > 1 &&
i === 0 &&
' <-- This element declared `pointer-events: none`',
tree.length > 1 &&
i === tree.length - 1 &&
' <-- Asserted pointer events here',
]
.filter(Boolean)
.join(''),
)
.join('\n')
}

function getLabelDescr(element: Element) {
let label: string | undefined | null
if (element.hasAttribute('aria-label')) {
label = element.getAttribute('aria-label') as string
} else if (element.hasAttribute('aria-labelledby')) {
label = element.ownerDocument
.getElementById(element.getAttribute('aria-labelledby') as string)
?.textContent?.trim()
} else if (
isElementType(element, [
'button',
'input',
'meter',
'output',
'progress',
'select',
'textarea',
]) &&
element.labels?.length
) {
label = Array.from(element.labels)
.map(el => el.textContent?.trim())
.join('|')
} else if (isElementType(element, 'button')) {
label = element.textContent?.trim()
}
label = label?.replace(/\n/g, ' ')
if (Number(label?.length) > 30) {
label = `${label?.substring(0, 29)}…`
}
return label ? `(label=${label})` : ''
}

// With the eslint rule and prettier the bitwise operation isn't nice to read
function hasBitFlag(conf: number, flag: number) {
// eslint-disable-next-line no-bitwise
Expand Down
2 changes: 1 addition & 1 deletion tests/convenience/click.ts
Expand Up @@ -21,7 +21,7 @@ describe.each([
const {element, user} = setup(`<div style="pointer-events: none"></div>`)

await expect(user[method](element)).rejects.toThrowError(
/has or inherits pointer-events/i,
/has `pointer-events: none`/i,
)
})

Expand Down
2 changes: 1 addition & 1 deletion tests/convenience/hover.ts
Expand Up @@ -33,7 +33,7 @@ describe.each([
clearEventCalls()

await expect(user[method](element)).rejects.toThrowError(
/has or inherits pointer-events/i,
/has `pointer-events: none`/i,
)
})

Expand Down
17 changes: 0 additions & 17 deletions tests/utils/misc/hasPointerEvents.ts

This file was deleted.

86 changes: 86 additions & 0 deletions tests/utils/pointer/cssPointerEvents.ts
@@ -0,0 +1,86 @@
import {createConfig} from '#src/setup/setup'
import {assertPointerEvents, hasPointerEvents} from '#src/utils'
import {setup} from '#testHelpers'

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

expect(hasPointerEvents(element)).toBe(false)
expect(hasPointerEvents(element.children[0])).toBe(true)
expect(hasPointerEvents(element.children[1])).toBe(false)
expect(hasPointerEvents(element.children[2])).toBe(false)
})

test('report element that declared pointer-events', async () => {
const {element} = setup(`
<div id="foo" style="pointer-events: none">
<span id="listlabel">Some list</span>
<ul aria-labelledby="listlabel">
<li aria-label="List entry">
<span data-testid="target"></span>
<button>foo</button>
<label>
An input element with a really long label text
<input/>
</label>
</li>
</ul>
</div>
`)

expect(() => assertPointerEvents(createConfig(), element))
.toThrowErrorMatchingInlineSnapshot(`
Unable to perform pointer interaction as the element has \`pointer-events: none\`:
DIV#foo
`)

expect(() =>
assertPointerEvents(
createConfig(),
element.querySelector('[data-testid="target"]') as Element,
),
).toThrowErrorMatchingInlineSnapshot(`
Unable to perform pointer interaction as the element inherits \`pointer-events: none\`:
DIV#foo <-- This element declared \`pointer-events: none\`
UL(label=Some list)
LI(label=List entry)
SPAN(testId=target) <-- Asserted pointer events here
`)

expect(() =>
assertPointerEvents(
createConfig(),
element.querySelector('button') as Element,
),
).toThrowErrorMatchingInlineSnapshot(`
Unable to perform pointer interaction as the element inherits \`pointer-events: none\`:
DIV#foo <-- This element declared \`pointer-events: none\`
UL(label=Some list)
LI(label=List entry)
BUTTON(label=foo) <-- Asserted pointer events here
`)

expect(() =>
assertPointerEvents(
createConfig(),
element.querySelector('input') as Element,
),
).toThrowErrorMatchingInlineSnapshot(`
Unable to perform pointer interaction as the element inherits \`pointer-events: none\`:
DIV#foo <-- This element declared \`pointer-events: none\`
UL(label=Some list)
LI(label=List entry)
LABEL
INPUT(label=An input element with a reall…) <-- Asserted pointer events here
`)
})

0 comments on commit 31b7091

Please sign in to comment.