From 4bda5b4397471bf60d733bd710426ded061e5f43 Mon Sep 17 00:00:00 2001 From: Francis Secada Date: Wed, 29 Jul 2026 12:25:47 -0400 Subject: [PATCH 1/3] fix(a11y): dialog semantics, focus management, server-rendered tab state (#21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three gaps that predate 0.1.0, flagged in the #6 review and deferred so a theme PR would not widen into an accessibility PR. Fixed against both shipped themes now, because every pattern here is one the three themes in the expansion epic will copy — landing it after would mean writing it five times instead of two. Modal: role="dialog" + aria-modal, named by its header via aria-labelledby or by a new `label` prop when there is none, focus moved in on open, restored to the trigger on close, trapped while open, Escape to close. It stays a
; showModal() would give all of this for free but would make cfModal branch per theme, and the identical cross-theme Alpine contract is what the theme work protects. The whole implementation is in cf_ui_alpine.js, so a new theme inherits it by writing markup. Recorded in docs/accessibility.md. Tabs: `active` is now a prop, server-rendering the theme's active class plus aria-selected, aria-controls and a roving tabindex. The Alpine binding stays on top — server state is the initial value, not a replacement. Arrow/Home/End move focus, Enter/Space activates; automatic activation would fire an HTMX request on every arrow press. Panel: `open` was declared but never rendered, so an open panel still emitted x-cloak and stayed hidden without JS. Now server-rendered, with initPanel() seeding Alpine from it, and a real + +