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

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

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

การสาธิต

หากต้องการดูวิดีโอ โปรดดูโพสต์นี้ใน 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 Queries 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

เลย์เอาต์ระดับบนสุดสำหรับการสลับคือ 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ออกและระบุขนาดของตัวเองแทน

การวางซ้อน 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;
}

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

ภาพย่อ

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

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

DevTools แสดงภาพขนาดย่อขององค์ประกอบเสมือนที่วางตำแหน่งไว้ภายในตารางกริด 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 และองค์ประกอบ เสมือน thumb ใช้ตัวแปรนี้เป็นตำแหน่ง 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() ภายใน transform ดังนี้

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

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