Repository navigation
Generalize role=presentation/none unwrapping beyond tables #50
Description
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:
- The raw attribute:
getAttribute("role") in {"presentation", "none"}— explicit, unambiguous, does not require the experimental flag. computedRolereflecting presentation conflict resolution (ARIA spec: a focusable element or one with global ARIA states/properties does NOT actually become presentational).computedRolealready 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>→apromoted, noul/li.<h2 role="none">x</h2>→x, noh2.- Presentation conflict:
<button role="presentation">x</button>(focusable) → role is NOT honoured (still a button) — assert it is kept, validating thecomputedRolepath over the naive attribute check. - Layout-table regression: the existing table tests must still pass.
Out of scope
- Recovering structure from positive roles (that's Recover semantic structure from ARIA roles (div-soup → headings, lists) #49).
- Pruning a11y-ignored nodes (that's Prune accessibility-ignored empty nodes #52) — presentation unwrap keeps the content, pruning removes it entirely; distinct.
Activity
morisil commentedon Jun 30, 2026
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.ktunwraps a<table>whoseAccessibilityAnnotations.ROLEis not inDATA_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:
roleattribute so ARIA's presentation-conflict resolution (focusable / globally-aria'd elements keep their role) is honoured. In the new pipeline that means relying onCapturePage'sROLEannotation (which is Blink's computed role) rather than re-readinggetAttribute("role"). A<button role="presentation">should still report a button role and be kept — that's the regression-guard test, now expressed as anApplyAccessibilityTestcase.Unwrapcase to theApplyAccessibilityoperator 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.genericis ubiquitous (everydiv/span) — never unwrap ongeneric, only on explicitpresentation/none.Tests move to
ApplyAccessibilityTest(deterministic event streams), away from the removed karma path.