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を検証する
- Mouse hoverで表示する
- keyboard focusがなければmouse leaveで消える
- mouse clickによるfocusだけでは表示しない
- Tab focusで表示する
- Escapeでfocusを残したまま消える
aria-describedbyがvisible tooltip IDを参照する- pointerをtooltipへ移動しても必要情報を読める
最後はWCAGのhover-or-focus要件に関係します。重要情報ならtooltipではなくvisible textも検討します。
参考
- WAI-ARIA Tooltip Pattern
- MDN: tooltip role
- WCAG 2.2: Content on Hover or Focus
- Reactのnested interactive要素はsibling buttonsへ分解する
まとめ
onFocusにはmouse由来のfocusも含まれます。event.currentTarget.matches(':focus-visible')を確認し、hoverとkeyboard stateを分け、aria-describedbyで接続し、focusを動かさずEscapeで閉じます。