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

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

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

דמו

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

סיכום

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

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

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