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

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

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

דמו

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

סקירה כללית

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

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

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

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

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

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

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

אוספים

אוסף הוא מערך של אפשרויות לבחירה. בדף הבית של אב הטיפוס של הלחצן 'לחצן חזרה' שמוצג בפוסט הזה, האוספים הם: FPS,‏ RPG,‏ brawler,‏ dungeon crawler,‏ ספורט ופאזל.

פריטים

משחק וידאו הוא פריט, אוסף ספציפי יכול להיות גם פריט אם הוא מייצג אוסף אחר. לדוגמה, משחקי תפקידים הם פריט ואוסף תקף. אם מדובר בפריט, המשתמש נמצא בדף הקולקציה. לדוגמה, הן מופיעות בדף RPG, שבו מוצגת רשימה של משחקי RPG, כולל קטגוריות המשנה הנוספות AAA,‏ Indie ו-Self Published.

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

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

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

פריסות

Markup

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

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

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

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

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

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

סמלים

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

סגנונות

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

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

DevTools שמוצגת בו התאמת הניווט של נתיב הניווט עם התכונה של שכבת-העל של 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;
  }}
}

סגנונות ה-overflow מגדירים את חוויית המשתמש הבאה:

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

יש צורך במניעת אירועים מיידית בגלל השימוש ברכיב <select>. בדפדפן Windows Edge, וכנראה גם בדפדפנים אחרים, האירוע changed של הבחירה מופעל כשהמשתמש מנווט באפשרויות באמצעות המקלדת. לכן קראתי לזה 'מוקדש', כי המשתמש בחר באפשרות רק באופן סופי, כמו העברת העכבר מעל האפשרות או התמקדות בה, אבל לא אישר את הבחירה באמצעות 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> מופעל.

סיכום

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

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

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