סקירה כללית בסיסית על יצירת רכיב רב-בחירה רספונסיבי, מותאם ונגיש, לצורך מיון וחיפוש של חוויית משתמש.
בפוסט הזה אני רוצה לשתף את הדרך שבה פיתחתי רכיב לבחירה מרובה. נסו את ההדגמה.
אם אתם מעדיפים סרטון, הנה גרסה של הפוסט הזה ב-YouTube:
סקירה כללית
לרוב מוצגים למשתמשים פריטים, לפעמים הרבה פריטים, ובמקרים כאלה כדאי לספק דרך לצמצם את הרשימה כדי למנוע עומס בחירה. בפוסט הזה בבלוג אנחנו בודקים את ממשק המשתמש של הסינון כדי לצמצם את האפשרויות. כדי לעשות זאת, המערכת מציגה מאפייני פריטים שהמשתמשים יכולים לבחור או לבטל את הבחירה שלהם, וכך מצמצמת את מספר התוצאות וכתוצאה מכך את עומס הבחירות.
אינטראקציות
המטרה היא לאפשר מעבר מהיר של אפשרויות הסינון לכל המשתמשים ולכל סוגי הקלט השונים. הוא יינתן עם זוג רכיבים שניתנים להתאמה ורספונסיבית. סרגל צד מסורתי של תיבות סימון למחשב, למקלדת ולקוראי מסך, ו<select
multiple>
למשתמשי מגע.
ההחלטה להשתמש באפשרות הבחירה בכמה פריטים מובנית למגע, ולא למחשב, חוסכת עבודה ויוצרת עבודה, אבל לדעתי היא מספקת חוויות מתאימות עם פחות חובות קוד מאשר פיתוח כל חוויית השימוש הרספונסיבית ברכיב אחד.
מגע
רכיב המגע חוסך מקום ומאפשר אינטראקציה מדויקת יותר של המשתמשים בנייד. כדי לחסוך מקום, אפשר לכווץ סרגל צד שלם של תיבות סימון ל<select>
שכבת-על מובנית עם ממשק מגע. היא עוזרת לשפר את הדיוק של הקלט על ידי הצגת שכבת-על גדולה למגע שמספקת המערכת.
מקלדת וגיימפאד
בהמשך מופיעה הדגמה של שימוש ב-<select multiple>
מהמקלדת.
אי אפשר לשנות את העיצוב של האפשרות המובנית הזו לבחירת כמה פריטים, והיא מוצגת רק בפריסה קומפקטית שלא מתאימה להצגת הרבה אפשרויות. ראיתם איך אתם לא באמת יכולים לראות את מגוון האפשרויות בתוך התיבה הקטנה הזו? למרות שאפשר לשנות את הגודל שלו, הוא עדיין לא שימושי כמו סרגל צד של תיבות סימון.
Markup
שני הרכיבים ייכללו באותו רכיב <form>
. המערכת תבדוק את התוצאות של הטופס הזה, בין אם מדובר בתיבות סימון או בבחירה מרובה, ונשתמש בהן כדי לסנן את הרשת, אבל אפשר גם לשלוח אותן לשרת.
<form>
</form>
רכיב של תיבות סימון
קבוצות של תיבות סימון צריכות להיות עטופות ברכיב <fieldset>
ולהקצות להן את הערך <legend>
.
כשה-HTML בנוי בצורה הזו, קוראי מסך ו-FormData יבינו באופן אוטומטי את הקשר בין הרכיבים.
<form>
<fieldset>
<legend>New</legend>
… checkboxes …
</fieldset>
</form>
כשהקיבוץ נוצר, מוסיפים <label>
ו-<input type="checkbox">
לכל אחד מהמסננים. בחרתי לעטוף את התוויות שלי ב-<div>
כדי שמאפיין ה-CSS gap
יוכל לפזר אותן באופן שווה ולשמור על ההתאמה כשהתוויות נפרשות על כמה שורות.
<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>
מעקב אחרי קלט באמצעות דלפקים כדי לשפר את הטכנולוגיה המסייעת
הטכניקה status
role
משמשת בחוויית המשתמש הזו כדי לעקוב אחרי מספר המסננים ולעדכן אותו עבור קוראי מסך וטכנולוגיות מסייעות אחרות. הסרטון ב-YouTube מדגים את התכונה. השילוב מתחיל ב-HTML ובמאפיין role="status"
.
<div role="status" class="sr-only" id="applied-filters"></div>
הרכיב הזה יקריא בקול את השינויים שבוצעו בתוכן. אנחנו יכולים לעדכן את התוכן באמצעות מספרים ב-CSS כשהמשתמשים מקישים על התיבות הסימון. לשם כך, קודם כול צריך ליצור ספירה עם שם ברכיב הורה של רכיבי הקלט ורכיב המצב.
aside {
counter-reset: filters;
}
כברירת מחדל, המספר יהיה 0
, וזה מצוין כי אף דבר לא מוגדר כברירת מחדל כ-:checked
בתכנון הזה.
בשלב הבא, כדי להגדיל את המונה החדש שיצרנו, נטרגט ילדים של הרכיב <aside>
שהם :checked
. כשהמשתמש משנה את המצב של הקלט, המונה filters
יתעדכן.
aside :checked {
counter-increment: filters;
}
שירות ה-CSS מודע עכשיו לפרטים הכלליים של ממשק המשתמש של תיבות הסימון, ורכיב תפקיד הסטטוס ריק וממתין לערכים. מכיוון ששירות CSS שומר על המספר בזיכרון, הפונקציה counter()
מאפשרת לגשת לערך מתוכן של רכיב מדומה:
aside #applied-filters::before {
content: counter(filters) " filters ";
}
עכשיו ה-HTML של רכיב התפקיד 'סטטוס' יכריז על '2 מסננים' לקורא מסך. זהו התחלה טובה, אבל אנחנו יכולים לעשות יותר, למשל לשתף את ספירת התוצאות שהמסננים עדכנו. נעשה את העבודה הזו מ-JavaScript, כי היא לא מה שמונים יכולים לעשות.
התרגשות מקונן
אלגוריתם המונה הרגיש טוב עם הצבת ה-CSS -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
כי בדרך כלל מחובר עכבר או מכשיר קלט אחר עם דיוק גבוה.
קבוצות השדות
העיצוב והפריסה של <fieldset>
עם <legend>
הם ייחודיים כברירת מחדל:
בדרך כלל, כדי ליצור רווח בין רכיבי הצאצא, משתמשים במאפיין gap
, אבל המיקום הייחודי של <legend>
מקשה ליצור קבוצה של צאצאים עם רווחים שווים ביניהם. במקום gap
, נעשה שימוש בבורר של אח/ה צמוד/ה וב-margin-block-start
.
fieldset {
padding: 2ch;
& > div + div {
margin-block-start: 2ch;
}
}
כך ניתן למנוע את שינוי השטח של <legend>
על ידי טירגוט של <div>
ילדים בלבד.
תווית הסינון תיבת הסימון
כצאצא ישיר של <fieldset>
ובתוך הרוחב המקסימלי של 30ch
בטופס, טקסט התווית עשוי לעבור שורה אם הוא ארוך מדי. זה שימושי מאוד לגלישת טקסט, אבל לא לחוסר התאמה בין הטקסט לתיבת הסימון. Flexbox הוא פתרון אידיאלי לכך.
fieldset > div {
display: flex;
gap: 2ch;
align-items: baseline;
}
התצוגה של הרשת האנימציה
אנימציית הפריסה מתבצעת על ידי Isotope. פלאגין יעיל וחזק לסינון ולמיון אינטראקטיבי.
JavaScript
בנוסף לסיוע בתזמור של רשת אינטראקטיבית ומוארת, JavaScript משמשת גם לליטוש כמה פינות לא חלקות.
נירמול הקלט של המשתמשים
בתכנון הזה יש טופס אחד עם שתי דרכים שונות לספק קלט, והן לא מסדרות אותו. עם זאת, בחלק מ-JavaScript אנחנו יכולים לנרמל את הנתונים.
בחרתי להתאים את מבנה הנתונים של הרכיב <select>
למבנה תיבות הסימון המקובצות. לשם כך, מוסיפים למאפיין <select>
מאזין לאירועים מסוג input
, ואז מתבצע מיפוי של 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 יכול לזהות אינטראקציה עם תיבות הסימון, ואחרי סינון הרשת, להוסיף את textContent
כמו שהרכיב <select>
עשה.
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 תוצאות'.
עכשיו חוויית השימוש הטובה שלנו בטכנולוגיה מסייעת תהיה זמינה לכל המשתמשים, בכל דרך שבה הם יוצרים איתה אינטראקציה.
סיכום
עכשיו, אחרי שהסברתי איך עשיתי את זה, איך היית? 🙂
נרחיב את הגישות שלנו ונלמד את כל הדרכים לפיתוח באינטרנט. אפשר ליצור הדגמה, לשלוח ציוץ לי ולהוסיף אותה לקטע 'רמיקסים של הקהילה' בהמשך!
רמיקסים של הקהילה
עדיין אין מה לראות כאן