Skip to content

Commit 31b7091

Browse files
authored
feat: report element with declaration in pointerEventsCheck (#950)
1 parent 7ea7a77 commit 31b7091

File tree

5 files changed

+169
-26
lines changed

5 files changed

+169
-26
lines changed

src/utils/pointer/cssPointerEvents.ts

+81-7
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,33 @@ import {PointerEventsCheckLevel} from '../../options'
22
import {Config} from '../../setup'
33
import {ApiLevel, getLevelRef} from '..'
44
import {getWindow} from '../misc/getWindow'
5+
import {isElementType} from '../misc/isElementType'
56

67
export function hasPointerEvents(element: Element): boolean {
8+
return closestPointerEventsDeclaration(element)?.pointerEvents !== 'none'
9+
}
10+
11+
function closestPointerEventsDeclaration(element: Element):
12+
| {
13+
pointerEvents: string
14+
tree: Element[]
15+
}
16+
| undefined {
717
const window = getWindow(element)
818

919
for (
10-
let el: Element | null = element;
20+
let el: Element | null = element, tree: Element[] = [];
1121
el?.ownerDocument;
1222
el = el.parentElement
1323
) {
24+
tree.push(el)
1425
const pointerEvents = window.getComputedStyle(el).pointerEvents
1526
if (pointerEvents && !['inherit', 'unset'].includes(pointerEvents)) {
16-
return pointerEvents !== 'none'
27+
return {pointerEvents, tree}
1728
}
1829
}
1930

20-
return true
31+
return undefined
2132
}
2233

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

55-
const result = hasPointerEvents(element)
66+
const declaration = closestPointerEventsDeclaration(element)
5667

5768
element[PointerEventsCheck] = {
5869
[ApiLevel.Call]: getLevelRef(config, ApiLevel.Call),
5970
[ApiLevel.Trigger]: getLevelRef(config, ApiLevel.Trigger),
60-
result,
71+
result: declaration?.pointerEvents !== 'none',
6172
}
6273

63-
if (!result) {
74+
if (declaration?.pointerEvents === 'none') {
6475
throw new Error(
65-
'Unable to perform pointer interaction as the element has or inherits pointer-events set to "none".',
76+
[
77+
`Unable to perform pointer interaction as the element ${
78+
declaration.tree.length > 1 ? 'inherits' : 'has'
79+
} \`pointer-events: none\`:`,
80+
'',
81+
printTree(declaration.tree),
82+
].join('\n'),
6683
)
6784
}
6885
}
6986

87+
function printTree(tree: Element[]) {
88+
return tree
89+
.reverse()
90+
.map((el, i) =>
91+
[
92+
''.padEnd(i),
93+
el.tagName,
94+
el.id && `#${el.id}`,
95+
el.hasAttribute('data-testid') &&
96+
`(testId=${el.getAttribute('data-testid')})`,
97+
getLabelDescr(el),
98+
tree.length > 1 &&
99+
i === 0 &&
100+
' <-- This element declared `pointer-events: none`',
101+
tree.length > 1 &&
102+
i === tree.length - 1 &&
103+
' <-- Asserted pointer events here',
104+
]
105+
.filter(Boolean)
106+
.join(''),
107+
)
108+
.join('\n')
109+
}
110+
111+
function getLabelDescr(element: Element) {
112+
let label: string | undefined | null
113+
if (element.hasAttribute('aria-label')) {
114+
label = element.getAttribute('aria-label') as string
115+
} else if (element.hasAttribute('aria-labelledby')) {
116+
label = element.ownerDocument
117+
.getElementById(element.getAttribute('aria-labelledby') as string)
118+
?.textContent?.trim()
119+
} else if (
120+
isElementType(element, [
121+
'button',
122+
'input',
123+
'meter',
124+
'output',
125+
'progress',
126+
'select',
127+
'textarea',
128+
]) &&
129+
element.labels?.length
130+
) {
131+
label = Array.from(element.labels)
132+
.map(el => el.textContent?.trim())
133+
.join('|')
134+
} else if (isElementType(element, 'button')) {
135+
label = element.textContent?.trim()
136+
}
137+
label = label?.replace(/\n/g, ' ')
138+
if (Number(label?.length) > 30) {
139+
label = `${label?.substring(0, 29)}…`
140+
}
141+
return label ? `(label=${label})` : ''
142+
}
143+
70144
// With the eslint rule and prettier the bitwise operation isn't nice to read
71145
function hasBitFlag(conf: number, flag: number) {
72146
// eslint-disable-next-line no-bitwise

tests/convenience/click.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe.each([
2121
const {element, user} = setup(`<div style="pointer-events: none"></div>`)
2222

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

tests/convenience/hover.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe.each([
3333
clearEventCalls()
3434

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

tests/utils/misc/hasPointerEvents.ts

-17
This file was deleted.
+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import {createConfig} from '#src/setup/setup'
2+
import {assertPointerEvents, hasPointerEvents} from '#src/utils'
3+
import {setup} from '#testHelpers'
4+
5+
test('get pointer-events from element or ancestor', async () => {
6+
const {element} = setup(`
7+
<div style="pointer-events: none">
8+
<input style="pointer-events: initial"/>
9+
<input style="pointer-events: inherit"/>
10+
<input/>
11+
</div>
12+
`)
13+
14+
expect(hasPointerEvents(element)).toBe(false)
15+
expect(hasPointerEvents(element.children[0])).toBe(true)
16+
expect(hasPointerEvents(element.children[1])).toBe(false)
17+
expect(hasPointerEvents(element.children[2])).toBe(false)
18+
})
19+
20+
test('report element that declared pointer-events', async () => {
21+
const {element} = setup(`
22+
<div id="foo" style="pointer-events: none">
23+
<span id="listlabel">Some list</span>
24+
<ul aria-labelledby="listlabel">
25+
<li aria-label="List entry">
26+
<span data-testid="target"></span>
27+
<button>foo</button>
28+
<label>
29+
An input element with a really long label text
30+
<input/>
31+
</label>
32+
</li>
33+
</ul>
34+
</div>
35+
`)
36+
37+
expect(() => assertPointerEvents(createConfig(), element))
38+
.toThrowErrorMatchingInlineSnapshot(`
39+
Unable to perform pointer interaction as the element has \`pointer-events: none\`:
40+
41+
DIV#foo
42+
`)
43+
44+
expect(() =>
45+
assertPointerEvents(
46+
createConfig(),
47+
element.querySelector('[data-testid="target"]') as Element,
48+
),
49+
).toThrowErrorMatchingInlineSnapshot(`
50+
Unable to perform pointer interaction as the element inherits \`pointer-events: none\`:
51+
52+
DIV#foo <-- This element declared \`pointer-events: none\`
53+
UL(label=Some list)
54+
LI(label=List entry)
55+
SPAN(testId=target) <-- Asserted pointer events here
56+
`)
57+
58+
expect(() =>
59+
assertPointerEvents(
60+
createConfig(),
61+
element.querySelector('button') as Element,
62+
),
63+
).toThrowErrorMatchingInlineSnapshot(`
64+
Unable to perform pointer interaction as the element inherits \`pointer-events: none\`:
65+
66+
DIV#foo <-- This element declared \`pointer-events: none\`
67+
UL(label=Some list)
68+
LI(label=List entry)
69+
BUTTON(label=foo) <-- Asserted pointer events here
70+
`)
71+
72+
expect(() =>
73+
assertPointerEvents(
74+
createConfig(),
75+
element.querySelector('input') as Element,
76+
),
77+
).toThrowErrorMatchingInlineSnapshot(`
78+
Unable to perform pointer interaction as the element inherits \`pointer-events: none\`:
79+
80+
DIV#foo <-- This element declared \`pointer-events: none\`
81+
UL(label=Some list)
82+
LI(label=List entry)
83+
LABEL
84+
INPUT(label=An input element with a reall…) <-- Asserted pointer events here
85+
`)
86+
})

0 commit comments

Comments
 (0)