🧑‍🏫

WAI-ARIA中級編 ロールの決まり方

2022/12/14に公開

前回の「WAI-ARIAを学ぶときに整理しておきたいこと」(以下、「前回の記事」と略)やYouTubeで生配信した「WAI-ARIA勉強会」で

  • 要素には暗黙のロールをもつ
  • role属性を使うことでロールを上書きできる
  • 要素によって上書きできないロールがある

と説明した。

しかし、上書きできないロールをrole属性で指定してしまったときに最終的にロールが何になるかの説明をしていなかったし、要素の条件次第によっては上書き可能なはずのロールが無効化されたり、ロールが変化したりすることに触れていなかった。今回はそこが仕様でどうなっているのか深堀りしてみたいと思う。

ロールの決定に影響するもの

まず、簡単にロールがどういったものから決定されるのかをざっくりと最初にまとめる。

  • 要素の種類(HTMLとしてのセマンティック)
  • 属性
  • 構造(先祖・子孫関係)

これをまず念頭に置いて、仕様を読みすすめると理解がしやすいと思う。ひとまずは複雑なことを予め覚悟して読み進めていきたい。

暗黙のロールはどう決まるのか

まずは暗黙のロールが定義されているARIA in HTMLを確認していこう。

属性による暗黙のロール

仕様を見てみると早速、属性が関係していることがわかるだろう。「a with href」「a without href」という記述があるように、href属性をもつa要素と、それを持たないa要素で判定される暗黙のロールが異なる。

<a href="path/to"><!-- ロールは link --></a>

<a><!-- ロールは generic --></a>

img要素はさらにややこしい。

  • altなし
  • altが空
  • altにテキストが入っている(つまり空ではない)

の3つ条件があり、「altなし」と「altにテキストが入っている(つまり空ではない)」はロールがimgになり、altが空はロールがpresentationになる。

<img src="path/to" /><!-- ロールは img -->

<img src="path/to" alt="" /><!-- ロールは presentation -->

<img src="path/to" alt="何かしらのテキスト" /><!-- ロールは img -->

属性そのものの有無だけでなく、値によってもロールは変わるということだ。

値によってロールが変わることについては、input要素はわかりやすい。type属性によってロールが変化する。チェックボックスであればcheckboxロールになるし、ラジオボタンであればradioロールになる。

ちなみに、

input type=text or with a missing or invalid type, with no list attribute

という条件の with a missing or invalid type というのは「type属性がない」もしくは「type属性に無効なデタラメの値がある」の状態をことを指していて、HTMLにも定義されているとおり、属性がなかったり属性値が無効だった場合はtype=textと同じ扱いになるため、type=textと同じ条件となっている。

構造による暗黙のロール

さらに見ていくと、先祖要素によってロールが変わるものがあることもわかる。header要素の暗黙のロールは次のように定義されている。

If not a descendant of an article, aside, main, nav or section element, or an element with role=article, complementary, main, navigation or region then role=banner

Otherwise, role=generic

否定文から始まっているので非常にややこしいんだけど、つまり言い換えると、

  • 基本的にはbannerロール
  • ただし祖先にarticleasidemainnavsection要素、またはrole属性にarticlecomplementarymainnavigationregionをもつ要素があればgenericロール

ということだ。

<body>
  <header><!-- ロールは banner --></header>
</body>

<body>
  <article>
    <header><!-- ロールは generic --></header>
  </article>
  <div role="article">
    <header><!-- ロールは generic --></header>
  </div>
</body>

td要素は3パターンある。

role=cell if the ancestor table element is exposed as a role=table

role=gridcell if the ancestor table element is exposed as a role=grid or treegrid

No corresponding role if the ancestor table element is not exposed as a role=table, grid or treegrid

  • 先祖のtable要素がtableロール(暗黙のロールなのでrole属性なしの場合も)だったらcellロール
  • 先祖のtable要素がgridロールもしくはtreegridロールだったらgridcellロール
  • 先祖のtable要素が上記以外だったらロールなし
<table>
  <tr>
    <td><!-- ロールは cell --></td>
  </tr>
</table>

<table role="grid">
  <tr>
    <td><!-- ロールは gridcell --></td>
  </tr>
</table>

<table role="treegrid">
  <tr>
    <td><!-- ロールは gridcell --></td>
  </tr>
</table>

<table role="presentation">
  <tr>
    <td><!-- ロールは なし --></td>
  </tr>
</table>

th要素も同様の3パターンなのだが、結果が role=columnheader, rowheader or gridcell と曖昧になってるところが現仕様として困りどころだ。

おそらくは「thead要素を親にもつ」とか「scope属性による」だったりの条件が明記されているのが望ましいのだけどARIA in HTMLには言及がない。WAI-ARIA 1.2columnheaderの説明にはBase Conceptとして、 <th[scope="col"]> in [HTML] と記載があるので、まあそういうことなんだろうとは思うんだけど、ちゃんと厳密に記載して両仕様で足並みを揃えて欲しいところだ。

アクセシブルな名前の有無による暗黙ロール

section要素は、アクセシブルな名前を持てばregionロールになり、持たなければロールはない。

<section aria-label="なにかしらのテキスト"><!-- ロールは region --></section>

<section aria-labelledby="heading-id">
  <!-- ロールは region -->
  <h2 id="heading-id">なにかしらのテキスト</h2>
</section>

<section><!-- ロールは なし --></section>

さて、ここまでは要素がもつ暗黙のロールであくまでも基本。本番はここからだ。

role属性による明示的なロールの決まり方

基本的にはrole属性に明示的にロールを指定すれば、暗黙のロールを上書きすることになるが、以下の条件では指定が無視される。

仕様にないロールは無視される

これは言わずもがな。当然、仕様にないロールは明示しても意味がない。

<div role="my-role"><!-- ロールは generic --></div>

抽象ロールは無視される

これは「前回の記事」でも説明したとおり。ロールには抽象ロールに属するものがあるので注意が必要。

<div role="section"><!-- ロールは generic --></div>

上書きできないロールは無視される

これも「前回の記事」で言及しているが、より詳しく説明すると、これは「ARIA in HTML」の ARIA roles, states and properties which MAY be used の列を参照することになる。

ここでも暗黙のロールと同様、要素の条件によって使用できるロールが決まっている。

a with href ではbuttonをはじめとしたいくつかのロールしか認められていないが、a without href では全てのロールが認められている。

<a href="path/to" role="button"><!-- ロールは button --></a>

<a href="path/to" role="alert"><!-- ロールは link (alertは認められない) --></a>

<a role="alert"><!-- ロールは alert --></a>

アクセシブルな名前をもたない場合に無視されるロールがある

Handling Author Errorsという解説にWAI-ARIA 1.3[1]で追記された新しい仕様がある。

Certain landmark roles require names from authors. In situations where an author has not specified names for these landmarks, it is considered an authoring error. The user agent MUST treat such elements as if no role had been provided. If a valid fallback role had been specified, or if the element had an implicit ARIA role, then user agents would continue to expose that role, instead. Instances of such roles are as follows:

  • form
  • region

これは、formregionロールは明示してもアクセシブルな名前を持たなければ無視されますよ、という仕様だ。

<div role="form"><!-- ロールは generic --></div>

<div role="form" aria-label="何かしらのテキスト"><!-- ロールは form --></div>

暗黙のformロールをもっているfrom要素の場合どうなのかというと、現行のARIA in HTMLform要素には A form is not exposed as a landmark region unless it has been provided an accessible name. という注釈があり、アクセシブルな名前がなければ実質ロールが機能しないことを示している。(ロールが機能しないことと、ロールが無視されることの差はなんなのか曖昧なところは、もっと厳密に決定して欲しいところ)

どのみち、ロールには「アクセシブルな名前が必須」となる場合があり、formregionはどちらもそうなので、仕様に順当に従えば気にしなくてよい細かい仕様かもしれない。

必須の構造を満たしていない場合にロールが消失することがある

ロールにはRequired Context Roleの条件をもっているものがあり、たとえばlistitemロールの場合、listdirectoryロールを親にもっていないといけない。

For example, an element with role listitem is only meaningful when contained inside (or owned by) an element with role list.

only meaningful という言葉を使っていて、ロールがgenericになったりロールなしになるとは言及されていないので解釈はブラウザ次第になるが、いくつかのブラウザではロールが消失したり、アクセシビリティツリーに要素そのものが公開されないケース[2]があるので注意が必要だ。

<ul>
  <li><!-- ロールは listitem --></li>
</ul>

<ul role="generic">
  <li>
    <!-- ロールが消失したり、アクセシビリティツリーに公開されない場合がある -->
  </li>
</ul>

<ul role="tablist">
  <li>
    <!-- ロールが消失したり、アクセシビリティツリーに公開されない場合がある -->
  </li>
</ul>

<div>
  <li>
    <!-- ロールが消失したり、アクセシビリティツリーに公開されない場合がある -->
  </li>
</div>

ちなみに、table要素にrole="presentation"を指定することで子孫要素の意味を打ち消すテクニックを使うことがあるけど、その場合この仕様が作用しているとも言えるし、ARIA in HTMLの暗黙のロールの条件「先祖のtable要素がtablegridtreegrid以外のロールだったらロールなし」という仕様が作用しているとも言える。このあたり、各仕様が絶妙に辻褄をあわせているところは仕様を読んでいて面白い。

指定しても無視されるpresentation

少し特殊なロールとしてpresentationロールがあるが、このロール固有で無視されるケースがあることがWAI-ARIA 1.2Presentational Roles Conflict Resolutionで決められている。

presentationロールはアクセシビリティツリーに要素を公開しないロールで、つまりrole="presentation"と記述すれば、支援技術からみたら何もない状態にすることができる。

presentationロールの使い所としては

<ul role="tablist">
  <li role="presentation"><button role="tab">Apple</button></li>
  <li role="presentation"><button role="tab">Orange</button></li>
  <li role="presentation"><button role="tab">Banana</button></li>
</ul>

のように、tablisttabの親子関係の間にある要素を透明にするような場合に効果的に利用できる。

フォーカス可能であればpresentationは無視される

If an element is focusable, or otherwise interactive, user agents MUST ignore the presentation role and expose the element with its implicit role, in order to ensure that the element is operable.

ごくごくシンプル。フォーカスできたりインタラクティブであれば、明示的にpresentationを指定しても無視され、暗黙のロールにフォールバックされる。

理由は「ユーザーが操作できるのに、公開されてないのはおかしいでしょ?」ということ。

<a href="path/to" role="presentation"><!-- ロールは link --></a>

<button role="presentation"><!-- ロールは button --></button>

<input type="text" role="presentation" /><!-- ロールは textbox -->

<div tabindex="0" role="presentation"><!-- ロールは generic --></div>

ただしWAI-ARIAはHTMLのみに言及できる仕様ではないため、HTMLの各要素のどれがフォーカス可能またはインタラクティブ要素かどうかには明記していない(interactive = コンテンツ・モデルがインタラクティブコンテンツの要素とは明言していない)。ブラウザがどう解釈してどう実装されるかはブラウザ次第になる。この曖昧さに関してはIssueが作られていて、まだクローズされていない。

また、サンプルコードに記載はしたものの、ARIA in HTMLではhrefをもつa要素も、button要素も、type="text"をもつinput要素も、そもそもpresentationで上書きできないことになっているので、その仕様に則っても無視されて暗黙のロールにフォールバックされるのは同じである。ここもARIA in HTMLWAI-ARIAで矛盾が発生しないようになっている。

継承されたpresentationが無視される

前提の知識としてWAI-ARIARequired Owned Elementという仕様がある。これは、たとえば「listロールの子はlistitemでなければならない」といった構造上のルールだ。

<ul>
  <!-- ul のロールは list なので -->
  <li>
    <!-- 子となる li のロールは listitem でなければならない。ここでは違反していない。 -->
  </li>
</ul>

そして、このRequired Owned Elementにおいてpresentationロールは継承される性質を持つ。

<ul role="presentation">
  <!--
    ul のロールは list で元々 Required Owned Element をもつ
    この ul 自身のロールは presentation
  -->
  <li>
    <!--
      親が presentation となった Required Owned Element なので
      ロールは presentation
    -->
  </li>
</ul>

そして、そのpresentationが無視される条件がある。

If a required owned element has an explicit non-presentational role, user agents MUST ignore an inherited presentational role and expose the element with its explicit role. If the action of exposing the explicit role causes the accessibility tree to be malformed, the expected results are undefined.

  • Required Owned Elementの要素
  • presentationを継承している
  • 明示的なロール(presentationでない)をもつ

の3つの条件が揃えば、継承されたpresentationは無視されて、明示的なロールが適用される。ただし、これの結果、構造が不正になる場合はロールが未定義になってしまう。

う〜ん、この仕様は何度読んでも難しい。そもそもpresentationを継承しているということはRequired Owned Elementの要素ということになるし、親がpresentationの状態ということはRequired Context Roleから構造の不正にになるので、どう考えても未定義にしかなりえないと思うんだけど…何か重要な仕様を見逃してるのかもしれない…。※ご指摘待ってます。

グローバルWAI-ARIAプロパティをもつとpresentationは無視される

If an element has global WAI-ARIA states or properties, user agents MUST ignore the presentation role and expose the element with its implicit role.

要素がグローバルなWAI-ARIAプロパティをもつのなら、presentationロールは無視されて要素は公開される。

<div role="presentation" aria-busy="false"><!-- ロールは generic --></div>

<div role="presentation" aria-live="polite"><!-- ロールは generic --></div>

However, if an element has only non-global, role-specific WAI-ARIA states or properties, the element MUST NOT be exposed unless the presentational role is inherited and an explicit non-presentational role is applied.

説明には続いて上記のように書かれている。逆に非グローバルでロール固有のプロパティのみを持つ場合、そのままpresentationロールとなる、ということだが、グローバルプロパティもってないんだからそりゃそうでしょ、といった説明だ[3]

Presentational Childrenの要素はロールが無意味

ロールにはChildren Presentationalという性質をもち、いくつかのロールはこれがtrueとなっている。

このPresentational Childrenがどういうものかというと、たとえばbuttonロールのChildren Presentationalの性質はtrueで、このロールの子孫要素はアクセシビリティツリーに公開されない、というもの。一見不思議に思うかもしれないが、buttonロールはアクセシブルな名前をコンテンツから計算するので、中身自体がツリーから消えても問題ない。

<button>何かしらのテキスト</button>
<!--
  button要素の計算されたアクセシビリティプロパティは
  Role: button
  name: 何かしらのテキスト (👈 アクセシブルな名前をしっかり持っている)
  focusable: true
-->

具体的にこれの何がロールに影響を与えるかというと、button要素にテキストだけでなく要素を入れるとわかりやすい。

<button>
  <img
    src="path/to"
    alt="何かしらのテキスト"
  /><!-- img要素はアクセシビリティツリーに公開されない -->
</button>
<!--
  ただし、button要素の計算されたアクセシビリティプロパティは
  Role: button
  name: 何かしらのテキスト (👈 計算されてalt属性から抽出される)
  focusable: true
-->

このようにimg要素だろうがなんだろうが、buttonロールをもつ要素に何を入れても公開されない。

つまり、button要素の中で明示的にロールを指定したりしても、機能しないということだ。

<button>
  <div role="alert">メッセージ</div>
  <!-- div要素はアクセシビリティツリーに公開されない -->
</button>
<!--
  ただし、button要素の計算されたアクセシビリティプロパティは
  Role: button
  name: メッセージ (👈 計算されてdivのコンテンツから抽出される)
  focusable: true
-->

ブラウザと支援技術の確認を忘れずに

以上が把握している限りのロールの決まり方なのだけど、これはあくまでも仕様の話。実際はブラウザの実装次第な部分は多く、さらにそのブラウザと支援技術との組み合わせでも挙動は変わるので、そのマークアップが期待通りかどうかは実際に動作させてみて確認するしかない。

最後に

なぜ、この記事を書いたかというと、もちろんWAI-ARIAの理解を深めるためもそうだけど、MarkuplintWAI-ARIAのルールを作る上でこのあたりの調査をしたからだ。まだv2.xでは中途半端な実装しかできていなかったが、近日リリース予定のv3.0.0ではこのあたりをかなり強化した。すでにv3.0.0-rcはプレリリースしていて試すことができるし、また、興味があればどういった実装をしたのかソースコード見てもらえると嬉しい。この記事に書いたことのほとんどがJavaScriptで書かれている。

つまり、この記事の変なところがあった場合にご指摘いただけるとMarkuplintも改善される、という寸法です。ご協力よろしくお願いします!

脚注
  1. まだ草案 ↩︎

  2. ロールをnoneとして計算していて結果的に公開されない状態になるケースも確認された。 ↩︎

  3. 仕様ってへんなところでやけに詳しく冗長に書かれていることがある。 ↩︎

GitHubで編集を提案

Discussion