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

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

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

عرض توضيحي

إذا كنت تفضّل الفيديو، يمكنك الاطّلاع على نسخة من هذه المشاركة على 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> سيُعلن عن نفسه على أنّه "قائمة أزرار" لبرامج قراءة الشاشة، أي الواجهة التي يتم عرضها تحديدًا.

<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')

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

مؤشر التنقّل

عندما تركّز لوحة مفاتيح أو قارئ شاشة على .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، وسنضيفه إلى قسم الريمكسات التي أنشأها المستخدمون أدناه.

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