الانتقالات في المستند نفسه

ميزات الويب: عرض الانتقالات

المشكلة

غالبًا ما توفّر المواقع الإلكترونية طرق عرض متعددة لعنصر معيّن، مثل قائمة بالمنتجات، ثم صفحة تفاصيل لكل منتج. غالبًا ما يبدو التنقل بين طريقتَي العرض غير مترابط. عندما ينقر المستخدم على صورة مصغّرة لمنتج لعرض تفاصيله، تختفي الصورة المصغّرة وتظهر صورة جديدة أكبر على الفور في مكان آخر على الشاشة. هذا النقص في الاستمرارية يجعل من الصعب على المستخدمين تتبع العلاقات بين العناصر.

الحل

تتيح لك 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: تخصيص أسماء انتقالات مشتركة

استخدِم السمة view-transition-name في CSS لإعلام المتصفّح بالعناصر التي يجب تحويلها. يمكن أن يكون الاسم أي شيء (باستثناء none). إلزامي: يجب ألا يكون هناك أكثر من عنصر واحد قبل وبعد view-transition-name معيّن. إذا كان هناك عنصران أو أكثر يحملان السمة view-transition-name نفسها، سيتم تعديل نموذج DOM إلى الحالة الجديدة على الفور بدون انتقال.

يمكنك استخدام عدة view-transition-name لتحويل عدة أزواج من العناصر. على سبيل المثال، قد ترغب في نقل كل من صورة المنتج والعنوان مع انتقالات منفصلة.

بسبب وجود عناصر متعددة في العرض على شكل قائمة، لا يمكنك منحها جميعًا السمة view-transition-name نفسها. يمكن حلّ هذه المشكلة بطريقتَين في تطبيقات الصفحة الواحدة.

  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() وتطبيق التغييرات لنقل الصفحة من العرض التفصيلي إلى العرض على شكل قائمة.

بعد الرجوع إلى العرض على شكل قائمة، يجب تنظيف فئات انتقال العرض لمنع حدوث خطأ في عملية التنقّل التالية. يمكنك إجراء عملية التنظيف هذه بعد حلّ الوعد 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 للتحسين التدريجي. أما المتصفّحات التي لا تتيح استخدامها، فستنفّذ تعديل DOM على الفور بدون تأثيرات متحركة.

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