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

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

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

דמו

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

סקירה כללית

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

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

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

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

חלקים

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

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

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

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

בדיקה של המחלקה gui-split-button ונכסי ה-CSS שבהם נעשה שימוש במחלקה הזו.

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

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

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

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

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

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

הכרטיס הקופץ

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

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

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

לחצן <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 הוא צאצא של הלחצן שמרחיב אותו. החיסרון היחיד באסטרטגיה הזו הוא שאי אפשר להשתמש ב-overflow: hidden בקונטיינר .gui-split-button, כי הוא יקפיץ את חלון הקופץ כך שלא יהיה גלוי.

<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 והסלקטור הפונקציונלי :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 יוצר רכיב גמיש מוטבע. - 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().

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

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

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

סיכום

עכשיו, אחרי שהסברתי איך עשיתי את זה, איך היית? 🙂

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

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