Skip to content

Prune accessibility-ignored empty nodes #52

Description

@morisil

Summary

toSemanticEvents(respectAccessibility = true) skips subtrees excluded from the accessibility tree by aria-hidden="true", display:none, and visibility:hidden (isAccessibilityHidden). Chrome's tree drops more than that: empty inline wrappers, hidden-attribute elements, elements with no accessible content, some content clipped/positioned offscreen specifically for assistive tech. A node Chrome ignores and that carries no text is safe to drop, removing structural noise the current capture keeps.

Depends on #48 (shared introspection layer). Lowest priority of the accessibility set — hold until a real capture demonstrates the noise; do not implement speculatively.

Signal

An element is ignored by the accessibility tree roughly when its computed role resolves to nothing renderable AND it contributes no name/text. Detection options:

  • computedRole == "" (present-but-roleless / ignored) combined with an empty computedName and no descendant text. The combination is essential — see the trap.
  • The hidden attribute and inert are cheap explicit signals not currently checked by isAccessibilityHidden; those can be added independently of the flag.

⚠️ The trap

This is the most dangerous of the accessibility issues because "ignored" is easy to over-apply:

  • computedRole == "" also describes a layout-table node (the shipped layout-table case) and presentation nodes (Generalize role=presentation/none unwrapping beyond tables #50) whose children are meaningful — those must be unwrapped, not skipped. Skipping would delete content.
  • Offscreen "visually-hidden but screen-reader-visible" patterns (.sr-only) are the OPPOSITE — present in the tree, must be kept.

So the rule must be ignored AND empty (no text, no accessible name, no meaningful descendants) → skip; never "ignored" alone. The classifier ordering from #48 matters: unwrap/rename cases must be evaluated before the skip case.

Suggested approach

Add a Skip case to the #48 classifier, gated on the conjunction above, evaluated after the unwrap/rename cases. Start conservative: only hidden/inert attributes + truly empty roleless nodes. Expand the net only against evidence from rendered-Markdown diffs on real dumps (./gradlew :markanywhere-html:renderDumpMarkdown).

Test approach

Deterministic, under ChromeHeadlessExperimentalA11y:

  • <span></span> between content → dropped, surrounding text intact.
  • <div hidden>secret</div> → dropped.
  • Negative / safety: <div class="sr-only">screen reader text</div> (offscreen but in the tree) → KEPT.
  • Negative / safety: a roleless wrapper with text children → children survive (not skipped).

Out of scope

  • Anything that moves or reorders content (aria-owns).
  • Re-deciding the existing aria-hidden/display:none/visibility:hidden behaviour, which already works.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions