Vuốt để xem

Mẫu vuốt để hiện cho phép người dùng trượt một hàng hoặc ngăn nội dung trên nền trước theo chiều ngang để hiển thị giao diện người dùng bị ẩn (chẳng hạn như các nút thao tác theo bối cảnh (Ghim, Tắt tiếng, Lưu trữ, Xoá) hoặc trình đơn phụ bên cạnh) mà không tự động kích hoạt bất kỳ thao tác nào. Thay vì thực hiện một hành động trong khi vuốt, vùng chứa sẽ mở ra để hiển thị các thành phần điều khiển tương tác mà người dùng có thể kiểm tra và kích hoạt, hoặc vuốt ngược lại để đóng.

Để biết cử chỉ vuốt toàn chiều rộng giúp xác nhận và đóng ngay một mục vượt quá ngưỡng, hãy xem phần Vuốt để xoá. Để hiển thị tiêu đề bị ẩn ở đầu vùng chứa cuộn dọc, hãy xem phần Kéo để hiển thị.

Cách triển khai

Cho dù bạn đang hiển thị các nút hành động trên một mục danh sách hay trình đơn bên cạnh một ngăn có thể cuộn, thì cơ chế đều giống nhau: gói nội dung trên nền trước và bảng điều khiển ẩn bên trong một vùng chứa cuộn ngang bên ngoài được định cấu hình bằng CSS Scroll Snap (scroll-snap-type: x mandatory) và overscroll-behavior: chain. Công cụ cuộn gốc của trình duyệt xử lý động lượng, chụp nhanh, cuộn tiêu điểm bàn phím và chuỗi cuộn mà không cần trình nghe cử chỉ JavaScript.

Bước 1: Đánh dấu trình bao bọc vuốt, nội dung và bảng điều khiển được hiển thị

Đặt nội dung trên nền trước ở vị trí đầu tiên và các chế độ điều khiển được hiển thị (nút hành động hoặc trình đơn bên) ở vị trí thứ hai bên trong vùng chứa cuộn .SwipeReveal. Vì nội dung đứng đầu trong thứ tự DOM, nên vùng chứa cuộn sẽ bắt đầu một cách tự nhiên ở độ lệch cuộn 0 với bảng điều khiển bị ẩn khỏi màn hình ở bên phải.

<!-- 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>

Bước 2: Định cấu hình vùng chứa đính ngang và overscroll-behavior: chain

Khi bạn gói một phần tử trong vùng chứa cuộn bên ngoài chỉ để tạo hiệu ứng vuốt để hiển thị, thì trình bao bọc bên ngoài đó sẽ trở thành trình cuộn trung gian bên trong tài liệu (hoặc một vùng chứa cuộn mẹ khác).

Bốn từ khoá của overscroll-behavior kiểm soát độc lập việc thao tác cuộn quá mức có truyền đến vùng chứa cuộn cấp trên hay không (chuỗi cuộn) và liệu vùng chứa có hiển thị các hiệu ứng cuộn quá mức cục bộ như kéo giãn dây thun hay phát sáng ở cạnh hay không (hiệu ứng ranh giới cục bộ):

Giá trị Chuỗi cuộn (Truyền đến phần tử cấp trên) Hiệu ứng ranh giới cục bộ (Dây cao su/Phát sáng) Vai trò trong tính năng Vuốt để hiện
auto Có Có Hành vi mặc định; giữ chế độ này trên các trình cuộn nội dung bên trong khi muốn có phản hồi nảy bình thường, nhưng tránh trên trình bao bọc vuốt bên ngoài khi chế độ này khiến trình đơn nảy lên
chain Có Không Sử dụng trên thanh cuộn bên ngoài .SwipeReveal để chuỗi cuộn thừa vào thanh cuộn mẹ (chẳng hạn như tài liệu) mà không có trình đơn hoặc dải hành động nảy lên
contain Không Có Bẫy cuộn bên trong vùng chứa trong khi vẫn nảy cục bộ
none Không Không Chặn cả hiệu ứng nảy cục bộ và hiệu ứng cuộn theo chuỗi đến các phần tử cấp trên

Tạo .SwipeReveal thành lưới hai cột (grid-template-columns: 100% max-content), trong đó .SwipeReveal-content bám vào start và .SwipeReveal-panel bám vào 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;
  }
}

Nếu bạn cũng muốn hiển thị các thành phần điều khiển ở bên trái (inline-start) bằng cách sử dụng ba cột (grid-template-columns: max-content 100% max-content) hoặc hai cột (max-content 100%), hãy đặt .SwipeReveal-panel ở đầu trước .SwipeReveal-content. Vì .SwipeReveal-content có scroll-initial-target: nearest, nên vùng chứa cuộn ban đầu nằm trên cột nội dung thay vì bảng điều khiển đầu tiên.

Bước 3: Đưa các ngăn nội dung có thể làm tâm điểm trở lại chế độ xem khi tiêu điểm bàn phím

Vì các nút hoặc đường liên kết bên trong .SwipeReveal-panel nằm ngoài màn hình khi ở trạng thái nghỉ, nên việc nhấn phím Tab để chuyển đến các nút hoặc đường liên kết đó bằng bàn phím sẽ tự động cuộn .SwipeReveal để hiển thị thành phần điều khiển được lấy tiêu điểm. Tuy nhiên, vì .SwipeReveal-content chiếm 100% chiều rộng của vùng chứa bên ngoài, nên hầu hết phần tử này vẫn nằm trong cổng cuộn ngay cả khi bảng điều khiển bên đang mở. Do đó, việc nhấn Shift+Tab để di chuyển tiêu điểm trở lại .SwipeReveal-content (hoặc vào một phần tử có thể làm tâm điểm bên trong phần tử đó) sẽ không tự động cuộn bản nhạc trở lại 0.

Lắng nghe focusin trên .SwipeReveal-content và kiểm tra event.target.matches(':focus-visible') để tiêu điểm bàn phím trên ngăn hoặc bất kỳ thành phần con nào đều đưa .SwipeReveal-content về vị trí bắt đầu mà không can thiệp vào thao tác nhấp bằng con trỏ hoặc kéo bằng thao tác chạm:

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

Các phương pháp hay nhất và những điều cần tránh

  • NÊN sử dụng scroll-snap-type: x mandatory thay vì proximity. Với proximity, một thao tác vuốt nhẹ có thể khiến các nút hành động hoặc trình đơn bên hiển thị một phần khi ở trạng thái nghỉ.
  • NÊN đặt overscroll-behavior: chain trên trình cuộn vuốt ngang bên ngoài để hiển thị (.SwipeReveal) để thao tác cuộn có thể liên kết vào trình cuộn mẹ mà không làm trình đơn hoặc dải hành động hiển thị bị nảy.
  • NÊN để overscroll-behavior: auto (mặc định) trên các ngăn nội dung có thể cuộn bên trong (.SwipeReveal-content.is-scrollable) nơi bạn muốn phản hồi nảy bình thường xảy ra trong khi vẫn liên kết các thao tác vuốt ngang với trình cuộn vuốt bên ngoài.
  • NÊN triển khai các thao tác được hiển thị và các mục trong trình đơn bên dưới dạng các thành phần điều khiển DOM thực sự có thể làm tâm điểm (<button>, <a>). Khi .SwipeReveal-content có thể làm tâm điểm hoặc chứa các thành phần điều khiển có thể làm tâm điểm, hãy ghép nối trình nghe focusin với event.target.matches(':focus-visible') để gọi scrollIntoView({ block: 'nearest', inline: 'start' }), nhờ đó, việc chuyển về ngăn nội dung sẽ đóng bảng điều khiển bên mà không ảnh hưởng đến các hoạt động tương tác của con trỏ.
  • KHÔNG chặn các sự kiện wheel, touchmove hoặc pointermove trong JavaScript để dịch nội dung theo cách thủ công hoặc mô phỏng chuỗi cuộn. Việc chặn cử chỉ trên luồng chính sẽ chặn thao tác cuộn do trình kết hợp điều khiển, làm gián đoạn động lượng cuộn gốc và làm giảm khả năng phản hồi.

Dự phòng

Khi vùng chứa .SwipeReveal đặt .SwipeReveal-panel ở bên trái (inline-start) trước .SwipeReveal-content, các trình duyệt không hỗ trợ scroll-initial-target: nearest ban đầu sẽ hiển thị ở độ lệch cuộn 0 khi bảng điều khiển chính hiển thị.

Nếu mục tiêu Baseline của bạn không hỗ trợ scroll-initial-target, hãy phát hiện tính năng hỗ trợ bằng CSS.supports('scroll-initial-target', 'nearest') và gọi scrollTo() bằng behavior: 'instant' trên load để các vùng chứa có bảng điều khiển chính chuyển thẳng đến .SwipeReveal-content mà không cần cuộn trang:

Cải tiến tăng dần

Hãy coi overscroll-behavior: chain và scrollbar-width: none là các cải tiến tăng dần mà không có phương án dự phòng JavaScript:

  • Dự phòng xếp tầng CSS tự động: Giá trị ban đầu của overscroll-behavior là auto. Những trình duyệt chưa nhận dạng được từ khoá chain sẽ bỏ qua overscroll-behavior: chain tại thời điểm phân tích cú pháp và giữ lại auto. Vì auto đã cho phép tạo chuỗi cuộn cho các vùng chứa cuộn cấp trên, nên cả cử chỉ vuốt để hiện và chuỗi cuộn cho vùng chứa mẹ đều hoạt động trên tất cả các trình duyệt ngay từ đầu; các trình duyệt hỗ trợ chỉ cần chặn hiệu ứng nảy cao su không mong muốn trên trình bao bọc vuốt bên ngoài.
  • Ghi đè contain hoặc none: Nếu một biểu định kiểu cơ sở hoặc lớp tiện ích đặt overscroll-behavior: contain hoặc none, hãy khai báo overscroll-behavior: auto ngay trước overscroll-behavior: chain để các trình duyệt chưa hỗ trợ chain vẫn liên kết thao tác cuộn với phần tử mẹ thay vì giữ lại thao tác cuộn.
  • Ẩn thanh cuộn (scrollbar-width: none): Việc ẩn thanh cuộn ngang hoàn toàn chỉ mang tính thẩm mỹ; nếu một công cụ cũ bỏ qua scrollbar-width: none, thì vùng chứa vuốt để hiện vẫn hoạt động đầy đủ với thanh cuộn tiêu chuẩn.