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

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

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

הדגמה

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

סקירה כללית

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

אינטראקציות

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

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

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

מגע

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

א&#39;
צילום מסך של אלמנט הבחירה המרבה ב-Chrome ב-Android, ב-iPhone
אייפד. ב-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>

צילום מסך של רינדור שולחן העבודה של רכיב מסוג &#39;בחירה מרובה&#39;.

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

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

תצוגת הרשת עם אנימציה

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

JavaScript

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

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

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

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

בחרתי להתאים את מבנה הנתונים של הרכיב <select> לתיבות הסימון המקובצות שלנו. כדי לעשות זאת, input ה-event listener יתווסף לרכיב <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.

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

סיכום

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

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

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

אין כאן שום דבר עדיין לראות!