@@ -49,6 +49,50 @@ function isMenuItemNode(node) {
4949 return MENUITEM_ROLES . has ( getTextAttr ( node , 'role' ) ) ;
5050}
5151
52+ // Build the element-description string used in error messages. Surfaces the
53+ // attribute that *makes* the element interactive when the bare tag would be
54+ // uninformative — e.g. `<div role="menu">`, `<div contenteditable>`, or
55+ // `<div tabindex="0">`. For self-explanatory native interactive tags
56+ // (button, input, a, etc.) the tag alone is returned, since adding the
57+ // triggering attribute would be redundant noise.
58+ function describeInteractive ( node ) {
59+ const tag = node . tag ;
60+
61+ const role = getTextAttr ( node , 'role' ) ;
62+ if ( role && INTERACTIVE_ROLES . has ( role ) ) {
63+ return `${ tag } role="${ role } "` ;
64+ }
65+
66+ if ( hasAttr ( node , 'contenteditable' ) ) {
67+ const ce = getTextAttr ( node , 'contenteditable' ) ;
68+ const normalized = typeof ce === 'string' ? ce . trim ( ) . toLowerCase ( ) : ce ;
69+ if ( normalized !== 'false' ) {
70+ // Surface 'plaintext-only' as a distinct spec keyword; collapse the
71+ // empty string, 'true', and the bare attribute to a uniform form.
72+ if ( normalized === 'plaintext-only' ) {
73+ return `${ tag } contenteditable="plaintext-only"` ;
74+ }
75+ return `${ tag } contenteditable` ;
76+ }
77+ }
78+
79+ // Tabindex-only interactivity: the tag (typically <div>/<span>) carries no
80+ // signal on its own, so surface the tabindex value. Skip for elements that
81+ // are already interactive via tag/usemap/canvas — for those the tag itself
82+ // is the source of interactivity and the tabindex would be redundant.
83+ if (
84+ hasAttr ( node , 'tabindex' ) &&
85+ ! isHtmlInteractiveContent ( node , getTextAttr , { ignoreUsemap : false } ) &&
86+ tag !== 'canvas' &&
87+ ! ( tag === 'object' && hasAttr ( node , 'usemap' ) )
88+ ) {
89+ const tabindex = getTextAttr ( node , 'tabindex' ) ;
90+ return tabindex === undefined ? `${ tag } tabindex` : `${ tag } tabindex="${ tabindex } "` ;
91+ }
92+
93+ return tag ;
94+ }
95+
5296function isAllowedDetailsChild ( childNode , parentEntry ) {
5397 if ( parentEntry . tag !== 'details' ) {
5498 return false ;
@@ -222,7 +266,7 @@ module.exports = {
222266 context . report ( {
223267 node,
224268 messageId : 'nested' ,
225- data : { parent : parentEntry . tag , child : node . tag } ,
269+ data : { parent : parentEntry . describe , child : describeInteractive ( node ) } ,
226270 } ) ;
227271 }
228272 parentEntry . interactiveChildCount ++ ;
@@ -244,15 +288,20 @@ module.exports = {
244288 context . report ( {
245289 node,
246290 messageId : 'nested' ,
247- data : { parent : parentEntry . tag , child : node . tag } ,
291+ data : { parent : parentEntry . describe , child : describeInteractive ( node ) } ,
248292 } ) ;
249293 }
250294 }
251295
252296 // Push interactive elements to the stack, but tabindex-only elements
253297 // should not become parent interactive nodes
254298 if ( currentIsInteractive && ! isInteractiveOnlyFromTabindex ( node ) ) {
255- interactiveStack . push ( { tag : node . tag , node, interactiveChildCount : 0 } ) ;
299+ interactiveStack . push ( {
300+ tag : node . tag ,
301+ node,
302+ describe : describeInteractive ( node ) ,
303+ interactiveChildCount : 0 ,
304+ } ) ;
256305 }
257306 } ,
258307
0 commit comments