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

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

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

דמו

אם אתם מעדיפים סרטון, הנה גרסה של הפוסט הזה ב-YouTube:

סקירה כללית

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

הדמו הזה משתמש ב-<input type="checkbox" role="switch"> לרוב הפונקציונליות שלו, והיתרון של השימוש ב-<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 על סמך טיוטת המפרט הזה ב-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 שכבת-על של המסלול למעבר, שמוצגים בו אזורים של מסלולי רשת עם השם &#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:

DevTools שמוצגת בהם תמונה ממוזערת של פסאודו-הרכיב כשהוא ממוקם בתוך רשת 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)
  );
}

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

לאורך

התמיכה בוצעה באמצעות סיווג מודификатор -vertical שמוסיף רוטציה עם טרנספורמציות CSS לרכיב input.

עם זאת, רכיב שמוצג ב-3D בזווית לא משנה את הגובה הכולל של הרכיב, ולכן יכול לגרום לשינוי בפריסה של הבלוק. כדי להביא זאת בחשבון, משתמשים במשתנים --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 כי הפריסה שלנו היא משמאל לימין כברירת מחדל. לאחר מכן, באמצעות פסאודו-הקלאס של 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%);
    }}
  }
}

המתג בסגנון כהה במצבים מושבת, מסומן ולא מסומן.

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

לא קבוע

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

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

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

אם אצטרך לעשות זאת שוב, יכול להיות שאשתמש ב-JavaScript כדי לשנות את DOM במהלך השדרוג של חוויית המשתמש, כדי ליצור רכיב שיטפל בעצמו בלחיצות על התוויות ולא יתנגש בהתנהגות המובנית.

קוד JavaScript מהסוג הזה הוא קוד שאני הכי לא אוהב לכתוב, כי אני לא רוצה לנהל את ההעברה (bubbling) של אירועים מותנים:

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

סיכום

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

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

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

משאבים

קוד המקור של .gui-switch זמין ב-GitHub.