Xây dựng thành phần chuyển đổi

Thông tin tổng quan cơ bản về cách tạo một thành phần công tắc 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 các thành phần công tắc. 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

Công tắc hoạt động tương tự như hộp đánh dấu nhưng thể hiện rõ ràng trạng thái bật và tắt của boolean.

Bản minh hoạ này sử dụng <input type="checkbox" role="switch"> cho phần lớn chức năng của mình. Điều này có ưu điểm là không cần CSS hoặc JavaScript để hoạt động đầy đủ và dễ truy cập. CSS tải mang đến sự hỗ trợ cho các ngôn ngữ viết từ phải sang trái, tính dọc, ảnh động và nhiều tính năng khác. Việc tải JavaScript giúp công tắc có thể kéo và hữu hình.

Thuộc tính tuỳ chỉnh

Các biến sau đây biểu thị nhiều phần của công tắc và các lựa chọn của chúng. Là lớp cấp cao nhất, .gui-switch chứa các thuộc tính tuỳ chỉnh được dùng trong các thành phần con và điểm truy cập để tuỳ chỉnh tập trung.

Theo dõi

Chiều dài (--track-size), khoảng đệm và 2 màu:

.gui-switch {
  --track-size: calc(var(--thumb-size) * 2);
  --track-padding: 2px;

  --track-inactive: hsl(80 0% 80%);
  --track-active: hsl(80 60% 45%);

  --track-color-inactive: var(--track-inactive);
  --track-color-active: var(--track-active);

  @media (prefers-color-scheme: dark) {
    --track-inactive: hsl(80 0% 35%);
    --track-active: hsl(80 60% 60%);
  }
}

Thumb

Kích thước, màu nền và màu đánh dấu tương tác:

.gui-switch {
  --thumb-size: 2rem;
  --thumb: hsl(0 0% 100%);
  --thumb-highlight: hsl(0 0% 0% / 25%);

  --thumb-color: var(--thumb);
  --thumb-color-highlight: var(--thumb-highlight);

  @media (prefers-color-scheme: dark) {
    --thumb: hsl(0 0% 5%);
    --thumb-highlight: hsl(0 0% 100% / 25%);
  }
}

Giảm chuyển động

Để thêm một bí danh rõ ràng và giảm sự lặp lại, bạn có thể đặt truy vấn nội dung nghe nhìn ưu tiên giảm chuyển động của người dùng vào một thuộc tính tuỳ chỉnh bằng trình bổ trợ PostCSS dựa trên bản nháp đặc tả này trong Truy vấn nội dung nghe nhìn 5:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

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

Tôi chọn bao bọc phần tử <input type="checkbox" role="switch"> bằng <label>, liên kết mối quan hệ của chúng để tránh sự mơ hồ về mối liên kết giữa hộp đánh dấu và nhãn, đồng thời cho phép người dùng tương tác với nhãn để bật/tắt dữ liệu đầu vào.

Một nhãn và hộp đánh dấu tự nhiên, không có kiểu.

<label for="switch" class="gui-switch">
  Label text
  <input type="checkbox" role="switch" id="switch">
</label>

<input type="checkbox"> được tạo sẵn bằng APItrạng thái. Trình duyệt quản lý thuộc tính checkedcác sự kiện đầu vào như oninputonchanged.

Bố cục

Flexbox, lướithuộc tính tuỳ chỉnh là những yếu tố quan trọng trong việc duy trì kiểu của thành phần này. Chúng tập trung các giá trị, đặt tên cho các phép tính hoặc khu vực không rõ ràng và cho phép một API thuộc tính tuỳ chỉnh nhỏ để dễ dàng tuỳ chỉnh thành phần.

.gui-switch

Bố cục cấp cao nhất cho công tắc là flexbox. Lớp .gui-switch chứa các thuộc tính tuỳ chỉnh riêng tư và công khai mà các thành phần con dùng để tính toán bố cục của chúng.

Flexbox DevTools phủ lên nhãn và công tắc ngang, cho thấy bố cục phân phối không gian của chúng.

.gui-switch {
  display: flex;
  align-items: center;
  gap: 2ch;
  justify-content: space-between;
}

Việc mở rộng và sửa đổi bố cục hộp linh hoạt cũng giống như việc thay đổi bất kỳ bố cục hộp linh hoạt nào. Ví dụ: để đặt nhãn ở phía trên hoặc phía dưới một công tắc, hoặc để thay đổi flex-direction:

Flexbox DevTools phủ lên nhãn dọc và nút chuyển.

<label for="light-switch" class="gui-switch" style="flex-direction: column">
  Default
  <input type="checkbox" role="switch" id="light-switch">
</label>

Theo dõi

Đầu vào hộp đánh dấu được tạo kiểu dưới dạng một đường chuyển đổi bằng cách xoá appearance: checkbox thông thường và cung cấp kích thước riêng:

Lớp phủ Lưới DevTools phủ lên công tắc theo dõi, cho thấy các khu vực theo dõi lưới được đặt tên bằng tên &quot;track&quot;.

.gui-switch > input {
  appearance: none;

  inline-size: var(--track-size);
  block-size: var(--thumb-size);
  padding: var(--track-padding);

  flex-shrink: 0;
  display: grid;
  align-items: center;
  grid: [track] 1fr / [track] 1fr;
}

Đường dẫn này cũng tạo ra một vùng đường dẫn lưới một ô duy nhất để một ngón tay cái có thể yêu cầu.

Thumb

Kiểu appearance: none cũng xoá dấu đánh dấu trực quan do trình duyệt cung cấp. Thành phần này sử dụng một phần tử giả:checked lớp giả trên dữ liệu đầu vào để thay thế chỉ báo trực quan này.

Ngón tay cái là một phần tử con giả được đính kèm vào input[type="checkbox"] và xếp chồng lên trên đường theo dõi thay vì bên dưới bằng cách xác nhận vùng lưới track:

DevTools cho thấy hình thu nhỏ của phần tử giả được đặt bên trong một lưới CSS.

.gui-switch > input::before {
  content: "";
  grid-area: track;
  inline-size: var(--thumb-size);
  block-size: var(--thumb-size);
}

Kiểu

Các thuộc tính tuỳ chỉnh cho phép một thành phần công tắc linh hoạt thích ứng với các bảng phối màu, ngôn ngữ từ phải sang trái và lựa chọn ưu tiên về chuyển động.

So sánh giao diện sáng và tối của nút bật/tắt và các trạng thái của nút này.

Kiểu tương tác bằng thao tác chạm

Trên thiết bị di động, các trình duyệt sẽ thêm tính năng đánh dấu khi nhấn và chọn văn bản vào nhãn và dữ liệu đầu vào. Những yếu tố này ảnh hưởng tiêu cực đến kiểu và phản hồi tương tác trực quan mà nút chuyển này cần. Với một vài dòng CSS, tôi có thể xoá những hiệu ứng đó và thêm kiểu cursor: pointer của riêng mình:

.gui-switch {
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

Bạn không nên xoá các kiểu đó vì chúng có thể là thông tin phản hồi hữu ích về tương tác trực quan. Hãy nhớ cung cấp các lựa chọn thay thế tuỳ chỉnh nếu bạn xoá các lựa chọn này.

Theo dõi

Kiểu của phần tử này chủ yếu là về hình dạng và màu sắc, mà phần tử này truy cập từ .gui-switch mẹ thông qua cascade (thác).

Các biến thể của công tắc có kích thước và màu sắc tuỳ chỉnh.

.gui-switch > input {
  appearance: none;
  border: none;
  outline-offset: 5px;
  box-sizing: content-box;

  padding: var(--track-padding);
  background: var(--track-color-inactive);
  inline-size: var(--track-size);
  block-size: var(--thumb-size);
  border-radius: var(--track-size);
}

Có nhiều lựa chọn tuỳ chỉnh cho đường chuyển đổi đến từ 4 thuộc tính tuỳ chỉnh. border: none được thêm vào vì appearance: none không xoá đường viền khỏi hộp đánh dấu trên tất cả các trình duyệt.

Thumb

Phần tử ngón tay cái đã nằm ở bên phải track nhưng cần có kiểu hình tròn:

.gui-switch > input::before {
  background: var(--thumb-color);
  border-radius: 50%;
}

Công cụ cho nhà phát triển hiển thị phần đánh dấu phần tử giả hình tròn.

Tương tác

Sử dụng các thuộc tính tuỳ chỉnh để chuẩn bị cho các lượt tương tác sẽ cho thấy điểm nổi bật khi di chuột và các thay đổi về vị trí ngón tay cái. Lựa chọn ưu tiên của người dùng cũng được kiểm tra trước khi chuyển đổi kiểu chuyển động hoặc kiểu làm nổi bật khi di chuột.

.gui-switch > input::before {
  box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);

  @media (--motionOK) { & {
    transition:
      transform var(--thumb-transition-duration) ease,
      box-shadow .25s ease;
  }}
}

Vị trí ngón tay cái

Các thuộc tính tuỳ chỉnh cung cấp một cơ chế nguồn duy nhất để đặt ngón tay cái vào vị trí trong bản nhạc. Chúng ta có kích thước của thanh trượt và hình thu nhỏ. Chúng ta sẽ dùng các kích thước này trong các phép tính để giữ cho hình thu nhỏ được bù đắp đúng cách và nằm trong thanh trượt: 0%100%.

Phần tử input sở hữu biến vị trí --thumb-position và phần tử giả thumb sử dụng biến này làm vị trí translateX:

.gui-switch > input {
  --thumb-position: 0%;
}

.gui-switch > input::before {
  transform: translateX(var(--thumb-position));
}

Giờ đây, chúng ta có thể thay đổi --thumb-position từ CSS và các lớp giả được cung cấp trên các phần tử hộp đánh dấu. Vì chúng ta đã đặt transition: transform var(--thumb-transition-duration) ease có điều kiện cho phần tử này trước đó, nên những thay đổi này có thể tạo ảnh động khi thay đổi:

/* positioned at the end of the track: track length - 100% (thumb width) */
.gui-switch > input:checked {
  --thumb-position: calc(var(--track-size) - 100%);
}

/* positioned in the center of the track: half the track - half the thumb */
.gui-switch > input:indeterminate {
  --thumb-position: calc(
    (var(--track-size) / 2) - (var(--thumb-size) / 2)
  );
}

Tôi nghĩ việc điều phối tách rời này đã hoạt động hiệu quả. Phần tử ngón tay cái chỉ liên quan đến một kiểu, đó là vị trí translateX. Đầu vào có thể quản lý tất cả độ phức tạp và các phép tính.

Dọc

Việc hỗ trợ được thực hiện bằng một lớp đối tượng sửa đổi -vertical. Lớp này sẽ thêm một vòng xoay bằng các phép biến đổi CSS vào phần tử input.

Tuy nhiên, một phần tử xoay 3D không làm thay đổi chiều cao tổng thể của thành phần, điều này có thể làm sai lệch bố cục khối. Hãy tính đến điều này bằng cách sử dụng các biến --track-size--track-padding. Tính toán lượng không gian tối thiểu cần thiết để một nút dọc có thể chuyển động trong bố cục như mong đợi:

.gui-switch.-vertical {
  min-block-size: calc(var(--track-size) + calc(var(--track-padding) * 2));

  & > input {
    transform: rotate(-90deg);
  }
}

(RTL) từ phải sang trái

Một người bạn CSS, Elad Schecter và tôi đã cùng nhau tạo mẫu trình đơn bên trượt ra bằng cách sử dụng các biến đổi CSS xử lý các ngôn ngữ từ phải sang trái bằng cách lật một biến duy nhất. Chúng tôi đã làm như vậy vì không có các biến đổi thuộc tính logic trong CSS và có thể sẽ không bao giờ có. Elad đã có ý tưởng tuyệt vời là sử dụng một giá trị thuộc tính tuỳ chỉnh để đảo ngược tỷ lệ phần trăm, nhằm cho phép quản lý một vị trí duy nhất của logic tuỳ chỉnh của chúng tôi cho các phép biến đổi logic. Tôi đã sử dụng chính kỹ thuật này trong công tắc này và tôi nghĩ nó hoạt động rất tốt:

.gui-switch {
  --isLTR: 1;

  &:dir(rtl) {
    --isLTR: -1;
  }
}

Ban đầu, một thuộc tính tuỳ chỉnh có tên --isLTR sẽ giữ giá trị 1, tức là true vì bố cục của chúng ta theo mặc định là từ trái sang phải. Sau đó, bằng cách sử dụng lớp giả CSS :dir(), giá trị được đặt thành -1 khi thành phần nằm trong bố cục từ phải sang trái.

Đưa --isLTR vào hoạt động bằng cách sử dụng nó trong calc() bên trong một phép biến đổi:

.gui-switch.-vertical > input {
  transform: rotate(-90deg);
  transform: rotate(calc(90deg * var(--isLTR) * -1));
}

Giờ đây, việc xoay công tắc dọc sẽ tính đến vị trí bên đối diện mà bố cục từ phải sang trái yêu cầu.

Bạn cũng cần cập nhật các phép biến đổi translateX trên phần tử giả ngón tay cái để tính đến yêu cầu về phía đối diện:

.gui-switch > input:checked {
  --thumb-position: calc(var(--track-size) - 100%);
  --thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}

.gui-switch > input:indeterminate {
  --thumb-position: calc(
    (var(--track-size) / 2) - (var(--thumb-size) / 2)
  );
  --thumb-position: calc(
   ((var(--track-size) / 2) - (var(--thumb-size) / 2))
    * var(--isLTR)
  );
}

Mặc dù phương pháp này không giải quyết được mọi nhu cầu liên quan đến một khái niệm như biến đổi CSS logic, nhưng nó cung cấp một số nguyên tắc DRY cho nhiều trường hợp sử dụng.

Tiểu bang

Việc sử dụng input[type="checkbox"] tích hợp sẽ không hoàn chỉnh nếu không xử lý các trạng thái mà thành phần này có thể ở trong đó: :checked, :disabled, :indeterminate:hover. :focus được cố ý giữ nguyên, chỉ điều chỉnh độ bù trừ; vòng tiêu điểm trông rất đẹp trên Firefox và Safari:

Ảnh chụp màn hình vòng tiêu điểm đang tập trung vào một công tắc trong Firefox và Safari.

Đã chọn

<label for="switch-checked" class="gui-switch">
  Default
  <input type="checkbox" role="switch" id="switch-checked" checked="true">
</label>

Trạng thái này biểu thị trạng thái on. Ở trạng thái này, nền "track" đầu vào được đặt thành màu đang hoạt động và vị trí ngón tay cái được đặt thành "cuối".

.gui-switch > input:checked {
  background: var(--track-color-active);
  --thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}

Đã tắt

<label for="switch-disabled" class="gui-switch">
  Default
  <input type="checkbox" role="switch" id="switch-disabled" disabled="true">
</label>

Nút :disabled không chỉ có hình thức khác biệt mà còn phải làm cho phần tử không thay đổi.Khả năng tương tác không thay đổi không phụ thuộc vào trình duyệt, nhưng các trạng thái trực quan cần có kiểu do việc sử dụng appearance: none.

.gui-switch > input:disabled {
  cursor: not-allowed;
  --thumb-color: transparent;

  &::before {
    cursor: not-allowed;
    box-shadow: inset 0 0 0 2px hsl(0 0% 100% / 50%);

    @media (prefers-color-scheme: dark) { & {
      box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 50%);
    }}
  }
}

Công tắc có kiểu tối ở trạng thái tắt, đã đánh dấu và chưa đánh dấu.

Trạng thái này khá phức tạp vì cần có giao diện tối và giao diện sáng với cả trạng thái đã tắt và đã đánh dấu. Tôi đã chọn phong cách tối giản cho các trạng thái này để giảm bớt gánh nặng duy trì các tổ hợp phong cách.

Không xác định

Một trạng thái thường bị bỏ quên là :indeterminate, trong đó hộp đánh dấu không được đánh dấu hoặc bỏ đánh dấu. Đây là một trạng thái vui vẻ, hấp dẫn và khiêm tốn. Một lời nhắc hữu ích rằng các trạng thái boolean có thể có các trạng thái trung gian khó nhận biết.

Bạn khó có thể đặt hộp đánh dấu ở trạng thái không xác định, chỉ JavaScript mới có thể đặt hộp đánh dấu ở trạng thái này:

<label for="switch-indeterminate" class="gui-switch">
  Indeterminate
  <input type="checkbox" role="switch" id="switch-indeterminate">
  <script>document.getElementById('switch-indeterminate').indeterminate = true</script>
</label>

Trạng thái không xác định có ngón tay cái ở giữa, cho biết trạng thái chưa quyết định.

Vì trạng thái này đối với tôi là khiêm tốn và hấp dẫn, nên tôi cảm thấy phù hợp khi đặt vị trí ngón tay cái trên nút bật/tắt ở giữa:

.gui-switch > input:indeterminate {
  --thumb-position: calc(
    calc(calc(var(--track-size) / 2) - calc(var(--thumb-size) / 2))
    * var(--isLTR)
  );
}

Khoảng cách di

Tương tác di chuột phải hỗ trợ trực quan cho giao diện người dùng được kết nối, đồng thời cung cấp hướng dẫn cho giao diện người dùng tương tác. Công tắc này làm nổi bật ngón tay cái bằng một vòng bán trong suốt khi nhãn hoặc đầu vào được di chuột qua. Sau đó, hiệu ứng di chuột này sẽ cung cấp hướng dẫn về phần tử hình thu nhỏ có thể tương tác.

Hiệu ứng "làm nổi bật" được thực hiện bằng box-shadow. Khi di chuột lên một thành phần đầu vào không bị vô hiệu hoá, hãy tăng kích thước của --highlight-size. Nếu người dùng không gặp vấn đề với chuyển động, chúng ta sẽ chuyển đổi box-shadow và thấy nó phát triển. Nếu người dùng gặp vấn đề với chuyển động, phần nổi bật sẽ xuất hiện ngay lập tức:

.gui-switch > input::before {
  box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);

  @media (--motionOK) { & {
    transition:
      transform var(--thumb-transition-duration) ease,
      box-shadow .25s ease;
  }}
}

.gui-switch > input:not(:disabled):hover::before {
  --highlight-size: .5rem;
}

JavaScript

Đối với tôi, giao diện của công tắc có thể tạo cảm giác kỳ lạ khi cố gắng mô phỏng một giao diện thực, đặc biệt là loại giao diện có một vòng tròn bên trong một đường kẻ. iOS đã làm đúng điều này với công tắc của họ, bạn có thể kéo công tắc từ bên này sang bên kia và cảm thấy rất hài lòng khi có lựa chọn này. Ngược lại, một thành phần trên giao diện người dùng có thể trông không hoạt động nếu người dùng cố gắng thực hiện cử chỉ kéo nhưng không có gì xảy ra.

Hình thu nhỏ có thể kéo

Phần tử giả hình thu nhỏ nhận vị trí từ .gui-switch > input var(--thumb-position) có phạm vi, JavaScript có thể cung cấp giá trị kiểu nội tuyến trên đầu vào để cập nhật động vị trí hình thu nhỏ, khiến hình thu nhỏ xuất hiện theo cử chỉ con trỏ. Khi con trỏ được nhả ra, hãy xoá các kiểu nội tuyến và xác định xem thao tác kéo gần với trạng thái tắt hay bật bằng cách sử dụng thuộc tính tuỳ chỉnh --thumb-position. Đây là nền tảng của giải pháp; các sự kiện con trỏ theo dõi có điều kiện vị trí con trỏ để sửa đổi các thuộc tính tuỳ chỉnh CSS.

Vì thành phần này đã hoạt động 100% trước khi tập lệnh này xuất hiện, nên bạn cần phải bỏ ra khá nhiều công sức để duy trì hành vi hiện có, chẳng hạn như nhấp vào nhãn để bật/tắt dữ liệu đầu vào. JavaScript của chúng tôi không được thêm các tính năng làm ảnh hưởng đến các tính năng hiện có.

touch-action

Kéo là một cử chỉ tuỳ chỉnh, nên đây là lựa chọn lý tưởng cho các lợi ích của touch-action. Trong trường hợp này, một cử chỉ ngang sẽ được tập lệnh của chúng tôi xử lý hoặc một cử chỉ dọc sẽ được ghi lại cho biến thể công tắc dọc. Với touch-action, chúng ta có thể cho trình duyệt biết những cử chỉ cần xử lý trên phần tử này, nhờ đó, một tập lệnh có thể xử lý một cử chỉ mà không bị xung đột.

CSS sau đây hướng dẫn trình duyệt rằng khi một cử chỉ bằng con trỏ bắt đầu từ trong đường chuyển đổi này, hãy xử lý các cử chỉ dọc, không làm gì với các cử chỉ ngang:

.gui-switch > input {
  touch-action: pan-y;
}

Kết quả mong muốn là một cử chỉ ngang mà không đồng thời di chuyển hoặc cuộn trang. Con trỏ có thể cuộn theo chiều dọc bắt đầu từ bên trong dữ liệu đầu vào và cuộn trang, nhưng các con trỏ ngang được xử lý tuỳ chỉnh.

Tiện ích kiểu giá trị pixel

Trong quá trình thiết lập và kéo, bạn sẽ cần lấy nhiều giá trị số được tính toán từ các phần tử. Các hàm JavaScript sau đây trả về các giá trị pixel được tính toán cho một thuộc tính CSS. Nó được dùng trong tập lệnh thiết lập như thế này getStyle(checkbox, 'padding-left').

​​const getStyle = (element, prop) => {
  return parseInt(window.getComputedStyle(element).getPropertyValue(prop));
}

const getPseudoStyle = (element, prop) => {
  return parseInt(window.getComputedStyle(element, ':before').getPropertyValue(prop));
}

export {
  getStyle,
  getPseudoStyle,
}

Lưu ý cách window.getComputedStyle() chấp nhận đối số thứ hai, một phần tử giả mục tiêu. JavaScript có thể đọc rất nhiều giá trị từ các phần tử, ngay cả từ các phần tử giả.

dragging

Đây là thời điểm cốt lõi cho logic kéo và có một vài điều cần lưu ý từ trình xử lý sự kiện hàm:

const dragging = event => {
  if (!state.activethumb) return

  let {thumbsize, bounds, padding} = switches.get(state.activethumb.parentElement)
  let directionality = getStyle(state.activethumb, '--isLTR')

  let track = (directionality === -1)
    ? (state.activethumb.clientWidth * -1) + thumbsize + padding
    : 0

  let pos = Math.round(event.offsetX - thumbsize / 2)

  if (pos < bounds.lower) pos = 0
  if (pos > bounds.upper) pos = bounds.upper

  state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
}

Thành phần chính của tập lệnh là state.activethumb, vòng tròn nhỏ mà tập lệnh này đang định vị cùng với một con trỏ. Đối tượng switches là một Map(), trong đó các khoá là .gui-switch và các giá trị là kích thước và ranh giới được lưu vào bộ nhớ đệm giúp tập lệnh hoạt động hiệu quả. Hướng từ phải sang trái được xử lý bằng cách sử dụng cùng một thuộc tính tuỳ chỉnh mà CSS --isLTR, đồng thời có thể sử dụng thuộc tính này để đảo ngược logic và tiếp tục hỗ trợ hướng từ phải sang trái. event.offsetX cũng có giá trị vì chứa giá trị delta hữu ích cho việc định vị ngón tay cái.

state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)

Dòng CSS cuối cùng này đặt thuộc tính tuỳ chỉnh mà phần tử hình thu nhỏ sử dụng. Nếu không, việc chỉ định giá này sẽ chuyển đổi theo thời gian, nhưng một sự kiện con trỏ trước đó đã tạm thời đặt --thumb-transition-duration thành 0s, loại bỏ những gì lẽ ra là một hoạt động tương tác chậm chạp.

dragEnd

Để người dùng được phép kéo ra ngoài công tắc và thả ra, cần đăng ký một sự kiện cửa sổ chung:

window.addEventListener('pointerup', event => {
  if (!state.activethumb) return

  dragEnd(event)
})

Tôi nghĩ điều rất quan trọng là người dùng có quyền tự do kéo một cách thoải mái và giao diện đủ thông minh để tính đến điều đó. Không mất nhiều thời gian để xử lý bằng nút chuyển này, nhưng bạn cần cân nhắc kỹ lưỡng trong quá trình phát triển.

const dragEnd = event => {
  if (!state.activethumb) return

  state.activethumb.checked = determineChecked()

  if (state.activethumb.indeterminate)
    state.activethumb.indeterminate = false

  state.activethumb.style.removeProperty('--thumb-transition-duration')
  state.activethumb.style.removeProperty('--thumb-position')
  state.activethumb.removeEventListener('pointermove', dragging)
  state.activethumb = null

  padRelease()
}

Tương tác với phần tử đã hoàn tất, đã đến lúc đặt thuộc tính đầu vào đã kiểm tra và xoá tất cả các sự kiện cử chỉ. Hộp đánh dấu được thay đổi bằng state.activethumb.checked = determineChecked().

determineChecked()

Hàm này (do dragEnd gọi) xác định vị trí hiện tại của ngón tay cái trong phạm vi của đường theo dõi và trả về giá trị true nếu vị trí đó bằng hoặc lớn hơn một nửa đường theo dõi:

const determineChecked = () => {
  let {bounds} = switches.get(state.activethumb.parentElement)

  let curpos =
    Math.abs(
      parseInt(
        state.activethumb.style.getPropertyValue('--thumb-position')))

  if (!curpos) {
    curpos = state.activethumb.checked
      ? bounds.lower
      : bounds.upper
  }

  return curpos >= bounds.middle
}

Suy nghĩ thêm

Cử chỉ kéo gây ra một chút nợ mã do cấu trúc HTML ban đầu được chọn, đáng chú ý nhất là việc gói đầu vào trong một nhãn. Nhãn này là một phần tử mẹ, sẽ nhận được các lượt tương tác nhấp sau khi người dùng nhập. Ở cuối sự kiện dragEnd, có thể bạn đã nhận thấy padRelease() là một hàm có vẻ lạ.

const padRelease = () => {
  state.recentlyDragged = true

  setTimeout(_ => {
    state.recentlyDragged = false
  }, 300)
}

Điều này là để tính đến nhãn nhận được lượt nhấp này sau đó, vì nhãn sẽ bỏ chọn hoặc chọn hoạt động tương tác mà người dùng đã thực hiện.

Nếu làm lại việc này, tôi có thể cân nhắc điều chỉnh DOM bằng JavaScript trong quá trình nâng cấp trải nghiệm người dùng, để tạo một phần tử tự xử lý các lượt nhấp vào nhãn và không xung đột với hành vi tích hợp.

Đây là loại JavaScript mà tôi ít thích viết nhất, tôi không muốn quản lý tính năng truyền sự kiện có điều kiện:

const preventBubbles = event => {
  if (state.recentlyDragged)
    event.preventDefault() && event.stopPropagation()
}

Kết luận

Thành phần chuyển đổi nhỏ xíu này hoá ra lại là thành phần tốn nhiều công sức nhất trong tất cả các Thử thách về giao diện người dùng cho đến nay! 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

Tài nguyên

Tìm .gui-switch mã nguồn trên GitHub.