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 から試しにやってみると良いだろう。