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

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

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

デモ

動画で確認したい場合は、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 は、アニメーション化されたインタラクティブなグリッドをきちんとオーケストレートするだけでなく、いくつかの粗いエッジを磨き上げるためにも使用されます。

ユーザー入力の正規化

この設計では、1 つのフォームで 2 つの異なる入力方法を使用できますが、同じものをシリアル化していません。JavaScript を使用すれば、データを正規化できます。

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

<select> 要素のデータ構造を、グループ化されたチェックボックス構造に合わせることにしました。これを行うには、input イベント リスナーを <select> 要素に追加します。この時点で、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 の優れた支援技術を、ユーザーがどのように操作しても、すべてのユーザーに提供できるようになりました。

まとめ

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

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

コミュニティ リミックス

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