בניית רכיב מתג

סקירה בסיסית של תהליך הבנייה של רכיב מתג נגיש ורספונסיבי.

בפוסט הזה אני רוצה לשתף חשיבה על דרך לבניית רכיבי מתג. רוצים לנסות את ההדגמה?

הדגמה

אם ברצונך ליצור סרטון, הנה גרסת YouTube של הפוסט הזה:

סקירה כללית

מתג פועל בדומה לתיבת סימון, אך מייצג במפורש מצבי הפעלה והשבתה של הערך הבוליאני.

בהדגמה הזו נעשה שימוש ב-<input type="checkbox" role="switch"> ברוב הפונקציונליות, מכיוון שאין צורך ב-CSS או ב-JavaScript כדי לאפשר פונקציונליות מלאה ונגישה. טעינת CSS תומכת בשפות מימין לשמאל, לאורך, אנימציה ועוד. טעינת JavaScript הופכת את המתג לגרירה ומוחשי.

מאפיינים מותאמים אישית

המשתנים הבאים מייצגים את החלקים השונים של המתג ואת האפשרויות שלהם. כמחלקה ברמה העליונה, .gui-switch מכיל מאפיינים מותאמים אישית שמשמשים בכל רכיבי הצאצאים של הרכיבים, ונקודות כניסה להתאמה אישית ריכוזית.

טראק

האורך (--track-size), המרווח הפנימי ושני צבעים:

.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 ומאפיינים מותאמים אישית הם חיוניים לשמירה על הסגנונות של הרכיב הזה. הם מרכזים את הערכים, נותנים שמות לחישובים או לאזורים שאינם ברורים, ומאפשרים ממשק API קטן של מאפיין מותאם אישית, שמאפשר התאמה אישית קלה של רכיבים.

.gui-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 מוצג בשכבת-על של מסלול המתג, ומציג את אזורי ה-Grid Track שנקראים &#39;track&#39;.

.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 גם מסיר את סימן הווי החזותי שהדפדפן מספק. הרכיב הזה משתמש בפסאודו-רכיב וב-:checked פסאודו-סיווג בקלט כדי להחליף את האינדיקטור החזותי הזה.

האגודל הוא פסאודו-רכיב צאצא המחובר ל-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 דרך המפל.

אפשר להחליף וריאנטים באמצעות גדלים וצבעים מותאמים אישית של טראקים.

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

מגוון רחב של אפשרויות להתאמה אישית של מסלול המתג מגיע מארבעה מאפיינים מותאמים אישית. 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.

עם זאת, רכיב מסובב בתלת-ממד לא משנה את הגובה הכולל של הרכיב, וזה עלול לגרום לסידור בלוקים מסוימים. מביאים בחשבון את הנתונים האלה באמצעות המשתנים --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, אלד שכקטר ואני יוצרים אב טיפוס תפריט צדדי באמצעות המרות CSS שטיפל בשפות מימין לשמאל על ידי הפיכת משתנה יחיד. עשינו זאת כי אין טרנספורמציות לוגיות של מאפיינים ב-CSS, ויכול להיות שלעולם לא יהיו. אלעד היה רעיון נהדר להשתמש בערך של נכס מותאם אישית כדי להפוך אחוזים, כדי לאפשר ניהול של מיקום יחיד באמצעות הלוגיקה המותאמת אישית שלנו עבור טרנספורמציות לוגיות. השתמשתי באותה שיטה במתג הזה ולדעתי זה עבד נהדר:

.gui-switch {
  --isLTR: 1;

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

נכס מותאם אישית שנקרא --isLTR מכיל בהתחלה את הערך 1, כלומר הוא true כי הפריסה שלנו מופיעה משמאל לימין כברירת מחדל. לאחר מכן, באמצעות פסאודו המחלקה :dir() של CSS, הערך מוגדר ל--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, אבל היא כן מציעה כמה עקרונות יבשיים לתרחישים רבים לדוגמה.

מדינות

כדי להשתמש בגרסה המובנית של 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%);
    }}
  }
}

המתג עם העיצוב הכהה במצבים &#39;מושבתים&#39;, &#39;מסומנים&#39; ולא מסומנים

המצב הזה מסובך כי צריך להשתמש בעיצוב כהה ובעיצוב בהיר גם במצב מושבת וגם במצב מסומן. בחרתי בסגנונות סגנוניים מינימליים למצבים האלה כדי להקל על נטל התחזוקה של שילובי הסגנונות.

לא קבוע

מצב שנשכח לעיתים קרובות הוא :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)
  );
}

העברת העכבר מלמעלה

האינטראקציות של העברת העכבר אמורות לספק תמיכה חזותית בממשק משתמש מחובר וגם לספק כיוון ביחס לממשק משתמש אינטראקטיבי. המתג הזה מדגיש את האגודל עם טבעת שקופה למחצה כשמעבירים את העכבר מעל התווית או הקלט. אנימציה של העברת העכבר מספקת כיוון לכיוון לאלמנט האינטראקטיבי.

אפקט ה'הדגשה' מסתיים ב-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 יש את זה כמו שצריך עם המתג שלהם, ואפשר לגרור אותם מצד לצד, וזה מאוד שמח לקבל את האפשרות. לעומת זאת, אם יתבצע ניסיון גרירה ושום דבר לא יקרה, רכיב בממשק המשתמש ירגיש לא פעיל.

אגודלים שניתן לגרור

פסאודו-הרכיב האגודל מקבל את המיקום שלו מה-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;
}

התוצאה הרצויה היא תנועה אופקית שלא מאפשרת הזזה או גלילה בדף. מצביע יכול להתחיל גלילה אנכית מתוך הקלט ולגלול בדף, אבל הגלילה האופקית מטופלת בהתאמה אישית.

כלי עזר בסגנון ערך Pixel

במהלך ההגדרה ובמהלך הגרירה, צריך לחלץ ערכים שונים של מספרים מחושבים מאלמנטים. פונקציות ה-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() מקבל ארגומנט שני, רכיב פסאודו-יעד. מהמם, קוד 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()
}

האינטראקציה עם הרכיב הושלמה, הגיע הזמן להגדיר את מאפיין הקלט שסומן ולהסיר את כל אירועי התנועה. תיבת הסימון תשתנה עם state.activethumb.checked = determineChecked().

determineChecked()

הפונקציה הזו, שנקראת על ידי dragEnd, קובעת את המיקום של האגודל הנוכחי בתוך גבולות המסלול שלה, ומחזירה TRUE אם הוא שווה לחצי לאורך המסלול או מעליו:

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 במהלך שדרוג חוויית המשתמש, כדי ליצור רכיב שמטפל בעצמו בקליקים על תוויות, ולא יילחם בהתנהגות מובנית.

סוג ה-JavaScript הזה הוא הכי פחות אהוב עליי לכתוב, ואני לא רוצה לנהל את הבועות של האירועים המותנים:

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

סיכום

רכיב המתג הקטן הזה יצר בסופו של דבר את רוב העבודה הקשה מכל אתגרי GUI! עכשיו, אחרי שאת יודעת איך עשיתי את זה, איך היית רוצה ‽ 🙂

בואו נגוון את הגישות שלנו ונלמד את כל הדרכים לבנות באינטרנט. צור הדגמה (דמו), ציוץ לי קישורים ואני אוסיף אותה לקטע 'רמיקסים של הקהילה' למטה!

רמיקסים של הקהילה

משאבים

מאתרים את קוד המקור ב-GitHub.gui-switch.