ARIA-Barriers

混合状態のチェックボックス

2024年10月20日

@nishimotz です。

ウェブアクセシビリティLT&交流会 vol.4 に参加して、交流会で話したことを、あとで気になって調べてみたのが今回の記事です。

混合状態(部分的にチェック済み)のチェックボックスは、あるオプションが複数のサブオプションを統括しているような場合に便利な UI 要素です。

しかし、アクセシビリティを確保するためには、適切な実装が必要だとされています。

さて、どうしたらいいのでしょうか。

APGの実装

ARIA Authoring Practices Guide (APG) Checkbox Example (Mixed-State) の実装を見てみましょう。

テキストだけ日本語に翻訳しました。

<fieldset class="checkbox-mixed">
  <legend>
    サンドイッチの具材
  </legend>
  <div role="checkbox"
       class="group_checkbox"
       aria-checked="mixed"
       aria-controls="cond1 cond2 cond3 cond4"
       tabindex="0">
    すべての具材
  </div>
  <ul class="checkboxes">
    <li>
      <label>
        <input type="checkbox" id="cond1">
        レタス
      </label>
    </li>
    <li>
      <label>
        <input type="checkbox"
               id="cond2"
               checked="">
        トマト
      </label>
    </li>
    <li>
      <label>
        <input type="checkbox" id="cond3">
        マスタード
      </label>
    </li>
    <li>
      <label>
        <input type="checkbox" id="cond4">
        スプラウト
      </label>
    </li>
  </ul>
</fieldset>

APGの実装で重要なARIA属性は以下の通りです。

JavaScriptでは、チェックボックスの状態を管理し、aria-checked属性を適切に更新する必要があります。状態は以下の通りです。

以下のキーボード操作に対応しています。

APGの実装では、OSのハイコントラストモードを考慮してCSSでカスタマイズを行っています。 ここでは深入りしません。

以下はこのコードの一部を取り出したデモです。操作はできませんが、NVDA が「一部チェック」と読み上げます。

改善したつもりなのに

混合状態のチェックボックスを、div 要素ではなく、input 要素で実装してみます。

<fieldset class="checkbox-mixed">
  <legend>
    サンドイッチの具材
  </legend>
  <label>
    <input 
      type="checkbox"
      role="checkbox"
      class="group_checkbox"
      aria-checked="mixed"
      aria-controls="cond1 cond2 cond3 cond4"
      tabindex="0">
    すべての具材
  </label>
  <!-- 省略 -->
</fieldset>

なんとなく、このほうがよさそうだな、と思いましたか?

どうして APG はこうしなかったのでしょう?

ためしにこれを NVDA と Chrome で読み上げてみましょう。

aria-checked=”mixed” という属性があるのに「チェックボックス チェックなし すべての具材」になりました。

もうすこしよく考えてみましょう。

強いネイティブセマンティクス

HTML標準のチェックボックス (input) 要素には、以下の値があります。

もちろん div 要素にはこれらの属性はありません。

checked プロパティはチェックの有無を表します。値は true または false となり、JavaScript からは boolean 値を代入できます。HTML では checked 属性が存在する場合に true となります。このコードでは true の意味で checked="" と書かれています。

indeterminate プロパティは混合状態を表します。 aria-checked 属性の mixed に相当する場合に true となり、それ以外の場合に false となります。HTML としては indeterminate 属性は存在しません。

checked や indeterminate は「強いネイティブセマンティクス」を持つとされ、aria-checked 属性よりも優先されます。

role 属性とは、aria-* 属性とは、WAI-ARIA とは、いったい何なのか、いつ使うべきなのか

だから div 要素にはただ

とするだけで混合状態を表現できたのですが、input 要素の場合は

と解釈され、indeterminate の値が優先されて、混合状態を表現できなかったようです。

HTML の属性では制御できないとのことなので JavaScript を直さないといけませんね。

でも input 要素でもともと混合状態がサポートされているのであれば、もしかして WAI-ARIA はいらないのでは?

標準的な要素を使用した実装

標準的なチェックボックス要素を使用しつつ、混合状態を表現する方法を考えてみましょう。

MDN input 要素の checkbox 型 には「未決定状態のチェックボックス」として説明されています。

<fieldset class="checkbox-mixed">
  <legend>サンドイッチの具材</legend>
  <label>
    <input type="checkbox"
           class="group_checkbox"
           id="all-ingredients"
           aria-controls="cond1 cond2 cond3 cond4">
    すべての具材
  </label>
  <ul>
    <li>
      <label>
        <input type="checkbox" id="cond1" name="ingredient" value="レタス">
        レタス
      </label>
    </li>
    <li>
      <label>
        <input type="checkbox" id="cond2" name="ingredient" value="トマト" checked="">
        トマト
      </label>
    </li>
    <li>
      <label>
        <input type="checkbox" id="cond3" name="ingredient" value="マスタード">
        マスタード
      </label>
    </li>
    <li>
      <label>
        <input type="checkbox" id="cond4" name="ingredient" value="スプラウト">
        スプラウト
      </label>
    </li>
  </ul>
</fieldset>
const allCheckbox = document.getElementById('all-ingredients');
const ingredientCheckboxes = document.querySelectorAll('input[name="ingredient"]');

function updateAllCheckbox() {
  const checkedCount = document.querySelectorAll('input[name="ingredient"]:checked').length;
  if (checkedCount === 0) {
    allCheckbox.checked = false;
    allCheckbox.indeterminate = false;
  } else if (checkedCount === ingredientCheckboxes.length) {
    allCheckbox.checked = true;
    allCheckbox.indeterminate = false;
  } else {
    allCheckbox.checked = false;
    allCheckbox.indeterminate = true;
  }
}

allCheckbox.addEventListener('change', () => {
  ingredientCheckboxes.forEach(cb => cb.checked = allCheckbox.checked);
});

ingredientCheckboxes.forEach(cb => {
  cb.addEventListener('change', updateAllCheckbox);
});

window.addEventListener('load', () => {
  updateAllCheckbox();
});

aria-checked を使わないデモ

サンドイッチの具材

aria-controls 属性

ところで aria-controls属性がスクリーンリーダーによって適切にサポートされている場合、以下が期待されます。

ただし、例えばNVDAは aria-controls 属性をまだサポートしていません

aria-controls 属性はアクセシビリティ サポーテッドではないかも知れないですね。

やっぱり less ARIA is better

Accessible Rich Internet Applications (WAI-ARIA) 1.3 日本語訳 checkboxロール の注には、

HTMLのネイティヴチェックボックスの強いネイティヴセマンティックスのために、著者はinput type=checkboxでaria-checkedを使用しないことを勧める。むしろ、チェックボックスの”チェック済み”または”混合”状態をそれぞれ指定するために、ネイティヴのchecked属性またはindeterminate IDL属性を使用する。

と説明されていました。

APG の実装例は、なにげなく書かれているようで、実はちょっとアレンジしただけで前提が成り立たなくなることがあると気づきました。注意が必要です。

さらに言えば、APG の実装例は WAI-ARIA をあえて使っている例ばかりです。可能であれば「WAI-ARIA を使用しない」ほうがよいはずです。

HTML の要素そのものを正しく理解し、HTML を適切に使用し、不要な WAI-ARIA 属性は削除することをお勧めします。