การเปลี่ยนเอกสารเดียวกัน

ฟีเจอร์บนเว็บ: การเปลี่ยนมุมมอง

ปัญหา

เว็บไซต์มักจะแสดงมุมมองหลายแบบของออบเจ็กต์ เช่น รายการผลิตภัณฑ์ และหน้ารายละเอียดของผลิตภัณฑ์แต่ละรายการ การสลับไปมาระหว่าง 2 มุมมองนี้มักทำให้รู้สึกไม่ต่อเนื่อง เมื่อผู้ใช้คลิกภาพขนาดย่อของผลิตภัณฑ์เพื่อดูรายละเอียด ภาพขนาดย่อจะหายไปและรูปภาพใหม่ที่มีขนาดใหญ่ขึ้นจะปรากฏขึ้นทันทีที่ตำแหน่งอื่นบนหน้าจอ การขาดความต่อเนื่องนี้ทำให้ผู้ใช้ติดตามความสัมพันธ์ระหว่างองค์ประกอบต่างๆ ได้ยากขึ้น

วิธีแก้ปัญหา

View Transitions API ช่วยให้คุณระบุคู่ขององค์ประกอบที่มีอยู่ในสถานะต่างๆ ก่อนและหลังการเปลี่ยนผ่านได้ เมื่อทริกเกอร์การเปลี่ยนด้วย document.startViewTransition() ในการนำทางหน้าเว็บเดียว (SPA) เบราว์เซอร์จะระบุองค์ประกอบที่ใช้ร่วมกันเหล่านี้ด้วย view-transition-name ที่ไม่ซ้ำกันที่ใช้ร่วมกัน จากนั้นระบบจะคำนวณความแตกต่างของตำแหน่ง ขนาด และการจัดสไตล์โดยอัตโนมัติ และทำให้องค์ประกอบเคลื่อนไหวอย่างราบรื่นจากสถานะเดิมไปยังสถานะใหม่ การเปลี่ยนนี้จะเกิดขึ้นในเลเยอร์บนสุด เหนือองค์ประกอบที่มีค่า z-index สูง

คู่มือการติดตั้งใช้งาน

ขั้นตอนที่ 1: รวมการเปลี่ยนแปลงสถานะไว้ใน startViewTransition

สำหรับแอปพลิเคชันหน้าเว็บเดียว (SPA) หรือการเปลี่ยนแปลงสถานะแบบง่าย ให้รวมตรรกะที่อัปเดต DOM ไว้ใน document.startViewTransition เบราว์เซอร์จะจับภาพสแนปชอตของสถานะปัจจุบัน เรียกใช้การอัปเดต แล้วจับภาพสถานะใหม่

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

ขั้นตอนที่ 2: กำหนดชื่อการเปลี่ยนที่ใช้ร่วมกัน

ใช้พร็อพเพอร์ตี้ CSS view-transition-name เพื่อบอกเบราว์เซอร์ว่าควรเปลี่ยนรูปแบบองค์ประกอบใด ชื่อจะเป็นอะไรก็ได้ (ยกเว้น none) บังคับ: ต้องมีองค์ประกอบไม่เกิน 1 รายการก่อนและหลัง view-transition-name ที่ระบุ หากมีองค์ประกอบตั้งแต่ 2 รายการขึ้นไปที่มี view-transition-name ที่ระบุ ระบบจะอัปเดต DOM เป็นสถานะใหม่ทันทีโดยไม่มีการเปลี่ยนผ่าน

คุณใช้ view-transition-name หลายรายการเพื่อเปลี่ยนรูปร่างองค์ประกอบหลายคู่ได้ ตัวอย่างเช่น คุณอาจต้องการเปลี่ยนทั้งรูปภาพและชื่อผลิตภัณฑ์ด้วยการเปลี่ยนภาพที่แยกกัน

เนื่องจากมีหลายรายการในมุมมองรายการ คุณจึงไม่สามารถกำหนด view-transition-name เดียวกันให้กับทุกรายการได้ ปัญหานี้แก้ไขได้ 2 วิธีใน SPA

  1. หน้ารายละเอียดแบบไดนามิก: กำหนด view-transition-name ที่ไม่ซ้ำกันให้กับแต่ละรายการในหน้ารายการ แล้วใช้ชื่อนั้นกับองค์ประกอบที่ตรงกันในหน้ารายละเอียดแบบไดนามิกเมื่อเลือกรายการในลิสต์ ดังที่แสดงที่นี่

    /* In the list view, give each */
    #product-1 { view-transition-name: p1 }
    #product-2 { view-transition-name: p2 }
    #product-3 { view-transition-name: p3 }
    
    function updateDOM(clickedTransitionName){
      const hero = document.getElementById("hero");
      hero.style.viewTransitionName = clickedTransitionName;
    }
    
  2. รายการแบบไดนามิก: กำหนด view-transition-name ให้กับองค์ประกอบในหน้ารายละเอียด และใช้ชื่อนั้นกับรายการในหน้ารายการเมื่อเลือกองค์ประกอบดังกล่าว นำ view-transition-name ออกจากรายการในหน้ารายการเมื่อกลับไปที่หน้ารายการ

องค์ประกอบ #hero ในหน้ารายละเอียดและองค์ประกอบ .thumbnail ที่เลือกในหน้ารายการใช้ view-transition-name ร่วมกัน

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

เมื่อมีการคลิกภาพขนาดย่อ เราต้องเตรียมมุมมองรายการโดยกำหนด view-transition-name โดยใช้ตัวเลือกคลาส .selected และทำการเปลี่ยนแปลง DOM ก่อนเริ่มการเปลี่ยนภาพ

จากนั้น คุณสามารถเรียกใช้ document.startViewTransition() และใช้การเปลี่ยนแปลงเพื่อเปลี่ยนหน้าจากมุมมองรายละเอียดเป็นมุมมองรายการ

หลังจากกลับไปที่มุมมองรายการแล้ว คุณต้องล้างคลาสการเปลี่ยนมุมมองเพื่อป้องกันไม่ให้การนำทางครั้งถัดไปเกิดข้อผิดพลาด คุณสามารถดำเนินการล้างข้อมูลนี้ได้หลังจากที่ Promise finished ของการเปลี่ยนผ่านได้รับการแก้ไขแล้ว

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

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

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

โดยวิธีที่คุณเลือกจะขึ้นอยู่กับกรณีการใช้งาน รายการแบบไดนามิกต้องใช้ CSS ซ้ำน้อยลง แต่ต้องล้างข้อมูล JavaScript ด้วยตนเองมากขึ้น

ขั้นตอนที่ 3: แก้ไข "การยืด" สัดส่วนภาพ

โดยค่าเริ่มต้น เบราว์เซอร์จะเฟดภาพรวมเก่าและใหม่ภายในกลุ่มที่ยืดออกเพื่อให้พอดีกับทั้ง 2 ภาพ หากคุณกำลังเปลี่ยนข้อความ ให้ตั้งค่าความกว้างขององค์ประกอบของข้อความให้เป็น 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());
  }
}