無障礙網頁元件

網頁功能: ARIA 屬性反射 Shadow DOM

Shadow 邊界會改變語意和 ARIA 的運作方式。主要有兩種失敗情況:Shadow Tree 內缺少語意,以及 ARIA 參照因無法跨越 Shadow Root 而自動中斷。

從語意標記開始:在範本中使用實際的 <button>、<nav>、<ul>,這樣大部分語意都會自動產生,並保留 ElementInternals/ARIA 用於填補空白。

透過 ElementInternals ARIA mixin 設定預設語意

透過 ElementInternals 在 JS 中設定自訂元素的角色和 ARIA 狀態,而不是在主機上寫入 ARIA 屬性。這樣一來,元件就能保留預設語意,同時讓使用者透過 Light DOM 中的屬性覆寫語意。

class ToggleSwitch extends HTMLElement {
  #internals;
  constructor() {
    super();
    this.#internals = this.attachInternals();
    this.#internals.role = 'switch';          // default role, no host attribute needed
    this.#internals.ariaChecked = 'false';    // exposed to the accessibility tree
    this.addEventListener('click', () => this.toggle());
    this.addEventListener('keydown', (e) => {
      if (e.key === ' ') { e.preventDefault(); this.toggle(); }
    });
  }
  connectedCallback() {
    // Put the host in the tab order on connect (setting tabIndex in the constructor throws on createElement).
    if (!this.hasAttribute('tabindex')) this.tabIndex = 0;
  }
  toggle() {
    const on = this.#internals.ariaChecked !== 'true';
    this.#internals.ariaChecked = String(on);
  }
}
customElements.define('toggle-switch', ToggleSwitch);

這樣可以避免 ARIA 屬性汙染 DOM,並防止使用者不小心覆寫這些屬性,同時無障礙樹狀結構仍會看到正確的角色和狀態。

焦點管理和 delegatesFocus

  • 當主機元素本身是互動式控制項,並透過 ElementInternals 攜帶角色和狀態 (如上方的 ToggleSwitch) 時,請將主機放在 connectedCallback 內的 Tab 鍵順序中 (if (!this.hasAttribute('tabindex')) this.tabIndex = 0 - 絕不要放在建構函式中,否則會在 document.createElement 上擲回),這樣焦點就會落在主機上,而輔助技術會宣告其角色和狀態。請勿在靜態、非互動式元件 (例如摘要或資訊卡) 的主機上設定 tabindex。
  • 當元件包裝擁有語意的內部可聚焦控制項時,請使用 delegatesFocus: true 附加 Shadow Root (或在具有 static formAssociated = true 的表單關聯自訂元素上)。聚焦主體 (並點選表單關聯元素的關聯 <label>) 接著會將焦點轉送至第一個可聚焦的子項,且 :focus 會與主體相符。請勿將 delegatesFocus: true 與主機上的角色/狀態合併,否則焦點會落在內部元素上,並略過主機的狀態。
  • 如要移動焦點或將焦點移至新顯示的區域,請為目標提供 tabindex="-1" 並呼叫 .focus();請明確指定可透過程式輔助方式聚焦的目標,而非依賴分頁順序。

跨 Shadow Root 的 ARIA 關係

  • 強制:aria-labelledby、aria-describedby、aria-controls 等參照的 id 只能在參照元素「本身」的樹狀結構中解析。Light DOM 中以 id 為基礎的屬性無法指向 Shadow Tree 內的 id,反之亦然;系統會忽略參照,且不會顯示任何訊息。
  • 如要跨越邊界,請使用反映的 ARIA 元素屬性 (例如 ariaLabelledByElements、ariaDescribedByElements 等),這些屬性會採用直接 JS 元素參照,而非 ID。它們會向外解析:shadow tree 內的元素可以指向 Light DOM 或任何祖系樹狀結構 (包括插槽內容) 中的目標,因此您可以從 Light DOM 內容標示 shadow 內部控制項,這是 id 參照無法表達的。
  • 但不會向內解析:元素無法參照後代 shadow tree (例如主體無法由自己的 shadow 內容標示)。新興的參考目標機制是標準方向,但尚未廣泛推出。
  • 如果無法以任何一種方式表示關係,請將關係保留在一個樹狀結構中 (在同一個樹狀結構中顯示標籤),或使用 aria-label 文字,而非 id 參照。

備用策略

如果無法使用 ElementInternals ARIA mixin,請改為主機上的屬性設定角色和 ARIA 狀態 (this.setAttribute('role', 'switch'))。這種做法適用於所有情況,但代價是將屬性放在 DOM 中,而使用者可能會覆寫這些屬性。

如果無法使用反映的 ARIA 元素屬性,請使用以 id 為基礎的 aria-* 屬性來建立同樹狀結構關係;跨邊界標籤沒有以 id 為基礎的對應屬性,因此請改用 aria-label 文字。