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

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

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

פריסות גלילה

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

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

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

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

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

ברכיבים &#39;ניווט&#39; ו-&#39;span.indicator&#39; יש שכבות-על של נקודה לשיתוף אינטרנט, שמתארת את המרחב שהם תופסים ברכיב

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

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

תכונה בולטת

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

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

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

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

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

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

סיכום

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

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

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