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

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

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

דמו

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

סקירה כללית

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

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

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

טקטיקה באינטרנט

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

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

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

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

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

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

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

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

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

.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;
    }
  }
}

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

פריסה של <nav> בכותרת הכרטיסיות

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

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

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

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

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

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

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

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

כלי הפיתוח יכולים לעזור לנו להציג את זה באופן חזותי:

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

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

תכונות עיקריות

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

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

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

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

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

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

כרטיסייה פעילה וכרטיסייה לא פעילה מוצגות עם שכבות-על של 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)",
]

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

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

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

סיכום

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

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

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