בניית רכיב כרטיסיות

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

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

הדגמה

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

סקירה כללית

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

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

הקולאז&#39; מבולגן למדי בגלל המגוון העצום של סגנונות שהאינטרנט החילה על הקונספט של הרכיב
קולאז' של סגנונות עיצוב אתרים שכוללים רכיבי כרטיסיות מ-10 השנים האחרונות

טקטיקות אינטרנט

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

  • scroll-snap-points להחלקה אלגנטית ולאינטראקציות עם המקלדת, עם מיקומי עצירה מתאימים של גלילה
  • קישורי עומק דרך גיבובים של כתובות URL לתמיכה בעיגון גלילה בדף ובשיתוף של הדפדפן
  • תמיכה בקורא מסך עם תגי עיצוב של <a> ו-id="#hash"
  • prefers-reduced-motion להפעלה של מעברים עם עמעום הדרגתי וגלילה מיידית בדף
  • תכונת האינטרנט @scroll-timeline בתוך טיוטה, לשימוש בקו תחתון דינמי ולשינוי הצבע של הכרטיסייה שנבחרה

קבוצת ה-HTML

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

הדף כולל כמה חברים בתוכן מבני: קישורים ו-:target. אנחנו צריכים רשימה של קישורים, ש-<nav> יכול להתאים להם, ורשימה של רכיבי <article>, שלפיהם <section> יכול להיות שימושי מאוד. כל גיבוב של קישור יתאים לקטע, וכך הדפדפן יוכל לגלול דברים באמצעות עיגון.

לוחצים על לחצן קישור שמחליק בתוכן המיקוד

לדוגמה, לחיצה על קישור תמקד באופן אוטומטי את המאמר :target ב-Chrome 89, ללא צורך ב-JS. לאחר מכן המשתמש יכול לגלול את תוכן המאמר באמצעות מכשיר הקלט שלו, כמו תמיד. זהו תוכן משלים, כפי שמצוין בתגי העיצוב.

השתמשתי בתגי העיצוב הבאים כדי לארגן את הכרטיסיות:

<snap-tabs>
  <header>
    <nav>
      <a></a>
      <a></a>
      <a></a>
      <a></a>
    </nav>
  </header>
  <section>
    <article></article>
    <article></article>
    <article></article>
    <article></article>
  </section>
</snap-tabs>

אפשר ליצור חיבורים בין האלמנטים <a> ו-<article> עם המאפיינים href ו-id באופן הבא:

<snap-tabs>
  <header>
    <nav>
      <a href="#responsive"></a>
      <a href="#accessible"></a>
      <a href="#overscroll"></a>
      <a href="#more"></a>
    </nav>
  </header>
  <section>
    <article id="responsive"></article>
    <article id="accessible"></article>
    <article id="overscroll"></article>
    <article id="more"></article>
  </section>
</snap-tabs>

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

פריסות גלילה

ברכיב הזה יש 3 סוגים שונים של אזורי גלילה:

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

הגלילה כוללת שני סוגים שונים של רכיבים:

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

פריסה של <snap-tabs>

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

HTML
<snap-tabs>
  <header></header>
  <section></section>
</snap-tabs>
CSS
  snap-tabs {
  display: flex;
  flex-direction: column;

  /* establish primary containing box */
  overflow: hidden;
  position: relative;

  & > section {
    /* be pushy about consuming all space */
    block-size: 100%;
  }

  & > header {
    /* defend against 
needing 100% */ flex-shrink: 0; /* fixes cross browser quarks */ min-block-size: fit-content; } }

חוזרים לתרשים 3 הגלילה הצבעוני:

  • <header> מוכן עכשיו לשימוש בקונטיינר הגלילה (ורוד).
  • <section> מוכן לשמש כמאגר הגלילה (כחול).

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

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

פריסת <header> כרטיסיות

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

HTML
<snap-tabs>
  <header>
    <nav></nav>
    <span class="snap-indicator"></span>
  </header>
  <section></section>
</snap-tabs>
CSS
header {
  display: flex;
  flex-direction: column;
}

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

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

לאחר מכן, סגנונות הגלילה. מסתבר שאנחנו יכולים לשתף את סגנונות הגלילה בין שני אזורי הגלילה האופקיים (כותרת וקטע), ולכן יצרתי מחלקה של כלי עזר, .scroll-snap-x.

.scroll-snap-x {
  /* browser decide if x is ok to scroll and show bars on, y hidden */
  overflow: auto hidden;
  /* prevent scroll chaining on x scroll */
  overscroll-behavior-x: contain;
  /* scrolling should snap children on x */
  scroll-snap-type: x mandatory;

  @media (hover: none) {
    scrollbar-width: none;

    &::-webkit-scrollbar {
      width: 0;
      height: 0;
    }
  }
}

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

פריסת <nav> כותרת כרטיסיות

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

HTML
<nav>
  <a></a>
  <a></a>
  <a></a>
  <a></a>
</nav>
CSS
  nav {
  display: flex;

  & a {
    scroll-snap-align: start;

    display: inline-flex;
    align-items: center;
    white-space: nowrap;
  }
}

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

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

פריסת <section> כרטיסיות

הקטע הזה הוא פריט גמיש והוא צריך להיות הצרכן הדומיננטי של החלל. צריך גם ליצור עמודות שבהן יוצבו המאמרים. שוב, עבודה מהירה עבור CSS 2021! הרכיב block-size: 100% מותח את הרכיב הזה כדי למלא את הורה ככל האפשר, ואז לפריסה שלו נוצר סדרה של עמודות שהרוחב שלהן הוא 100% מהרוחב של תבנית ההורה. אחוזים עובדים כאן נהדר, כי כתבנו מגבלות מחמירות על ההורה.

HTML
<section>
  <article></article>
  <article></article>
  <article></article>
  <article></article>
</section>
CSS
  section {
  block-size: 100%;

  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
}

זה כאילו אנחנו אומרים "הרחב אנכית ככל האפשר, בצורה דחופה" (זוכרים את הכותרת שהגדרנו כ-flex-shrink: 0: זוהי הגנה מפני דחיפת ההרחבה), שמגדירה את גובה השורה של קבוצה של עמודות בגובה מלא. הסגנון auto-flow מורה לרשת תמיד לפרוס את הילדים בקו אופקי, בלי אריזה, בדיוק מה שאנחנו רוצים לעשות כדי לגלוש בחלון ההורה.

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

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

פריסת <article> כרטיסיות

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

HTML
<article>
  <h2></h2>
  <p></p>
  <p></p>
  <h2></h2>
  <p></p>
  <p></p>
  ...
</article>
CSS
article {
  scroll-snap-align: start;

  overflow-y: auto;
  overscroll-behavior-y: contain;
}

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

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

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

סיכום של 3 אזורי גלילה

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

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

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

כלי הפיתוח יכולים לעזור לנו לראות את זה:

באזורי הגלילה יש שכבות-על של רשת וכלים של flexbox, שמפרטים את השטח שהם תופסים ברכיב ואת כיוון הגלישה שלהם
Chromium Devtools, מציג את הפריסה של רכיב הניווט של Flexbox עם רכיבי עוגן, את הפריסה של קטעי הרשת שמלאים ברכיבי מאמרים ואת רכיבי הכתבה שמלאים בפסקאות ובאלמנט כותרת.

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

הדגשת תכונה

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

Animation

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

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

התנהגות הגלילה

יש הזדמנות לשפר את התנהגות התנועה של :target וגם של element.scrollIntoView(). כברירת מחדל, זה מיידי. הדפדפן רק מגדיר את מיקום הגלילה. ובכן, מה קורה אם אנחנו רוצים לעבור למיקום הגלילה במקום למצמץ?

@media (prefers-reduced-motion: no-preference) {
  .scroll-snap-x {
    scroll-behavior: smooth;
  }
}

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

אינדיקטור של כרטיסיות

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

ב-Chromium Devtools אפשר לשנות את ההעדפה ולהדגים את שני סגנונות המעבר השונים. נהניתי מאוד לבנות את זה.

@media (prefers-reduced-motion: reduce) {
  snap-tabs > header a {
    border-block-end: var(--indicator-size) solid hsl(var(--accent) / 0%);
    transition: color .7s ease, border-color .5s ease;

    &:is(:target,:active,[active]) {
      color: var(--text-active-color);
      border-block-end-color: hsl(var(--accent));
    }
  }

  snap-tabs .snap-indicator {
    visibility: hidden;
  }
}

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

רק עוד כמה שורות של CSS יגרמו לאנשים להרגיש שהם רואים אותם (במובן שאנחנו מכבדים את העדפות התנועה שלהם בזהירות). אני מת על זה.

@scroll-timeline

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

const { matches:motionOK } = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
);

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

if (motionOK) {
  // motion based animation code
}

נכון לעכשיו, התמיכה בדפדפן עבור @scroll-timeline אינה זמינה. מדובר במפרט טיוטה שכולל הטמעות ניסיוניות בלבד. אבל יש בו פוליפיל (Polyfill) שבו אני משתמש בהדגמה הזו.

ScrollTimeline

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

const sectionScrollTimeline = new ScrollTimeline({
  scrollSource: tabsection,  // snap-tabs > section
  orientation: 'inline',     // scroll in the direction letters flow
  fill: 'both',              // bi-directional linking
});

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

tabindicator.animate({
    transform: ...,
    width: ...,
  }, {
    duration: 1000,
    fill: 'both',
    timeline: sectionScrollTimeline,
  }
);

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

תמונות מפתח דינמיות

יש דרך CSS מוצהרת וחזקה מאוד ליצירת אנימציה עם @scroll-timeline, אבל האנימציה שבחרתי הייתה דינמית מדי. אין דרך לעבור בין הרוחב של auto, ואין דרך ליצור באופן דינמי כמה תמונות מפתח בהתאם לאורך של הילד או הילדה.

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

tabindicator.animate({
    transform: [...tabnavitems].map(({offsetLeft}) =>
      `translateX(${offsetLeft}px)`),
    width: [...tabnavitems].map(({offsetWidth}) =>
      `${offsetWidth}px`)
  }, {
    duration: 1000,
    fill: 'both',
    timeline: sectionScrollTimeline,
  }
);

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

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

מסגרות מפתח של TranslateX:

[...tabnavitems].map(({offsetLeft}) =>
  `translateX(${offsetLeft}px)`)

// results in 4 array items, which represent 4 keyframe states
// ["translateX(0px)", "translateX(121px)", "translateX(238px)", "translateX(464px)"]

תמונות מפתח ברוחב:

[...tabnavitems].map(({offsetWidth}) =>
  `${offsetWidth}px`)

// results in 4 array items, which represent 4 keyframe states
// ["121px", "117px", "226px", "67px"]

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

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

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

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

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

כך עשיתי זאת:

tabnavitems.forEach(navitem => {
  navitem.animate({
      color: [...tabnavitems].map(item =>
        item === navitem
          ? `var(--text-active-color)`
          : `var(--text-color)`)
    }, {
      duration: 1000,
      fill: 'both',
      timeline: sectionScrollTimeline,
    }
  );
});

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

[...tabnavitems].map(item =>
  item === navitem
    ? `var(--text-active-color)`
    : `var(--text-color)`)

// results in 4 array items, which represent 4 keyframe states
// [
  "var(--text-active-color)",
  "var(--text-color)",
  "var(--text-color)",
  "var(--text-color)",
]

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

נהניתי מאוד לכתוב את זה. כל כך הרבה.

עוד שיפורים ב-JavaScript

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

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

window.onload = () => {
  if (location.hash) {
    tabsection.scrollLeft = document
      .querySelector(location.hash)
      .offsetLeft;
  }
}

סנכרון סיום גלילה

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

כך אני מחכה לסיום הגלילה: js tabsection.addEventListener('scroll', () => { clearTimeout(tabsection.scrollEndTimer); tabsection.scrollEndTimer = setTimeout(determineActiveTabSection, 100); });

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

const determineActiveTabSection = () => {
  const i = tabsection.scrollLeft / tabsection.clientWidth;
  const matchingNavItem = tabnavitems[i];

  matchingNavItem && setActiveTab(matchingNavItem);
};

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

const setActiveTab = tabbtn => {
  tabnav
    .querySelector(':scope a[active]')
    .removeAttribute('active');

  tabbtn.setAttribute('active', '');
  tabbtn.scrollIntoView();
};

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

.scroll-snap-x {
  overflow: auto hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;

  @media (prefers-reduced-motion: no-preference) {
    scroll-behavior: smooth;
  }
}

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

סיכום

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

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

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