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

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

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

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

إذا كنت تفضّل مشاهدة فيديو، إليك نسخة من هذا المنشور على 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 استنادًا إلى مسودة المواصفات في Media Queries 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 DevTools على تصنيف أفقي ومفتاح تبديل، مع عرض توزيع المساحة في تخطيطهما

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

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

تراكب Flexbox DevTools على تصنيف عمودي ومفتاح تبديل

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

مسار

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

تراكب Grid DevTools على مسار التبديل، يعرض مناطق مسار الشبكة المسماة بالاسم &quot;track&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;
}

ينشئ المسار أيضًا مساحة مسار شبكة من خلية واحدة بحجم 1×1 يمكن فيها المطالبة بإبهام.

صورة مصغرة

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

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

أداة DevTools تعرض صورة مصغّرة للعنصر الصوري في موضع داخل شبكة 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; مع تمييز العنصر الزائف &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)
  );
}

أعتقد أنّ عملية التنسيق المنفصلة هذه كانت ناجحة. يهتم عنصر الإبهام بنمط واحد فقط، وهو موضع 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، وقد لا تتوفّر أبدًا. راودت &quot;إيلاد&quot; فكرة رائعة باستخدام قيمة سمة مخصّصة لعكس النسب المئوية، وذلك للسماح بإدارة موقع جغرافي واحد لمنطقنا المخصّص الخاص بالتحويلات المنطقية. استخدمتُ التقنية نفسها في هذا التبديل وأعتقد أنّها كانت ناجحة:

.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 المخصّصة.

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

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-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()
}

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

determineChecked()

تحدّد هذه الدالة، التي يتم استدعاؤها من خلال dragEnd، الموضع الحالي للإبهام ضمن حدود مساره وتعرض القيمة "صحيح" إذا كان الموضع مساويًا لنصف المسار أو أكبر منه:

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.