בניית רכיב של לחצן מפוצל

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

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

הדגמה

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

סקירה כללית

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

לחצן פיצול נפוץ נמצא באפליקציית האימייל. הפעולה העיקרית היא שליחה, אבל אפשר גם לשלוח מאוחר יותר או לשמור טיוטה:

דוגמה ללחצן מפוצל כפי שהוא מוצג באפליקציית אימייל.

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

חלקים

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

רכיבי ה-HTML שמרכיבים את הכפתור המפוצל.

מאגר של לחצן מפוצל ברמה העליונה

הרכיב ברמה העליונה הוא תיבת flexbox מוטמעת, עם מחלקה של gui-split-button, שמכילה את הפעולה הראשית ואת .gui-popup-button.

הכיתה gui-split-button נבדקת ומוצגות מאפייני ה-CSS שמשמשים בכיתה הזו.

לחצן הפעולה הראשי

האלמנט <button> שגלוי בהתחלה וניתן להעברה אליו של המיקוד, נכנס לתוך הקונטיינר עם שני צורות פינתיות תואמות עבור אינטראקציות של מיקוד, ריחוף ופעילות, כך שהן מופיעות בתוך .gui-split-button.

כלי הבדיקה מציג את כללי ה-CSS של רכיב הלחצן.

הכפתור להחלפת המצב של החלון הקופץ

רכיב התמיכה popup button (לחצן קופץ) מיועד להפעלה ולרמז לרשימת הלחצנים המשניים. שימו לב שזה לא <button> ואי אפשר להתמקד בו. עם זאת, הוא משמש כנקודת עיגון למיקום של .gui-popup ומארח את :focus-within שמשמש להצגת החלון הקופץ.

כלי הבדיקה שמציג את כללי ה-CSS של המחלקה gui-popup-button.

הכרטיס הקופץ

זהו כרטיס צאצא צף של רכיב העוגן שלו .gui-popup-button, עם מיקום מוחלט ועטיפה סמנטית של רשימת הלחצנים.

כלי הבדיקה שמציג את כללי ה-CSS של המחלקה gui-popup

הפעולות המשניות

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

כלי הבדיקה מציג את כללי ה-CSS של רכיב הלחצן.

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

המשתנים הבאים עוזרים ליצור הרמוניה של צבעים ומקום מרכזי לשינוי ערכים שמשמשים בכל הרכיב.

@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --light (prefers-color-scheme: light);

.gui-split-button {
  --theme:             hsl(220 75% 50%);
  --theme-hover:  hsl(220 75% 45%);
  --theme-active:  hsl(220 75% 40%);
  --theme-text:      hsl(220 75% 25%);
  --theme-border: hsl(220 50% 75%);
  --ontheme:         hsl(220 90% 98%);
  --popupbg:         hsl(220 0% 100%);

  --border: 1px solid var(--theme-border);
  --radius: 6px;
  --in-speed: 50ms;
  --out-speed: 300ms;

  @media (--dark) {
    --theme:             hsl(220 50% 60%);
    --theme-hover:  hsl(220 50% 65%);
    --theme-active:  hsl(220 75% 70%);
    --theme-text:      hsl(220 10% 85%);
    --theme-border: hsl(220 20% 70%);
    --ontheme:         hsl(220 90% 5%);
    --popupbg:         hsl(220 10% 30%);
  }
}

פריסות וצבעים

Markup

האלמנט מתחיל כ-<div> עם שם מחלקה מותאם אישית.

<div class="gui-split-button"></div>

מוסיפים את הכפתור הראשי ואת הרכיבים .gui-popup-button.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions"></span>
</div>

שימו לב למאפייני ה-ARIA‏ aria-haspopup ו-aria-expanded. הסימנים האלה חיוניים כדי שקוראי המסך ידעו מה היכולת של חוויית הלחצן המפוצל ומה המצב שלה. המאפיין title שימושי לכולם.

מוסיפים את הסמל <svg> ואת רכיב הקונטיינר .gui-popup.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup"></ul>
  </span>
</div>

למיקום פשוט של חלון קופץ, .gui-popup הוא צאצא של הלחצן שמרחיב אותו. החיסרון היחיד בשיטה הזו הוא שמאגר התגים .gui-split-button לא יכול להשתמש ב-overflow: hidden, כי הוא יגרום לחלון הקופץ להיחתך ולא יוצג.

קוראי מסך יקריאו את התוכן של <ul> מלא בתוכן <li><button> כ'רשימת לחצנים', שזה בדיוק הממשק שמוצג.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup">
      <li>
        <button>Schedule for later</button>
      </li>
      <li>
        <button>Delete</button>
      </li>
      <li>
        <button>Save draft</button>
      </li>
    </ul>
  </span>
</div>

כדי להוסיף קצת סגנון וצבע, הוספתי סמלים ללחצנים המשניים מ-https://heroicons.com. הסמלים הם אופציונליים גם ללחצנים הראשיים וגם ללחצנים המשניים.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup">
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
        </svg>
        Schedule for later
      </button></li>
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
        </svg>
        Delete
      </button></li>
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
        </svg>
        Save draft
      </button></li>
    </ul>
  </span>
</div>

סגנונות

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

עיצוב המאגר של הכפתור המפוצל

סוג התצוגה inline-flex מתאים לרכיב העוטף הזה כי הוא צריך להשתלב בשורה עם כפתורים מפוצלים, פעולות או רכיבים אחרים.

.gui-split-button {
  display: inline-flex;
  border-radius: var(--radius);
  background: var(--theme);
  color: var(--ontheme);
  fill: var(--ontheme);

  touch-action: manipulation;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

הכפתור המפוצל.

הסגנון <button>

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

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

.gui-split-button button {
  cursor: pointer;
  appearance: none;
  background: none;
  border: none;

  display: inline-flex;
  align-items: center;
  gap: 1ch;
  white-space: nowrap;

  font-family: inherit;
  font-size: inherit;
  font-weight: 500;

  padding-block: 1.25ch;
  padding-inline: 2.5ch;

  color: var(--ontheme);
  outline-color: var(--theme);
  outline-offset: -5px;
}

מוסיפים מצבי אינטראקציה באמצעות כמה פסאודו-מחלקות של CSS ושימוש במאפיינים מותאמים אישית תואמים למצב:

.gui-split-button button {
  

  &:is(:hover, :focus-visible) {
    background: var(--theme-hover);
    color: var(--ontheme);

    & > svg {
      stroke: currentColor;
      fill: none;
    }
  }

  &:active {
    background: var(--theme-active);
  }
}

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

.gui-split-button > button {
  border-end-start-radius: var(--radius);
  border-start-start-radius: var(--radius);

  & > svg {
    fill: none;
    stroke: var(--ontheme);
  }
}

ולסיום, כדי להוסיף קצת סטייל, הכפתור והסמל של העיצוב הבהיר מקבלים צל:

.gui-split-button {
  @media (--light) {
    & > button,
    & button:is(:focus-visible, :hover) {
      text-shadow: 0 1px 0 var(--theme-active);
    }
    & > .gui-popup-button > svg,
    & button:is(:focus-visible, :hover) > svg {
      filter: drop-shadow(0 1px 0 var(--theme-active));
    }
  }
}

כפתור טוב הוא כזה ששמים לב בו למיקרו-אינטראקציות ולפרטים הקטנים.

הערה לגבי :focus-visible

שימו לב שסגנונות הלחצנים משתמשים ב-:focus-visible במקום ב-:focus. :focus הוא תוסף חשוב ליצירת ממשק משתמש נגיש, אבל יש לו חיסרון אחד: הוא לא יודע אם המשתמש צריך לראות אותו או לא, והוא יופעל בכל מיקוד.

בסרטון שלמטה מפורטות הפעולות שצריך לבצע כדי להשתמש ב-:focus-visible.

עיצוב הכפתור של החלון הקופץ

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

החלק של החץ בלחצן המפוצל שמשמש להפעלת החלון הקופץ.

.gui-popup-button {
  inline-size: 4ch;
  cursor: pointer;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-inline-start: var(--border);
  border-start-end-radius: var(--radius);
  border-end-end-radius: var(--radius);
}

אפשר להוסיף מצבי ריחוף, מיקוד ופעילות באמצעות CSS Nesting והסלקטור הפונקציונלי :is():

.gui-popup-button {
  

  &:is(:hover,:focus-within) {
    background: var(--theme-hover);
  }

  /* fixes iOS trying to be helpful */
  &:focus {
    outline: none;
  }

  &:active {
    background: var(--theme-active);
  }
}

הסגנונות האלה הם המנגנון העיקרי להצגה ולהסתרה של החלון הקופץ. אם ל-.gui-popup-button יש focus באחד מהצאצאים שלו, מגדירים את opacity, את המיקום ואת pointer-events בסמל ובחלון הקופץ.

.gui-popup-button {
  

  &:focus-within {
    & > svg {
      transition-duration: var(--in-speed);
      transform: rotateZ(.5turn);
    }
    & > .gui-popup {
      transition-duration: var(--in-speed);
      opacity: 1;
      transform: translateY(0);
      pointer-events: auto;
    }
  }
}

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

.gui-popup-button {
  

  @media (--motionOK) {
    & > svg {
      transition: transform var(--out-speed) ease;
    }
    & > .gui-popup {
      transform: translateY(5px);

      transition:
        opacity var(--out-speed) ease,
        transform var(--out-speed) ease;
    }
  }
}

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

עיצוב החלון הקופץ

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

אלמנט כרטיס צף.

.gui-popup {
  --shadow: 220 70% 15%;
  --shadow-strength: 1%;

  opacity: 0;
  pointer-events: none;

  position: absolute;
  bottom: 80%;
  left: -1.5ch;

  list-style-type: none;
  background: var(--popupbg);
  color: var(--theme-text);
  padding-inline: 0;
  padding-block: .5ch;
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  font-size: .9em;
  transition: opacity var(--out-speed) ease;

  box-shadow:
    0 -2px 5px 0 hsl(var(--shadow) / calc(var(--shadow-strength) + 5%)),
    0 1px 1px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 10%)),
    0 2px 2px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 12%)),
    0 5px 5px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 13%)),
    0 9px 9px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 14%)),
    0 16px 16px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 20%))
  ;
}

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

קישורים וסמלים של אפשרויות התשלום: תשלום בקופה, תשלום מהיר ושמירה לקנייה מאוחרת יותר.

.gui-popup {
  

  & svg {
    fill: var(--popupbg);
    stroke: var(--theme);

    @media (prefers-color-scheme: dark) {
      stroke: var(--theme-border);
    }
  }

  & button {
    color: var(--theme-text);
    width: 100%;
  }
}

בפריט הקופץ של העיצוב הכהה נוספו צללים לטקסט ולסמלים, וגם צל לתיבה עם עוצמה קצת יותר גבוהה:

החלון הקופץ בעיצוב הכהה.

.gui-popup {
  

  @media (--dark) {
    --shadow-strength: 5%;
    --shadow: 220 3% 2%;

    & button:not(:focus-visible, :hover) {
      text-shadow: 0 1px 0 var(--ontheme);
    }

    & button:not(:focus-visible, :hover) > svg {
      filter: drop-shadow(0 1px 0 var(--ontheme));
    }
  }
}

סגנונות גנריים של סמלי <svg>

כל הסמלים מותאמים לגודל הכפתור font-size שבו הם משמשים באמצעות יחידת ch כ-inline-size. בנוסף, לכל אחד מהסמלים מוקצים סגנונות מסוימים כדי ליצור קווי מתאר רכים וחלקים.

.gui-split-button svg {
  inline-size: 2ch;
  box-sizing: content-box;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 2px;
}

פריסה מימין לשמאל

מאפיינים לוגיים מבצעים את כל העבודה המורכבת. זו רשימת המאפיינים הלוגיים שבהם נעשה שימוש: - display: inline-flex יוצר רכיב flex מוטבע. ‫– padding-block ו-padding-inline כזוג, במקום הקיצור padding, כדי ליהנות מהיתרונות של הוספת רווחים בצדדים הלוגיים. ‫- border-end-start-radius ו-friends יציגו פינות מעוגלות בהתאם לכיוון המסמך. ‫– inline-size במקום width מוודא שהגודל לא קשור לממדים פיזיים. ‫– border-inline-start מוסיף גבול להתחלה, שיכול להיות בצד ימין או בצד שמאל, בהתאם לכיוון התסריט.

JavaScript

כמעט כל קוד ה-JavaScript הבא נועד לשפר את הנגישות. השתמשתי בשתי ספריות עזר כדי להקל על המשימות. ‫BlingBlingJS משמש לשאילתות DOM תמציתיות ולהגדרה קלה של מאזיני אירועים, ואילו roving-ux עוזר להקל על אינטראקציות נגישות עם מקלדת ועם בקר משחקים בחלון הקופץ.

import $ from 'blingblingjs'
import {rovingIndex} from 'roving-ux'

const splitButtons = $('.gui-split-button')
const popupButtons = $('.gui-popup-button')

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

אינדקס משתנה

כשמקלדת או קורא מסך מתמקדים ב-.gui-popup-button, אנחנו רוצים להעביר את המיקוד ללחצן הראשון (או ללחצן שהמיקוד היה עליו לאחרונה) ב-.gui-popup. הספרייה עוזרת לנו לעשות את זה באמצעות הפרמטרים element ו-target.

popupButtons.forEach(element =>
  rovingIndex({
    element,
    target: 'button',
  }))

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

החלפת המצב של aria-expanded

ברור מבחינה ויזואלית שחלון קופץ מוצג ומוסתר, אבל קורא מסך צריך יותר מרמזים ויזואליים. במקרה הזה, נעשה שימוש ב-JavaScript כדי להשלים את האינטראקציה :focus-within שמבוססת על CSS, על ידי החלפה של מאפיין שמתאים לקורא מסך.

popupButtons.on('focusin', e => {
  e.currentTarget.setAttribute('aria-expanded', true)
})

popupButtons.on('focusout', e => {
  e.currentTarget.setAttribute('aria-expanded', false)
})

הפעלת המקש Escape

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

popupButtons.on('keyup', e => {
  if (e.code === 'Escape')
    e.target.blur()
})

אם לחצן החלון הקופץ מזהה לחיצות על מקש Escape, הוא מסיר את המיקוד מעצמו באמצעות blur().

קליקים על לחצן מפוצל

לבסוף, אם המשתמש לוחץ על הלחצנים, מקיש עליהם או מבצע איתם אינטראקציה באמצעות המקלדת, האפליקציה צריכה לבצע את הפעולה המתאימה. שוב נעשה כאן שימוש ב-Event bubbling, אבל הפעם ב-container .gui-split-button, כדי לתפוס קליקים על לחצנים בחלון קופץ צאצא או בפעולה הראשית.

splitButtons.on('click', event => {
  if (event.target.nodeName !== 'BUTTON') return
  console.info(event.target.innerText)
})

סיכום

עכשיו כשאתה יודע איך עשיתי את זה, איך היית עושה את זה‽ 🙂

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

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