نظرة عامة أساسية حول كيفية إنشاء مكوّن زر مُقسَّم سهل الاستخدام
في هذه المشاركة، أريد مشاركة أفكار حول طريقة إنشاء زر مُقسّم . جرِّب العرض التجريبي.
إذا كنت تفضّل مشاهدة الفيديوهات، يمكنك الاطّلاع على نسخة من هذه المشاركة على YouTube:
نظرة عامة
الأزرار المجزّأة هي أزرار تخفي زرًا أساسيًا وقائمة بأزرار إضافية. وهي مفيدة لعرض إجراء شائع مع تداخل إجراءات تكميلية يتم استخدامها بشكلٍ أقل إلى أن يتمّ استخدامها. يمكن أن يكون الزر المُقسَّم عنصرًا مهمًا لجعل التصميم المُشغّل يبدو بسيطًا. قد يتذكر زر التقسيم المتقدّم أيضًا آخر إجراء للمستخدِم ويصعّده إلى الموضع الأساسي.
يمكنك العثور على زر تقسيم شائع في تطبيق البريد الإلكتروني. الإجراء الأساسي هو الإرسال، ولكن قد يكون بإمكانك الإرسال لاحقًا أو حفظ مسودة بدلاً من ذلك:
إنّ منطقة الإجراءات المشترَكة رائعة، لأنّ المستخدم لا يحتاج إلى البحث في جميع الاتجاهات. ويعلمون أنّ الإجراءات الأساسية للبريد الإلكتروني مضمّنة في زرّ التقسيم.
الأجزاء
لنلقِ نظرة على الأجزاء الأساسية للزر المُقسّم قبل مناقشة عملية التنسيق الشاملة وتجربة المستخدم النهائي. يتم استخدام أداة فحص إمكانية الوصول في VisBug للمساعدة في عرض عرض إجمالي للمكوّن، مع إبراز جوانب HTML والنمط وإمكانية الوصول لكل جزء رئيسي.
حاوية الزر المُقسَّم على مستوى أعلى
العنصر الأعلى مستوى هو مربّع مرن مضمّن، وله فئة
gui-split-button
، ويحتوي على الإجراء الأساسي
و.gui-popup-button
.
زر الإجراء الأساسي
يتلاءم العنصر <button>
المرئي في البداية والذي يمكن التركيز عليه مع الحاوية مع
شكلَين متطابقَين للزوايا من أجل
التركيز والمرّر فوقه والتفاعلات النشطة ليظهر
ضمن .gui-split-button
.
زر إيقاف/تفعيل النافذة المنبثقة
يُستخدَم عنصر الدعم "زر النافذة المنبثقة" لتفعيل قائمة buttons
الثانوية والإشارة إليها. يُرجى ملاحظة أنّه ليس <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>
نفسه على أنّه "قائمة buttons" لبرامج قراءة الشاشة، وهي الواجهة التي يتم عرضها بالضبط.
<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
صندوق مرن لتوسط رمز وتثبيت قائمة أزرار منبثقة مثل الزر الأساسي، يكون هذا الزر شفافًا إلى أن يتم تمرير مؤشر الماوس فوقه أو التفاعل معه، ويتم تمديده لملء المساحة.
.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
و
الأصدقاء:
ستتم إزالة الزوايا الحادة استنادًا إلى اتجاه المستند.
- يضمن استخدام 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)
})
الخاتمة
الآن بعد أن عرفت كيف فعلت ذلك، كيف ستفعل ذلك؟ 🙂
لننوّع أساليبنا ونتعرّف على جميع الطرق لإنشاء تطبيقات على الويب. أنشئ عرضًا توضيحيًا وأرسِل إلينا رابطًا على Twitter، وسنضيفه إلى قسم الريمكسات التي أنشأها المستخدمون أدناه.
الريمكسات التي أنشأها المستخدمون
- Codepen من إنشاء Joost van der Schee