Skip to content

feat(Tooltip): type propの追加とchildrenのタブストップ二重問題の解消#6307

Open
schktjm wants to merge 2 commits into
masterfrom
feat-tooltip-type
Open

feat(Tooltip): type propの追加とchildrenのタブストップ二重問題の解消#6307
schktjm wants to merge 2 commits into
masterfrom
feat-tooltip-type

Conversation

@schktjm
Copy link
Copy Markdown
Contributor

@schktjm schktjm commented May 8, 2026

関連URL

https://www.notion.so/SmartHR-UI-Tooltip-type-label-description-35337b6398eb8076a66fe40f2d4a0d84?v=31737b6398eb807b8dc3000c583344f5&source=copy_link

概要

Tooltip コンポーネントに2つの機能を追加します。

  1. type prop の追加: Primer UI の Tooltip と同様に、'label''description'(デフォルト)を選択可能にし、アクセシビリティ上の役割を明確に使い分けられるようにする
  2. tabIndex の自動制御: children がフォーカス可能な要素(Button, Input 等)の場合、wrapper の tabIndex を自動的に設定しないようにし、二重タブストップ問題を解消する

変更内容

type prop

  • type="description"(デフォルト): 従来通り aria-describedby で補足説明として機能
  • type="label": children に aria-labelledby を付与し、ツールチップのメッセージがアクセシブルネームとして機能(アイコンのみのボタン等に有用)
  • type="label" の場合は aria-describedby は付与されない(ラベルであり説明であるのは不自然なため)
  • type="label" の場合は ariaDescribedbyTarget の指定は無視される
// アクセシブルネームとして使用(type="label")
<Tooltip type="label" message="保存" triggerType="icon">
  <Button><FaPencilIcon /></Button>
</Tooltip>

// 補足説明として使用(type="description" / デフォルト)
<Tooltip message="この操作は取り消せません" ariaDescribedbyTarget="inner">
  <Button>削除</Button>
</Tooltip>

tabIndex の自動制御

  • children がフォーカス可能な要素(button, a[href], input, select, textarea, [tabindex])の場合、wrapper に tabIndex を設定しない
  • children がフォーカス不可能な要素(テキストノード等)の場合、従来通り tabIndex=0 を設定
  • tabIndex prop を明示的に指定した場合はその値が優先される(後方互換)

テスト

Tooltip.test.tsx を新規作成し、tabIndextype="description"type="label" の各パターンをテスト。

Storybook

type prop 用のストーリーを追加。

プロダクト側で対応が必要な事項

確認方法

https://63d0ccabb5d2dd29825524ab-iwqplfqzqm.chromatic.com/?path=/story/components-tooltip--type

  • Storybook の Components/Tooltip > type ストーリーで label / description の動作確認
  • DevTools で aria-labelledby / aria-describedby の付与先を確認
  • Tab キー操作で Button を children にした場合にダブルタブストップが発生しないことを確認

schktjm and others added 2 commits May 7, 2026 18:50
Primer UIのTooltipのように、type propでツールチップの役割を選択できるようにする。
type="label"の場合はchildren要素にaria-labelledbyを付与しアクセシブルネームとして機能する。
type="description"(デフォルト)の場合は従来通りaria-describedbyを付与し補足説明として機能する。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@schktjm schktjm requested a review from a team as a code owner May 8, 2026 05:02
@schktjm schktjm requested review from uknmr and yt-ymmt and removed request for a team May 8, 2026 05:02
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 8, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@smarthr/smarthr-ui-charts@6307
npm i https://pkg.pr.new/smarthr-ui@6307

commit: 6930e06

@schktjm schktjm changed the title feat(Tooltip): type propの追加とchildrenのフォーカス可能判定によるtabIndex自動制御 feat(Tooltip): type propの追加とchildrenのタブストップ二重問題の解消 May 8, 2026
}

const isFocusable =
childElement.matches('a[href], button, input, select, textarea, [tabindex]') &&
Copy link
Copy Markdown
Contributor Author

@schktjm schktjm May 8, 2026

Choose a reason for hiding this comment

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

children にフォームコントロール要素がいるかどうかの判定を、FormControl のように [data-smarthr-ui-input="true"]' などの smarthr-ui 内で定義しているものを使うか迷いましたが、 野良の button や a の見落としが発生しそうな気がして要素を指定しています

@schktjm
Copy link
Copy Markdown
Contributor Author

schktjm commented May 12, 2026

https://github.com/kufu/skills/accessibility の結果、たしかに

  ---
  アクセシビリティレビュー: Tooltip type prop & tabIndex 自動制御

  変更の概要

  1. type prop 追加 ('label' | 'description'、デフォルト 'description')
    - label: children に aria-labelledby を付与 → ツールチップがアクセシブルネームになる
    - description: 従来通り aria-describedby を付与 → 補足説明として機能
  2. tabIndex 自動制御: children がフォーカス可能な要素の場合、wrapper の tabIndex を設定しない
  3. ariaDescribedbyTarget: type="label" の場合は無視される

  ---
  良い点

  ┌───────────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
  │       チェック項目        │                                                                                評価                                                                                │
  ├───────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ 5-1. キーボードで操作可能 │ 改善。従来は wrapper(tabIndex=0) + 内部の Button で2回 Tab が必要だったが、フォーカス可能な子要素がある場合は wrapper の tabIndex を除去し、1回の Tab で到達可能に │
  ├───────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ 5-2. フォーカス順序       │ 改善。不要なフォーカスストップが除去され、視覚的な順序とフォーカス順序の一致が向上                                                                                 │
  ├───────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ 7-2. アクセシブルネーム   │ 改善。type="label" により、アイコンのみのボタンに Tooltip 経由でアクセシブルネームを付与可能に                                                                     │
  └───────────────────────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

  aria-labelledby は aria-hidden="true" な要素も参照できるため(Accessible Name Computation 仕様)、ツールチップが非表示の状態でもアクセシブルネームは正しく計算されます。

  フォーカスイベントのバブリングにより、wrapper に tabIndex がなくても内部要素へのフォーカス時にツールチップは表示されます。

  ---
  確認・検討が必要な点

  1. disabled なネイティブ要素のフォーカス判定(軽微)

  Tooltip.tsx:121-123 のフォーカス判定:

  const isFocusable =
    childElement.matches('a[href], button, input, select, textarea, [tabindex]') &&
    !childElement.matches('[tabindex="-1"]')

  ネイティブの <button disabled> は button にマッチするため isFocusable = true になりますが、実際にはフォーカスできません。SmartHR UI の Button は aria-disabled
  パターンを使うため実用上は問題ない可能性が高いですが、ネイティブの disabled 要素を children に渡した場合は wrapper にもフォーカスが当たらず、ツールチップが表示できなくなります。

  2. type="label" 使用時の cloneElement 前提条件

  type="label" を指定すると cloneElement(children as ReactElement, ...) が実行されます。children がテキストノードの場合はランタイムエラーになりますが、type="label"
  の想定用途(アイコンのみのボタンなど)ではフォーカス可能な ReactElement が children になるため、実用上は問題ありません。JSDoc のコメントで使い方を案内している点は良いです。

  3. type の使い分けガイダンス

  利用者が type="label" と type="description" を誤用するリスクがあります。例えば、既にテキストを持つボタンに type="label" を使うと、aria-labelledby
  がボタンの自然なアクセシブルネームを上書きしてしまいます(可視ラベルとの不一致、WCAG 2.5.3 違反の可能性)。Storybook のドキュメントや JSDoc で使い分けの指針を示すと安全です。

  ---
  総合評価

  この変更はアクセシビリティを明確に改善しています。特に tabIndex の自動制御によるダブルフォーカスの解消と、type="label"
  によるアイコンボタンへのアクセシブルネーム付与は、チェックリストの「キーボードで操作可能」「フォーカス順序」「アクセシブルネーム」の各項目に対してポジティブな変更です。

  上記の検討事項はいずれも軽微であり、ブロッカーとなるアクセシビリティ上の問題は見つかりませんでした。

  ---
  なお、画面拡大、コントラスト比、キーボード操作などについて追加の検証が必要です。

  Slackチャンネル #accessibility_相談 でアクセシビリティ部にご相談ください。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants