다중 선택 구성요소 빌드

정렬 및 필터 사용자 환경을 위한 반응형, 적응형, 접근성 있는 다중 선택 구성요소를 빌드하는 방법에 관한 기본 개요입니다.

이 게시물에서는 다중 선택 구성요소를 빌드하는 방법에 관한 생각을 공유하고자 합니다. 데모를 사용해 보세요.

데모

동영상을 선호하는 경우 이 게시물의 YouTube 버전을 확인하세요.

개요

사용자에게 항목이 표시되는 경우가 많으며 때로는 많은 항목이 표시되기도 합니다. 이러한 경우 목록을 줄여 선택 과부하를 방지하는 방법을 제공하는 것이 좋습니다. 이 블로그 게시물에서는 선택사항을 줄이는 방법으로 필터링 UI를 살펴봅니다. 사용자가 선택하거나 선택 해제할 수 있는 상품 속성을 표시하여 결과를 줄이고 선택 과부하를 줄입니다.

상호작용 수

목표는 모든 사용자와 다양한 입력 유형에 대해 필터 옵션을 빠르게 탐색할 수 있도록 하는 것입니다. 이는 적응형 및 반응형 구성요소 쌍과 함께 제공됩니다. 데스크톱, 키보드, 스크린 리더를 위한 체크박스가 있는 기존 사이드바와 터치 사용자를 위한 <select multiple>

체크박스가 있는 데스크톱 밝은 테마와 어두운 테마를 다중 선택 요소가 있는 모바일 iOS 및 Android와 비교하는 스크린샷

터치에는 내장된 다중 선택을 사용하고 데스크톱에는 사용하지 않기로 한 결정으로 인해 작업이 절약되기도 하고 작업이 추가되기도 하지만, 하나의 구성요소에서 전체 반응형 환경을 빌드하는 것보다 코드 부채가 적은 적절한 환경을 제공한다고 생각합니다.

터치

터치 구성요소는 공간을 절약하고 모바일에서 사용자 상호작용 정확도를 높이는 데 도움이 됩니다. 체크박스로 구성된 전체 사이드바를 <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>

범례와 필드셋 요소에 대한 정보 오버레이가 있는 스크린샷으로, 색상과 요소 이름을 보여줍니다.

<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 nesting-1에서 유용했습니다. 읽고 업데이트하기에 휴대하기 쉽고 중앙 집중식으로 느껴집니다.

aside {
  counter-reset: filters;

  & :checked {
    counter-increment: filters;
  }

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

레이아웃

이 섹션에서는 두 구성요소 간의 레이아웃을 설명합니다. 대부분의 레이아웃 스타일은 데스크톱 체크박스 구성요소용입니다.

양식

사용자의 가독성과 검색 가능성을 최적화하기 위해 양식의 최대 너비는 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;
}
여러 줄로 텍스트가 래핑되는 시나리오에서 체크표시가 텍스트의 첫 번째 줄에 정렬되는 방식을 보여주는 스크린샷
Codepen에서 더 많이 플레이하기

애니메이션 그리드

레이아웃 애니메이션은 Isotope에 의해 실행됩니다. 대화형 정렬 및 필터링을 위한 성능이 우수하고 강력한 플러그인입니다.

자바스크립트

JavaScript는 깔끔한 애니메이션 대화형 그리드를 오케스트레이션하는 데 도움이 될 뿐만 아니라 몇 가지 거친 부분을 다듬는 데도 사용됩니다.

사용자 입력 정규화

이 디자인에는 입력하는 두 가지 방법이 있는 하나의 양식이 있으며, 이 두 가지 방법은 동일하게 직렬화되지 않습니다. 하지만 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 스크린 리더의 스크린샷

이제 사용자가 어떻게 상호작용하든 우수한 지원 기술 환경이 모든 사용자에게 제공됩니다.

결론

이제 제가 어떻게 했는지 아셨으니, 어떻게 하시겠어요? 🙂

다양한 접근 방식을 사용하고 웹에서 빌드하는 모든 방법을 알아보세요. 데모를 만들고 트윗으로 링크를 보내주세요. 아래 커뮤니티 리믹스 섹션에 추가해 드리겠습니다.

커뮤니티 리믹스

아직 표시할 내용이 없습니다.