ساخت یک جزء سوئیچ

یک نمای کلی از نحوه ساخت یک جزء سوئیچ پاسخگو و در دسترس.

در این پست می‌خواهم تفکری را درباره روشی برای ساخت اجزای سوئیچ به اشتراک بگذارم. نسخه ی نمایشی را امتحان کنید .

نسخه ی نمایشی

اگر ویدیو را ترجیح می دهید، در اینجا یک نسخه YouTube از این پست وجود دارد:

نمای کلی

یک سوئیچ شبیه به یک چک باکس عمل می کند، اما به صراحت حالت های روشن و خاموش بولین را نشان می دهد.

این نسخه نمایشی از <input type="checkbox" role="switch"> برای اکثر قابلیت‌های خود استفاده می‌کند، که این مزیت را دارد که به CSS یا جاوا اسکریپت برای کاملا کاربردی و در دسترس بودن نیاز ندارد. بارگیری CSS پشتیبانی از زبان های راست به چپ، عمودی بودن، انیمیشن و موارد دیگر را به ارمغان می آورد. بارگیری جاوا اسکریپت سوئیچ را قابل کشیدن و ملموس می کند.

خواص سفارشی

متغیرهای زیر قسمت های مختلف سوئیچ و گزینه های آنها را نشان می دهند. به عنوان کلاس سطح بالا، .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);

نشانه گذاری

من انتخاب کردم که عنصر <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"> با یک API و حالت از پیش ساخته شده است. مرورگر ویژگی های checked و رویدادهای ورودی مانند oninput و onchanged را مدیریت می کند.

طرح بندی ها

ویژگی‌های Flexbox ، grid و سفارشی در حفظ سبک‌های این مؤلفه حیاتی هستند. آنها مقادیر را متمرکز می‌کنند، نام‌هایی را برای محاسبات یا مناطق مبهم می‌گذارند و یک API ویژگی سفارشی کوچک را برای سفارشی‌سازی آسان اجزا فعال می‌کنند.

.gui-switch

طرح سطح بالای سوئیچ فلکس باکس است. کلاس .gui-switch شامل خصوصیات سفارشی خصوصی و عمومی است که کودکان برای محاسبه طرح‌بندی‌های خود استفاده می‌کنند.

ابزارهای توسعه‌دهنده Flexbox یک برچسب افقی و سوئیچ را پوشانده و توزیع طرح‌بندی فضا را نشان می‌دهد.

.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 روی مسیر سوئیچ پوشانده شده است و نواحی مسیر شبکه نام‌گذاری شده را با نام «تراک» نشان می‌دهد.

.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 همچنین تیک بصری ارائه شده توسط مرورگر را حذف نمی کند. این کامپوننت از یک شبه عنصر و کلاس شبه :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;
}

حذف آن سبک ها همیشه توصیه نمی شود، زیرا می توانند بازخورد تعامل بصری ارزشمندی باشند. اگر آنها را حذف کردید، حتماً جایگزین های سفارشی ارائه دهید.

آهنگ

استایل‌های این عنصر بیشتر در مورد شکل و رنگ آن است که از طریق cascade از .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%;
}

DevTools نشان داده شده است که شبه عنصر انگشت شست دایره را برجسته می کند.

تعامل

از ویژگی های سفارشی برای آماده شدن برای فعل و انفعالاتی استفاده کنید که نقاط برجسته شناور و تغییرات موقعیت شست را نشان می دهد. اولویت کاربر نیز قبل از انتقال سبک‌های برجسته حرکت یا شناور بررسی می‌شود .

.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 اضافه می کند.

یک عنصر چرخش سه بعدی، ارتفاع کلی مولفه را تغییر نمی دهد، که می تواند طرح بلوک را از بین ببرد. این را با استفاده از متغیرهای --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، Elad Schecter ، و من با استفاده از تبدیل‌های CSS که زبان‌های راست به چپ را با چرخاندن یک متغیر واحد کنترل می‌کرد، با هم یک منوی کناری اسلاید بیرون را نمونه‌سازی کردیم. ما این کار را انجام دادیم زیرا هیچ تغییر خاصیت منطقی در CSS وجود ندارد و ممکن است هرگز وجود نداشته باشد. Elad ایده عالی استفاده از یک مقدار ویژگی سفارشی برای معکوس کردن درصدها را داشت تا امکان مدیریت مکان واحد از منطق سفارشی خودمان برای تبدیل‌های منطقی را فراهم کند. من از همین تکنیک در این سوئیچ استفاده کردم و فکر می کنم عالی عمل کرد:

.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 عمداً به حال خود رها شد، با تنظیمی که فقط برای جبران آن انجام شد. حلقه فوکوس در فایرفاکس و سافاری عالی به نظر می رسید:

تصویری از حلقه فوکوس که بر روی یک سوئیچ در فایرفاکس و سافاری متمرکز شده است.

بررسی شد

<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 ، که در آن یک چک باکس نه علامت زده می شود و نه علامت زده می شود. این حالت سرگرم کننده ای است، دعوت کننده و بی تکلف است. یک یادآوری خوب که حالت‌های بولی می‌توانند در بین حالت‌های یواشکی داشته باشند.

تنظیم نامشخص یک چک باکس مشکل است، فقط جاوا اسکریپت می تواند آن را تنظیم کند:

<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;
}

جاوا اسکریپت

برای من، یک رابط سوئیچ در تلاش برای تقلید از یک رابط فیزیکی، به خصوص این نوع با دایره ای در داخل یک مسیر، می تواند عجیب باشد. iOS با سوئیچ خود این کار را درست انجام داد، می‌توانید آن‌ها را به سمت دیگری بکشید، و داشتن این گزینه بسیار راضی‌کننده است. برعکس، اگر حرکتی درگ انجام شود و هیچ اتفاقی نیفتد، یک عنصر رابط کاربری ممکن است غیرفعال به نظر برسد.

شست قابل کشیدن

شبه عنصر انگشت شست موقعیت خود را از .gui-switch > input scoped var(--thumb-position) دریافت می کند، جاوا اسکریپت می تواند یک مقدار سبک درون خطی را در ورودی ارائه کند تا به صورت پویا موقعیت انگشت شست را به روز کند و به نظر برسد که از ژست اشاره گر پیروی می کند. . هنگامی که نشانگر آزاد شد، استایل های درون خطی را حذف کنید و با استفاده از ویژگی سفارشی --thumb-position مشخص کنید که کشیدن به خاموش یا روشن نزدیکتر بوده است. این ستون فقرات راه حل است. رویدادهای اشاره گر به طور مشروط موقعیت های اشاره گر را ردیابی می کنند تا ویژگی های سفارشی CSS را تغییر دهند.

از آنجایی که قبل از نمایش این اسکریپت کامپوننت 100٪ کارایی داشت، برای حفظ رفتار موجود، مانند کلیک کردن روی برچسب برای تغییر دادن ورودی، کمی کار لازم است. جاوا اسکریپت ما نباید ویژگی ها را به قیمت ویژگی های موجود اضافه کند.

touch-action

کشیدن یک حرکت، یک حرکت سفارشی است که آن را به یک کاندیدای عالی برای مزایای touch-action تبدیل می‌کند. در مورد این سوئیچ، یک حرکت افقی باید توسط اسکریپت ما مدیریت شود یا یک حرکت عمودی برای نوع سوئیچ عمودی گرفته شود. با touch-action می‌توانیم به مرورگر بگوییم که چه حرکاتی را روی این عنصر انجام دهد، بنابراین یک اسکریپت می‌تواند یک حرکت را بدون رقابت مدیریت کند.

CSS زیر به مرورگر دستور می دهد که وقتی یک اشاره اشاره گر از داخل این مسیر سوئیچ شروع می شود، حرکات عمودی را مدیریت کنید، هیچ کاری با حرکات افقی انجام ندهید:

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

نتیجه مورد نظر یک حرکت افقی است که صفحه را نیز حرکت نمی دهد یا اسکرول نمی کند. یک اشاره گر می تواند به صورت عمودی از داخل ورودی شروع و صفحه را اسکرول کند، اما موارد افقی به صورت سفارشی مدیریت می شوند.

ابزارهای سبک ارزش پیکسل

هنگام راه اندازی و در حین کشیدن، مقادیر اعداد محاسبه شده مختلف باید از عناصر گرفته شود. توابع جاوا اسکریپت زیر مقادیر پیکسل محاسبه شده را با یک ویژگی 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() آرگومان دوم، یک عنصر شبه هدف را می پذیرد. بسیار تمیز است که جاوا اسکریپت می تواند مقادیر زیادی را از عناصر، حتی از عناصر شبه، بخواند.

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 است، مدیریت می شود و می تواند از آن برای معکوس کردن منطق و ادامه پشتیبانی از RTL استفاده کند. 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)
}

این برای در نظر گرفتن برچسبی است که بعداً این کلیک را دریافت می کند، زیرا علامت تعاملی را که کاربر انجام داده است، برداشته یا بررسی می کند.

اگر بخواهم دوباره این کار را انجام دهم، ممکن است در حین ارتقای UX، DOM را با جاوا اسکریپت تنظیم کنم، تا عنصری ایجاد کنم که خود کلیک‌های برچسب را کنترل کند و با رفتارهای داخلی مقابله نکند.

این نوع جاوا اسکریپت کمترین علاقه من را برای نوشتن دارد، من نمی خواهم حباب رویداد شرطی را مدیریت کنم:

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

نتیجه گیری

این کامپوننت سوئیچ کوچک در نهایت بیشترین کار را در بین چالش‌های رابط کاربری گرافیکی تا کنون داشته است! حالا که می دانید من چگونه این کار را انجام دادم، چگونه این کار را انجام می دهید‽🙂

بیایید رویکردهای خود را متنوع کنیم و همه راه‌های ساخت در وب را بیاموزیم. یک نسخه نمایشی ایجاد کنید، پیوندها را برای من توییت کنید ، و من آن را به بخش ریمیکس های انجمن در زیر اضافه می کنم!

ریمیکس های انجمن

منابع

کد منبع .gui-switch را در GitHub پیدا کنید.

،

یک نمای کلی از نحوه ساخت یک جزء سوئیچ پاسخگو و در دسترس.

در این پست می‌خواهم تفکری را درباره روشی برای ساخت اجزای سوئیچ به اشتراک بگذارم. نسخه ی نمایشی را امتحان کنید .

نسخه ی نمایشی

اگر ویدیو را ترجیح می دهید، در اینجا یک نسخه YouTube از این پست وجود دارد:

نمای کلی

یک سوئیچ شبیه به یک چک باکس عمل می کند، اما به صراحت حالت های روشن و خاموش بولین را نشان می دهد.

این نسخه نمایشی از <input type="checkbox" role="switch"> برای اکثر قابلیت‌های خود استفاده می‌کند، که این مزیت را دارد که به CSS یا جاوا اسکریپت برای کاملا کاربردی و در دسترس بودن نیاز ندارد. بارگیری CSS پشتیبانی از زبان های راست به چپ، عمودی بودن، انیمیشن و موارد دیگر را به ارمغان می آورد. بارگیری جاوا اسکریپت سوئیچ را قابل کشیدن و ملموس می کند.

خواص سفارشی

متغیرهای زیر قسمت های مختلف سوئیچ و گزینه های آنها را نشان می دهند. به عنوان کلاس سطح بالا، .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);

نشانه گذاری

من انتخاب کردم که عنصر <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"> با یک API و حالت از پیش ساخته شده است. مرورگر ویژگی های checked و رویدادهای ورودی مانند oninput و onchanged را مدیریت می کند.

طرح بندی ها

ویژگی‌های Flexbox ، grid و سفارشی در حفظ سبک‌های این مؤلفه حیاتی هستند. آنها مقادیر را متمرکز می‌کنند، نام‌هایی را برای محاسبات یا مناطق مبهم می‌گذارند و یک API ویژگی سفارشی کوچک را برای سفارشی‌سازی آسان اجزا فعال می‌کنند.

.gui-switch

طرح سطح بالای سوئیچ فلکس باکس است. کلاس .gui-switch شامل خصوصیات سفارشی خصوصی و عمومی است که کودکان برای محاسبه طرح‌بندی‌های خود استفاده می‌کنند.

ابزارهای توسعه‌دهنده Flexbox یک برچسب افقی و سوئیچ را پوشانده و توزیع طرح‌بندی فضا را نشان می‌دهد.

.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 روی مسیر سوئیچ پوشانده شده است و نواحی مسیر شبکه نام‌گذاری شده را با نام «تراک» نشان می‌دهد.

.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 همچنین تیک بصری ارائه شده توسط مرورگر را حذف نمی کند. این کامپوننت از یک شبه عنصر و کلاس شبه :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;
}

حذف آن سبک ها همیشه توصیه نمی شود، زیرا می توانند بازخورد تعامل بصری ارزشمندی باشند. اگر آنها را حذف کردید، حتماً جایگزین های سفارشی ارائه دهید.

آهنگ

استایل‌های این عنصر بیشتر در مورد شکل و رنگ آن است که از طریق cascade از .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%;
}

DevTools نشان داده شده است که شبه عنصر انگشت شست دایره را برجسته می کند.

تعامل

از ویژگی های سفارشی برای آماده شدن برای فعل و انفعالاتی استفاده کنید که نقاط برجسته شناور و تغییرات موقعیت شست را نشان می دهد. اولویت کاربر نیز قبل از انتقال سبک‌های برجسته حرکت یا شناور بررسی می‌شود .

.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 اضافه می کند.

یک عنصر چرخش سه بعدی، ارتفاع کلی مولفه را تغییر نمی دهد، که می تواند طرح بلوک را از بین ببرد. این را با استفاده از متغیرهای --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، Elad Schecter ، و من با استفاده از تبدیل‌های CSS که زبان‌های راست به چپ را با چرخاندن یک متغیر واحد کنترل می‌کرد، با هم یک منوی کناری اسلاید بیرون را نمونه‌سازی کردیم. ما این کار را انجام دادیم زیرا هیچ تغییر خاصیت منطقی در CSS وجود ندارد و ممکن است هرگز وجود نداشته باشد. Elad ایده عالی استفاده از یک مقدار ویژگی سفارشی برای معکوس کردن درصدها را داشت تا امکان مدیریت مکان واحد از منطق سفارشی خودمان برای تبدیل‌های منطقی را فراهم کند. من از همین تکنیک در این سوئیچ استفاده کردم و فکر می کنم عالی عمل کرد:

.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 عمداً به حال خود رها شد، با تنظیمی که فقط برای جبران آن انجام شد. حلقه فوکوس در فایرفاکس و سافاری عالی به نظر می رسید:

تصویری از حلقه فوکوس که بر روی یک سوئیچ در فایرفاکس و سافاری متمرکز شده است.

بررسی شد

<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 ، که در آن یک چک باکس نه علامت زده می شود و نه علامت زده می شود. این حالت سرگرم کننده ای است، دعوت کننده و بی تکلف است. یک یادآوری خوب که حالت‌های بولی می‌توانند در بین حالت‌های یواشکی داشته باشند.

تنظیم نامشخص یک چک باکس مشکل است، فقط جاوا اسکریپت می تواند آن را تنظیم کند:

<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;
}

جاوا اسکریپت

برای من، یک رابط سوئیچ در تلاش برای تقلید از یک رابط فیزیکی، به خصوص این نوع با دایره ای در داخل یک مسیر، می تواند عجیب باشد. iOS با سوئیچ خود این کار را درست انجام داد، می‌توانید آن‌ها را به سمت دیگری بکشید، و داشتن این گزینه بسیار راضی‌کننده است. برعکس، اگر حرکتی درگ انجام شود و هیچ اتفاقی نیفتد، یک عنصر رابط کاربری ممکن است غیرفعال به نظر برسد.

شست قابل کشیدن

شبه عنصر انگشت شست موقعیت خود را از .gui-switch > input scoped var(--thumb-position) دریافت می کند، جاوا اسکریپت می تواند یک مقدار سبک درون خطی را در ورودی ارائه کند تا به صورت پویا موقعیت انگشت شست را به روز کند و به نظر برسد که از ژست اشاره گر پیروی می کند. . هنگامی که نشانگر آزاد شد، استایل های درون خطی را حذف کنید و با استفاده از ویژگی سفارشی --thumb-position مشخص کنید که کشیدن به خاموش یا روشن نزدیکتر بوده است. این ستون فقرات راه حل است. رویدادهای اشاره گر به طور مشروط موقعیت های اشاره گر را ردیابی می کنند تا ویژگی های سفارشی CSS را تغییر دهند.

از آنجایی که قبل از نمایش این اسکریپت کامپوننت 100٪ کارایی داشت، برای حفظ رفتار موجود، مانند کلیک کردن روی برچسب برای تغییر دادن ورودی، کمی کار لازم است. جاوا اسکریپت ما نباید ویژگی ها را به قیمت ویژگی های موجود اضافه کند.

touch-action

کشیدن یک حرکت، یک حرکت سفارشی است که آن را به یک کاندیدای عالی برای مزایای touch-action تبدیل می‌کند. در مورد این سوئیچ، یک حرکت افقی باید توسط اسکریپت ما مدیریت شود یا یک حرکت عمودی برای نوع سوئیچ عمودی گرفته شود. با touch-action می‌توانیم به مرورگر بگوییم که چه حرکاتی را روی این عنصر انجام دهد، بنابراین یک اسکریپت می‌تواند یک حرکت را بدون رقابت مدیریت کند.

CSS زیر به مرورگر دستور می دهد که وقتی یک اشاره اشاره گر از داخل این مسیر سوئیچ شروع می شود، حرکات عمودی را مدیریت کنید، هیچ کاری با حرکات افقی انجام ندهید:

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

نتیجه مورد نظر یک حرکت افقی است که صفحه را نیز حرکت نمی دهد یا اسکرول نمی کند. یک اشاره گر می تواند به صورت عمودی از داخل ورودی شروع و صفحه را اسکرول کند، اما موارد افقی به صورت سفارشی مدیریت می شوند.

ابزارهای سبک ارزش پیکسل

هنگام راه اندازی و در حین کشیدن، مقادیر اعداد محاسبه شده مختلف باید از عناصر گرفته شود. توابع جاوا اسکریپت زیر مقادیر پیکسل محاسبه شده را با یک ویژگی 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() آرگومان دوم، یک عنصر شبه هدف را می پذیرد. بسیار تمیز است که جاوا اسکریپت می تواند مقادیر زیادی را از عناصر، حتی از عناصر شبه، بخواند.

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 است، مدیریت می شود و می تواند از آن برای معکوس کردن منطق و ادامه پشتیبانی از RTL استفاده کند. 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)
}

این برای در نظر گرفتن برچسبی است که بعداً این کلیک را دریافت می کند، زیرا علامت تعاملی را که کاربر انجام داده است، برداشته یا بررسی می کند.

اگر بخواهم دوباره این کار را انجام دهم، ممکن است در حین ارتقای UX، DOM را با جاوا اسکریپت تنظیم کنم، تا عنصری ایجاد کنم که خود کلیک‌های برچسب را کنترل کند و با رفتارهای داخلی مقابله نکند.

این نوع جاوا اسکریپت کمترین علاقه من را برای نوشتن دارد، من نمی خواهم حباب رویداد شرطی را مدیریت کنم:

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

نتیجه گیری

این کامپوننت سوئیچ کوچک در نهایت بیشترین کار را در بین چالش‌های رابط کاربری گرافیکی تا کنون داشته است! حالا که می دانید من چگونه این کار را انجام دادم، چگونه این کار را انجام می دهید‽🙂

بیایید رویکردهای خود را متنوع کنیم و همه راه‌های ساخت در وب را بیاموزیم. یک نسخه نمایشی ایجاد کنید، پیوندها را برای من توییت کنید ، و من آن را به بخش ریمیکس های انجمن در زیر اضافه می کنم!

ریمیکس های انجمن

منابع

کد منبع .gui-switch را در GitHub پیدا کنید.