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

ユーザー エクスペリエンスの並べ替えとフィルタリングを行うための、応答性、適応性、アクセス性に優れた複数選択コンポーネントを構築する方法の基本的な概要です。

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

デモ

動画をご覧になる場合は、この投稿の 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>

凡例とフィールドセット要素の情報オーバーレイを含むスクリーンショットで、色と要素名が表示されています。

<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 になるものはありません。

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

aside :checked {
  counter-increment: filters;
}

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

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

ステータスのロール要素の HTML が、スクリーン リーダーに「2 個のフィルタ」と読み上げます。まずは良いスタートですが、フィルタによって更新された結果の集計を共有するなど、より良い方法もあります。この処理は JavaScript から行います。これはカウンタでは対応できない部分です。

アクティブなフィルタの数を示している MacOS のスクリーン リーダーのスクリーンショット。

次の興奮

CSS nesting-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> のデフォルトのスタイルとレイアウトは一意です。

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

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

fieldset {
  padding: 2ch;

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

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

入力間のマージンの間隔を示すが凡例ではないスクリーンショット。

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

<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 に指示してください。

status ロール要素を終了する

この要素は、チェックボックスの操作に基づいてフィルタ数を集計して通知するだけですが、結果の数も共有し、<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 の優れた支援技術エクスペリエンスをすべてのユーザーに提供できるようになりました。

おわりに

私のやり方がわかったところで、どうしたらいいですか? 🙂?

多様なアプローチと、ウェブでの構築方法を学んでいきましょう。 デモを作成してツイートのリンクをお願いします。下のコミュニティ リミックス セクションに追加します。

コミュニティのリミックス

まだ何も表示されません。