בניית רכיב של בחירה מרובה

סקירה כללית בסיסית על יצירת רכיב רב-בחירה רספונסיבי, מותאם ונגיש, לצורך מיון וחיפוש של חוויית משתמש.

בפוסט הזה אני רוצה לשתף את הדרך שבה פיתחתי רכיב לבחירה מרובה. נסו את ההדגמה.

דמו

אם אתם מעדיפים סרטון, הנה גרסה של הפוסט הזה ב-YouTube:

סקירה כללית

לרוב מוצגים למשתמשים פריטים, לפעמים הרבה פריטים, ובמקרים כאלה כדאי לספק דרך לצמצם את הרשימה כדי למנוע עומס בחירה. בפוסט הזה בבלוג אנחנו בודקים את ממשק המשתמש של הסינון כדי לצמצם את האפשרויות. כדי לעשות זאת, המערכת מציגה מאפייני פריטים שהמשתמשים יכולים לבחור או לבטל את הבחירה שלהם, וכך מצמצמת את מספר התוצאות וכתוצאה מכך את עומס הבחירות.

אינטראקציות

המטרה היא לאפשר מעבר מהיר של אפשרויות הסינון לכל המשתמשים ולכל סוגי הקלט השונים. הוא יינתן עם זוג רכיבים שניתנים להתאמה ורספונסיבית. סרגל צד מסורתי של תיבות סימון למחשב, למקלדת ולקוראי מסך, ו<select multiple> למשתמשי מגע.

צילום מסך של השוואה בין גרסת המחשב בצבעים בהיר וחושך עם סרגל צד של תיבות סימון לבין גרסת iOS ו-Android לנייד עם רכיב לבחירת מספר פריטים.

ההחלטה להשתמש באפשרות הבחירה בכמה פריטים מובנית למגע, ולא למחשב, חוסכת עבודה ויוצרת עבודה, אבל לדעתי היא מספקת חוויות מתאימות עם פחות חובות קוד מאשר פיתוח כל חוויית השימוש הרספונסיבית ברכיב אחד.

מגע

רכיב המגע חוסך מקום ומאפשר אינטראקציה מדויקת יותר של המשתמשים בנייד. כדי לחסוך מקום, אפשר לכווץ סרגל צד שלם של תיבות סימון ל<select> שכבת-על מובנית עם ממשק מגע. היא עוזרת לשפר את הדיוק של הקלט על ידי הצגת שכבת-על גדולה למגע שמספקת המערכת.

תצוגה מקדימה של צילום מסך של הרכיב עם כמה אפשרויות ב-Chrome ב-Android, ב-iPhone וב-iPad. ב-iPad וב-iPhone מופעלת מצב &#39;בחירה מרובה&#39;, וכל אחד מהם מספק חוויה ייחודית שעברה אופטימיזציה לגודל המסך.

מקלדת וגיימפאד

בהמשך מופיעה הדגמה של שימוש ב-<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>

צילום מסך עם שכבת-על מידעית של המקרא ורכיבי fieldset, מוצגים הצבע ושם הרכיב.

רכיב <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, כי היא לא מה שמונים יכולים לעשות.

צילום מסך של קורא המסך של MacOS שמקריא את מספר המסננים הפעילים.

התרגשות מקונן

אלגוריתם המונה הרגיש טוב עם הצבת ה-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> הם ייחודיים כברירת מחדל:

צילום מסך של סגנונות ברירת המחדל של fieldset ומקרא.

בדרך כלל, כדי ליצור רווח בין רכיבי הצאצא, משתמשים במאפיין 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;
}
צילום מסך שבו מוצג איך סימן הווי מיושר לשורת הטקסט הראשונה בתרחיש של גלישת טקסט בכמה שורות.
אפשר לשחק במשחקים נוספים ב-Codepen

התצוגה של הרשת האנימציה

אנימציית הפריסה מתבצעת על ידי Isotope. פלאגין יעיל וחזק לסינון ולמיון אינטראקטיבי.

JavaScript

בנוסף לסיוע בתזמור של רשת אינטראקטיבית ומוארת, JavaScript משמשת גם לליטוש כמה פינות לא חלקות.

נירמול הקלט של המשתמשים

בתכנון הזה יש טופס אחד עם שתי דרכים שונות לספק קלט, והן לא מסדרות אותו. עם זאת, בחלק מ-JavaScript אנחנו יכולים לנרמל את הנתונים.

צילום מסך של מסוף JavaScript ב-DevTools שבו מוצגים התוצאות של הנתונים המנורמלים של היעד.

בחרתי להתאים את מבנה הנתונים של הרכיב <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 תוצאות'.

צילום מסך של הכרזה על התוצאות של קורא המסך ב-MacOS.

עכשיו חוויית השימוש הטובה שלנו בטכנולוגיה מסייעת תהיה זמינה לכל המשתמשים, בכל דרך שבה הם יוצרים איתה אינטראקציה.

סיכום

עכשיו, אחרי שהסברתי איך עשיתי את זה, איך היית? 🙂

נרחיב את הגישות שלנו ונלמד את כל הדרכים לפיתוח באינטרנט. אפשר ליצור הדגמה, לשלוח ציוץ לי ולהוסיף אותה לקטע 'רמיקסים של הקהילה' בהמשך!

רמיקסים של הקהילה

עדיין אין מה לראות כאן