סקירה בסיסית על האופן שבו אפשר ליצור רכיב כרטיסיות שדומה לאלה שנמצאים באפליקציות ל-iOS ולאפליקציות ל-Android.
בפוסט הזה אני רוצה לשתף את החשיבה על בניית רכיב כרטיסיות לאינטרנט שהוא רספונסיבי, תומך בקלט של כמה מכשירים ופועל בדפדפנים שונים. נסו את ההדגמה.
אם אתם מעדיפים לצפות בסרטון, הנה גרסת YouTube של הפוסט הזה:
סקירה כללית
כרטיסיות הן רכיב נפוץ במערכות עיצוב, אבל הן יכולות להופיע בהרבה צורות ופורמטים. בהתחלה היו כרטיסיות למחשב שמבוססות על רכיב <frame>, ועכשיו יש לנו רכיבים חלקים לנייד שמנפישים תוכן על סמך מאפיינים פיזיקליים.
כולם מנסים לעשות את אותו הדבר: לחסוך מקום.
כיום, חוויית המשתמש הבסיסית של כרטיסיות היא אזור ניווט עם כפתורים שמאפשרים להציג או להסתיר את התוכן במסגרת תצוגה. אזורי תוכן שונים חולקים את אותו מרחב, אבל מוצגים באופן מותנה בהתאם ללחצן שנבחר בסרגל הניווט.
טקטיקות לאתרים
בסך הכול, היה די פשוט לבנות את הרכיב הזה, בזכות כמה תכונות קריטיות של פלטפורמת האינטרנט:
scroll-snap-pointsלאינטראקציות אלגנטיות של החלקה ומקלדת עם מיקומי עצירה מתאימים לגלילה- קישורי עומק באמצעות גיבובים של כתובות URL לעיגון גלילה בדף ושיתוף תמיכה שמטופלים על ידי הדפדפן
- תמיכה בקורא מסך עם תגי עיצוב של רכיבי
<a>ו-id="#hash" prefers-reduced-motionלהפעלת מעברים הדרגתיים וגלילה מיידית בדף- תכונת האינטרנט
@scroll-timelineבטיוטה להוספת קו תחתון דינמי ולשינוי הצבע של הכרטיסייה שנבחרה
ה-HTML
בעיקרון, חוויית המשתמש כאן היא: לוחצים על קישור, כתובת ה-URL מייצגת את מצב הדף בתצוגת עץ, ואז רואים את העדכון של אזור התוכן כשהדפדפן גולל לרכיב התואם.
יש שם כמה רכיבי תוכן מבניים: קישורים ו-:target. אנחנו צריכים רשימה של קישורים, ש-<nav> מתאים לה, ורשימה של רכיבי <article>, ש-<section> מתאים לה. כל גיבוב (hash) של קישור יתאים לקטע,
כך שהדפדפן יוכל לגלול דברים באמצעות עיגון.
לדוגמה, לחיצה על קישור מתמקדת באופן אוטומטי במאמר :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 סוגים שונים של אזורי גלילה ברכיב הזה:
- אפשר לגלול את סרגל הניווט (ורוד) אופקית
- אפשר לגלול את אזור התוכן (כחול) לרוחב
- אפשר לגלול אנכית בכל פריט של מאמר (ירוק).
יש 2 סוגים שונים של רכיבים שקשורים לגלילה:
- חלון
תיבה עם מידות מוגדרות שכוללת את סגנון המאפייןoverflow. - משטח גדול מדי
בפריסה הזו, אלה מאגרי הרשימות: קישורי ניווט, מאמרים בקטעים ותוכן של מאמרים.
פריסה של <snap-tabs>
הפריסה ברמה העליונה שבחרתי הייתה flex (Flexbox). הגדרתי את הכיוון ל-column, כך שהכותרת והקטע מסודרים אנכית. זה חלון הגלילה הראשון שלנו, והוא מסתיר את כל מה שמוגדר כ-overflow hidden. בקרוב, הכותרת והקטע ישתמשו בגלילה מעבר לקצה כאזורים נפרדים.
<snap-tabs> <header></header> <section></section> </snap-tabs>
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 againstneeding 100% */ flex-shrink: 0; /* fixes cross browser quarks */ min-block-size: fit-content; } }
הפניה חזרה לתרשים הצבעוני של 3 המגילות:
- עכשיו
<header>מוכן להיות מאגר הגלילה (הוורוד). <section>מוכן להיות מאגר הגלילה (כחול).
המסגרות שהדגשתי למטה באמצעות VisBug עוזרות לנו לראות את החלונות שנוצרו על ידי מאגרי הגלילה.
פריסת כרטיסיות <header>
הפריסה הבאה כמעט זהה: אני משתמש ב-flex כדי ליצור סדר אנכי.
<snap-tabs>
<header>
<nav></nav>
<span class="snap-indicator"></span>
</header>
<section></section>
</snap-tabs>header { display: flex; flex-direction: column; }
ה-.snap-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, הכלה של הגלילה כדי ללכוד גלילה מעבר לקצה, פסי גלילה מוסתרים למכשירים, ולבסוף גלילה עם נקודות עצירה (scroll snapping) כדי לנעול את אזורי התצוגה של התוכן. סדר הכרטיסיות במקלדת נגיש, וכל האינטראקציות מנחות את המיקוד באופן טבעי. גם קונטיינרים של גלילה עם נקודות עצירה מקבלים אינטראקציה נחמדה בסגנון קרוסלה מהמקלדת.
פריסת הכותרת <nav> של הכרטיסיות
הקישורים לניווט צריכים להיות מסודרים בשורה, בלי מעברי שורה, ממורכזים אנכית, וכל פריט קישור צריך להיצמד לקונטיינר של הגלילה עם נקודות העצירה. Swift work for 2021 CSS!
<nav> <a></a> <a></a> <a></a> <a></a> </nav>
nav { display: flex; & a { scroll-snap-align: start; display: inline-flex; align-items: center; white-space: nowrap; } }
לכל קישור יש סגנון וגודל משלו, כך שבפריסת הניווט צריך לציין רק את הכיוון והזרימה. רוחב ייחודי של פריטי הניווט הופך את המעבר בין הכרטיסיות למהנה, כי האינדיקטור מתאים את הרוחב שלו ליעד החדש. בהתאם למספר הרכיבים שיש כאן, הדפדפן יציג או לא יציג פס גלילה.
פריסת כרטיסיות <section>
הקטע הזה הוא פריט גמיש וצריך להיות הצרכן הדומיננטי של המרחב. הוא גם צריך ליצור עמודות שבהן יוצבו המאמרים. שוב, עבודה מהירה
ל-CSS 2021! ה-block-size: 100% מותח את הרכיב הזה כדי למלא את ההורה ככל האפשר, ואז יוצר פריסה משלו עם סדרה של עמודות ברוחב 100% של ההורה. אחוזים עובדים מצוין כאן
כי כתבנו אילוצים חזקים על ההורה.
<section> <article></article> <article></article> <article></article> <article></article> </section>
section { block-size: 100%; display: grid; grid-auto-flow: column; grid-auto-columns: 100%; }
זה כאילו שאנחנו אומרים "תרחיב אנכית כמה שאפשר, בצורה אגרסיבית" (זוכרים את הכותרת שהגדרנו ל-flex-shrink: 0? היא הגנה מפני ההרחבה האגרסיבית הזו), מה שמגדיר את גובה השורה לקבוצה של עמודות בגובה מלא. הסגנון auto-flow מורה לרשת תמיד לפרוס את הצאצאים בשורה אופקית, בלי גלישה, בדיוק מה שאנחנו רוצים: לחרוג מחלון ההורה.
לפעמים קשה לי להבין את זה. רכיב הקטע הזה מתאים לתיבה, אבל גם יוצר קבוצה של תיבות. אני מקווה שההסברים והתמונות עוזרים.
פריסת כרטיסיות <article>
המשתמשים צריכים להיות מסוגלים לגלול את תוכן המאמר, וסרגלי הגלילה צריכים להופיע רק אם יש תוכן שגולש מהמסך. הרכיבים האלה במאמר נמצאים במיקום מסודר. הם בו-זמנית הורה לגלילה וצאצא לגלילה. הדפדפן מטפל כאן באינטראקציות מורכבות של מגע, עכבר ומקלדת.
<article> <h2></h2> <p></p> <p></p> <h2></h2> <p></p> <p></p> ... </article>
article { scroll-snap-align: start; overflow-y: auto; overscroll-behavior-y: contain; }
בחרתי שהמאמרים יתייצבו בתוך הגלילה של ההורה שלהם. אני ממש אוהב את האופן שבו פריטי הקישור לניווט ורכיבי המאמר נצמדים ל-inline-start של מאגרי הגלילה שלהם. זה נראה ומרגיש כמו מערכת יחסים הרמונית.
המאמר הוא צאצא של הרשת, והגודל שלו נקבע מראש כאזור של אזור התצוגה שבו אנחנו רוצים לספק חוויית משתמש של גלילה. זה אומר שאני לא צריך סגנונות גובה או רוחב כאן, אני רק צריך להגדיר איך הוא גולש. אני מגדיר את overflow-y ל-auto, ואז גם לוכד את האינטראקציות של הגלילה באמצעות המאפיין השימושי overscroll-behavior.
3 scroll areas recap
בתמונה הבאה, בחרתי בהגדרות המערכת שלי באפשרות "תמיד להציג פסי גלילה". אני חושב שחשוב מאוד שהפריסה תעבוד כשההגדרה הזו מופעלת, וחשוב גם שאבדוק את הפריסה ואת תזמון הגלילה.
לדעתי, הצגת פס הגלילה ברכיב הזה עוזרת להראות בבירור איפה נמצאים אזורי הגלילה, הכיוון שבו הם תומכים ואיך הם פועלים זה עם זה. חשבו איך כל אחד מהחלונות האלה עם פס הגלילה הוא גם רכיב הורה של פריסה מסוג flex או grid.
כלי הפיתוח יכולים לעזור לנו להמחיש את זה:
פריסות הגלילה הושלמו: הצמדה, קישור עומק ונגישות באמצעות המקלדת. בסיס חזק לשיפורים בחוויית המשתמש, לסגנון ולחוויה מהנה.
תכונה מומלצת
רכיבי הצאצא של הגלילה עם נקודות העצירה שומרים על המיקום הנעול שלהם במהלך שינוי הגודל. כלומר, 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, אני יכול להחליף את ההעדפה ולהדגים את 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, יונפש על סמך ציר זמן מותאם אישית, הגלילה של הקטע שלנו. כך משלימים את הקישור, אבל חסר הרכיב האחרון, נקודות עם מצב (stateful) שביניהן תתבצע האנימציה, שנקראות גם פריימים מרכזיים (keyframes).
קיפריימים דינמיים
יש דרך ממש יעילה להנפיש באמצעות @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 Keyframes:
[...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 פריימים מרכזיים, בהתאם למיקום של הגלילה עם נקודות העצירה (scroll snapping) של סרגל הגלילה של הקטע. נקודות ההצמדה יוצרות תיחום ברור בין הקיפריימים, ומוסיפות לתחושת הסנכרון של האנימציה.
המשתמש מפעיל את האנימציה באמצעות האינטראקציה שלו, ורואה את הרוחב והמיקום של האינדיקטור משתנים מקטע אחד למשנהו, תוך מעקב מושלם אחרי הגלילה.
אולי לא שמתם לב, אבל אני מאוד גאה במעבר הצבעים כשפריט הניווט המודגש נבחר.
האפור הבהיר יותר שלא נבחר נראה עוד יותר מאחור כשהפריט המודגש הוא בעל ניגודיות גבוהה יותר. מקובל להעביר צבע לטקסט, כמו במעבר מעל וכשנבחר, אבל זה השלב הבא להעביר את הצבע הזה בגלילה, מסונכרן עם מחוון הקו התחתון.
ככה עשיתי את זה:
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 ישירות לתוכן של כרטיסייה. הדפדפן ינווט בתוך הדף למזהה שתואם ל-hash של כתובת ה-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);
};
אם הגלילה עם נקודות העצירה (scroll snapping) פועלת, חלוקה של מיקום הגלילה הנוכחי ברוחב של אזור הגלילה אמורה להניב מספר שלם ולא מספר עשרוני. אחר כך אני מנסה לקבל פריט ניווט מהמטמון שלנו דרך האינדקס המחושב הזה, ואם הוא מוצא משהו, אני שולח את ההתאמה להגדרה כפעילה.
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 יכול לנהל את חוויית המשתמש באופן הצהרתי.
לפעמים הם יוצרים שילוב ממש נחמד.
סיכום
עכשיו, אחרי שאתם יודעים איך אני עשיתי את זה, איך אתם הייתם עושים את זה? זה מאפשר ליצור ארכיטקטורת רכיבים מעניינת. מי יצור את הגרסה הראשונה עם משבצות במסגרת האהובה עליו? 🙂
בואו נגוון את הגישות שלנו ונלמד את כל הדרכים לבנות באינטרנט. אתם יכולים ליצור Glitch, לצייץ לי את הגרסה שלכם, ואוסיף אותה לקטע Community remixes שבהמשך.
הרשאה לקהילה ליצור רמיקסים
- @devnook, @rob_dodson ו-@DasSurma עם רכיבי אינטרנט: מאמר.
- @jhvanderschee עם לחצנים: Codepen.