Xây dựng thành phần chọn nhiều mục

Thông tin tổng quan cơ bản về cách tạo thành phần nhiều lựa chọn, thích ứng, có thể tiếp cận và thích ứng để sắp xếp và lọc trải nghiệm người dùng.

Trong bài đăng này, tôi muốn chia sẻ suy nghĩ về cách tạo một thành phần chọn nhiều mục. Hãy dùng thử bản minh hoạ.

Bản minh hoạ

Nếu bạn thích xem video, hãy xem phiên bản video của bài đăng này trên YouTube:

Tổng quan

Người dùng thường được trình bày các mục, đôi khi là nhiều mục. Trong những trường hợp này, bạn nên cung cấp cách để giảm danh sách để tránh quá tải lựa chọn. Bài đăng này trên blog khám phá giao diện người dùng lọc như một cách để giảm số lựa chọn. Cách này được thực hiện bằng cách trình bày các thuộc tính mặt hàng mà người dùng có thể chọn hoặc bỏ chọn, giảm số lượng kết quả và do đó giảm tình trạng quá tải lựa chọn.

Lượt tương tác

Mục tiêu là cho phép chuyển đổi nhanh các tuỳ chọn bộ lọc cho tất cả người dùng và các loại dữ liệu đầu vào khác nhau của họ. Tính năng này sẽ được phân phối bằng một cặp thành phần thích ứng và thích ứng. Một thanh bên truyền thống gồm các hộp đánh dấu cho máy tính, bàn phím và trình đọc màn hình, cũng như <select multiple> cho người dùng cảm ứng.

Ảnh chụp màn hình so sánh chế độ sáng và tối trên máy tính có thanh bên chứa hộp đánh dấu so với iOS và Android dành cho thiết bị di động có phần tử nhiều lựa chọn.

Quyết định sử dụng tính năng chọn nhiều mục tích hợp cho thiết bị cảm ứng chứ không phải cho máy tính để bàn sẽ giúp tiết kiệm công sức và tạo ra công việc, nhưng tôi tin rằng việc này sẽ mang lại trải nghiệm phù hợp với ít nợ mã hơn so với việc xây dựng toàn bộ trải nghiệm thích ứng trong một thành phần.

Cảm ứng

Thành phần cảm ứng giúp tiết kiệm không gian và giúp người dùng tương tác chính xác trên thiết bị di động. Tính năng này giúp tiết kiệm không gian bằng cách thu gọn toàn bộ thanh bên của hộp đánh dấu thành một trải nghiệm chạm dạng lớp phủ tích hợp sẵn <select>. Tính năng này giúp tăng độ chính xác khi nhập bằng cách hiển thị một trải nghiệm lớp phủ cảm ứng lớn do hệ thống cung cấp.

Ảnh chụp màn hình xem trước phần tử nhiều lựa chọn trong Chrome trên Android, iPhone và iPad. iPad và iPhone đã mở chế độ chọn nhiều mục và mỗi thiết bị đều có trải nghiệm riêng biệt được tối ưu hoá cho kích thước màn hình.

Bàn phím và tay điều khiển trò chơi

Dưới đây là bản minh hoạ cách sử dụng <select multiple> từ bàn phím.

Bạn không thể tạo kiểu cho tính năng chọn nhiều mục tích hợp sẵn này và tính năng này chỉ được cung cấp ở bố cục nhỏ gọn không phù hợp để trình bày nhiều tuỳ chọn. Bạn có thấy mình không thể xem được nhiều tuỳ chọn trong hộp nhỏ đó không? Mặc dù bạn có thể thay đổi kích thước của hộp đánh dấu, nhưng hộp đánh dấu vẫn không hữu dụng bằng thanh bên của hộp đánh dấu.

Markup (note: đây là tên ứng dụng)

Cả hai thành phần sẽ nằm trong cùng một phần tử <form>. Kết quả của biểu mẫu này, cho dù là hộp đánh dấu hay chọn nhiều đáp án, sẽ được ghi nhận và sử dụng để lọc lưới, nhưng cũng có thể được gửi đến máy chủ.

<form>

</form>

Thành phần hộp đánh dấu

Các nhóm hộp đánh dấu phải được gói trong phần tử <fieldset> và được cung cấp <legend>. Khi HTML được cấu trúc theo cách này, trình đọc màn hình và FormData sẽ tự động hiểu được mối quan hệ của các phần tử.

<form>
  <fieldset>
    <legend>New</legend>
    … checkboxes …
  </fieldset>
</form>

Sau khi thiết lập nhóm, hãy thêm <label><input type="checkbox"> cho từng bộ lọc. Tôi chọn gói nhãn của mình trong <div> để thuộc tính gap CSS có thể tạo khoảng cách đồng đều và duy trì căn chỉnh khi nhãn có nhiều dòng.

<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>

Ảnh chụp màn hình có lớp phủ thông tin cho các phần tử chú giải và fieldset, cho thấy màu sắc và tên phần tử.

Thành phần <select multiple>

Một tính năng ít được sử dụng của phần tử <select>multiple. Khi thuộc tính này được sử dụng với phần tử <select>, người dùng được phép chọn nhiều phần tử trong danh sách. Nó giống như thay đổi lượt tương tác từ một danh sách chọn đài sang một danh sách hộp đánh dấu.

<form>
  <select multiple="true" title="Filter results by category">
    …
  </select>
</form>

Để gắn nhãn và tạo nhóm bên trong <select>, hãy dùng phần tử <optgroup> đồng thời cung cấp thuộc tính và giá trị label cho phần tử đó. Phần tử và giá trị thuộc tính này tương tự như các phần tử <fieldset><legend>.

<form>
  <select multiple="true" title="Filter results by category">
    <optgroup label="New">
      …
    </optgroup>
  </select>
</form>

Bây giờ, hãy thêm các phần tử <option> cho bộ lọc.

<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>

Ảnh chụp màn hình kết xuất trên máy tính của một phần tử có thể chọn nhiều.

Theo dõi dữ liệu đầu vào bằng bộ đếm để thông báo cho công nghệ hỗ trợ

Kỹ thuật vai trò trạng thái được sử dụng trong trải nghiệm người dùng này để theo dõi và duy trì số lượng bộ lọc cho trình đọc màn hình và các công nghệ hỗ trợ khác. Video trên YouTube minh hoạ tính năng này. Quá trình tích hợp bắt đầu bằng HTML và thuộc tính role="status".

<div role="status" class="sr-only" id="applied-filters"></div>

Phần tử này sẽ đọc to những thay đổi đối với nội dung. Chúng tôi có thể cập nhật nội dung bằng bộ đếm CSS khi người dùng tương tác với các hộp đánh dấu. Để làm được việc đó, trước tiên, chúng ta cần tạo một bộ đếm có tên trên phần tử mẹ của phần tử đầu vào và trạng thái.

aside {
  counter-reset: filters;
}

Theo mặc định, số lượng sẽ là 0, rất tuyệt, không có gì là :checked theo mặc định trong thiết kế này.

Tiếp theo, để tăng bộ đếm mới tạo, chúng ta sẽ nhắm đến các phần tử con của phần tử <aside>:checked. Khi người dùng thay đổi trạng thái của dữ liệu đầu vào, bộ đếm filters sẽ tính tổng.

aside :checked {
  counter-increment: filters;
}

CSS hiện đã biết kết quả kiểm đếm chung của giao diện người dùng hộp đánh dấu và phần tử vai trò trạng thái đang trống và đang chờ các giá trị. Vì CSS đang duy trì số liệu tính tổng trong bộ nhớ, nên hàm counter() cho phép truy cập vào giá trị từ nội dung phần tử giả:

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

Giờ đây, HTML cho phần tử vai trò trạng thái sẽ thông báo "2 bộ lọc" cho trình đọc màn hình. Đây là một khởi đầu tốt, nhưng chúng ta có thể làm tốt hơn, chẳng hạn như chia sẻ số liệu thống kê về kết quả mà bộ lọc đã cập nhật. Chúng ta sẽ thực hiện công việc này từ JavaScript, vì bộ đếm không thể làm việc này.

Ảnh chụp màn hình trình đọc màn hình MacOS thông báo số lượng bộ lọc đang hoạt động.

Sự phấn khích khi lồng ghép

Thuật toán bộ đếm hoạt động hiệu quả với CSS Nesting-1, vì tôi có thể đưa tất cả logic vào một khối. Có thể di chuyển và tập trung để đọc và cập nhật.

aside {
  counter-reset: filters;

  & :checked {
    counter-increment: filters;
  }

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

Bố cục

Phần này mô tả bố cục giữa hai thành phần. Hầu hết các kiểu bố cục đều dành cho thành phần hộp đánh dấu trên máy tính.

Biểu mẫu

Để tối ưu hoá khả năng đọc và quét cho người dùng, biểu mẫu được cung cấp chiều rộng tối đa là 30 ký tự, về cơ bản là thiết lập chiều rộng dòng quang học cho mỗi nhãn bộ lọc. Biểu mẫu sử dụng bố cục lưới và thuộc tính gap để tạo khoảng cách giữa các trường.

form {
  display: grid;
  gap: 2ch;
  max-inline-size: 30ch;
}

Phần tử <select>

Danh sách nhãn và hộp đánh dấu đều chiếm quá nhiều không gian trên thiết bị di động. Do đó, bố cục sẽ kiểm tra để xem thiết bị trỏ chính của người dùng nhằm thay đổi trải nghiệm cho thao tác chạm.

@media (pointer: coarse) {
  select[multiple] {
    display: block;
  }
}

Giá trị coarse cho biết người dùng sẽ không thể tương tác với màn hình một cách chính xác cao bằng thiết bị đầu vào chính của họ. Trên thiết bị di động, giá trị con trỏ thường là coarse, vì hoạt động tương tác chính là chạm. Trên thiết bị máy tính, giá trị con trỏ thường là fine vì thường có chuột hoặc thiết bị đầu vào có độ chính xác cao khác được kết nối.

Các fieldset

Kiểu và bố cục mặc định của <fieldset><legend> là duy nhất:

Ảnh chụp màn hình về các kiểu mặc định cho fieldset và chú thích.

Thông thường, để tạo khoảng trống cho các phần tử con, tôi sẽ sử dụng thuộc tính gap, nhưng vị trí duy nhất của <legend> khiến khó tạo một nhóm phần tử con có khoảng cách đồng đều. Thay vì gap, bộ chọn thành phần đồng cấp liền kềmargin-block-start được sử dụng.

fieldset {
  padding: 2ch;

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

Thao tác này sẽ bỏ qua việc điều chỉnh không gian của <legend> bằng cách chỉ nhắm đến các thành phần con <div>.

Ảnh chụp màn hình cho thấy khoảng cách lề giữa các dữ liệu đầu vào nhưng không có chú giải.

Nhãn bộ lọc và hộp đánh dấu

Là thành phần con trực tiếp của <fieldset> và nằm trong chiều rộng tối đa của 30ch của biểu mẫu, văn bản nhãn có thể xuống dòng nếu quá dài. Việc ngắt dòng văn bản là rất tốt, nhưng việc không căn chỉnh giữa văn bản và hộp đánh dấu là không tốt. Flexbox là giải pháp lý tưởng cho việc này.

fieldset > div {
  display: flex;
  gap: 2ch;
  align-items: baseline;
}
Ảnh chụp màn hình cho thấy cách dấu kiểm căn chỉnh với dòng văn bản đầu tiên trong trường hợp ngắt dòng nhiều dòng.
Chơi nhiều hơn trong Codepen này

Lưới động

Ảnh động bố cục do Isotope thực hiện. Một trình bổ trợ hiệu quả và mạnh mẽ để sắp xếp và lọc tương tác.

JavaScript

Ngoài việc giúp điều phối một lưới tương tác, động và gọn gàng, JavaScript còn được dùng để đánh bóng một số điểm còn thô.

Điều chỉnh dữ liệu đầu vào của người dùng

Thiết kế này có một biểu mẫu với hai cách khác nhau để cung cấp dữ liệu đầu vào và các cách này không sê-ri hoá giống nhau. Tuy nhiên, với một số JavaScript, chúng ta có thể bình thường hoá dữ liệu.

Ảnh chụp màn hình bảng điều khiển JavaScript của DevTools cho thấy mục tiêu, kết quả dữ liệu đã chuẩn hoá.

Tôi đã chọn căn chỉnh cấu trúc dữ liệu phần tử <select> với cấu trúc hộp đánh dấu được nhóm. Để thực hiện việc này, trình nghe sự kiện input sẽ được thêm vào phần tử <select>, tại thời điểm đó, selectedOptions sẽ được liên kết.

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
  }, [])
})

Bây giờ, bạn có thể gửi biểu mẫu một cách an toàn. Trong trường hợp bạn có bản minh hoạ này, hãy hướng dẫn cho Isotope về những gì cần lọc.

Hoàn tất phần tử vai trò trạng thái

Phần tử này chỉ tính và thông báo số lượng bộ lọc dựa trên lượt tương tác với hộp đánh dấu, nhưng tôi nghĩ bạn cũng nên chia sẻ số lượng kết quả và đảm bảo các lựa chọn phần tử <select> cũng được tính.

Lựa chọn phần tử <select> được phản ánh trong counter()

Trong mục chuẩn hoá dữ liệu, một trình nghe đã được tạo khi có dữ liệu đầu vào. Ở cuối hàm này, đã biết số lượng bộ lọc đã chọn và số lượng kết quả của các bộ lọc đó. Các giá trị có thể được chuyển đến phần tử vai trò trạng thái như thế này.

let statusRoleElement = document.querySelector('#applied-filters')
statusRoleElement.style.counterSet = selectData.length

Kết quả được phản ánh trong phần tử role="status"

:checked cung cấp một cách tích hợp để truyền số lượng bộ lọc đã chọn đến phần tử vai trò trạng thái, nhưng không hiển thị số lượng kết quả đã lọc. JavaScript có thể theo dõi hoạt động tương tác với các hộp đánh dấu và sau khi lọc lưới, hãy thêm textContent như phần tử <select> đã làm.

document
  .querySelector('aside form')
  .addEventListener('input', e => {
    // isotope demo code
    let filterResults = IsotopeGrid.getFilteredItemElements().length
    document.querySelector('#applied-filters').textContent = `giving ${filterResults} results`
})

Nhìn chung, công việc này hoàn tất thông báo "2 bộ lọc cho ra 25 kết quả".

Ảnh chụp màn hình trình đọc màn hình MacOS thông báo kết quả.

Giờ đây, trải nghiệm công nghệ hỗ trợ tuyệt vời của chúng tôi sẽ được cung cấp cho tất cả người dùng, bất kể họ tương tác với công nghệ đó như thế nào.

Kết luận

Giờ thì bạn đã biết cách tôi làm, còn bạn thì sao‽ 🙂

Hãy đa dạng hoá các phương pháp và tìm hiểu tất cả các cách xây dựng trên web. Tạo một bản minh hoạ, tweet cho tôi các đường liên kết và tôi sẽ thêm vào phần bản phối lại của cộng đồng ở bên dưới!

Bản phối lại của cộng đồng

Chưa có nội dung nào!