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

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

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

דמו

אם אתם מעדיפים סרטון, הנה גרסה של הפוסט הזה ב-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>

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

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

הטכניקה 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() מאפשרת לגשת לערך מהתוכן של pseudo-element:

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 ומקרא.

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

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

סיכום

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

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

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

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