fix(a11y): make unavailable placeholder buttons reachable by keyboard - #1778
Conversation
The disabled-placeholder pattern put `disabled` on the button and attached the reason it was unavailable via `aria-describedby` to an `sr-only` span, plus a `title`. But `disabled` removes the tab stop, so a keyboard user — and a screen-reader user moving by Tab rather than by virtual cursor — could never land on the control. The explanation was written and unreachable; the control simply vanished for them. Convert the controls that are unavailable for a *stated* reason (feature not built yet, or this record lacks the data) to `aria-disabled="true"` plus a shared inert handler, keeping the title and the described-by reason. 24 sites across 12 components, including the four that carried `disabled` and `aria-disabled` together — belt and braces that never helped, because the native attribute still won on focus. Transiently inert controls keep native `disabled`, which is correct there: a request in flight, a pager at its last page, a form action awaiting validity. Those are listed in the wiring doc so the next pass does not "fix" them. Styling had to move with the attribute: `disabled:` variants stop applying once the native attribute is gone, and the control becomes hoverable. The `controlDisabled` recipe grew its `aria-disabled:` half, and the therapy recipes switched `hover:enabled:` to `hover:not-aria-disabled:enabled:` so a converted control does not light up under the cursor. Contract updated with the code, rather than left saying the opposite: - docs/wiring-conventions.md replaces the "native disabled is the default here" argument with the stated-reason vs transient split, and settles the unreconciled pairing tracked as ledger #291. - require-button-wiring gains `redundantDisabledPair`, failing on the two attributes together on any <button>; a statically-off side still passes. - tests/require-button-wiring.test.ts pins that the rule fires in both directions, since a green lint run cannot distinguish a clean repo from a rule that matches nothing. - favourites-hub-unavailable-controls tabs onto a converted placeholder, asserts it takes focus, asserts the description is what the reader gets, and asserts keyboard and pointer activation do nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hT4qToXT5nr3hV4ENqRgS
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 28 minutes Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (32)
Comment |
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018hT4qToXT5nr3hV4ENqRgS
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 722abdb780
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex resolve actionable Codex review findings for this pull request and current head using the repository instructions. This is the pull request's single automatic repair pass: do not perform a fresh review, create new standalone findings, or request another review. Work only the existing unresolved Codex threads on the current head. The only repository destination is BigSimmo/Database, and the only branch destination is the pull request head branch claude/disabled-button-accessibility-piclvr at starting commit fb36de3; never publish fixes to a detached or synthetic work branch and never create a stacked pull request. Use the authenticated GitHub connector to commit each approved fix to BigSimmo/Database:claude/disabled-button-accessibility-piclvr, then verify that the pull request head contains the published commit before reporting success. Always fix P0 and P1 findings. For P2 and lower findings, fix only clear, scoped, low-risk issues; otherwise disposition them with a concise reason. For a fixed thread, reply with as the first line and as the second line. For a no-code disposition, use followed by . These result markers authorize the workflow to close that exact thread only after it verifies a fixed commit is the pull request head; a local-only commit is not a fix. If publication or verification fails, do not use either result marker, do not claim success, and leave the thread open with the blocker. If human input or new authorization is required, do the same. Finish only after every actionable thread is fixed or dispositioned and closed, or explicitly left open for a human decision. Do not update the branch from main, address unrelated reviews, broaden scope, or create more than one scoped fix commit. Do not use external APIs, paid services, credentials, dependency changes, or broad refactors unless explicitly authorized. Add targeted tests where behavior changes and run the narrowest relevant validation. |
|
Codex Review: Didn't find any major issues. Chef's kiss. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
CI triageCI failed on this PR. Automated classification of the 3 failed job(s):
Compared with main CI run #9463 (cancelled). Classification is evidence routing, not permission to ignore a failure. Exact quarantined Playwright identities remain governed by the flake ledger. |
…esolve GitHub DIRTY/staleness) Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
|
@cursoragent Work the current open PR end-to-end. Confirm the PR number and GitHub head first from context. If more than one open PR could apply, stop and say which one you would use and why. Fetch and start from the remote tip that matches that GitHub head. If the named branch ref is missing or stale, use the PR head ref. Preserve unrelated local WIP, including any local-only ledger commits; do not discard dirty work, and do not treat a local-ahead commit as the reviewed tip. Do not merge the PR, force-push, rebase, or arm auto-merge unless I explicitly ask. No provider-backed gates without separate approval. If you cannot push or resolve threads, diagnose and comment only; if inline replies fail, resolve when possible and put dispositions in the summary comment. If auto-merge is already armed, push only for a real blocker, and avoid pushes that would cancel in-flight required CI unless the push itself clears that blocker. If the PR is already merged or closed: confirm the head and merge commit, note required-CI outcome, post one summary, and stop. Goal: deep review plus Bugbot, fix actionable issues with the smallest correct changes, clear merge / required-CI / thread blockers, run strong local offline verification, push fixes, append the review ledger, and post one PR summary. Prefer thoroughness over speed. Regenerate large assets only when a fix requires it; then run the asset check and keep compatibility aliases byte-identical where the repo uses them. Snapshot the GitHub head SHA: tip, base, behind/ahead, mergeable state, merge-tree versus origin/main (real conflict versus behind-but-clean), required checks on that tip including Production UI when selected, advisory separately, unresolved actionable threads. Missing checks while dirty are not green. If the tip moves mid-work, re-snapshot and continue from the new head. Ledger-lookup against that GitHub head under the heavy review-and-fix scope for this PR. Already reviewed at this head with clean merge-tree, green required checks, and no new actionable threads → summarize, comment, stop unless I asked for a fresh superseding pass. Follow the repo review protocol. Unblock once: real conflict → merge origin/main (prefer main’s shared queues; keep this PR’s notes); behind-but-clean → one sync, late if required CI is in flight; clean → leave. Labels like skip-branch-sync do not block a manual sync needed to clear a real blocker. No sync thrash. After any sync or push, re-snapshot tip, merge-tree, and required checks before declaring done. Dedupe the ledger if a merge touched it. Review high-confidence delta risks only. Separate PR-introduced defects from pre-existing re-emitted debt. For generated assets, review contracts, aliases, cache, and manifests—not every generated line. If protected RAG or ranking surfaces are touched, say so before editing; fix PR-body policy text only when wrong or missing. Ignore bot noise. No nit spam or broad rewrites. Escalate verification by touched risk: clinical / RAG / privacy / migrations / auth → domain check plus production-readiness when warranted; UI / phone-chrome / routing / styling → ensure plus phone-chrome or UI gates when warranted; generated assets or docs inventory/links → asset or docs checks when warranted. Fix P0/P1 always; clear scoped P2 when locally provable; else disposition and resolve. Required-check failures on this tip only; ignore advisory. Prefer reverting a bad autofix commit. After fixes: format and commit before push; smallest targeted proof; repo cheap gate; repo PR-local gate. No release, lighthouse, live eval, or live provider gates without approval. Push only this PR’s fix commits. Append ledger for the final GitHub head under the heavy scope. Never push a ledger-only tip—include ledger in a real fix push, or leave a no-change ledger append local and unpushed. One PR comment: tip, sync/merge-tree, fixed versus dispositioned, threads, required CI, decisive local gate lines, residual risks; merge left to me. Inline only for remaining human-needed P0/P1. Stop when merge-tree is clean on the current GitHub tip, actionable threads are clear, required checks are green or clearly in progress, heavy gates for scope passed, summary posted, merge left to me. |
|
You need to increase your spend limit or enable usage-based billing to run background agents. Go to Cursor |
|
@cursoragent Work the current open PR end-to-end. Confirm the PR number and GitHub head first from context. If more than one open PR could apply, stop and say which one you would use and why. Fetch and start from the remote tip that matches that GitHub head. If the named branch ref is missing or stale, use the PR head ref. Preserve unrelated local WIP, including any local-only ledger commits; do not discard dirty work, and do not treat a local-ahead commit as the reviewed tip. Do not merge the PR, force-push, rebase, or arm auto-merge unless I explicitly ask. No provider-backed gates without separate approval. If you cannot push or resolve threads, diagnose and comment only; if inline replies fail, resolve when possible and put dispositions in the summary comment. If auto-merge is already armed, push only for a real blocker, and avoid pushes that would cancel in-flight required CI unless the push itself clears that blocker. If the PR is already merged or closed: confirm the head and merge commit, note required-CI outcome, post one summary, and stop. Goal: deep review plus Bugbot, fix actionable issues with the smallest correct changes, clear merge / required-CI / thread blockers, run strong local offline verification, push fixes, append the review ledger, and post one PR summary. Prefer thoroughness over speed. Regenerate large assets only when a fix requires it; then run the asset check and keep compatibility aliases byte-identical where the repo uses them. Snapshot the GitHub head SHA: tip, base, behind/ahead, mergeable state, merge-tree versus origin/main (real conflict versus behind-but-clean), required checks on that tip including Production UI when selected, advisory separately, unresolved actionable threads. Missing checks while dirty are not green. If the tip moves mid-work, re-snapshot and continue from the new head. Ledger-lookup against that GitHub head under the heavy review-and-fix scope for this PR. Already reviewed at this head with clean merge-tree, green required checks, and no new actionable threads → summarize, comment, stop unless I asked for a fresh superseding pass. Follow the repo review protocol. Unblock once: real conflict → merge origin/main (prefer main’s shared queues; keep this PR’s notes); behind-but-clean → one sync, late if required CI is in flight; clean → leave. Labels like skip-branch-sync do not block a manual sync needed to clear a real blocker. No sync thrash. After any sync or push, re-snapshot tip, merge-tree, and required checks before declaring done. Dedupe the ledger if a merge touched it. Review high-confidence delta risks only. Separate PR-introduced defects from pre-existing re-emitted debt. For generated assets, review contracts, aliases, cache, and manifests—not every generated line. If protected RAG or ranking surfaces are touched, say so before editing; fix PR-body policy text only when wrong or missing. Ignore bot noise. No nit spam or broad rewrites. Escalate verification by touched risk: clinical / RAG / privacy / migrations / auth → domain check plus production-readiness when warranted; UI / phone-chrome / routing / styling → ensure plus phone-chrome or UI gates when warranted; generated assets or docs inventory/links → asset or docs checks when warranted. Fix P0/P1 always; clear scoped P2 when locally provable; else disposition and resolve. Required-check failures on this tip only; ignore advisory. Prefer reverting a bad autofix commit. After fixes: format and commit before push; smallest targeted proof; repo cheap gate; repo PR-local gate. No release, lighthouse, live eval, or live provider gates without approval. Push only this PR’s fix commits. Append ledger for the final GitHub head under the heavy scope. Never push a ledger-only tip—include ledger in a real fix push, or leave a no-change ledger append local and unpushed. One PR comment: tip, sync/merge-tree, fixed versus dispositioned, threads, required CI, decisive local gate lines, residual risks; merge left to me. Inline only for remaining human-needed P0/P1. Stop when merge-tree is clean on the current GitHub tip, actionable threads are clear, required checks are green or clearly in progress, heavy gates for scope passed, summary posted, merge left to me. |
|
You need to increase your spend limit or enable usage-based billing to run background agents. Go to Cursor |
There was a problem hiding this comment.
Pull request overview
Makes persistent unavailable buttons keyboard-focusable while retaining inert behavior and clear accessibility semantics.
Changes:
- Converts 25 placeholders to
aria-disabledwith guarded activation. - Adds shared styling/handler support and a lint rule preventing redundant disabled attributes.
- Updates tests and documentation for the new contract.
Reviewed changes
Copilot reviewed 29 out of 29 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
AGENTS.md |
Updates button-wiring policy. |
CLAUDE.md |
Updates assistant orientation. |
docs/branch-review-ledger.md |
Records review activity. |
docs/wiring-conventions.md |
Documents disabled-state semantics. |
eslint-rules/require-button-wiring.mjs |
Rejects redundant disabled pairs. |
src/components/calculators/search-page.tsx |
Converts filters placeholder. |
src/components/clinical-dashboard/account-setup-dialog.tsx |
Converts provider placeholders. |
src/components/clinical-dashboard/auth-panel.tsx |
Converts unavailable sign-in. |
src/components/clinical-dashboard/evidence-panels.tsx |
Converts favourites placeholder. |
src/components/clinical-dashboard/favourites-command-library-page.tsx |
Converts unavailable actions. |
src/components/clinical-dashboard/favourites-hub.tsx |
Converts three placeholders. |
src/components/clinical-dashboard/settings-dialog.tsx |
Converts provider sign-in row. |
src/components/clinical-dashboard/visual-evidence.tsx |
Converts favourites placeholder. |
src/components/forms/form-detail-page.tsx |
Converts missing-source action. |
src/components/forms/forms-search-results-page.tsx |
Converts filters/pathway placeholders. |
src/components/services/service-detail-page.tsx |
Converts missing-contact action. |
src/components/therapy-compass/controls.ts |
Adds aria-disabled styling. |
src/components/therapy-compass/screens/brief-screen.tsx |
Converts handout actions. |
src/components/therapy-compass/screens/recommend-screen.tsx |
Converts sheet actions. |
src/components/therapy-compass/therapy-card.tsx |
Converts unavailable card actions. |
src/components/therapy-compass/workspace.tsx |
Retains native transient disabling. |
src/components/ui-primitives.tsx |
Adds shared inert handler and styling. |
tests/auth-panel-product-truth.dom.test.tsx |
Updates sign-in assertions. |
tests/favourites-auth-gate.dom.test.tsx |
Updates provider assertions. |
tests/favourites-hub-unavailable-controls.dom.test.tsx |
Tests focus and inert activation. |
tests/mobile-interaction-regressions.test.ts |
Updates placeholder contract. |
tests/require-button-wiring.test.ts |
Tests the lint rule. |
tests/settings-dialog-actions.dom.test.tsx |
Updates settings assertions. |
tests/therapy-compass-responsive-contract.test.ts |
Updates therapy button contract. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Update the therapy hover contract assertion to accept hover:not-aria-disabled:enabled:, and run redundantDisabledPair before the JSX spread escape so explicit disabled+aria-disabled pairs stay gated. Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Clear behind-but-clean GitHub DIRTY/staleness after #1777 landed on main. Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
PR #1778 review-and-fix summaryTip: Sync
Fixed
Dispositioned
Threads
Required CI (this tip)
Local gates (decisive lines)
Not run (needs approval / env)
Ledger
Protected surfaces
Merge left to you. |
Gate in-page action-row hover behind not-aria-disabled, require an onClick whenever aria-disabled is used without native disabled, and wire the Language/region placeholder that the new lint caught. Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Follow-up (deep-review P2s)Tip: Fixed from deep review
Gates
Required CI restarted on this tip. Merge still left to you. Ledger append for this tip remains local/unpushed. |
Resolve therapy-card.tsx by keeping the densified ResultCard layout and adopting main's keyboard-reachable aria-disabled placeholders from #1778. Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Summary
disabledon the button and attached the reason it was unavailable viaaria-describedbyto ansr-onlyspan, plus atitle. Butdisabledremoves the tab stop, so a keyboard user — and a screen-reader user moving by Tab rather than by virtual cursor — could never land on the control. The explanation was written and unreachable.aria-disabled="true"plus a shared inert handler (ignoreUnavailableActivation, new inui-primitives.tsx), keeping the existingtitleand thearia-describedby/sr-onlyreason. Four of them carrieddisabledandaria-disabledtogether — belt and braces that never helped, because the native attribute still won on focus.disabledon transient states, which is correct there: a request in flight, a pager at its last page, a form action awaiting validity. Making those focusable would be a regression, not a fix. The full list is in the doc so the next pass does not "fix" them.disabled:variant classes stop applying and the control becomes hoverable.controlDisabledinui-primitives.tsxgrew itsaria-disabled:half, and the therapy recipes intherapy-compass/controls.tsswitchedhover:enabled:tohover:not-aria-disabled:enabled:so a converted control does not light up under the cursor. Verified in the built CSS, not just in review: 13:hover:not([aria-disabled=true])selectors and 10[aria-disabled=true]rules are emitted bynpm run build.docs/wiring-conventions.mdpreviously argued that nativedisabledwas deliberately the default here (see "Deviation from a stated position" below).redundantDisabledPairineslint-rules/require-button-wiring.mjs, failing on the two attributes together on any<button>. A statically-off side (disabled={false}besidearia-disabled="true") still passes, since that is a real way to spell a conditional placeholder. This settles ledger#291, which tracked the repo pinning that shape two contradictory ways.tests/require-button-wiring.test.tspins that the lint rule actually fires in both directions. A greennpm run lintcannot distinguish a clean repo from a rule that matches nothing, and this rule had no test at all before.tests/favourites-hub-unavailable-controls.dom.test.tsx: it tabs onto a converted placeholder, asserts it takes focus, asserts the accessible description is what the reader gets instead, and asserts that activating it by keyboard (Enter and Space) and by pointer does nothing. That assertion is the whole point of the change and nothing pinned it before.Sites converted
calculators/search-page.tsxclinical-dashboard/account-setup-dialog.tsxclinical-dashboard/auth-panel.tsxclinical-dashboard/evidence-panels.tsxclinical-dashboard/favourites-command-library-page.tsx×3clinical-dashboard/favourites-hub.tsx×3clinical-dashboard/settings-dialog.tsxclinical-dashboard/visual-evidence.tsxforms/form-detail-page.tsxforms/forms-search-results-page.tsx×4services/service-detail-page.tsxtherapy-compass/screens/brief-screen.tsx×2,screens/recommend-screen.tsx×2,therapy-card.tsx×2therapy-compass/workspace.tsxwent the other way: its retry-while-loading button carried both attributes, and as a transient state it keeps nativedisabledalone. It was found by the new lint rule, not by my own audit — it has neither atitlenor anaria-describedby, so it sat outside the scope this change was measured against.Deviation from a stated position — please read before approving
docs/wiring-conventions.mddid not merely happen to use nativedisabled; it argued for it, in a section headed "Nativedisabled, notaria-disabled, is the default here", andtests/favourites-hub-unavailable-controls.dom.test.tsxpinnedtoBeDisabled()andnot.toHaveAttribute("aria-disabled")together. This PR overturns that. The old argument was half right — a screen-reader user browsing with a virtual cursor genuinely can reach a disabled button and hear its description, which is what that section asserted — but it does not hold for keyboard-only users, or for screen-reader users navigating by Tab, and it was silently doing the work of justifying the pairing that#291had already flagged as unreconciled. Five tests that pinned the old shape were updated rather than deleted, each with the reason inline.Deliberately not converted (follow-ups, no ledger row —
docs/outstanding-issues.mdis owned by another session this cycle)differentials/differential-presentation-workflow-page.tsx— four "not available in this comparison view" placeholders plus the Compact/Detailed density pair. They qualify, but that page is scheduled for a rewrite andtests/mobile-interaction-regressions.test.tsstill pins the density pair as native-only. Convert with the rewrite.document-viewer/document-image-filmstrip.tsx— out of scope; a parallel session owns that subtree.DocumentViewer.tsxsummarize action — genuinely ambiguous:summarizeTitlemixes a persistent auth reason ("Sign in before answering from this document") with a transient loading state behind one flag. It needs splitting before it can be classified, so I left it rather than guess.search-pins-menu.tsx"New pin" at the pin cap ("Maximum N pins reached") — ambiguous. It states a reason, but it is a capacity state the user clears by deleting a pin, which reads as transient. Left native.calculator-mockups/search-page-mockup.tsx— design scratch, exempt from the wiring gates.Verification
npm run verify:pr-local— 22 of 24 gates completed;testreported the pre-existing failure below, which stoppedbuildandcheck:rag:fixturesbeing reached, so both were then run directly and passed.npm run lint— exit 0. Re-run uncached (rm -rf node_modules/.cache/eslint) after the new rule landed: the cached run had reused a stale entry and missedworkspace.tsx; the pre-push guard, which lints the pushed commit in a clean worktree, caught it. Uncached run iseslint exit=0.npm run typecheck— exit 0.npm run test—Test Files 546 passed (547),Tests 5878 passed | 4 skipped (5883), 1 failed.npm run build— exit 0; built CSS greped for the new variants (13not([aria-disabled=true])selectors, 10[aria-disabled=true]rules).npm run check:rag:fixtures—Offline RAG fixture and manifest validation passed (36 golden cases, 23 suites).npm run formatrun and committed before pushing;npx prettier --check .→All matched files use Prettier code style!npm run verify:ui— UI verification not run: this remote container has no Playwright browser provisioned, andverify:uiis a hosted-browser gate. Chromium journeys should be treated as owed for this change: the conversion alters focus order and hover behaviour on real pages. CI Production UI covers it on this PR.The one failing test is pre-existing and unrelated.
tests/pr-handoff-stop.test.ts > emits handoff context only when the marker file existschmods a.gitdirectory to000and asserts the hook's write fails; this container runs as uid 0, where chmod does not restrict root, so the marker is created and the assertion inverts. My diff touches no file undertests/pr-handoff-stop.test.ts,.claude/hooks/, orscripts/(git diff --stat HEADfor those paths is empty), and the test fails identically in isolation.Note on environment: the container shipped Node 24.13.0, below the 24.15.0 floor
jsdom@30declares, sonpm cifailedEBADENGINE. I installed Node 24.18.1 locally and ran every gate on it rather than passing--engine-strict=false. Open PR #1771 addresses that floor.Risk and rollout
controlDisabledrecipe and the therapy control recipes, which are used well beyond the converted sites. The realistic failure mode is visual — a converted control that reads as available because adisabled:variant no longer applies — which is why the built CSS was checked for the emittedaria-disabled:andhover:not-aria-disabled:rules rather than trusting review.git revertof the single commit restores both the markup and the contract; no data, schema, or configuration is involved.Clinical Governance Preflight
SourceActionRow, the Copy actions inevidence-panels.tsxandvisual-evidence.tsx) are unchanged.Clinical KB Database(sjrfecxgysukkwxsowpy) — no Supabase configuration, migration, or environment value is touched.form-detail-page.tsx,service-detail-page.tsx) are precisely the "this record has no such data" case, and the conversion makes that absence more discoverable rather than less: the reason is now reachable by keyboard instead of only by pointer hover.Notes
ignoreUnavailableActivationcallspreventDefault()andstopPropagation()deliberately: a natively disabled button fires no click at all, so nothing bubbled to a clickable ancestor. WithoutstopPropagationa placeholder inside a clickable row would begin activating the row — a behaviour change the conversion must not introduce.<button>, not justtype="button", since the pairing is wrong regardless of type. The existingunwiredcheck keeps its narrowertype="button"scope and its spread escape hatch untouched.not-aria-disabled:had no prior use in this repo. I confirmed Tailwind 4.3.1 compiles it (to:not([aria-disabled="true"])) before adopting it, and the production build confirms it end to end.🤖 Generated with Claude Code
https://claude.ai/code/session_018hT4qToXT5nr3hV4ENqRgS
Generated by Claude Code