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

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

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

הדגמה

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

סקירה כללית

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

אינטראקציות

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

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

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

מגע

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

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

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

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

צילום מסך של עיבוד במחשב של רכיב בחירה מרובה.

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

הטכניקה של סטטוס תפקיד משמשת בחוויית המשתמש הזו כדי לעקוב אחרי מספר המסננים ולשמור אותו עבור קוראי מסך וטכנולוגיות מסייעות אחרות. הסרטון ב-YouTube מדגים את התכונה. השילוב מתחיל ב-HTML ובמאפיין role="status".

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

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

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 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> הם ייחודיים:

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

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

הטבלה המונפשת

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

JavaScript

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

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

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

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

בחרתי להתאים את מבנה הנתונים של רכיב <select> למבנה של תיבות הסימון המקובצות. לשם כך, מוסיפים event listener‏ 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()

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

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 שמקריא תוצאות.

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

סיכום

עכשיו כשאתה יודע איך עשיתי את זה, איך היית עושה את זה‽ 🙂

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

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

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