Skip to content

feat(a11y): WalkA11y — every accessible element WITH its position - #177

Merged
tannevaled merged 1 commit into
mainfrom
feat/a11y-positioned-walk
Aug 10, 2026
Merged

feat(a11y): WalkA11y — every accessible element WITH its position#177
tannevaled merged 1 commit into
mainfrom
feat/a11y-positioned-walk

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

CollectA11y answers what a tree contains. Nothing answered where each
thing is — and that is the other half every platform accessibility API asks for.
A screen reader draws its focus ring, routes a touch and moves the pointer from a
rectangle, so an element described without one can be read but never pointed at.

type A11yNode struct {
    A11yInfo
    Rect Rect
}

func WalkA11y(w Widget) []A11yNode

WalkA11y descends the widget tree through the same childContainer convention
CollectRuns already uses, so a host no longer has to keep a flat list of
everything it composed — which CollectA11y requires and a nested layout makes
impractical. RolePresentation widgets are skipped exactly as CollectA11y
skips them, but the walk still descends into them: a box contributes nothing
to announce while its children usually do.

The part worth reviewing

The node carries Bounds() verbatim rather than accumulating ancestors'
offsets, because bounds in this toolkit are already absolute. translateEvent
converts a parent-local event to child-local with
ev.X + parentRect.X - childRect.X, which only holds when both rectangles share
the surface origin.

Accumulating — the obvious reading of "placement within its parent surface" in
the Widget.Bounds doc — would place every nested element at roughly twice its
true distance from the origin: plausible-looking, and pointing a screen reader at
the wrong part of the window. TestWalkA11yBoundsAreNotAccumulated locks that
down rather than leaving it to a comment.

Nothing else is filtered. Whether an unnamed or zero-area element is worth
publishing belongs to the platform bridge, which knows what its own screen reader
does with one.

Why now

This is the primitive the NSAccessibility, AT-SPI and UI Automation bridges need
in order to move into go-widgets/window. They exist and work today — each
verified with its platform's own client — but they live in
go-news-reader/reader/internal/window, a duplicate of this org's windowing
library. Upstreamed onto WalkA11y, they make every go-widgets application
accessible without the application doing anything, because window.Run already
receives the widget tree.

Verification

gofmt, go vet, full package tests green; WalkA11y at 100% statement
coverage and the whole-package 100% gate still passes.

🤖 Generated with Claude Code

CollectA11y answers what a tree contains. Nothing answered where each
thing is, and that is the other half every platform accessibility API
asks for: a screen reader draws its focus ring, routes a touch and moves
the pointer from a rectangle, so an element described without one can be
read but never pointed at.

WalkA11y descends the widget tree through the same childContainer
convention CollectRuns uses, so a host no longer has to keep a flat list
of everything it composed — which CollectA11y requires and a nested
layout makes impractical. RolePresentation widgets are skipped exactly as
CollectA11y skips them, but the walk still descends INTO them, because a
box contributes nothing to announce while its children usually do.

The node carries Bounds verbatim rather than accumulating ancestors'
offsets. Bounds in this toolkit are already absolute: translateEvent
converts a parent-local event to child-local with `ev.X + parentRect.X -
childRect.X`, which only holds when both rectangles share the surface
origin. Accumulating — the obvious reading of "placement within its
parent surface" — would place every nested element at roughly twice its
true distance from the origin, plausible-looking and wrong. A test locks
that down rather than a comment alone.

Nothing else is filtered. Whether an unnamed or zero-area element is
worth publishing belongs to the platform bridge that knows what its own
screen reader does with one.

This is the primitive the NSAccessibility, AT-SPI and UI Automation
bridges need to move into go-widgets/window, where they will make every
go-widgets application accessible without the application doing anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit ba4318e into main Aug 10, 2026
1 check passed
@tannevaled
tannevaled deleted the feat/a11y-positioned-walk branch August 10, 2026 09:56
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.

1 participant