t28.dev

アクセシブルな Vue コンポーネントを実装するための `useId()` を作る

2023/10/16
Tech

Tab に渡す ID を作りたい

「アクセシブルな Tabs を実装するには適切な ID を使用する」と、W3C の ガイド (Example of Tabs with Automatic Activation) が言っています1

<div>
  <div role="tablist">
    <button
      id="tab-1 👈👈👈 これ"
      type="button"
      role="tab"
      aria-selected="true"
      aria-controls="tabpanel-1"
    >
      タブボタンの中身
    </button>
  </div>
  <div
    id="tabpanel-1 👈👈👈 これ"
    role="tabpanel"
    tabindex="0"
    aria-labelledby="tab-1"
  >
    タブパネルの中身
  </div>
</div>

Vue で Tabs を実装するとき、tabtabpanel は動的に構築されるので id 属性も動的に構築したくなります

Tabs を表現する配列 の index を活用して tab-0, tab-1 … というid属性を定義する手段が考えられますが、 id属性は document 全体で一意である必要がある ので複数の Tabs を定義するときに属性値が衝突してしまいます。

tab の 表示名 を使えば index (数字) より衝突するケースも減るんじゃない?…という考え方もあると思いますが、 MDN ではASCII 文字、数字、’_’、’-’ のみを使用することを推奨していますし、根本解決になってないです。

Tabs を使う側としては a11y のためにコンポーネント内部で使う id 属性を意識したくないので 「tabpanel 内でいい感じに ID を採番してくれないかな〜」 って気持ち。

Vue の UI フレームワークではどうしているの?

Vuetify 3

Vuetify3 の Tabs では、role="tab" 要素に id 属性を設定していない?

And Design Vue

Ant Design Vue の Tabs では、role="tab" 要素に id="rc-tabs-0-tab-1" が設定されている。

id="rc-tabs-0-tab-1" に相当する実装が TabNode.tsx#L99 にあります。 これに渡す id の実態はいくつかの UI コンポーネントを挟んで、Tabs.tsx#L270 で定義されています。

面倒なので色々すっ飛ばすと、 モジュールスコープで定義された変数を onMounted のたびにインクリメントして、Tabs を使用するとき (Mount するとき) に重複していない整数値を取得する仕組みにしていました。

// Used for accessibility
let uuid = 0;

// (略)

onMounted(() => {
  if (!props.id) {
    setMergedId(`rc-tabs-${process.env.NODE_ENV === "test" ? "test" : uuid}`);
    uuid += 1;
  }
});

// (略)

Chakra UI

Chakra UI の Tabs では、role="tab" 要素に id="tabs-:r1:--tab-0 が含まれている。

role="tab" 要素に渡す id 属性は use-tabs.ts#L317 で定義されています。 この id 属性を作る makeTabId() が受け取る引数の id は 面倒なので色々すっ飛ばしてuse-tabs.ts#L153-L155 で構築されたものが Context 経由で渡されるものです。

const uuid = useId();
const uid = props.id ?? uuid;
const id = `tabs-${uid}`;

useId()React が提供している useId で、これで一意の ID を取得しています。

UI ライブラリ側の ID 採番機能状況

React

useId is a React Hook for generating unique IDs that can be passed to accessibility attributes.

https://react.dev/reference/react/useId

A11y のための ID 採番機能が提供されている。 便利っすね…。

Vue

一方 Vue の方は Discussion (Can vue provide useId hook #557) が作られているけれど、進展はなさそう。 じゃあライブラリは?ってことで vueuse を見てみると、close された issue (useUid - generate unique id for a Vue instance #465)があった。

<script setup>
import { v4 as uuid } from "uuid";
const id = uuid();
</script>

ってするだけだから要らんやろ…っていう結論だった。

結論: 自分で hook を作ろうね

let internalId = 0; // ant design vue を参考に module scope の変数を定義する

export const useId = (): string => {
  internalId += 1;
  return `:${internalId}:`; // react を参考に `:` を挟んだ ID 文字列を作る
};
<template>
  <!-- 属性値いろいろ省略 -->
  <div>
    <div role="tablist">
      <button :id="`${tabsId}-0 👈これ`" type="button" role="tab">
        タブボタンの中身
      </button>
    </div>
    <div role="tabpanel" :aria-labelledby="`${tabsId}-0 👈これ`">
      タブパネルの中身
    </div>
  </div>
</template>
<script lang="ts" setup>
import { useId } from "./useId";
let tabsId = useId(); // 👈 これ
</script>

Footnotes

  1. 原文ではそうは言ってないけれど、説明文とサンプルがそう言ってる。