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 thành phần nút chuyển đổi 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 thành phần nút chuyển. Dùng thử bản minh hoạ.

Bản minh hoạ

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

Tổng quan

Nút chuyển hoạt động tương tự như hộp đánh dấu nhưng thể hiện rõ trạng thái bật và tắt 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 nó. Ưu điểm của việc này là không cần CSS hoặc JavaScript để có thể hoạt động và truy cập đầy đủ. Tính năng tải CSS hỗ trợ các ngôn ngữ từ phải sang trái, chiều dọc, ảnh động và nhiều tính năng khác. Việc tải JavaScript giúp nút chuyển có thể kéo và hữu hình.

Thuộc tính tuỳ chỉnh

Các biến sau đây đại diện cho nhiều phần của nút chuyển và các tuỳ chọn của các phần đó. Là lớp cấp cao nhất, .gui-switch chứa các thuộc tính tuỳ chỉnh được sử dụng trong tất cả các thành phần con và các điểm truy cập để tuỳ chỉnh tập trung.

Theo dõi

Chiều dài (--track-size), khoảng đệm và hai 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ể đưa truy vấn nội dung nghe nhìn ưu tiên chuyển động giảm 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 thông số kỹ thuật nháp 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 gói phần tử <input type="checkbox" role="switch"> bằng <label>, gói mối quan hệ của chúng để tránh sự mơ hồ liên kết 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 theo 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 với một 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 đóng vai trò quan trọng trong việc duy trì kiểu của thành phần này. Các thành phần này tập trung các giá trị, đặt tên cho các phép tính hoặc vùng không rõ ràng và bật 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 nút chuyển là flexbox. Lớp .gui-switch chứa các thuộc tính tuỳ chỉnh công khai và riêng tư mà các thành phần con cháu sử dụng để tính toán bố cục của chúng.

Flexbox DevTools phủ lên một nhãn và nút chuyển 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 flexbox cũng giống như thay đổi bất kỳ bố cục flexbox nào. Ví dụ: để đặt nhãn ở phía trên hoặc phía dưới nút chuyển hoặc để thay đổi flex-direction:

Công cụ cho nhà phát triển Flexbox phủ lên một nhãn và nút chuyển dọc.

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

Theo dõi

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

Grid DevTools phủ lên kênh chuyển đổi, hiển thị các khu vực kênh lưới được đặt tên có tên là &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;
}

Video nhạc cũng tạo một vùng theo dõi lưới ô đơn lẻ để một video thu nhỏ có thể xác nhận quyền sở hữu.

Thumb

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

Thumb là một phần tử con giả mạo được đính kèm vào input[type="checkbox"] và xếp chồng lên đầu kênh thay vì bên dưới bằng cách xác nhận quyền sở hữu vùng lưới track:

DevTools hiển thị ngón tay giả định được đặt bên trong 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 thành phần nút chuyển linh hoạt thích ứng với các giao diện 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 song song giao diện sáng và tối cho nút chuyển và các trạng thái của nút chuyển.

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

Trên thiết bị di động, trình duyệt sẽ thêm các tính năng nhấn để làm nổi bật và chọn văn bản vào nhãn và đầu vào. Những điều này đã ảnh hưởng tiêu cực đến phản hồi về kiểu và tương tác hình ảnh mà nút chuyển này cần. Với một vài dòng CSS, tôi có thể xoá các 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;
}

Không phải lúc nào bạn cũng nên xoá các kiểu đó, vì đó có thể là phản hồi tương tác trực quan có giá trị. 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 đó.

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 loạt.

Các biến thể của nút chuyển có kích thước và màu sắc của kênh 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 tuỳ chọn tuỳ chỉnh cho kênh chuyển đổi 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ả trình duyệt.

Thumb

Phần tử con trỏ đã nằm ở track bên phải nhưng cần có kiểu vòng 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ị, làm nổi bật phần tử giả lập 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ẽ hiển thị các điểm nổi bật khi di chuột và thay đổi 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

Thuộc tính tuỳ chỉnh cung cấp một cơ chế nguồn duy nhất để định vị con trỏ trong bản nhạc. Chúng ta có thể sử dụng kích thước của bản nhạc và ngón tay cái trong các phép tính để giữ cho ngón tay cái được bù chính xác và nằm trong bản nhạc: 0%100%.

Phần tử input sở hữu biến vị trí --thumb-position và phần tử giả ngón tay cái 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 trước đó trên phần tử này, 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ĩ cách điều phối tách biệt này đã hoạt động tốt. Phần tử con trỏ chỉ liên quan đến một kiểu, vị trí translateX. Dữ liệu đầu vào có thể quản lý tất cả độ phức tạp và phép tính.

Dọc

Hoạt động 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 với các phép biến đổi CSS vào phần tử input.

Tuy nhiên, phần tử xoay 3D không thay đổi chiều cao tổng thể của thành phần, điều này có thể làm hỏng 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 không gian tối thiểu cần thiết để một nút dọc có thể hiển thị 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

Tôi và một người bạn CSS, Elad Schecter, đã cùng nhau tạo bản nguyên mẫu cho một trình đơn bên trượt ra bằng cách sử dụng các phép 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ó phép biến đổi thuộc tính logic nào trong CSS và có thể sẽ không bao giờ có. Elad đã có ý tưởng tuyệt vời về việc sử dụng giá trị thuộc tính tuỳ chỉnh để đảo ngược tỷ lệ phần trăm, cho phép quản lý một vị trí duy nhất của logic tuỳ chỉnh của chúng ta cho các phép biến đổi logic. Tôi đã sử dụng chính kỹ thuật này trong nút chuyển này và tôi nghĩ nó đã hoạt động rất hiệu quả:

.gui-switch {
  --isLTR: 1;

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

Một thuộc tính tuỳ chỉnh có tên là --isLTR ban đầu giữ giá trị 1, nghĩa là thuộc tính này là true vì bố cục của chúng ta là từ trái sang phải theo mặc định. Sau đó, 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.

Áp dụng --isLTR bằng cách sử dụng 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 nút chuyển 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 để 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 tất cả nhu cầu liên quan đến một khái niệm như chuyển đổi CSS logic, nhưng phương pháp này 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 nhiều trạng thái mà input[type="checkbox"] có thể ở: :checked, :disabled, :indeterminate:hover. :focus được cố ý để nguyên, chỉ điều chỉnh độ lệch; vòng tròn tiêu điểm trông rất đẹp trên Firefox và Safari:

Ảnh chụp màn hình vòng tròn tiêu điểm tập trung vào một nút chuyển 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 "đường dẫn" đầ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ỉ khác về mặt hình ảnh mà còn phải làm cho phần tử không thể thay đổi.Tính chất không thể thay đổi của hoạt động tương tác không bị ảnh hưởng bởi trình duyệt, nhưng các trạng thái hình ảnh cần có kiểu do 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%);
    }}
  }
}

Nút chuyển 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 sáng và tối với cả trạng thái tắt và trạng thái đã đánh dấu. Tôi đã chọn các kiểu tối giản cho các trạng thái này để giảm bớt gánh nặng bảo trì của các kiểu kết hợp.

Không xác định

Một trạng thái thường 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 thú vị, hấp dẫn và không phô trương. Xin nhắc lại rằng các trạng thái boolean có thể có trạng thái ẩn giữa các trạng thái.

Rất khó để đặt hộp đánh dấu thành không xác định, chỉ JavaScript mới có thể đặt hộp đánh dấu 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 theo dõi ở giữa để cho biết chưa quyết định.

Vì trạng thái này đối với tôi là không phô trương và hấp dẫn, nên tôi thấy phù hợp khi đặt vị trí ngón tay cái của nút chuyển ở 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

Thao tác di chuột phải hỗ trợ hình ảnh cho giao diện người dùng được kết nối và cũng phải hướng đến 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 tròn bán trong suốt khi bạn di chuột qua nhãn hoặc dữ liệu đầu vào. Sau đó, ảnh động khi di chuột này sẽ cung cấp hướng dẫn đến phần tử ngón tay cái 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 qua một thành phần nhập 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 đồng ý với chuyển động, chúng ta sẽ chuyển đổi box-shadow và xem nó phát triển, nếu họ không đồng ý với chuyển động, thì điểm 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 nút chuyển có thể gây cảm giác kỳ lạ khi cố gắng mô phỏng giao diện thực tế, đặc biệt là loại giao diện này có một vòng tròn bên trong một kênh. iOS đã làm đúng điều này với nút chuyển, bạn có thể kéo chúng sang hai bên và 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ể không hoạt động nếu bạn cố gắng thực hiện một cử chỉ kéo nhưng không có gì xảy ra.

Nút Thích có thể kéo

Phần tử giả ngón tay nhận vị trí của nó từ .gui-switch > input trong phạm vi var(--thumb-position), JavaScript có thể cung cấp giá trị kiểu nội tuyến trên đầu vào để cập nhật linh động vị trí ngón tay cái, khiến nó có vẻ như tuân theo cử chỉ con trỏ. Khi bạn nhả con trỏ, hãy xoá các kiểu nội tuyến và xác định xem thao tác kéo gần hơ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à phần cốt lõi của giải pháp; các sự kiện con trỏ theo dõi vị trí con trỏ theo điều kiện để 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 làm khá nhiều việc để duy trì hành vi hiện có, chẳng hạn như nhấp vào một nhãn để bật/tắt dữ liệu đầu vào. JavaScript của chúng ta không được thêm các tính năng mà làm giảm chất lượng của các tính năng hiện có.

touch-action

Kéo là một cử chỉ, một cử chỉ tuỳ chỉnh, giúp cử chỉ này trở thành một ứng cử viên tuyệt vời cho các lợi ích của touch-action. Trong trường hợp của nút chuyển này, tập lệnh của chúng ta sẽ xử lý cử chỉ ngang hoặc cử chỉ dọc được ghi lại cho biến thể nút chuyển 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ờ đó, tập lệnh có thể xử lý một cử chỉ mà không có sự cạnh tranh.

CSS sau đây hướng dẫn trình duyệt rằng khi một cử chỉ con trỏ bắt đầu từ trong kênh 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 không kéo 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ỏ theo chiều 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 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ề giá trị pixel được tính toán dựa trên một thuộc tính CSS. Phương thức này được dùng trong tập lệnh thiết lập như getStyle(checkbox, 'padding-left') sau.

​​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,
}

Hãy lưu ý cách window.getComputedStyle() chấp nhận đối số thứ hai, một phần tử giả mạo 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`)
}

Nhân vật 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 con trỏ. Đối tượng switches là một Map(), trong đó các khoá là .gui-switch và các giá trị là giới hạn và kích thước đượ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 là --isLTR và có thể sử dụng thuộc tính này để đảo ngược logic và tiếp tục hỗ trợ RTL. event.offsetX cũng có giá trị vì chứa giá trị delta rất 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ử thumb sử dụng. Việc gán giá trị 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, xoá đi tương tác chậm chạp.

dragEnd

Để cho phép người dùng kéo ra xa khỏi nút chuyển và thả, bạn cần đăng ký một sự kiện cửa sổ toàn cục:

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

  dragEnd(event)
})

Tôi nghĩ rằng điều quan trọng là người dùng có thể tự do kéo và giao diện đủ thông minh để tính đến điều đó. Bạn không cần phải mất nhiều thời gian để xử lý vấn đề này 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()
}

Quá trình tương tác với phần tử đã hoàn tất, đã đến lúc đặt thuộc tính đã đánh dấu đầu vào và xoá tất 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í của dòng ngón tay cái nằm trong giới hạn của kênh và trả về true nếu dòng này bằng hoặc lớn hơn một nửa dọc theo kênh:

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ĩ khác

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 dữ liệu đầu vào trong một nhãn. Nhãn, là một phần tử mẹ, sẽ nhận được các lượt tương tác nhấp sau khi nhập. Ở cuối sự kiện dragEnd, bạn có thể 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 việc nhãn nhận được lượt nhấp sau này, vì nhãn sẽ bỏ đánh dấu hoặc đánh dấu hoạt động tương tác mà người dùng đã thực hiện.

Nếu phải 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.

Loại JavaScript này là loại tôi ít thích viết nhất, tôi không muốn quản lý việc đưa sự kiện có điều kiện lên trên:

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

Kết luận

Thành phần nút chuyển nhỏ bé này đã trở thành phần tốn nhiều công sức nhất trong tất cả các Thử thách GUI cho đến nay! Giờ thì bạn đã biết cách tôi làm, còn bạn thì sao‽ 🙂

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 bản minh hoạ, gửi đường liên kết cho tôi trên Twitter và tôi sẽ thêm bản minh hoạ đó vào phầ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 mã nguồn .gui-switch trên GitHub.