Skip to content

Generalize role=presentation/none unwrapping beyond tables #50

Open

Description

@morisil

Summary

toSemanticEvents(respectAccessibility = true) currently unwraps role="presentation"/role="none" only for tables (via isLayoutTable()). The same stripping should apply to any element whose role has been neutralised: <ul role="presentation">, <h2 role="none">, <nav role="presentation">, etc. should shed their semantics and promote their children, mirroring the accessibility tree. This is a small generalization of the shipped layout-table code.

Depends on #48 (shared introspection layer + flowElement treatment classifier).

Examples

DOM Desired output
<ul role="presentation"><li>a</li><li>b</li></ul> children promoted (no ul); the lis themselves are also presentational in a presentation list → promoted too
<h2 role="none">Not a heading</h2> Not a heading as plain content, no h2
<table role="presentation">… already handled — must keep working

⚠️ The trap

computedRole == "generic" is ubiquitous — every <div>/<span> reports it. The unwrap rule must therefore key off an explicit presentation/none role, NOT off a generic computed role. A blanket "unwrap when generic" would dissolve the entire document.

Two viable detection signals:

  1. The raw attribute: getAttribute("role") in {"presentation", "none"} — explicit, unambiguous, does not require the experimental flag.
  2. computedRole reflecting presentation conflict resolution (ARIA spec: a focusable element or one with global ARIA states/properties does NOT actually become presentational). computedRole already accounts for these exceptions, so it is the more correct signal — but it needs the flag.

Recommendation: prefer computedRole (handles the "presentation role is ignored on focusable / aria-labelled elements" edge cases the spec mandates), with the explicit-attribute check as the flag-off fallback. Document which path ran.

Suggested approach

Add an Unwrap case to the #48 classifier for the presentation/none verdict, reusing the structural-promotion machinery already built for tables (flowChildren with the tag dropped). For presentation containers with structural children (a presentation <ul>'s <li>s, a presentation table's rows — already handled), the children's own semantics are also stripped per ARIA "required owned elements" rules; the table code does this via TABLE_STRUCTURAL_TAGS + insideLayoutTable. Generalize that notion so a presentation <ul> likewise strips its <li>s.

Test approach

Deterministic, under ChromeHeadlessExperimentalA11y:

  • <ul role="presentation"><li>a</li></ul> → a promoted, no ul/li.
  • <h2 role="none">x</h2> → x, no h2.
  • Presentation conflict: <button role="presentation">x</button> (focusable) → role is NOT honoured (still a button) — assert it is kept, validating the computedRole path over the naive attribute check.
  • Layout-table regression: the existing table tests must still pass.

Out of scope

Activity

  1. morisil commented on Jun 30, 2026

    @morisil
    MemberAuthor

    Rescope — unblocked from #48 (closed), feature partially done.

    Architecture changed (see #48): no browser-side classifier / isLayoutTable() / karma launcher anymore. The layout-table half of this generalization already shipped in the new home — markanywhere-html/.../ApplyAccessibility.kt unwraps a <table> whose AccessibilityAnnotations.ROLE is not in DATA_TABLE_ROLES. What's still missing is generalizing that unwrap to any presentation/none element (<ul role="presentation">, <h2 role="none">, <nav role="presentation">).

    New shape:

    • Decide the signal as before — prefer the computed verdict over the raw role attribute so ARIA's presentation-conflict resolution (focusable / globally-aria'd elements keep their role) is honoured. In the new pipeline that means relying on CapturePage's ROLE annotation (which is Blink's computed role) rather than re-reading getAttribute("role"). A <button role="presentation"> should still report a button role and be kept — that's the regression-guard test, now expressed as an ApplyAccessibilityTest case.
    • Add an Unwrap case to the ApplyAccessibility operator keyed on the computed presentation/none verdict, reusing the existing layout-table structural-promotion machinery (LAYOUT_TABLE_MODE / children(mode=…)), generalized so a presentation <ul> also strips its <li>s.
    • The ⚠️ trap still stands: generic is ubiquitous (every div/span) — never unwrap on generic, only on explicit presentation/none.

    Tests move to ApplyAccessibilityTest (deterministic event streams), away from the removed karma path.

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