การสร้างคอมโพเนนต์สวิตช์

ภาพรวมพื้นฐานของวิธีสร้างคอมโพเนนต์สวิตช์ที่ปรับเปลี่ยนตามอุปกรณ์และเข้าถึงได้

ในโพสต์นี้ผมอยากจะแชร์วิธีคิดวิธีสร้างคอมโพเนนต์สวิตช์ ลองใช้การสาธิต

สาธิต

หากต้องการดูวิดีโอ โปรดใช้โพสต์นี้ในเวอร์ชัน YouTube

ภาพรวม

ฟังก์ชันสวิตช์ที่คล้ายกับช่องทำเครื่องหมาย แต่จะแสดงสถานะเปิดและปิดบูลีนอย่างชัดเจน

การสาธิตนี้ใช้ <input type="checkbox" role="switch"> เป็นหลัก ใหม่ ซึ่งมีข้อดีคือไม่จำเป็นต้องมี CSS หรือ JavaScript ใช้งานได้เต็มรูปแบบและเข้าถึงได้ การโหลด CSS จะรองรับการเขียนจากขวาไปซ้าย ภาษา ประเภทธุรกิจ ภาพเคลื่อนไหว และอื่นๆ การโหลด JavaScript ทำให้การเปลี่ยน ลากได้และจับต้องได้

คุณสมบัติที่กำหนดเอง

ตัวแปรต่อไปนี้แสดงส่วนต่างๆ ของสวิตช์และ ตัวเลือก ในฐานะคลาสระดับบนสุด .gui-switch มีพร็อพเพอร์ตี้ที่กำหนดเองที่ใช้ ในคอมโพเนนต์ย่อย และจุดแรกเข้าสำหรับการรวมศูนย์ การปรับแต่งได้มากขึ้น

ติดตาม

ความยาว (--track-size) ระยะห่างจากขอบ และ 2 สี:

.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%);
  }
}

ภาพย่อ

ขนาด สีพื้นหลัง และสีไฮไลต์สำหรับการโต้ตอบมีดังนี้

.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%);
  }
}

การเคลื่อนไหวลดลง

หากต้องการเพิ่มนามแฝงที่ชัดเจนและลดการกล่าวซ้ำๆ ผู้ใช้ให้ความสำคัญกับการเคลื่อนไหวน้อยลง สามารถนำคิวรี่สื่อไปใส่ไว้ในพร็อพเพอร์ตี้ที่กำหนดเองด้วย PostCSS ปลั๊กอิน ตามฉบับร่างนี้ ข้อกำหนดเฉพาะในคำค้นหาสื่อ 5:

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

Markup

ฉันเลือกรวมองค์ประกอบ <input type="checkbox" role="switch"> ด้วย <label> รวมความสัมพันธ์เข้าด้วยกันเพื่อหลีกเลี่ยงช่องทำเครื่องหมายและการเชื่อมโยงป้ายกำกับ ที่คลุมเครือ ในขณะเดียวกันก็ทำให้ผู้ใช้สามารถโต้ตอบกับป้ายกำกับเพื่อ เปิด/ปิดอินพุต

ต
ป้ายกำกับและช่องทำเครื่องหมาย
ที่ไม่มีการจัดรูปแบบ

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

<input type="checkbox"> สร้างไว้ล่วงหน้าโดยมี API และ state เบราว์เซอร์จะจัดการ checked คุณสมบัติและอินพุต กิจกรรม เช่น oninputและ onchanged

เลย์เอาต์

Flexbox grid และcustom พร็อพเพอร์ตี้มีความสำคัญ ในการรักษาสไตล์ของคอมโพเนนต์นี้ไว้ รวมค่าไว้ในที่เดียวและตั้งชื่อ ในการคำนวณหรือพื้นที่ที่ไม่ชัดเจน และเปิดใช้คุณสมบัติที่กำหนดเองขนาดเล็ก API สำหรับการปรับแต่งคอมโพเนนต์ที่ง่ายดาย

.gui-switch

เลย์เอาต์ระดับบนสุดสำหรับสวิตช์คือ Flexbox คลาส .gui-switch ประกอบด้วย พร็อพเพอร์ตี้ที่กำหนดเองและสาธารณะที่เด็กใช้ในการคำนวณ เลย์เอาต์

Flexbox DevTools วางซ้อนป้ายกำกับและสวิตช์แนวนอน แสดงเลย์เอาต์
การกระจายพื้นที่

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

การขยายและแก้ไขเลย์เอาต์ Flexbox จะเหมือนกับการเปลี่ยนเลย์เอาต์ Flexbox ตัวอย่างเช่น หากต้องการวางป้ายกำกับเหนือหรือใต้สวิตช์ หรือเพื่อเปลี่ยน flex-direction:

เครื่องมือสำหรับนักพัฒนาเว็บของ Flexbox วางซ้อนป้ายกำกับแนวตั้งและสวิตช์

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

ติดตาม

อินพุตของช่องทำเครื่องหมายถูกกำหนดลักษณะเป็นแทร็กสวิตช์โดยนำช่องทำเครื่องหมายปกติออก appearance: checkbox และระบุขนาดของตนเองแทน:

เครื่องมือสำหรับนักพัฒนาเว็บในตารางกริดวางซ้อนแทร็กสวิตช์ แสดงแทร็กแบบตารางกริดที่มีชื่อ
พื้นที่ที่ชื่อว่า &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;
}

แทร็กดังกล่าวยังสร้างพื้นที่แทร็กแบบตารางกริดเซลล์แบบทีละเซลล์สำหรับนิ้วโป้งด้วย การอ้างสิทธิ์

ภาพย่อ

รูปแบบ appearance: none จะลบเครื่องหมายถูกซึ่งมองเห็นได้โดย เบราว์เซอร์ คอมโพเนนต์นี้ใช้ pseudo-element และ :checked Pseudo-class ในอินพุตไปยัง แทนที่สัญญาณบอกสถานะภาพนี้

นิ้วหัวแม่มือเป็นองค์ประกอบย่อยองค์ประกอบเทียมที่แนบอยู่กับ input[type="checkbox"] และ วางซ้อนอยู่ด้านบนแทร็กแทนที่จะอยู่ด้านล่างแทร็กโดยอ้างสิทธิ์ในพื้นที่ตารางกริด track:

เครื่องมือสำหรับนักพัฒนาเว็บแสดงนิ้วหัวแม่มือองค์ประกอบเทียมตามที่ตำแหน่งอยู่ภายในตารางกริด CSS

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

รูปแบบ

คุณสมบัติที่กำหนดเองจะทำให้คอมโพเนนต์สวิตช์อเนกประสงค์ซึ่งจะปรับตามสี รูปแบบ ภาษาที่เขียนจากขวาไปซ้าย และค่ากำหนดการเคลื่อนไหว

การเปรียบเทียบธีมสว่างและธีมมืดสำหรับสวิตช์และธีม
รัฐ

รูปแบบการโต้ตอบด้วยการสัมผัส

ในอุปกรณ์เคลื่อนที่ เบราว์เซอร์จะเพิ่มไฮไลต์การแตะและฟีเจอร์การเลือกข้อความลงในป้ายกำกับและ อินพุต ซึ่งส่งผลเสียต่อรูปแบบและการโต้ตอบผ่านภาพ ต้องใช้สวิตช์นี้ ผมสามารถใช้ CSS เพียงไม่กี่บรรทัด ซึ่งสามารถนำเอฟเฟกต์เหล่านั้นออกแล้วเพิ่ม สไตล์ cursor: pointer ของฉันเอง:

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

คุณไม่ควรนำสไตล์เหล่านี้ออกเสมอไป เนื่องจากอาจเป็นรูปภาพที่มีคุณค่า ความคิดเห็นเกี่ยวกับการโต้ตอบ อย่าลืมระบุทางเลือกที่กำหนดเองในกรณีที่คุณนำตัวเลือกเหล่านั้นออก

ติดตาม

สไตล์ขององค์ประกอบนี้ส่วนใหญ่จะเกี่ยวกับรูปร่างและสี ซึ่งองค์ประกอบจะเข้าถึง จาก .gui-switch ระดับบนสุดผ่าน cascade

เปลี่ยนตัวแปรที่มีขนาดและสีแทร็กที่กำหนดเอง

.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);
}

ตัวเลือกการปรับแต่งที่หลากหลายสำหรับแทร็กสวิตช์มีให้เลือก 4 แบบ พร็อพเพอร์ตี้ที่กำหนดเอง มีการเพิ่ม border: none เนื่องจาก appearance: none ไม่ได้เพิ่ม นำเส้นขอบออกจากช่องทำเครื่องหมายบนเบราว์เซอร์ทั้งหมด

ภาพย่อ

องค์ประกอบนิ้วหัวแม่มืออยู่ที่ track ที่ถูกต้องอยู่แล้ว แต่ต้องการรูปแบบวงกลม:

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

แสดงเครื่องมือสำหรับนักพัฒนาเว็บซึ่งไฮไลต์องค์ประกอบเทียมแบบวงกลมรูปนิ้วหัวแม่มือ

การโต้ตอบ

ใช้พร็อพเพอร์ตี้ที่กำหนดเองเพื่อเตรียมรับการโต้ตอบซึ่งจะแสดงเป็นการวางเมาส์เหนือ ไฮไลต์และการเปลี่ยนตำแหน่งนิ้วหัวแม่มือ นอกจากนี้ ค่ากำหนดของผู้ใช้ ตรวจสอบแล้ว ก่อนที่จะเปลี่ยน การเคลื่อนไหวหรือไฮไลต์ไฮไลต์เมื่อวางเมาส์

.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;
  }}
}

ตำแหน่งนิ้วโป้ง

พร็อพเพอร์ตี้ที่กำหนดเองมีกลไกแหล่งที่มาเดียวสำหรับการวางภาพขนาดย่อ แทร็กนั้น เรามีขนาดแทร็กและนิ้วหัวแม่มือที่เราจะนำมาใช้ เพื่อแยกความแตกต่างระหว่างนิ้วโป้งและนิ้วโป้งอย่างเหมาะสมและอยู่ระหว่างรอยทาง 0% และ 100%

องค์ประกอบ input เป็นเจ้าของตัวแปรตำแหน่ง --thumb-position และนิ้วโป้ง องค์ประกอบเทียมใช้องค์ประกอบนี้เป็นตำแหน่ง translateX:

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

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

ตอนนี้เราเปลี่ยน --thumb-position จาก CSS และ Pseudo-class ได้อย่างอิสระ ที่ให้ไว้ในองค์ประกอบช่องทำเครื่องหมาย เนื่องจากเราตั้งค่า transition: transform var(--thumb-transition-duration) ease ตามเงื่อนไขก่อนหน้านี้ในองค์ประกอบนี้ การเปลี่ยนแปลงเหล่านี้ อาจมีการเคลื่อนไหวเมื่อมีการเปลี่ยนแปลง

/* 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)
  );
}

ผมคิดว่าการเรียบเรียงนี้แบบแยกกันใช้ได้ผลดี องค์ประกอบของนิ้วหัวแม่มือคือ กังวลเพียงรูปแบบเดียว นั่นคือตำแหน่ง translateX อินพุตจะจัดการทั้งหมด ความซับซ้อนและการคำนวณ

ประเภทธุรกิจ

สนับสนุนโดยใช้คลาสตัวปรับแต่ง -vertical ซึ่งเพิ่มการหมุนด้วย CSS จะเปลี่ยนรูปแบบเป็นองค์ประกอบ input

แต่องค์ประกอบที่หมุนแบบ 3 มิติ จะไม่เปลี่ยนความสูงโดยรวมของคอมโพเนนต์ ซึ่งจะตัดเลย์เอาต์บล็อกออก โปรดรับผิดชอบเรื่องนี้โดยใช้--track-size และ --track-paddingตัวแปร คำนวณพื้นที่ขั้นต่ำที่จำเป็นสำหรับ ปุ่มแนวตั้งเพื่อให้ลื่นไหลในเลย์เอาต์ตามที่คาดไว้

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

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

(RTL) จากขวาไปซ้าย

Elad Schecter เพื่อน CSS และผมได้สร้างต้นแบบ เมนูด้านข้างแบบเลื่อนออกโดยใช้การแปลง CSS ที่จัดการจากขวาไปซ้าย ภาษาด้วยการสลับ ตัวแปร เราทำเช่นนี้เนื่องจากไม่มีการแปลงคุณสมบัติเชิงตรรกะใน CSS และอาจไม่มีวันนั้นเลย Elad มีไอเดียดีๆ ในการใช้ค่าพร็อพเพอร์ตี้ที่กำหนดเอง กลับค่าเปอร์เซ็นต์ เพื่อให้การจัดการสถานที่ตั้งแห่งเดียวของ สำหรับการแปลงเชิงตรรกะ ฉันใช้เทคนิคเดียวกันนี้กับสวิตช์นี้และ คิดว่าทุกอย่างเข้าท่าดี:

.gui-switch {
  --isLTR: 1;

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

พร็อพเพอร์ตี้ที่กำหนดเองชื่อ --isLTR จะมีค่า 1 ในตอนแรก true เนื่องจากการจัดวางของเราเป็นแบบจากซ้ายไปขวาโดยค่าเริ่มต้น จากนั้นการใช้ CSS คลาสสมมติ :dir() ค่านี้ถูกกำหนดเป็น -1 เมื่อคอมโพเนนต์อยู่ภายในการจัดวางแบบขวาไปซ้าย

นำ --isLTR ไปใช้จริงด้วยการใช้ภายใน calc() ภายในการเปลี่ยนรูปแบบ:

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

ตอนนี้การหมุนของสวิตช์แนวตั้งจะคำนึงถึงตำแหน่งด้านตรงข้าม สำหรับเค้าโครงแบบขวาไปซ้าย

การเปลี่ยนรูปแบบ translateX ในองค์ประกอบเทียมของภาพขนาดย่อต้องได้รับการอัปเดตเป็น ความต้องการอีกฝั่งตรงข้ามคือ

.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)
  );
}

แม้วิธีการนี้จะไม่สามารถแก้ปัญหาความต้องการทั้งหมดที่เกี่ยวข้องกับแนวคิดอย่าง CSS เชิงตรรกะ เปลี่ยนรูปแบบนั้น ก็ยังนำเสนอ หลักการเกี่ยวกับ DRY สำหรับหลายๆ คน กรณีการใช้งาน

รัฐ

การใช้ input[type="checkbox"] ในตัวจะไม่สมบูรณ์หากไม่มี จัดการรัฐต่างๆ ที่เป็นไปได้ เช่น :checked, :disabled :indeterminate และ :hover :focus ถูกปล่อยไว้โดยเจตนา การปรับค่าชดเชยเท่านั้น วงแหวนโฟกัสดูดีใน Firefox และ Safari

ภาพหน้าจอของวงแหวนโฟกัสที่สวิตช์ใน Firefox และ Safari

เลือกแล้ว

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

รัฐนี้แสดงถึงรัฐ on ในสถานะนี้ อินพุต "track" พื้นหลังจะตั้งค่าเป็นสีแบบแอ็กทีฟ และตั้งค่าตำแหน่งนิ้วหัวแม่มือเป็น end"

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

ปิดใช้

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

ปุ่ม :disabled ไม่เพียงแต่ดูแตกต่างเท่านั้น แต่ยังควรทำให้ องค์ประกอบที่ไม่สามารถเปลี่ยนแปลงได้ การไม่เปลี่ยนแปลงของการโต้ตอบไม่มีค่าใช้จ่ายจากเบราว์เซอร์ แต่ สถานะภาพต้องมีรูปแบบเนื่องจากการใช้ 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%);
    }}
  }
}

ปิดใช้สวิตช์สไตล์มืดใน &quot;ปิดใช้&quot; &quot;เลือกแล้ว&quot; และ &quot;ยกเลิกการเลือกแล้ว&quot;
รัฐ

สถานะนี้จะซับซ้อนเนื่องจากต้องใช้ธีมสีเข้มและสีอ่อนโดยมีทั้งการปิดใช้และ รัฐที่ตรวจสอบแล้ว ฉันเลือกสไตล์มินิมอลสำหรับรัฐเหล่านี้เพื่อให้ดูง่าย ถึงภาระในการบำรุงรักษา การผสมผสานสไตล์

ไม่ระบุ

สถานะที่มักลืมคือ :indeterminate ซึ่งช่องทำเครื่องหมายไม่ใช่ทั้ง 2 สถานะ เลือกไว้หรือไม่ได้เลือกไว้ ฉากนี้เป็นเรื่องที่สนุกสนาน น่าเชิญ และไม่น่าเชื่อ ระดับพอดี เตือนความจำว่าสถานะบูลีนอาจมีการแอบแฝงระหว่างรัฐต่างๆ

การตั้งค่าช่องทำเครื่องหมายเป็น "ไม่ทราบ" เป็นเรื่องยาก มีเพียง JavaScript เท่านั้นที่สามารถตั้งค่า:

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

สถานะไม่ชัดเจนซึ่งมีนิ้วโป้งของแทร็กอยู่ใน
ตรงกลาง เพื่อหมายถึงยังไม่ได้ตัดสินใจ

เนื่องจากสำหรับฉันแล้ว รัฐนั้นไม่ซับซ้อนและน่าดึงดูด จึงเหมาะที่จะใส่ ตำแหน่งหัวแม่มือของสวิตช์ที่อยู่ตรงกลาง

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

วางเมาส์

การโต้ตอบเมื่อวางเมาส์ควรรองรับ UI ที่เชื่อมต่อและ แสดงทิศทางไปยัง UI แบบอินเทอร์แอกทีฟ สวิตช์นี้ไฮไลต์รูปนิ้วหัวแม่มือที่มี วงแหวนกึ่งโปร่งใสเมื่อวางเมาส์เหนือป้ายกำกับหรืออินพุต โฮเวอร์นี้ ภาพเคลื่อนไหวจะให้ทิศทางไปยังองค์ประกอบนิ้วหัวแม่มือแบบอินเทอร์แอกทีฟ

"ไฮไลต์" ใช้ box-shadow แล้ว เมื่อวางเมาส์เหนืออินพุตที่ไม่ได้ปิดใช้ ให้เพิ่มขนาด --highlight-size หากผู้ใช้เล่นภาพเคลื่อนไหวได้ เราจะเปลี่ยน box-shadow ให้ใหญ่ขึ้น และเห็นการเติบโต แต่หากดูแล้วไม่ลื่นไหล ไฮไลต์จะปรากฏขึ้นทันทีดังนี้

.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

สำหรับผมแล้ว อินเทอร์เฟซของสวิตช์อาจดูแปลกๆ ในช่วงที่เราลองเลียนแบบ ของอินเทอร์เฟซ โดยเฉพาะโค้ดในลักษณะนี้ที่มีวงกลมอยู่ภายในแทร็ก iOS ตอบถูกแล้ว คุณสามารถลากพวกเขาไปอีกด้านได้ และความพึงพอใจมาก ให้ตัดสินใจ ในทางกลับกัน องค์ประกอบ UI อาจดูเหมือนไม่ได้ทำงานหากท่าทางสัมผัสการลาก พยายามแล้ว แต่ไม่มีอะไรเกิดขึ้น

ลากนิ้วโป้งได้

องค์ประกอบเทียมแบบภาพขนาดย่อได้รับตำแหน่งจาก .gui-switch > input var(--thumb-position) ของขอบเขต JavaScript สามารถระบุค่ารูปแบบอินไลน์ใน อินพุตเพื่ออัปเดตตำแหน่งนิ้วหัวแม่มือแบบไดนามิก ซึ่งจะทำให้ดูเหมือนว่าจะเคลื่อนตาม ท่าทางสัมผัสของตัวชี้ เมื่อมีการปล่อยเคอร์เซอร์ ให้นำรูปแบบแทรกในบรรทัดออก และ พิจารณาว่าการลากใกล้เคียงกับปิดหรือเปิดมากขึ้นโดยใช้พร็อพเพอร์ตี้ที่กำหนดเอง --thumb-position หัวใจสำคัญของโซลูชันนี้คือ เหตุการณ์ Pointer การติดตามตำแหน่งตัวชี้แบบมีเงื่อนไขเพื่อแก้ไขคุณสมบัติที่กำหนดเองของ CSS

เนื่องจากคอมโพเนนต์ทำงาน 100% อยู่แล้วก่อนที่สคริปต์นี้จะแสดง ก็ต้องใช้ความพยายามเล็กน้อยในการคงลักษณะการทำงานเดิมไว้ เช่น คลิกป้ายกำกับเพื่อสลับอินพุต JavaScript ของเราไม่ควรเพิ่มคุณลักษณะที่ ฟีเจอร์ที่มีอยู่

touch-action

การลากเป็นท่าทางสัมผัสที่กำหนดเอง ซึ่งทำให้เป็นรูปแบบที่เหมาะสำหรับ สิทธิประโยชน์ของ touch-action ในกรณีของสวิตช์นี้ ท่าทางสัมผัสในแนวนอน ได้รับการจัดการโดยสคริปต์ของเรา หรือท่าทางสัมผัสในแนวตั้งที่จับภาพไว้สำหรับการสลับแนวตั้ง รายละเอียดปลีกย่อย touch-action ช่วยให้เราบอกเบราว์เซอร์ว่าควรใช้ท่าทางสัมผัสใด องค์ประกอบนี้เพื่อให้สคริปต์สามารถจัดการกับท่าทางสัมผัสโดยไม่มีการแข่งขัน

CSS ต่อไปนี้บอกเบราว์เซอร์ว่า เมื่อท่าทางสัมผัสของตัวชี้เริ่มต้นมาจาก ภายในแทร็กสวิตช์นี้ จัดการท่าทางสัมผัสในแนวตั้ง ไม่ต้องทำอะไรกับแนวนอน ดังนี้

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

ผลลัพธ์ที่ต้องการคือท่าทางสัมผัสแนวนอนที่ไม่ต้องเลื่อนหรือเลื่อน ตัวชี้ในแนวตั้งสามารถเลื่อนได้โดยเริ่มจากในอินพุตและเลื่อน แต่หน้าเว็บแนวนอนจะได้รับการจัดการแบบกำหนดเอง

ยูทิลิตีของรูปแบบค่าพิกเซล

ในการตั้งค่าและระหว่างการลาก จะต้องเลือกค่าตัวเลขที่คำนวณแล้วหลายๆ ค่า จากองค์ประกอบ ฟังก์ชัน JavaScript ต่อไปนี้แสดงค่าพิกเซลที่คำนวณ ได้รับพร็อพเพอร์ตี้ CSS ซึ่งจะใช้ในสคริปต์การตั้งค่าแบบนี้ 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,
}

สังเกตวิธีที่ window.getComputedStyle() ยอมรับอาร์กิวเมนต์ที่ 2 ซึ่งเป็นองค์ประกอบเทียมเป้าหมาย เยี่ยมไปเลยที่ JavaScript อ่านค่าได้มากมายจากองค์ประกอบ แม้กระทั่งจากองค์ประกอบสมมติ

dragging

นี่คือช่วงเวลาสำคัญสำหรับตรรกะการลากและมีบางอย่างที่ควรสังเกต จากตัวแฮนเดิลเหตุการณ์ของฟังก์ชัน

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`)
}

ฮีโร่ของสคริปต์คือ state.activethumb วงกลมเล็กๆ ของสคริปต์นี้คือ การวางตำแหน่งควบคู่ไปกับตัวชี้ ออบเจ็กต์ switches คือ Map() โดยที่ เป็นของ .gui-switch และค่าจะเป็นขอบเขตที่แคชไว้และขนาดที่คง สคริปต์มีประสิทธิภาพ ฟังก์ชันจากขวาไปซ้ายจะได้รับการจัดการโดยใช้พร็อพเพอร์ตี้ที่กำหนดเองแบบเดียวกัน CSS นั้นคือ --isLTR และสามารถใช้ CSS เพื่อกลับด้านตรรกะเพื่อดำเนินการต่อ ซึ่งรองรับ RTL event.offsetX มีค่าเช่นกัน เนื่องจากมีเดลต้า ที่มีประโยชน์สำหรับการวางตำแหน่งของนิ้วหัวแม่มือ

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

บรรทัดสุดท้ายของ CSS นี้จะตั้งค่าคุณสมบัติที่กำหนดเองซึ่งองค์ประกอบนิ้วหัวแม่มือใช้ ช่วงเวลานี้ มิเช่นนั้น การกำหนดมูลค่าจะเปลี่ยนแปลงเมื่อเวลาผ่านไป แต่ตัวชี้ก่อนหน้า กิจกรรมตั้งค่า --thumb-transition-duration เป็น 0s ชั่วคราว กำลังนำรายการออก น่าจะเป็นการโต้ตอบที่ไม่มากนัก

dragEnd

เพื่อให้ผู้ใช้สามารถลากออกนอกสวิตช์ได้ไกลออกไป แล้วปล่อย ต้องลงทะเบียนกิจกรรมกรอบเวลาทั่วโลก:

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

  dragEnd(event)
})

ผมคิดว่าการที่ผู้ใช้มีอิสระในการลากอย่างหลวมๆ และต้องมี อินเทอร์เฟซของคุณฉลาดพอที่จะรองรับการดู จัดการได้ไม่ยากเลย ของการเปลี่ยนแปลงนี้ แต่ต้องพิจารณาอย่างรอบคอบในช่วงการพัฒนา ขั้นตอนได้

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()
}

การโต้ตอบกับองค์ประกอบสำเร็จแล้ว ถึงเวลาตั้งค่าอินพุตที่เลือก และนำเหตุการณ์ท่าทางสัมผัสออกทั้งหมด ช่องทำเครื่องหมายจะมีการเปลี่ยนแปลงด้วย state.activethumb.checked = determineChecked()

determineChecked()

ฟังก์ชันนี้เรียกใช้โดย dragEnd เป็นตัวกำหนดตำแหน่งของกระแสเงินสด ภายในขอบเขตของแทร็กและแสดงค่า "จริง" หากค่าเท่ากับหรือมากกว่า มาครึ่งทางแล้ว:

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
}

ความคิดเพิ่มเติม

ท่าทางสัมผัสการลากก่อให้เกิดหนี้โค้ดเล็กน้อยเนื่องจากโครงสร้าง HTML เริ่มต้น ที่เลือกไว้ โดยเฉพาะอย่างยิ่ง การรวมข้อมูลที่ป้อนไว้ในป้ายกำกับ ป้ายกำกับ การเป็นผู้ปกครอง จะได้รับการโต้ตอบคลิกหลังจากอินพุต ในตอนท้ายของ มี dragEnd เหตุการณ์ คุณอาจสังเกตเห็นแล้วว่า padRelease() ออกเสียงแปลกๆ

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

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

วิธีนี้เป็นการพิจารณาถึงป้ายกำกับ ที่ได้รับคลิกในภายหลัง เนื่องจากป้ายกำกับจะถูกยกเลิกการเลือก หรือตรวจสอบการโต้ตอบของผู้ใช้

หากฉันต้องทำเช่นนี้อีกครั้ง ฉันอาจพิจารณาปรับ DOM ด้วย JavaScript ระหว่างการอัปเกรด UX เช่น การสร้างองค์ประกอบที่จัดการการคลิกป้ายกำกับด้วยตัวเอง และไม่ต่อสู้กับพฤติกรรมที่มีในตัว

JavaScript ชนิดนี้ชอบเขียนน้อยที่สุด ฉันไม่ต้องการจัดการ การฟองอากาศของเหตุการณ์ตามเงื่อนไข:

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

บทสรุป

คอมโพเนนต์สวิตช์เล็กๆ นี้กลายเป็นงานยากที่สุดในความท้าทายของ GUI ที่ผ่านมา ตอนนี้คุณก็รู้แล้วว่าฉันทำท่านั้นได้อย่างไร คุณจะทำยังไงบ้างคะ‽ 🙂

มาเพิ่มความหลากหลายให้กับแนวทางของเราและเรียนรู้วิธีทั้งหมดในการสร้างเนื้อหาบนเว็บกัน สร้างลิงก์สาธิต ทวีตฉัน แล้วฉันจะเพิ่มให้เอง ที่ส่วนรีมิกซ์ของชุมชนด้านล่างนี้ได้เลย

รีมิกซ์ในชุมชน

แหล่งข้อมูล

ค้นหาซอร์สโค้ด .gui-switch ใน GitHub