Skip to content

Commit

Permalink
Add draggable switch example
Browse files Browse the repository at this point in the history
  • Loading branch information
Clauderic Demers committed Jul 22, 2021
1 parent 4b344df commit 65836ea
Show file tree
Hide file tree
Showing 6 changed files with 332 additions and 0 deletions.
133 changes: 133 additions & 0 deletions stories/3 - Examples/FormElements/Switch/Switch.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
$switch-border-radius: 50px;
$switch-track-length: 50px;
$switch-track-height: 32px;
$switch-track-border: 2px;
$switch-thumb-diameter: 28px;
$switch-thumb-offset-left: 1px;
$switch-thumb-offset-right: $switch-track-length - $switch-thumb-diameter -
$switch-thumb-offset-left - $switch-track-border;

.Switch {
--color: rgba(70, 70, 70, 0.5);
--background-color: rgba(0, 0, 0, 0.02);
--cursor: grab;

position: relative;
appearance: none;
background-color: var(--background-color);
border: 0;
border-radius: $switch-border-radius;
cursor: pointer;
display: inline-block;
padding: 0;
margin: 0 1rem;
user-select: none;
box-sizing: border-box;
outline: none;

&:focus-visible {
box-shadow: 0 0 3px 2px #4c9ffe;
}

&::after {
border-radius: $switch-border-radius;
}

&.checked:not(.off):not(.disabled),
&.on {
--color: #2389ff;
--background-color: #56a1f8;
}

&.disabled {
--color: rgba(34, 33, 81, 0.5);
}

&.dragging {
--cursor: grabbing;
cursor: var(--cursor);
}

&:not(.disabled) .Thumb {
cursor: var(--cursor);
}

&:hover:not(.disabled, .dragging) .Track {
background-color: rgba(0, 0, 0, 0.05);
}
}

.Track {
display: block;
box-sizing: border-box;
border-radius: $switch-border-radius;
height: $switch-track-height;
width: $switch-track-length;
border: $switch-track-border solid var(--color);
transition: background-color 150ms ease;
}

.ThumbWrapper {
position: absolute;
top: $switch-track-border;
left: $switch-thumb-offset-left;
bottom: $switch-track-border;
right: $switch-track-border;
}

.Thumb {
position: absolute;
top: 0;
left: 0;
display: block;
box-sizing: border-box;
background-color: #fff;
border: 0.1em solid var(--color);
border-radius: 50%;
width: $switch-thumb-diameter;
height: $switch-thumb-diameter;
transition: transform 150ms ease;
transform: translate3d(var(--transform), 0, 0);

.checked & {
transform: translate3d(
calc($switch-thumb-offset-right + var(--transform)),
0,
0
);
}

.checked:not(.off) &,
.on & {
border-color: transparent;
transform: translateX($switch-thumb-offset-right);
}
}

.OnRegion,
.OffRegion {
position: absolute;
top: 0;
bottom: 0;
width: 50%;
pointer-events: none;
}

.OnRegion {
right: 0;
}

.OffRegion {
left: 0;
}

.disabled {
cursor: not-allowed;
opacity: 0.5;
}

.Label {
display: block;
color: #666;
padding: 1rem;
}
29 changes: 29 additions & 0 deletions stories/3 - Examples/FormElements/Switch/Switch.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, {ComponentProps, useState} from 'react';

import {Switch} from './Switch';

export default {
title: 'Examples/Form Elements/Switch',
};

type Props = Pick<ComponentProps<typeof Switch>, 'disabled' | 'label'>;

function SwitchExample({disabled, label}: Props) {
const [checked, setChecked] = useState(false);

return (
<div style={{padding: 40}}>
<Switch
checked={checked}
label={label}
onChange={setChecked}
disabled={disabled}
/>
</div>
);
}

export const BasicSetup = () => <SwitchExample label="Draggable switch" />;
export const Disabled = () => (
<SwitchExample label="Disabled switch" disabled />
);
120 changes: 120 additions & 0 deletions stories/3 - Examples/FormElements/Switch/Switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React, {useState} from 'react';
import {
DndContext,
DragEndEvent,
DragOverEvent,
getBoundingClientRect,
MeasuringConfiguration,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
restrictToHorizontalAxis,
restrictToParentElement,
} from '@dnd-kit/modifiers';
import classNames from 'classnames';

import {State} from './constants';
import {Thumb} from './Thumb';
import {Track} from './Track';
import styles from './Switch.module.css';

export interface Props {
accessibilityLabel?: string;
disabled?: boolean;
id?: string;
label?: string;
checked: boolean;
onChange: (checked: boolean) => void;
}

const modifiers = [restrictToParentElement, restrictToHorizontalAxis];
const measuring: MeasuringConfiguration = {
draggable: {
measure: getBoundingClientRect,
},
};

export function Switch({
accessibilityLabel = '',
label = '',
disabled = false,
id,
checked,
onChange,
}: Props) {
const [overId, setOverId] = useState<string | null>(null);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 2,
},
})
);

const markup = (
<>
{label ? (
<label className={styles.Label} htmlFor={id}>
{label}
</label>
) : null}
<button
id={id}
type="button"
className={classNames(styles.Switch, {
[styles.checked]: checked,
[styles.dragging]: overId != null,
[styles.on]: overId === State.On,
[styles.off]: overId === State.Off,
[styles.disabled]: disabled,
})}
onClick={disabled ? undefined : handleClick}
aria-pressed={checked}
aria-label={accessibilityLabel}
disabled={disabled}
>
<Track />
<Thumb />
</button>
</>
);

return disabled ? (
markup
) : (
<DndContext
autoScroll={false}
measuring={measuring}
modifiers={modifiers}
sensors={sensors}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
{markup}
</DndContext>
);

function handleClick() {
onChange(!checked);
}

function handleDragOver({over}: DragOverEvent) {
setOverId(over?.id ?? null);
}

function handleDragEnd({over}: DragEndEvent) {
if (over) {
const checked = over.id === State.On;
onChange(checked);
}

setOverId(null);
}

function handleDragCancel() {
setOverId(null);
}
}
25 changes: 25 additions & 0 deletions stories/3 - Examples/FormElements/Switch/Thumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import {useDraggable} from '@dnd-kit/core';
import classNames from 'classnames';

import styles from './Switch.module.css';

export function Thumb() {
const {isDragging, listeners, transform, setNodeRef} = useDraggable({
id: 'thumb',
});

return (
<span className={styles.ThumbWrapper} {...listeners}>
<span
ref={setNodeRef}
className={classNames(styles.Thumb, isDragging && styles.dragging)}
style={
{
'--transform': `${transform?.x ?? 0}px`,
} as React.CSSProperties
}
/>
</span>
);
}
21 changes: 21 additions & 0 deletions stories/3 - Examples/FormElements/Switch/Track.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import {useDroppable} from '@dnd-kit/core';

import {State} from './constants';
import styles from './Switch.module.css';

export function Track() {
const {setNodeRef: setOnRegionNodeRef} = useDroppable({
id: State.On,
});
const {setNodeRef: setOffRegionNodeRef} = useDroppable({
id: State.Off,
});

return (
<span className={styles.Track}>
<span ref={setOffRegionNodeRef} className={styles.OffRegion} />
<span ref={setOnRegionNodeRef} className={styles.OnRegion} />
</span>
);
}
4 changes: 4 additions & 0 deletions stories/3 - Examples/FormElements/Switch/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum State {
On = 'on',
Off = 'off',
}

0 comments on commit 65836ea

Please sign in to comment.