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

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

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

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

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

نظرة عامة

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

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

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

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

الأجزاء

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

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

حاوية الزر المُقسَّم على مستوى أعلى

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

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

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

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

المفتش الذي يعرض قواعد CSS لعنصر الزر

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

يُستخدَم عنصر الدعم "زر النافذة المنبثقة" لتفعيل قائمة buttons الثانوية والإشارة إليها. يُرجى ملاحظة أنّه ليس <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 عنصرًا فرعيًا للزر الذي يوسيّعه. العائق الوحيد في هذه الاستراتيجية هو أنّ حاوية .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%))
  ;
}

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

روابط ورموز لعمليات الدفع والدفع السريع و&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().

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

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

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

الخاتمة

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

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

الريمكسات التي أنشأها المستخدمون