انتقالات عرض المستند نفسه لتطبيقات الصفحة الواحدة

تاريخ النشر: 17 آب (أغسطس) 2021، تاريخ آخر تعديل: 25 أيلول (سبتمبر) 2024

عندما يتم تشغيل انتقال عرض على مستند واحد، يُعرف ذلك باسم انتقال عرض المستند نفسه. ويحدث ذلك عادةً في تطبيقات الصفحة الواحدة (SPA) التي يتم فيها استخدام JavaScript لتعديل DOM. أصبحت عمليات الانتقال بين طرق عرض المستند نفسه متاحة في Chrome اعتبارًا من الإصدار 111.

لبدء عملية انتقال إلى عرض المستند نفسه، اتصل document.startViewTransition:

function handleClick(e) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow();
    return;
  }

  // With a View Transition:
  document.startViewTransition(() => updateTheDOMSomehow());
}

عند الطلب، يُسجِّل المتصفّح تلقائيًا لقطات لجميع العناصر التي تحتوي على خاصيّة view-transition-name CSS تمّ الإعلان عنها.

بعد ذلك، يتم تنفيذ دالة الاستدعاء التي تم تمريرها لتعديل بنية DOM، وبعد ذلك يتم التقاط لقطات للحالة الجديدة.

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


الانتقال التلقائي: تمويه متبادل

انتقال العرض التلقائي هو انتقال مموّه، لذا فهو يشكّل مقدّمة رائعة لواجهة برمجة التطبيقات:

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // With a transition:
  document.startViewTransition(() => updateTheDOMSomehow(data));
}

حيث يغيّر updateTheDOMSomehow نموذج DOM إلى الحالة الجديدة. يمكنك إجراء ذلك بالطريقة التي تريدها. على سبيل المثال، يمكنك إضافة عناصر أو إزالتها أو تغيير أسماء الفئات أو تغيير الأنماط.

وهكذا، يتم تمويه الصفحات:

مؤثر التلاشي التلقائي. العرض التوضيحي الأدنى: المصدر:

حسنًا، لا يُعدّ الانتقال البيني مؤثرًا جدًا. لحسن الحظ، يمكن تخصيص الانتقالات، ولكن عليك أولاً فهم آلية عمل هذا الانتقال الأساسي.


آلية عمل هذه الانتقالات

لنعدِّل نموذج الرمز البرمجي السابق.

document.startViewTransition(() => updateTheDOMSomehow(data));

عند استدعاء .startViewTransition()، تسجِّل واجهة برمجة التطبيقات الحالة الحالية للصفحة. ويشمل ذلك أخذ لقطة شاشة.

بعد اكتمال العملية، يتم استدعاء دالة الاستدعاء التي تم تمريرها إلى .startViewTransition(). وهذا هو المكان الذي يتم فيه تغيير DOM. بعد ذلك، تسجِّل واجهة برمجة التطبيقات الحالة الجديدة للصفحة.

بعد تسجيل الحالة الجديدة، تنشئ واجهة برمجة التطبيقات شجرة عناصر زائفة على النحو التالي:

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

يظهر الرمز ::view-transition في تراكب فوق كل المحتوى الآخر على الصفحة. يكون هذا مفيدًا إذا كنت تريد ضبط لون خلفية للانتقال.

::view-transition-old(root) هي لقطة شاشة للعرض القديم، و::view-transition-new(root) هي تمثيل مباشر للعرض الجديد. ويتم عرض كليهما على أنّهما "محتوى تم استبداله" في CSS (مثل <img>).

يتم عرض العرض القديم بشكل متحرك من opacity: 1 إلى opacity: 0، بينما يتم عرض العرض الجديد بشكل متحرك من opacity: 0 إلى opacity: 1، ما يؤدي إلى إنشاء انتقال تدريجي.

يتم تنفيذ جميع الرسومات المتحركة باستخدام رسومات CSS المتحركة، لذا يمكن تخصيصها باستخدام CSS.

تخصيص الانتقال

يمكن استهداف جميع العناصر الزائفة لنقل العرض باستخدام CSS، وبما أنّه يتم تحديد الرسومات المتحركة باستخدام CSS، يمكنك تعديلها باستخدام خصائص الرسومات المتحركة الحالية في CSS. على سبيل المثال:

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 5s;
}

بعد إجراء هذا التغيير، أصبح التمويه بطيئًا جدًا:

تمويه ممتد. العرض التوضيحي الأدنى: المصدر:

حسنًا، لا يزال هذا الرقم غير مُرضٍ. بدلاً من ذلك، ينفِّذ الرمز البرمجي التالي انتقال محور مشترَك في Material Design:

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

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

@keyframes slide-from-right {
  from { transform: translateX(30px); }
}

@keyframes slide-to-left {
  to { transform: translateX(-30px); }
}

::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

إليك النتيجة:

انتقال محور مشترَك. العرض التوضيحي الأدنى: المصدر:

نقل عناصر متعددة

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

لتجنّب ذلك، يمكنك استخراج العنوان من بقية الصفحة حتى يمكن إضافة تأثيرات متحركة إليه بشكل منفصل. ويتم ذلك من خلال إسناد view-transition-name إلى العنصر.

.main-header {
  view-transition-name: main-header;
}

يمكن أن تكون قيمة view-transition-name أيّ قيمة تريدها (باستثناء none، ما يعني عدم توفّر اسم انتقال). ويُستخدَم لتحديد العنصر بشكل فريد خلال عملية النقل.

والنتيجة هي:

انتقال محور مشترَك مع عنوان ثابت العرض التوضيحي الأدنى: المصدر:

يبقى العنوان الآن في مكانه ويتلاشى تدريجيًا.

أدّى بيان CSS هذا إلى تغيير شجرة العناصر الصورية:

::view-transition
├─ ::view-transition-group(root)
│  └─ ::view-transition-image-pair(root)
│     ├─ ::view-transition-old(root)
│     └─ ::view-transition-new(root)
└─ ::view-transition-group(main-header)
   └─ ::view-transition-image-pair(main-header)
      ├─ ::view-transition-old(main-header)
      └─ ::view-transition-new(main-header)

تتوفّر الآن مجموعتَان للانتقال. أحدهما للعنوان والآخر للمحتوى. ويمكن استهدافها بشكل مستقل باستخدام CSS، وتقديم انتقالات مختلفة لها. ومع ذلك، في هذه الحالة، تم استخدام الانتقال التلقائي، وهو انتقال محوّل.main-header

حسنًا، عملية النقل التلقائية ليست مجرد انتقال مموّه، بل إنّ ::view-transition-group تنقل أيضًا:

  • الموضع والتحويل (باستخدام transform)
  • العرض
  • الطول

لم يكن ذلك مهمًا حتى الآن، لأنّ العنوان له الحجم والموقع نفسهما على جانبَي تغيير DOM. يمكنك أيضًا استخراج النص في العنوان:

.main-header-text {
  view-transition-name: main-header-text;
  width: fit-content;
}

يُستخدَم الرمز fit-content لكي يكون حجم العنصر هو حجم النص، بدلاً من التمديد إلى العرض المتبقّي. بدون هذا الإجراء، يقلّل السهم المتّجه للخلف حجم عنصر نص العنوان، بدلاً من الحجم نفسه في كلتا الصفحتَين.

لدينا الآن ثلاثة أجزاء يمكننا الاستفادة منها:

::view-transition
├─ ::view-transition-group(root)
│  └─ …
├─ ::view-transition-group(main-header)
│  └─ …
└─ ::view-transition-group(main-header-text)
   └─ …

ولكن مرة أخرى، يمكنك استخدام الإعدادات التلقائية:

نص العنوان المتحرك. العرض التوضيحي الأدنى: المصدر:

الآن، ينزلق نص العنوان قليلاً إلى الجانب لإفساح المجال لزر الرجوع.


إضافة مؤثرات متحركة إلى عناصر زائفة متعددة بالطريقة نفسها باستخدام view-transition-class

توافق المتصفّح

  • Chrome: 125
  • الحافة: 125
  • Firefox: غير متوافق
  • ‫Safari: 18.2

لنفترض أنّ لديك انتقالًا للعرض يتضمّن مجموعة من البطاقات ولكنّه يتضمّن أيضًا عنوانًا على الصفحة. لإضافة تأثيرات متحركة إلى جميع البطاقات باستثناء العنوان، عليك كتابة أداة اختيار تستهدف كل بطاقة فردية.

h1 {
    view-transition-name: title;
}
::view-transition-group(title) {
    animation-timing-function: ease-in-out;
}

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }

#card20 { view-transition-name: card20; }

::view-transition-group(card1),
::view-transition-group(card2),
::view-transition-group(card3),
::view-transition-group(card4),

::view-transition-group(card20) {
    animation-timing-function: var(--bounce);
}

هل لديك 20 عنصرًا؟ هذا يعني أنّك بحاجة إلى كتابة 20 أداة اختيار. هل تريد إضافة عنصر جديد؟ بعد ذلك، عليك أيضًا توسيع أداة الاختيار التي تطبّق أنماط الصور المتحركة. لا يمكن توسيع نطاقها بدقة.

يمكن استخدام view-transition-class في العناصر الزائفة لنقل العرض لتطبيق قاعدة النمط نفسها.

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
#card5 { view-transition-name: card5; }

#card20 { view-transition-name: card20; }

#cards-wrapper > div {
  view-transition-class: card;
}
html::view-transition-group(.card) {
  animation-timing-function: var(--bounce);
}

يستفيد مثال البطاقات التالي من مقتطف CSS السابق. يتم تطبيق التوقيت نفسه على جميع البطاقات، بما في ذلك البطاقات التي تمت إضافتها حديثًا، باستخدام أداة اختيار واحدة: html::view-transition-group(.card).

تسجيل العرض التقديمي لميزة "البطاقات" باستخدام view-transition-class، يتم تطبيق animation-timing-function نفسه على جميع البطاقات باستثناء البطاقات التي تمت إضافتها أو إزالتها.

تصحيح أخطاء الانتقالات

بما أنّ انتقالات العرض تستند إلى الرسوم المتحرّكة في CSS، فإنّ لوحة الرسوم المتحرّكة في "أدوات مطوّري البرامج في Chrome" هي وسيلة رائعة لتصحيح أخطاء الانتقالات.

باستخدام لوحة الرسوم المتحركة، يمكنك إيقاف المحتوى المتحرك التالي مؤقتًا، ثم التمرير للخلف وللأمام خلال المحتوى المتحرك. خلال هذه العملية، يمكن العثور على العناصر الزائفة للانتقال في لوحة العناصر.

تصحيح أخطاء انتقالات العرض باستخدام "أدوات مطوّري البرامج في Chrome"

لا يلزم أن تكون العناصر الانتقالية هي عنصر نموذج DOM نفسه.

لقد استخدمنا حتى الآن view-transition-name لإنشاء عناصر انتقال منفصلة للعنوان والنص في العنوان. من الناحية المفهومية، هذان العنصران متطابقان قبل تغيير DOM وبعده، ولكن يمكنك إنشاء انتقالات لا ينطبق عليها ذلك.

على سبيل المثال، يمكن إضافة view-transition-name إلى الفيديو المضمّن الرئيسي:

.full-embed {
  view-transition-name: full-embed;
}

بعد ذلك، عند النقر على الصورة المصغّرة، يمكن منحها الرمز view-transition-name نفسه، وذلك فقط خلال فترة الانتقال:

thumbnail.onclick = async () => {
  thumbnail.style.viewTransitionName = 'full-embed';

  document.startViewTransition(() => {
    thumbnail.style.viewTransitionName = '';
    updateTheDOMSomehow();
  });
};

والنتيجة هي:

عنصر واحد ينتقل إلى عنصر آخر العرض التوضيحي الأدنى: المصدر:

تنتقل الصورة المصغّرة الآن إلى الصورة الرئيسية. على الرغم من أنّهما عنصران مختلفان من الناحية المفهومية (والمادية)، تتعامل واجهة برمجة التطبيقات لنقل البيانات معهما على أنّهما الشيء نفسه لأنّهما يتشاركان view-transition-name نفسه.

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


انتقالات مخصّصة للدخول والخروج

راجِع هذا المثال:

الدخول إلى الشريط الجانبي والخروج منه العرض التوضيحي الأدنى: المصدر:

يُعدّ الشريط الجانبي جزءًا من عملية النقل:

.sidebar {
  view-transition-name: sidebar;
}

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

::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
   └─ ::view-transition-image-pair(sidebar)
      ├─ ::view-transition-old(sidebar)
      └─ ::view-transition-new(sidebar)

ومع ذلك، إذا كان الشريط الجانبي يظهر في الصفحة الجديدة فقط، لن يظهر العنصر الاصطناعي ::view-transition-old(sidebar). بما أنّه لا تتوفّر صورة "قديمة" للشريط الجانبي، سيتضمّن زوج الصور ::view-transition-new(sidebar) فقط. وبالمثل، إذا كان الشريط الجانبي يظهر في الصفحة القديمة فقط، سيحتوي زوج الصور على ::view-transition-old(sidebar) فقط.

في العرض الترويجي السابق، ينتقل الشريط الجانبي بشكل مختلف حسب ما إذا كان يظهر أو يختفي أو يظهر في كلتا الحالتَين. يظهر الشعار من خلال التمرير من اليمين وتدريجه إلى أن يظهر، ويختفي من خلال التمرير إلى اليمين وتدريجه إلى أن يختفي، ويبقى في مكانه عند ظهوره في كلتا الحالتَين.

لإنشاء انتقالات دخول وخروج محدّدة، يمكنك استخدام الفئة الزائفة :only-child لاستهداف العناصر الزائفة القديمة أو الجديدة عندما تكون هي العنصر الفرعي الوحيد في زوج الصور:

/* Entry transition */
::view-transition-new(sidebar):only-child {
  animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Exit transition */
::view-transition-old(sidebar):only-child {
  animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}

في هذه الحالة، ما مِن انتقال محدّد عندما يكون الشريط الجانبي متوفّرًا في كلتا الحالتَين، لأنّ الإعداد التلقائي مثالي.

تعديلات DOM غير المتزامنة والانتظار إلى أن يظهر المحتوى

يمكن أن يعرض الإجراء المُعاد الاتصال به الذي تم تمريره إلى .startViewTransition() وعدًا، ما يسمح بتعديلات DOM غير المتزامنة والانتظار إلى أن يصبح المحتوى المهم جاهزًا.

document.startViewTransition(async () => {
  await something;
  await updateTheDOMSomehow();
  await somethingElse;
});

ولن يبدأ النقل إلا بعد استيفاء الوعد. خلال هذه الفترة، يتم تجميد الصفحة، لذا يجب تقليل التأخيرات إلى أدنى حد. على وجه التحديد، يجب إجراء عمليات جلب البيانات من الشبكة قبل استدعاء .startViewTransition()، عندما تكون الصفحة لا تزال تفاعلية بالكامل، بدلاً من إجرائها كجزء من ردّ الاتصال .startViewTransition().

إذا قرّرت الانتظار إلى أن تصبح الصور أو الخطوط جاهزة، احرص على استخدام مهلة قصيرة:

const wait = ms => new Promise(r => setTimeout(r, ms));

document.startViewTransition(async () => {
  updateTheDOMSomehow();

  // Pause for up to 100ms for fonts to be ready:
  await Promise.race([document.fonts.ready, wait(100)]);
});

في بعض الحالات، من الأفضل تجنّب التأخير تمامًا واستخدام المحتوى الذي لديك.


الاستفادة إلى أقصى حد من المحتوى الذي لديك

في حال انتقال الصورة المصغّرة إلى صورة أكبر:

الصورة المصغّرة تتحول إلى صورة أكبر. جرِّب الموقع الإلكتروني التجريبي.

الإعداد التلقائي للانتقال هو التلاشي المتقاطع، ما يعني أنّ الصورة المصغّرة يمكن أن تتلاشى مع صورة كاملة لم يتم تحميلها بعد.

ويمكنك معالجة هذه المشكلة من خلال الانتظار إلى أن يتم تحميل الصورة بالكامل قبل بدء الانتقال. من الأفضل إجراء ذلك قبل استدعاء .startViewTransition()، حتى تظل الصفحة تفاعلية، ويمكن عرض مؤشر تدوير لإعلام المستخدم بأنّه يتم تحميل العناصر. ولكن في هذه الحالة، هناك طريقة أفضل:

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
}

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

لن يكون هذا الإجراء مفيدًا إذا كانت طريقة العرض الجديدة شفافة، ولكن في هذه الحالة نعلم أنّها ليست كذلك، لذا يمكننا إجراء هذا التحسين.

التعامل مع التغييرات في نسبة العرض إلى الارتفاع

من المفيد أنّ جميع عمليات النقل إلى الآن كانت إلى عناصر لها نسبة العرض إلى الارتفاع نفسها، ولكن لن يكون هذا هو الحال دائمًا. ماذا لو كانت الصورة المصغّرة بنسبة عرض إلى ارتفاع 1:1 وكانت الصورة الرئيسية بنسبة عرض إلى ارتفاع 16:9؟

عنصر واحد ينتقل إلى عنصر آخر مع تغيير نسبة العرض إلى الارتفاع العرض التوضيحي الأدنى: المصدر:

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

هذه القيمة التلقائية جيدة، ولكنّها ليست القيمة المطلوبة في هذه الحالة. وبالتالي:

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
  /* Make the height the same as the group,
  meaning the view size might not match its aspect-ratio. */
  height: 100%;
  /* Clip any overflow of the view */
  overflow: clip;
}

/* The old view is the thumbnail */
::view-transition-old(full-embed) {
  /* Maintain the aspect ratio of the view,
  by shrinking it to fit within the bounds of the element */
  object-fit: contain;
}

/* The new view is the full image */
::view-transition-new(full-embed) {
  /* Maintain the aspect ratio of the view,
  by growing it to cover the bounds of the element */
  object-fit: cover;
}

وهذا يعني أنّ الصورة المصغّرة تبقى في وسط العنصر مع توسيع العرض، ولكن يتم "إلغاء اقتصاص" الصورة الكاملة عند انتقالها من نسبة العرض إلى الارتفاع 1:1 إلى 16:9.

لمزيد من المعلومات التفصيلية، اطّلِع على مقالة عرض الانتقالات: التعامل مع تغييرات نسبة العرض إلى الارتفاع.


استخدام طلبات البحث عن الوسائط لتغيير عمليات النقل لحالات الأجهزة المختلفة

قد تحتاج إلى استخدام انتقالات مختلفة على الأجهزة الجوّالة مقارنةً بأجهزة الكمبيوتر المكتبي، مثل هذا المثال الذي يُجري انتقالًا كاملاً من الجانب على الأجهزة الجوّالة، ولكن انتقالًا أكثر دقة على أجهزة الكمبيوتر المكتبي:

عنصر واحد ينتقل إلى عنصر آخر العرض التوضيحي الأدنى: المصدر:

ويمكن تحقيق ذلك باستخدام طلبات البحث العادية عن الوسائط:

/* Transitions for mobile */
::view-transition-old(root) {
  animation: 300ms ease-out both full-slide-to-left;
}

::view-transition-new(root) {
  animation: 300ms ease-out both full-slide-from-right;
}

@media (min-width: 500px) {
  /* Overrides for larger displays.
  This is the shared axis transition from earlier in the article. */
  ::view-transition-old(root) {
    animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
  }

  ::view-transition-new(root) {
    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
  }
}

قد تحتاج أيضًا إلى تغيير العناصر التي تحدّد لها view-transition-name استنادًا إلى مطابقة طلبات البحث عن الوسائط.


التفاعل مع الإعداد المفضّل "الحدّ من الحركة"

يمكن للمستخدمين الإشارة إلى أنّهم يفضّلون استخدام ميزة "خفض الحركة" من خلال نظام التشغيل، ويتم عرض هذا الخيار في CSS.

يمكنك اختيار منع أي عمليات انتقال لهؤلاء المستخدمين:

@media (prefers-reduced-motion) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

ومع ذلك، لا يعني تفضيل "خفض الحركة" أنّ المستخدم يريد عدم استخدام أي حركة. بدلاً من المقتطف السابق، يمكنك اختيار صورة متحركة أكثر دقة، ولكن لا تزال تعبّر عن العلاقة بين العناصر وتدفق البيانات.


التعامل مع أنماط متعددة لنقل العرض باستخدام أنواع نقل العرض

توافق المتصفّح

  • Chrome: 125
  • الحافة: 125
  • Firefox: غير متوافق
  • Safari: 18

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

تسجيل العرض التوضيحي لميزة "الفهرسة حسب الصفحات" ويستخدم هذا التأثير انتقالات مختلفة حسب الصفحة التي ستنتقل إليها.

ويمكنك لهذا الغرض استخدام أنواع انتقالات العرض التي تتيح لك تحديد نوع واحد أو أكثر لعملية انتقال عرض نشطة. على سبيل المثال، عند الانتقال إلى صفحة أعلى في تسلسل التقسيم على صفحات، استخدِم النوع forwards، وعند الانتقال إلى صفحة أقل، استخدِم النوع backwards. ولا تكون هذه الأنواع نشطة إلا عند التقاط انتقال أو تنفيذه، ويمكن تخصيص كل نوع من خلال CSS لاستخدام رسوم متحركة مختلفة.

لاستخدام الأنواع في عملية انتقال عرض المستند نفسه، عليك تمرير types إلى طريقة startViewTransition. للسماح بذلك، يقبل document.startViewTransition أيضًا عنصرًا: update هي دالة ردّ الاتصال التي تعدّل DOM، وtypes هي صفيف يتضمّن الأنواع.

const direction = determineBackwardsOrForwards();

const t = document.startViewTransition({
  update: updateTheDOMSomehow,
  types: ['slide', direction],
});
.

للردّ على هذه الأنواع، استخدِم أداة الاختيار :active-view-transition-type(). نقْل type الذي تريد استهدافه إلى أداة الاختيار. يتيح لك ذلك فصل أنماط عمليات النقل بين طرق العرض المتعددة عن بعضها، بدون أن تتداخل بيانات أحدها مع بيانات الآخر.

وبما أنّ الأنواع لا تنطبق إلا عند تسجيل الانتقال أو تنفيذه، يمكنك استخدام المحدِّد لضبط view-transition-name على عنصر أو إلغاء ضبطه فقط لعملية انتقال العرض باستخدام هذا النوع.

/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
  :root {
    view-transition-name: none;
  }
  article {
    view-transition-name: content;
  }
  .pagination {
    view-transition-name: pagination;
  }
}

/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-left;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-right;
  }
}

/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-right;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-left;
  }
}

/* Animation styles for reload type only (using the default root snapshot) */
html:active-view-transition-type(reload) {
  &::view-transition-old(root) {
    animation-name: fade-out, scale-down;
  }
  &::view-transition-new(root) {
    animation-delay: 0.25s;
    animation-name: fade-in, scale-up;
  }
}

في العرض التجريبي للتقسيم على صفحات التالي، يتم عرض محتوى الصفحة للأمام أو للخلف استنادًا إلى رقم الصفحة التي تنتقل إليها. يتم تحديد الأنواع عند النقر عليها، ويتم تمريرها إلى document.startViewTransition.

لاستهداف أي انتقال نشط للعرض، بغض النظر عن النوع، يمكنك استخدام أداة اختيار الفئة الزائفة :active-view-transition بدلاً من ذلك.

html:active-view-transition {
    
}

التعامل مع أنماط متعددة لنقل العرض باستخدام اسم فئة في جذر انتقال العرض

في بعض الأحيان، يجب أن يتضمّن الانتقال من نوع معيّن من طرق العرض إلى نوع آخر انتقالًا مصمّمًا خصيصًا. أو يجب أن يكون رمز التنقّل "للخلف" مختلفًا عن رمز التنقّل "للأعلى".

عمليات انتقال مختلفة عند الرجوع إلى الخلف العرض التوضيحي الأدنى: المصدر:

قبل أنواع الانتقال، كانت طريقة التعامل مع هذه الحالات هي ضبط اسم فئة مؤقتًا على جذر الانتقال. عند استدعاء document.startViewTransition، يكون جذر الانتقال هذا هو العنصر <html>، ويمكن الوصول إليه باستخدام document.documentElement في JavaScript:

if (isBackNavigation) {
  document.documentElement.classList.add('back-transition');
}

const transition = document.startViewTransition(() =>
  updateTheDOMSomehow(data)
);

try {
  await transition.finished;
} finally {
  document.documentElement.classList.remove('back-transition');
}

لإزالة الفئات بعد انتهاء الانتقال، يستخدم هذا المثال transition.finished، وهو وعد يتم حلّه بعد وصول الانتقال إلى حالته النهائية. يتم تناول الخصائص الأخرى لهذا العنصر في مرجع واجهة برمجة التطبيقات.

يمكنك الآن استخدام اسم الفئة هذا في CSS لتغيير الانتقال:

/* 'Forward' transitions */
::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms
      cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Overrides for 'back' transitions */
.back-transition::view-transition-old(root) {
  animation-name: fade-out, slide-to-right;
}

.back-transition::view-transition-new(root) {
  animation-name: fade-in, slide-from-left;
}

كما هو الحال مع طلبات البحث عن الوسائط، يمكن أيضًا استخدام وجود هذه الفئات لتغيير العناصر التي تحصل على view-transition-name.


تشغيل الانتقالات بدون تجميد الصور المتحركة الأخرى

اطّلِع على هذا الفيديو التجريبي الذي يعرض موضع انتقال الفيديو:

انتقال الفيديو. العرض التوضيحي الأدنى: المصدر:

هل لاحظت أي مشكلة؟ لا داعي للقلق إذا لم يسبق لك ذلك. في ما يلي الفيديو ببطء شديد:

انتقال الفيديو، أبطأ. العرض التوضيحي الأدنى: المصدر:

أثناء عملية النقل، يبدو أنّ الفيديو يتوقّف، ثم يبدأ بالظهور تدريجيًا. ويرجع ذلك إلى أنّ ::view-transition-old(video) هو لقطة شاشة للعرض القديم، في حين أنّ ::view-transition-new(video) هو صورة حية للعرض الجديد.

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

إذا أردت إصلاح المشكلة، يجب عدم عرض ::view-transition-old(video)، بل الانتقال مباشرةً إلى ::view-transition-new(video). يمكنك إجراء ذلك من خلال إلغاء الأنماط والرسومات المتحرّكة التلقائية:

::view-transition-old(video) {
  /* Don't show the frozen old view */
  display: none;
}

::view-transition-new(video) {
  /* Don't fade the new view in */
  animation: none;
}

هذا كل ما في الأمر!

انتقال الفيديو، أبطأ. العرض التوضيحي الأدنى: المصدر:

يتم الآن تشغيل الفيديو أثناء عملية النقل.


الدمج مع Navigation API (وإطارات العمل الأخرى)

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

في المقتطف التالي من الرمز البرمجي المأخوذ من هذه النسخة التجريبية من تقسيم الصفحات، تم تعديل معالج اعتراض Navigation API لاستدعاء document.startViewTransition عندما تكون عمليات انتقال العرض متوافقة.

navigation.addEventListener("navigate", (e) => {
    // Don't intercept if not needed
    if (shouldNotIntercept(e)) return;

    // Intercept the navigation
    e.intercept({
        handler: async () => {
            // Fetch the new content
            const newContent = await fetchNewContent(e.destination.url, {
                signal: e.signal,
            });

            // The UA does not support View Transitions, or the UA
            // already provided a Visual Transition by itself (e.g. swipe back).
            // In either case, update the DOM directly
            if (!document.startViewTransition || e.hasUAVisualTransition) {
                setContent(newContent);
                return;
            }

            // Update the content using a View Transition
            const t = document.startViewTransition(() => {
                setContent(newContent);
            });
        }
    });
});

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

لذلك، يُنصح بمنع بدء انتقال عرض عندما يقدّم المتصفّح انتقالًا مرئيًا خاصًا به. لتحقيق ذلك، تحقّق من قيمة سمة hasUAVisualTransition لنسخة NavigateEvent. يتم ضبط السمة على true عندما يقدّم المتصفّح انتقالًا مرئيًا. تتوفّر سمة hasUIVisualTransition هذه أيضًا في نُسخ PopStateEvent.

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

if (!document.startViewTransition || e.hasUAVisualTransition) {
  setContent(newContent);
  return;
}

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

مقارنة الموقع الإلكتروني نفسه بدون (على يمين) وعرض (على يمين) علامة اختيار hasUAVisualTransition
.

إضافة الحركة باستخدام JavaScript

حتى الآن، تم تحديد جميع عمليات النقل باستخدام CSS، ولكن في بعض الأحيان لا تكون CSS كافية:

انتقال دائري. العرض التوضيحي الأدنى: المصدر:

لا يمكن تحقيق جزءَين من عملية النقل هذه باستخدام CSS فقط:

  • تبدأ الصورة المتحركة من مكان النقر.
  • تنتهي الصورة المتحركة عندما تصبح الدائرة ذات نصف قطر يصل إلى أقصى زاوية. نأمل أن نتمكّن من إجراء ذلك باستخدام CSS في المستقبل.

لحسن الحظ، يمكنك إنشاء انتقالات باستخدام واجهة برمجة التطبيقات Web Animation API.

let lastClick;
addEventListener('click', event => (lastClick = event));

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // Get the click position, or fallback to the middle of the screen
  const x = lastClick?.clientX ?? innerWidth / 2;
  const y = lastClick?.clientY ?? innerHeight / 2;
  // Get the distance to the furthest corner
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  );

  // With a transition:
  const transition = document.startViewTransition(() => {
    updateTheDOMSomehow(data);
  });

  // Wait for the pseudo-elements to be created:
  transition.ready.then(() => {
    // Animate the root's new view
    document.documentElement.animate(
      {
        clipPath: [
          `circle(0 at ${x}px ${y}px)`,
          `circle(${endRadius}px at ${x}px ${y}px)`,
        ],
      },
      {
        duration: 500,
        easing: 'ease-in',
        // Specify which pseudo-element to animate
        pseudoElement: '::view-transition-new(root)',
      }
    );
  });
}

يستخدم هذا المثال transition.ready، وهو وعد يتم حلّه بعد إنشاء العناصر الزائفة للانتقال بنجاح. يتم تناول الخصائص الأخرى لهذا العنصر في مرجع واجهة برمجة التطبيقات.


الانتقالات كميزة تحسين

تم تصميم View Transition API "لتضمين" تغيير DOM وإنشاء انتقال له. ومع ذلك، يجب التعامل مع عملية النقل على أنّها ميزة محسّنة، أي أنّه يجب ألا يدخل تطبيقك في حالة "خطأ" إذا تم تغيير DOM بنجاح، ولكن تعذّر النقل. من المفترض ألا يتعذّر إجراء عملية النقل، ولكن إذا حدث ذلك، يجب ألا تتأثر بقية تجربة المستخدم.

لمعالجة عمليات النقل على أنّها تحسين، يجب الحرص على عدم استخدام وعد النقل بطريقة قد تؤدي إلى إغلاق تطبيقك في حال تعذّر النقل.

الإجراءات غير المُوصى بها
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  await transition.ready;

  document.documentElement.animate(
    {
      clipPath: [`inset(50%)`, `inset(0)`],
    },
    {
      duration: 500,
      easing: 'ease-in',
      pseudoElement: '::view-transition-new(root)',
    }
  );
}

تكمن المشكلة في هذا المثال في أنّه سيتم رفض switchView() إذا تعذّر على عملية النقل الوصول إلى حالة ready، ولكن هذا لا يعني أنّه تعذّر تبديل طريقة العرض. من المحتمل أنّه تم تعديل نموذج DOM بنجاح، ولكن كانت هناك عناصر view-transition-name مكرّرة، لذا تمّ تخطي عملية النقل.

بدلاً من ذلك:

الإجراءات الموصى بها
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  animateFromMiddle(transition);

  await transition.updateCallbackDone;
}

async function animateFromMiddle(transition) {
  try {
    await transition.ready;

    document.documentElement.animate(
      {
        clipPath: [`inset(50%)`, `inset(0)`],
      },
      {
        duration: 500,
        easing: 'ease-in',
        pseudoElement: '::view-transition-new(root)',
      }
    );
  } catch (err) {
    // You might want to log this error, but it shouldn't break the app
  }
}

يستخدم هذا المثال transition.updateCallbackDone للانتظار إلى أن يتم تحديث DOM، وللرفض في حال تعذّر التحديث. لم يعُد switchView يرفض العنصر إذا تعذّر الانتقال، بل يحلّه عند اكتمال تحديث DOM، ويرفضه إذا تعذّر.

إذا كنت تريد أن يتم حلّ switchView عندما "تستقر" طريقة العرض الجديدة، أي عندما يكتمل أي انتقال متحرك أو يتم تخطّيه إلى النهاية، استبدِل transition.updateCallbackDone بـ transition.finished.


ليس polyfill، ولكن…

هذه الميزة ليست سهلة الإضافة. ومع ذلك، تُسهّل هذه الدالة المساعِدة الأمور كثيرًا في المتصفّحات التي لا تتوافق مع عمليات النقل بين طرق العرض:

function transitionHelper({
  skipTransition = false,
  types = [],
  update,
}) {

  const unsupported = (error) => {
    const updateCallbackDone = Promise.resolve(update()).then(() => {});

    return {
      ready: Promise.reject(Error(error)),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
      types,
    };
  }

  if (skipTransition || !document.startViewTransition) {
    return unsupported('View Transitions are not supported in this browser');
  }

  try {
    const transition = document.startViewTransition({
      update,
      types,
    });

    return transition;
  } catch (e) {
    return unsupported('View Transitions with types are not supported in this browser');
  }
}

ويمكن استخدامه على النحو التالي:

function spaNavigate(data) {
  const types = isBackNavigation ? ['back-transition'] : [];

  const transition = transitionHelper({
    update() {
      updateTheDOMSomehow(data);
    },
    types,
  });

  // …
}

في المتصفحات التي لا تتيح انتقالات العرض، سيظلّ يتمّ استدعاء updateDOM، ولكن لن يكون هناك انتقال متحرك.

يمكنك أيضًا تقديم بعض classNames لإضافتها إلى <html> أثناء الانتقال، ما يسهّل تغيير الانتقال استنادًا إلى نوع التنقّل.

يمكنك أيضًا ضبط true على skipTransition إذا كنت لا تريد عرض صورة متحركة، حتى في المتصفحات التي تتيح انتقالات العرض. يكون هذا مفيدًا إذا كان موقعك الإلكتروني يتضمّن إعدادًا مفضّلاً للمستخدم لإيقاف الانتقالات.


العمل مع الأطر

إذا كنت تعمل مع مكتبة أو إطار عمل يزيل تغييرات نموذج DOM، يكون الجزء الصعب هو معرفة وقت اكتمال تغيير نموذج DOM. في ما يلي مجموعة من الأمثلة، باستخدام المساعدة أعلاه، في إطارات عمل مختلفة.

  • React: المفتاح هنا هو flushSync، الذي يطبّق مجموعة من تغييرات الحالة بشكل متزامن. نعم، هناك تحذير كبير بشأن استخدام واجهة برمجة التطبيقات هذه، ولكنّ دان أبراموڤ يؤكّد لي أنّها مناسبة في هذه الحالة. كما هو الحال مع React والرمز غير المتزامن، عند استخدام الوعود المختلفة التي يعرضها startViewTransition، يجب التأكّد من أنّ رمزك يتم تنفيذه بالحالة الصحيحة.
  • Vue.js: المفتاح هنا هو nextTick، الذي يتم تنفيذه بعد تعديل عنصر DOM.
  • Svelte: تشبه Vue كثيرًا، ولكن طريقة انتظار التغيير التالي هي tick.
  • Lit: المفتاح هنا هو وعد this.updateComplete ضمن المكوّنات، والذي يتم تنفيذه بعد تعديل نموذج DOM.
  • Angular: المفتاح هنا هو applicationRef.tick، الذي يُزيل التغييرات المعلّقة في نموذج DOM. اعتبارًا من الإصدار 17 من Angular، يمكنك استخدام withViewTransitions المضمّن في @angular/router.

مرجع واجهة برمجة التطبيقات

const viewTransition = document.startViewTransition(update)

ابدأ ViewTransition جديدًا.

update هي دالة يتمّ استدعاؤها بعد تسجيل الحالة الحالية للمستند.

بعد ذلك، عندما يتم استيفاء الوعد الذي تم إرجاعه بواسطة updateCallback، يبدأ الانتقال في الإطار التالي. إذا تم رفض الوعد الذي تم إرجاعه من خلال updateCallback، يتم التخلي عن عملية النقل.

const viewTransition = document.startViewTransition({ update, types })

بدء ViewTransition جديد بالأنواع المحدّدة

يتمّ استدعاء update بعد تسجيل الحالة الحالية للمستند.

types لضبط الأنواع النشطة للانتقال عند التقاطه أو تنفيذه يكون هذا الحقل فارغًا في البداية. يُرجى الاطّلاع على viewTransition.types أدناه للحصول على مزيد من المعلومات.

أعضاء الإصدار ViewTransition:

viewTransition.updateCallbackDone

وعد يتم تنفيذه عندما يتم تنفيذ الوعد الذي يعرضه updateCallback، أو يتم رفضه عندما يتم رفضه.

تُغلِّف واجهة برمجة التطبيقات View Transition API تغيير DOM وتُنشئ انتقالًا. ومع ذلك، في بعض الأحيان لا يهمّك نجاح أو تعذُّر الانتقال المتحرك، بل تريد فقط معرفة ما إذا كان تغيير DOM سيحدث ومتى. ‫updateCallbackDone مخصّص لحالة الاستخدام هذه.

viewTransition.ready

وعد يتم تنفيذه بعد إنشاء العناصر الزائفة للانتقال، وقبل بدء الحركة.

ويتم رفضه إذا تعذّر بدء عملية النقل. يمكن أن يرجع ذلك إلى خطأ في الضبط، مثل القيم المكرّرة من view-transition-name، أو إذا كانت updateCallback تعرض وعدًا مرفوضًا.

ويُعدّ ذلك مفيدًا لإضافة حركة إلى العناصر الزائفة للانتقال باستخدام JavaScript.

viewTransition.finished

وعد يتم تنفيذه بعد أن تصبح الحالة النهائية مرئية للمستخدم بشكل كامل وتفاعلية معه.

ولا يتم الرفض إلا إذا أرجعت updateCallback وعدًا مرفوضًا، لأنّ ذلك يشير إلى أنّه لم يتم إنشاء الحالة النهائية.

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

viewTransition.types

عنصر مثل Set يحتوي على أنواع انتقالات العرض النشطة. لإجراء تغييرات على الإدخالات، استخدِم طرق المثيل clear() وadd() وdelete().

للردّ على نوع معيّن في CSS، استخدِم أداة اختيار الفئة الزائفة :active-view-transition-type(type) في جذر الانتقال.

يتم تلقائيًا تنظيف الأنواع عند انتهاء عملية انتقال العرض.

viewTransition.skipTransition()

تخطّي جزء الرسوم المتحركة من الانتقال

لن يؤدي ذلك إلى تخطّي استدعاء updateCallback، لأنّ تغيير DOM منفصل عن عملية النقل.


إشارة النمط التلقائي والانتقال

::view-transition
العنصر الوهمي الجذر الذي يملأ إطار العرض ويحتوي على كل ::view-transition-group.
::view-transition-group

تمّ تحديد موضع الإعلان بدقة.

انتقالات width وheight بين حالتَي "قبل" و"بعد"

الانتقالات transform بين مربّع مساحة إطار العرض "قبل" و"بعد"

::view-transition-image-pair

تم وضعها تمامًا لملء المجموعة.

يجب أن يكون isolation: isolate للحد من تأثير mix-blend-mode على المشاهدات القديمة والجديدة.

::view-transition-new و::view-transition-old

يتم وضعها بشكل مطلق في أعلى يمين الغلاف.

تملأ 100% من عرض المجموعة، ولكنّ ارتفاعها تلقائي، لذا ستحافظ على نسبة العرض إلى الارتفاع بدلاً من ملء المجموعة.

يجب أن يتضمّن mix-blend-mode: plus-lighter للسماح بتمويه حقيقي.

يتم نقل طريقة العرض القديمة من opacity: 1 إلى opacity: 0. ينتقل العرض الجديد من opacity: 0 إلى opacity: 1.


ملاحظات

نحن نقدِّر دائمًا ملاحظات المطوّرين. لإجراء ذلك، يُرجى إرسال مشكلة إلى مجموعة عمل CSS على GitHub مع تضمين اقتراحات وأسئلة. أضِف [css-view-transitions] إلى بداية مشكلتك.

إذا واجهت خللاً، يُرجى إرسال تقرير عن خلل في Chromium بدلاً من ذلك.