Scoped Component Transitions

شروع انتقال نما با document.startViewTransition()، هم‌پوشانی ::view-transition را به عنصر :root متصل می‌کند که رویدادهای اشاره‌گر را در بقیه صفحه مسدود می‌کند، از انتقال‌های هم‌زمان جلوگیری می‌کند، و سرصفحه‌های position: fixed یا عناصر باز [popover] را پنهان می‌کند (حتی زمانی که از گروه‌های انتقال نمای تودرتو برای جلوگیری از سرریز شدن استفاده می‌شود).

فراخوانی element.startViewTransition() در ریشه مؤلفه این مشکل را با محدود کردن انتقال به آن زیردرخت DOM برطرف می‌کند: درخت شبه‌عنصر ::view-transition مستقیماً به عنصر تزریق می‌شود، زمینه‌های پشته‌سازی خارجی و تعامل صفحه را حفظ می‌کند و در عین حال به چندین مؤلفه اجازه می‌دهد به‌طور هم‌زمان انتقال یابند.

پیاده‌سازی

۱. راه‌اندازی «گذار محدودشده»

‫element.startViewTransition() را در عنصر محتوی ریشه مؤلفه به‌جای document.startViewTransition() فراخوانی کنید. عنصری که startViewTransition() را روی آن فراخوانی می‌کنید، به دامنه انتقال (عنصر دامنه) تبدیل می‌شود: فقط عناصر درون زیردرخت آن ضبط می‌شوند، فقط رندر کردن دامنه درطول فراخوانی برگشتی به‌روزرسانی متوقف می‌شود، و هم‌پوشانی ::view-transition حاصل با اندازه، شکل، و زمینه پشته‌سازی عنصر دامنه مطابقت دارد.

۲. نام‌گذاری عناصر شرکت‌کننده

یک view-transition-name به عناصر فرزند داخل محدوده که باید پویانمایی شوند اختصاص دهید. می‌توانید از view-transition-name: match-element در موارد فهرست پویا استفاده کنید تا نام انتقال هر عنصر به‌طور خودکار از هویت گره DOM آن مشتق شود، یا نام‌های صریح اختصاص دهید (که فقط باید در آن محدوده منحصربه‌فرد باشند).

نام‌ها در هر دامنه جدا هستند و این امر از تداخل بین مؤلفه‌ها در بخش‌های مختلف DOM جلوگیری می‌کند. اما view-transition-name می‌تواند با نام‌ها در انتقال‌های نمای محدوده تودرتو تداخل داشته باشد. برای جلوگیری از این اتفاق، view-transition-scope: all را به عنصر محدوده فرزند اضافه کنید.

۳. مشارکت خود و برش سرریز

عنصر دامنه به‌طور خودکار در گذار نمای خودش شرکت می‌کند زیرا برگه سبک نگارش نماینده کاربر view-transition-name: root و view-transition-group: contain را بر آن اعمال می‌کند. به این ترتیب، وقتی هندسه محدوده تحت تأثیر تغییری قرار می‌گیرد که در DOM ایجاد می‌کنید، هندسه خود را به‌آرامی در کنار فرزندانش، به‌عنوان بخشی از «انتقال نما»، پویانمایی می‌کند.

وقتی دامنه سرریز خود را برش می‌دهد (overflow: hidden، scroll، یا clip)، ::view-transition-group-children(root) به‌طور خودکار overflow: clip را اعمال می‌کند و از بیرون زدن فرزندان درحال انتقال از مرزهای مؤلفه جلوگیری می‌کند.

۴. انتقال‌های هم‌زمان و تودرتو

زیردرخت‌های مؤلفه جداگانه می‌توانند element.startViewTransition() را به‌طور هم‌زمان بدون رد شدن از یکدیگر اجرا کنند. دامنه‌های فعال به‌طور خودکار view-transition-scope: all را در برگه سبک نگارش UA دریافت می‌کنند تا مقادیر view-transition-name داخلی به گذارهای بیرونی نشت نکنند. ‫view-transition-scope: all را در CSS روی محفظه‌های مؤلفه تکرارشده اعمال کنید تا از ضبط نام‌های داخلی توسط انتقال‌های بیرونی جلوگیری شود، حتی زمانی که مؤلفه غیرفعال است.

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

مثال

برای ایجاد دامنه، به هیچ CSS در خود عنصر دامنه (.card یا .card ul) نیاز نیست—دامنه با فراخوانی startViewTransition() در عنصر به‌جای document تعیین می‌شود (اگر مؤلفه در داخل یک انتقال نمای بیرونی تودرتو شود، اجرای انتقال بیرونی را برای view-transition-scope: all ببینید).

/* 1. Name participating child items */
.card ul li {
  view-transition-name: match-element;
}

/* 2. OPTIONAL: Customize transition timing (300ms is an example duration) */
::view-transition-group(*) {
  animation-duration: 300ms;
  animation-timing-function: ease-in-out;
}

/* 3. MANDATORY: Respect user preference for reduced motion */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}
// 1. Trigger a transition scoped to a single component subtree
function runScopedTransition(scopeEl, updateDOM) {
  // MANDATORY: Call startViewTransition on the component element, not document
  const transition = scopeEl.startViewTransition(() => {
    updateDOM();
  });

  // Re-triggering startViewTransition() on the same element skips the active transition
  // and rejects `ready`. Catch the rejection to avoid unhandled promise rejection errors.
  transition.ready.catch(() => {});
}

// 2. Reorder items by moving existing DOM nodes (required for `match-element`)
// Scope to `listEl` since only the list order changes, keeping controls outside `listEl` interactive
function reorderItems(listEl, orderedIds) {
  const existing = new Map(
    Array.from(listEl.children, (li) => [li.dataset.id, li]),
  );

  runScopedTransition(listEl, () => {
    for (const id of orderedIds) {
      const li = existing.get(id);
      if (li) listEl.appendChild(li);
    }
  });
}

// 3. Remove an item while preserving exit animation and keyboard focus
// Scope to the outer `cardEl` so the card itself also transitions its height change
function removeItem(cardEl, itemEl) {
  const hadFocus = itemEl.contains(document.activeElement);
  const nextFocusTarget =
    itemEl.nextElementSibling?.querySelector('button') ??
    itemEl.previousElementSibling?.querySelector('button') ??
    cardEl.querySelector('button');

  runScopedTransition(cardEl, () => {
    itemEl.remove();
    if (hadFocus) {
      nextFocusTarget?.focus();
    }
  });
}

درحال اجرای گذار بیرونی

هنگام شروع انتقال نما در خارج از محدوده موردنظر (مثلاً از یک مؤلفه والد، یا از سند برای انتقال در سطح صفحه)، انتقال بیرونی عنصر محدوده و همه چیز داخل آن را ضبط و پویانمایی می‌کند.

برای جلوگیری از این اتفاق، عنصر دامنه را در عنصر پوشاننده جداگانه‌ای با view-transition-name خودش بپیچید و view-transition-scope: all را روی عنصر دامنه داخلی اعمال کنید. این کار به خود پوشاننده اجازه می‌دهد تا توسط انتقال بیرونی ضبط و پویانمایی شود، درحالی‌که از ضبط محتویات دامنه داخلی جلوگیری می‌کند.

<div class="page" id="page">
  <div class="card-wrapper">
    <section class="card" id="left">
      <h2>Card A</h2>
      <ul id="left-list">
        <li>…</li>
      </ul>
    </section>
  </div>
  <div class="card-wrapper">
    <section class="card" id="right">
      <h2>Card B</h2>
      <ul id="right-list">
        <li>…</li>
      </ul>
    </section>
  </div>
</div>
document.querySelector('#page').startViewTransition(() => {
  // do your DOM updates here...
});
/* Apply a view-transition-name on the wrapper */
.card-wrapper {
  view-transition-name: match-element;
}

/* Prevent the contents of the card being captured by an outer transition */
.card {
  view-transition-scope: all;
}

.card ul li {
  view-transition-name: match-element;
}

یا می‌توانید به انتقال بیرونی اجازه دهید تا فرزندان را ضبط کند. برای حفظ جلوه‌های برش، از «گروه‌های انتقال نمای تودرتو» برای برش دادن فرزندان ضبط‌شده در مرزهای پوشاننده آن‌ها استفاده کنید. اما در این حالت، باید clip را به‌صورت دستی روی ::view-transition-group-children() کاذب حاصل اعمال کنید.

.card-wrapper {
  overflow: clip;
  view-transition-name: match-element; /* Capture me as part of the outer VT */
  view-transition-group: contain; /* Nest children that also have a view-transition-name */
  view-transition-class: card; /* For targeting purposes */
}

/* Manually copy back the clip onto the ::view-transition-group-children pseudo */
::view-transition-group-children(.card) {
  overflow: clip;
}

.card ul li {
  view-transition-name: match-element;
}

محدودیت‌ها و دسترس‌پذیری

  • اجباری: startViewTransition() را به‌جای document در عنصر ریشه مؤلفه فراخوانی کنید تا عناصر خارجی position: fixed و هم‌پوشانی‌های [popover] به‌صورت لایه‌ای در بالا باقی بمانند و بقیه صفحه تعاملی بماند.
  • اجباری: هنگام استفاده از view-transition-name: match-element، گره‌های DOM موجود را حفظ و منتقل کنید (به‌جای اینکه آن‌ها را با innerHTML بازآفرینی کنید)، زیرا match-element هویت گره DOM را ردیابی می‌کند.
  • اجباری: با تنظیم کردن animation: none !important روی ::view-transition-group(*)، ::view-transition-old(*)، و ::view-transition-new(*)، به اولویت‌های کاربر برای کاهش حرکت بااستفاده از @media (prefers-reduced-motion: reduce) احترام بگذارید.
  • اجباری: هنگام برداشتن عنصر دارای تمرکز درطول انتقال، تمرکز صفحه‌کلید را با جابه‌جا کردن تمرکز به عنصر یا کنترل مجاور در بازخوان به‌روزرسانی حفظ کنید.
  • باید: از «گروه‌های انتقال نمای تودرتو» (view-transition-group: contain و overflow: clip در ::view-transition-group-children()) استفاده کنید وقتی عنصر ضبط‌شده داخل محدوده، سرریز خودش را برش می‌دهد و حاوی عناصر ضبط‌شده تودرتو است.
  • باید: وقتی تعاملات سریع می‌تواند element.startViewTransition() را در همان عنصر دامنه دوباره راه‌اندازی کند، transition.ready.catch(() => {}) را پیوست کنید.
  • باید: اگر انتقال بیرونی نیاز به پویانمایی موقعیت مؤلفه درحین اجرای انتقال داخلی دارد، عنصر محدوده داخلی را در عنصر پوشاننده نام‌گذاری‌شده جداگانه‌ای قرار دهید.
  • این کار را نکنید: view-transition-name: none را روی عنصر دامنه‌ای تنظیم کنید که سرریز آن را برش می‌دهد (overflow: hidden، scroll، یا clip) یا هندسه خودش را تغییر می‌دهد، زیرا انصراف از مشارکت خودکار، برش خودکار سرریز را غیرفعال می‌کند.

راهبردهای جایگزین

اگر هدف Baseline شما از انتقال‌های نمای با محدوده عنصر پشتیبانی نمی‌کند، با اجرای فوری فراخوانی برگشت به‌روزرسانی DOM بدون پویانمایی، به‌طور تدریجی و ملایم تنزل کنید. اگرچه می‌توانید به document.startViewTransition() برگردید، اما این کار برای به‌روزرسانی‌های مؤلفه مجزا توصیه نمی‌شود، زیرا انتقال در محدوده سند، انتقال‌های مؤلفه هم‌زمان را مسدود می‌کند و عناصر position: fixed و [popover] را در لایه بالایی پنهان می‌کند.

// Feature detect specifically on Element.prototype (not document.startViewTransition)
const supportsScopedTransition = 'startViewTransition' in Element.prototype;

function runWithTransition(scopeEl, updateDOM) {
  if (!supportsScopedTransition) {
    // Fallback: apply the DOM update immediately without animation
    updateDOM();
    return;
  }

  const transition = scopeEl.startViewTransition(() => {
    updateDOM();
  });

  transition.ready.catch(() => {});
}

اگر واقعاً لازم است بااستفاده از document.startViewTransition() یک جایگزین داشته باشید، باید مطمئن شوید که جلوه‌های برش محفظه‌ها حفظ می‌شوند، همان‌طور که در بخش اجرای انتقال بیرونی به‌تفصیل توضیح داده شده است. توجه داشته باشید که این رویکرد جایگزین، توانایی اجرای هم‌زمان چندین انتقال نما را ازبین می‌برد و عناصر position: fixed و [popover] را در لایه بالایی پنهان می‌کند.