Switch-Komponente erstellen

Ein grundlegender Überblick über die Erstellung einer responsiven und barrierefreien Schalterkomponente.

In diesem Beitrag möchte ich meine Gedanken zu einer Möglichkeit zum Erstellen von Schalterkomponenten teilen. Demo ansehen.

Demo

Wenn du lieber ein Video ansiehst, findest du hier eine YouTube-Version dieses Beitrags:

Übersicht

Ein Schalter funktioniert ähnlich wie ein Kästchen, stellt aber explizit boolesche Ein- und Aus-Zustände dar.

In dieser Demo wird für den Großteil der Funktionalität <input type="checkbox" role="switch"> verwendet. Dies hat den Vorteil, dass kein CSS oder JavaScript erforderlich ist, um voll funktionsfähig und zugänglich zu sein. Durch das Laden von CSS wird die Unterstützung von Sprachen, die von rechts nach links geschrieben werden, vertikaler Ausrichtung und Animationen ermöglicht. Wenn Sie JavaScript laden, wird der Schalter beweglich und greifbar.

Benutzerdefinierte Eigenschaften

Die folgenden Variablen stehen für die verschiedenen Teile des Schalters und ihre Optionen. Als oberste Klasse enthält .gui-switch benutzerdefinierte Eigenschaften, die in allen untergeordneten Komponenten verwendet werden, sowie Einstiegspunkte für die zentrale Anpassung.

Verfolgen

Die Länge (--track-size), der Abstand und zwei Farben:

.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%);
  }
}

Thumbnails

Größe, Hintergrundfarbe und Farben für Interaktions-Highlights:

.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%);
  }
}

Weniger Bewegung

Um einen eindeutigen Alias hinzuzufügen und Wiederholungen zu vermeiden, kann eine Media-Abfrage für Nutzer mit reduzierter Bewegungspräferenz mit dem PostCSS-Plug-in in eine benutzerdefinierte Property eingefügt werden. Dabei wird diese Vorlage für Media-Abfragen Version 5 verwendet:

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

Markieren & Zeichnen

Ich habe mein <input type="checkbox" role="switch">-Element in ein <label>-Element gewickelt, um ihre Beziehung zu bündeln und Unklarheiten bei der Verknüpfung von Kästchen und Label zu vermeiden. Gleichzeitig haben Nutzer die Möglichkeit, mit dem Label zu interagieren, um die Eingabe zu aktivieren oder zu deaktivieren.

Ein natürliches, ohne Stil gestaltetes Label und Kästchen.

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

<input type="checkbox"> ist bereits mit einer API und einem Status ausgestattet. Der Browser verwaltet das checked-Attribut und Eingabeereignisse wie oninput und onchanged.

Layouts

Flexbox-, Grid- und benutzerdefinierte Eigenschaften sind entscheidend für die Verwaltung der Stile dieser Komponente. Sie zentralisieren Werte, geben ansonsten mehrdeutigen Berechnungen oder Bereichen Namen und ermöglichen eine kleine API für benutzerdefinierte Properties, um Komponenten ganz einfach anzupassen.

.gui-switch

Das Layout der obersten Ebene für den Schalter ist Flexbox. Die Klasse .gui-switch enthält die privaten und öffentlichen benutzerdefinierten Properties, die die untergeordneten Elemente zum Berechnen ihrer Layouts verwenden.

Flexbox-DevTools, die ein horizontales Label und einen Schalter überlagern und die Layoutverteilung des Bereichs zeigen

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

Das Flexbox-Layout kann wie jedes andere Flexbox-Layout erweitert und geändert werden. So platzieren Sie beispielsweise Labels über oder unter einem Schalter oder ändern die flex-direction:

Flexbox-DevTools, die ein vertikales Label und einen Schalter überlagern

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

Verfolgen

Die Kästcheneingabe wird wie eine Schaltspur gestaltet, indem die normale appearance: checkbox entfernt und stattdessen eine eigene Größe angegeben wird:

Grid DevTools, die den Schaltertrack überlagern und die benannten Grid-Track-Bereiche mit dem Namen „track“ anzeigen

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

Außerdem wird ein Rasterbereich mit einer einzelnen Zelle erstellt, den ein Nutzer beanspruchen kann.

Thumbnails

Mit dem Stil appearance: none wird auch das vom Browser bereitgestellte visuelle Häkchen entfernt. Diese Komponente verwendet ein Pseudo-Element und die :checked-Pseudoklasse für die Eingabe, um diesen visuellen Indikator zu ersetzen.

Der Vorschaubereich ist ein untergeordnetes Pseudo-Element, das an input[type="checkbox"] angehängt ist und sich über dem Titeltrack statt darunter stapelt, indem es den Rasterbereich track beansprucht:

In den DevTools wird der Pseudo-Element-Miniaturansichten innerhalb eines CSS-Rasters angezeigt.

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

Stile

Mit benutzerdefinierten Eigenschaften können Sie eine vielseitige Schalterkomponente erstellen, die sich an Farbschemata, von rechts nach links geschriebene Sprachen und Bewegungseinstellungen anpasst.

Ein direkter Vergleich des hellen und dunklen Designs für den Schalter und seine Status.

Touch-Interaktionsstile

Auf Mobilgeräten fügen Browser Labels und Eingaben Tippeffekte und Textauswahlfunktionen hinzu. Dies wirkte sich negativ auf den Stil und das visuelle Interaktionsfeedback aus, das für diese Umstellung erforderlich war. Mit ein paar Zeilen CSS kann ich diese Effekte entfernen und meinen eigenen cursor: pointer-Stil hinzufügen:

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

Es ist nicht immer ratsam, diese Stile zu entfernen, da sie wertvolles visuelles Interaktionsfeedback sein können. Wenn Sie sie entfernen, müssen Sie benutzerdefinierte Alternativen angeben.

Verfolgen

Die Stile dieses Elements beziehen sich hauptsächlich auf seine Form und Farbe, auf die es über die Kaskade vom übergeordneten Element .gui-switch zugreift.

Die Schaltervarianten mit benutzerdefinierten Spurgrößen und -farben.

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

Vier benutzerdefinierte Properties bieten eine Vielzahl von Anpassungsoptionen für den Schalter-Track. border: none wird hinzugefügt, da appearance: none die Rahmen nicht in allen Browsern aus dem Kästchen entfernt.

Thumbnails

Das Vorschauelement befindet sich bereits rechts track, benötigt aber Kreisstile:

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

In den DevTools wird das Pseudo-Element „circle-thumb“ hervorgehoben.

Interaktion

Mit benutzerdefinierten Properties können Sie sich auf Interaktionen vorbereiten, bei denen Highlights beim Hovern und Änderungen der Position des Schiebereglers angezeigt werden. Außerdem wird die Einstellung des Nutzers geprüft, bevor die Stilvorlage für die Bewegung oder die hervorgehobenen Elemente beim Hovering geändert wird.

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

Daumenposition

Benutzerdefinierte Properties bieten einen einzigen Mechanismus zur Positionierung des Schiebereglers im Titel. Es stehen die Track- und Thumbnail-Größen zur Verfügung, die wir in Berechnungen verwenden, damit der Daumen-Versatz innerhalb des Tracks korrekt verschoben wird: 0% und 100%.

Das input-Element hat die Positionierungsvariable --thumb-position und das Pseudo-Element „thumb“ verwendet sie als translateX-Position:

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

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

Wir können --thumb-position jetzt über CSS und die Pseudoklassen für Kästchenelemente ändern. Da wir transition: transform var(--thumb-transition-duration) ease zuvor bedingt für dieses Element festgelegt haben, können diese Änderungen animiert werden, wenn sie geändert werden:

/* 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)
  );
}

Ich fand, dass diese entkoppelte Orchestrierung gut funktioniert hat. Das thumb-Element bezieht sich nur auf einen Stil, eine translateX-Position. Die Eingabe kann alle Komplexität und Berechnungen verwalten.

Branche

Die Unterstützung wurde mit der Modifikatorklasse -vertical durchgeführt, die eine Rotation mit CSS-Transformationen zum input-Element hinzufügt.

Durch ein in 3D gedrehtes Element ändert sich jedoch nicht die Gesamthöhe der Komponente, was das Blocklayout beeinträchtigen kann. Berücksichtigen Sie dies mit den Variablen --track-size und --track-padding. Berechnen Sie den Mindestabstand, der erforderlich ist, damit eine vertikale Schaltfläche im Layout wie erwartet dargestellt wird:

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

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

(RTL) Linksläufig

Zusammen mit einem CSS-Freund, Elad Schecter, habe ich einen Prototyp für ein ausziehbares Seitenmenü mit CSS-Transformierungen erstellt, das Sprachen von rechts nach links unterstützt, indem eine einzelne Variable umgedreht wurde. Wir haben dies getan, weil es in CSS keine logischen Attributtransformationen gibt und es dies vielleicht nie gibt. Elad hatte die gute Idee, einen benutzerdefinierten Property-Wert zu verwenden, um Prozentsätze umzukehren, damit unsere benutzerdefinierte Logik für logische Transformationen an einem einzigen Ort verwaltet werden kann. Ich habe bei diesem Wechsel dieselbe Technik verwendet und es hat gut funktioniert:

.gui-switch {
  --isLTR: 1;

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

Eine benutzerdefinierte Eigenschaft namens --isLTR hat anfangs den Wert 1, was true bedeutet, da unser Layout standardmäßig von links nach rechts ausgerichtet ist. Mit dem CSS-Pseudoklass :dir() wird der Wert dann auf -1 gesetzt, wenn sich die Komponente in einem Layout von rechts nach links befindet.

Verwenden Sie --isLTR in einer calc() innerhalb einer Transformation:

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

Die Drehung des vertikalen Schalters berücksichtigt jetzt die gegenüberliegende Seitenposition, die für das Layout von rechts nach links erforderlich ist.

Die translateX-Transformationen für das Daumen-Pseudoelement müssen ebenfalls aktualisiert werden, um die gegenüberliegende Anforderung zu berücksichtigen:

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

Mit diesem Ansatz lassen sich zwar nicht alle Anforderungen an ein Konzept wie logische CSS-Transformationen erfüllen, er bietet aber für viele Anwendungsfälle einige DRY-Prinzipien.

Bundesstaaten

Die Verwendung der integrierten input[type="checkbox"] wäre nicht vollständig, ohne die verschiedenen Status zu berücksichtigen, in denen sie sich befinden kann: :checked, :disabled, :indeterminate und :hover. :focus blieb absichtlich außen und wurde nur am Versatz angepasst. Der Fokusring sah in Firefox und Safari gut aus:

Ein Screenshot des Fokusrings, der auf einen Schalter in Firefox und Safari gerichtet ist.

Geprüft

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

Dieser Status entspricht dem Status on. In diesem Status ist der Hintergrund der Eingabe „track“ auf die aktive Farbe und die Position des Schiebereglers auf „Ende“ festgelegt.

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

Deaktiviert

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

Eine :disabled-Schaltfläche sieht nicht nur optisch anders aus, sondern sollte das Element auch unveränderlich machen.Die Unveränderlichkeit von Interaktionen ist unabhängig vom Browser, aber die visuellen Status benötigen Stile, da appearance: none verwendet wird.

.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%);
    }}
  }
}

Der dunkel gestaltete Schalter im deaktivierten, angeklickten und nicht angeklickten Zustand.

Dieser Status ist schwierig, da er dunkle und helle Designs mit deaktiviertem und aktiviertem Status benötigt. Ich habe für diese Status minimalistische Stile ausgewählt, um die Wartungsbelastung der Stilkombinationen zu verringern.

Unklar

Ein häufig vergessener Status ist :indeterminate, bei dem ein Kästchen weder aktiviert noch deaktiviert ist. Das ist ein unterhaltsamer Zustand, er ist einladend und unprätentiös. Eine gute Erinnerung daran, dass boolesche Status Zwischenstatus haben können.

Es ist schwierig, ein Kästchen auf „Unbestimmt“ zu setzen. Das geht nur mit 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>

Der unbestimmte Zustand, bei dem der Track-Daumen in der Mitte angezeigt wird, um „Unentschlossen“ anzuzeigen.

Da der Bundesstaat für mich unscheinbar und einladend ist, erschien es mir angemessen, die Position des Schalters in der Mitte zu platzieren:

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

Mauszeiger hierher bewegen

Hover-Interaktionen sollten eine visuelle Unterstützung für die verbundene Benutzeroberfläche bieten und auch eine Richtung für die interaktive Benutzeroberfläche vorgeben. Mit diesem Schalter wird der Daumen als halbtransparentes Ring hervorgehoben, wenn der Mauszeiger auf das Label oder die Eingabe bewegt wird. Diese Hover-Animation weist dann auf das interaktive Vorschauelement hin.

Der Effekt „Hervorheben“ wird mit box-shadow erstellt. Erhöhen Sie die Größe von --highlight-size, wenn der Mauszeiger auf eine nicht deaktivierte Eingabe schwebt. Wenn der Nutzer mit Bewegung einverstanden ist, wird die box-shadow animiert und vergrößert sich. Wenn er mit Bewegung nicht einverstanden ist, wird das Highlight sofort angezeigt:

.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

Ich finde, dass ein Schalter, der versucht, eine physische Oberfläche zu emulieren, etwas unheimlich wirken kann, vor allem diese Art mit einem Kreis in einem Track. iOS hat das mit seinem Schalter richtig gemacht. Sie können ihn hin und her ziehen und es ist sehr zufriedenstellend, diese Option zu haben. Umgekehrt kann ein UI-Element inaktiv erscheinen, wenn ein Ziehen versucht wird und nichts passiert.

Ziehbare „Mag ich“-Bewertungen

Das Pseudo-Element „thumb“ erhält seine Position vom .gui-switch > input-Element mit dem var(--thumb-position)-Scope. JavaScript kann einen Inline-Stilwert für die Eingabe bereitstellen, um die Position des Schiebereglers dynamisch zu aktualisieren, sodass es so aussieht, als würde er der Mausbewegung folgen. Wenn der Mauszeiger losgelassen wird, entfernen Sie die Inline-Styles und ermitteln Sie mithilfe des benutzerdefinierten Attributs --thumb-position, ob der Drag-Vorgang eher dem Aus- oder dem Ein-Status entsprach. Dies ist das Rückgrat der Lösung: Mithilfe von Mauszeiger-Ereignissen werden die Mauszeigerpositionen bedingt erfasst, um benutzerdefinierte CSS-Properties zu ändern.

Da die Komponente bereits zu 100 % funktionierte, bevor dieses Script angezeigt wurde, ist es ziemlich aufwendig, das vorhandene Verhalten beizubehalten, z. B. das Klicken auf ein Label, um die Eingabe zu aktivieren oder zu deaktivieren. Unser JavaScript sollte keine Funktionen hinzufügen, die auf Kosten bestehender Funktionen gehen.

touch-action

Ziehen ist eine Geste, eine benutzerdefinierte Geste, was sie zu einem guten Kandidaten für touch-action-Vorteile macht. Bei diesem Schalter sollte unser Script eine horizontale Geste verarbeiten oder eine vertikale Geste für die vertikale Schaltervariante erfassen. Mit touch-action können wir dem Browser mitteilen, welche Touch-Gesten für dieses Element verarbeitet werden sollen, damit ein Script eine Geste ohne Konkurrenz verarbeiten kann.

Im folgenden CSS-Code wird dem Browser mitgeteilt, dass vertikale Touch-Gesten verarbeitet werden sollen, wenn eine Touch-Geste innerhalb dieses Schalters beginnt, und horizontale Touch-Gesten ignoriert werden sollen:

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

Das gewünschte Ergebnis ist eine horizontale Geste, die nicht auch die Seite schwenken oder scrollen lässt. Ein vertikaler Cursor kann innerhalb der Eingabe beginnen und die Seite scrollen, horizontale Cursor werden jedoch benutzerdefiniert verarbeitet.

Dienstprogramme für Pixelwerte

Bei der Einrichtung und während des Ziehens müssen verschiedene berechnete Zahlenwerte aus Elementen übernommen werden. Die folgenden JavaScript-Funktionen geben berechnete Pixelwerte für eine CSS-Eigenschaft zurück. Sie wird im Einrichtungsskript so verwendet: 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,
}

Beachten Sie, dass window.getComputedStyle() ein zweites Argument akzeptiert, ein Ziel-Pseudo-Element. Es ist ziemlich praktisch, dass JavaScript so viele Werte aus Elementen lesen kann, sogar aus Pseudoelementen.

dragging

Dies ist ein wichtiger Moment für die Drag-Logik. Im Funktions-Ereignis-Handler gibt es einige Dinge zu beachten:

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

Der Script-Held ist state.activethumb, der kleine Kreis, den dieses Script zusammen mit einem Zeiger positioniert. Das switches-Objekt ist ein Map(), bei dem die Schlüssel .gui-switch sind und die Werte gecachte Grenzen und Größen sind, die das Script effizient halten. Für die Ausrichtung von rechts nach links wird dieselbe benutzerdefinierte CSS-Eigenschaft verwendet wie für --isLTR. Damit kann die Logik umgekehrt und die Unterstützung für RTL fortgesetzt werden. Auch event.offsetX ist wertvoll, da es einen Deltawert enthält, der für die Positionierung des Schiebereglers nützlich ist.

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

Mit dieser letzten CSS-Zeile wird die benutzerdefinierte Property festgelegt, die vom „thumb“-Element verwendet wird. Andernfalls würde diese Wertzuweisung im Laufe der Zeit übergangen, aber durch ein früheres Zeigerereignis wurde --thumb-transition-duration vorübergehend auf 0s gesetzt. Dadurch wird eine langsame Interaktion entfernt.

dragEnd

Damit der Nutzer weit außerhalb des Schalters ziehen und loslassen kann, musste ein globales Fensterereignis registriert werden:

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

  dragEnd(event)
})

Ich denke, es ist sehr wichtig, dass die Nutzenden frei ziehen können und die Benutzeroberfläche so intelligent ist, dass sie dies berücksichtigt. Die Umstellung erforderte nicht viel, musste aber während des Entwicklungsprozesses sorgfältig berücksichtigt werden.

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

Die Interaktion mit dem Element ist abgeschlossen. Jetzt muss die Eigenschaft „Eingabe“ aktiviert und alle Gestenereignisse entfernt werden. Das Kästchen wird durch state.activethumb.checked = determineChecked() ersetzt.

determineChecked()

Diese Funktion, die von dragEnd aufgerufen wird, bestimmt, wo sich der aktuelle Zeiger innerhalb der Grenzen seines Tracks befindet, und gibt „wahr“ zurück, wenn er mindestens 50 % des Tracks zurückgelegt hat:

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
}

Weitere Überlegungen

Aufgrund der gewählten ursprünglichen HTML-Struktur war für die Drag-Geste etwas Code-Schulden erforderlich, insbesondere die Einbettung der Eingabe in ein Label. Da das Label ein übergeordnetes Element ist, erhält es nach der Eingabe Klickinteraktionen. Am Ende des dragEnd-Ereignisses haben Sie vielleicht die merkwürdig klingende Funktion padRelease() bemerkt.

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

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

So wird berücksichtigt, dass das Label diesen Klick später erhält, da die Interaktion eines Nutzers dadurch deaktiviert oder aktiviert wird.

Wenn ich das noch einmal machen würde, würde ich möglicherweise das DOM während der UX-Optimierung mit JavaScript anpassen, um ein Element zu erstellen, das Labelklicks selbst verarbeitet und nicht mit dem integrierten Verhalten in Konflikt steht.

Diese Art von JavaScript ist meine am wenigsten bevorzugte, da ich kein bedingtes Ereignis-Bubbling verwalten möchte:

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

Fazit

Diese winzige Schalterkomponente war am Ende die größte Herausforderung aller GUI-Herausforderungen! Wie würden Sie es machen?

Lassen Sie uns unsere Ansätze diversifizieren und alle Möglichkeiten kennenlernen, wie Sie im Web entwickeln können. Erstelle eine Demo und sende Tweets an mich. Ich füge sie dann unten im Abschnitt zu Community-Remixen hinzu.

Remixe der Community

Ressourcen

Suchen Sie den .gui-switch-Quellcode auf GitHub.