@@ -2,22 +2,33 @@ import {PointerEventsCheckLevel} from '../../options'
22import { Config } from '../../setup'
33import { ApiLevel , getLevelRef } from '..'
44import { getWindow } from '../misc/getWindow'
5+ import { isElementType } from '../misc/isElementType'
56
67export 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
2334const 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
71145function hasBitFlag ( conf : number , flag : number ) {
72146 // eslint-disable-next-line no-bitwise
0 commit comments