Skip to content

feat(commons/text): support form-associated labels via element internals#5182

Draft
chutchins25 wants to merge 1 commit into
developfrom
chut/5045-form-associated-labels
Draft

feat(commons/text): support form-associated labels via element internals#5182
chutchins25 wants to merge 1 commit into
developfrom
chut/5045-form-associated-labels

Conversation

@chutchins25

Copy link
Copy Markdown
Contributor

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.

Changes

  • native-text-alternative.jsfindTextMethods detects a form-associated custom element via ElementInternals and appends labelText.
  • label-text.jslabelText 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

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

Detect form-associated custom elements in nativeTextAlternative and add
labelText, and resolve labels from internals.labels in labelText. Both are
gated by the elementInternals run option.

Closes issue #5045
@chutchins25 chutchins25 force-pushed the chut/5045-form-associated-labels branch from 74d3009 to b0fbb14 Compare June 18, 2026 18:16

Copilot AI 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.

Pull request overview

This PR updates axe-core’s commons/text accessible-name computation to recognize native <label> associations for form-associated custom elements by leveraging ElementInternals.labels (when available and enabled via the existing elementInternals feature flag).

Changes:

  • Extend nativeTextAlternative to append labelText for form-associated custom elements detected via ElementInternals.
  • Update labelText to prefer internals.labels (authoritative explicit + implicit labels) and fall back to DOM-based lookup otherwise.
  • Add/expand test utilities and unit tests covering explicit/implicit/multiple labels, deduping, global map internals, and Shadow DOM scenarios.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
lib/commons/text/native-text-alternative.js Detects form-associated custom elements via internals and conditionally adds labelText as a naming method.
lib/commons/text/label-text.js Uses ElementInternals.labels (guarded) to compute label text, falling back to existing DOM resolution.
test/testutils.js Introduces testutils-form-element (form-associated) for label-related tests.
test/commons/text/native-text-alternative.js Adds unit tests validating label pickup + feature-flag gating for form-associated custom elements.
test/commons/text/label-text.js Adds unit tests for internals-based label resolution (including global map + Shadow DOM).
test/commons/aria/get-role.js Adds regression tests clarifying presentational role inheritance behavior with ElementInternals roles.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +47 to +56
const internals = virtualNode.elementInternals;
if (internals && !methods.includes('labelText')) {
try {
// `labels` throws on custom elements that aren't form-associated
internals.labels;
textMethods.push(nativeTextMethods.labelText);
} catch {
// not a form-associated custom element
}
}
Comment on lines +155 to +166
const { actualNode } = target;
const internals = actualNode._internals;
// remove the property protocol so the global map is the only source.
// elementInternals is read lazily, so the first read happens below
delete actualNode._internals;
globalThis._elementInternals = new WeakMap();
globalThis._elementInternals.set(actualNode, internals);

assert.equal(labelText(target), 'My explicit label');

delete globalThis._elementInternals;
});
Comment on lines +73 to +81
it('does not add a label when element internals are disabled', () => {
axe._enableElementInternals = false;
const vNode = queryFixture(`
<label for="target">My explicit label</label>
<testutils-form-element id="target"></testutils-form-element>
`);
assert.equal(nativeTextAlternative(vNode), '');
axe._enableElementInternals = true;
});
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: Update the explicitLabel functions to support form associated labels

2 participants