Skip to content

Tab hidden attribute not re-applied on deactivation in activateTab() #699

@memtomem

Description

@memtomem

Summary

activateTab() at packages/memtomem/src/memtomem/web/static/app.js:680-694 removes the .active class from previously-active panels but never re-applies hidden = true. After the first tab activation any panel that has been visited stays hidden = false for the rest of the session, relying solely on CSS display: none (from the absent .active class) to hide content.

Found by Playwright UX review of v0.1.34 prod (2026-05-02). See docs/reports/mm-web-prod-v0.1.34-playwright-review.md (originally classified as P0 cross-tab a11y leak; verification reclassified to P2 — modern browsers do exclude display:none from the a11y tree, but the DOM-state hygiene gap is real and Playwright snapshots do dump the leaked nodes).

Evidence

packages/memtomem/src/memtomem/web/static/app.js:680-694:

// Hide all panels
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));   // 681

// ...

// Show panel
const panel = qs(`tab-${tabName}`);
if (panel) {
  panel.hidden = false;          // 694 — set on activate, never undone elsewhere
  panel.classList.add('active');
  // ...
}

The HTML (packages/memtomem/src/memtomem/web/static/index.html) starts with hidden on each inactive panel, but once activateTab() runs for a panel it strips hidden and never reapplies it on tab switch.

Why this is worth fixing even though display:none works

  • DOM state accumulates; over a session every visited panel ends with hidden=false. Test automation tools (Playwright browser_snapshot) walk the DOM and report stale content.
  • Future CSS edits or a switch from display:none to e.g. visibility:hidden would silently regress a11y. Belt-and-braces (hidden attr + class) is cheap.
  • inert could be added at the same time to also remove focus / pointer events from inactive panels.

Suggested fix

In activateTab(), change the deactivation pass to:

document.querySelectorAll('.tab-panel').forEach(p => {
  p.classList.remove('active');
  p.hidden = true;          // belt-and-braces with the CSS-driven display:none
});

…and the activation block continues to set panel.hidden = false.

Optionally also toggle inert for keyboard / pointer isolation.

References

  • Review: docs/reports/mm-web-prod-v0.1.34-playwright-review.md
  • Tracking umbrella: TBD

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions