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

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

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

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

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

نظرة عامة

يعمل switch بشكل مشابه لمربع اختيار، ولكنه يمثل بشكل صريح حالات التشغيل والإيقاف المنطقي.

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

.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، ويستخدمه عنصر الإبهام الزائف كموضع 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)
  );
}

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

موضع الإعلان

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

بالرغم من ذلك، لا يؤدي العنصر الذي يتم تدويره ثلاثي الأبعاد إلى تغيير الارتفاع الإجمالي للمكون، مما قد يؤدي إلى اختفاء تخطيط الكتلة. يمكنك مراعاة ذلك باستخدام المتغيّرين --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.

ونظرًا لأن المكون كان يعمل بنسبة 100% قبل عرض هذا النص البرمجي، يتطلب الأمر الكثير من العمل للحفاظ على السلوك الحالي، مثل النقر على تصنيف لتبديل الإدخال. يجب ألا تضيف 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()
}

الخاتمة

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

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

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

الموارد

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