מעברים באותו מסמך

תכונות לאתרים: מעברים בין תצוגות

הבעיה

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

הפתרון

View Transitions API מאפשר לכם לציין זוגות של רכיבים שקיימים במצבים שונים לפני ואחרי מעבר. כשמפעילים מעבר באמצעות document.startViewTransition() בניווט בדף יחיד (SPA), הדפדפן מזהה את הרכיבים המשותפים האלה לפי ה-view-transition-name הייחודי המשותף שלהם. לאחר מכן, הוא מחשב באופן אוטומטי את ההבדל במיקום, בגודל ובסגנון שלהם, ויוצר אנימציה חלקה מהמצב הישן למצב החדש. המעבר הזה מתרחש בשכבה העליונה, אפילו מעל רכיבים עם ערכי z-index גבוהים.

מדריך הטמעה

שלב 1: עוטפים את השינויים במצב ב-startViewTransition

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

function navigate(view) {
  // MANDATORY: Wrap the update in startViewTransition
  document.startViewTransition(() => updateDOM(view));
}

‫שלב 2: הקצאת שמות למעברים משותפים

משתמשים במאפיין ה-CSS‏ view-transition-name כדי לציין לדפדפן אילו רכיבים צריך לשנות. השם יכול להיות כל דבר (חוץ מ-none). חובה: לא יכול להיות יותר מרכיב אחד לפני ואחרי עם view-transition-name נתון. אם יש 2 רכיבים או יותר עם view-transition-name נתון, ה-DOM יתעדכן למצב החדש באופן מיידי, בלי מעבר.

אפשר להשתמש בכמה רכיבי view-transition-name כדי לשנות כמה זוגות של רכיבים. לדוגמה, יכול להיות שתרצו להוסיף מעברים נפרדים לתמונת המוצר ולשם הפריט.

יש כמה פריטים בתצוגת הרשימה, ולכן אי אפשר לתת לכולם את אותו view-transition-name. יש שתי דרכים לפתור את הבעיה הזו באפליקציית SPA.

  1. דף פרטים דינמי: מקצים לכל פריט בדף הרשימה view-transition-name ייחודי, ואז מחילים את השם הזה באופן דינמי על הרכיב התואם בדף הפרטים כשבוחרים את הפריט ברשימה, כמו שמוצג כאן.

    /* In the list view, give each */
    #product-1 { view-transition-name: p1 }
    #product-2 { view-transition-name: p2 }
    #product-3 { view-transition-name: p3 }
    
    function updateDOM(clickedTransitionName){
      const hero = document.getElementById("hero");
      hero.style.viewTransitionName = clickedTransitionName;
    }
    
  2. פריט ברשימה דינמית: מקצים לרכיב בדף הפרטים את השם view-transition-name, ומחילים את השם הזה על הפריט בדף הרשימה כשהוא נבחר. הסרת view-transition-name מהפריט בדף הרשימה כשחוזרים לדף הרשימה.

הרכיב #hero בדף הפרטים והרכיב .thumbnail שנבחר בדף הרשימה חולקים view-transition-name.

#hero{
  view-transition-name: hero;
}
.thumbnail.selected {
  view-transition-name: hero;
}

כשלוחצים על תמונה ממוזערת, צריך להכין את תצוגת הרשימה על ידי הקצאת view-transition-name באמצעות בורר המחלקות .selected, ולבצע שינויים ב-DOM לפני שמתחילים את המעבר.

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

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

// Function called when a thumbnail is clicked
function goFromListToDetail(e){
  e.currentTarget.classList.add("selected");
  const hero = document.getElementById("hero");
  const bgColor = getComputedStyle(e.currentTarget).backgroundColor;
  hero.style.background = bgColor;

  // Trigger the transition, checking for support
  if (!document.startViewTransition) {
    document.body.classList.add("detail");
    // MANDATORY Accessibility Routing: Route focus to the newly revealed heading to announce context and preserve logical tab flow
    document.getElementById("detail-heading")?.focus();
    return; // MANDATORY: End function execution if view transitions are not supported.  
  }
  const transition = document.startViewTransition(() => {
    document.body.classList.add("detail");
  });
  // MANDATORY Accessibility Routing: Route focus after the view transition resolves
  transition.finished.finally(() => {
    document.getElementById("detail-heading")?.focus();
  });
}

// Function called when navigating from detail back to list view
function goFromDetailToList() {
  if (!document.startViewTransition) {
    document.body.classList.remove("detail");
    document.getElementById("list-heading")?.focus();
    return;
  }
  const transition = document.startViewTransition(() => {
    document.body.classList.remove("detail");
  });
  // Clean up the list view and route focus
  transition.finished.finally(() => {
    // Route focus back to list view
    document.getElementById("list-heading")?.focus();
    // Remove selected classList to remove view-transition-names
    document.querySelectorAll(".selected").forEach(
      (element) => {
        element.classList.remove("selected");
      },
    );
  });
}

השיטה שתבחרו תלויה בתרחיש לדוגמה. הפריט ברשימה הדינמית דורש פחות חזרות על CSS, אבל יותר ניקוי ידני של JavaScript.

שלב 3: תיקון "מתיחה" של יחס הגובה-רוחב

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

#list-page .title {
  width: fit-content;
}

#detail-page #title {
  width: fit-content;
}

אם אתם מעבירים רכיבים שמשנים את יחס הגובה-רוחב, יכול להיות שתצטרכו להגדיר את הגובה של הרכיבים המדומים הישנים והחדשים ל-100% מהרכיב המדומה ::view-transition-pair().

::view-transition-old(hero),
::view-transition-new(hero){
  height: 100%;
}

הרכיבים המדומים הם תמונות מצב של הרכיבים הפעילים, כך שאפשר גם להשתמש בהצהרות object-fit ו-object-position כדי לשלוט יותר באפקט המעבר.

שיטות מומלצות

  • אל תציינו יותר מדי מעברים. השתמשו ברכיבים משותפים רק לתוכן ראשי שהמשתמש עוקב אחריו באופן פעיל (למשל, תמונות ראשיות, כותרות).
  • חשוב להסיר ערכי view-transition-name זמניים אחרי שהמעבר מסתיים, כדי למנוע השפעות על מעברים עתידיים.
  • אל תעבירו רכיבים עם אנימציות פעילות. מעברי תצוגה פועלים על תמונות מצב, כך שכל האנימציות יופיעו כשהן מושהות במהלך מעבר התצוגה.
  • חשוב לכבד את העדפות המשתמשים לגבי הפחתת תנועה באמצעות שאילתת המדיה prefers-reduced-motion.
  • ניתוב חובה לנגישות: מעברי תצוגה משנים את פריסות הדפים באופן דינמי, אבל לא מנהלים את המיקוד הפרוגרמטי. אם המיקוד נשאר על רכיב שמוסתר או מוסר במהלך המעבר, המיקוד ננטש, והמשתמשים במקלדת ובטכנולוגיה מסייעת נשארים בלי הקשר. מעבירים את המיקוד באופן פרוגרמטי לכותרת דף מעודכנת או למאגר תצוגה (באמצעות tabindex="-1") מיד אחרי שה-DOM מתעדכן או כשההבטחה finished של מעבר התצוגה מתקיימת.
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

שיטות חלופיות

‫View Transitions API מיועד לשיפור הדרגתי (progressive enhancement). דפדפנים שלא תומכים ב-API פשוט יבצעו את עדכון ה-DOM באופן מיידי בלי אנימציה.

function navigate(){
  if (!document.startViewTransition) {
    // Fallback: Just update the DOM
    updateDOM();
  } else {
    document.startViewTransition(() => updateDOM());
  }
}