← ./articles-ja

React tooltipがmouse clickでも出る時はfocus-visibleを確認する

HTML title属性をcustom React tooltipへ置き換えると、Tabだけでなくmouse click後にもtooltipが出ることがあります。keyboard navigationとpointer clickのどちらでもtriggerへfocusが入るためです。

focus対応を削除せず、:focus-visibleでkeyboard型focusを判定し、hoverとは別stateで管理します。

onFocusだけではkeyboard navigationを判定できない

ReactのonFocusが示すのはfocusが入った事実だけです。buttonはmouse click後にも通常focusされるため、次の実装は誤表示を起こします。

<button onFocus={() => setVisible(true)}>Open</button>

「最後のinputがkeyboardか」をglobal flagで持つ方法もありますが、document listenerが増え、別windowやembedded WebViewでずれる可能性があります。visual tooltip表示の判断にはbrowserの:focus-visible heuristicがよい初期値です。

実際のtriggerでfocus-visibleを確認する

refはfocusできないwrapperではなくfocusable triggerへ置きます。

const triggerRef = useRef<HTMLButtonElement>(null);
const [hovered, setHovered] = useState(false);
const [keyboardFocused, setKeyboardFocused] = useState(false);

const visible = hovered || keyboardFocused;

<button
  ref={triggerRef}
  aria-describedby={visible ? tooltipId : undefined}
  onFocus={(event) => {
    setKeyboardFocused(event.currentTarget.matches(":focus-visible"));
  }}
  onBlur={() => setKeyboardFocused(false)}
  onMouseEnter={() => setHovered(true)}
  onMouseLeave={() => setHovered(false)}
>
  Open
</button>

currentTargetはhandlerを持つ要素です。targetは内側のiconやspanになる場合があります。

hoverとfocusのstateを分ける

1つのBooleanだけでは早く消える場合があります。mouse leaveでfalseにしてもkeyboard focusが残っている可能性があります。

const visible = hovered || keyboardFocused;

triggerから参照できる安定したIDでtooltipを描画します。

{visible && (
  <div id={tooltipId} role="tooltip">
    Open a project
  </div>
)}

tooltip自身にはfocusを移さず、buttonやlinkを入れません。interactive contentにはnon-modal dialogまたはpopoverを使います。

focusを捨てずEscapeで閉じる

WAI guidanceではEscapeでtooltipを閉じられることが期待されます。triggerのfocusは残します。

onKeyDown={(event) => {
  if (event.key === "Escape") {
    setKeyboardFocused(false);
  }
}}

tooltipを隠すためだけにblur()を呼ぶとkeyboard navigationを壊します。render後すぐ:focus-visibleで再表示される構成なら、escapeDismissed flagを持ち、blur時にresetします。

全input pathを検証する

  1. Mouse hoverで表示する
  2. keyboard focusがなければmouse leaveで消える
  3. mouse clickによるfocusだけでは表示しない
  4. Tab focusで表示する
  5. Escapeでfocusを残したまま消える
  6. aria-describedbyがvisible tooltip IDを参照する
  7. pointerをtooltipへ移動しても必要情報を読める

最後はWCAGのhover-or-focus要件に関係します。重要情報ならtooltipではなくvisible textも検討します。

参考

まとめ

onFocusにはmouse由来のfocusも含まれます。event.currentTarget.matches(':focus-visible')を確認し、hoverとkeyboard stateを分け、aria-describedbyで接続し、focusを動かさずEscapeで閉じます。