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