From 4196ef08b8cc6e45b6402a10cdf9f958d0fe4029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Mon, 13 Apr 2026 14:37:58 +0200 Subject: [PATCH] Fix template-require-context-role: align aria-hidden scope and report loc with upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Limit aria-hidden suppression to the immediate parent (port previously walked the full ancestor stack — over-permissive). Report on the role attribute node, not the element, to match upstream. --- lib/rules/template-require-context-role.js | 60 +++++++++---------- .../rules/template-require-context-role.js | 29 ++++++++- 2 files changed, 55 insertions(+), 34 deletions(-) diff --git a/lib/rules/template-require-context-role.js b/lib/rules/template-require-context-role.js index abc78f9e2a..ea7aba85f4 100644 --- a/lib/rules/template-require-context-role.js +++ b/lib/rules/template-require-context-role.js @@ -49,13 +49,20 @@ module.exports = { if (role && ROLES_REQUIRING_CONTEXT[role]) { // Skip check if at root level (no parent elements — context may be external) - if (elementStack.length > 1 && !isInsideAriaHidden(elementStack)) { - const parentRole = getAccessibleParentRole(elementStack); + if (elementStack.length > 1) { + const parentContext = getParentContext(elementStack); + if (parentContext.ariaHidden) { + // aria-hidden on the effective parent (or a transparent wrapper + // walked through on the way up) — upstream suppresses the rule. + return; + } + const parentRole = parentContext.role; if (parentRole === undefined) { // No non-transparent parent found (effectively root) — skip } else if (!parentRole || !ROLES_REQUIRING_CONTEXT[role].includes(parentRole)) { + const roleAttr = node.attributes?.find((a) => a.name === 'role'); context.report({ - node, + node: roleAttr || node, messageId: 'missingContext', data: { role, @@ -82,46 +89,37 @@ function getRoleFromNode(node) { return null; } -/** - * Check if any ancestor element in the stack has aria-hidden="true". - */ -function isInsideAriaHidden(elementStack) { - // Check ancestors (all elements except the current one) - for (let i = elementStack.length - 2; i >= 0; i--) { - const node = elementStack[i]; - const ariaHidden = node.attributes?.find((a) => a.name === 'aria-hidden'); - if (ariaHidden?.value?.type === 'GlimmerTextNode' && ariaHidden.value.chars === 'true') { - return true; - } - } - return false; +function hasAriaHiddenTrue(node) { + const attr = node.attributes?.find((a) => a.name === 'aria-hidden'); + return attr?.value?.type === 'GlimmerTextNode' && attr.value.chars === 'true'; } /** - * Get the role of the nearest non-transparent ancestor element. - * Transparent elements are those with role="presentation"/"none" or named blocks (tag starts with ':'). - * Returns: - * - a role string if a non-transparent ancestor with a role is found - * - null if a non-transparent ancestor WITHOUT a role is found (breaks context) - * - undefined if no non-transparent ancestor exists (root level) + * Walk up the ancestor chain through transparent wrappers (named-block slots, + * `