סקירה כללית בסיסית שמסבירה איך ליצור רכיב רספונסיבי, נגיש ונגיש, שמאפשר לבחור כמה אפשרויות למיון ולסינון של חוויות המשתמש.
בפוסט הזה אני רוצה לשתף את המחשבה שלי על בניית רכיב של בחירה מרובה. תוכלו לנסות את ההדגמה.
אם ברצונך ליצור סרטון, הנה גרסת 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>
מעקב אחר קלט באמצעות דלפקים כדי לשפר את הטכנולוגיה המסייעת
בחוויית המשתמש הזו משתמשים בשיטה תפקיד הסטטוס, כדי לעקוב אחרי ספירת המסננים לקוראי מסך וטכנולוגיות מסייעות אחרות, ולתחזק אותם. הסרטון ב-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 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
, מכיוון שמקובל לחבר עכבר או מכשיר קלט אחר ברמת דיוק גבוהה.
קבוצות השדות
העיצוב והפריסה המוגדרים כברירת מחדל של <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;
}
![צילום מסך שמראה איך סימן הווי מיושר לשורת הטקסט הראשונה בתרחיש של גלישת טקסט בכמה שורות.](https://web.dev/static/articles/building/a-multi-select-component/image/screenshot-showing-the-c-b135285ef7d8a.png?authuser=5&hl=he)
אנימציה של רשת
אנימציית הפריסה מבוצעת על ידי Isotope. פלאגין יעיל וחזק למיון וסינון אינטראקטיבי.
JavaScript
בנוסף לעזרה בתזמור רשת מונפשת ואינטראקטיבית בצורה מסודרת, JavaScript משמש לליטוש שני קצוות גסים.
נרמול הקלט של המשתמש
לעיצוב הזה יש צורה אחת עם שתי דרכים שונות לספק קלט, והם לא משלימים את אותו התוכן. אבל בעזרת 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()
בקטע של נורמליזציה של הנתונים, כבר נוצר מאזין לאחר הקלט. בסוף הפונקציה הזו ידוע מספר המסננים שנבחרו ומספר התוצאות של המסננים האלה. ניתן להעביר את הערכים לאלמנט של State role כך.
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 תוצאות".
מעכשיו, החוויה המצוינת של הטכנולוגיה המסייעת שלנו תישלח לכל המשתמשים, בכל אינטראקציה שלהם איתה.
סיכום
עכשיו, אחרי שאת יודעת איך עשיתי את זה, איך היית רוצה ‽ 🙂
בואו נגוון את הגישות שלנו ונלמד את כל הדרכים לבנות באינטרנט. צור הדגמה (דמו), ציוץ לי קישורים ואני אוסיף אותה לקטע 'רמיקסים של הקהילה' למטה!
רמיקסים של הקהילה
עדיין אין מה לראות כאן!