🎮

【番外編】Focus Management APIについて(実装編) - React Ariaの実装読むぞ

2024/12/18に公開

こんにちは、フロントエンドエンジニアの mehm8128 です。
今日は Focus Management API の実装について書いていきます。

FocusScope

FocusScopeコンポーネント内で色んな hooks を実行したり、useFocusManagerを実行できるようにするための Provider を提供したりしています。

scopeRefFocusScope内の要素を配列として保持しているようで、以下のuseLayoutEffect内で取得しています。コメントにあるsentinelsというのはstartRefendRefをつけているspan要素のことで、これを目印にしてここからここまで、というのを決めているようです。

https://github.com/adobe/react-spectrum/blob/326f48154e301edab425c8198c5c3af72422462b/packages/%40react-aria/focus/src/FocusScope.tsx#L117-L136

https://github.com/adobe/react-spectrum/blob/326f48154e301edab425c8198c5c3af72422462b/packages/%40react-aria/focus/src/FocusScope.tsx#L187-L193

ここで取得したscopeRefは次から見ていくuseFocusContainmentなどの hooks にも渡されています。
それでは、FocusScopeに渡すことができる props であるcontain, restoreFocus, autoFocusに関係する hooks を見ていきます。

useFocusContainment

focus containment を実現する hook です。
onKeyDown関数で Tab キーによるフォーカス移動をe.preventDefault()した上で、 TreeWalker API(参考: Radio と Checkbox について - React Aria の実装読むぞ)などを用いて、最後の tabbable な要素から最初の tabbable な要素にフォーカスを移動する処理などが実装されています。

https://github.com/adobe/react-spectrum/blob/326f48154e301edab425c8198c5c3af72422462b/packages/%40react-aria/focus/src/FocusScope.tsx#L330-L357

useRestoreFocus

フォーカスの復元を実現する hook です。
mount 時にnodeToRestoreRefdocument.activeElementで取得した現在フォーカスされている(このFocusScope外で最後にフォーカスされた)要素を入れておき、記憶しておきます。
つまり、RFC に書かれていたように「FocusScope内で最後にフォーカスを持っていた要素をそのFocusScopeが記憶しておく」のではなく、「現在アクティブな(内側にフォーカスされている要素を持っている)FocusScopeが、その外で最後にフォーカスを持っていた要素を記憶しておく」ような実装になっているのだと理解しました。
例えばダイアログとそのトリガーボタンだと、トリガーボタンが押されてフォーカスがダイアログ内に移動したときに、トリガーボタンに最後にフォーカスがあったということをダイアログとトリガーボタンを囲っているFocusScopeが記憶しているのではなく、ダイアログだけを囲っているFocusScopeが新しく記憶し、そのFocusScopeが unmount されたタイミングでその記憶している要素にフォーカスを戻すようになっているということです。
こっそり追記しておいたのですが、Toast について - React Aria の実装読むぞの記事で言及していた疑問もこれで解消されました。

フォーカスの復元処理はここらへんでrestoreFocusToElement関数で行っているようです。

https://github.com/adobe/react-spectrum/blob/326f48154e301edab425c8198c5c3af72422462b/packages/%40react-aria/focus/src/FocusScope.tsx#L690-L726

また、FocusScopeコンポーネント内で、アクティブなFocusScopeの変更も行っています。

https://github.com/adobe/react-spectrum/blob/326f48154e301edab425c8198c5c3af72422462b/packages/%40react-aria/focus/src/FocusScope.tsx#L171-L176

useAutoFocus

auto focus を実現する hook です。

mount 時にgetFirstInScope関数を用いて、FocusScope内の最初の tabbable な要素にフォーカスします。なお、tabbable な要素が見つからなかったら最初の focusable な要素にフォーカスします。

https://github.com/adobe/react-spectrum/blob/326f48154e301edab425c8198c5c3af72422462b/packages/%40react-aria/focus/src/FocusScope.tsx#L507-L516

https://github.com/adobe/react-spectrum/blob/326f48154e301edab425c8198c5c3af72422462b/packages/%40react-aria/focus/src/FocusScope.tsx#L483-L499

useFocusManager

useFocusManagerは親のFocusScopeから context を受け取って色んなメソッドを実行できるようになっています。ここらへんで TreeWalker API を使って実装されています。

https://github.com/adobe/react-spectrum/blob/326f48154e301edab425c8198c5c3af72422462b/packages/%40react-aria/focus/src/FocusScope.tsx#L205-L268

focusgroupについて

本当は昨日の記事で書く予定だったのですが、書く時間がなかったのでこの記事で補足します。

Open UI に、focusgroupという HTML 属性の Proposal があります。これは現在 ref などを用いて Programmically にキーボード操作によるフォーカス移動をしているのを、HTML 属性だけで制御できるようにするというものです。詳しくは僕もまだ読めていないので、Open UI の Proposal や azukiazusa さんの記事をご覧ください。

https://open-ui.org/components/focusgroup.explainer/
https://azukiazusa.dev/blog/focusgroup-arrow-key-focus-navigation/

まとめ

明日の担当は @mehm8128 さんで、 ProgressBar についての記事です。お楽しみにー

Discussion