بناء مكون انقسام الزر

نظرة عامة أساسية حول كيفية إنشاء مكوّن زر مقسّم يسهل استخدامه

في هذه المشاركة، أريد مشاركة أفكار حول طريقة إنشاء زر مقسّم . تجربة العرض التوضيحي

العرض التوضيحي

إذا كنت تفضّل مشاهدة فيديو، إليك نسخة من هذا المنشور على YouTube:

نظرة عامة

الأزرار المقسّمة هي أزرار تخفي زرًا أساسيًا وقائمة بأزرار إضافية. وهي مفيدة لعرض إجراء شائع مع تضمين إجراءات ثانوية أقل استخدامًا إلى أن تحتاج إليها. يمكن أن يكون الزر المنقسم مهمًا للمساعدة في جعل التصميم المزدحم يبدو بسيطًا. قد يتذكّر زر التقسيم المتقدّم آخر إجراء اتّخذه المستخدم ويضعه في الموضع الأساسي.

يمكن العثور على زر تقسيم شائع في تطبيق البريد الإلكتروني. الإجراء الأساسي هو الإرسال، ولكن يمكنك الإرسال لاحقًا أو حفظ مسودّة بدلاً من ذلك:

مثال على زر مقسّم كما يظهر في تطبيق بريد إلكتروني

من الجيد توفير مساحة الإجراءات المشترَكة، لأنّ المستخدم لا يحتاج إلى البحث في كل مكان. ويدركون أنّ الإجراءات الأساسية المتعلقة بالبريد الإلكتروني مضمّنة في الزر المنقسم.

الأجزاء

دعونا نوضّح الأجزاء الأساسية من الزر المنقسم قبل مناقشة التنسيق العام وتجربة المستخدم النهائية. يتم هنا استخدام أداة فحص إمكانية الوصول في VisBug للمساعدة في عرض نظرة عامة على المكوّن، مع إبراز جوانب HTML والنمط وإمكانية الوصول لكل جزء رئيسي.

عناصر HTML التي تشكّل الزر المنقسم

حاوية زر التقسيم في المستوى الأعلى

المكوّن الأعلى مستوى هو مربع مرن مضمّن، ويحمل الفئة gui-split-button، ويحتوي على الإجراء الأساسي و.gui-popup-button.

فئة gui-split-button التي تم فحصها وعرض خصائص CSS المستخدَمة في هذه الفئة

زر الإجراء الأساسي

يجب أن يتناسب العنصر <button> المرئي والقابل للتركيز في البداية مع الحاوية، وأن يتضمّن شكلَي زاويتين متطابقَين لكل من التفاعلات التركيز والتمرير والنشط، وذلك لكي يظهر العنصر <button> وكأنّه مضمّن في .gui-split-button.

أداة الفحص التي تعرض قواعد CSS لعنصر الزر

زر الإيقاف/التفعيل الخاص بالنافذة المنبثقة

عنصر "زر النافذة المنبثقة" مخصّص لتفعيل قائمة الأزرار الثانوية والإشارة إليها. لاحظ أنّها ليست <button> ولا يمكن التركيز عليها. ومع ذلك، يشكّل هذا العنصر نقطة الارتكاز لتحديد موضع .gui-popup، كما يشكّل المضيف لعنصر :focus-within المستخدَم لعرض النافذة المنبثقة.

تعرض أداة الفحص قواعد CSS الخاصة بفئة gui-popup-button.

البطاقة المنبثقة

هذه بطاقة عائمة تابعة لعنصر الربط .gui-popup-button، ويتم تحديد موضعها بشكل مطلق، كما أنّها تتضمّن بشكل دلالي قائمة الأزرار.

نافذة &quot;أداة الفحص&quot; تعرض قواعد CSS للفئة gui-popup

الإجراءات الثانوية

يحتوي <button> القابل للتركيز<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 عنصرًا تابعًا للزر الذي يوسّعها. الشرط الوحيد لهذه الاستراتيجية هو أنّه لا يمكن لحاوية .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%))
  ;
}

يتم منح الأيقونات والأزرار ألوان العلامة التجارية لتصميمها بشكل جيد ضمن كل بطاقة ذات مظهر داكن وفاتح:

روابط ورموز لإتمام الدفع و&quot;الدفع السريع&quot; و&quot;الحفظ لوقت لاحق&quot;

.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().

النقرات على الزر المقسَّم

أخيرًا، إذا نقر المستخدم أو ضغط أو تفاعل مع الأزرار باستخدام لوحة المفاتيح، يجب أن ينفّذ التطبيق الإجراء المناسب. يتم استخدام ميزة &quot;تسرُّب الأحداث&quot; هنا مرة أخرى، ولكن هذه المرة على الحاوية .gui-split-button، وذلك لرصد نقرات الأزرار من نافذة منبثقة فرعية أو الإجراء الأساسي.

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

الخاتمة

بعد أن عرفت كيف فعلتُ ذلك، كيف ستفعل أنت ذلك؟ 🙂

لنستكشف الطرق المختلفة لإنشاء مواقع إلكترونية على الويب. يمكنك إنشاء عرض توضيحي، إرسال تغريدة إليّ تتضمّن رابطًا إليه، وسأضيفه إلى قسم "ريمكسات من المنتدى" أدناه.

ريمكسات من إنشاء المنتدى