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

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

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

הדגמה

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

סקירה כללית

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

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

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

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

חלקים

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

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

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

הרכיב ברמה הגבוהה ביותר הוא flexbox מוטבע, בסיווג gui-split-button, שמכיל את הפעולה הראשית ואת .gui-popup-button.

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

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

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

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

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

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

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

הכרטיס הקופץ

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

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

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

<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 היא חלופה חכמה.

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

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

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

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

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

קישורים וסמלים לתשלום, ל-Quick Pay ול&#39;שמירה למועד מאוחר יותר&#39;.

.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 יוצרת רכיב גמיש בתוך השורה. - padding-block ו-padding-inline כצמד, במקום להזין padding כקיצור דרך, יש לכם את היתרונות של ריפוד בין הצדדים הלוגיים. - border-end-start-radius וחברים יעגלו פינות בהתאם לכיוון המסמך. - 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().

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

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

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

סיכום

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

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

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