Skip to content

feat(checks/aria): support ARIA element internals properties - #5172

Merged
chutchins25 merged 6 commits into
developfrom
internals-checks-aria
Jun 18, 2026
Merged

feat(checks/aria): support ARIA element internals properties#5172
chutchins25 merged 6 commits into
developfrom
internals-checks-aria

Conversation

@straker

@straker straker commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

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

Here's a report of the full directory:

  • checks/aria/aria-busy - aria-busy → getAriaValue with { lowercase: true } (boolean type)
  • checks/aria/aria-conditional-checkbox-attr - aria-checked → getAriaValue with { lowercase: true } (normalizeAriaChecked calls toLowerCase)
  • checks/aria/aria-conditional-radio-attr - aria-checked → getAriaValue with { lowercase: true } (same reason as checkbox)
  • checks/aria/aria-conditional-row-attr - checks aria-rowindex/aria-colspan, but only via .attrNames presence check, no value access
  • checks/aria/aria-errormessage - aria-errormessage → getAriaValue + hasAriaValue; aria-invalid → getAriaValue + hasAriaValue; idrefs() → getResolvedRefs; since getResolvedRefs returns a VirtualNode, changed idref.getAttribute('aria-live') to idref.attr('aria-live'); aria-describedby → getAriaValue
  • checks/aria/aria-hidden-body - aria-hidden → getAriaValue (nmtoken type, no toLowerCase since original didn't use it)
  • checks/aria/aria-level - aria-level → getAriaValue (int type, no toLowerCase)
  • checks/aria/aria-required-attr - aria-valuetext → getAriaValue; aria-expanded → getAriaValue (in isClosedCombobox); required attr presence check → hasAriaValue (so internals-set required attrs are recognized)
  • checks/aria/aria-required-children - aria-busy → getAriaValue with { lowercase: true } (boolean type, two uses)
  • checks/aria/aria-valid-attr-value - preCheck functions for aria-haspopup, aria-controls, aria-expanded, aria-selected, aria-current, aria-owns, aria-describedby, aria-labelledby → getAriaValue; the main attrNames loop and const attrValue = virtualNode.attr(attrName) remain as-is since validation iterates over actual HTML attributes only
  • checks/aria/braille-label-equivalent - aria-braillelabel → getAriaValue (string type)
  • checks/aria/braille-roledescription-equivalent - aria-brailleroledescription and aria-roledescription → getAriaValue (both string type); defaulted roleDesc to null so typeof roleDesc !== 'string' check is preserved
  • checks/aria/abstractrole - checks role attribute, not an ARIA prop
  • checks/aria/aria-allowed-attr / aria-allowed-attr-elm / aria-prohibited-attr / aria-unsupported-attr / aria-valid-attr - iterate over attrNames, validate ARIA attribute names (not values)
  • checks/aria/aria-allowed-role - checks role attribute, not an ARIA prop
  • checks/aria/aria-required-parent - checks role hierarchy, no ARIA attr value access
  • checks/aria/aria-roledescription - checks aria-roledescription presence via attrNames, no value access
  • checks/aria/deprecatedrole / fallbackrole / invalidrole / unsupportedrole - check role attribute, not an ARIA prop
  • checks/aria/has-global-aria-attribute - checks for presence of any global ARIA attr via attrNames, no value access
  • checks/aria/has-widget-role - checks role, not an ARIA prop
  • checks/aria/is-element-focusable - checks focusability, no ARIA attr value access
  • checks/aria/no-implicit-explicit-label / valid-scrollable-semantics - no ARIA attr value access

Refs: #5142

@straker
straker requested a review from a team as a code owner June 15, 2026 21:59
Convert .attr()/.hasAttr()/idrefs() calls in lib/checks/aria/ to use
getAriaValue(), hasAriaValue(), and getResolvedRefs() so that checks
honour values set via the ElementInternals API in addition to HTML
attributes.
@straker
straker force-pushed the internals-checks-aria branch from 6fb6013 to 5c7365e Compare June 15, 2026 22:07
straker added 2 commits June 15, 2026 16:24
…gressions

- aria-required-attr: preserve empty/valueless HTML attr = missing behavior
  by only falling through to hasAriaValue when the attr is completely absent
  from HTML (null); also guard against non-ARIA option attrs that would
  cause hasAriaValue to throw
- aria-conditional-checkbox/radio-attr: revert getAriaValue since getAriaValue
  trims nmtoken values which breaks the untrimmed comparison logic in
  normalizeAriaChecked; standard input elements also cannot use ElementInternals
- aria-errormessage: remove elementInternals test since ariaErrorMessageElements
  is an IDL sequence property that cannot be set to a single element via
  testutils-element
straker added 3 commits June 16, 2026 09:34
- aria-required-attr: pass15 — required attr (aria-checked) via internals
  satisfies the required-attr check for role=switch
- aria-required-children: pass21 — aria-busy=true via internals satisfies
  the loading-state exception for a listbox with no option children
- aria-valid-attr-value: internals-pass — aria-expanded=false via internals
  causes the preCheck to skip aria-controls validation
Comment thread lib/checks/aria/aria-busy-evaluate.js Outdated
export default function ariaBusyEvaluate(node, options, virtualNode) {
return virtualNode.attr('aria-busy') === 'true';
return (
getAriaValue(virtualNode, 'aria-busy', { lowercase: true })?.value ===

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Tested aria-busy case insensitivty:

<div tabindex="0" role="list" aria-busy="true">
  <div role="listitem">hi</div>
</div>

<div tabindex="0" role="list" aria-busy="TRuE">
  <div role="listitem">hi</div>
</div>

JAWS/Chrome - did not report busy state
NDVA/Firefox - reported busy on first list, and didn't report busy on second list (so not case insensitive)
VoiceOver/Safari - both reported as busy

)?.value;
const hasAttr = hasAriaValue(virtualNode, 'aria-errormessage');
const invaid = getAriaValue(virtualNode, 'aria-invalid', {
lowercase: true

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Tested aria-invalid case insensitivity:

<ul>
  <li>
    <label for="name">Full Name</label>
    <input
      type="text"
      name="name"
      id="name"
      aria-required="true"
      aria-invalid="true" />
  </li>
  <li>
    <label for="email">Email Address</label>
    <input
      type="email"
      name="email"
      id="email"
      aria-required="true"
      aria-invalid="TRuE" />
  </li>
</ul>

JAWS/Chrome - both reported as invalid
NDVA/Firefox - both reported as invalid
VoiceOver/Safari - both reported as invalid

return role === 'combobox' && vNode.attr('aria-expanded') === 'false';
return (
role === 'combobox' &&
getAriaValue(vNode, 'aria-expanded', { lowercase: true })?.value === 'false'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Tested aria-expanded case insensitivity:

<button aria-expanded="true" aria-controls="widget1">Toggle widget</button>
<button aria-expanded="TRuE" aria-controls="widget1">Toggle widget</button>

JAWS/Chrome - both reported as expanded
NDVA/Firefox - both reported as expanded
VoiceOver/Safari - both reported as expanded

const hasPopup =
['false', null].includes(virtualNode.attr('aria-haspopup')) === false;
const hasPopup = !['false', null].includes(
getAriaValue(virtualNode, 'aria-haspopup', { lowercase: true })

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Tested aria-haspoup case insensitivity:

<button aria-haspopup="true">hello</button>
<button aria-haspopup="TRuE">hello</button>

<button aria-haspopup="dialog">hello</button>
<button aria-haspopup="DIALog">hello</button>

JAWS/Chrome - first set reported as menu pop up, second set reported as dialog pop up
NDVA/Firefox - first set reported as menu pop up, second set reported as dialog pop up
VoiceOver/Safari - first set reported as menu pop up, second set reported as dialog pop up

getAriaValue(virtualNode, 'aria-expanded', {
lowercase: true
})?.value !== 'false' &&
getAriaValue(virtualNode, 'aria-selected', {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Tested aria-selected case insensitivity:

<div role="tablist" aria-label="Sample Tabs">
  <span
    role="tab"
    aria-selected="true"
    aria-controls="panel-1"
    id="tab-1"
    tabindex="0">
    First Tab
  </span>
  <span
    role="tab"
    aria-selected="TRue"
    aria-controls="panel-1"
    id="tab-1"
    tabindex="0">
    First Tab
  </span>
</div>

JAWS/Chrome - both reported as selected
NDVA/Firefox - both reported as selected
VoiceOver/Safari - both reported as selected

*/
export default function ariaBusyEvaluate(node, options, virtualNode) {
return virtualNode.attr('aria-busy') === 'true';
return getAriaValue(virtualNode, 'aria-busy')?.value === 'true';

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Tested aria-invalid case insensitivity:

<div tabindex="0" role="list" aria-busy="true">
  <div role="listitem">hi</div>
</div>

<div tabindex="0" role="list" aria-busy="TRuE">
  <div role="listitem">hi</div>
</div>

JAWS/Chrome - did not report busy
NDVA/Firefox - first list reported busy, second list did not
VoiceOver/Safari - both reported as busy

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.

Can we put in a comment?

requiredAttr =>
!virtualNode.attr(requiredAttr) && !hasImplicitAttr(elmSpec, requiredAttr)
!getAriaValue(virtualNode, requiredAttr)?.value &&
!hasImplicitAttr(elmSpec, requiredAttr)

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.

Separate PR if necessary, but should this be part of getAriaValue? I can't think of where it makes a difference for any of the rules today, but things like a visible popover do set aria-expanded=true as an implicit property, which does impact a few rules when we end up implementing it.

@chutchins25
chutchins25 merged commit 9b7f754 into develop Jun 18, 2026
23 checks passed
@chutchins25
chutchins25 deleted the internals-checks-aria branch June 18, 2026 17:32
chutchins25 added a commit that referenced this pull request Jul 15, 2026
…nflict resolution (#5175)

Audited `lib/checks/shared` for the #5044 conversion — no
`getAriaValue`/`hasAriaValue` conversion applies:

- `aria-label` / `aria-labelledby` checks delegate to the
already-converted `commons/aria` helpers (`arialabelText` /
`arialabelledbyText`), so they're internals-aware transitively.
- `presentational-role` uses one global-ARIA **presence** check for role
conflict resolution, which stays `hasAttr` — element internals must
**not** trigger conflict resolution (no browser supports it yet; see
#5162). This matches the convention already set in #5172
(`has-global-aria-attribute`) and #5171 (`get-role`).

So this is a **test-only** change: it adds a test demonstrating that a
global ARIA attribute supplied via element internals does **not**
override a presentational role. No production code changes.

Closes #5145
WilcoFiers added a commit that referenced this pull request Aug 5, 2026
All notable changes to this project will be documented in this file. See
[commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version)
for commit guidelines.

##
[4.13.0](v4.12.1...v4.13.0)
(2026-08-05)

### Features

- **aria-actions:** add aria-actions to allowed ARIA attributes
([#5200](#5200))
([029655d](029655d)),
closes [#4584](#4584)
[#5199](#5199), references
[#5215](#5215)
[#5215](#5215)
- **aria-allowed-attr:** flag deprecated ARIA attributes as needs-review
([#5246](#5246))
([518f3cc](518f3cc)),
closes [#3341](#3341)
- **aria-prohibited-attr:** allow many elements to be named and disallow
label and body from being named
([#5259](#5259))
([d8b1ea5](d8b1ea5))
- **aria-roles:** add sectionheader and sectionfooter roles
([#5238](#5238))
([c36c109](c36c109)),
closes [#4734](#4734),
references [#4734](#4734)
- **aria/get-aria-value:** new function to get aria values of a node
([#5109](#5109))
([a7d8f3e](a7d8f3e)),
references [#5042](#5042)
- **aria/has-attr-value:** new function to check if node has aria value
([#5136](#5136))
([61f2624](61f2624)),
references [#5109](#5109)
- **aria:** support role=image as equivalent to role=img
([#5248](#5248))
([5aa8aaf](5aa8aaf)),
closes [#4656](#4656),
references [#5272](#5272)
- **checks/aria:** support ARIA element internals properties
([#5172](#5172))
([9b7f754](9b7f754))
- **checks/label:** support ARIA element internals properties
([#5170](#5170))
([21c5f8b](21c5f8b))
- **checks/navigation:** support ARIA element internals properties
([#5167](#5167))
([2c3a98f](2c3a98f))
- **commons/aria:** support ARIA element internals properties
([#5171](#5171))
([31f09e7](31f09e7))
- **commons/dom:** support ARIA element internals properties
([#5163](#5163))
([f0a12cf](f0a12cf))
- **commons/forms:** support ARIA element internals properties
([#5165](#5165))
([27a4686](27a4686))
- **commons/matches/fromPrimative:** deprecate in favor of correct
spelling ([#5270](#5270))
([31cfb2e](31cfb2e))
- **commons/text:** support ARIA element internals properties
([#5169](#5169))
([e841a33](e841a33))
- **commons/text:** support form-associated labels via element internals
([#5182](#5182))
([57cfe0a](57cfe0a)),
closes [#5045](#5045),
references [#5170](#5170)
[#5039](#5039)
[#5151](#5151)
[#5039](#5039)
- **dom/getResolvedRefs:** new function to get the resolved virtual
nodes of idrefs
([#5151](#5151))
([489cdea](489cdea)),
references [#5109](#5109)
- **element-internals:** enable ElementInternals by default
([#5284](#5284))
([2740d42](2740d42)),
closes [#5277](#5277)
- **i18n:** Add Swedish locale
([#5190](#5190))
([dcd13f2](dcd13f2)),
references [#5189](#5189)
- **matches:** add inSectioningContent, hasChild, and
isSummaryForDetails matches
([#5262](#5262))
([c47cdcd](c47cdcd))
- **rules:** support ARIA element internals properties
([#5168](#5168))
([065baf7](065baf7))
- **standards/ariaAttrs:** add caseInsensitive property for attributes
([#5224](#5224))
([bcd791c](bcd791c))

### Bug Fixes

- **aria-allowed-role:** allow roles on a non-details summary
([#5242](#5242))
([3bd9875](3bd9875)),
closes [#3911](#3911),
references [#3443](#3443)
[#3911](#3911)
- **aria-allowed-role:** restrict figure roles with child figcaption
([#5240](#5240))
([178a635](178a635)),
closes [#3443](#3443)
- **aria-prohibited-attr:** visible aria-labelledby requires review only
([#5285](#5285))
([fd6fa9f](fd6fa9f))
- **axe.d.ts:** make enabled property of RuleMetadata optional
([#5129](#5129))
([90fce18](90fce18))
- **color-contrast:** fix various stacking context bugs
([#5214](#5214))
([d5e5b04](d5e5b04)),
references [#8](#8)
[#5213](#5213)
- **gather-internals:** handle non-HTMLElement nodes
([#5161](#5161))
([06e84c3](06e84c3))
- **get-selector:** escape control characters in attribute selectors
([#5273](#5273))
([4b60ac5](4b60ac5)),
closes [#5204](#5204)
[#5204](#5204)
- **image-alt:** allow whitespace alt on presentational images
([#5218](#5218))
([c5dd0ef](c5dd0ef)),
closes [#5216](#5216)
- **landmark-unique:** exclude section/form with non-landmark roles from
landmark match
([#5085](#5085))
([c5fd013](c5fd013)),
closes [#4722](#4722)
[#5064](#5064)
- name the image role in role-img-alt and svg-img-alt metadata
([#5279](#5279))
([995a269](995a269)),
closes [#5272](#5272),
references [#5248](#5248)
[#5248](#5248)
- **standards:** update aria-errormessage and aria-details to be idrefs
([#5157](#5157))
([fb94f8a](fb94f8a))

This PR was opened by a robot 🤖 🎉
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.

3 participants