다중 선택 구성요소 빌드

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

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

데모

동영상을 선호한다면 이 게시물의 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 카운터로 콘텐츠를 업데이트할 수 있습니다. 이렇게 하려면 먼저 input 및 state 요소의 상위 요소에 이름이 있는 카운터를 만들어야 합니다.

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 스크린 리더 스크린샷

Nesting 설레는 마음

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를 사용하여 애니메이션 대화형 그리드를 조정할 수 있을 뿐만 아니라 몇 가지 다듬어진 가장자리를 다듬을 수 있습니다.

사용자 입력 정규화

이 설계는 두 가지 방법으로 입력을 제공하는 하나의 양식을 가지며 동일한 형식을 직렬화하지 않습니다. 하지만 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`
})

이 작업은 모두 '25개의 결과를 제공하는 필터 2개'라는 공지사항을 완성합니다.

결과를 알려주는 MacOS 스크린 리더의 스크린샷

이제 Google의 탁월한 보조 기술 환경은 사용자와 상호작용하는 방식에 관계없이 모든 사용자에게 제공될 것입니다.

결론

이제 제가 어떻게 했는지 알았으니 어떻게 되세요?‽ 🙂

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

커뮤니티 리믹스

표시할 항목이 없습니다.