LoginSignup
4
2

【アクセシビリティ】アクセシビリティを意識したツリーリストの作り方

Last updated at Posted at 2023-12-20

はじめに

みなさんアクセシビリティを意識して開発できていますか?

必要なところにrole属性を記述したり、tabキーでフォーカスができるようにしたりなど、意識しないといけないことも多いです。
そのため、アクセシビリティを完璧にやろうとするのは一苦労です。

ただ、コンポーネントごとに区切って、アクセシビリティを理解しておけば、実装するタイミングに思い出しやすく、アクセシビリティも意識しやすいと思います。

そのため、この記事では「ツリーリスト」に焦点を当てて、アクセシビリティを意識したツリーリストの実装方法とツリーリストで意識した方がいいアクセシビリティを解説しようと思います。

アクセシビリティを意識したツリーリストの仕様

⚪︎ ツリーリストとは?

ツリーリストは、階層的なリストのコンポーネントです。
ツリーリストの階層内のアイテムは子アイテムを持ち、子アイテムを持つアイテムは展開と折りたたみをすることで子アイテムを表示・非表示することができます。
また、キーボードを使用してツリーをナビゲートする場合、視覚的にどのアイテムにフォーカスがあるかをユーザーに伝えます。

ツリーリストには、以下の2つの種類があります。

  • シングルセレクトツリー
    • ユーザーのアクションのために1つのアイテムを選択させるツリー
    • シングルセレクトツリーの実装では、フォーカスされたアイテムが選択状態になる
  • マルチセレクトツリー
    • ユーザーがアクションのために複数のアイテムを選択できるツリー
    • マルチセレクトツリーの実装では、フォーカスと選択状態は、別でそれぞれが独立している
    • 選択されているアイテムとフォーカスのあるアイテムは視覚的に区別する必要がある

ツリーリストを説明する用語は以下の通りです。

  • Node(ノード)
    • ツリー内のアイテム
  • Root Node(ルート ノード)
    • 親ノードを持ってない、ツリーの基部にあるアイテム
  • Child Node(子ノード)
    • 親ノードを持つツリーのアイテム
  • End Node(エンド ノード)
    • 子ノードを持たないツリーのアイテム
  • Parent Node(親ノード)
    • 1つ以上の子ノードを持つツリーのアイテム
  • Open Node(オープン ノード)
    • 子ノードが見えるように展開されている親ノード
  • Close Node(クローズ ノード)
    • 子ノードが見えないように折りたたまれた親ノード

⚪︎ キーボードインタラクション

  • シングルセレクトツリーがフォーカスを受け取った時
    • ツリーがフォーカスを受け取る前にどのノードも選択されていない場合、フォーカスは最初のノードに移動する
    • ツリーがフォーカスを受け取る前にノードが選択されている場合、フォーカスは選択されたノードに移動する
  • マルチセレクトツリーがフォーカスを受け取った時
    • ツリーがフォーカスを受け取る前にどのノードも選択されていない場合、フォーカスは最初のノードに移動する
    • ツリーがフォーカスを受け取る前に一つ以上のノードが選択されている場合、フォーカスは最初に選択されたノードに移動する
  • →キー
    • フォーカスが閉じているノードにある場合、ノードを開き、フォーカスは移動しない
    • フォーカスが開いているノードにある場合、最初の子ノードにフォーカスを移動する
    • フォーカスがエンドノードにある場合、何もしない
  • ←キー
    • フォーカスが開いているノードにある場合、ノードを閉じる
    • フォーカスが子ノードにあり、そのノードがエンドノードや閉じているノードの場合、親ノードにフォーカスを移動する
    • フォーカスがルートノードにあり、そのノードがエンドノードや閉じているノードの場合、何もしない
  • ↓キー
    • ノードを開閉せずに、次のフォーカス可能なノードにフォーカスを移動する
  • ↑キー
    • ノードを開閉せずに、前のフォーカス可能なノードにフォーカスを移動する
  • Homeキー
    • ノードを開閉せずに、ツリーの最初のノードにフォーカスを移動する
  • Endキー
    • ノードを開閉せずに、ツリーの最後のノードにフォーカスを移動する
  • Enterキー
    • ノードをアクティブにする
    • 親ノードの場合、デフォルトアクションは、ノードを開閉すること
    • フォーカスに従わない選択のシングルセレクトツリーのデフォルトアクションは、フォーカスされたノードを選択すること
  • 検索サジェスト
    • 7つ以上のルートノードを持つツリーリストで実装することが推奨される
    • 文字を入力した場合
      • 入力した文字で始まる名前の次のノードにフォーカスが移動する
    • 連続して複数文字を入力した場合
      • 入力された文字列で始まる名前の次のノードにフォーカスが移動する
  • *キー(任意)
    • 現在のノードと同じレベルにあるすべての兄弟ノードを展開する
  • マルチセレクトツリーの選択について
    +
    • 推奨する選択キーボードアクション
      • フォーカスを移動する際に修飾キーを押す必要がないキーボードアクション
      • Spaceキー
        • フォーカスされたノードの選択状態を切り替る
      • Shiftキー + ↓キー(任意)
        • 次のノードにフォーカスを移動し、その選択状態を切り替る
      • Shiftキー + ↑キー(任意)
        • 前のノードにフォーカスを移動し、その選択状態を切り替る
      • Shiftキー + Spaceキー(任意)
        • 最近選択したノードから現在のノードまでの連続するノードを選択する。
      • Controlキー + Shiftキー + Homeキー(任意)
        • フォーカスがあるノードと最初のノードまでのノードを選択する
        • 最初のノードにフォーカスを移動する(任意)
      • Controlキー + Shiftキー + Endキー(任意)
        • フォーカスがあるノードと最後のノードまでのノードを選択する
        • 最後のノードにフォーカスを移動する(任意)
      • Controlキー + Aキー(任意)
        • ツリー内のすべてのノードを選択する
        • すべてのノードが選択されている場合は、すべてのノードの選択を解除する(任意)
    • 代替用の選択キーボードアクション
      • ShiftやControl修飾キーを押さずにフォーカスを移動すると、フォーカスされたノード以外の選択されたノードの選択が解除されるキーボードアクション
      • Shiftキー + ↓キー
        • 次のノードにフォーカスを移動し、その選択状態を切り替る
      • Shiftキー + ↑キー
        • 前のノードにフォーカスを移動し、その選択状態を切り替る
      • Controlキー + ↓キー
        • 選択状態を変更せずに、次のノードにフォーカスを移動させる
      • Controlキー + ↑キー
        • 選択状態を変更せずに、前のノードにフォーカスを移動させる
      • Controlキー + Spaceキー
        • フォーカスされたノードの選択状態を切り替える
      • Shiftキー + Spaceキー(任意)
        • 最近選択されたノードから現在のノードまでの連続するノードを選択する
      • Controlキー + Shiftキー + Homeキー(任意)
        • フォーカスがあるノードと最初のノードまでのノードを選択する
        • 最初のノードにフォーカスを移動する(任意)
      • Controlキー + Shiftキー + Endキー(任意)
        • フォーカスがあるノードと最後のノードまでのノードを選択する
        • 最後のノードにフォーカスを移動する(任意)
      • Controlキー + Aキー(任意)
        • ツリー内のすべてのノードを選択する
        • すべてのノードが選択されている場合は、すべてのノードの選択を解除する(任意)

⚪︎ WAI-ARIA の役割、状態、プロパティ

  • すべてのツリーノードは、role="tree" を持つ要素に含まれているか、role="tree" を持つ要素に所有されている
  • ツリーノードとして機能する各要素には、role="treeitem" を設定する
  • 各ルートノードは、role="tree" を持つ要素に含まれているか、tree要素に設定した aria-owns で指定する
  • 各親ノードは、role="group" を持つ要素を含んでいるか、各親ノードに、role="group" を設定する
  • 各子ノードは、その子ノードの親ノードに含まれるか、その子ノードが所有している role="group" を持つ要素に含まれる
  • 親ノードとして機能する role="treeitem" を持つ各要素は、ノードが閉じた状態にあるときは aria-expanded="false" を設定する
    • 開いた状態にあるときは aria-expanded="true" を設定する
    • エンドノードには aria-expanded を設定しない
  • マルチセレクトツリーの場合、role="tree" 持つ要素には aria-multiselectable="true" を設定する
    • マルチセレクトツリーでない場合は、 aria-multiselectable="false" を設定するか、aria-multiselectable を設定しない
  • 選択可能な各ノードの選択状態は、aria-selectedaria-checked を設定する
    • 選択状態を aria-selected で設定する場合、どのノードにも aria-checked は設定しない
    • 選択状態が aria-checked で設定する場合、どのノードにも aria-selected は設定しない
    • 選択されたノードには、aria-selected="true" か aria-checked="true" を設定する
      • aria-multiselectable が設定していない場合、一度に選択されるノードは1つ
    • 選択可能で選択されていないノードは、aria-selected="false" か aria-checked="false" を設定する
    • ツリーに選択できないノードがある場合、選択できないノードには、aria-selectedaria-checked は設定しない
    • フォーカスに従って選択されるツリーを除き、選択された状態はフォーカスとは別であることに注意する
  • role="tree" を持つ要素には、aria-labelledby で参照している可視ラベルか、aria-label でラベルを指定する
  • ツリーをスクロールすることによって動的に読み込まれるなどで、完全なDOMが存在しない場合、各ノードに aria-levelaria-setsizearia-posinset を設定する
  • ツリー要素が水平方向に配置されている場合、aria-orientation="horizontal" を設定する
    • ツリーの aria-orientation のデフォルト値は vertical

アクセシビリティを意識したツリーリストの完成形

See the Pen Tooltip Accessibillity by でぐぅー | Qiita (@sp_degu) on CodePen.

アクセシビリティを意識したツリーリストの作り方

1. HTMLを実装する

sample.html
<ul role="tree" aria-labelledby="tree_label">
  <li role="treeitem" aria-expanded="false" aria-selected="false">
    <span>プロダクト</span>
    <ul role="group">
      <li role="treeitem" aria-selected="false" class="doc">Qiita.docx</li>
      <li role="treeitem" aria-selected="false" class="doc">Qiita Team.docx</li>
      <li role="treeitem" aria-selected="false" class="doc">Qiita Jobs.docx</li>
    </ul>
  </li>
  <li role="treeitem" aria-expanded="false" aria-selected="false">
    <span>プロジェクト</span>
    <ul role="group">
      <li role="treeitem" aria-selected="false" class="doc">プロジェクト1.docx</li>
      <li role="treeitem" aria-selected="false" class="doc">プロジェクト2.docx</li>
    </ul>
  </li>
  <li role="treeitem" aria-expanded="false" aria-selected="false">
    <span>チーム</span>
    <ul role="group">
      <li role="treeitem" aria-selected="false" class="doc">チームA.docx</li>
      <li role="treeitem" aria-selected="false" class="doc">チームB.docx</li>
      <li role="treeitem" aria-selected="false" class="doc">チームC.docx</li>
    </ul>
  </li>  
</ul>

2. CSSを実装する

sample.css
body {
  background-color: #212529;
  color: #fff;
  display: grid;
  gap: 40px;
  height: calc(100vh - 40px);
  margin: 0;
  padding: 20px 0;
  place-items: center;
  width: 100vw;
}

ul[role="tree"] {
  margin: 0;
  backdrop-filter: blur(50px);
  background-color: rgb(128 128 128 / .3);
  background-blend-mode: luminosity;
  border-radius: 32px;
  list-style: none;
  max-width: 400px;
  padding: 24px;
  position: relative;
  width: 100%;
  &::before {
    background: linear-gradient(135deg, rgb(255 255 255 / .4) 0, rgb(255 255 255 / 0) 40%, rgb(255 255 255 / 0) 60%, rgb(255 255 255 / .1) 100%);
    border: 1.4px solid transparent;
    border-radius: 32px;
    content: "";
    inset: 0;
    position: absolute;
    -webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0) border-box;
    -webkit-mask-composite: destination-out;
    mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0) border-box;
    mask-composite: exclude;
    z-index: -1;
  }
}

ul[role="tree"] li {
  margin: 0;
  padding: 0;
  list-style: none;
}

[role="treeitem"][aria-expanded="false"] > ul {
  display: none;
}

[role="treeitem"][aria-expanded="true"] > ul {
  display: block;
}

[role="treeitem"] > span {
  border-radius: 8px;
  display: block;
  font-size: 16px;
  font-weight: 600;
  padding: 8px;
  &:hover, &:focus, &:active {
    background: radial-gradient(34.12% 136.61% at 50% 100%, rgba(94, 94, 94, 0.14) 0%, rgba(94, 94, 94, 0.00) 73.85%), radial-gradient(50% 164.29% at 50% 100%, rgba(255, 255, 255, 0.07) 0%, rgba(255, 255, 255, 0.00) 60.33%), linear-gradient(0deg, rgba(94, 94, 94, 0.18) 0%, rgba(94, 94, 94, 0.18) 100%), rgba(255, 255, 255, 0.06);
    background-blend-mode: color-dodge, normal, color-dodge, lighten;
  }
}

ul[role="group"] {
  padding-left: 32px;
}

ul[role="group"] > [role="treeitem"] {
  border-radius: 8px;
  display: block;
  font-size: 16px;
  padding: 8px;
  &:hover, &:focus, &:active {
    background: radial-gradient(34.12% 136.61% at 50% 100%, rgba(94, 94, 94, 0.14) 0%, rgba(94, 94, 94, 0.00) 73.85%), radial-gradient(50% 164.29% at 50% 100%, rgba(255, 255, 255, 0.07) 0%, rgba(255, 255, 255, 0.00) 60.33%), linear-gradient(0deg, rgba(94, 94, 94, 0.18) 0%, rgba(94, 94, 94, 0.18) 100%), rgba(255, 255, 255, 0.06);
    background-blend-mode: color-dodge, normal, color-dodge, lighten;
  }
}

[role="treeitem"][aria-expanded="false"] > span::before {
  content: "folder";
  font-family: "Material Symbols Outlined";
  font-size: 18px;
  font-variation-settings:
    'FILL' 0,
    'wght' 400,
    'GRAD' 0,
    'opsz' 24;
  margin-right: 8px;
}

[role="treeitem"][aria-expanded="true"] > span::before {
  content: "folder_open";
  font-family: "Material Symbols Outlined";
  font-size: 18px;
  font-variation-settings:
    'FILL' 0,
    'wght' 400,
    'GRAD' 0,
    'opsz' 24;
  margin-right: 8px;
}

[role="treeitem"].doc::before {
  content: "description";
  font-family: "Material Symbols Outlined";
  font-size: 18px;
  font-variation-settings:
    'FILL' 0,
    'wght' 400,
    'GRAD' 0,
    'opsz' 24;
  margin-right: 8px;
}

3. JavaScriptを実装する

sample.js
window.addEventListener('load', function () {
  var trees = document.querySelectorAll('[role="tree"]');

  for (var i = 0; i < trees.length; i++) {
    var t = new Tree(trees[i]);
    t.init();
  }
});

var Tree = function (node) {
  if (typeof node !== 'object') {
    return;
  }

  this.domNode = node;

  this.treeitems = [];
  this.firstChars = [];

  this.firstTreeitem = null;
  this.lastTreeitem = null;
  this.selectedItem = null;
};

Tree.prototype.init = function () {
  function findTreeitems(node, tree, group) {
    var elem = node.firstElementChild;
    var ti = group;

    while (elem) {
      if (elem.tagName.toLowerCase() === 'li') {
        ti = new Treeitem(elem, tree, group);
        ti.init();
        tree.treeitems.push(ti);
        tree.firstChars.push(ti.label.substring(0, 1).toLowerCase());
      }

      if (elem.firstElementChild) {
        findTreeitems(elem, tree, ti);
      }

      elem = elem.nextElementSibling;
    }
  }

  if (!this.domNode.getAttribute('role')) {
    this.domNode.setAttribute('role', 'tree');
  }

  findTreeitems(this.domNode, this, false);

  this.updateVisibleTreeitems();

  this.firstTreeitem.domNode.tabIndex = 0;
};

Tree.prototype.setSelectedToItem = function (treeitem) {
  if (this.selectedItem) {
    this.selectedItem.domNode.setAttribute('aria-selected', 'false');
  }
  treeitem.domNode.setAttribute('aria-selected', 'true');
  this.selectedItem = treeitem;
};

Tree.prototype.setFocusToItem = function (treeitem) {
  for (var i = 0; i < this.treeitems.length; i++) {
    var ti = this.treeitems[i];

    if (ti === treeitem) {
      ti.domNode.tabIndex = 0;
      ti.domNode.focus();
    } else {
      ti.domNode.tabIndex = -1;
    }
  }
};

Tree.prototype.setFocusToNextItem = function (currentItem) {
  var nextItem = false;

  for (var i = this.treeitems.length - 1; i >= 0; i--) {
    var ti = this.treeitems[i];
    if (ti === currentItem) {
      break;
    }
    if (ti.isVisible) {
      nextItem = ti;
    }
  }

  if (nextItem) {
    this.setFocusToItem(nextItem);
  }
};

Tree.prototype.setFocusToPreviousItem = function (currentItem) {
  var prevItem = false;

  for (var i = 0; i < this.treeitems.length; i++) {
    var ti = this.treeitems[i];
    if (ti === currentItem) {
      break;
    }
    if (ti.isVisible) {
      prevItem = ti;
    }
  }

  if (prevItem) {
    this.setFocusToItem(prevItem);
  }
};

Tree.prototype.setFocusToParentItem = function (currentItem) {
  if (currentItem.groupTreeitem) {
    this.setFocusToItem(currentItem.groupTreeitem);
  }
};

Tree.prototype.setFocusToFirstItem = function () {
  this.setFocusToItem(this.firstTreeitem);
};

Tree.prototype.setFocusToLastItem = function () {
  this.setFocusToItem(this.lastTreeitem);
};

Tree.prototype.expandTreeitem = function (currentItem) {
  if (currentItem.isExpandable) {
    currentItem.domNode.setAttribute('aria-expanded', true);
    this.updateVisibleTreeitems();
  }
};

Tree.prototype.expandAllSiblingItems = function (currentItem) {
  for (var i = 0; i < this.treeitems.length; i++) {
    var ti = this.treeitems[i];

    if (ti.groupTreeitem === currentItem.groupTreeitem && ti.isExpandable) {
      this.expandTreeitem(ti);
    }
  }
};

Tree.prototype.collapseTreeitem = function (currentItem) {
  var groupTreeitem = false;

  if (currentItem.isExpanded()) {
    groupTreeitem = currentItem;
  } else {
    groupTreeitem = currentItem.groupTreeitem;
  }

  if (groupTreeitem) {
    groupTreeitem.domNode.setAttribute('aria-expanded', false);
    this.updateVisibleTreeitems();
    this.setFocusToItem(groupTreeitem);
  }
};

Tree.prototype.updateVisibleTreeitems = function () {
  this.firstTreeitem = this.treeitems[0];

  for (var i = 0; i < this.treeitems.length; i++) {
    var ti = this.treeitems[i];

    var parent = ti.domNode.parentNode;

    ti.isVisible = true;

    while (parent && parent !== this.domNode) {
      if (parent.getAttribute('aria-expanded') == 'false') {
        ti.isVisible = false;
      }
      parent = parent.parentNode;
    }

    if (ti.isVisible) {
      this.lastTreeitem = ti;
    }
  }
};

Tree.prototype.setFocusByFirstCharacter = function (currentItem, char) {
  var start, index;

  char = char.toLowerCase();

  start = this.treeitems.indexOf(currentItem) + 1;
  if (start === this.treeitems.length) {
    start = 0;
  }

  index = this.getIndexFirstChars(start, char);

  if (index === -1) {
    index = this.getIndexFirstChars(0, char);
  }

  if (index > -1) {
    this.setFocusToItem(this.treeitems[index]);
  }
};

Tree.prototype.getIndexFirstChars = function (startIndex, char) {
  for (var i = startIndex; i < this.firstChars.length; i++) {
    if (this.treeitems[i].isVisible) {
      if (char === this.firstChars[i]) {
        return i;
      }
    }
  }
  return -1;
};

var Treeitem = function (node, treeObj, group) {
  if (typeof node !== 'object') {
    return;
  }

  node.tabIndex = -1;
  this.tree = treeObj;
  this.groupTreeitem = group;
  this.domNode = node;
  this.label = node.textContent.trim();

  if (node.getAttribute('aria-label')) {
    this.label = node.getAttribute('aria-label').trim();
  }

  this.isExpandable = false;
  this.isVisible = false;
  this.inGroup = false;

  if (group) {
    this.inGroup = true;
  }

  var elem = node.firstElementChild;

  while (elem) {
    if (elem.tagName.toLowerCase() == 'ul') {
      elem.setAttribute('role', 'group');
      this.isExpandable = true;
      break;
    }

    elem = elem.nextElementSibling;
  }

  this.keyCode = Object.freeze({
    RETURN: 13,
    SPACE: 32,
    PAGEUP: 33,
    PAGEDOWN: 34,
    END: 35,
    HOME: 36,
    LEFT: 37,
    UP: 38,
    RIGHT: 39,
    DOWN: 40,
  });
};

Treeitem.prototype.init = function () {
  this.domNode.tabIndex = -1;

  if (!this.domNode.getAttribute('role')) {
    this.domNode.setAttribute('role', 'treeitem');
  }

  this.domNode.addEventListener('keydown', this.handleKeydown.bind(this));
  this.domNode.addEventListener('click', this.handleClick.bind(this));
  this.domNode.addEventListener('focus', this.handleFocus.bind(this));
  this.domNode.addEventListener('blur', this.handleBlur.bind(this));

  if (!this.isExpandable) {
    this.domNode.addEventListener('mouseover', this.handleMouseOver.bind(this));
    this.domNode.addEventListener('mouseout', this.handleMouseOut.bind(this));
  }
};

Treeitem.prototype.isExpanded = function () {
  if (this.isExpandable) {
    return this.domNode.getAttribute('aria-expanded') === 'true';
  }

  return false;
};

Treeitem.prototype.handleKeydown = function (event) {
  var flag = false,
    char = event.key;

  function isPrintableCharacter(str) {
    return str.length === 1 && str.match(/\S/);
  }

  function printableCharacter(item) {
    if (char == '*') {
      item.tree.expandAllSiblingItems(item);
      flag = true;
    } else {
      if (isPrintableCharacter(char)) {
        item.tree.setFocusByFirstCharacter(item, char);
        flag = true;
      }
    }
  }

  if (event.altKey || event.ctrlKey || event.metaKey) {
    return;
  }

  if (event.shift) {
    if (isPrintableCharacter(char)) {
      printableCharacter(this);
    }
  } else {
    switch (event.keyCode) {
      case this.keyCode.RETURN:
      case this.keyCode.SPACE:
        var treeitem = event.currentTarget;
        var label = treeitem.getAttribute('aria-label');
        if (!label) {
          var child = treeitem.firstElementChild;
          label = child ? child.innerText : treeitem.innerText;
        }
        document.getElementById('last_action').value = label.trim();

        if (!this.isExpandable) this.tree.setFocusToItem(this);
        this.tree.setSelectedToItem(this);
        flag = true;
        break;

      case this.keyCode.UP:
        this.tree.setFocusToPreviousItem(this);
        flag = true;
        break;

      case this.keyCode.DOWN:
        this.tree.setFocusToNextItem(this);
        flag = true;
        break;

      case this.keyCode.RIGHT:
        if (this.isExpandable) {
          if (this.isExpanded()) {
            this.tree.setFocusToNextItem(this);
          } else {
            this.tree.expandTreeitem(this);
          }
        }
        flag = true;
        break;

      case this.keyCode.LEFT:
        if (this.isExpandable && this.isExpanded()) {
          this.tree.collapseTreeitem(this);
          flag = true;
        } else {
          if (this.inGroup) {
            this.tree.setFocusToParentItem(this);
            flag = true;
          }
        }
        break;

      case this.keyCode.HOME:
        this.tree.setFocusToFirstItem();
        flag = true;
        break;

      case this.keyCode.END:
        this.tree.setFocusToLastItem();
        flag = true;
        break;

      default:
        if (isPrintableCharacter(char)) {
          printableCharacter(this);
        }
        break;
    }
  }

  if (flag) {
    event.stopPropagation();
    event.preventDefault();
  }
};

Treeitem.prototype.handleClick = function (event) {
  if (this.isExpandable) {
    if (this.isExpanded()) {
      this.tree.collapseTreeitem(this);
    } else {
      this.tree.expandTreeitem(this);
    }
    event.stopPropagation();
  } else {
    this.tree.setFocusToItem(this);
  }
  this.tree.setSelectedToItem(this);
};

Treeitem.prototype.handleFocus = function () {
  var node = this.domNode;
  if (this.isExpandable) {
    node = node.firstElementChild;
  }
  node.classList.add('focus');
};

Treeitem.prototype.handleBlur = function () {
  var node = this.domNode;
  if (this.isExpandable) {
    node = node.firstElementChild;
  }
  node.classList.remove('focus');
};

Treeitem.prototype.handleMouseOver = function (event) {
  event.currentTarget.classList.add('hover');
};

Treeitem.prototype.handleMouseOut = function (event) {
  event.currentTarget.classList.remove('hover');
};

window.addEventListener('load', function () {
  var treeitems = document.querySelectorAll('[role="treeitem"]');

  for (var i = 0; i < treeitems.length; i++) {
    treeitems[i].addEventListener('click', function (event) {
      var treeitem = event.currentTarget;
      var label = treeitem.getAttribute('aria-label');
      if (!label) {
        var child = treeitem.firstElementChild;
        label = child ? child.innerText : treeitem.innerText;
      }
  
      document.getElementById('last_action').value = label.trim();

      event.stopPropagation();
      event.preventDefault();
    });
  }
});

まとめ

この記事では、「ツリーリスト」に焦点を当てて、アクセシビリティを意識したツリーリストの実装方法とツリーリストで意識した方がいいアクセシビリティを解説しました。

ぜひこの記事をストックして、ツリーリストを実装する時にアクセシビリティについて思い出してもらえると嬉しいです。

Advent Calendar 2023では、他のコンポーネントにも焦点を当てて、アクセシビリティについても解説しているので、ぜひ購読していてください。


最後まで読んでくださってありがとうございます!

普段はデザインやフロントエンドを中心にQiitaに記事を投稿しているので、ぜひQiitaのフォローとX(Twitter)のフォローをお願いします。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2