סקירה כללית בסיסית של אופן יצירת רכיב נגיש של לחצן מפוצל.
בפוסט הזה אני רוצה לשתף מחשבות על דרך לבנות לחצן מפוצל . רוצים לנסות את ההדגמה?
אם אתם מעדיפים לצפות בסרטון, הנה גרסת YouTube של הפוסט הזה:
סקירה כללית
לחצנים מפוצלים הם לחצנים שמסתירים לחצן ראשי ורשימה של לחצנים נוספים. הם שימושיים להצגת פעולה נפוצה, תוך הסתרת פעולות משניות שמשתמשים בהן לעיתים רחוקות יותר עד שצריך אותן. כפתור מפוצל יכול להיות חיוני כדי לעזור לעיצוב עמוס להיראות מינימליסטי. יכול להיות שגם לחצן מפוצל מתקדם יזכור את הפעולה האחרונה של המשתמש ויקדם אותה למיקום הראשי.
לחצן פיצול נפוץ נמצא באפליקציית האימייל. הפעולה העיקרית היא שליחה, אבל אפשר גם לשלוח מאוחר יותר או לשמור טיוטה:
אזור הפעולות המשותף נוח, כי המשתמש לא צריך לחפש. הם יודעים שפעולות חיוניות שקשורות לאימייל נמצאות בכפתור המפוצל.
חלקים
לפני שנדון בתיאום הכולל של לחצנים מפוצלים ובחוויית המשתמש הסופית, נפרט את החלקים החיוניים שלהם. כאן נעשה שימוש בכלי לבדיקת נגישות של VisBug כדי להציג תצוגה כללית של הרכיב, עם היבטים של HTML, סגנון ונגישות לכל חלק מרכזי.
מאגר של לחצן מפוצל ברמה העליונה
הרכיב ברמה העליונה הוא תיבת flexbox מוטמעת, עם מחלקה של
gui-split-button
, שמכילה את הפעולה הראשית
ואת .gui-popup-button
.
לחצן הפעולה הראשי
האלמנט <button>
שגלוי בהתחלה וניתן להעברה אליו של המיקוד, נכנס לתוך הקונטיינר עם שני צורות פינתיות תואמות עבור אינטראקציות של מיקוד, ריחוף ופעילות, כך שהן מופיעות בתוך .gui-split-button
.
הכפתור להחלפת המצב של החלון הקופץ
רכיב התמיכה popup button (לחצן קופץ) מיועד להפעלה ולרמז לרשימת הלחצנים המשניים. שימו לב שזה לא <button>
ואי אפשר להתמקד בו. עם זאת, הוא משמש כנקודת עיגון למיקום של .gui-popup
ומארח את :focus-within
שמשמש להצגת החלון הקופץ.
הכרטיס הקופץ
זהו כרטיס צאצא צף של רכיב העוגן שלו .gui-popup-button
, עם מיקום מוחלט ועטיפה סמנטית של רשימת הלחצנים.
הפעולות המשניות
<button>
לחצן שאפשר להתמקד בו לפעולה הראשית, עם גודל גופן קטן יותר, כולל סמל וסגנון משלים ללחצן הראשי.
מאפיינים מותאמים אישית
המשתנים הבאים עוזרים ליצור הרמוניה של צבעים ומקום מרכזי לשינוי ערכים שמשמשים בכל הרכיב.
@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)
})
סיכום
עכשיו כשאתה יודע איך עשיתי את זה, איך היית עושה את זה‽ 🙂
כדאי לגוון את הגישות שלנו וללמוד את כל הדרכים לבנות אתרים. אפשר ליצור סרטון הדגמה, לצייץ לי קישורים, ואוסיף אותו לקטע של רמיקסים מהקהילה שבהמשך!