다중 선택 구성요소 빌드

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

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

데모

동영상을 선호하는 경우 이 게시물의 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>의 기본 스타일과 레이아웃은 고유합니다.

필드 세트 및 범례의 기본 스타일 스크린샷

일반적으로 하위 요소의 간격을 두려면 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 스크린 리더의 스크린샷

이제 Google의 우수한 보조 기술 환경이 상호작용하는 방식과 관계없이 모든 사용자에게 제공됩니다.

결론

이제 제가 어떻게 했는지 알았으니 어떻게 하시겠어요? 🙂

접근 방식을 다양화하고 웹에서 빌드하는 모든 방법을 알아보겠습니다. 데모를 만들어 트윗해 주시면 아래의 커뮤니티 리믹스 섹션에 추가해 드리겠습니다.

커뮤니티 리믹스

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