Skip to content

fix(shell): clean keyboard-only focus ring on Compliance toggle - #10

Merged
build-with-dhiraj merged 1 commit into
mainfrom
fix/compliance-toggle-focus
Jun 23, 2026
Merged

build-with-dhiraj merged 1 commit into
mainfrom
fix/compliance-toggle-focus

Conversation

@build-with-dhiraj

Copy link
Copy Markdown
Owner

What

Fixes the Compliance toggle's heavy, stuck focus ring in the LabOS top nav. The pill showed a thick blue ring around the whole control in both on and off states, including after a mouse click, while the adjacent McClean Lab switcher did not.

Root cause

src/components/shell/ComplianceToggle.tsx is a <button role="switch"> with no local focus styling, so it fell back to the global button:focus-visible rule in src/app/globals.css (ring-2 ring-action ring-offset-2 ring-offset-white plus a border-radius: 0.5rem that squares the pill). On macOS Safari, clicking a role="switch" button can leave :focus-visible applied after mouse activation, so the ring looked stuck. The sibling LabSwitcher already overrides the global rule locally, which is why it looked clean.

Fix (one file, ComplianceToggle.tsx only)

  • Add the same local keyboard focus treatment the LabSwitcher uses: focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action.
  • Drop focus on pointer-driven activation only via onPointerUp, gated on a real pointerType (empty for keyboard-synthesized clicks), so a mouse click leaves no stuck ring while keyboard Tab still shows a visible, AA focus ring (WCAG 2.4.7).

No change to globals.css (other controls still rely on the global rule) and no change to mcclean.ts. Toggle behavior, aria-checked, size, colors, and layout are unchanged.

Verification (in-browser, Chromium via Playwright)

  • Mouse: clicking on and off flips aria-checked (false to true to false) and leaves no ring; the pill is as clean as the McClean switcher in both states.
  • Keyboard: Tab to the toggle shows a clean, visible ring computed identical to the LabSwitcher's focus ring; Enter and Space toggle aria-checked and keep the element focused with the ring. The pointer-blur guard never fires for keyboard activation (no pointerup, empty pointerType).
  • No layout shift; no console errors.

Engine note: the stuck-on-mouse-click ring is a WebKit/Safari :focus-visible quirk and does not reproduce in Chromium (a mouse click there never leaves :focus-visible), so the pointer-blur guard is applied as belt-and-suspenders. Keyboard focus visibility was confirmed preserved.

CI parity

  • npx tsc --noEmit: pass
  • npm run build (Next build): pass
  • Lint: this project has no eslint config, dependency, or lint script; Next's build-time type and lint step passed.

🤖 Generated with Claude Code

The Compliance toggle (a button role="switch") had no local focus
styling, so it fell back to the global button:focus-visible rule in
globals.css, which adds ring-offset-2 and squares the pill corners. On
macOS Safari, clicking a role="switch" button can leave :focus-visible
applied after a mouse click, so the heavy ring looked stuck in both the
on and off states.

Match the adjacent LabSwitcher's cleaner treatment by adding
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action
locally, and drop focus on pointer-driven activation only (onPointerUp,
gated on a real pointerType) so a mouse click leaves no stuck ring while
keyboard Tab still shows a visible, AA focus ring (WCAG 2.4.7).

Scope is ComplianceToggle.tsx only. No change to globals.css or other
controls. Toggle behavior, aria-checked, size, colors, and layout are
unchanged. Verified in-browser (Chromium via Playwright): mouse click
leaves no ring and flips aria-checked; keyboard Tab shows the same ring
as the LabSwitcher; Enter and Space toggle without blurring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@build-with-dhiraj
build-with-dhiraj merged commit 52257a6 into main Jun 23, 2026
1 check passed
build-with-dhiraj added a commit that referenced this pull request Jun 23, 2026
The switch knob span had a vertical anchor (top-0.5) but no horizontal
anchor, so left:auto resolved its base position to mid-track (~14px)
instead of 0. Measured live: OFF rendered the knob 16px from the track
left (reading like ON), and ON pushed the knob to 28px, overflowing the
28px track by 12px (knob off the right edge, looks broken).

Add left-0 to the knob so its base is 0. OFF now translates 2px (knob at
the left), ON translates 14px (knob right, right edge at 26px, fully
inside the 28px track, no overflow). Verified in a headless browser:
OFF knob.left - track.left = 2px, ON = 14px, right-overflow 0 in both,
aria-checked flips correctly, no console errors.

Follow-up to the focus-ring fix shipped in #10. Track, colors, sizes,
the translate values, and the focus treatment are unchanged.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant