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 ではどんなトーストでもalert
role になり、さらにそれを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 を見るとalert
role も同様に、緊急度の高い場合のみ使うべきということが書かれています。
しかし、useToast
で表示するようなトーストは必ずしも緊急を要する情報であることは想定されていないですし、少なくともドキュメントのページのデモではモーダルにもなっていません。よって、代わりにstatus
role を利用するべきだと考えています。
Chakra-UI で用いられている zag-ui だとここで role がstatus
になっています。
Vercel のデザインシステムも、このページでトーストを表示してみるとstatus
role であることが分かります。
他にも調べたのがちょっと前なのでどのデザインシステムだったか忘れてしまったのですが、error toast だけalert
role、それ以外はstatus
role、と分けているデザインシステムもありました。
ここらへんの PR が部分的に関係していそうなのですが、時間がなくて調査しきれませんでした。
live region
alert
role を使うと、暗黙的にaria-live="assertive"
がつくので更新が通知されます。polite
とは違い、現在のタスクを中断してでも緊急で情報を伝えてきます。
キーボード操作
useToastRegion
でトースト全体のコンテナーにregion
role をつけて 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