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

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

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

הדגמה

אם ברצונך ליצור סרטון, הנה גרסת 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>

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

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

צילום מסך של קורא המסך 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> הם ייחודיים:

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

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

צילום מסך של לוח ה-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 תוצאות".

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

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

סיכום

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

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

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

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