Cardのマークアップでもう悩まない!NestedでClickableなUIを実現する、Link Area Delegationとは

2024-12-16

目次

目次

  1. はじめに
  2. Link Area Delegationとは
  3. Link Area Delegation 提案の背景 - マークアップの制限
    1. クリッカブルなコンテナを実現する既存の方法
    2. 既存方法とその問題
  4. 現状検討されている4つの実装方法
    1. 1. Link delegation attributes
    2. 2. <linkarea> Element
    3. 3. Use CSS pointer-area Property
    4. 4. Use Invokers
  5. おわりに
    1. Appendix

はじめに

🎄 この記事はアクセシビリティ Advent Calendar 2024の16日目の記事です。

Open UI Community Groupで、仕様の策定が検討されているProposalの一つに、Link Area Delegationがあります。

本エントリでは、Link Area Delegationの仕様がどのような背景から検討され、何が可能となり、これから私たちがどのように利用していけるのかを紹介します。

Link Area Delegation(= リンク領域の委譲)Proposal は、コンテナがインタラクティブな領域を特定の要素に委譲することで、コンテナを「クリッカブル」にする機能を標準的に導入しようとする試みです。

例えば、提案されている手法のひとつである、linkarea属性defaultlink属性を使用すると、Link Area Delegationは以下のように表現されます。

以下の例では、linkarea属性のつく<div>をClickすることで、defaultlink属性を持つ最初の子孫要素、つまりこの場合は、最初の<a>が活性化されます。

<div class="card" linkarea>
  <a href="/post?id=123" defaultlink>Post Title</a>
  <img ...>
  <button>Join</button>
  <button>Share</button>
</div>

歴史的背景から、<a><button>のようなインタラクティブな要素をネストしたUIを実装することは、開発者にとって難しい課題でした。

インタラクティブな要素同士を子孫関係におくことができない旨は仕様に明記されており、例えば<a>のContent Modelであれば以下のように定義されています。

Transparent, but there must be no interactive content descendant, a element descendant, or descendant with the tabindex attribute specified.


透明、しかしインタラクティブなコンテンツの子孫、a要素の子孫、tabindex属性が指定された子孫が存在してはならない。 https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-a-element

<a>はインタラクティブな要素であるゆえ、<a>の中に<a>をネストすることはできません。<button>もインタラクティブな要素なので、<a>の中に<button>をネストすることもできませんし、その逆も然りです。

インタラクティブな要素 インタラクティブな要素 - 出典: HTML Standard - Kinds of content

もし仮に、以下のように<a>の中に<a>をネストした場合、それぞれは兄弟関係のリンクとしてパースされます。

<a href="#hoge">
    ここは親リンクのエリア
    <a href="#fuga"> 私は子のリンク </a>
    この中に#fugaへのリンクがある
</a>

兄弟関係のa tagになる 兄弟関係のa tagになる

このように、HTMLパーサは期待通りのインタラクティブな要素の解釈をしてくれないため、私たちはNested Linksを実現するために、さまざまなHackをしてきました。

クリッカブルなコンテナを実現する既存の方法

ネストされたインタラクティブな要素の代表的なユースケースとして、Cardコンポーネントが挙げられるでしょう。

例えば、本ブログの記事一覧ページでも、Cardコンポーネントを使用しています。

Card本体をホバー Card内tagをホバー
Card本体をホバーした時、記事へのURLに変わるCard本体をホバーした時、記事へのURLに変わる Card内のtagをホバーした時、tagのURLに変わるCard内のtagをホバーした時、tagのURLに変わる

このCardコンポーネントは、<a>タグのネスティングを使用せず、CSSのSubgridを使用して、クリッカブルなエリアを拡張しています。

実装を簡略化すると、以下のようなマークアップとスタイリングをすることで、上記のCardコンポーネントの実現が可能です。

<div class="card">
  <a class="link" href="https://blog.sakupi01.com/dev/articles/2024-openui-advent-12">
    <h2>🎄Open UI Advent Calendar: Day 12 / Customizable Select Element Ep.10</h2>
    <p>Customizable Select Elementの関連仕様: `::picker`のデフォルト色から深掘る、system-color/ color-scheme/ prefers-color-schemeの関係</p>
  </a>
  <p class="tags">
    <a href="https://blog.sakupi01.com/dev/tag/openui">openui</a>
    <a href="https://blog.sakupi01.com/dev/tag/advent%20calendar">advent calendar</a>
  </p>
</div>
.card {
  display: grid;
}
 
.link {
  grid-row: 1 / 3;
  grid-column: 1;
  display: grid;
  grid-template-rows: subgrid;
}
 
.tags {
  grid-row: 3 / 3;
  grid-column: 1;
}

しかし、CSSのSubgridが全てのブラウザで実装されたのは2023/9/15と比較的最近で、それ以前はさまざまなワークアラウンドが取られてました。

このSubgridの例も含め、HTMLのバリデーションを通過するようにしつつ、CSSやJavaScriptを使ってリンクやボタンのクリッカブルな領域を拡張する方法はさまざまにあります。こうしたHackyな方法は、標準化された方法ではなく、UXやアクセシビリティの観点からさまざまな問題を引き起こす可能性があります。

既存方法とその問題

「クリッカブル」なコンテナを実現するための既存方法とそのトレードオフを、もう少し細かく見てみましょう👀

<a>でコンテンツをラップする

クリック可能にしたいコンテンツ全体を<a>でラップする方法です。

e.g. 1
<a href="https://example.com">
  <div class="card">
    <h2>タイトル</h2>
    <p>コンテンツの説明</p>
    <button>詳細を見る</button>
  </div>
</a>
e.g. 2
<table>
  <tr>
    <a href="https://example.com">
      <td>hoge</td>
      <td>fuga</td>
    </a>
  </tr>
</table>
トレードオフ
  • ネストされたインタラクティブ要素は仕様に反する: 先にも述べたように、インタラクティブな要素をネストすることは、HTMLのContent Modelで禁止されています。
  • 冗長なアクセシブルネームになる: e.g.1 のようなマークアップをすると、リンクのアクセシブルネームが冗長になり、<h2><p><button>のテキストをすべて読み上げる可能性があります。これにより、支援技術を使うユーザーにとって理解しづらくなることがあります。

リンクに対して必要以上の情報を読み上げてしまう リンクに対して必要以上の情報を読み上げてしまう

  • 特定の要素でサポートされない: e.g. 2に示すような、<a>でテーブルの行<tr>をラップするマークアップは、HTMLの仕様で許可されていません。HTMLパーサはこのような構造を検出した場合に、リンクを削除してしまいます。

2. CSSでインタラクティブな要素の配置を調整する

重複したリンクやコンテナのアクションを ::before::after に配置し、コンテナサイズまで広げる方法です。

<div class="card">
  <a class="link" href="#title">
    <h1>タイトル</h1>
  </a>
  <p>コンテンツ</p>
  <p class="tags">
    <a href="#hoge">#hoge</a>
    <a href="#fuga">#fuga</a>
  </p>
</div>
.card {
  /* elevate the links up */
  position: relative;
  a {
    position: relative;
    z-index: 1;
  }
  .link {
    position: static;
    /* expand the pseudo-element to cover the post area */
    &::before {
      content: "";
      position: absolute;
      inset: 0;
    }
  }
  .tags a {
    position: relative;
  }
}
トレードオフ
  • 標準化されていない: 実装方法が標準化されておらず、重複したリンクがキーボードやスクリーンリーダーユーザーに冗長さを生むことがあります。
  • テキストの選択が難しい: CSSにより、リンクで静的テキストがラップされているので、カード内部のテキストを選択できません。

3. JavaScriptでクリックをキャプチャする

JavaScriptを使って、コンテナ要素がクリックをキャプチャし、メインリンクに委譲する方法です。

<div class="card" onclick="handleClick(event)">
  <h2>タイトル</h2>
  <p>コンテンツの説明</p>
  <a>詳細を見る</a>
</div>
 
<script>
  function handleClick(event) {
    if (!event.target.closest('a')) {
      window.location.href = 'https://example.com';
    }
  }
</script>
トレードオフ
  • JavaScriptの使用: JSの使用を避けたい開発者にとっては、大きなトレードオフとなります。
  • UXの問題: mouosedonwnでの活性化や、ネストされたリンクの考慮漏れにより、支援技術での操作保証が難しいです。
  • ブラウザデフォルトの動作との競合: Context Menuなどと競合する可能性があります。

Link Area Delegationは、こうしたワークアラウンドによる問題を解決するために、インタラクティブな要素のネスティングに関する新しいガイドラインや標準を設けることを目指しています。

現状検討されている4つの実装方法

上記を満たすLink Area Delegationの提案として、現時点では以下の4つの実装方法が検討されています。

HTMLで表現する方法の一つとして、linkarea属性とdefaultlink属性を使用する方法が提案されています。

冒頭の例と重複しますが、以下のようにlinkarea属性を持つ<div>をクリックすることで、defaultlink属性を持つ最初の子孫要素が活性化されます。つまり、linkarea属性は、クリッカブルな領域を拡張するためのコンテナを示し、defaultlink属性は、そのコンテナのクリックイベント委譲先となります。

<div class="card" linkarea>
  <a href="/post?id=123" defaultlink>Post Title</a>
  <img ...>
  <button>Join</button>
  <button>Share</button>
</div>

defaultlink属性を持つ最初の子孫要素だけではなく、IDによってlinkarea属性を持つコンテナと紐づけられた、特定の子孫要素を活性化することも可能です。

以下の例では、最初の<a>がページ内パーマリンクとして機能し、コンテナクリックで発火されるアクションは「Learn More」のリンクになります。

<section class="card" linkarea="card123-cta">
  <h2><a href="#topic-123" id="card123-title">Post Title</a></h2>
  <img ... alt="">
  <p>...</p>
  <a href="/post?id=123" id="card123-cta" aria-labelledby="card123-cta card123-title">Learn More</a>
  <button>Join</button>
  <button>Share</button>
</section>

この提案が採用された場合、ARIA in HTML仕様に新しいルールを追加し、この属性が使用される要素に許可される役割を制限する必要があるかもしれません。例えば、<div role=button linkarea>のような場合、現在ではdivに任意のARIAロールを設定できますが、このインスタンスでは何らかの検証メッセージを生成するべきだと述べられています。

2. <linkarea> Element

linkarea属性の代わりに、<linkarea>を導入する方法も提案されています。

<section class="card">
  <linkarea>
    <a href="/post?id=123" defaultlink>Post Title</a>
    <img ...>
    <button>Join</button>
    <button>Share</button>
  </linkarea>
</section>

しかし、HTML要素の柔軟性を考えると、この方法は望ましくありません。

例えば、以下のようなマークアップを実現するには、<tr>が新しい要素タイプを含むよう、HTMLパーサを変更する必要があり、ブラウザ側からすると、基本的には避けたい実装です。また、全てのブラウザで<linkarea>実装によるHTMLパーサ変更の実装が完了しない限り、<tr>のContent Modelに反して挙動が壊れる可能性があり、段階的なサポートが難しいです。

<table>
  <tr>
    <linkarea>
      <th><a href="/details?id=1" defaultlink>Title</a></th>
      <td>...</td>
      <td>...</td>
      <td><button>Edit</button></td>
    </linkarea>
  <tr>
</table>

一方、属性を用いた手法では、UAはポリフィルを通じて段階的に新しい動作をサポートするよう実装でき、パースの問題を軽減できます。

3. Use CSS pointer-area Property

HTMLではなくCSSを使用する方法も考えられています。HTML属性の代わりに、新しいCSSプロパティを提供することで、クリッカブルな領域を拡張する方法です。

<section class="card">
  <a href="/post?id=123">Post Title</a>
  <img ...>
  <button>Join</button>
  <button>Share</button>
</section>
.card { pointer-area: contain; }
.card a:nth-child(1) { pointer-area: expand; }

この方法では、事前に与えられたHTMLの構造にCSSを適用することでクリッカブルな領域が決定されます。つまり、動的にHTML構造が変更された場合は対応が複雑になる可能性があります。

4. Use Invokers

現在、Open UIで別Proposalとして検討が進んでいる、Invoker Commandsを使用する方法も提案されています。

commandでは、clickcontextmenutouchkeypressなどのCommandEventをDispatchし、commandforでは、CommandEventを受け取るIDを持つ要素との関連付けをします。

<section class="card" commandfor="card123-title" command="click">
  <a href="/post?id=123" id="card123-title">Post Title</a>
  <img ...>
  <button>Join</button>
  <button>Share</button>
</section>

この手法を使用するには、属性を2つ宣言する必要があり、かつIDを頻繁に宣言する必要も出てくる可能性があります。 とはいえ、この手法を用いると、クリックを「リンク」以外の要素、例えば、<button>に委譲することも可能なため、前向きに検討されているようです。

おわりに

今回は、Link Area Delegationの全体像について、広く浅く紹介しました。

筆者自身が、以前カードコンポーネントを実装する際、Nested Linksを実現したく、その時に読んだSara Soueidanの記事がとても勉強になりました。

Nested Links Without Nesting Links
– The personal website of Sara Soueidan, inclusive design engineer
Nested Links Without Nesting Links favicon https://www.sarasoueidan.com/blog/nested-links/
Nested Links Without Nesting Links

その後、Link Area DelegationがOpen UIで提案され、自分したHackyな実装を標準的な方法で解決する仕様策定が検討されていることを知って興味をもっていたのが、本エントリ執筆の背景です。

Open UI Advent Calendarでは、Customizable Select Element を中心に、さらに詳しくOpen UIに関する記事を書いているので、そちらも読んでいただけると嬉しいです🤸🏻‍♀️

Thank you for reading!⛄

Appendix

Copyright © 2024 saku 🌸 All rights reserved.