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

نظرة عامة أساسية حول كيفية بناء مكون مجزأ يمكن الوصول إليه.

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

إصدار تجريبي

في ما يلي إصدار YouTube من هذه المشاركة إذا كنت تفضّل الفيديوهات:

نظرة عامة

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

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

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

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

قطع الغيار

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

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

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

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

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

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

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

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

زر تبديل النافذة المنبثقة

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

الخلاصة

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

دعونا ننويع أساليبنا ونتعلم جميع طرق الإنشاء على الويب. يمكنك إنشاء عرض توضيحي وروابط تغريدات لي وسنضيفها إلى قسم الريمكسات في المنتدى أدناه.

ريمكسات من المنتدى