Tạo thành phần thông báo ngắn

Thông tin tổng quan cơ bản về cách tạo một thành phần thông báo có thể thích ứng và hỗ trợ tiếp cận.

Trong bài đăng này, tôi muốn chia sẻ suy nghĩ về cách tạo một thành phần thông báo tạm thời. Dùng thử bản minh hoạ.

Bản minh hoạ

Nếu bạn thích xem video, thì đây là phiên bản video của bài đăng này trên YouTube:

Tổng quan

Thông báo dạng nổi là những thông báo ngắn, thụ động, không có tính tương tác và không đồng bộ dành cho người dùng. Nhìn chung, các thành phần này được dùng làm mẫu phản hồi giao diện để thông báo cho người dùng về kết quả của một hành động.

Lượt tương tác

Thông báo tạm thời khác với thông báo, cảnh báolời nhắc vì chúng không có tính tương tác; chúng không được thiết kế để bị loại bỏ hoặc duy trì. Thông báo dành cho thông tin quan trọng hơn, tin nhắn đồng bộ cần có sự tương tác hoặc tin nhắn ở cấp hệ thống (thay vì cấp trang). Thông báo dạng nổi thụ động hơn so với các chiến lược thông báo khác.

Markup (note: đây là tên ứng dụng)

Phần tử <output> là lựa chọn phù hợp cho thông báo tạm thời vì phần tử này được thông báo cho trình đọc màn hình. HTML chính xác cung cấp một cơ sở an toàn để chúng ta có thể cải thiện bằng JavaScript và CSS, đồng thời sẽ có nhiều JavaScript.

Nâng ly

<output class="gui-toast">Item added to cart</output>

Bạn có thể bao gồm nhiều người hơn bằng cách thêm role="status". Điều này cung cấp một giải pháp dự phòng nếu trình duyệt không cung cấp cho các phần tử <output> vai trò ngầm theo quy cách.

<output role="status" class="gui-toast">Item added to cart</output>

Vùng chứa thông báo

Bạn có thể hiển thị nhiều thông báo cùng một lúc. Để điều phối nhiều thông báo tạm thời, một vùng chứa sẽ được dùng. Vùng chứa này cũng xử lý vị trí của thông báo tạm thời trên màn hình.

<section class="gui-toast-group">
  <output role="status">Wizard Rose added to cart</output>
  <output role="status">Self Watering Pot added to cart</output>
</section>

Bố cục

Tôi chọn ghim thông báo tạm thời vào inset-block-end của khung hiển thị. Nếu có thêm thông báo tạm thời, chúng sẽ xếp chồng lên nhau từ cạnh màn hình đó.

Vùng chứa GUI

Vùng chứa thông báo tạm thời thực hiện mọi công việc bố trí để trình bày thông báo tạm thời. Nó fixed vào khung nhìn và sử dụng thuộc tính logic inset để chỉ định các cạnh cần ghim, cộng thêm một chút padding từ cùng một cạnh block-end.

.gui-toast-group {
  position: fixed;
  z-index: 1;
  inset-block-end: 0;
  inset-inline: 0;
  padding-block-end: 5vh;
}

Ảnh chụp màn hình có kích thước hộp và khoảng đệm của Công cụ cho nhà phát triển được phủ lên một phần tử .gui-toast-container.

Ngoài việc tự định vị trong khung nhìn, vùng chứa thông báo tạm thời còn là một vùng chứa lưới có thể căn chỉnh và phân phối thông báo tạm thời. Các mục được đặt ở giữa dưới dạng một nhóm bằng justify-content và được đặt ở giữa riêng lẻ bằng justify-items. Thêm một chút gap để bánh mì nướng không chạm vào nhau.

.gui-toast-group {
  display: grid;
  justify-items: center;
  justify-content: center;
  gap: 1vh;
}

Ảnh chụp màn hình có lớp phủ lưới CSS trên nhóm thông báo, lần này làm nổi bật khoảng trống và khoảng cách giữa các phần tử con của thông báo.

GUI Toast

Mỗi thông báo có một số padding, một số góc mềm hơn với border-radius và hàm min() để hỗ trợ việc điều chỉnh kích thước trên thiết bị di động và máy tính. Kích thước thích ứng trong CSS sau đây ngăn các thông báo tạm thời có chiều rộng lớn hơn 90% khung hiển thị hoặc 25ch.

.gui-toast {
  max-inline-size: min(25ch, 90vw);
  padding-block: .5ch;
  padding-inline: 1ch;
  border-radius: 3px;
  font-size: 1rem;
}

Ảnh chụp màn hình của một phần tử .gui-toast duy nhất, có khoảng đệm và bán kính đường viền.

Kiểu

Sau khi thiết lập bố cục và vị trí, hãy thêm CSS để giúp thích ứng với các chế độ cài đặt và hoạt động tương tác của người dùng.

Vùng chứa thông báo

Thông báo dạng toast không có tính tương tác, việc nhấn hoặc vuốt trên thông báo này sẽ không có tác dụng gì, nhưng hiện tại, thông báo dạng toast có sử dụng các sự kiện con trỏ. Ngăn thông báo tạm thời chặn các lượt nhấp bằng CSS sau.

.gui-toast-group {
  pointer-events: none;
}

GUI Toast

Cung cấp cho thông báo nhanh một giao diện thích ứng sáng hoặc tối với các thuộc tính tuỳ chỉnh, HSL và một truy vấn phương tiện ưu tiên.

.gui-toast {
  --_bg-lightness: 90%;

  color: black;
  background: hsl(0 0% var(--_bg-lightness) / 90%);
}

@media (prefers-color-scheme: dark) {
  .gui-toast {
    color: white;
    --_bg-lightness: 20%;
  }
}

Hoạt ảnh

Thông báo mới sẽ xuất hiện kèm theo một ảnh động khi thông báo đó xuất hiện trên màn hình. Để điều chỉnh chế độ giảm chuyển động, hãy đặt các giá trị translate thành 0 theo mặc định, nhưng hãy cập nhật giá trị chuyển động thành một độ dài trong truy vấn nội dung nghe nhìn về lựa chọn ưu tiên chuyển động . Mọi người đều thấy một số ảnh động, nhưng chỉ một số người dùng thấy thông báo dạng toast di chuyển một khoảng cách.

Dưới đây là các khung hình chính được dùng cho ảnh động thông báo tạm thời. CSS sẽ kiểm soát lối vào, thời gian chờ và lối thoát của thông báo, tất cả trong một ảnh động.

@keyframes fade-in {
  from { opacity: 0 }
}

@keyframes fade-out {
  to { opacity: 0 }
}

@keyframes slide-in {
  from { transform: translateY(var(--_travel-distance, 10px)) }
}

Sau đó, phần tử thông báo tạm thời sẽ thiết lập các biến và điều phối các khung hình chính.

.gui-toast {
  --_duration: 3s;
  --_travel-distance: 0;

  will-change: transform;
  animation: 
    fade-in .3s ease,
    slide-in .3s ease,
    fade-out .3s ease var(--_duration);
}

@media (prefers-reduced-motion: no-preference) {
  .gui-toast {
    --_travel-distance: 5vh;
  }
}

JavaScript

Khi các kiểu và HTML có thể truy cập bằng trình đọc màn hình đã sẵn sàng, bạn cần JavaScript để điều phối việc tạo, thêm và huỷ thông báo dựa trên các sự kiện của người dùng. Trải nghiệm của nhà phát triển đối với thành phần thông báo tạm thời phải tối thiểu và dễ bắt đầu, chẳng hạn như:

import Toast from './toast.js'

Toast('My first toast')

Tạo nhóm thông báo và thông báo

Khi tải mô-đun thông báo nhanh từ JavaScript, mô-đun này phải tạo một vùng chứa thông báo nhanh và thêm vùng chứa đó vào trang. Tôi chọn thêm phần tử trước body, điều này sẽ khiến các vấn đề về việc xếp chồng z-index khó xảy ra vì vùng chứa nằm phía trên vùng chứa cho tất cả các phần tử nội dung.

const init = () => {
  const node = document.createElement('section')
  node.classList.add('gui-toast-group')

  document.firstElementChild.insertBefore(node, document.body)
  return node
}

Ảnh chụp màn hình nhóm thông báo tạm thời giữa thẻ head và thẻ body.

Hàm init() được gọi nội bộ đến mô-đun, lưu trữ phần tử dưới dạng Toaster:

const Toaster = init()

Việc tạo phần tử HTML thông báo tạm thời được thực hiện bằng hàm createToast(). Hàm này yêu cầu một số văn bản cho thông báo tạm thời, tạo một phần tử <output>, trang trí phần tử đó bằng một số lớp và thuộc tính, đặt văn bản và trả về nút.

const createToast = text => {
  const node = document.createElement('output')
  
  node.innerText = text
  node.classList.add('gui-toast')
  node.setAttribute('role', 'status')

  return node
}

Quản lý một hoặc nhiều thông báo tạm thời

Giờ đây, JavaScript sẽ thêm một vùng chứa vào tài liệu để chứa thông báo và sẵn sàng thêm các thông báo đã tạo. Hàm addToast() điều phối việc xử lý một hoặc nhiều thông báo toast. Trước tiên, hãy kiểm tra số lượng thông báo và xem chuyển động có ổn không, sau đó sử dụng thông tin này để thêm thông báo hoặc thực hiện một số hiệu ứng động đẹp mắt để các thông báo khác xuất hiện nhằm "nhường chỗ" cho thông báo mới.

const addToast = toast => {
  const { matches:motionOK } = window.matchMedia(
    '(prefers-reduced-motion: no-preference)'
  )

  Toaster.children.length && motionOK
    ? flipToast(toast)
    : Toaster.appendChild(toast)
}

Khi thêm thông báo đầu tiên, Toaster.appendChild(toast) sẽ thêm một thông báo vào trang kích hoạt ảnh động CSS: ảnh động xuất hiện, chờ 3s, ảnh động biến mất. flipToast() được gọi khi có các thông báo tạm thời hiện có, sử dụng một kỹ thuật có tên là FLIP của Paul Lewis. Ý tưởng là tính toán sự khác biệt về vị trí của vùng chứa, trước và sau khi thông báo mới được thêm vào. Hãy coi như bạn đang đánh dấu vị trí hiện tại của Toaster, vị trí mà Toaster sẽ đến, sau đó tạo hiệu ứng chuyển động từ vị trí cũ sang vị trí mới.

const flipToast = toast => {
  // FIRST
  const first = Toaster.offsetHeight

  // add new child to change container size
  Toaster.appendChild(toast)

  // LAST
  const last = Toaster.offsetHeight

  // INVERT
  const invert = last - first

  // PLAY
  const animation = Toaster.animate([
    { transform: `translateY(${invert}px)` },
    { transform: 'translateY(0)' }
  ], {
    duration: 150,
    easing: 'ease-out',
  })
}

Lưới CSS sẽ thực hiện việc nâng bố cục. Khi một thông báo mới được thêm vào, lưới sẽ đặt thông báo đó ở đầu và tạo khoảng cách với các thông báo khác. Trong khi đó, một ảnh động trên web được dùng để tạo ảnh động cho vùng chứa từ vị trí cũ.

Kết hợp tất cả JavaScript

Khi Toast('my first toast') được gọi, một thông báo sẽ được tạo, thêm vào trang (thậm chí có thể vùng chứa được chuyển động để phù hợp với thông báo mới), một promise sẽ được trả về và thông báo đã tạo sẽ được theo dõi để hoàn tất ảnh động CSS (3 ảnh động khung hình chính) cho độ phân giải promise.

const Toast = text => {
  let toast = createToast(text)
  addToast(toast)

  return new Promise(async (resolve, reject) => {
    await Promise.allSettled(
      toast.getAnimations().map(animation => 
        animation.finished
      )
    )
    Toaster.removeChild(toast)
    resolve() 
  })
}

Tôi cảm thấy phần gây nhầm lẫn của mã này nằm trong hàm Promise.allSettled() và ánh xạ toast.getAnimations(). Vì tôi đã sử dụng nhiều ảnh động khung hình chính cho thông báo tạm thời, nên để chắc chắn biết tất cả ảnh động đã hoàn tất, mỗi ảnh động phải được yêu cầu từ JavaScript và mỗi ảnh động trong số đó phải được theo dõi finished để hoàn tất. allSettled có hiệu quả với chúng ta, tự giải quyết khi tất cả các lời hứa của nó đã được thực hiện. Việc sử dụng await Promise.allSettled() có nghĩa là dòng mã tiếp theo có thể tự tin xoá phần tử và giả định rằng thông báo tạm thời đã hoàn tất vòng đời của mình. Cuối cùng, việc gọi resolve() sẽ thực hiện lời hứa Toast ở cấp cao để nhà phát triển có thể dọn dẹp hoặc thực hiện công việc khác sau khi thông báo xuất hiện.

export default Toast

Cuối cùng, hàm Toast được xuất từ mô-đun để các tập lệnh khác nhập và sử dụng.

Sử dụng thành phần Toast

Việc sử dụng thông báo tạm thời hoặc trải nghiệm của nhà phát triển thông báo tạm thời được thực hiện bằng cách nhập hàm Toast và gọi hàm đó bằng một chuỗi thông báo.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

Nếu muốn dọn dẹp hoặc làm bất cứ việc gì khác sau khi thông báo xuất hiện, nhà phát triển có thể sử dụng async và await.

import Toast from './toast.js'

async function example() {
  await Toast('Wizard Rose added to cart')
  console.log('toast finished')
}

Kết luận

Giờ bạn đã biết cách tôi làm, vậy bạn sẽ làm như thế nào‽ 🙂

Hãy đa dạng hoá các phương pháp và tìm hiểu tất cả các cách để xây dựng trên web. Hãy tạo một bản minh hoạ, gửi đường liên kết cho tôi qua Twitter và tôi sẽ thêm bản minh hoạ đó vào phần bản phối lại của cộng đồng bên dưới!

Bản phối lại của cộng đồng