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

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

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

הדגמה

אם ברצונך ליצור סרטון, הנה גרסת YouTube של הפוסט הזה:

סקירה כללית

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

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

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

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

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

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

מומלץ לשים לב לאוספים ולפריטים.

אוספים

קולקציה כוללת מגוון אפשרויות לבחירה. מדף הבית של אב-הטיפוס של פוסט זה, האוספים הם FPS, RPG, brawler, סורק מבוכים, ספורט ופאזלים.

פריטים

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

מערכת כהה וערכת תאורה בהירה

<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, שמפורט בקטע הבא, מאפשר להוסיף בקלות רווחים בין התווים האלה.

סגנונות

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

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

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

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

אירוע <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> מופעל.

סיכום

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

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

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