Skip to content

feat(dom/getResolvedRefs): new function to get the resolved virtual nodes of idrefs - #5151

Merged
straker merged 11 commits into
developfrom
elm-internals-get-aria-refs
Jun 12, 2026
Merged

feat(dom/getResolvedRefs): new function to get the resolved virtual nodes of idrefs#5151
straker merged 11 commits into
developfrom
elm-internals-get-aria-refs

Conversation

@straker

@straker straker commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

A new function that is very similar to commons.dom.idrefs function, but understands handling ARIA idrefs properties and element internals properties and additionally returns virtual nodes rather than DOM nodes. This was the leftover work from #5109 to handle idrefs being a string (attribute) or array of nodes (property).

I also update the queryShadowFixture function to allow both #shadow and #target ids to exist in the light DOM. I looked at all uses of queryShadowFixture and almost all of them use the #shadow in the light DOM and #target in the shadow DOM, though a few rely on #target in the light DOM (but no #shadow). The changes prioritize the order of find things so that #shadow is used first as the container, then #target (instead of the other way around) and the target selector is found in the shadow DOM first, then in the light DOM after (instead of the other way around). Lastly to support changing either selector I allowed the 3rd parameter to be an object that could define them (no current uses of queryShadowFixture pass a 3rd parameter, but I left the option to just pass the target selector as a string).

@straker
straker requested a review from a team as a code owner June 9, 2026 17:39

@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 addition, the shadow-DOM scoping tests are a good touch, and the content instanceof NodeshadowContent instanceof Node typo fix is a nice catch along the way. Two suggestions below, both bugs with simple fixes: one cross-browser correctness issue in the lib, one in the new queryShadowFixture object-param path.

Comment thread lib/commons/dom/get-resolved-refs.js Outdated
Comment thread test/testutils.js Outdated

@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.

Some comments. Think we need to at least consider deduplication

Comment thread lib/commons/dom/get-resolved-refs.js Outdated
Comment on lines +62 to +66

const attrValue = vNode.attr(attr);
if (attrValue !== null) {
return attrValue;
}

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.

Should this be in an else block? Is there ever a case where we want to run this if we checked the prop?

Suggested change
const attrValue = vNode.attr(attr);
if (attrValue !== null) {
return attrValue;
}
else {
const attrValue = vNode.attr(attr);
if (attrValue !== null) {
return attrValue;
}
}

@straker straker Jun 10, 2026

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.

Maybe? With the current code, if we check the prop and it's null we fallback to the attribute, which we would assume would also be null. Is there ever a chance that it wouldn't be null if the prop is?

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.

I just remembered that we still support IE11 so we have to fallback to the attribute case as the prop won't exist.


it('should find referenced nodes by ID', () => {
const vNode = queryFixture(html`
<div aria-cats="target1 target2" id="target"></div>

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.

If ARIA WG ever decides aria-cats needs to be an ARIA property we have a problem 🤣

Comment thread lib/commons/dom/get-resolved-refs.js Outdated
Comment thread test/testutils.js
Comment on lines +221 to +225
testUtils.queryShadowFixture = (
content,
shadowContent,
targetSelector = '#target'
) => {

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.

If we're going to tweak this I prefer we start using declarative shadow DOM. I did that in advanced rules. It requires a bit of setup, but is much easier to read than what we've done here with queryShadowFixture.

Not a blocker.

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.

Created #5158

@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.

Re-review — the earlier queryShadowFixture order-of-operations and instanceof issues look fixed, and the !== undefined guard is correct (needed for IE11). Two things left: a question on the self-reference filtering (replied on that thread), and the null-vs-undefined return inconsistency flagged inline.

Comment thread lib/commons/dom/get-resolved-refs.js Outdated

@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.

Everything seems resolved and taken care of now. Thanks for that. Looks good to me. Leaving Security Review for Wilco when he applies his review.

@straker
straker merged commit 489cdea into develop Jun 12, 2026
23 checks passed
@straker
straker deleted the elm-internals-get-aria-refs branch June 12, 2026 13:24
straker pushed a commit that referenced this pull request Jul 20, 2026
…als (#5182)

## Summary

Form-associated custom elements (`static formAssociated = true` +
`attachInternals()`) can be labeled with native `<label>` elements,
which the browser exposes through `ElementInternals.labels`. axe-core's
accessible-name computation dropped these labels entirely, because:

1. `nativeTextAlternative` builds its naming methods from the element
spec, and `getElementSpec()` returns `{}` for custom element names — so
`labelText` was never invoked for a custom element.
2. Label resolution in `labelText` only looked at the DOM by `id`
(`label[for]`) / ancestor wrapping.

This wires up the [proposal's final
step](https://github.com/dequelabs/ocarina-team/blob/main/proposals/axe-core/element-internals.md#supporting-formassociated-elements-with-labels).

## Changes

- **`native-text-alternative.js`** — `findTextMethods` detects a
form-associated custom element via `ElementInternals` and appends
`labelText`.
- **`label-text.js`** — `labelText` reads `internals.labels` directly
when present (the authoritative, complete set of explicit + implicit
labels), falling back to the existing DOM resolution otherwise.

`ElementInternals.labels` throws `NotSupportedError` on a custom element
that isn't form-associated, so both reads are guarded with try/catch.
Both paths are gated by the existing `elementInternals` run option —
with the flag off, `vNode.elementInternals` is `undefined` and behavior
is unchanged.

### Why `internals.labels` (not the DOM lookup)?

It's the browser-authoritative association and is the *complete* set —
it already includes the ancestor-wrapping label. Routing it through the
existing explicit + implicit merge would count a wrapping label twice,
so `labelText` uses the internals list exclusively when present.

## Out of scope

- **`checks/label` (`explicit`/`implicit`)** — no change needed. They
resolve labels via `label[for=id]` / `closest('label')`, which already
cover custom elements, and the `label` rule selector is `input,
textarea` anyway. Matches the proposal ("rules that look at
implicit/explicit labels will not need to be updated") and #5170 leaving
those checks untouched.
- **Non-labelable custom elements with a stray `<label for>`** —
labelable-ness/role territory (#5039), pre-existing.
- `getResolvedRefs` (#5151) — native `<label>`/`for` association isn't
an ARIA idref, so it doesn't apply here.

## Testing

- `test/testutils.js` — new shared `testutils-form-element`
(form-associated).
- `label-text.js` — explicit, implicit, multiple, implicit+explicit
dedup, global-map protocol, non-form-associated fall-through, Shadow
DOM.
- `native-text-alternative.js` — explicit/implicit pickup, gating
(non-form-associated gets no label), flag-off no-op.

No integration test added — this is a `commons/text` change with no
rule/check edits; the nearest rule impact (`aria-input-field-name` on a
role-bearing custom element) depends on role-via-internals selection
(#5039), which isn't wired yet.

Closes #5045
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