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

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

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

การสาธิต

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

Thumb

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

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

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

แทร็ก

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

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

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

Thumb

รูปแบบ 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);
}

รูปแบบ

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

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

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

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

Thumb

องค์ประกอบนิ้วโป้งอยู่ทางขวา 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 และ 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) ขวาไปซ้าย

เพื่อน 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 ในสถานะนี้ พื้นหลัง "track" ของอินพุตตั้งค่าเป็นสีที่ใช้งานได้ และตำแหน่งนิ้วหัวแม่มือตั้งค่าเป็น "จุดสิ้นสุด"

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

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

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

องค์ประกอบเทียมของภาพขนาดย่อได้รับตำแหน่งจาก var(--thumb-position) ที่มีขอบเขต .gui-switch > input, 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

ช่วงเวลานี้เป็นช่วงหลักของตรรกะการลากและมี 2-3 อย่างที่ควรสังเกตจากเครื่องจัดการเหตุการณ์ของฟังก์ชัน ดังนี้

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

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