Tổng quan cơ bản về cách tạo một thành phần có thể chọn nhiều mục, có khả năng thích ứng và hỗ trợ tiếp cận để 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. Dùng thử bản minh hoạ.
Nếu bạn thích xem video, thì đây là phiên bản video của bài đăng này trên YouTube:
Tổng quan
Người dùng thường thấy nhiều mặt hàng, đôi khi là rất nhiều mặt hàng. Trong những trường hợp này, bạn nên cung cấp một cách để giảm danh sách nhằm ngăn chặn tình trạng quá nhiều 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ượng lựa chọn. Tính năng này thực hiện việc này 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à giúp tất cả người dùng có thể nhanh chóng di chuyển qua các lựa chọn bộ lọc và các loại dữ liệu đầu vào khác nhau. Điều này sẽ được cung cấp cùng với một cặp thành phần có khả năng thích ứng và phản hồi. 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 một <select
multiple>
cho người dùng sử dụng cảm ứng.
Quyết định sử dụng tính năng chọn nhiều tích hợp cho thao tác chạm và không dùng cho máy tính 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 quyết định 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 tạo 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à cải thiện độ chính xác khi người dùng tương tác trên thiết bị di động. Thao tác này giúp tiết kiệm không gian bằng cách thu gọn toàn bộ thanh bên gồm các hộp đánh dấu thành một trải nghiệm chạm lớp phủ tích hợp <select>
. Lớp phủ cảm ứng lớn do hệ thống cung cấp giúp tăng độ chính xác khi nhập.
Bàn phím và tay cầm chơi game
Dưới đây là ví dụ minh hoạ cách sử dụng <select multiple>
trên bàn phím.
Bạn không thể tạo kiểu cho tính năng chọn nhiều tích hợp này và tính năng này chỉ được cung cấp trong bố cục nhỏ gọn, không phù hợp để trình bày nhiều lựa chọn. Bạn thấy rằng mình không thể thực sự xem hết các lựa chọn trong hộp nhỏ đó, phải không? Mặc dù bạn có thể thay đổi kích thước của danh sách này, nhưng danh sách này vẫn không hữu ích bằng một thanh bên gồm các 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 (dù là hộp đánh dấu hay lựa chọn đa lựa chọn) sẽ được quan sát và dùng để lọc lưới, nhưng cũng có thể được gửi đến một 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 bao bọc 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 mối quan hệ của các phần tử.
<form>
<fieldset>
<legend>New</legend>
… checkboxes …
</fieldset>
</form>
Sau khi nhóm xong, hãy thêm <label>
và <input type="checkbox">
cho từng bộ lọc. Tôi chọn gói các nút này trong một <div>
để thuộc tính CSS gap
có thể phân bổ đều khoảng cách giữa các nút và duy trì sự căn chỉnh khi nhãn chuyển sang 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>
Thành phần <select multiple>
Một tính năng ít dùng của phần tử <select>
là multiple
.
Khi thuộc tính này được dùng với một phần tử <select>
, người dùng có thể chọn nhiều mục trong danh sách. Việc này giống như thay đổi hoạt động tương tác từ danh sách nút chọn thành 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 một <select>
, hãy dùng phần tử <optgroup>
rồi chỉ định cho phần tử đó một thuộc tính và giá trị label
. Giá trị thuộc tính và phần tử này tương tự như phần tử <fieldset>
và <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>
Theo dõi dữ liệu đầu vào bằng bộ đếm để cung cấp thông tin cho công nghệ hỗ trợ
Kỹ thuật vai trò trạng thái được 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 này minh hoạ tính năng đó. 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 ta 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 hộp đánh dấu. Để làm được điều đó, trước tiên, chúng ta cần tạo một bộ đếm có tên trên một phần tử mẹ của các 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
, tức là không có gì được :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>
là :checked
. Khi người dùng thay đổi trạng thái của các thành phần đầu vào, bộ đếm filters
sẽ tính tổng.
aside :checked {
counter-increment: filters;
}
CSS hiện đã nhận biết được tổng số 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à chờ giá trị. Vì CSS đang duy trì số liệu thống kê trong bộ nhớ, 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 ";
}
HTML cho phần tử vai trò trạng thái hiện 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ố lượng kết quả mà các bộ lọc đã cập nhật. Chúng ta sẽ thực hiện việc này bằng JavaScript, vì nó nằm ngoài khả năng của các bộ đếm.
Háo hức làm tổ
Thuật toán bộ đếm hoạt động rất tốt với CSS nesting-1, vì tôi có thể đặt tất cả logic vào một khối. Cảm giác như một nơi tập trung và dễ dàng mang theo để đọ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à xem nhanh cho người dùng, biểu mẫu có chiều rộng tối đa là 30 ký tự, về cơ bản là đặt chiều rộng dòng quang học cho từng nhãn bộ lọc. Biểu mẫu này sử dụng bố cục lưới và thuộc tính gap
để tạo khoảng cách cho các fieldset.
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 thiết bị trỏ chính của người dùng để 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 có độ 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ì lượt tương tác chính là lượt chạm. Trên thiết bị máy tính, giá trị con trỏ thường là fine
vì người dùng thường kết nối chuột hoặc thiết bị đầu vào có độ chính xác cao khác.
Các nhóm trường
Kiểu và bố cục mặc định của một <fieldset>
có <legend>
là duy nhất:
Thông thường, để tạo khoảng cách cho các phần tử con, tôi sẽ dùng thuộc tính gap
, nhưng vị trí duy nhất của <legend>
khiến tôi khó tạo được một nhóm con có khoảng cách đều nhau. Thay vì gap
, bộ chọn anh chị em liền kề và margin-block-start
sẽ được dùng.
fieldset {
padding: 2ch;
& > div + div {
margin-block-start: 2ch;
}
}
Thao tác này bỏ qua <legend>
để không gian của thành phần này được điều chỉnh bằng cách chỉ nhắm đến các thành phần con <div>
.
Nhãn bộ lọc và hộp đánh dấu
Là phần tử con trực tiếp của <fieldset>
và nằm trong chiều rộng tối đa của 30ch
trong biểu mẫu, văn bản nhãn có thể xuống dòng nếu quá dài. Việc xuống dòng văn bản là rất tốt, nhưng việc văn bản và hộp đánh dấu bị lệch hàng thì không. Flexbox là lựa chọn lý tưởng cho trường hợp này.
fieldset > div {
display: flex;
gap: 2ch;
align-items: baseline;
}

Lưới động
Ảnh động bố cục được thực hiện bằng Isotope. Một trình bổ trợ mạnh mẽ và hiệu quả để 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 hoạt ảnh, tương tác gọn gàng, JavaScript còn được dùng để tinh chỉnh một số điểm chưa hoàn thiện.
Chuẩn hoá 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 nhập dữ liệu khác nhau và chúng không serialize giống nhau. Tuy nhiên, với một số JavaScript, chúng ta có thể chuẩn hoá dữ liệu.
Tôi chọn căn chỉnh cấu trúc dữ liệu phần tử <select>
theo 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
}, [])
})
Giờ đây, bạn có thể gửi biểu mẫu một cách an toàn hoặc trong trường hợp của bản minh hoạ này, hãy hướng dẫn 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 hoạt động tương tác với hộp đánh dấu, nhưng tôi cảm thấy nên chia sẻ thêm số lượng kết quả và đảm bảo các lựa chọn của phần tử <select>
cũng được tính.
Lựa chọn <select>
của phần tử được phản ánh trong counter()
Trong phần chuẩn hoá dữ liệu, một trình nghe đã được tạo trên đầu vào. Khi kết thúc hàm này, số lượng bộ lọc đã chọn và số lượng kết quả cho các bộ lọc đó sẽ được xác định. Bạn có thể truyền các giá trị này đến phần tử vai trò trạng thái như sau.
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 thức 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, thao tác này hoàn tất thông báo "2 bộ lọc cho 25 kết quả".
Giờ đây, tất cả người dùng đều có thể trải nghiệm công nghệ hỗ trợ tuyệt vời của chúng tôi, bất kể họ tương tác với công nghệ này như thế nào.
Kết luận
Giờ bạn đã biết cách tôi làm, vậy bạn sẽ làm như thế nào‽ 🙂
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. Hãy tạo một bản minh hoạ, gửi đường liên kết cho tôi qua Twitter và tôi sẽ thêm bản minh hoạ đó 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 ở đây!