WAI-ARIA 対応のアクセシブルなタブ UI を React で実装する
Posted: Updated:
タブ UI のアクセシビリティ対応
この記事は、下記の点に留意してご覧いただきたい。
- タブ UI におけるアクセシビリティ対応(主に WAI-ARIA)の参考実装であること
- React コンポーネントとしての機能性は二の次のサンプルであること
- アクセシビリティ実装についてのツッコミは歓迎であること
ひさびさにPCキーボード的なアクセシビリティ対応が必要そうな性質のサービスを開発することになったので、コンポーネント単位でのアクセシビリティ関連実装を進めている。
React 実装
今回はタブ UI を題材に、React コンポーネントを実装した。
行うべき操作を見通しよくするために、タブ UI を <Tabs>
コンポーネント1つで実装した。厳密には <TabList>
や <TabPanel>
などの要素を分解してコンポーネント化したほうが良いだろうが、今回は簡易実装としている。
キーボード操作への対応
- タブキーを押すと「選択中のタブ」→「タブコンテンツ」の順にフォーカスが移る
- 「選択中のタブ」の上で「→」または「←」の矢印キーを押すとタブが切り替わる
動作サンプル
下の CodePen 埋め込みが動作サンプルだ。
See the Pen react-a11y-tabs by Ayumu Sato (@ahomu) on CodePen.
その他の情報提供
コンポーネント内の要素についている [aria-*]
の属性を適宜更新することによって、各要素の状態についてユーザーエージェントに情報を提供している。
aria-selected
による選択中であるかどうかのフラグaria-controls
による何の要素をコントロールしているかの識別子aria-hidden
による表示中であるかどうかのフラグ
.is-active
や .is-hidden
といったスタイル操作用の class と同様のタイミングで操作すべき「状態」を示す属性が多い。.is-*
と併用するか、属性セレクタによるスタイル指定に抵抗がなければ [aria-*]
だけで運用してもよいだろう。
Tabs コンポーネントの実装
コンポーネント本体の実装コードに軽くコメントしたものを以下に貼った。CodePen が Babel に対応していたのでいつもどおり ECMAScript 6 で書けて満足している。
createClass
スタイルの React に親しみが薄いので・・・
const KEYCODE_LEFT = 37; const KEYCODE_RIGHT = 39; class Tabs extends React.Component { constructor() { super(); this.state = { index: 0 }; } updateIndex(i, fn) { // index が更新されて DOM が更新されたのちフォーカスをあてる this.setState({index: i}, () => { React.findDOMNode(this.refs['tab' + i]).focus(); }); } onClickTab(i) { this.updateIndex(i); } onMoveTab(e) { // ここでは index の更新に専念すればいい let curtIndex = this.state.index; switch(e.keyCode) { case KEYCODE_LEFT: if (curtIndex !== 0) { this.updateIndex(curtIndex - 1); } break; case KEYCODE_RIGHT: if (curtIndex !== this.props.children.length - 1) { this.updateIndex(curtIndex + 1); } break; default: break; } } render() { let curtIndex = this.state.index; // タブ部分を生成する、ラベルは child.props.label を使う // aria-* 系の属性は、React らしく index を使った条件式を宣言するだけでよい // あとからフォーカスをあてるのに ref を仕込んでおいた方がラクだった let tabs = this.props.children.map((child, i) => { return ( <li role="presentation" key={i}> <button role="tab" ref={'tab' + i} tabIndex={curtIndex === i ? '0' : '-1'} aria-selected={curtIndex === i ? 'true' : 'false'} aria-controls={child.props.id} onKeyUp={this.onMoveTab.bind(this)} onClick={this.onClickTab.bind(this, i)}> {child.props.label} </button> </li> ); }); // パネル部分も同様に生成する let panels = this.props.children.map((child, i) => { return ( <div role="tabpanel" key={i} id={child.props.id} aria-hidden={curtIndex === i ? 'false' : 'true'}> {child} </div> ); }); return ( <div> <ul role="tablist"> {tabs} </ul> {panels} </div> ); } }
感想など
今回 React で書いてみたところ、JSX 上に tabIndex={curtIndex === i ? '0' : '-1'}
のような条件を書き込んでおけば、state なり props を更新するだけで現在の状態を気にせず WAI-ARIA の属性が書き換わるのでラクだった。
コンポーネントの細かい状態は管理しなくても、パラメータさえ更新すれば自動でアトミックな更新がかかる React の良さがあらわれていたように思う。
React と属性操作の相性が良いのでオススメできる
これまで WAI-ARIA の対応で面倒だったのは、ことあるごとに要素の状態を取得したり保持する部分や、頻繁な DOM API へのアクセスが強いられる部分だ。その点、JSX でもコード量は確かに増えるが、複雑さはないのでスパゲティ化の原因にはなりそうにない。
jQuery で同じことをやれと言われたら少々暗い気持ちになるが、React なら相性が良いと思うので簡単な UI から試しにやってみると良いだろう。