fix(agent): name a form control from the label that names it - #55
Merged
Merged
Conversation
A semantic name came from `aria-label`, `alt` and `title` and from nothing else, so the ordinary way to label a field -- a `<label for>` beside it, or a `<label>` wrapped around it -- produced no name at all. Every text input on every page in the fleet arrived in the tree anonymous. That is not only a reporting defect. A harness addresses a control by name, so an anonymous field cannot be typed into, and a check that means "enter a URL and save" cannot be written at all. Measured on support.cafe's connection settings: three fields, each with a correct `<Label for>` beside it, all three reported as `textbox ""`. Measured against a probe page as well, where a `for` association, a wrapping label and a placeholder all produced nothing and only `aria-label` worked. Resolution order follows a browser's: `aria-label` first, because that is the author overriding the visible text on purpose; then the label, `for` before containment; then `alt` and `title`; then the text content of the roles that are named by their own content; and a placeholder last, which is the only thing naming a great many search fields and must never displace a real label. The index is built once per snapshot. The question a control asks is "which label points at me", and answering that from the control means scanning the document for every named node. It is built over the whole document even for a rooted inspection, because a label is frequently a sibling of the control rather than a descendant of the node the caller rooted at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`alert` and `status` are the roles an application uses to report that something happened -- a refusal, a saved confirmation -- and what they report is their content. They arrived anonymous, so "the reason is shown" was not a question a check could ask, and every validation outcome had to be approximated by something else that moved at the same time. Found writing checks for support.cafe's connection settings, where the refusal and the success message are both bare paragraphs. Marking them as live regions is the accessibility fix on the site's side; naming them is this side of it. Deliberately not `generic`. A wrapper's text content is its whole subtree, so naming containers would give every one of them a name made of the entire page, and any name-matching selector would then match everything. The test pins that half too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A version bump on master is the release trigger, and this is what carries the naming fixes to the harness that needs them: without them every form field in the fleet arrives anonymous and cannot be addressed by name at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`textContent` is the DOM property and includes every text node in the
subtree, `<style>` and `<script>` among them. An accessible name does
not, because those elements are not rendered.
Measured on honey.id, whose header logo is an anchor wrapping an inline
SVG with a `<style>` in it. The site's home link arrived named
".animated-logo path { fill-opacity: 0; stroke: currentColor; ... }",
which is unusable to a person reading the page and unaddressable to a
check trying to press it. Any site whose logo is an inline SVG with
scoped styles has the same link.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An alert stacking two lines arrived named "This page could not loadWebSocket connection failed", because every text node under the element was concatenated with nothing between it and the next. A name that reads as one run-on word is one a person cannot read and a check cannot address. accname separates each descendant's contribution with a space unless the descendant is inline, which is why two adjacent spans are still one word. The walk now asks the resolved style which of the two a child is, and treats an element with no style as inline so a name never gains a space the page does not draw. Measured on crates.vip's failure alert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`inline-block` and `inline-flex` establish their own box, so a browser separates them from their siblings the way it separates a block. The first pass asked only whether the display was outside-inline, which put an inline-block back in the same word as its neighbour. Checked against `dom-accessibility-api`, which reaches the same result by comparing the computed display against the string "inline", and against the pair of names crates.vip's navbar and its failure alert produce. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The "named by their own content" list had `option` but not its menu, tab and tree equivalents, and `semantic_role` returns the `role` attribute verbatim. So `<button role="menuitem">Platform Admin</button>` arrived with an empty name: a screen reader announces nothing, and no check can name the option it means to press. Writing the role is the author opting out of the native naming this list exists to provide, which is exactly when it is needed. Reduced from honey.id, where the role selector on the platform users table offered two anonymous entries and promoting a user could not be driven or described. It is not specific to that dropdown: every `Dropdown.Item` in @pathscale/ui carries `role="menuitem"`, so the whole fleet was affected. Confirmed against a minimal fixture, where a plain `<button>` and an `<a>` were named and all three of `<button role="menuitem">`, the same wrapped in a span, and `<div role="menuitem">` were not. All three are named now. `cell` and `row` stay off the list. Their content is a whole subtree, which is the objection the comment above already raises against `generic`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`name_for` returned the first label it found, empty or not, and stopped. Labels nested inside labels are invalid markup but they happen: a checkbox component that draws its own `<label>` around a styled box, wrapped again by the page to put the text beside it. The inner label's content is the box, so it is empty, and returning that emptiness made the control anonymous while its name sat one level further out. Skipping an empty label costs nothing when the markup is well formed, because a real label has text. The same guard on the `for` lookup, for the same reason. Found in honey.id, where the sign-up form's accept-terms checkbox came back with no name and nothing announced what was being accepted. That page is fixed at source too -- the text belongs to the checkbox -- but a control should not go anonymous because the markup around it is careless. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A semantic name came from
aria-label,altandtitleand from nothing else. The ordinary way to label a form control — a<label for>beside it, or a<label>wrapped around it — produced no name at all, so every text input on every page in the fleet arrived in the tree anonymous.That is not only a reporting defect. A harness addresses a control by name, so an anonymous field cannot be typed into and a check that means "enter a URL and save" cannot be written at all.
Measured
A probe page through the headless host, before:
<label for=a>Endpoint URL</label><input id=a>""<label>Wrapping <input></label>""<input aria-label="Direct aria">Direct aria<input placeholder="Only a placeholder">""And on support.cafe's connection settings: three
Input.Fields, each with a correct<Label for>beside it, all three reported astextbox "".Resolution order
A browser's:
aria-labelfirst, because that is the author overriding the visible text on purpose; then the label,forbefore containment; thenaltandtitle; then the text content of the roles named by their own content; and a placeholder last, which is the only thing naming a great many search fields and must never displace a real label.Cost
The index is built once per snapshot. The question a control asks is "which label points at me", and answering that from the control means scanning the document for every named node. It is built over the whole document even for a rooted inspection, because a label is frequently a sibling of the control rather than a descendant of the node the caller rooted at.
a_field_is_named_by_its_labelpins all five cases, including the two that must not change:aria-labelstill wins over a label, and a control nothing names still has no name.🤖 Generated with Claude Code