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 chuyển đổi thích ứng và dễ tiếp cận.

Trong bài đăng này, tôi muốn chia sẻ suy nghĩ về cách xây dựng các thành phần chuyển đổi. Dùng thử bản minh hoạ.

Bản minh hoạ

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

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õ ràng 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, ưu điểm là không cần CSS hoặc JavaScript để có đầy đủ chức năng và dễ truy cập. Khi tải CSS, bạn sẽ hỗ trợ các ngôn ngữ viết từ phải sang trái, độ dọc, ảnh động, v.v. Khi tải JavaScript, bạn có thể kéo và xem nút chuyển.

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à 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 toàn bộ thành phần con và các điểm truy cập để tuỳ chỉnh tập trung.

Track

Độ 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 làm nổi bật 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 tình trạng lặp lại, truy vấn nội dung nghe nhìn của người dùng giảm mức độ ưu tiên chuyển động có thể được đưa 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ố 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 một <label>, gói mối quan hệ của chúng để tránh sự không rõ ràng về hộp đánh dấu và nhãn liên kết, đồng thời cho phép người dùng tương tác với nhãn để chuyển đổi dữ liệu đầu vào.

Hộp đánh dấu và nhãn tự nhiên, chưa được định 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 một APIstate. Trình duyệt quản lý thuộc tính checked và các sự kiện đầu vào, chẳng hạn như oninputonchanged.

Bố cục

Flexbox, gridcác thuộ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. Chúng tập trung vào các giá trị, đặt tên cho các phép tính hoặc khu vực không rõ ràng, đồng thời kích hoạ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 của nút chuyển này là Linh hoạt. Lớp .gui-switch chứa các thuộc tính tuỳ chỉnh riêng tư và công khai mà trẻ dùng để tính toán bố cục.

Công cụ cho nhà phát triển của hộp linh hoạt phủ một nhãn và công tắc theo chiều ngang, cho thấy cách phân bổ không gian theo bố cục.

.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 một nút chuyển hoặc để thay đổi flex-direction:

Công cụ cho nhà phát triển của hộp linh hoạt phủ một nhãn dọc và công tắc.

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

Track

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

Công cụ cho nhà phát triển dạng lưới phủ lên kênh chuyển đổi, hiển thị các khu vực theo dõi lưới đã đặ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;
}

Bản nhạc này cũng tạo một khu vực theo dõi dạng lưới từng ô đơn để ngón cái xác nhận.

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ảgiả lớp :checked trên dữ liệu đầu vào để thay thế chỉ báo trực quan này.

Ngón cái là một phần tử con giả lập đượ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 khu vực lưới track:

Công cụ cho nhà phát triển cho thấy phần tử thu nhỏ giả phần tử đượ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 một thành phần chuyển đổi linh hoạt thích ứng với bảng phối màu, ngôn ngữ từ phải sang trái cũng như các 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à 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 thêm các tính năng làm nổi bật thao tác nhấn và lựa 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 hình ảnh mà việc chuyển đổi này cần đế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ì chúng có thể là phản hồi tương tác hình ảnh 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 đó.

Track

Kiểu của phần tử này chủ yếu dựa trên 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 tầng.

Các biến thể của nút chuyển với kích thước và màu sắc theo dõi 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);
}

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

Thumb

Phần tử thumb đã có ở bên phải track nhưng cần 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 được làm nổi bật phần tử giả ngón cái ở dạng vòng 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 thay đổi về vị trí di chuột và vị trí ngón cái. Lựa chọn ưu tiên của người dùng cũng được chọn trước khi chuyển đổi kiểu đánh dấu chuyển động hoặc 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 để định vị thumb trong bản nhạc. Hiện tại, chúng ta có các kích thước thumb và track mà chúng ta sẽ sử dụng trong các phép tính để giữ cho thumb đúng cách và ở giữa các track: 0%100%.

Phần tử input sở hữu biến vị trí --thumb-position, còn 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));
}

Chúng tôi hiện miễn phí thay đổi --thumb-position từ CSS và các lớp giả lập đã cung cấp trong các phần tử hộp đánh dấu. Vì trước đó chúng ta đã đặt transition: transform var(--thumb-transition-duration) ease có điều kiện cho phần tử này, nên các thay đổi này có thể tạo ảnh động khi được 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ĩ rằng sự phối hợp tách biệt này làm tốt lắm. Phần tử thumb chỉ liên quan đến một kiểu, vị trí translateX. Dữ liệu đầu vào có thể quản lý mọi độ phức tạp và phép tính.

Dọc

Bạn đã hỗ trợ xong bằng một lớp đối tượng sửa đổi -vertical. Lớp này sẽ thêm chế độ xoay với các phép biến đổi CSS cho phần tử input.

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

.gui-switch {
  --isLTR: 1;

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

Ban đầu, thuộc tính tuỳ chỉnh có tên là --isLTR chứa 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ả lập 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 thực tế 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));
}

Bây giờ, chế độ xoay của nút chuyển dọc sẽ tài khoản cho vị trí bên đối diện theo yêu cầu của bố cục từ phải sang trái.

Bạn cũng cần cập nhật translateX biến đổi trên phần tử giả thumb để đáp ứng 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 có tác dụng để giải quyết mọi nhu cầu liên quan đến một khái niệm như biến đổi CSS logic, nhưng có một số nguyên tắc DRY (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 tất nếu không xử lý nhiều trạng thái có thể ở trong: :checked, :disabled, :indeterminate:hover. :focus được giữ nguyên một cách có chủ đích, chỉ điều chỉnh phần bù trừ; vòng điều chỉnh 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 tập trung vào một nút chuyển trong Firefox và Safari.

Đã đánh dấu

<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 đại diện cho trạng thái on. Ở trạng thái này, nền "theo dõi" đầu vào được đặt thành màu hoạt động và vị trí ngón cái được đặt là "kết thúc".

.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ó giao diện khác nhau mà còn khiến cho phần tử không thể thay đổi được.Không thể thay đổi tương tác trong trình duyệt, nhưng các trạng thái trực quan 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 có kiểu tối ở các trạng thái đã tắt, đã đánh dấu và đã bỏ đánh dấu.

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

Không xác định

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 tiểu bang thú vị, hấp dẫn và đơn giản. Xin lưu ý rằng các trạng thái boolean có thể lén lút giữa các trạng thái.

Việc đặt hộp đánh dấu thành không xác định là rất khó, 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ó dấu ngoặc nhọn ở giữa, để cho biết chưa quyết định.

Đối với tôi, trạng thái này đơn giản và hấp dẫn, nên tôi nên đặt vị trí 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

Các hoạt động tương tác khi di chuột sẽ 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 về giao diện người dùng tương tác. Nút chuyển này làm nổi bật nút ngón cái bằng một vòng bán trong suốt khi di chuột lên nhãn hoặc mục nhập. Sau đó, ảnh động di chuột này cung cấp hướng về phần tử thumb tương tác.

Hiệu ứng "đánh dấu" được thực hiện bằng box-shadow. Khi di chuột, của một phương thức nhập không bị tắt, hãy tăng kích thước của --highlight-size. Nếu người dùng ổn với chuyển động, chúng ta sẽ chuyển đổi box-shadow và thấy nó tăng lên. Nếu người dùng không hài lòng với chuyển động, phần đánh dấu 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 chuyển đổi có thể tạo ra cảm giác lạ lẫm khi cố gắng mô phỏng giao diện thực, đặc biệt là loại này với một vòng tròn bên trong một kênh. iOS làm được điều này với công tắc, bạn có thể kéo chúng từ bên này sang bên kia và thật sự hài lòng khi có tuỳ 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ể cảm thấy không hoạt động nếu bạn đã thử dùng cử chỉ kéo mà không có gì xảy ra.

Hình ngón tay cái có thể kéo

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

touch-action

Kéo là một cử chỉ tuỳ chỉnh, khiến đây là một cử chỉ tuyệt vời để hưởng các lợi ích của touch-action. Trong trường hợp nút chuyển này, tập lệnh của chúng tôi 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 cần xử lý những cử chỉ nào 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 cần 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 luồng chuyển đổi nà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ỉ theo chiều 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 giá trị đầu vào và cuộn trang, nhưng con trỏ chiều ngang được xử lý tuỳ chỉnh.

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

Khi thiết lập và trong quá trình kéo, cần lấy nhiều giá trị số đã 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 do có một thuộc tính CSS. Tham số này được dùng trong tập lệnh thiết lập như getStyle(checkbox, 'padding-left') này.

​​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ục tiêu). Khá rõ ràng là JavaScript có thể đọc rất nhiều giá trị của các phần tử, thậm chí cả từ các phần tử giả.

dragging

Đây là khoảnh khắc cốt lõi đối với logic kéo và có một vài điều bạn cần lưu ý từ trình xử lý sự kiện của 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`)
}

Hình ảnh chính của tập lệnh là state.activethumb, vòng tròn nhỏ mà tập lệnh này đang đặt cùng với một con trỏ. Đối tượng switches là một Map(), trong đó các khoá là .gui-switch và 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ả. Thao tác từ phải sang trái được xử lý bằng cùng một thuộc tính tuỳ chỉnh mà CSS là --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ợ RTL. event.offsetX cũng có giá trị vì chứa giá trị delta hữu ích cho việc định vị thumb.

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

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

dragEnd

Để người dùng có thể kéo ra ngoài nút chuyển và thả ra, bạn cần đăng ký một sự kiện cửa sổ toàn cầu:

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ó thể tự do kéo thoải mái và có giao diện đủ thông minh để giải quyết vấn đề này. Việc chuyển đổi này không mất nhiều thời gian, nhưng cần xem xét cẩn thận 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()
}

Đã hoàn tất tương tác với phần tử, đã đến lúc đặt thuộc tính đầu vào đã đánh dấu 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 (được gọi bởi dragEnd) sẽ xác định vị trí dòng thumb nằm trong giới hạn của kênh và trả về giá trị true nếu dòng này bằng hoặc hơn 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ĩ bổ sung

Thao tác kéo đã phát sinh một chút phần nợ mã do cấu trúc HTML ban đầu được chọn, đáng chú ý là việc gói dữ liệu đầu vào trong một nhãn. Nhãn, là phần tử mẹ, sẽ nhận được các lượt tương tác nhấp sau khi nhập. Khi kết thúc sự kiện dragEnd, bạn có thể nhận thấy padRelease() là một hàm có âm thanh lạ.

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

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

Việc này là để tính đến việc nhãn nhận được lượt nhấp sau này (vì nhãn này sẽ bỏ đánh dấu hoặc kiểm tra 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 việ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 xử lý hành vi tích hợp sẵn.

Tôi không thích viết loại JavaScript này nhất, tôi không muốn quản lý bong bóng 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 công tắc nhỏ này đã trở thành thành phần hiệu quả nhất trong tất cả Thử thách GUI từ trước đến nay! Giờ bạn đã biết tôi làm được như thế nào, bạn sẽ làm thế nào 🙂

Hãy đa dạng hoá phương pháp tiếp cận của chúng ta 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ạ, đường liên kết tweet me và tôi sẽ thêm bản phối lại đó 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.