Toastについて - React Ariaの実装読むぞ
こんにちは、フロントエンドエンジニアの mehm8128 です。
今日は Toast について書いていきます。
useToastとは
トーストを表示するための hook です。
使用例
ドキュメントからそのまま取ってきています。
function ToastProvider({ children, ...props }) {
let state = useToastState({
maxVisibleToasts: 5,
});
return (
<>
{children(state)}
{state.visibleToasts.length > 0 && (
<ToastRegion {...props} state={state} />
)}
</>
);
}
function ToastRegion<T extends React.ReactNode>({
state,
...props
}: ToastRegionProps<T>) {
let ref = React.useRef(null);
let { regionProps } = useToastRegion(props, state, ref);
return (
<div {...regionProps} ref={ref} className="toast-region">
{state.visibleToasts.map((toast) => (
<Toast key={toast.key} toast={toast} state={state} />
))}
</div>
);
}
本題
APG はこちらです。
role について
React Aria ではどんなトーストでもalertrole になり、さらにそれをalertdialogのコンテナーで囲うような形になっているのですが、これは実装がよくないと思っています。
以下の画像が、ドキュメントのページでトーストを表示したときの DOM の画像です。

MDN のalertdialogのページでは以下のような記載があります。
alertdialog ロールは、ユーザーの即時の注意を要する緊急情報をユーザーに通知するために使用されます。
その緊急性のために、アラートダイアログは常にモーダルでなければなりません。
また、WAI-ARIA にも同様のことが書かれています。
Content authors SHOULD make alert dialogs modal by ensuring that, while the alertdialog is shown, keyboard and mouse interactions only operate within the dialog.
MDN を見るとalertrole も同様に、緊急度の高い場合のみ使うべきということが書かれています。
しかし、useToastで表示するようなトーストは必ずしも緊急を要する情報であることは想定されていないですし、少なくともドキュメントのページのデモではモーダルにもなっていません。よって、代わりにstatusrole を利用するべきだと考えています。
Chakra-UI で用いられている zag-ui だとここで role がstatusになっています。
Vercel のデザインシステムも、このページでトーストを表示してみるとstatusrole であることが分かります。
他にも調べたのがちょっと前なのでどのデザインシステムだったか忘れてしまったのですが、error toast だけalertrole、それ以外はstatusrole、と分けているデザインシステムもありました。
ここらへんの PR が部分的に関係していそうなのですが、時間がなくて調査しきれませんでした。
live region
alertrole を使うと、暗黙的にaria-live="assertive"がつくので更新が通知されます。politeとは違い、現在のタスクを中断してでも緊急で情報を伝えてきます。
キーボード操作
useToastRegionでトースト全体のコンテナーにregionrole をつけて landmark にしていて、そこで使っているuseLandmarkによってF6でトーストにフォーカスできるようになっています。
F6がどこからきているのか分からなかったのですが、discussion にありました。
F6を用いてこのような操作ができることってスクリーンリーダー利用者や日常的にキーボード操作をする人たちの間では有名なんですかね。
フォーカス操作
状況に応じて色々フォーカスを操作しているので、簡単にまとめます。
フォーカスしているトーストが消えたら(消されたら)、その次のトーストか、なければ前のトーストにフォーカスが移動します。
また、トーストが 1 つもなくなったらF6を用いて移動してきたときの、元々フォーカスしていた要素にフォーカスを戻します。
数日後の記事で紹介する(本当は今日紹介する予定だったけど延期にした)FocusScopeの focus restore が効かないからここで実装してい るとのことなのですが、なんで効かないのか分かりませんでした...。まだ ます。追記: 読みました。FocusScopeの実装を読めていないので、読んだら分かるかもです。
FocusScopeは mount するタイミングでフォーカスされている要素をdocument.activeElementで取得するので、useToastRegionを使っているコンポーネント(ToastRegion)が mount するタイミングでしかフォーカスの復元先を更新しません。つまり、一回ToastRegionに入って再度元の場所に戻り、別の要素からF6キーで入ったとしてもToastRegionが mount したタイミングでフォーカスされていた要素が記憶されている状態のままということです。よって、lastFocusedを別で保持しておき、unmount するタイミングでそちらにフォーカスを戻すようにしています。
ちなみにlastFocusedはuseFocusWithinで、この前紹介したe.relatedTargetを用いて取得しています。
まとめ
明日の担当は @mehm8128 さんで、 Menu についての記事です。お楽しみにー
Discussion