انتقال نمای همان سند برای برنامه های کاربردی تک صفحه ای

منتشر شده: ۱۷ آگوست ۲۰۲۱، آخرین به‌روزرسانی: ۲۵ سپتامبر ۲۰۲۴

وقتی یک گذار نما روی یک سند واحد اجرا می‌شود، به آن گذار نمای سند یکسان می‌گویند. این معمولاً در برنامه‌های تک صفحه‌ای (SPA) که در آن‌ها از جاوا اسکریپت برای به‌روزرسانی DOM استفاده می‌شود، صادق است. گذارهای نمای سند یکسان در کروم از نسخه ۱۱۱ پشتیبانی می‌شوند.

برای شروع گذار نمای سند مشابه، 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());
}

هنگام فراخوانی، مرورگر به طور خودکار از تمام عناصری که ویژگی CSS مربوط به view-transition-name روی آنها تعریف شده است، snapshot می‌گیرد.

سپس تابع فراخوانی ارسالی را اجرا می‌کند که DOM را به‌روزرسانی می‌کند و پس از آن از حالت جدید عکس می‌گیرد.

سپس این تصاویر لحظه‌ای در یک درخت از شبه‌عناصر مرتب شده و با استفاده از قدرت انیمیشن‌های CSS متحرک‌سازی می‌شوند. جفت تصاویر لحظه‌ای از حالت قدیمی و جدید به آرامی از موقعیت و اندازه قدیمی خود به مکان جدیدشان منتقل می‌شوند، در حالی که محتوای آنها به صورت متقاطع محو می‌شود. در صورت تمایل، می‌توانید از CSS برای سفارشی‌سازی انیمیشن‌ها استفاده کنید.


گذار پیش‌فرض: محو شدن متقاطع

گذار پیش‌فرض نما، محوشدگی متقاطع است، بنابراین به عنوان مقدمه‌ای خوب برای API عمل می‌کند:

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() ‎ فراخوانی می‌شود، API وضعیت فعلی صفحه را ثبت می‌کند. این شامل گرفتن یک snapshot نیز می‌شود.

پس از تکمیل، تابع فراخوانی (callback) ارسال شده به .startViewTransition() فراخوانی می‌شود. در اینجا DOM تغییر می‌کند. سپس، API وضعیت جدید صفحه را ثبت می‌کند.

پس از دریافت حالت جدید، API یک درخت شبه عنصر مانند این می‌سازد:

::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;
}

با همین یک تغییر، محو شدن حالا خیلی کند شده است:

محو شدن متقاطع طولانی. دموی مینیمال . منبع .

خب، این هنوز خیلی جالب نیست. در عوض، کد زیر انتقال محور مشترک طراحی متریال را پیاده‌سازی می‌کند:

@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

Browser Support

  • کروم: ۱۲۵.
  • لبه: ۱۲۵.
  • فایرفاکس: ۱۴۴.
  • سافاری: ۱۸.۲.

Source

فرض کنید یک گذار نمایشی با تعدادی کارت و همچنین یک عنوان در صفحه دارید. برای متحرک‌سازی همه کارت‌ها به جز عنوان، باید یک انتخابگر بنویسید که تک تک کارت‌ها را هدف قرار دهد.

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);
}

بیست عنصر دارید؟ یعنی باید بیست انتخابگر بنویسید. آیا می‌خواهید یک عنصر جدید اضافه کنید؟ در این صورت باید انتخابگری که استایل‌های انیمیشن را اعمال می‌کند، بزرگتر هم بکنید. دقیقاً مقیاس‌پذیر نیست.

view-transition-class می‌تواند در شبه عناصر view transition برای اعمال همان قانون استایل استفاده شود.

#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 DevTools برای اشکال‌زدایی انتقال‌ها عالی است.

با استفاده از پنل انیمیشن‌ها ، می‌توانید انیمیشن بعدی را متوقف کنید، سپس در طول انیمیشن به جلو و عقب حرکت کنید. در طول این مدت، شبه‌عناصر انتقال را می‌توان در پنل عناصر یافت.

اشکال‌زدایی انتقال‌های نما با Chrome DevTools.

عناصر در حال گذار نیازی ندارند که همان عنصر 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();
  });
};

و نتیجه:

یک عنصر در حال انتقال به عنصر دیگر. دموی مینیمال . منبع .

تصویر بندانگشتی اکنون به تصویر اصلی منتقل می‌شود. اگرچه از نظر مفهومی (و به معنای واقعی کلمه) عناصر متفاوتی هستند، API گذار با آنها به عنوان یک چیز رفتار می‌کند زیرا آنها 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;
}

در این حالت، هیچ گذار خاصی برای زمانی که نوار کناری در هر دو حالت وجود دارد، وجود ندارد، زیرا حالت پیش‌فرض بی‌نقص است.

به‌روزرسانی‌های Async DOM و انتظار برای محتوا

تابع فراخوانی (callback) ارسالی به .startViewTransition() می‌تواند یک promise را برگرداند که امکان به‌روزرسانی‌های ناهمگام DOM و انتظار برای آماده شدن محتوای مهم را فراهم می‌کند.

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

انتقال تا زمانی که promise محقق نشود، آغاز نخواهد شد. در طول این مدت، صفحه ثابت می‌ماند، بنابراین تأخیرها در اینجا باید به حداقل برسند. به طور خاص، واکشی‌های شبکه باید قبل از فراخوانی .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() انجام می‌شود، بنابراین صفحه تعاملی باقی می‌ماند و می‌توان یک spinner را نشان داد تا به کاربر نشان دهد که همه چیز در حال بارگذاری است. اما در این مورد راه بهتری وجود دارد:

::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;
}

حالا تصویر بندانگشتی محو نمی‌شود، فقط زیر تصویر کامل قرار می‌گیرد. این یعنی اگر نمای جدید بارگذاری نشده باشد، تصویر بندانگشتی در طول گذار قابل مشاهده است. این یعنی گذار می‌تواند بلافاصله شروع شود و تصویر کامل می‌تواند در زمان خودش بارگذاری شود.

اگر نمای جدید شفافیت داشته باشد، این کار نمی‌کند، اما در این مورد می‌دانیم که اینطور نیست، بنابراین می‌توانیم این بهینه‌سازی را انجام دهیم.

مدیریت تغییرات در نسبت ابعاد

خوشبختانه، تا اینجا تمام انتقال‌ها به عناصری با نسبت ابعاد یکسان بوده‌اند، اما همیشه اینطور نخواهد بود. اگر تصویر بندانگشتی ۱:۱ و تصویر اصلی ۱۶:۹ باشد، چه می‌شود؟

یک عنصر در حال انتقال به عنصر دیگر، با تغییر نسبت ابعاد. نسخه نمایشی مینیمال . منبع .

در حالت پیش‌فرض، گروه از اندازه قبل به اندازه بعد انیمیشن می‌شود. نماهای قدیمی و جدید ۱۰۰٪ عرض گروه و ارتفاع خودکار دارند، به این معنی که نسبت ابعاد خود را صرف نظر از اندازه گروه حفظ می‌کنند.

این پیش‌فرض خوبی است، اما در این مورد مطلوب نیست. بنابراین:

::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;
}

این یعنی تصویر بندانگشتی با افزایش عرض در مرکز عنصر باقی می‌ماند، اما تصویر کامل با تغییر از ۱:۱ به ۱۶:۹ از حالت برش خارج می‌شود.

برای اطلاعات بیشتر، به بخش انتقال‌های نما: مدیریت تغییرات نسبت ابعاد مراجعه کنید.


از کوئری‌های رسانه‌ای برای تغییر انتقال‌ها برای حالت‌های مختلف دستگاه استفاده کنید

ممکن است بخواهید از انتقال‌های متفاوتی در موبایل در مقابل دسکتاپ استفاده کنید، مانند این مثال که در موبایل یک اسلاید کامل از کنار اجرا می‌کند، اما در دسکتاپ اسلاید ظریف‌تری را نمایش می‌دهد:

یک عنصر در حال انتقال به عنصر دیگر. دموی مینیمال . منبع .

این کار را می‌توان با استفاده از کوئری‌های رسانه‌ای معمولی انجام داد:

/* 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;
  }
}

با این حال، ترجیح «حرکت کاهش‌یافته» به این معنی نیست که کاربر هیچ حرکتی نمی‌خواهد. به جای قطعه کد قبلی، می‌توانید یک انیمیشن ظریف‌تر انتخاب کنید، اما انیمیشنی که همچنان رابطه بین عناصر و جریان داده‌ها را بیان کند.


مدیریت چندین سبک انتقال نما با انواع انتقال نما

Browser Support

  • کروم: ۱۲۵.
  • لبه: ۱۲۵.
  • فایرفاکس: ۱۴۴.
  • سافاری: ۱۸.

Source

گاهی اوقات، گذار از یک نمای خاص به نمای دیگر باید یک گذار اختصاصی داشته باشد. برای مثال، هنگام رفتن به صفحه بعدی یا قبلی در یک توالی صفحه‌بندی، ممکن است بخواهید بسته به اینکه به صفحه بالاتر یا پایین‌تر از توالی می‌روید، محتوا را در جهت متفاوتی حرکت دهید.

ضبط دموی صفحه‌بندی . بسته به اینکه به کدام صفحه می‌روید، از انتقال‌های مختلفی استفاده می‌کند.

برای این کار می‌توانید از انواع انتقال نما استفاده کنید که به شما امکان می‌دهند یک یا چند نوع را به یک انتقال نمای فعال اختصاص دهید. به عنوان مثال، هنگام انتقال به صفحه بالاتر در یک توالی صفحه‌بندی، از نوع 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 در جاوا اسکریپت قابل دسترسی است:

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 استفاده می‌کند، وعده‌ای که پس از رسیدن انتقال به حالت پایانی خود، پایان می‌یابد. سایر ویژگی‌های این شیء در مرجع API پوشش داده شده است.

حالا می‌توانید از آن نام کلاس در CSS خود برای تغییر transition استفاده کنید:

/* '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;
}

و تمام!

انتقال ویدیو، کندتر. نسخه آزمایشی مینیمال . منبع .

حالا ویدیو در طول انتقال پخش می‌شود.


ادغام با API ناوبری (و سایر چارچوب‌ها)

انتقال‌های ویو به گونه‌ای مشخص می‌شوند که بتوانند با سایر چارچوب‌ها یا کتابخانه‌ها ادغام شوند. برای مثال، اگر برنامه تک صفحه‌ای (SPA) شما از یک روتر استفاده می‌کند، می‌توانید مکانیزم به‌روزرسانی روتر را طوری تنظیم کنید که محتوا را با استفاده از یک انتقال ویو به‌روزرسانی کند.

در قطعه کد زیر که از این دموی صفحه‌بندی گرفته شده است، کنترل‌کننده‌ی رهگیری 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);
            });
        }
    });
});

بعضی از مرورگرها، اما نه همه آنها، وقتی کاربر برای پیمایش، حرکت کشیدن انگشت را انجام می‌دهد، گذار (transition) مخصوص به خود را ارائه می‌دهند. در این صورت، شما نباید گذار نمای خودتان را فعال کنید، زیرا منجر به یک تجربه کاربری ضعیف یا گیج‌کننده می‌شود. کاربر دو گذار - یکی توسط مرورگر و دیگری توسط شما - را می‌بیند که پشت سر هم اجرا می‌شوند.

بنابراین، توصیه می‌شود از شروع انتقال نما هنگامی که مرورگر انتقال بصری خود را ارائه داده است، جلوگیری شود. برای دستیابی به این هدف، مقدار ویژگی hasUAVisualTransition از نمونه NavigateEvent را بررسی کنید. این ویژگی زمانی که مرورگر یک انتقال بصری ارائه داده است، روی true تنظیم می‌شود. این ویژگی hasUIVisualTransition در نمونه‌های PopStateEvent نیز وجود دارد.

در قطعه کد قبلی، بررسی که تعیین می‌کند آیا گذار نما اجرا شود یا خیر، این ویژگی را در نظر می‌گیرد. هنگامی که هیچ پشتیبانی برای گذارهای نمای سند یکسان وجود ندارد یا زمانی که مرورگر از قبل گذار خود را ارائه کرده است، گذار نما نادیده گرفته می‌شود.

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

در ضبط زیر، کاربر برای بازگشت به صفحه قبل، انگشت خود را روی صفحه می‌کشد. تصویر سمت چپ شامل بررسی پرچم hasUAVisualTransition نمی‌شود. تصویر سمت راست شامل بررسی این پرچم است و در نتیجه از انتقال دستی نمایش صرف نظر می‌کند زیرا مرورگر یک انتقال بصری ارائه داده است.

مقایسه همان سایت بدون (چپ) و بدون عرض (راست) که بررسی hasUAVisualTransition در آن انجام نشده است

متحرک سازی با جاوا اسکریپت

تاکنون، تمام انتقال‌ها با استفاده از 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 استفاده می‌کند، وعده‌ای که پس از ایجاد موفقیت‌آمیز شبه‌عنصرهای transition، اجرا می‌شود. سایر ویژگی‌های این شیء در مرجع API پوشش داده شده است.


انتقال‌ها به عنوان یک پیشرفت

رابط برنامه‌نویسی کاربردی View Transition برای «پوشش» یک تغییر 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)',
    }
  );
}

مشکل این مثال این است که اگر گذار نتواند به حالت ready برسد، switchView() آن را رد می‌کند، اما این بدان معنا نیست که نما نتوانسته تغییر کند. ممکن است 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 جایگزین کنید.


پلی‌فیل نیست، اما…

این ویژگی برای پر کردن چندوجهی آسان نیست. با این حال، این تابع کمکی، کار را در مرورگرهایی که از انتقال نما پشتیبانی نمی‌کنند، بسیار آسان‌تر می‌کند:

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) {
    return unsupported('skipTransition was set to true');
  }

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

  if (!ViewTransition || !("types" in ViewTransition.prototype)) {
    return unsupported('View Transitions with types are not supported in this browser');
  }

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

  return transition;
}

و می‌توان آن را به این صورت استفاده کرد:

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

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

  // …
}

در مرورگرهایی که از انتقال نما پشتیبانی نمی‌کنند، updateDOM همچنان فراخوانی می‌شود، اما انتقال انیمیشنی وجود نخواهد داشت.

همچنین می‌توانید تعدادی classNames برای اضافه کردن به <html> در طول انتقال ارائه دهید، که تغییر انتقال را بسته به نوع ناوبری آسان‌تر می‌کند.

همچنین اگر نمی‌خواهید انیمیشنی نمایش داده شود، می‌توانید true را به skipTransition ارسال کنید، حتی در مرورگرهایی که از انتقال‌های view پشتیبانی می‌کنند. این کار در صورتی مفید است که سایت شما ترجیح کاربر برای غیرفعال کردن انتقال‌ها را داشته باشد.


کار با فریم ورک ها

اگر با کتابخانه یا چارچوبی کار می‌کنید که تغییرات DOM را خلاصه می‌کند، بخش دشوار کار این است که بدانید تغییر DOM چه زمانی کامل می‌شود. در اینجا مجموعه‌ای از مثال‌ها با استفاده از تابع کمکی بالا در چارچوب‌های مختلف آورده شده است.

  • React — نکته کلیدی در اینجا flushSync است که مجموعه‌ای از تغییرات حالت را به صورت همزمان اعمال می‌کند. بله، یک هشدار بزرگ در مورد استفاده از این API وجود دارد، اما دن آبراموف به من اطمینان می‌دهد که در این مورد مناسب است. طبق معمول با React و کد async، هنگام استفاده از promise های مختلف برگردانده شده توسط startViewTransition ، مراقب باشید که کد شما با حالت صحیح اجرا شود.
  • Vue.js - نکته کلیدی در اینجا nextTick است که پس از به‌روزرسانی DOM اجرا می‌شود.
  • Svelte - بسیار شبیه به Vue است، اما روش انتظار برای تغییر بعدی tick است.
  • Lit — نکته کلیدی در اینجا، promise مربوط به this.updateComplete درون کامپوننت‌ها است که پس از به‌روزرسانی DOM، اجرا می‌شود.
  • Angular - نکته کلیدی در اینجا applicationRef.tick است که تغییرات DOM در حال انتظار را پاک می‌کند. از نسخه ۱۷ Angular می‌توانید withViewTransitions که همراه با @angular/router ارائه می‌شود، استفاده کنید .

مرجع API

const viewTransition = document.startViewTransition(update)

یک ViewTransition جدید را شروع کنید.

update تابعی است که پس از دریافت وضعیت فعلی سند فراخوانی می‌شود.

سپس، هنگامی که promise برگردانده شده توسط updateCallback برآورده شود، انتقال در فریم بعدی آغاز می‌شود. اگر promise برگردانده شده توسط updateCallback رد شود، انتقال رها می‌شود.

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

یک ViewTransition جدید با انواع مشخص شده شروع کنید

update زمانی فراخوانی می‌شود که وضعیت فعلی سند ثبت شده باشد.

types انواع فعال برای انتقال را هنگام ضبط یا انجام انتقال تنظیم می‌کند. در ابتدا خالی است. برای اطلاعات بیشتر به viewTransition.types در ادامه مراجعه کنید.

اعضای نمونه ViewTransition :

viewTransition.updateCallbackDone

وعده‌ای که وقتی وعده‌ی برگردانده شده توسط updateCallback برآورده می‌شود، اجرا می‌شود، یا وقتی رد می‌شود، رد می‌شود.

رابط برنامه‌نویسی کاربردی View Transition، یک تغییر DOM را در بر می‌گیرد و یک گذار ایجاد می‌کند. با این حال، گاهی اوقات موفقیت یا شکست انیمیشن گذار برایتان مهم نیست، فقط می‌خواهید بدانید که آیا تغییر DOM اتفاق می‌افتد یا خیر و چه زمانی. updateCallbackDone برای این مورد استفاده است.

viewTransition.ready

وعده‌ای که پس از ایجاد شبه‌عناصر برای گذار و شروع انیمیشن، محقق می‌شود.

اگر گذار نتواند شروع شود، رد می‌شود. این می‌تواند به دلیل پیکربندی نادرست، مانند view-transition-name تکراری، یا اگر updateCallback یک promise رد شده را برگرداند، باشد.

این برای متحرک‌سازی شبه‌عنصرهای گذار با جاوااسکریپت مفید است.

viewTransition.finished

وعده‌ای که زمانی محقق می‌شود که وضعیت نهایی کاملاً برای کاربر قابل مشاهده و تعاملی باشد.

فقط در صورتی رد می‌شود که updateCallback یک promise رد شده را برگرداند، زیرا این نشان می‌دهد که وضعیت نهایی ایجاد نشده است.

در غیر این صورت، اگر یک گذار شروع نشود یا در حین گذار از آن صرف نظر شود، حالت پایانی همچنان حاصل شده است، بنابراین finished اجرا می‌شود.

viewTransition.types

یک شیء شبیه به Set که انواع گذارهای نمای فعال را در خود نگه می‌دارد. برای دستکاری ورودی‌ها، از متدهای نمونه آن clear() ، add() و delete() استفاده کنید.

برای پاسخ به یک نوع خاص در CSS، از انتخابگر شبه کلاس :active-view-transition-type(type) روی ریشه transition استفاده کنید.

نوع‌ها به طور خودکار پس از پایان انتقال نما، پاک‌سازی می‌شوند.

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

کاملاً در سمت چپ بالای بسته‌بندی قرار گرفته است.

۱۰۰٪ عرض گروه را پر می‌کند، اما ارتفاع خودکار دارد، بنابراین نسبت ابعاد خود را حفظ می‌کند و گروه را پر نمی‌کند.

دارای mix-blend-mode: plus-lighter تا امکان محو شدن متقاطع (cross-fade) واقعی را فراهم کند.

نمای قدیمی از opacity: 1 به opacity: 0 تغییر می‌کند. نمای جدید از opacity: 0 به opacity: 1 تغییر می‌کند.


بازخورد

بازخورد توسعه‌دهندگان همیشه مورد قدردانی است. برای انجام این کار، یک مشکل را با گروه کاری CSS در GitHub به همراه پیشنهادات و سوالات خود ثبت کنید . مشکل خود را با [css-view-transitions] شروع کنید.

اگر با اشکالی مواجه شدید، به جای آن، یک اشکال کرومیوم ثبت کنید .