おはこんばんにちは(言ってみたかった)。
タブUIを作成する際にコンテンツをStoreで管理して表示させたら早いなーと思ってたんですが、そういえばWAI-ARIAあったなってなってロジックが思いっきり変わりました。笑
管理画面等ならまだしも、コーポレートサイトで使用するような場合にはアクセシビリティにもしっかり対応したいところですよね。誰にでも優しいサイトを作りたいですから!
WAI-ARIAの前置きが長いので実装だけ読みたい人は目次から飛ばしてください
環境
- Nuxt 2.14.0
そもそもWAI-ARIAとは
『わいありあ』って読むらしいです。
WAI-ARIA は W3C によって定められた仕様で、要素に適用できる追加の意味論を提供する一連の HTML 属性を定義しており、それが欠けているどのような場所でもアクセシビリティを向上させます。
WAI-ARIA の導入 – MDN
基本的にHTMLの補完としてWAI-ARIAがあり、複雑な要素の使い方をする場合にはアクセシビリティ向上のために追加で意味付けしてねってことですね。
こちらの記事にWAI-ARIAを使用するルールが簡潔にまとめられていてよかったのでご紹介します。
WAI-ARIA 実装の5つのルール
WAI-ARIAに対応させようと思ったらrole属性とaria属性の2種類になるのですが、機能としては以下の3つに分類されます。
ロール(Role)
role属性を付与したタグに役割や意味を与えることができる。レイアウトに関連するランドマークロールと、ランドマークロールより小さい構造を表す文書構造ロール、ユーザーインターフェースを表すウィジェットロールに分けられる。
そのうちランドマークロールには、role="navigation"
などが有名かと思いますが、<nav>
や<header>
などのHTML5のタグにはrole属性が暗黙的に含まれているので、きちんとタグを使用していれば使用しなくても問題はないとされています。
難しいことはこちらに書いてあります😇
2.1 WAI-ARIAロール – Accessible Rich Internet Applications (WAI-ARIA) 1.1 日本語訳
プロパティ(Property)
要素の性質や特性を定義する。
ステート(State)
要素の現在の状態を示す。性質・特性(property)を示す属性、ロールとの組み合わせで、ユーザーに『何が、いつ、どう』変化したのかを通知できる。
タブUIを作成する
前置きが長くなりましたが、早速。
今回は以下の記事を参考に、Nuxtでタブとタブパネルのコンポーネントを作成してview側からタブをコントロールしていきます。
WAI-ARIA対応のタブ型UIの作り方(Vue.js編)
WAI-ARIA属性使用のイメージ
何の属性を指定すればいいかすぐには覚えられないので自分への覚書きとして今回のタブUIで使用するWAI-ARIAの属性を画像にしときました。
Tabコンポーネントを作成する
components/Tab.vue
に下記のように記述しました。
propsで受け取るデータはビュー側で定義しているので、比較して確認してください。
個人的にはタブはリスト表示させないタイプなのでbuttonタグのみをv-forで回しています。
$emitに関しては後ほど。
CSSに関しては以下に賛同してaria属性をセレクターとして使用しました。
CSSの実装はCodeGridの記事「WAI-ARIAを活用したフロントエンド実装」で紹介されている「aria属性をCSSセレクターとして利用する」「独自に名前を付けるくらいなら、意味的に合致するaria属性を利用して、アクセシビリティーを確保しましょう」の提案をアイデアとしています。
WAI-ARIA対応のタブ型UIの作り方(Vue.js編)
<template>
<div role="tablist">
<button
v-for="tab in tabData"
:key="tab.id"
type="button"
role="tab"
:aria-controls="tab.id"
:aria-selected="tabState === tab.id"
@click="handleTabChange($event)"
v-text="tab.title"
/>
</div>
</template>
<script>
export default {
name: 'Tab',
props: {
tabData: {
type: Array,
required: true,
},
tabState: {
type: String,
required: true,
},
},
methods: {
handleTabChange(event) {
const element = event.currentTarget
const tabState = element.getAttribute('aria-controls')
this.$emit('onTabChange', tabState)
},
},
}
</script>
<style lang="scss" scoped>
[aria-selected="true"] {
background-color: royalblue;
color: white;
}
</style>
TabPanelコンポーネントを作成する
続いてcomponents/TabPanel.vue
を作成し、タブパネル用のパーツを作成します。
<template>
<div>
<div
v-for="panel in panelData"
:id="panel.id"
:key="panel.id"
role="tabpanel"
:aria-labelledby="panel.id"
:aria-hidden="tabState !== panel.id"
>
<p v-text="panel.content" />
</div>
</div>
</template>
<script>
export default {
name: 'TabPanel',
props: {
panelData: {
type: Array,
required: true,
},
tabState: {
type: String,
required: true,
},
},
}
</script>
<style lang="scss">
[aria-hidden='true'] {
display: none;
}
[aria-hidden='false'] {
display: block;
}
</style>
ビュー側のコード
TabコンポーネントとTabPanelコンポーネントを内包するpages/index.vue
を作成します。下記のように記述しました。
<template>
<div>
<Tab
:tab-data="tabData"
:tab-state="tabState"
@onTabChange="handleTabChange"
/>
<TabPanel :panel-data="tabData" :tab-state="tabState" />
</div>
</template>
<script>
const tabData = [
{
id: 'tab1',
title: 'タブ1',
content: 'コンテンツ1',
},
{
id: 'tab2',
title: 'タブ2',
content: 'コンテンツ2',
},
{
id: 'tab3',
title: 'タブ3',
content: 'コンテンツ3',
},
]
export default {
data() {
return {
tabState: '',
tabData,
}
},
created() {
this.tabState = this.tabData[0].id
},
methods: {
handleTabChange(tabState) {
this.tabState = tabState
},
},
}
</script>
tabState
というデータで現在のアクティブなタブIDを管理し、TabとTabPanelにpropsとして渡すことで各パーツの表示をコントロールています。
$emit
を使用すると子コンポーネントから親コンポーネントへデータを渡すことができるので、Tabコンポーネントで発生するクリックイベントからaria-controls
(連動しているタブパネルのid)の値を拾ってビュー側(index.vue
)に送ります。
ビュー側ではTabコンポーネントから受け取ったデータでtabStateを更新して、propsを通してTabとTabPanelコンポーネントに値を渡すことでタブが切り替わるようにCSSで調整しています。
なぜCSSでタブを切り替えるのか
こちらの実装ではTabPanelコンポーネントに表示するデータを全て渡して、CSSでアクティブなタブに連動した内容の表示を制御しているのですが、パフォーマンスを考えると最初から切り替える内容だけビューからTabPanelコンポーネントに送れば良くない?って思うかもしれません。(私のことですが)
しかし、その実装ではaria-controlsがa11y のチェッカーでエラーになるようです。
タブ切り替えを実装する時の注意点 – grgr-dkrkのブログ
aria-controls
に関わらず、id を参照するものは要素があらかじめ存在する必要がある。Accessibility Tree が DOM を元に生成する都合からだと思われる。
v-if
などでタブを切り替えてしまうとDOMからも消えてエラーになるので、一度タブパネルのデータを全てレンダリングしてからCSSで非表示にするということをしています。
最後に
タブパネル内のデータがめちゃめちゃ重かったりするとレンダリングのパフォーマンスにも影響が大きいと思うので、本当に重かったらデータをできるだけ分割させるとか、ページとして分割させるとかの工夫が必要になるかも?と思っています。
あとはタブにpropsで渡すデータも絞ったり、プロジェクトによってはリファクタしても良いと思います。
参考にしたサイト
- $emitで複数の値を親コンポーネントに渡す方法
- Accessible Rich Internet Applications (WAI-ARIA) 1.1
- WAI-ARIA対応のタブ型UIの作り方(Vue.js編)
- WAI-ARIAの基本 – MDN
- WAI-ARIAのrole属性一覧
- WAI-ARIA 実装の5つのルール
- タブ切り替えを実装する時の注意点 – grgr-dkrkのブログ