Switch-Komponente erstellen

Eine grundlegende Übersicht zum Erstellen 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 <input type="checkbox" role="switch"> für den Großteil der Funktionen verwendet. Das hat den Vorteil, dass weder CSS noch JavaScript erforderlich sind, damit die Demo vollständig funktionsfähig und barrierefrei ist. 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

Länge (--track-size), 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-Query 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-Queries 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 Beibehaltung 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ästchenauswahl wird als Schalter-Track 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 einzelner Zellenrasterbereich für einen Screenshot erstellt, den du beanspruchen kannst.

Thumbnails

Mit dem Stil appearance: none wird auch das visuelle Häkchen entfernt, das vom Browser bereitgestellt wird. 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 Track 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 Markierungen für Tippaktionen 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 Feedback zur Interaktion 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. Wir haben die Größe des Thumbnails und des Tracks, die wir bei den Berechnungen verwenden, damit der Thumbnail-Cursor richtig versetzt ist und sich innerhalb des Tracks befindet: 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 erfolgte mit einer Modifikatorklasse -vertical, die dem input-Element eine Drehung mit CSS-Transformationen 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 entwickelt, das auch Sprachen von rechts nach links unterstützt, indem eine einzige Variable umgedreht wird. Das liegt daran, dass es in CSS keine logischen Property-Transformationen gibt und es sie möglicherweise auch nie geben wird. 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 diese Technik auch bei diesem Schalter verwendet und ich denke, 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 Pseudoelement „thumb“ müssen ebenfalls aktualisiert werden, um die Anforderung für die gegenüberliegende Seite zu erfüllen:

.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 wurde bewusst nicht verändert, sondern nur der 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 in den Status „Deaktiviert“, „Angeklickt“ und „Nicht angeklickt“

Dieser Status ist etwas schwierig, da sowohl ein dunkles als auch ein helles Design mit deaktiviertem und aktiviertem Status erforderlich sind. Ich habe für diese Status minimalistische Stile ausgewählt, um die Wartungsbelastung der Stilkombinationen zu verringern.

Unklar

Ein oft vergessener Status ist :indeterminate, bei dem ein Kästchen weder angeklickt noch abgewählt 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 Status, bei dem der Miniaturtrack in der Mitte angezeigt wird, um anzugeben, dass noch keine Entscheidung getroffen wurde.

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. Wenn diese Option aktiviert ist, wird der Schieberegler mit einem halbtransparenten 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 Schnittstelle 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 der benutzerdefinierten Property --thumb-position, ob die Mausbewegung 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-Eigenschaften 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 eine horizontale Geste von unserem Script verarbeitet oder eine vertikale Geste für die vertikale Schaltervariante erfasst werden. 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 beim Ziehen müssen verschiedene berechnete Zahlenwerte aus Elementen abgerufen werden. Die folgenden JavaScript-Funktionen geben berechnete Pixelwerte für eine CSS-Eigenschaft zurück. Im Einrichtungsskript wird es 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. Diese Zuweisung würde sich sonst im Laufe der Zeit ändern, aber ein vorheriges Zeigerereignis hat --thumb-transition-duration vorübergehend auf 0s gesetzt, wodurch eine sonst verzögerte Interaktion vermieden wird.

dragEnd

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

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

  dragEnd(event)
})

Ich finde es sehr wichtig, dass Nutzer die Möglichkeit haben, frei zu ziehen, und dass die Benutzeroberfläche intelligent genug ist, dies zu berücksichtigen. Die Umstellung war nicht sehr aufwendig, erforderte aber eine sorgfältige Überlegung während des Entwicklungsprozesses.

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 können Sie die Eigenschaft „input checked“ festlegen und alle Touch-Ereignisse entfernen. 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, vor allem für das Einschließen 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 das machen?

Lassen Sie uns unsere Ansätze diversifizieren und alle Möglichkeiten kennenlernen, wie Sie im Web entwickeln können. Erstelle eine Demo, tweete mir Links und ich füge sie unten in den Abschnitt „Community-Remixe“ hinzu.

Remixe der Community

Ressourcen

.gui-switch Quellcode auf GitHub