בניית רכיב של נתיבי ניווט

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

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

הדגמה

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

סקירה

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

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

חוויית המשתמש ברקע

בסרטון ההדגמה של הרכיבים שלמעלה, קטגוריות ה-placeholder הן ז'אנרים של משחקי וידאו. השביל הזה נוצר על ידי ניווט בנתיב הבא: home » rpg » indie » on sale, כפי שמוצג בהמשך.

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

ארכיטקטורת מידע

לדעתי כדאי להתייחס לאוספים ולפריטים.

אוספים

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

פריטים

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

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

const rawBreadcrumbData = {
  "FPS": {...},
  "RPG": {
    "AAA": {...},
    "indie": {
      "new": {...},
      "on sale": {...},
      "under 5": {...},
    },
    "self published": {...},
  },
  "brawler": {...},
  "dungeon crawler": {...},
  "sports": {...},
  "puzzle": {...},
}

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

פריסות

Markup

רכיבים טובים מתחילים ב-HTML מתאים. בקטע הבא אעבור על בחירות העיצוב שלי ואת האופן שבו הן משפיעות על הרכיב הכולל.

עיצוב כהה ובהיר

<meta name="color-scheme" content="dark light">

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

<nav class="breadcrumbs" role="navigation"></nav>

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

סמלים

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

כדי להשתמש בשיטה הזו, מוסיפים לדף רכיב SVG מוסתר ועוטפים את הסמלים ברכיב <symbol> עם מזהה ייחודי:

<svg style="display: none;">

  <symbol id="icon-home">
    <title>A home icon</title>
    <path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
  </symbol>

  <symbol id="icon-dropdown-arrow">
    <title>A down arrow</title>
    <path d="M19 9l-7 7-7-7"/>
  </symbol>

</svg>

הדפדפן קורא את ה-HTML של SVG, שומר את פרטי הסמל לזיכרון וממשיך בשאר הדף שמפנה למזהה לצורך שימושים נוספים בסמל, למשל:

<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
  <use href="#icon-home" />
</svg>

<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
  <use href="#icon-dropdown-arrow" />
</svg>

כלי פיתוח שמציגים רכיב SVG מעובד.

תוכלו להגדיר פעם אחת ולהשתמש בכמה פעמים שתרצו, עם השפעה מינימלית על ביצועי הדף ועיצוב גמיש. שימו לב שהמאפיין aria-hidden="true" נוסף לרכיב ה-SVG. הסמלים לא שימושיים למשתמשים שגולשים ורק שומעים את התוכן, אבל הסתרתם מהמשתמשים האלה מונעת מהם להוסיף רעשי רקע.

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

<span class="crumb">
  <a href="#sub-collection-b">Category B</a>
  <span class="crumbicon">
    <svg>...</svg>
    <select class="disguised-select" title="Navigate to another category">
      <option>Category A</option>
      <option selected>Category B</option>
      <option>Category C</option>
    </select>
  </span>
</span>

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

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

קישוטים למפריד

<span class="crumb-separator" aria-hidden="true">→</span>

לא חובה להוסיף מפרידים, אבל מומלץ להוסיף רק פריט אחד (ראו את הדוגמה השלישית בסרטון שלמעלה). לאחר מכן אני נותנת לכל aria-hidden="true" כי הם לקישוטיים ולא משהו שקורא המסך צריך להודיע עליהם.

המאפיין gap, שמופיע בהמשך, מאפשר ריווח ברור בין הנתונים האלו.

סגנונות

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

הכיוון והזרימה של הפריסה

כלי פיתוח שבהם מוצג יישור ניווט של נתיב ניווט עם תכונת שכבת-העל של Flexbox.

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

.breadcrumbs {
  --nav-gap: 2ch;

  display: flex;
  align-items: center;
  gap: var(--nav-gap);
  padding: calc(var(--nav-gap) / 2);
}

נתיב ניווט אחד מוצג לאורך עם שכבות-על של Flexbox.

כל .crumb יוצר גם פריסה אופקית אנכית עם פער מסוים, אבל מיועד במיוחד לצאצאים של הקישורים שלו ומציינים את הסגנון white-space: nowrap. זהו שלב חיוני בנתיבי ניווט מרובי מילים, כי אנחנו לא רוצים שהם יעברו כמה שורות. בהמשך הפוסט נוסיף סגנונות כדי לטפל בחריגות האופקיות שנכס white-space גרם.

.crumb {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--nav-gap) / 4);

  & > a {
    white-space: nowrap;

    &[aria-current="page"] {
      font-weight: bold;
    }
  }
}

הוספנו את aria-current="page" כדי שהקישור לדף הנוכחי יבלוט יותר מהשאר. למשתמשים בקורא מסך יהיה סימון ברור שהקישור מיועד לדף הנוכחי, אלא גם עיצבנו ויזואלי את הרכיב כדי לעזור למשתמשים רואים ליהנות מחוויית משתמש דומה.

הרכיב .crumbicon משתמש ברשת כדי לערום סמל SVG עם רכיב <select> 'כמעט בלתי נראה'.

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

.crumbicon {
  --crumbicon-size: 3ch;

  display: grid;
  grid: [stack] var(--crumbicon-size) / [stack] var(--crumbicon-size);
  place-items: center;

  & > * {
    grid-area: stack;
  }
}

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

.disguised-select {
  inline-size: 100%;
  block-size: 100%;
  opacity: .01;
  font-size: min(100%, 16px); /* Defaults to 16px; fixes iOS zoom */
}

אפשרויות נוספות

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

.breadcrumbs {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scroll-padding-inline: calc(var(--nav-gap) / 2);

  & > .crumb:last-of-type {
    scroll-snap-align: end;
  }

  @supports (-webkit-hyphens:none) { & {
    scroll-snap-type: none;
  }}
}

האפשרויות הנוספות מגדירות את חוויית המשתמש הבאה:

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

שאילתות מדיה

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

@media (width <= 480px) {
  .breadcrumbs .home-label {
    display: none;
  }
}

זה לצד זה של מיקומים באתר עם ובלי תווית בית, לצורך השוואה.

נגישות

תנועה

אין הרבה תנועה ברכיב הזה, אבל על ידי "גבולות" המעבר של הרכיב prefers-reduced-motion, אנחנו יכולים למנוע תנועה לא רצויה.

@media (prefers-reduced-motion: no-preference) {
  .crumbicon {
    transition: box-shadow .2s ease;
  }
}

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

JavaScript

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

שני מדדים חשובים של חוויית המשתמש לטיפול על ידי JavaScript: הבחירה השתנה ומניעת הפעלת האירועים של <select>.

נדרשת מניעת אירוע eager בגלל השימוש ברכיב <select>. ב-Windows Edge, וסביר להניח גם בדפדפנים אחרים, האירוע הנבחר changed מופעל כשהמשתמש מדפדפים בין האפשרויות באמצעות המקלדת. לכן קראתי לזה eager, כי המשתמש רק בחר באפשרות הזו, כמו העברת העכבר מעל או פוקוס, אבל עדיין לא אישר את הבחירה באמצעות enter או click. אירוע eager הופך את התכונה הזו של שינוי קטגוריית רכיב לבלתי נגישה, מפני שפתיחת תיבת הבחירה ודפדוף בפריט פשוט יפעיל את האירוע ותשנה את הדף לפני שהמשתמש יהיה מוכן.

אירוע <select> טוב יותר שהשתנה

const crumbs = document.querySelectorAll('.breadcrumbs select')
const allowedKeys = new Set(['Tab', 'Enter', ' '])
const preventedKeys = new Set(['ArrowUp', 'ArrowDown'])

// watch crumbs for changes,
// ensures it's a full value change, not a user exploring options via keyboard
crumbs.forEach(nav => {
  let ignoreChange = false

  nav.addEventListener('change', e => {
    if (ignoreChange) return
    // it's actually changed!
  })

  nav.addEventListener('keydown', ({ key }) => {
    if (preventedKeys.has(key))
      ignoreChange = true
    else if (allowedKeys.has(key))
      ignoreChange = false
  })
})

השיטה הזו היא לעקוב אחרי אירועי השבתה של המקלדת בכל רכיב <select>, ולקבוע אם המקש שנלחץ היה אישור ניווט (Tab או Enter) או ניווט מרחבי (ArrowUp או ArrowDown). על סמך ההחלטה הזו, הרכיב יכול להחליט להמתין או לעבור, כשהאירוע של הרכיב <select> מופעל.

סיכום

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

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

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