Skip to content

feat(checks/label): support ARIA element internals properties#5170

Open
straker wants to merge 5 commits into
developfrom
internals-checks-label
Open

feat(checks/label): support ARIA element internals properties#5170
straker wants to merge 5 commits into
developfrom
internals-checks-label

Conversation

@straker

@straker straker commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Updates the checks/label directory to use the new getAriaValue, hasAriaValue, and getResolvedRefs functions where it makes sense.

Here's a report of the full directory:

  • checks/label/help-same-as-label - aria-describedby: replaced node.getAttribute + idrefs() with hasAriaValue + getResolvedRefs; since getResolvedRefs returns VirtualNodes, updated accessibleText(thing) to accessibleText(thing.actualNode)
  • checks/label/multiple-label - aria-labelledby: replaced idrefs() with getResolvedRefs; since VirtualNodes are returned, updated .includes(domNode) to .some(ref => ref?.actualNode === domNode)
  • checks/label/title-only - aria-describedby: replaced .attr() truthy check with hasAriaValue since only presence is needed
  • checks/label/alt-space-value - checks alt attribute, not an ARIA prop
  • checks/label/duplicate-img-label - compares accessible names, no direct ARIA attr access
  • checks/label/explicit - checks label[for] association, no ARIA attr access
  • checks/label/hidden-explicit-label - checks label visibility, no ARIA attr access
  • checks/label/implicit - checks implicit label wrapping, no ARIA attr access
  • checks/label/label-content-name-mismatch - computes accessible names, no direct ARIA attr access
  • checks/label/no-implicit-explicit-label - no direct ARIA attr access (in aria directory, not label)

Closes: #5146

Convert aria-describedby and aria-labelledby attribute reads in
title-only, help-same-as-label, and multiple-label checks to use
getResolvedRefs, hasAriaValue, and getAriaValue so that values set
via ElementInternals are also detected.
@straker straker requested a review from a team as a code owner June 15, 2026 21:58

@chutchins25 chutchins25 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work — clean migration to the element-internals helpers, and the new testutils-element tests cover the internals path for each check. Two low-severity questions below, neither blocking.

Comment thread lib/checks/label/title-only-evaluate.js Outdated
const labelText = labelVirtual(virtualNode);
const title = titleText(virtualNode);
const ariaDescribedBy = virtualNode.attr('aria-describedby');
const ariaDescribedBy = hasAriaValue(virtualNode, 'aria-describedby');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker, just a consistency question — hasAriaValue is presence-based (it returns true for an empty aria-describedby=""), whereas the previous .attr('aria-describedby') truthy check treated an empty attribute as absent. So an element with an empty aria-describedby and no title/label would now flip title-only from false to true. Such an element has no accessible name anyway, so the practical impact is negligible — was the presence-based semantics (vs. value-truthy) intended here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. This isn't right.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved — switched to getAriaValue and check .value, so an empty aria-describedby is treated as absent again.

// make sure the ONE AT visible label is in the list of idRefs of aria-labelledby
const labelledby = idrefs(node, 'aria-labelledby');
return !labelledby.includes(ATVisibleLabels[0]) ? undefined : false;
const vNode = getNodeFromTree(node);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker, just a simplification — getResolvedRefs accepts a raw DOM node and calls nodeLookup internally, so you can pass node directly and drop the getNodeFromTree import/call (const labelledby = getResolvedRefs(node, 'aria-labelledby')). Up to you if the intermediate vNode reads more clearly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved — passing node directly to getResolvedRefs and dropped the getNodeFromTree call.

@WilcoFiers WilcoFiers left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See Chris's comment

…a-describedby

Use getAriaValue instead of hasAriaValue so an empty aria-describedby is
treated as absent, and simplify multiple-label to pass the DOM node
directly to getResolvedRefs.
Add coverage for the reflected ariaLabelledByElements property overriding
aria-labelledby in the multiple-label check. getResolvedRefs already honors
the property; this regression-tests the #4943 scenario. Tests adapted from
#5187.

Co-authored-by: JC Franco <jfranco@esri.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ElementInternals: convert lib/checks/label + lib/checks/forms to getAriaValue/hasAriaValue

3 participants