Skip to content

Commit

Permalink
feat(toggle): added new toggle component
Browse files Browse the repository at this point in the history
A toggle, it's like a checkbox but it goes left/right instead of not.

fix #382
  • Loading branch information
aVileBroker committed Jun 2, 2022
1 parent d2dcb59 commit fa9a187
Show file tree
Hide file tree
Showing 7 changed files with 469 additions and 13 deletions.
14 changes: 8 additions & 6 deletions src/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,14 @@ export const CheckboxContainer = styled.div`
${() => {
const { colors } = useTheme();
return `
display: inline-block;
vertical-align: middle;
&:focus-within {
${Box} {
box-shadow: 0 0 5px 0.150rem ${colors.tertiary};
}}`;
display: inline-block;
vertical-align: middle;
&:focus-within {
${Box} {
box-shadow: 0 0 5px 0.150rem ${colors.tertiary};
}
}
`;
}}
`;

Expand Down
13 changes: 6 additions & 7 deletions src/components/Examples/Form.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { name, address, internet, company, phone, commerce, lorem } from 'faker'
import TextInput from '../TextInput';
import Button from '../Button';
import Card from '../Card';
import Checkbox from '../Checkbox';
import Divider from '../Divider';
import Dropdown from '../Dropdown';
import Modal from '../Modal';
Expand All @@ -15,6 +14,7 @@ import Label from '../Label';

import colors from '../../enums/colors';
import variants from '../../enums/variants';
import Toggle from '../Toggle/Toggle';

// All 50 + DC
const stateAbbreviations = [
Expand Down Expand Up @@ -323,16 +323,15 @@ export const ControlledForm: Story = () => {
</Label>

<Label labelText="Notifications" htmlFor="notifications" key="notifications">
<Checkbox
onClick={() => {
<Toggle
onToggle={() => {
setState({ ...state, notifications: !state.notifications });
}}
checked={state.notifications}
checkboxType={Checkbox.Types.check}
inputProps={{ onChange: () => {} }}
>
{state.notifications ? 'Enabled' : 'Disabled'}
</Checkbox>
color={state.notifications ? '#8f8' : 'white'}
/>
{state.notifications ? ' Enabled' : ' Disabled'}
</Label>
</Card>
{isModalOpen && (
Expand Down
82 changes: 82 additions & 0 deletions src/components/Toggle/Toggle.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* eslint-disable dot-notation */
import React from 'react';

import { Story, Meta } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { useArgs } from '@storybook/store';

import { StyledBaseLabel } from '../../htmlElements';
import Text from '../Text';
import Toggle from './Toggle';
import variants from '../../enums/variants';

const Template: Story<typeof Toggle> = args => {
// eslint-disable-next-line react/destructuring-assignment

const [hookArgs, updateArgs] = useArgs();

return (
<>
<h1>
<StyledBaseLabel htmlFor="big-label">
<Toggle
{...args}
inputProps={{ name: 'big-label', id: 'big-label' }}
checked={hookArgs.checked}
onToggle={e => {
action('onToggle')(e);
updateArgs({ checked: !hookArgs.checked });
}}
/>
<Text>&nbsp;&nbsp;H1</Text>
</StyledBaseLabel>
</h1>
<h2>
<StyledBaseLabel>
<Toggle
{...args}
checked={hookArgs.checked}
onToggle={e => {
action('onToggle')(e);
updateArgs({ checked: !hookArgs.checked });
}}
/>
<Text>&nbsp;&nbsp;H2</Text>
</StyledBaseLabel>
</h2>
<StyledBaseLabel>
<Toggle
{...args}
checked={hookArgs.checked}
onToggle={e => {
action('onToggle')(e);
updateArgs({ checked: !hookArgs.checked });
}}
/>
<Text>&nbsp;&nbsp;Normal</Text>
</StyledBaseLabel>
</>
);
};

export const Default = Template.bind({});
Default.args = {
checked: true,
disabled: false,
variant: variants.fill,
};

const containerRef = React.createRef<HTMLDivElement>();
const boxRef = React.createRef<HTMLDivElement>();

export const Ref = Template.bind({});
Ref.args = {
...Default.args,
containerRef,
boxRef,
};

export default {
title: 'Toggle',
component: Toggle,
} as Meta;
226 changes: 226 additions & 0 deletions src/components/Toggle/Toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import React, { RefObject } from 'react';
import styled from 'styled-components';

import { setLightness, mix, transparentize, readableColor } from 'polished';
import { useSwitch } from 'react-aria';
import { useToggleState } from '@react-stately/toggle';
import { useSpring } from '@react-spring/web';
import useMeasure from 'react-use-measure';
import { ResizeObserver } from '@juggle/resize-observer';

import { AnimatedDiv, StyledBaseInput, StyledBaseLabel } from '../../htmlElements';
import { SubcomponentPropsType, StyledSubcomponentType } from '../commonTypes';
import { useAnalytics, useTheme } from '../../context';
import variants from '../../enums/variants';
import { disabledStyles } from '../../utils/color';
import { mergeRefs } from '../../utils/refs';

// Hide toggle visually but remain accessible to screen readers.
// Source: https://polished.js.org/docs/#hidevisually
export const Input = styled(StyledBaseInput).attrs({ type: 'checkbox' })`
border: 0;
clip: rect(0 0 0 0);
clippath: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
white-space: nowrap;
width: 1px;
`;

export const Handle = styled(AnimatedDiv)`
${({ variant, color }) => `
${
variant === variants.outline || variant === variants.text
? `border: 1px solid ${color};
margin-left: -1px;`
: ''
}
${
variant === variants.fill || variant === variants.text
? `
background-color: ${color};
`
: ''
}
${
variant === variants.fill
? `
box-shadow: 0 .125em .125em -.125em ${transparentize(0.3, setLightness(0.2, color))};
`
: ''
}
position: absolute;
top: 50%;
left 0;
transform: translateY(-50%) translateX(0px - calc(.25em / 2));
border-radius: .5em;
width: calc(1em - (.25em));
height: calc(1em - (.25em));
display: flex;
align-items: center;
justify-content: center;
overflow: visible;
margin-right: 0.5em;
`}}
`;

export const Container = styled(StyledBaseLabel)`
${({
color,
variant,
focusRingColor,
disabled,
}: {
color: string;
variant: variants;
focusRingColor: string;
disabled: boolean;
}) => `
${
variant === variants.outline || variant === variants.text
? `border: 1px solid ${color};`
: ''
}
${
variant === variants.fill
? `
background-color: ${mix(0.4, color, readableColor(color))};
box-shadow: 0 .125em .25em -.125em ${transparentize(0.3, setLightness(0.2, color))} inset;
`
: ''
}
&:hover {
background-color: ${mix(0.3, color, readableColor(color))}
}
transition: background-color .2s;
overflow: hidden;
position: relative;
cursor: pointer;
border-radius: .75em;
width: 2em;
height: 1em;
display: inline-block;
vertical-align: top;
&:focus-within {
${Handle} {
box-shadow: 0 0 5px 0.150rem ${focusRingColor};
}
}
${disabled ? disabledStyles() : ''}
`}
`;

const Toggle = ({
color,
StyledContainer = Container,
StyledHandle = Handle,
StyledInput = Input,

toggleContainerProps = {},
handleProps = {},
inputProps = {},

variant = variants.fill,
checked = false,
disabled = false,
onToggle,
containerRef,
handleRef,
inputRef,
}: {
color?: string;

StyledContainer?: StyledSubcomponentType;
StyledHandle?: StyledSubcomponentType;
StyledInput?: StyledSubcomponentType;
StyledIcon?: StyledSubcomponentType;

toggleContainerProps?: SubcomponentPropsType;
handleProps?: SubcomponentPropsType;
inputProps?: SubcomponentPropsType;
iconProps?: SubcomponentPropsType;

variant?: variants;
checked?: boolean;
disabled?: boolean;
onToggle?: (event: React.MouseEvent) => void;

containerRef?: React.RefObject<HTMLLabelElement>;
handleRef?: React.RefObject<HTMLDivElement>;
inputRef?: React.RefObject<HTMLInputElement>;
}): JSX.Element => {
const { colors } = useTheme();

const [measurableContainerRef, containerBounds] = useMeasure({ polyfill: ResizeObserver });
const [measurableHandleRef, handleBounds] = useMeasure({ polyfill: ResizeObserver });

const mergedInputProps = {
isDisabled: disabled,
isSelected: checked,
'aria-label': 'toggle',
...inputProps,
};

const state = useToggleState(mergedInputProps);

const gutterWidth = (containerBounds.height - handleBounds.height) / 2;

const { transform } = useSpring({
transform: checked
? `translateY(-50%) translateX(${containerBounds.width - handleBounds.width - gutterWidth}px)`
: `translateY(-50%) translateX(${gutterWidth}px)`,
config: { tension: 250, friction: 20 },
});

const internalRef = React.useRef<HTMLInputElement>();
const { inputProps: ariaProps } = useSwitch(
mergedInputProps,
state,
internalRef as RefObject<HTMLInputElement>,
);
const handleEventWithAnalytics = useAnalytics();
const handleToggle = (e: any) =>
handleEventWithAnalytics('Toggle', onToggle, 'onToggle', e, toggleContainerProps);

return (
<StyledContainer
color={color || colors.background}
ref={mergeRefs([containerRef, measurableContainerRef])}
variant={variant}
focusRingColor={colors.tertiary}
disabled={disabled}
{...toggleContainerProps}
>
<StyledHandle
color={color || colors.background}
style={{ transform }}
ref={mergeRefs([handleRef, measurableHandleRef])}
checked={checked}
variant={variant}
{...handleProps}
/>
<StyledInput
role="switch"
{...ariaProps}
checked={checked}
onClick={handleToggle}
ref={mergeRefs([inputRef, internalRef])}
{...inputProps}
/>
</StyledContainer>
);
};

Toggle.Handle = Handle;
Toggle.Input = Input;
Toggle.Container = Container;

export default Toggle;
Loading

0 comments on commit fa9a187

Please sign in to comment.