החלקה כדי לחשוף

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

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

איך מטמיעים

לא משנה אם אתם חושפים כפתורי פעולה בפריט ברשימה או בתפריט צדדי ליד חלונית שאפשר לגלול, המכניקה זהה: עוטפים את התוכן בחזית ואת החלונית הנסתרת בתוך מאגר גלילה אופקי חיצוני שמוגדר עם CSS Scroll Snap (scroll-snap-type: x mandatory) ו-overscroll-behavior: chain. מנוע הגלילה המקורי של הדפדפן מטפל בתנופה, בהצמדה, בגלילה של מיקוד המקלדת ובשרשור גלילה ללא מאזינים לתנועות ב-JavaScript.

שלב 1: מסמנים את רכיב ה-wrapper של ההחלקה, את התוכן ואת החלונית שנחשפת

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

<!-- Example A: List item with revealable action buttons -->
<div class="SwipeReveal">
  <div class="SwipeReveal-content">
    <strong>Design review notes</strong>
    <p>Updated component specs for the Q3 release.</p>
  </div>
  <div class="SwipeReveal-panel" role="group" aria-label="Item actions">
    <button type="button">Pin</button>
    <button type="button">Archive</button>
    <button type="button">Delete</button>
  </div>
</div>

<!-- Example B: Scrollable content pane with a revealable side menu -->
<div class="SwipeReveal" role="region" aria-label="Document preview with swipeable side menu">
  <div class="SwipeReveal-content is-scrollable" tabindex="0">
    <h2>Article Preview</h2>
    <p>Scroll vertically through this pane, or swipe left to reveal the side menu...</p>
  </div>
  <nav class="SwipeReveal-panel" aria-label="Contextual menu">
    <a href="#share">Share</a>
    <a href="#bookmark">Bookmark</a>
    <a href="#export">Export PDF</a>
  </nav>
</div>

שלב 2: מגדירים את הקונטיינר האופקי ואת overscroll-behavior: chain

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

ארבע מילות המפתח של overscroll-behavior קובעות באופן עצמאי אם גלילה מעבר לקצה מועברת למאגר גלילה אב (שרשור גלילה) ואם המאגר מציג אפשרויות גלילה מקומיות כמו מתיחה אלסטית של רצועת גומי או זוהר בקצה (אפקט גבול מקומי):

ערך שרשור גלילה (מופץ לאב הקדמון) אפקט גבול מקומי (גומי / זוהר) תפקיד ב"החלקה לחשיפה"
auto כן כן פעולת ברירת מחדל; צריך להשאיר את האפשרות הזו מופעלת בסרגלי גלילה של תוכן פנימי שבהם רוצים לקבל משוב רגיל על הקפצה, אבל להימנע ממנה במעטפת ההחלקה החיצונית שבה היא גורמת לתפריט להקפיץ
chain כן לא שימוש בסרגל הגלילה החיצוני של .SwipeReveal כדי ששרשראות גלילה עודפות יגללו את סרגל הגלילה הראשי (כמו המסמך) בלי שהתפריט או רצועת הפעולות יקפצו
contain לא כן הגלילה נלכדת בתוך הקונטיינר, אבל עדיין יש אפקט קפיצה מקומי
none לא לא חסימה של הקפצה מקומית ושל שרשור גלילה לאבות קדמונים

הופכים את .SwipeReveal לרשת עם שתי עמודות (grid-template-columns: 100% max-content) שבה .SwipeReveal-content נצמד אל start ו-.SwipeReveal-panel נצמד אל end:

.SwipeReveal {
  /* Column 1: full-width content. Column 2: auto-sized actions or side menu. */
  display: grid;
  grid-template-columns: 100% max-content;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;

  /* Prevent the swipe wrapper and revealed panel from bouncing locally while
     still chaining excess scroll into the parent scroller (the document). */
  overscroll-behavior: chain;
}

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

.SwipeReveal-content {
  scroll-snap-align: start;
  scroll-initial-target: nearest;
  background: Canvas;
}

.SwipeReveal-content:focus-visible {
  outline: auto;
  outline-offset: -2px;
}

.SwipeReveal-content.is-scrollable {
  block-size: 20rem;
  overflow: auto;
  /* Keeps default `overscroll-behavior: auto` so the content pane retains
     its normal bounce effect while chaining horizontal swipes to `.SwipeReveal`. */
}

.SwipeReveal-panel {
  /* Snapping a trailing panel to `end` (or a leading panel to `start`) brings
     all controls fully into view while keeping the adjacent edge of the content visible. */
  scroll-snap-align: end;
  display: flex;

  &:first-child {
    scroll-snap-align: start;
  }
}

אם רוצים להציג גם את אמצעי הבקרה בצד ימין (inline-start) באמצעות שלוש עמודות (grid-template-columns: max-content 100% max-content) או שתי עמודות (max-content 100%), צריך להציב את .SwipeReveal-panel לפני .SwipeReveal-content. בגלל שהמאפיין .SwipeReveal-content של scroll-initial-target: nearest מוגדר, מאגר הגלילה מתמקד בהתחלה בעמודת התוכן ולא בחלונית הראשית.

שלב 3: החזרת חלוניות תוכן שניתן להתמקד בהן לתצוגה באמצעות מיקוד מקלדת

הלחצנים או הקישורים בתוך .SwipeReveal-panel לא מוצגים במסך כשהם לא בשימוש, ולכן כשמקישים על Tab כדי להגיע אליהם באמצעות מקלדת, מתבצעת גלילה אוטומטית של .SwipeReveal כדי להציג את הפקד שבמיקוד. עם זאת, מכיוון ש-.SwipeReveal-content משתרע על פני 100% מהרוחב של מאגר התגים החיצוני, רובו נשאר בתוך יציאת הגלילה גם כשהחלונית הצדדית פתוחה. לכן, לחיצה על Shift+Tab כדי להחזיר את המיקוד אל .SwipeReveal-content (או אל רכיב שניתן להתמקד בו בתוכו) לא תגלול באופן אוטומטי את הטראק בחזרה אל 0.

מאזינים ל-focusin ב-.SwipeReveal-content ובודקים את event.target.matches(':focus-visible') כדי שמיקוד המקלדת בחלונית או בכל צאצא יחזיר את .SwipeReveal-content להתחלה בלי להפריע לקליקים של הסמן או לגרירות במגע:

for (const content of document.querySelectorAll('.SwipeReveal-content')) {
  content.addEventListener('focusin', (event) => {
    if (event.target.matches(':focus-visible')) {
      content.scrollIntoView({
        block: 'nearest',
        inline: 'start',
      });
    }
  });
}

שיטות מומלצות וטעויות נפוצות

  • מומלץ להשתמש ב-scroll-snap-type: x mandatory במקום ב-proximity. ב-proximity, החלקה קלה יכולה להשאיר את כפתורי הפעולה או את תפריט הצד חשופים חלקית במצב מנוחה.
  • מומלץ להגדיר את overscroll-behavior: chain בסרגל הגלילה האופקי החיצוני של החשיפה בהחלקה (.SwipeReveal) כדי שהגלילה תוכל להשתרשר לסרגל הגלילה הראשי בלי שהתפריט או רצועת הפעולות שנחשפו יקפצו.
  • מומלץ להשאיר את הערך overscroll-behavior: auto (ברירת המחדל) בחלוניות תוכן פנימיות שניתן לגלול בהן (.SwipeReveal-content.is-scrollable), שבהן רוצים שתהיה תגובת הקפצה רגילה, ועדיין לקשר החלקות אופקיות לסרגל הגלילה החיצוני.
  • מומלץ להטמיע פעולות שנחשפות ופריטים בתפריט הצדדי כפקדי DOM אמיתיים שאפשר להתמקד בהם (<button>, <a>). כש-.SwipeReveal-content הוא פקד שאפשר להתמקד בו או שהוא מכיל פקדים שאפשר להתמקד בהם, צריך להתאים מאזין focusin ל-event.target.matches(':focus-visible') כדי לקרוא ל-scrollIntoView({ block: 'nearest', inline: 'start' }), כך שמעבר חזרה לחלונית התוכן באמצעות Tab יסגור את החלונית הצדדית בלי להשפיע על האינטראקציות של הסמן.
  • אין ליירט אירועי wheel, touchmove או pointermove ב-JavaScript כדי לתרגם את התוכן באופן ידני או לדמות שרשור גלילה. יירוט תנועות ב-thread הראשי חוסם גלילה מבוססת-קומפוזיטור, משבש את המומנטום של הגלילה המקורית ופוגע בתגובתיות.

חלופי

כשמאגר .SwipeReveal ממקם .SwipeReveal-panel בצד שמאל (inline-start) לפני .SwipeReveal-content, דפדפנים שלא תומכים ב-scroll-initial-target: nearest יעבדו בהתחלה בהיסט גלילה 0 כשהחלונית הראשית חשופה.

אם יעד ה-Baseline לא תומך ב-scroll-initial-target, צריך לזהות תמיכה באמצעות CSS.supports('scroll-initial-target', 'nearest') ולהפעיל את scrollTo() עם behavior: 'instant' ב-load כדי שקונטיינרים עם חלונית ראשית יעברו ישירות אל .SwipeReveal-content בלי לגלול את הדף:

שיפור הדרגתי (progressive enhancement)

התייחסו אל overscroll-behavior: chain ו-scrollbar-width: none כשיפורים הדרגתיים (progressive enhancements) בלי חלופות JavaScript:

  • Automatic CSS cascade fallback: הערך הראשוני של overscroll-behavior הוא auto. דפדפנים שעדיין לא מזהים את מילת המפתח chain מתעלמים מ-overscroll-behavior: chain בזמן הניתוח ושומרים את auto. מכיוון ש-auto כבר מאפשר שרשור גלילה למאגרי גלילה קודמים, גם תנועת ההחלקה לחשיפה וגם שרשור הגלילה להורה פועלים בכל הדפדפנים באופן מיידי. דפדפנים תומכים פשוט מבטלים את ההקפצה הלא רצויה של הגומייה במעטפת ההחלקה החיצונית.
  • שינוי של contain או none: אם גיליון סגנונות בסיסי או מחלקת שירות מגדירים את overscroll-behavior: contain או none, צריך להצהיר על overscroll-behavior: auto מיד לפני overscroll-behavior: chain כדי שדפדפנים שעדיין לא תומכים ב-chain עדיין ישרשרו את הגלילה לאלמנט ההורה במקום ללכוד אותה.
  • הסתרת פס הגלילה (scrollbar-width: none): הסתרת פס הגלילה האופקי היא למטרות אסתטיות בלבד. אם מנוע ישן יותר מתעלם מ-scrollbar-width: none, הקונטיינר של החשיפה בהחלקה נשאר פונקציונלי לחלוטין עם פס גלילה רגיל.