ปัดเพื่อดู

รูปแบบการปัดเพื่อแสดงช่วยให้ผู้ใช้เลื่อนแถวหรือแผงเนื้อหาที่อยู่เบื้องหน้าในแนวนอนเพื่อแสดง UI ที่ซ่อนอยู่ เช่น ปุ่มดำเนินการตามบริบท (ปักหมุด ปิดเสียง เก็บ ลบ) หรือเมนูด้านข้างรอง โดยไม่ต้องเปิดใช้งานการดำเนินการใดๆ โดยอัตโนมัติ แทนที่จะดำเนินการในระหว่างการปัด คอนเทนเนอร์จะเปิดขึ้นเพื่อแสดงการควบคุมแบบอินเทอร์แอกทีฟที่ผู้ใช้สามารถตรวจสอบและเปิดใช้งาน หรือปัดกลับเพื่อปิด

ดูท่าทางสัมผัสการปัดแบบเต็มความกว้างที่ยืนยันและปิดรายการทันทีเมื่อผ่านเกณฑ์ได้ที่หัวข้อปัดเพื่อนำออก หากต้องการแสดงส่วนหัวที่ซ่อนอยู่ที่ด้านบนของคอนเทนเนอร์การเลื่อนแนวตั้ง โปรดดูหัวข้อดึงเพื่อแสดง

วิธีการนำไปใช้งาน

ไม่ว่าคุณจะแสดงปุ่มดำเนินการในรายการหรือเมนูด้านข้างถัดจากแผงที่เลื่อนได้ กลไกจะเหมือนกัน นั่นคือรวมเนื้อหาเบื้องหน้าและแผงที่ซ่อนไว้ภายในคอนเทนเนอร์การเลื่อนแนวนอนด้านนอกที่กำหนดค่าด้วย CSS Scroll Snap (scroll-snap-type: x mandatory) และ overscroll-behavior: chain เครื่องมือการเลื่อนแบบเนทีฟของเบราว์เซอร์จะจัดการโมเมนตัม การสแนป การเลื่อนโฟกัสของแป้นพิมพ์ และการต่อสายโซ่การเลื่อนโดยไม่ต้องใช้ Listener ท่าทางสัมผัสของ JavaScript

ขั้นตอนที่ 1: มาร์กอัป Wrapper การปัด เนื้อหา และแผงที่แสดง

วางเนื้อหาเบื้องหน้าก่อน แล้วจึงวางตัวควบคุมที่แสดง (ปุ่มดำเนินการหรือเมนูด้านข้าง) เป็นลำดับที่ 2 ภายในคอนเทนเนอร์การเลื่อน .SwipeReveal เนื่องจากเนื้อหาเป็นอันดับแรกในลำดับ DOM คอนเทนเนอร์การเลื่อนจึงเริ่มต้นที่ออฟเซ็ตการเลื่อน 0 โดยธรรมชาติ โดยแผงจะซ่อนอยู่นอกหน้าจอทางด้านขวา

<!-- Example A: List item with revealable action buttons -->
<div class="SwipeReveal">
  <div class="SwipeReveal-content">
    <strong>Design review notes</strong>
    <p>Updated component specs for the Q3 release.</p>
  </div>
  <div class="SwipeReveal-panel" role="group" aria-label="Item actions">
    <button type="button">Pin</button>
    <button type="button">Archive</button>
    <button type="button">Delete</button>
  </div>
</div>

<!-- Example B: Scrollable content pane with a revealable side menu -->
<div class="SwipeReveal" role="region" aria-label="Document preview with swipeable side menu">
  <div class="SwipeReveal-content is-scrollable" tabindex="0">
    <h2>Article Preview</h2>
    <p>Scroll vertically through this pane, or swipe left to reveal the side menu...</p>
  </div>
  <nav class="SwipeReveal-panel" aria-label="Contextual menu">
    <a href="#share">Share</a>
    <a href="#bookmark">Bookmark</a>
    <a href="#export">Export PDF</a>
  </nav>
</div>

ขั้นตอนที่ 2: กำหนดค่าคอนเทนเนอร์สแนปแนวนอนและ overscroll-behavior: chain

เมื่อคุณรวมองค์ประกอบไว้ในคอนเทนเนอร์การเลื่อนด้านนอกเพื่อสร้างเอฟเฟกต์ปัดเพื่อแสดงเท่านั้น Wrapper ด้านนอกนั้นจะกลายเป็นตัวเลื่อนระดับกลางภายในเอกสาร (หรือคอนเทนเนอร์การเลื่อนระดับบนอื่น)

คีย์เวิร์ด 4 รายการของ overscroll-behavior จะควบคุมอย่างอิสระว่าการเลื่อนที่เกินมาจะส่งต่อไปยังคอนเทนเนอร์การเลื่อนระดับบน (การต่อเชื่อมการเลื่อน) หรือไม่ และคอนเทนเนอร์จะแสดงการรองรับการเลื่อนเกินในเครื่อง เช่น การยืดแถบยางยืดหรือแสงที่ขอบ (เอฟเฟกต์ขอบเขตในเครื่อง) หรือไม่

ค่า การเชื่อมโยงการเลื่อน (ส่งต่อไปยังระดับบนสุด) เอฟเฟกต์ขอบเขตในเครื่อง (แถบยางยืด/แสงเรือง) บทบาทใน Swipe-to-Reveal
auto ใช่ ใช่ ลักษณะการทำงานเริ่มต้น เปิดใช้ลักษณะการทำงานนี้ไว้ในแถบเลื่อนเนื้อหาด้านในที่ต้องการการตอบสนองการเด้งตามปกติ แต่หลีกเลี่ยงการใช้ใน Wrapper การปัดด้านนอกที่ทำให้เมนูเด้ง
chain มี ไม่ ใช้กับแถบเลื่อนด้านนอก .SwipeReveal เพื่อให้การเลื่อนที่เกินมาเชื่อมโยงไปยังแถบเลื่อนหลัก (เช่น เอกสาร) โดยที่เมนูหรือแถบการดำเนินการไม่เด้ง
contain ไม่ใช่ ใช่ ดักจับการเลื่อนภายในคอนเทนเนอร์ขณะที่ยังคงเด้งในเครื่อง
none ไม่ใช่ ไม่ใช่ บล็อกทั้งการเด้งในเครื่องและการเชื่อมโยงการเลื่อนไปยังระดับบน

ทำให้ .SwipeReveal เป็นตารางกริด 2 คอลัมน์ (grid-template-columns: 100% max-content) โดยที่ .SwipeReveal-content จะสแนปกับ start และ .SwipeReveal-panel จะสแนปกับ end:

.SwipeReveal {
  /* Column 1: full-width content. Column 2: auto-sized actions or side menu. */
  display: grid;
  grid-template-columns: 100% max-content;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;

  /* Prevent the swipe wrapper and revealed panel from bouncing locally while
     still chaining excess scroll into the parent scroller (the document). */
  overscroll-behavior: chain;
}

@media (prefers-reduced-motion: no-preference) {
  .SwipeReveal {
    scroll-behavior: smooth;
  }
}

.SwipeReveal-content {
  scroll-snap-align: start;
  scroll-initial-target: nearest;
  background: Canvas;
}

.SwipeReveal-content:focus-visible {
  outline: auto;
  outline-offset: -2px;
}

.SwipeReveal-content.is-scrollable {
  block-size: 20rem;
  overflow: auto;
  /* Keeps default `overscroll-behavior: auto` so the content pane retains
     its normal bounce effect while chaining horizontal swipes to `.SwipeReveal`. */
}

.SwipeReveal-panel {
  /* Snapping a trailing panel to `end` (or a leading panel to `start`) brings
     all controls fully into view while keeping the adjacent edge of the content visible. */
  scroll-snap-align: end;
  display: flex;

  &:first-child {
    scroll-snap-align: start;
  }
}

หากต้องการแสดงการควบคุมทางด้านซ้าย (inline-start) โดยใช้ 3 คอลัมน์ (grid-template-columns: max-content 100% max-content) หรือ 2 คอลัมน์ (max-content 100%) ให้วาง .SwipeReveal-panel ไว้หน้า .SwipeReveal-content เนื่องจาก .SwipeReveal-content มี scroll-initial-target: nearest คอนเทนเนอร์การเลื่อนจึงวางอยู่บนคอลัมน์เนื้อหาในตอนแรก ไม่ใช่แผงนำทาง

ขั้นตอนที่ 3: จัดวางแผงเนื้อหาที่โฟกัสได้กลับเข้าสู่มุมมองเมื่อโฟกัสของแป้นพิมพ์

เนื่องจากปุ่มหรือลิงก์ภายใน .SwipeReveal-panel อยู่นอกหน้าจอเมื่อไม่มีการใช้งาน การกด Tab เพื่อไปยังปุ่มหรือลิงก์ดังกล่าวด้วยแป้นพิมพ์จะเลื่อน .SwipeReveal โดยอัตโนมัติเพื่อแสดงการควบคุมที่โฟกัส อย่างไรก็ตาม เนื่องจาก .SwipeReveal-content ครอบคลุม 100% ของความกว้างของคอนเทนเนอร์ด้านนอก ส่วนใหญ่จึงยังคงอยู่ภายในพอร์ตการเลื่อนแม้ว่าแผงด้านข้างจะเปิดอยู่ ดังนั้นการกด Shift+Tab เพื่อย้ายโฟกัสกลับไปที่ .SwipeReveal-content (หรือไปยังองค์ประกอบที่โฟกัสได้ภายใน) จะไม่เลื่อนแทร็กกลับไปที่ 0 โดยอัตโนมัติ

ฟัง focusin ใน .SwipeReveal-content และตรวจสอบ event.target.matches(':focus-visible') เพื่อให้โฟกัสของแป้นพิมพ์ในแผงหรือองค์ประกอบสืบทอดใดๆ เลื่อน .SwipeReveal-content กลับไปที่จุดเริ่มต้นโดยไม่รบกวนการคลิกเคอร์เซอร์หรือการลากด้วยการสัมผัส

for (const content of document.querySelectorAll('.SwipeReveal-content')) {
  content.addEventListener('focusin', (event) => {
    if (event.target.matches(':focus-visible')) {
      content.scrollIntoView({
        block: 'nearest',
        inline: 'start',
      });
    }
  });
}

แนวทางปฏิบัติแนะนำและข้อควรระวัง

  • ควรใช้ scroll-snap-type: x mandatory แทน proximity เมื่อใช้ proximity การปัดเบาๆ อาจทำให้ปุ่มดำเนินการหรือเมนูด้านข้างแสดงเพียงบางส่วนเมื่อไม่ได้ใช้งาน
  • ควรตั้งค่า overscroll-behavior: chain ในแถบเลื่อนแบบปัดเพื่อแสดงในแนวนอนด้านนอก (.SwipeReveal) เพื่อให้การเลื่อนเชื่อมโยงกับแถบเลื่อนหลักได้โดยที่เมนูที่แสดงหรือแถบการดำเนินการไม่เด้งกลับ
  • ควรปล่อย overscroll-behavior: auto (ค่าเริ่มต้น) ไว้ในแผงเนื้อหาที่เลื่อนได้ด้านใน (.SwipeReveal-content.is-scrollable) ซึ่งคุณต้องการให้มีการตอบสนองการเด้งตามปกติขณะที่ยังคงเชื่อมโยงการปัดแนวนอนกับแถบเลื่อนการปัดด้านนอก
  • ควรใช้การดำเนินการที่เปิดเผยและรายการเมนูด้านข้างเป็นตัวควบคุม DOM ที่โฟกัสได้จริง (<button>, <a>) เมื่อ .SwipeReveal-content โฟกัสได้หรือมีตัวควบคุมที่โฟกัสได้ ให้จับคู่ Listener focusin กับ event.target.matches(':focus-visible') เพื่อเรียกใช้ scrollIntoView({ block: 'nearest', inline: 'start' }) เพื่อให้การกด Tab กลับเข้าไปในแผงเนื้อหาปิดแผงด้านข้างโดยไม่ส่งผลต่อการโต้ตอบของเคอร์เซอร์
  • อย่าขัดจังหวะเหตุการณ์ wheel, touchmove หรือ pointermove ใน JavaScript เพื่อแปลเนื้อหาด้วยตนเองหรือจำลองการเชื่อมโยงการเลื่อน การสกัดกั้นท่าทางสัมผัสของเทรดหลักจะบล็อกการเลื่อนที่ขับเคลื่อนโดยคอมโพสิเตอร์ ทำลายโมเมนตัมการเลื่อนแบบเนทีฟ และลดการตอบสนอง

Fallback

เมื่อคอนเทนเนอร์ .SwipeReveal วาง .SwipeReveal-panel ไว้ทางด้านซ้าย (inline-start) ก่อน .SwipeReveal-content เบราว์เซอร์ที่ไม่รองรับ scroll-initial-target: nearest จะแสดงผลในตอนแรกที่ออฟเซ็ตการเลื่อน 0 โดยแสดงแผงด้านหน้า

หากเป้าหมาย Baseline ไม่รองรับ scroll-initial-target ให้ตรวจหาการรองรับด้วย CSS.supports('scroll-initial-target', 'nearest') และเรียกใช้ scrollTo() ด้วย behavior: 'instant' ใน load เพื่อให้คอนเทนเนอร์ที่มีแผงนำทางข้ามไปยัง .SwipeReveal-content โดยตรงโดยไม่ต้องเลื่อนหน้าเว็บ

การเพิ่มประสิทธิภาพแบบต่อเนื่อง

มองว่า overscroll-behavior: chain และ scrollbar-width: none เป็นการเพิ่มประสิทธิภาพแบบต่อเนื่องโดยไม่มี JavaScript สำรอง

  • CSS Cascade Fallback อัตโนมัติ: ค่าเริ่มต้นของ overscroll-behavior คือ auto เบราว์เซอร์ที่ยังไม่รู้จักคีย์เวิร์ด chain จะละเว้น overscroll-behavior: chain ณ เวลาแยกวิเคราะห์และคง auto ไว้ เนื่องจาก auto อนุญาตให้ใช้การเชื่อมโยงการเลื่อนไปยังคอนเทนเนอร์การเลื่อนระดับบนอยู่แล้ว ทั้งท่าทางสัมผัสแบบปัดเพื่อแสดงและการเชื่อมโยงการเลื่อนไปยังระดับบนจึงทำงานในเบราว์เซอร์ทั้งหมดได้ทันที โดยเบราว์เซอร์ที่รองรับจะระงับการเด้งแบบแถบยางที่ไม่ต้องการใน Wrapper การปัดด้านนอก
  • การลบล้าง contain หรือ none: หากสไตล์ชีตพื้นฐานหรือคลาสยูทิลิตีตั้งค่า overscroll-behavior: contain หรือ none ให้ประกาศ overscroll-behavior: auto ทันทีก่อน overscroll-behavior: chain เพื่อให้เบราว์เซอร์ที่ยังไม่รองรับ chain ยังคงเชื่อมโยงการเลื่อนไปยังระดับบนแทนที่จะดักจับไว้
  • การซ่อนแถบเลื่อน (scrollbar-width: none): การซ่อนแถบเลื่อนแนวนอนเป็นเพียงการตกแต่งเท่านั้น หากเครื่องมือค้นหาเวอร์ชันเก่าละเว้น scrollbar-width: none คอนเทนเนอร์แบบปัดเพื่อแสดงจะยังคงทำงานได้อย่างสมบูรณ์ด้วยแถบเลื่อนมาตรฐาน