إنشاء أحد مكونات المبدل

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

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

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

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

نظرة عامة

يعمل مفتاح التبديل بشكل مشابه لمربّع الاختيار، ولكنه يمثّل بشكل صريح الحالتَين المنطقيتَين "مفعّل" و"غير مفعّل".

يستخدم هذا العرض الترويجي <input type="checkbox" role="switch"> لمعظم وظائفه، ما يمنح ميزة عدم الحاجة إلى CSS أو JavaScript ليعمل بشكلٍ كامل ويكون متاحًا للجميع. يتيح تحميل CSS استخدام اللغات من اليمين إلى اليسار والعرض العمودي والصور المتحركة وغير ذلك. يؤدي تحميل JavaScript إلى جعل مفتاح التبديل قابلاً للتحريك واللمس.

الخصائص المخصّصة

تمثّل المتغيّرات التالية الأجزاء المختلفة للتبديل وخياراتها. بصفتها فئة المستوى الأعلى، تحتوي .gui-switch على خصائص مخصّصة يتم استخدامها في جميع العناصر الفرعية للمكونات، ونقاط دخول للقيام بعملية تخصيص مركزية.

مسار

الطول (--track-size) والتباعد واللونان:

.gui-switch {
  --track-size: calc(var(--thumb-size) * 2);
  --track-padding: 2px;

  --track-inactive: hsl(80 0% 80%);
  --track-active: hsl(80 60% 45%);

  --track-color-inactive: var(--track-inactive);
  --track-color-active: var(--track-active);

  @media (prefers-color-scheme: dark) {
    --track-inactive: hsl(80 0% 35%);
    --track-active: hsl(80 60% 60%);
  }
}

صورة مصغرة

حجم التفاعل ولونه ولون الخلفية:

.gui-switch {
  --thumb-size: 2rem;
  --thumb: hsl(0 0% 100%);
  --thumb-highlight: hsl(0 0% 0% / 25%);

  --thumb-color: var(--thumb);
  --thumb-color-highlight: var(--thumb-highlight);

  @media (prefers-color-scheme: dark) {
    --thumb: hsl(0 0% 5%);
    --thumb-highlight: hsl(0 0% 100% / 25%);
  }
}

الحدّ من الحركة

لإضافة عنوان بديل واضح وتقليل التكرار، يمكن وضع طلب ملف شخصي وسائط يفضّل استخدام الصور ذات الحركة المنخفضة في موقع مخصّص باستخدام مكوّن PostCSS الإضافي استنادًا إلى مسودة المواصفات في طلبات ملفات الوسائط 5:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

Markup

اختَرتُ لفّ عنصر <input type="checkbox" role="switch"> بعنصر <label>، ما يجمع علاقتهما لتجنُّب غموض ربط مربّع الاختيار بالتصنيف، مع منح المستخدم إمكانية التفاعل مع التصنيف لقلب الإدخال.


تصنيف ومربّع اختيار طبيعيان بدون تنسيق

<label for="switch" class="gui-switch">
  Label text
  <input type="checkbox" role="switch" id="switch">
</label>

<input type="checkbox"> مُعدّ مسبقًا باستخدام واجهة برمجة تطبيقات وحالة. يدير المتصفّح checked الخاصية وأحداث الإدخال مثل oninputوonchanged.

التنسيقات

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

.gui-switch

تنسيق المستوى الأعلى للتبديل هو flexbox. تحتوي الفئة .gui-switch على السمات المخصّصة الخاصة والعامة التي يستخدمها الأطفال لاحتساب التنسيقات.

أدوات تطوير Flexbox التي تتراكب على تصنيف وتبديل أفقيَين، وتُظهر تنسيقهما
توزيع المساحة

.gui-switch {
  display: flex;
  align-items: center;
  gap: 2ch;
  justify-content: space-between;
}

إنّ توسيع تنسيق المربّع المرن وتعديله يشبه تغيير أي تنسيق مربّع مرن. على سبيل المثال، لوضع تصنيفات فوق مفتاح تبديل أو تحته، أو لتغيير flex-direction:

أدوات مطوّري برامج Flexbox التي تتراكب على تصنيف وتبديل عموديَين

<label for="light-switch" class="gui-switch" style="flex-direction: column">
  Default
  <input type="checkbox" role="switch" id="light-switch">
</label>

مسار

تمّ تصميم إدخال مربّع الاختيار على أنّه مسار تبديل من خلال إزالة appearance: checkbox العادي وتقديم حجمه الخاص بدلاً من ذلك:

أدوات تطوير الشبكة التي تتراكب على مسار التبديل، وتعرض مناطق مسار الشبكة المُعنوَنة باسم &quot;مسار&quot;

.gui-switch > input {
  appearance: none;

  inline-size: var(--track-size);
  block-size: var(--thumb-size);
  padding: var(--track-padding);

  flex-shrink: 0;
  display: grid;
  align-items: center;
  grid: [track] 1fr / [track] 1fr;
}

ينشئ المقطع الصوتي أيضًا منطقة مسار شبكة خلية واحدة لكل إصبع لتحديده بهدف المطالبة به.

صورة مصغرة

يزيل النمط appearance: none أيضًا علامة الاختيار المرئية التي يوفّرها browser. يستخدم هذا المكوّن عنصرًا زائفًا و:checked الفئة الزائفة في الإدخال بهدف استبدال هذا المؤشر المرئي.

مؤشر التمرير هو عنصر زائف تابع لعنصر input[type="checkbox"] ويتم تجميعه فوق المقطع الصوتي بدلاً من تحته من خلال الاستيلاء على مساحة الشبكة track:

أدوات المطوّر تعرِض إصبع الإبهام للعنصر الصوري كما هو موضَّح داخل شبكة CSS.

.gui-switch > input::before {
  content: "";
  grid-area: track;
  inline-size: var(--thumb-size);
  block-size: var(--thumb-size);
}

الأنماط

تتيح السمات المخصّصة مكوّن تبديل متعدد الاستخدامات يتكيّف مع أنظمة الألوان واللغات من اليمين إلى اليسار والإعدادات المفضّلة للحركة.

مقارنة جنبًا إلى جنب بين المظهرَين الفاتح والداكن للتبديل وحالاته

أنماط التفاعل باللمس

على الأجهزة الجوّالة، تضيف المتصفّحات ميزات تمييز النقرات واختيار النص إلى التصنيفات والمدخلات. وقد أثّرت هذه التغييرات سلبًا في ملاحظات التفاعل المرئي والأسلوب التي كانت تحتاج إليها عملية التبديل هذه. باستخدام بضعة أسطر من CSS، يمكنني إزالة هذه التأثيرات وإضافة أسلوب cursor: pointer الخاص بي:

.gui-switch {
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

لا يُنصح دائمًا بإزالة هذه الأنماط، لأنّها قد توفّر ملاحظات قيّمة بشأن التفاعل البصري. احرص على تقديم بدائل مخصّصة في حال إزالتها.

مسار

ترتبط أنماط هذا العنصر بشكله ولونه في أغلب الأحيان، ويحصل عليها من العنصر الرئيسي .gui-switch من خلال التسلسل.

خيارات التبديل مع أحجام مسارات وألوان مخصّصة

.gui-switch > input {
  appearance: none;
  border: none;
  outline-offset: 5px;
  box-sizing: content-box;

  padding: var(--track-padding);
  background: var(--track-color-inactive);
  inline-size: var(--track-size);
  block-size: var(--thumb-size);
  border-radius: var(--track-size);
}

تأتي مجموعة كبيرة من خيارات التخصيص لمسار التبديل من أربع خصائص مخصّصة. تتم إضافة border: none لأنّ appearance: none لا يزيل الحدود من مربّع الاختيار في جميع المتصفّحات.

صورة مصغرة

عنصر الإبهام موجود على اليمين track ولكنّه يحتاج إلى أنماط الدوائر:

.gui-switch > input::before {
  background: var(--thumb-color);
  border-radius: 50%;
}

تظهر &quot;أدوات مطوّري البرامج&quot; مميّزة العنصر النائب للدائرة.

التفاعل

استخدِم السمات المخصّصة للاستعداد للتفاعلات التي ستُظهر تمييزات عند التمرير فوقها وتغييرات موضع إصبعَي الإبهام. يتم أيضًا التحقّق من إعدادات المستخدم المفضّلة قبل الانتقال إلى انماط تمييز العناصر أثناء التمرير أو أثناء الحركة.

.gui-switch > input::before {
  box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);

  @media (--motionOK) { & {
    transition:
      transform var(--thumb-transition-duration) ease,
      box-shadow .25s ease;
  }}
}

موضع الإبهام

توفّر السمات المخصّصة آلية مصدر واحدة لوضع إصبع الماوس في المقطع الصوتي. يتوفّر لدينا أحجام المقطع الصوتي والملصق التي سنستخدمها في الحسابات للحفاظ على وضع الملصق بشكل صحيح بين الإطارات ضمن المقطع الصوتي: 0% و100%.

يملك العنصر input متغيّر الموضع --thumb-position، ويستخدمه العنصر النائب thumb كقيمة لموضع translateX:

.gui-switch > input {
  --thumb-position: 0%;
}

.gui-switch > input::before {
  transform: translateX(var(--thumb-position));
}

أصبح بإمكاننا الآن تغيير --thumb-position من CSS والفئات الصورية المتوفّرة في عناصر مربّعات الاختيار. بما أنّنا ضبطنا transition: transform var(--thumb-transition-duration) ease بشكل مشروط في وقت سابق على هذا العنصر، قد تؤدي هذه التغييرات إلى ظهور صورة متحركة عند تغييرها:

/* positioned at the end of the track: track length - 100% (thumb width) */
.gui-switch > input:checked {
  --thumb-position: calc(var(--track-size) - 100%);
}

/* positioned in the center of the track: half the track - half the thumb */
.gui-switch > input:indeterminate {
  --thumb-position: calc(
    (var(--track-size) / 2) - (var(--thumb-size) / 2)
  );
}

أعتقد أنّ عملية التنسيق هذه التي تم فصلها عن بعضها قد نجحت بشكل جيد. لا يهتم عنصر Thumb سوى بنمط واحد، وهو موضع translateX. يمكن أن تعالج المدخلات كل الصعوبات والحسابات.

موضع الإعلان

تم إجراء عملية التحسين باستخدام فئة معدِّل -vertical التي تضيف دورانًا باستخدام عمليات تحويل CSS إلى عنصر input.

ومع ذلك، لا يؤدي العنصر المُدار بتقنية 3D إلى تغيير الارتفاع الإجمالي للمكوّن، مما قد يؤدي إلى تغيير تنسيق الكتلة. يمكنك مراعاة ذلك باستخدام المتغيّرين --track-size و --track-padding. احتسِب الحد الأدنى للمساحة المطلوبة لترتيب زر عمودي في التنسيق على النحو المتوقّع:

.gui-switch.-vertical {
  min-block-size: calc(var(--track-size) + calc(var(--track-padding) * 2));

  & > input {
    transform: rotate(-90deg);
  }
}

(RTL) من اليمين إلى اليسار

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

.gui-switch {
  --isLTR: 1;

  &:dir(rtl) {
    --isLTR: -1;
  }
}

تحتوي الخاصية المخصّصة التي تُسمّى --isLTR في البداية على القيمة 1، ما يعني أنّها true لأنّ تنسيقنا من اليمين إلى اليسار تلقائيًا. بعد ذلك، باستخدام فئة CSS المزوّدة بقيمة زائفة :dir()، يتم ضبط القيمة على -1 عندما يكون المكوّن ضمن تنسيق من اليمين إلى اليسار.

يمكنك استخدام --isLTR من خلال تضمينها في calc() ضمن عملية تحويل:

.gui-switch.-vertical > input {
  transform: rotate(-90deg);
  transform: rotate(calc(90deg * var(--isLTR) * -1));
}

يراعي الآن دوران مفتاح التبديل العمودي موضع الجانب المقابل الذي يتطلّبه التنسيق من اليمين إلى اليسار.

يجب أيضًا تعديل عمليات التحويل translateX في العنصر النائب للإصبع للتمكّن من مراعاة متطلبات الجانب المقابل:

.gui-switch > input:checked {
  --thumb-position: calc(var(--track-size) - 100%);
  --thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}

.gui-switch > input:indeterminate {
  --thumb-position: calc(
    (var(--track-size) / 2) - (var(--thumb-size) / 2)
  );
  --thumb-position: calc(
   ((var(--track-size) / 2) - (var(--thumb-size) / 2))
    * var(--isLTR)
  );
}

على الرغم من أنّ هذا النهج لن يحلّ جميع الاحتياجات المتعلقة بمفهوم مثل عمليات تحويل CSS المنطقية، إلا أنّه يقدّم بعض مبادئ DRY للعديد من حالات الاستخدام.

الولايات

لا يكتمل استخدام input[type="checkbox"] المضمّن بدون معالجة الحالات المختلفة التي يمكن أن يكون فيها: :checked و:disabled :indeterminate و:hover. تمّ عمدًا عدم إجراء أي تعديل على :focus، باستثناء تعديل التنسيق، وظهرت حلقة التركيز بشكل رائع على Firefox و Safari:

لقطة شاشة لحلقة التركيز التي تركّز على مفتاح تبديل في Firefox وSafari

محدد

<label for="switch-checked" class="gui-switch">
  Default
  <input type="checkbox" role="switch" id="switch-checked" checked="true">
</label>

تمثّل هذه الحالة حالة on. في هذه الحالة، يتم ضبط خلفية الإدخال "المسار" على اللون النشط ويتم ضبط موضع شريط التمرير على " النهاية".

.gui-switch > input:checked {
  background: var(--track-color-active);
  --thumb-position: calc((var(--track-size) - 100%) * var(--isLTR));
}

غير مفعّل

<label for="switch-disabled" class="gui-switch">
  Default
  <input type="checkbox" role="switch" id="switch-disabled" disabled="true">
</label>

لا يختلف زر :disabled فقط من الناحية المرئية، بل يجب أن يجعل العنصر غير قابل للتغيير.لا يعتمد عدم قابلية التفاعل للتغيير على المتصفّح، ولكن تحتاج الحالات المرئية إلى أنماط بسبب استخدام appearance: none.

.gui-switch > input:disabled {
  cursor: not-allowed;
  --thumb-color: transparent;

  &::before {
    cursor: not-allowed;
    box-shadow: inset 0 0 0 2px hsl(0 0% 100% / 50%);

    @media (prefers-color-scheme: dark) { & {
      box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 50%);
    }}
  }
}

مفتاح التبديل بتصميم داكن في حالات الإيقاف والاختيار وعدم الاختيار

هذه الحالة صعبة لأنّها تحتاج إلى مظهرَين داكن وفاتح مع حالتَي إيقاف و تفعيل. لقد اخترتُ أنماطًا بسيطة لهذه الحالات لتخفيف عبء صيانة مجموعات الأنماط.

غير محدَّد

إنّ الحالة :indeterminate التي غالبًا ما يتم تجاهلها هي الحالة التي لا يكون فيها مربّع الاختيار محدَّدًا أو غير محدَّد. هذه حالة ممتعة، وهي أسلوب ترحيب غير متكلف. نذكّرك بأنّه يمكن أن تتضمّن الحالات المنطقية حالات غامضة بين الحالات.

من الصعب ضبط مربّع اختيار على "غير محدّد"، ولا يمكن ضبطه إلا باستخدام JavaScript:

<label for="switch-indeterminate" class="gui-switch">
  Indeterminate
  <input type="checkbox" role="switch" id="switch-indeterminate">
  <script>document.getElementById('switch-indeterminate').indeterminate = true</script>
</label>

الحالة غير المحدّدة التي تظهر فيها صورة مصغّرة للأغنية في
الوسط للإشارة إلى عدم اتّخاذ قرار

بما أنّ الحالة تبدو لي متواضعة وداعمة، رأيت أنّه من المناسب وضع موضع إصبع الإبهام للتبديل في المنتصف:

.gui-switch > input:indeterminate {
  --thumb-position: calc(
    calc(calc(var(--track-size) / 2) - calc(var(--thumb-size) / 2))
    * var(--isLTR)
  );
}

تمرير مؤشر الماوس

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

يتم تطبيق تأثير "التمييز" باستخدام box-shadow. عند تمرير مؤشر الماوس فوق حقل إدخال غير مُعطَّل، يجب زيادة حجم --highlight-size. إذا كان المستخدم لا يمانع استخدام الصور المتحركة، ننقل الرمز box-shadow ونراقب نموه. وإذا كان المستخدم لا يوافق على استخدام الصور المتحركة، يظهر التمييز على الفور:

.gui-switch > input::before {
  box-shadow: 0 0 0 var(--highlight-size) var(--thumb-color-highlight);

  @media (--motionOK) { & {
    transition:
      transform var(--thumb-transition-duration) ease,
      box-shadow .25s ease;
  }}
}

.gui-switch > input:not(:disabled):hover::before {
  --highlight-size: .5rem;
}

JavaScript

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

الإبهام القابل للسحب

يتلقّى العنصر النائب للمؤشر موضعَه من .gui-switch > input var(--thumb-position) الذي يحدّد النطاق، ويمكن أن يوفّر JavaScript قيمة نمط مضمّنة في الإدخال لتعديل موضع المؤشر ديناميكيًا بحيث يبدو أنّه يتّبع إيماءة المؤشر. عند إزالة المؤشر، أزِل الأنماط المضمّنة وحدد ما إذا كان السحب أقرب إلى إيقاف أو تشغيل باستخدام السمة المخصّصة --thumb-position. هذا هو العمود الفقري للحلّ، وهو أحداث المؤشر التي تتبّع بشكل مشروط مواضع المؤشر لتعديل السمات المخصّصة لـ CSS.

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

touch-action

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

يوجّه ملف CSS التالي المتصفّح إلى أنّه عند بدء إيماءة مؤشر من داخل مسار التبديل هذا، يجب معالجة الإيماءات الرأسية وعدم اتّخاذ أي إجراء بشأن الإيماءات الأفقية:

.gui-switch > input {
  touch-action: pan-y;
}

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

أدوات تنسيق قيم وحدات البكسل

عند الإعداد وأثناء السحب، يجب الحصول على قيم أرقام مختلفة تم احتسابها من العناصر. تعرض دوال JavaScript التالية قيمًا محسوبة للبكسل استنادًا إلى سمة CSS. ويتم استخدامه في نص الإعداد على النحو التالي: getStyle(checkbox, 'padding-left').

​​const getStyle = (element, prop) => {
  return parseInt(window.getComputedStyle(element).getPropertyValue(prop));
}

const getPseudoStyle = (element, prop) => {
  return parseInt(window.getComputedStyle(element, ':before').getPropertyValue(prop));
}

export {
  getStyle,
  getPseudoStyle,
}

لاحظ أنّ window.getComputedStyle() تقبل مَعلمة ثانية، وهي عنصر زائف للهدف. من الرائع أنّ JavaScript يمكنه قراءة العديد من القيم من العناصر، حتى من العناصر الزائفة.

dragging

هذه لحظة أساسية لمنطق السحب، وهناك بعض الأمور التي يجب ملاحظتها من معالِج أحداث الدالة:

const dragging = event => {
  if (!state.activethumb) return

  let {thumbsize, bounds, padding} = switches.get(state.activethumb.parentElement)
  let directionality = getStyle(state.activethumb, '--isLTR')

  let track = (directionality === -1)
    ? (state.activethumb.clientWidth * -1) + thumbsize + padding
    : 0

  let pos = Math.round(event.offsetX - thumbsize / 2)

  if (pos < bounds.lower) pos = 0
  if (pos > bounds.upper) pos = bounds.upper

  state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)
}

عنصر النص البرمجي الرئيسي هو state.activethumb، وهي الدائرة الصغيرة التي يحدد موقعها النص البرمجي مع مؤشر. عنصر switches هو Map() حيث تكون المفاتيح من النوع .gui-switch والقيم هي الحدود والأحجام المخزّنة مؤقتًا التي تحافظ على فعالية النص البرمجي. يتم التعامل مع النص من اليمين إلى اليسار باستخدام السمة المخصّصة نفسها التي تستخدمها CSS وهي --isLTR، ويمكن استخدامها لعكس المنطق ومواصلة إتاحة النص من اليمين إلى اليسار. يُعدّ event.offsetX مفيدًا أيضًا، لأنّه يحتوي على قيمة فرقة مفيدة لتحديد موضع الإبهام.

state.activethumb.style.setProperty('--thumb-position', `${track + pos}px`)

يضبط هذا السطر الأخير من CSS السمة المخصّصة المستخدَمة من قِبل عنصر Thumb. كان من الممكن أن يتمّ تبديل عملية تحديد القيمة هذه بمرور الوقت، ولكنّ حدث مؤشر سابق حدد مؤقتًا --thumb-transition-duration على 0s، ما أدى إلى إزالة ما كان يمكن أن يكون تفاعلًا بطيئًا.

dragEnd

للسماح للمستخدم بسحب العنصر بعيدًا خارج مفتاح التبديل ثم تركه، يجب تسجيل حدث نافذة عام:

window.addEventListener('pointerup', event => {
  if (!state.activethumb) return

  dragEnd(event)
})

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

const dragEnd = event => {
  if (!state.activethumb) return

  state.activethumb.checked = determineChecked()

  if (state.activethumb.indeterminate)
    state.activethumb.indeterminate = false

  state.activethumb.style.removeProperty('--thumb-transition-duration')
  state.activethumb.style.removeProperty('--thumb-position')
  state.activethumb.removeEventListener('pointermove', dragging)
  state.activethumb = null

  padRelease()
}

اكتمل التفاعل مع العنصر، وحان وقت ضبط سمة الإدخال التي تم وضع علامة عليها وإزالة جميع أحداث الإيماءات. تم تغيير مربّع الاختيار باستخدام state.activethumb.checked = determineChecked().

determineChecked()

تحدّد هذه الدالة التي تستدعيها dragEnd موضع إصبع الإبهام الحالي ضمن حدود مساره وتعرض القيمة true إذا كان يساوي نصف المسار أو أكثر:

const determineChecked = () => {
  let {bounds} = switches.get(state.activethumb.parentElement)

  let curpos =
    Math.abs(
      parseInt(
        state.activethumb.style.getPropertyValue('--thumb-position')))

  if (!curpos) {
    curpos = state.activethumb.checked
      ? bounds.lower
      : bounds.upper
  }

  return curpos >= bounds.middle
}

ملاحظات إضافية

أدّت إيماءة السحب إلى بعض المشاكل في الرمز البرمجي بسبب بنية HTML الأولية التي تم اختيارها، ولعلّ أبرزها تضمين الإدخال في تصنيف. سيتلقّى التصنيف، بصفته عنصرًا родительским ، تفاعلات النقر بعد الإدخال. في نهاية حدث dragEnd، ربما لاحظت أنّ padRelease() هي دالّة تبدو غريبة.

const padRelease = () => {
  state.recentlyDragged = true

  setTimeout(_ => {
    state.recentlyDragged = false
  }, 300)
}

يُعدّ ذلك لتوضيح أنّ التصنيف يتلقّى هذا النقر لاحقًا، لأنّه سيلغي وضع علامة في المربّع بجانب التفاعل الذي أجراه المستخدِم أو سيضع علامة فيه.

إذا أردت إجراء ذلك مرة أخرى، قد أفكر في تعديل DOM باستخدام JavaScript أثناء ترقية تجربة المستخدم، وذلك لإنشاء عنصر يعالج النقرات على التصنيفات بنفسه ولا يتعارض مع السلوك المضمّن.

إنّ هذا النوع من JavaScript هو الأقلّ تفضيلًا بالنسبة إليّ، ولا أريد إدارة تدفّق الأحداث الشَرطية:

const preventBubbles = event => {
  if (state.recentlyDragged)
    event.preventDefault() && event.stopPropagation()
}

الخاتمة

لقد كان مكوّن التبديل الصغير هذا هو الأكثر استهلاكًا للوقت من بين جميع تحدّيات واجهتها في واجهة المستخدم حتى الآن. الآن بعد أن عرفت كيف فعلت ذلك، كيف ستفعل ذلك؟ 🙂

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

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

الموارد

يمكنك العثور على .gui-switch الرمز المصدر على GitHub.