Skip to content

🐛 fix: asChild で無効化したときに aria-disabled: 由来の配色を当てる - #313

Open
touyou wants to merge 5 commits into
fix/310-button-aschild-parityfrom
fix/311-aria-disabled-styles
Open

🐛 fix: asChild で無効化したときに aria-disabled: 由来の配色を当てる#313
touyou wants to merge 5 commits into
fix/310-button-aschild-parityfrom
fix/311-aria-disabled-styles

Conversation

@touyou

@touyou touyou commented Aug 26, 2026

Copy link
Copy Markdown
Member

概要

asChild<a> / Link を差し込んで無効化したときに、無効状態の配色が一切当たらない問題を修正します。

問題

無効状態の配色は disabled: プレフィックスで出力されているため、:disabled 疑似クラスがマッチしない <a> では発火しません。結果として、aria-disabled は付き操作も抑止されるのに、配色だけが抜けた状態でレンダリングされていました。

対応

disabled: の各ユーティリティに aria-disabled: の対を literal で 併記しました。

-  "disabled:bg-surface-primary-high-disabled disabled:text-object-inverse"
+  "disabled:bg-surface-primary-high-disabled aria-disabled:bg-surface-primary-high-disabled disabled:text-object-inverse aria-disabled:text-object-inverse"

実行時に replace("disabled:", "aria-disabled:") で生成する形にはしていません。Tailwind はソース中のリテラル文字列を走査するため、生成では CSS が出力されないためです。

配色ギャップが埋まったので、両コンポーネントの「見た目の無効表現は差し込む要素側で用意してください」という dev 警告は削除し、JSDoc も実態に合わせました。あわせて aria-disabled の要素はフォーカス可能なままである点(タブ順から外すなら差し込み側で tabIndex={-1})を追記しています。

CSS 生成側の確認

#311 で「sparkle-design-cli が生成する CSS と Tailwind 4 で aria-disabled: が期待どおり出力されるか要確認」としていた点を、@tailwindcss/postcsssrc/app/sparkle-design.css を読ませて実際にコンパイルし確認しました。

  1. トークン付きで生成される

    .aria-disabled\:bg-surface-primary-high-disabled { &[aria-disabled="true"] { background-color: var(--color-surface-primary-high-disabled); } }
  2. hover / active に勝つaria-disabled:[aria-disabled="true"] 属性セレクタで hover: と詳細度が並びますが、出力順が hoveractivedisabledaria-disabled の順になるため、無効状態の配色が後勝ちします(Button は無効時も hover: クラスを出力し続けるので、この順序が効いています)

テスト

  • disabled: を持つすべてのユーティリティに aria-disabled: の対が存在することを、variant × theme の 9 パターンで機械的に検証するテストを両コンポーネントに追加(disabled: だけ足して aria-disabled: を足し忘れる退行を防ぐため)
  • 既存の「asChild 無効時に警告が出る」テストを、配色(aria-disabled:bg-*)の検証に置き換え
  • 廃止した警告に依存していたテストと、警告を黙らせるためだけの console.warn spy を削除

本 PR の変更を戻すと 20 ケースが失敗 することを確認済みです。

レビュー反映(2〜4 コミット目)

セルフレビュー(code-reviewer / pr-test-analyzer エージェント)の指摘を反映しました。

1. 「CSS が実際に出るか」をテストで担保するようにした

構造テストは className の文字列しか見ないため、Tailwind が実際に CSS を吐くか・hover: に勝つかは検証できていない、という指摘を受けて src/test/aria-disabled-css.test.ts を追加しました。@tailwindcss/postcsssrc/app/sparkle-design.css を読ませて実コンパイルし、トークン付きで出力されること・hoveractivedisabledaria-disabled の順になることを検証します(実行時間は 3 秒程度)。

  • postcss を devDependency に明示追加(これまで transitive 依存)
  • src/test/setup.ts の jsdom 固有 polyfill を window の有無でガードし、node 環境のテストを実行できるようにしました

2. 利用者が aria-disabled を直接渡したときの不整合を解消

この PR で Button の aria-disabled: 配色が常時出力されるようになったため、<Button aria-disabled="true"> が「見た目だけ無効・クリックは通る」状態になるという指摘がありました(IconButton は逆に「見た目は有効・抑止だけ効く」)。

両コンポーネントで、利用者が aria-disabled を直接渡した場合も配色と操作抑止を揃えるようにしました。native の disabled 属性は付けないため、フォーカスは残ります(soft disabled)。

3. 構造テストの穴を塞いだ

  • Button は無効時に loading 用 compound を使うため、isLoading 軸が無いと 9 件の compound が未検証だった → variant × theme × isLoading の 18 パターンに拡張
  • Button の base に disabled:cursor-not-allowed があるため length > 0 が常に真だった → 配色クラスの存在を要求
  • disabled:aria-disabled: の一方向しか見ていなかった → 両方向の集合一致に変更(aria-disabled: だけ書くと native button で配色が欠けるため)
  • className.split() ではなく classList を使用、テストの置き場所を両コンポーネントで統一

4. IconButton にも #312 の対応を移植

auxclick ガード / aria-disabled の優先順位 / type 無視の dev 警告を IconButton にも入れ、両コンポーネントの挙動を揃えました。

5. JSDoc の記述を実態に合わせた

  • native <button> 判定の限界(内部で <button> を描画するカスタムコンポーネントは非 button 扱い)
  • 無効時に抑止できるのは click / auxclick / Enter・Space までで、コンテキストメニューの「新しいタブで開く」は止められない
  • tabIndex={-1} の案内は「aria-disabled は通常フォーカス可能なままでよい」という ARIA の意図に沿った書き方に修正

6. テスト追加

outline / ghost の asChild 配色(border の上書きを含む)、slotted <button>disabled: 配色、Button の asChild + isLoading、aria-disabled 直接指定時の挙動、IconButton の auxclick 抑止。

検証

コマンド 結果
pnpm test ✅ 692 passed / 10 skipped / 19 todo
pnpm type-check ✅ エラーなし
pnpm lint:check ✅ エラーなし(既存の warning のみ)

🤖 Generated with Claude Code

- Button / IconButton の disabled: ユーティリティに aria-disabled: の対を literal で併記(Tailwind はソース中のリテラルを走査するため実行時生成では CSS が出ない)
- 配色ギャップが埋まったため「見た目は差し込み側で用意して」という dev 警告を両コンポーネントから削除
- JSDoc のアクセシビリティ節を実態に合わせ、aria-disabled 要素はフォーカス可能なままである点(tabIndex={-1} の案内)を追記
- disabled: に対応する aria-disabled: が必ず存在することを 9 パターン(variant × theme)で機械的に検証するテストを追加
- 既存の asChild 無効時テストを、警告の検証から配色の検証へ置き換え

Closes #311
@vercel

vercel Bot commented Aug 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sparkle-design Ready Ready Preview Aug 26, 2026 9:12am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3ea8fd87-c1da-471a-b275-2f5ba6428f4d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

touyou added 3 commits August 26, 2026 17:53
- Tailwind に sparkle-design.css を読ませて実際にコンパイルし、aria-disabled: がトークン付きで出力されること、hover / active / disabled より後に出力されること(詳細度が並ぶため出力順で勝敗が決まる)を検証するテストを追加
- コンポーネント側の構造テストは className の文字列しか見ないため、CSS が出るかどうかはここで担保する
- postcss を devDependency に明示追加(これまで transitive 依存だった)
- src/test/setup.ts の jsdom 固有 polyfill を window 有無でガードし、node 環境のテストを実行できるようにした
…bled-styles

# Conflicts:
#	src/components/ui/button/index.tsx
- 利用者が aria-disabled を直接渡した場合も配色と操作抑止を揃えた(従来は Button で「見た目だけ無効・クリックは通る」、IconButton で「見た目は有効・抑止だけ効く」と食い違っていた)。native の disabled 属性は付けないためフォーカスは残る
- IconButton にも auxclick ガード / aria-disabled の優先順位 / type 無視の警告を移植し、Button と挙動を揃えた
- JSDoc: native button 判定の限界、コンテキストメニュー経由は止められない点、aria-disabled 直接指定時の扱いを両コンポーネントに明記。tabIndex の案内は「通常はフォーカス可能なままでよい」という ARIA の意図に沿った書き方に修正
- 構造テストを両方向(disabled: ⇄ aria-disabled:)の集合一致に強化し、classList ベースに変更
- Button の構造テストに isLoading 軸を追加(loading 用 compound の disabled: が未検証だった)。base の disabled:cursor-not-allowed だけで空振りしないよう下限も配色クラスで判定
- 構造テストの置き場所を両コンポーネントで揃えた
- テスト追加: outline / ghost の asChild 配色、slotted button の disabled: 配色、Button の asChild + isLoading、aria-disabled 直接指定時の挙動、IconButton の auxclick 抑止
…bled-styles

- JSDoc の競合は #313 側(配色が当たる記述)を採用し、JP 側にも auxclick を反映
- 差し込んだ要素の capture ハンドラ / disabled を無効時に上書きする対応を IconButton にも移植
- soft disabled(利用者が aria-disabled を直接指定)でも子の capture ハンドラを外すようにし、native の disabled 属性は付けないままにした
- 上記の回帰テストを IconButton にも追加
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