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

סקירה כללית בסיסית של אופן הבנייה של רכיב טוסט גמיש וגמיש.

בפוסט הזה אני רוצה לשתף אתכם במחשבה על האופן שבו ניתן לפתח רכיב להפעיל טוסט. כדאי לנסות את demo.

הדגמה

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

סקירה כללית

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

אינטראקציות

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

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
}

צילום מסך של קבוצת ההודעות הקוליות בין התגים head ו-body.

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

export default Toast

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

שימוש ברכיב Toast

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

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

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

import Toast from './toast.js'

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

סיכום

עכשיו, אחרי שהסברתי איך עשיתי את זה, איך היית? 🙂

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

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