複数選択コンポーネントの作成

並べ替えとフィルタのユーザー エクスペリエンス向けに、レスポンシブで適応性があり、アクセシビリティに優れたマルチ選択コンポーネントを作成する方法の基本的な概要。

この記事では、複数選択コンポーネントを作成する方法について考えを共有したいと思います。デモをお試しください。

デモ

動画で確認したい場合は、YouTube 版の投稿をご覧ください。

概要

ユーザーにアイテムが表示されることはよくありますが、大量のアイテムが表示されることもあります。このような場合は、選択のオーバーロードを防ぐために、リストを減らす方法を用意することをおすすめします。このブログ投稿では、選択肢を減らす方法として UI のフィルタリングについて説明します。これは、ユーザーが選択または選択解除できるアイテム属性を提示することで行われます。これにより結果が減り、選択のオーバーロードが軽減されます。

インタラクション数

すべてのユーザーとさまざまな入力タイプに対して、フィルタ オプションをすばやく移動できるようにすることが目的です。これは、適応性と応答性に優れたコンポーネントのペアで提供されます。パソコン、キーボード、スクリーン リーダー向けのチェックボックスの従来のサイドバーと、タッチ操作のユーザー向けの <select multiple>

チェックボックスのサイドバーがあるパソコンの明るい画面と暗い画面と、マルチ選択要素があるモバイル(iOS と Android)の比較スクリーンショット。

タップ用に組み込みのマルチ選択を使用し、デスクトップには使用しないという判断は、作業を節約し、作業を増やしますが、レスポンシブ エクスペリエンス全体を 1 つのコンポーネントで構築するよりも、コード債務を減らして適切なエクスペリエンスを提供できると考えています。

タップ

タッチ コンポーネントはスペースを節約し、モバイルでのユーザー操作の精度を高めます。チェックボックスのサイドバー全体を <select> の組み込みオーバーレイ タップ操作に閉じて、スペースを節約します。システムが提供する大きなタップ オーバーレイ エクスペリエンスを表示することで、入力の精度を高めることができます。

Android、iPhone、iPad 版 Chrome のマルチ選択要素のスクリーンショット プレビュー。iPad と iPhone では、マルチ選択がオンになっており、それぞれ画面サイズに合わせて最適化された独自のエクスペリエンスが提供されます。

キーボードとゲームパッド

以下は、キーボードから <select multiple> を使用する方法のデモです。

この組み込みの複数選択にはスタイルを設定できません。また、コンパクトなレイアウトでのみ提供されるため、多くのオプションを表示するには適していません。この小さなボックスでは、選択できるオプションの幅を把握しづらいですよね。サイズは変更できますが、チェックボックスのサイドバーほど使い勝手はよくありません。

マークアップ

両方のコンポーネントは同じ <form> 要素に含まれます。このフォームの結果(チェックボックスまたは複数選択)は、グリッドのフィルタに使用されますが、サーバーに送信することもできます。

<form>

</form>

チェックボックス コンポーネント

チェックボックスのグループは、<fieldset> 要素でラップし、<legend> を指定する必要があります。HTML がこのように構造化されている場合、スクリーン リーダーと FormData は要素の関係を自動的に把握します。

<form>
  <fieldset>
    <legend>New</legend>
    … checkboxes …
  </fieldset>
</form>

グループ化が完了したら、各フィルタに <label><input type="checkbox"> を追加します。私は <div> でラベルをラップして、CSS gap プロパティでラベルを均等に配置し、ラベルが複数行にわたる場合でも位置を揃えられるようにしました。

<form>
  <fieldset>
    <legend>New</legend>
    <div>
      <input type="checkbox" id="last 30 days" name="new" value="last 30 days">
      <label for="last 30 days">Last 30 Days</label>
    </div>
    <div>
      <input type="checkbox" id="last 6 months" name="new" value="last 6 months">
      <label for="last 6 months">Last 6 Months</label>
    </div>
   </fieldset>
</form>

凡例要素と fieldset 要素に情報オーバーレイを適用したスクリーンショット。色と要素名が表示されています。

<select multiple> コンポーネント

<select> 要素のあまり使用されない機能に multiple があります。この属性を <select> 要素で使用すると、ユーザーはリストから複数を選択できます。ラジオリストからチェックボックス リストに操作を変更するようなものです。

<form>
  <select multiple="true" title="Filter results by category">
    …
  </select>
</form>

<select> 内にラベルを付けてグループを作成するには、<optgroup> 要素を使用して、label 属性と値を指定します。この要素と属性値は、<fieldset> 要素と <legend> 要素に似ています。

<form>
  <select multiple="true" title="Filter results by category">
    <optgroup label="New">
      …
    </optgroup>
  </select>
</form>

次に、フィルタの <option> 要素を追加します。

<form>
  <select multiple="true" title="Filter results by category">
    <optgroup label="New">
      <option value="last 30 days">Last 30 Days</option>
      <option value="last 6 months">Last 6 Months</option>
    </optgroup>
  </select>
</form>

複数選択要素のデスクトップ レンダリングのスクリーンショット。

カウンタを使用して入力をトラッキングし、支援技術に通知する

このユーザー エクスペリエンスでは、スクリーン リーダーなどの支援技術のフィルタの合計数を追跡して維持するために、ステータスロール手法が使用されます。YouTube 動画でこの機能のデモをご覧ください。統合は HTML と属性 role="status" から始まります。

<div role="status" class="sr-only" id="applied-filters"></div>

この要素は、コンテンツに加えられた変更を読み上げます。ユーザーがチェックボックスを操作すると、CSS カウンタでコンテンツを更新できます。そのためには、まず入力要素と状態要素の親要素に名前付きのカウンタを作成する必要があります。

aside {
  counter-reset: filters;
}

デフォルトでは、カウントは 0 になります。この設計では、デフォルトで :checked になるものは何もないため、これは問題ありません。

次に、新しく作成したカウンタをインクリメントするために、:checked である <aside> 要素の子をターゲットにします。ユーザーが入力の状態を変更すると、filters カウンタが加算されます。

aside :checked {
  counter-increment: filters;
}

CSS は、チェックボックス UI の一般的な集計を認識し、ステータスロール要素は空で、値を待機しています。CSS はメモリ内にカウントを保持するため、counter() 関数を使用すると、疑似要素のコンテンツから値にアクセスできます。

aside #applied-filters::before {
  content: counter(filters) " filters ";
}

ステータスロール要素の HTML がスクリーン リーダーに「2 つのフィルタ」と読み上げられるようになりました。これは良い出発点ですが、フィルタによって更新された結果の合計数を共有するなど、さらに改善できます。この処理はカウンタではできないため、JavaScript から行います。

有効なフィルタの数を読み上げる macOS スクリーン リーダーのスクリーンショット。

ネストに関する最新情報

カウンタ アルゴリズムは CSS ネスト 1 で非常に便利でした。すべてのロジックを 1 つのブロックにまとめることができたからです。持ち運びが容易で、読み取りと更新が集中化されている。

aside {
  counter-reset: filters;

  & :checked {
    counter-increment: filters;
  }

  & #applied-filters::before {
    content: counter(filters) " filters ";
  }
}

レイアウト

このセクションでは、2 つのコンポーネント間のレイアウトについて説明します。レイアウト スタイルのほとんどは、パソコンのチェックボックス コンポーネント用です。

フォーム

ユーザーにとっての読みやすさとスキャンしやすさを最適化するため、フォームの最大幅は 30 文字に設定されています。これは、フィルタラベルごとに光学的な行幅を設定していることになります。このフォームでは、グリッド レイアウトと gap プロパティを使用して、フィールドセットの間隔を空けています。

form {
  display: grid;
  gap: 2ch;
  max-inline-size: 30ch;
}

<select> 要素

ラベルとチェックボックスのリストはどちらも、モバイルではスペースを占有しすぎます。そのため、レイアウトはユーザーのメインの指示デバイスをチェックして、タップ操作のエクスペリエンスを変更します。

@media (pointer: coarse) {
  select[multiple] {
    display: block;
  }
}

値が coarse の場合、ユーザーはメインの入力デバイスで画面を高精度に操作できないことを示します。モバイル デバイスでは、主な操作がタップであるため、ポインタ値は多くの場合 coarse です。デスクトップ デバイスでは、マウスなどの高精度入力デバイスが接続されていることが一般的であるため、ポインタ値は fine であることが多いです。

フィールドセット

<legend> を含む <fieldset> のデフォルトのスタイル設定とレイアウトは独自です。

fieldset と凡例のデフォルト スタイルのスクリーンショット。

通常、子要素の間隔を空けるには gap プロパティを使用しますが、<legend> の独自の配置により、子要素を均等に配置することは困難です。gap の代わりに、隣接する兄弟セレクタmargin-block-start が使用されます。

fieldset {
  padding: 2ch;

  & > div + div {
    margin-block-start: 2ch;
  }
}

これにより、<div> の子のみをターゲットに設定して、<legend> のスペースが調整されなくなります。

入力間の余白スペースは表示されていますが、凡例は表示されていないスクリーンショット。

フィルタのラベルとチェックボックス

<fieldset> の直接の子として、フォームの 30ch の最大幅内に配置されている場合、ラベル テキストが長すぎると折り返されることがあります。テキストの折り返しは良いのですが、テキストとチェックボックスの位置がずれているのは問題です。このような場合に最適なのが Flexbox です。

fieldset > div {
  display: flex;
  gap: 2ch;
  align-items: baseline;
}
複数行の折り返しのシナリオで、チェックマークがテキストの最初の行にどのように配置されるかを示したスクリーンショット。
この Codepen でさらに遊ぶ

アニメーション化されたグリッド

レイアウト アニメーションは Isotope によって行われます。インタラクティブな並べ替えとフィルタリングのための高性能で強力なプラグイン。

JavaScript

JavaScript は、アニメーション化されたインタラクティブなグリッドをきちんとオーケストレートするだけでなく、いくつかの粗いエッジを磨き上げるためにも使用されます。

ユーザー入力の正規化

この設計では、入力を提供する 2 つの異なる方法を持つ 1 つのフォームがあり、同じものをシリアル化しません。ただし、一部の JavaScript では、データを正規化できます。

目標と正規化されたデータの結果を示す DevTools JavaScript コンソールのスクリーンショット。

<select> 要素のデータ構造を、グループ化されたチェックボックス構造に合わせることにしました。これを行うには、<select> 要素に input イベント リスナーを追加します。この時点で、selectedOptions がマッピングされます。

document.querySelector('select').addEventListener('input', event => {
  // make selectedOptions iterable then reduce a new array object
  let selectData = Array.from(event.target.selectedOptions).reduce((data, opt) => {
    // parent optgroup label and option value are added to the reduce aggregator
    data.push([opt.parentElement.label.toLowerCase(), opt.value])
    return data
  }, [])
})

これで、フォームを送信できます。このデモの場合は、フィルタ条件を Isotope に指示します。

ステータスロール要素の完成

この要素は、チェックボックスの操作に基づいてフィルタ数を集計して通知するだけですが、結果の数も共有し、<select> 要素の選択もカウントされるようにしたほうがよいと考えました。

<select> 要素の選択が counter() に反映されている

データの正規化セクションでは、入力時にリスナーがすでに作成されています。この関数の終了時に、選択したフィルタの数と、それらのフィルタの結果の数がわかります。値は次のように状態ロール要素に渡すことができます。

let statusRoleElement = document.querySelector('#applied-filters')
statusRoleElement.style.counterSet = selectData.length

role="status" 要素に反映された結果

:checked には、選択したフィルタの数をステータスロール要素に渡す組み込みの方法が用意されていますが、フィルタされた結果の数は表示されません。JavaScript は、チェックボックスの操作を監視し、グリッドをフィルタした後、<select> 要素と同様に textContent を追加できます。

document
  .querySelector('aside form')
  .addEventListener('input', e => {
    // isotope demo code
    let filterResults = IsotopeGrid.getFilteredItemElements().length
    document.querySelector('#applied-filters').textContent = `giving ${filterResults} results`
})

これで、通知「2 つのフィルタで 25 件の結果」が完成します。

macOS のスクリーン リーダーが結果を読み上げるスクリーンショット。

これで、Google の優れた支援技術を、ユーザーがどのように操作しても、すべてのユーザーに提供できるようになりました。

まとめ

私の方法をご覧になったところで、あなたならどうしますか?

アプローチを多様化し、ウェブで構築するすべての方法を学びましょう。デモを作成して、ツイートしてください。リンクを送信していただければ、下のコミュニティ リミックスのセクションに追加します。

コミュニティ リミックス

表示する項目はありません