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

並べ替えとフィルタのユーザー エクスペリエンスのための、レスポンシブで適応性があり、アクセシビリティの高い複数選択コンポーネントを構築する方法の基本的な概要。

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

デモ

動画でご覧になりたい場合は、こちらの 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"> を追加します。ラベルが複数行になったときに、CSS の gap プロパティで均等に間隔を空け、配置を維持できるように、<div> でラップすることにしました。

<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 と legend のデフォルト スタイルのスクリーンショット。

通常、子要素のスペースを設定するには 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;
}
複数行の折り返しシナリオでチェックマークがテキストの 1 行目に揃えられていることを示すスクリーンショット。
この 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> 要素の選択肢もカウントされるようにするとよいと思いました。

counter() に反映された <select> 要素の選択

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

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 のスクリーン リーダーのスクリーンショット。

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

まとめ

私がどのように行ったかをご理解いただけたかと思います。では、あなたならどのようにしますか?🙂

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

コミュニティ リミックス

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