Skip to content

♻️ refactor: Button を React.forwardRef 対応にする (#270)#275

Merged
touyou merged 2 commits into
mainfrom
fix/270-button-forwardref
May 20, 2026
Merged

♻️ refactor: Button を React.forwardRef 対応にする (#270)#275
touyou merged 2 commits into
mainfrom
fix/270-button-forwardref

Conversation

@touyou
Copy link
Copy Markdown
Member

@touyou touyou commented May 19, 2026

Summary

なぜ forwardRef なのか

issue #270 のコメントで「React 19 流の ref-as-prop が望ましい」と提案されていますが、ref-as-prop は React 19 ランタイム機能 で、React 18 では ref prop が剥がされて機能しません。sparkle-design の peerDep は ^18 || ^19 の両対応のため、現状の互換性を保ちつつ issue 解決を優先しました。

React 19 への完全移行(ref-as-prop 化)は別 PR #274 で全コンポーネント一括で進める想定です。そちらは breaking change として塩漬けし、モーション系等の他の大規模アップデートとまとめてリリースする方針です。

変更点

src/components/ui/button/index.tsx のみ:

  • function Button({...}: ButtonProps)React.forwardRef<HTMLButtonElement, ButtonProps>(function Button({...}, ref)) でラップ
  • 内部の <Comp> 要素に ref={ref} を渡す
  • 既存の関数本体ロジック(asChild 分岐、disabled ハンドリング、a11y warning など)には一切変更なし

Test plan

  • `tsc --noEmit` クリーン
  • vitest `unit` プロジェクト全 607 件 pass(Button 39 件含む)
  • sparkle-design-internal 側で PopoverTriggerButton workaround を削除して問題なく動作することを確認

関連

  • goodpatch/sparkle-design-internal#182(Popover workaround の元 PR)
  • sparkle-design#274(React 19 完全移行 PR・塩漬け)

🤖 Generated with Claude Code

PopoverTrigger 等で `asChild` 合成する際に Button へ直接 ref を渡せるよう、
`React.forwardRef<HTMLButtonElement, ButtonProps>` でラップする。

- 既存の関数本体ロジックには変更なし
- ref を内部の `<Comp>` 要素に渡す
- React 18 ランタイム互換のため、ref-as-prop ではなく forwardRef を採用

ref: sparkle-design-internal#182(PopoverTriggerButton workaround の削除につながる)

closes #270

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 19, 2026 09:54
@vercel
Copy link
Copy Markdown

vercel Bot commented May 19, 2026

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

Project Deployment Actions Updated (UTC)
sparkle-design Ready Ready Preview, Comment May 20, 2026 1:05am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 21beed0a-6917-4cac-941d-65ece8eb63b9

📥 Commits

Reviewing files that changed from the base of the PR and between e9d14d1 and 33c4db9.

📒 Files selected for processing (2)
  • src/components/ui/button/index.test.tsx
  • src/components/ui/button/index.tsx

ウォークスルー

Button を通常の関数定義から React.forwardRef でラップした関数式に置き換え、ref 引数を受け取り内部の Comp 要素へキャストして渡すよう変更しました。ref 転送の挙動を検証するテストが追加されています。

変更内容

Button の forwardRef 対応

Layer / File(s) Summary
コンポーネント定義と ref 伝播の実装
src/components/ui/button/index.tsx
ButtonReact.forwardRef<HTMLElement, ButtonProps> でラップし、ref 引数を受け取る関数式に変更。内部で ref={ref as React.Ref<HTMLButtonElement>} として Comp に明示的に渡し、forwardRef ラッパーの終了構文へ更新。
ref 伝播のユニットテスト追加
src/components/ui/button/index.test.tsx
通常レンダリング時に ref.current<button> 要素を指すこと、および asChild でスロットした場合に ref<a> 要素へ転送されることを検証するテストを追加。

推定レビュー所要時間

🎯 3 (Moderate) | ⏱️ ~20 minutes

ポエム

ぴょんと渡すその手は軽やかに、
ref は迷わず button へ行くよ。
asChild の道も忘れずに、
リファレンス届けるラビットです 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed プルリクエストのタイトルは「♻️ refactor: Button を React.forwardRef 対応にする (#270)」で、変更内容を明確に反映しており、Button コンポーネントの forwardRef 対応という主要な変更を適切に要約しています。
Description check ✅ Passed プルリクエストの説明は詳細で、変更の背景(React 18/19 互換性)、変更内容、テスト計画を含んでいますが、公式テンプレートの「動作確認」「セルフレビューリスト」セクションの一部チェックが未完了です。
Linked Issues check ✅ Passed PR は issue #270 の要件である Button コンポーネントを React.forwardRef でラップし ref を DOM に転送する実装を完了しており、asChild での 要素対応も含めてユニットテストでカバーされています。
Out of Scope Changes check ✅ Passed 変更は Button コンポーネント(index.tsx)と対応するテストファイル(index.test.tsx)のみであり、issue #270 の ref forwarding 対応という指定範囲内に限定されています。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/270-button-forwardref

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

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e9d14d14ba

ℹ️ 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".

Comment thread src/components/ui/button/index.tsx Outdated
children,
...props
}: ButtonProps) {
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Type refs for asChild targets

When asChild slots a non-button element such as the documented Link/anchor pattern, this forwardRef<HTMLButtonElement, ...> signature exposes only Ref<HTMLButtonElement>. A consumer using the new direct-ref API as <Button asChild ref={anchorRef}><a ... /></Button> (or an integration whose trigger ref is typed as HTMLElement/HTMLAnchorElement) is rejected by TypeScript even though Slot forwards the ref at runtime. Please make the ref typing compatible with slotted elements instead of hard-coding it to HTMLButtonElement.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Button コンポーネントを React.forwardRef でラップして ref を外部から渡せるようにし、Radix の Trigger(Popover など)との合成をしやすくするための変更です(React 18/19 両対応を維持)。

Changes:

  • ButtonReact.forwardRef でラップし、内部要素へ ref を伝播
  • 既存の asChild 分岐・disabled ハンドリング・a11y warning 等のロジックは維持

Comment thread src/components/ui/button/index.tsx Outdated
Comment on lines +400 to +416
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
{
className,
variant,
size,
theme,
isLoading = false,
isDisabled = false,
asChild = false,
disabled,
prefixIcon,
suffixIcon,
children,
...props
},
ref
) {
レビュー指摘(PR #275)への対応:
- `forwardRef<HTMLButtonElement, ButtonProps>` だと `asChild` で <a> 等の
  非 button 要素を渡した際に ref 型が実体と不整合になり TypeScript で弾かれる問題があった
- 公開 API としての ref 型を `HTMLElement` に広げ、Link/anchor 等の slot 先にも対応
- 内部 `<Comp>` の union 型整合のため、ref を `HTMLButtonElement` に narrow する
  キャストを 1 箇所追加(実行時は Slot が ref を適切に転送するので問題なし)

加えて Ref Forwarding describe ブロックを追加:
- 通常の <button> への ref forwarding
- `asChild` + <a> への ref forwarding(Slot 経由)

refs: PR #275 review (Codex, Copilot)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@touyou
Copy link
Copy Markdown
Member Author

touyou commented May 20, 2026

@chatgpt-codex-connector @copilot レビューありがとうございます。指摘いただいた forwardRef<HTMLButtonElement, ButtonProps> の型問題は仰る通りで、asChild<Link> / <a> を渡すケースで型エラーになる挙動を確認しました。

commit 33c4db9 で以下を修正しました:

  1. ref ジェネリクスを HTMLElement に広げるforwardRef<HTMLElement, ButtonProps> に変更。これで useRef<HTMLAnchorElement>Ref<HTMLElement> の Trigger ref も受け入れ可能に。
  2. 内部 <Comp> の union narrowing — Comp は SlotPrimitive.Slot | "button" の union のため "button" 側の型が Ref<HTMLButtonElement> を要求します。実行時には Slot が ref を適切に転送するため、ref as React.Ref<HTMLButtonElement> の 1 行キャストで内部の型整合を取りました(コメント付き)。
  3. ref forwarding テストを追加Button には ref に関するテストが無かったため、describe("Ref Forwarding") を追加し以下 2 ケースをカバー:
    • 通常の <button> への ref forwarding
    • asChild + <a> の場合に Slot 経由で <a> 要素へ ref が届くこと

vitest 41 件 pass(うち新規 ref forwarding 2 件)、tsc --noEmit クリーンです。

@chatgpt-codex-connector
Copy link
Copy Markdown

対応内容を確認しました。現在のローカルブランチはすでに 33c4db9fix(button): ref 型を HTMLElement に広げ ref forwarding テストを追加)まで進んでおり、追加で反映すべき差分はありませんでした。

そのため今回は 新規コード変更・追加コミット・make_pr 実行は行っていません(指示どおり、変更がない状態で make_pr は実行していません)。

確認コマンド

  • cd /workspace/sparkle-design && git status --short && git rev-parse --abbrev-ref HEAD && git log --oneline -5
  • cd /workspace/sparkle-design && find .. -name AGENTS.md -print

View task →

@touyou touyou merged commit bb0cf4b into main May 20, 2026
5 checks passed
@touyou touyou deleted the fix/270-button-forwardref branch May 20, 2026 01:20
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.

♻️ refactor: Button を React.forwardRef 対応にしてほしい(PopoverTrigger 等で asChild 合成するため)

2 participants