בניית רכיב של הודעה קופצת

סקירה בסיסית של תהליך הבנייה של רכיב הודעה גמיש ונגיש.

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

הדגמה

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

סקירה כללית

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

אינטראקציות

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

Markup

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

טוסט

<output class="gui-toast">Item added to cart</output>

זה יכול להיות יותר כולל על ידי הוספה של role="status". זו גם חלופה אם הדפדפן לא נותן לרכיבי <output> את התפקיד המשתמע לפי המפרט.

<output role="status" class="gui-toast">Item added to cart</output>

קופסת טוסט

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

<section class="gui-toast-group">
  <output role="status">Wizard Rose added to cart</output>
  <output role="status">Self Watering Pot added to cart</output>
</section>

פריסות

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

מאגר GUI

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

.gui-toast-group {
  position: fixed;
  z-index: 1;
  inset-block-end: 0;
  inset-inline: 0;
  padding-block-end: 5vh;
}

צילום מסך עם גודל ומרווח פנימי של תיבת כלי הפיתוח ושכבת-על מעל רכיב של מאגר תגים מסוג gui-toast-container.

בנוסף למיקום של עצמו באזור התצוגה, קונטיינר של טוסט הוא מאגר רשת שיכול ליישר ולהפיץ טוסטים. הפריטים ממורכזים כקבוצה עם justify-content וכל אחד מהם במרכז של justify-items. זורקים קצת gap כדי שהטוסטים לא ייגעו.

.gui-toast-group {
  display: grid;
  justify-items: center;
  justify-content: center;
  gap: 1vh;
}

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

השקה של GUI

בכל טוסט יש כמה padding, כמה פינות רכות יותר עם border-radius, ופונקציה min() שעוזרת לשנות את הגודל של הנייד והמחשב. הגודל הרספונסיבי ב-CSS הבא מונע התרחבות של טוסטים ליותר מ-90% מאזור התצוגה או מ-25ch.

.gui-toast {
  max-inline-size: min(25ch, 90vw);
  padding-block: .5ch;
  padding-inline: 1ch;
  border-radius: 3px;
  font-size: 1rem;
}

צילום מסך של רכיב אחד עם הסיומת .gui-toast, עם המרווח הפנימי ורדיוס הגבול.

סגנונות

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

קופסת טוסטים

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

.gui-toast-group {
  pointer-events: none;
}

השקה של GUI

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

.gui-toast {
  --_bg-lightness: 90%;

  color: black;
  background: hsl(0 0% var(--_bg-lightness) / 90%);
}

@media (prefers-color-scheme: dark) {
  .gui-toast {
    color: white;
    --_bg-lightness: 20%;
  }
}

Animation

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

אלה תמונות המפתח שמשמשות לאנימציה של ההודעה המתוכננת. שירות ה-CSS ישלוט בכניסה, בזמן ההמתנה וביציאה מהסרטון – והכול באנימציה אחת.

@keyframes fade-in {
  from { opacity: 0 }
}

@keyframes fade-out {
  to { opacity: 0 }
}

@keyframes slide-in {
  from { transform: translateY(var(--_travel-distance, 10px)) }
}

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

.gui-toast {
  --_duration: 3s;
  --_travel-distance: 0;

  will-change: transform;
  animation: 
    fade-in .3s ease,
    slide-in .3s ease,
    fade-out .3s ease var(--_duration);
}

@media (prefers-reduced-motion: no-preference) {
  .gui-toast {
    --_travel-distance: 5vh;
  }
}

JavaScript

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

import Toast from './toast.js'

Toast('My first toast')

יצירת קבוצת טוסטים וטוסטים

כשמודול ההודעה הראשונה נטען מ-JavaScript, צריך ליצור מאגר של הודעה משולשת ולהוסיף אותו לדף. בחרתי להוסיף את הרכיב לפני body, לכן לא סביר להניח שיהיו בעיות בסידור בערימה של z-index כי הקונטיינר נמצא מעל המאגר של כל האלמנטים בגוף.

const init = () => {
  const node = document.createElement('section')
  node.classList.add('gui-toast-group')

  document.firstElementChild.insertBefore(node, document.body)
  return node
}

צילום מסך של קבוצת הטוסט בין תגי הראש ותגי הגוף.

הפונקציה init() נקראת באופן פנימי מהמודול, שומרת את הרכיב כ-Toaster:

const Toaster = init()

יצירת רכיב HTML של ההעברה מתבצעת באמצעות הפונקציה createToast(). הפונקציה מחייבת טקסט כלשהו לטוסט, יוצרת אלמנט <output>, מעטרת אותו בחלק מהמחלקות ומהמאפיינים, מגדירה את הטקסט ומחזירה את הצומת.

const createToast = text => {
  const node = document.createElement('output')
  
  node.innerText = text
  node.classList.add('gui-toast')
  node.setAttribute('role', 'status')

  return node
}

ניהול מודעה אחת או יותר

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

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

  Toaster.children.length && motionOK
    ? flipToast(toast)
    : Toaster.appendChild(toast)
}

בעת הוספת ההודעה המשולשת הראשונה, Toaster.appendChild(toast) מוסיף טוסט לדף שמפעיל את אנימציות ה-CSS: אנימציה, המתנה 3s, והנפשה. מתבצעת קריאה לפונקציה flipToast() כשיש טוסטים קיימים, באמצעות טכניקה שנקראת FLIP של פול לואיס. הרעיון הוא לחשב את ההבדל בין המיקומים השונים של המכל, לפני ואחרי הוספת הטוסט החדש. אפשר לחשוב על זה כמו לסמן איפה הטוסטר נמצא עכשיו, איפה הוא אמור להיות, ואז ליצור אנימציה מהמקום שבו הוא נמצא.

const flipToast = toast => {
  // FIRST
  const first = Toaster.offsetHeight

  // add new child to change container size
  Toaster.appendChild(toast)

  // LAST
  const last = Toaster.offsetHeight

  // INVERT
  const invert = last - first

  // PLAY
  const animation = Toaster.animate([
    { transform: `translateY(${invert}px)` },
    { transform: 'translateY(0)' }
  ], {
    duration: 150,
    easing: 'ease-out',
  })
}

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

חיבור כל ה-JavaScript

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

const Toast = text => {
  let toast = createToast(text)
  addToast(toast)

  return new Promise(async (resolve, reject) => {
    await Promise.allSettled(
      toast.getAnimations().map(animation => 
        animation.finished
      )
    )
    Toaster.removeChild(toast)
    resolve() 
  })
}

הרגשתי שהחלק המבלבל של הקוד הזה נמצא בפונקציה Promise.allSettled() ובמיפוי toast.getAnimations(). מכיוון שהשתמשתי בכמה אנימציות של תמונות מפתח לטוסט, כדי לדעת בביטחון שכולן הושלמו, צריך לבקש מ-JavaScript כל אחת מהן וכל אחת מההבטחות שלהם ל-finished. allSettled עובדת עבורנו, ומתאוששת כהשלמה לאחר שכל ההבטחות שלה ימולאו. כשמשתמשים ב-await Promise.allSettled(), השורה הבאה של הקוד יכולה להסיר את האלמנט בצורה בטוחה ואמינה שמחזור החיים של הטוסט הושלם. לבסוף, קריאה ל-resolve() ממלאת את ההבטחה של Toast ברמה גבוהה, כך שהמפתחים יוכלו לנקות או לבצע עבודה אחרת אחרי הצגת ההודעה הקופצת.

export default Toast

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

שימוש ברכיב Toast

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

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

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

import Toast from './toast.js'

async function example() {
  await Toast('Wizard Rose added to cart')
  console.log('toast finished')
}

סיכום

עכשיו, אחרי שאת יודעת איך עשיתי את זה, איך היית רוצה ‽ 🙂

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

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