Skip to content

Commit 4845395

Browse files
Merge pull request #2686 from johanrd/day_fix/template-require-context-role
Post-merge-review: Fix template-require-context-role: align aria-hidden scope and report oc with upstream
2 parents 7f72f25 + 4196ef0 commit 4845395

2 files changed

Lines changed: 55 additions & 34 deletions

File tree

lib/rules/template-require-context-role.js

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,20 @@ module.exports = {
4949

5050
if (role && ROLES_REQUIRING_CONTEXT[role]) {
5151
// Skip check if at root level (no parent elements — context may be external)
52-
if (elementStack.length > 1 && !isInsideAriaHidden(elementStack)) {
53-
const parentRole = getAccessibleParentRole(elementStack);
52+
if (elementStack.length > 1) {
53+
const parentContext = getParentContext(elementStack);
54+
if (parentContext.ariaHidden) {
55+
// aria-hidden on the effective parent (or a transparent wrapper
56+
// walked through on the way up) — upstream suppresses the rule.
57+
return;
58+
}
59+
const parentRole = parentContext.role;
5460
if (parentRole === undefined) {
5561
// No non-transparent parent found (effectively root) — skip
5662
} else if (!parentRole || !ROLES_REQUIRING_CONTEXT[role].includes(parentRole)) {
63+
const roleAttr = node.attributes?.find((a) => a.name === 'role');
5764
context.report({
58-
node,
65+
node: roleAttr || node,
5966
messageId: 'missingContext',
6067
data: {
6168
role,
@@ -82,46 +89,37 @@ function getRoleFromNode(node) {
8289
return null;
8390
}
8491

85-
/**
86-
* Check if any ancestor element in the stack has aria-hidden="true".
87-
*/
88-
function isInsideAriaHidden(elementStack) {
89-
// Check ancestors (all elements except the current one)
90-
for (let i = elementStack.length - 2; i >= 0; i--) {
91-
const node = elementStack[i];
92-
const ariaHidden = node.attributes?.find((a) => a.name === 'aria-hidden');
93-
if (ariaHidden?.value?.type === 'GlimmerTextNode' && ariaHidden.value.chars === 'true') {
94-
return true;
95-
}
96-
}
97-
return false;
92+
function hasAriaHiddenTrue(node) {
93+
const attr = node.attributes?.find((a) => a.name === 'aria-hidden');
94+
return attr?.value?.type === 'GlimmerTextNode' && attr.value.chars === 'true';
9895
}
9996

10097
/**
101-
* Get the role of the nearest non-transparent ancestor element.
102-
* Transparent elements are those with role="presentation"/"none" or named blocks (tag starts with ':').
103-
* Returns:
104-
* - a role string if a non-transparent ancestor with a role is found
105-
* - null if a non-transparent ancestor WITHOUT a role is found (breaks context)
106-
* - undefined if no non-transparent ancestor exists (root level)
98+
* Walk up the ancestor chain through transparent wrappers (named-block slots,
99+
* `<template>`, role="presentation"/"none") checking `aria-hidden` at each
100+
* layer. Returns { ariaHidden, role } where:
101+
* - `ariaHidden` is true if aria-hidden="true" was seen on any traversed
102+
* element (including transparent wrappers) up to and including the first
103+
* non-transparent parent — matches upstream's semantics.
104+
* - `role` is the role of the first non-transparent parent: a role string,
105+
* null (element with no role), or undefined (no non-transparent parent).
107106
*/
108-
function getAccessibleParentRole(elementStack) {
107+
function getParentContext(elementStack) {
109108
for (let i = elementStack.length - 2; i >= 0; i--) {
110109
const node = elementStack[i];
111-
112-
// Named blocks (e.g. <:content>) and <template> wrapper are transparent
110+
if (hasAriaHiddenTrue(node)) {
111+
return { ariaHidden: true, role: undefined };
112+
}
113+
// Named blocks (`<:content>`) and the `<template>` wrapper are transparent
113114
if (node.tag && (node.tag.startsWith(':') || node.tag === 'template')) {
114115
continue;
115116
}
116-
117117
const role = getRoleFromNode(node);
118-
119-
// Presentation/none roles are transparent in the accessibility tree
118+
// presentation/none roles are transparent in the accessibility tree
120119
if (role === 'presentation' || role === 'none') {
121120
continue;
122121
}
123-
124-
return role; // could be null (element with no role) or a role string
122+
return { ariaHidden: false, role };
125123
}
126-
return undefined; // no non-transparent ancestor found
124+
return { ariaHidden: false, role: undefined };
127125
}

tests/lib/rules/template-require-context-role.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ ruleTester.run('template-require-context-role', rule, {
8989
{
9090
message:
9191
'Role "listitem" must be contained in an element with one of these roles: group, list',
92-
type: 'GlimmerElementNode',
92+
type: 'GlimmerAttrNode',
9393
},
9494
],
9595
},
@@ -103,7 +103,7 @@ ruleTester.run('template-require-context-role', rule, {
103103
errors: [
104104
{
105105
message: 'Role "tab" must be contained in an element with one of these roles: tablist',
106-
type: 'GlimmerElementNode',
106+
type: 'GlimmerAttrNode',
107107
},
108108
],
109109
},
@@ -118,7 +118,7 @@ ruleTester.run('template-require-context-role', rule, {
118118
{
119119
message:
120120
'Role "menuitem" must be contained in an element with one of these roles: group, menu, menubar',
121-
type: 'GlimmerElementNode',
121+
type: 'GlimmerAttrNode',
122122
},
123123
],
124124
},
@@ -275,6 +275,18 @@ ruleTester.run('template-require-context-role', rule, {
275275
},
276276
],
277277
},
278+
{
279+
// aria-hidden on a non-immediate ancestor must NOT suppress the rule
280+
// (upstream only honors aria-hidden on the immediate parent)
281+
code: '<template><div aria-hidden="true"><div><div role="listitem">Item</div></div></div></template>',
282+
output: null,
283+
errors: [
284+
{
285+
message:
286+
'Role "listitem" must be contained in an element with one of these roles: group, list',
287+
},
288+
],
289+
},
278290
],
279291
});
280292

@@ -482,5 +494,16 @@ hbsRuleTester.run('template-require-context-role', rule, {
482494
},
483495
],
484496
},
497+
{
498+
// aria-hidden on a non-immediate ancestor must NOT suppress the rule
499+
code: '<div aria-hidden="true"><div><div role="listitem">Item</div></div></div>',
500+
output: null,
501+
errors: [
502+
{
503+
message:
504+
'Role "listitem" must be contained in an element with one of these roles: group, list',
505+
},
506+
],
507+
},
485508
],
486509
});

0 commit comments

Comments
 (0)