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

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

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

สาธิต

หากต้องการดูวิดีโอ โปรดดูโพสต์เวอร์ชัน 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%);
  }
}

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

หากต้องการเพิ่มอีเมลแทนที่ชัดเจนและลดการซ้ำกัน ให้ใส่ Media Query ของผู้ใช้ที่มีค่ากำหนดการเคลื่อนไหวที่ลดลงไว้ในพร็อพเพอร์ตี้ที่กำหนดเองด้วยปลั๊กอิน PostCSS โดยอิงตามข้อกำหนดฉบับร่างใน Media Query 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 และสถานะที่สร้างไว้ล่วงหน้า เบราว์เซอร์จะจัดการพร็อพเพอร์ตี้ checked และเหตุการณ์การป้อนข้อมูล เช่น oninput และ onchanged

เลย์เอาต์

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

.gui-switch

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

เครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ Flexbox ที่วางซ้อนป้ายกำกับและปุ่มสลับแนวนอน ซึ่งแสดงเลย์เอาต์และการจัดสรรพื้นที่

.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ปกติออก แล้วระบุขนาดของตัวเองแทน

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

นอกจากนี้ แทร็กจะสร้างพื้นที่กริด 1 แถว 1 คอลัมน์สำหรับภาพปกเพื่ออ้างสิทธิ์

ภาพย่อ

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

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

เครื่องมือสำหรับนักพัฒนาซอฟต์แวร์แสดงภาพขนาดย่อขององค์ประกอบจำลองซึ่งวางไว้ภายในตารางกริด CSS

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

รูปแบบ

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

การเปรียบเทียบธีมสว่างและธีมมืดของปุ่มสลับและสถานะต่างๆ

สไตล์การโต้ตอบด้วยการสัมผัส

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

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

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

ติดตาม

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

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

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

เราได้สร้างต้นแบบเมนูด้านข้างแบบเลื่อนออกโดยใช้การเปลี่ยนรูปแบบ CSS ที่จัดการภาษาจากขวาไปซ้ายร่วมกับ Elad Schecter ซึ่งเป็นเพื่อนที่ใช้ 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 ในสถานะนี้ พื้นหลังของ "แทร็ก" อินพุตจะตั้งค่าเป็นสีที่ใช้งานอยู่ และตั้งค่าตำแหน่งแถบเลื่อนเป็น "สิ้นสุด"

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

สวิตช์สไตล์สีเข้มในสถานะปิดใช้ เลือก และไม่ได้เลือก

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

ระบุสถานะไม่ได้

สถานะที่มักลืมคือ :indeterminate ซึ่งช่องทําเครื่องหมายจะไม่ได้เลือกหรือยกเลิกการเลือก สถานะนี้เป็นสถานะที่สนุกสนาน น่าดึงดูด และถ่อมตัว โปรดทราบว่าสถานะบูลีนอาจมีสถานะอื่นๆ แทรกอยู่

การตั้งค่าช่องทําเครื่องหมายเป็น "ไม่แน่ใจ" นั้นค่อนข้างยุ่งยาก มีเพียง 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 เหตุการณ์เคอร์เซอร์เป็นหัวใจสําคัญของโซลูชันนี้ โดยจะติดตามตําแหน่งเคอร์เซอร์แบบมีเงื่อนไขเพื่อแก้ไขพร็อพเพอร์ตี้ที่กําหนดเองของ 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 และสามารถใช้เพื่อกลับค่าตรรกะและรองรับ 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()
}

การโต้ตอบกับองค์ประกอบเสร็จสมบูรณ์แล้ว ถึงเวลาตั้งค่าพร็อพเพอร์ตี้ checked ของอินพุตและนำเหตุการณ์ท่าทางสัมผัสทั้งหมดออก ช่องทำเครื่องหมายจะเปลี่ยนเป็น 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 เริ่มต้นที่เลือก ซึ่งส่วนใหญ่จะสังเกตได้จากการรวมอินพุตไว้ในป้ายกำกับ ป้ายกำกับซึ่งเป็นองค์ประกอบหลักจะได้รับการโต้ตอบด้วยการคลิกหลังจากอินพุต คุณอาจสังเกตเห็น padRelease() เป็นฟังก์ชันที่ฟังดูแปลกๆ ในตอนท้ายของเหตุการณ์ dragEnd

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