Skip to content
This repository was archived by the owner on Jan 20, 2022. It is now read-only.

Commit

Permalink
✨ add Toggle component
Browse files Browse the repository at this point in the history
  • Loading branch information
justinanastos committed Aug 6, 2020
1 parent 9ae36a8 commit 783c05b
Show file tree
Hide file tree
Showing 5 changed files with 465 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ tsconfig.tsbuildinfo
/SpaceKitProvider
/Table
/TextField
/Toggle
/Tooltip
/typography

Expand Down
167 changes: 167 additions & 0 deletions src/Toggle/Toggle.story.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { ToggleWrapper as Toggle } from "./toggle.story/ToggleWrapper";
import { Toggle as OriginalToggle } from "../Toggle";
import { colors } from "../colors";
import { Meta, Story, ArgsTable, Canvas } from "@storybook/addon-docs/blocks";
import noop from "lodash/noop";

<Meta title="Components/Toggle" component={OriginalToggle} />

# Toggle

**Toggles** use the same props schema as the underlying library from react-spectrum.

## Uncontrolled

Using the `defaultSelected` props to indicate these components are uncontrolled; meaning their state is not stored externally.

<Canvas>
<Story name="Uncontrolled Deselected">
<Toggle>Deslected</Toggle>
</Story>
<Story name="Uncontrolled Selected">
<Toggle defaultSelected>Selected</Toggle>
</Story>
<Story name="Uncontrolled Disabled Deselected">
<Toggle isDisabled>Disabled Deslected</Toggle>
</Story>
<Story name="Uncontrolled Disabled Selected">
<Toggle isDisabled defaultSelected>
Disabled Selected
</Toggle>
</Story>
</Canvas>

## Themed

### Midnight

<Canvas theme="midnight">
<Story name="Midnight Themed Uncontrolled Deselected">
<Toggle theme="midnight">Deslected</Toggle>
</Story>
<Story name="Midnight Themed Uncontrolled Selected">
<Toggle theme="midnight" defaultSelected>
Selected
</Toggle>
</Story>
<Story name="Midnight Themed Uncontrolled Disabled Deselected">
<Toggle theme="midnight" isDisabled>
Disabled Deslected
</Toggle>
</Story>
<Story name="Midnight Themed Uncontrolled Disabled Selected">
<Toggle theme="midnight" isDisabled defaultSelected>
Disabled Selected
</Toggle>
</Story>
</Canvas>

## Controlled

Pass `isSelectded` and `onChange` props to indicate these components are uncontrolled; meaning their state is not stored externally.

<Canvas>
<Story name="Controlled Deselected">
<Toggle isSelected={false} setSelected={noop}>
Deslected
</Toggle>
</Story>
<Story name="Controlled Selected">
<Toggle isSelected setSelected={noop}>
Selected
</Toggle>
</Story>
<Story name="Controlled Disabled Deselected">
<Toggle isDisabled isSelected={false} setSelected={noop}>
Disabled Deslected
</Toggle>
</Story>
<Story name="Controlled Disabled Selected">
<Toggle isDisabled isSelected setSelected={noop}>
Disabled Selected
</Toggle>
</Story>
</Canvas>

## Color

You can customize the color of the checkbox's selected state with the `color` prop

<Canvas>
<Story name="Color Deselected">
<Toggle color={colors.green.base}>Deslected</Toggle>
</Story>
<Story name="Color Selected">
<Toggle color={colors.green.base} defaultSelected>
Selected
</Toggle>
</Story>
<Story name="Color Disabled Deselected">
<Toggle color={colors.green.base} isDisabled>
Disabled Deslected
</Toggle>
</Story>
<Story name="Color Disabled Selected">
<Toggle color={colors.green.base} isDisabled defaultSelected>
Disabled Selected
</Toggle>
</Story>
</Canvas>

## Focus

Toggles will show a border when focused from keyboard navigation and not from touches or clicks.

<Canvas>
<Story name="Focus Deselected">
<Toggle isFocusVisible color={colors.green.base}>
Deslected
</Toggle>
</Story>
<Story name="Focus Selected">
<Toggle isFocusVisible color={colors.green.base} defaultSelected>
Selected
</Toggle>
</Story>
<Story name="Focus Disabled Deselected">
<Toggle isFocusVisible color={colors.green.base} isDisabled>
Disabled Deslected
</Toggle>
</Story>
<Story name="Focus Disabled Selected">
<Toggle isFocusVisible color={colors.green.base} isDisabled defaultSelected>
Disabled Selected
</Toggle>
</Story>
</Canvas>

## Size

Toggles support several sizes

### Large

<Canvas>
<Story name="Large, Deselected">
<Toggle size="large">Deslected</Toggle>
</Story>
<Story name="Large, Selected">
<Toggle size="large" defaultSelected>
Selected
</Toggle>
</Story>
<Story name="Large, Disabled Deselected">
<Toggle size="large" isDisabled>
Disabled Deslected
</Toggle>
</Story>
<Story name="Large, Disabled Selected">
<Toggle size="large" isDisabled defaultSelected>
Disabled Selected
</Toggle>
</Story>
</Canvas>

## Props

<ArgsTable of="." />
197 changes: 197 additions & 0 deletions src/Toggle/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import React from "react";
import { ClassNames } from "@emotion/core";
import { getOffsetInPalette } from "../colors/utils/getOffsetInPalette";
import { ShadedColor, colors } from "../colors";
import { useFocusRing } from "@react-aria/focus";
import { useSwitch } from "@react-aria/switch";
import { useToggleState } from "@react-stately/toggle";
import { VisuallyHidden } from "@react-aria/visually-hidden";
import { assertUnreachable } from "../shared/assertUnreachable";
import { motion } from "framer-motion";
import { useSpaceKitProvider } from "../SpaceKitProvider";
import { Label } from "./toggle/Label";

type ToggleProps = {
/**
* `className` to apply to the bounding `label`
*/
className?: string;
/**
* `style` to apply to the bounding `label`
*/
style?: React.CSSProperties;

/**
* Color to use for the checkbox itself. The check color and the border color
* will be automatically calculated.
*
* @default colors.blue.base
*/
color?: ShadedColor;

/**
* Force the focused styling
*
* This prop is typed as `never` so you can never legally pass it. This is
* intended only for testing because there's no other way to test a focus
* ring. The only place we're actually using this is in an `mdx` file, which
* doesn't check props with TypeScript.
*
* There's got to be a better way to do this; I just don't know what it is
* :shrug:
*/
isFocusVisible?: never;

/**
* Size to display the toggle at
*
* @default "normal"
*/
size?: "normal" | "large";

/**
* Show the "ON" or "OFF" textual state
*
* @default `true`
*/
showTextualState?: boolean;

theme?: "light" | "midnight";
} & Parameters<typeof useSwitch>[0] &
Parameters<typeof useFocusRing>[0];

export const Toggle: React.FC<ToggleProps> & { Label: typeof Label } = ({
className,
style,
color = colors.blue.base,
isFocusVisible: isFocusVisibleFromProps,
showTextualState = true,
size = "normal",
theme: propsTheme,
...props
}) => {
const state = useToggleState(props);
const ref = React.useRef<HTMLInputElement | null>(null);
const { inputProps } = useSwitch(props, state, ref);
const {
isFocusVisible: isFocusVisibleFromFocusRing,
focusProps,
} = useFocusRing(props);
// FYI: Hooks can't be called conditionally, so we must call the hook and then
// use the `||` in the subseuent line instead of combining them.
const { theme: providerTheme } = useSpaceKitProvider();
const theme = propsTheme || providerTheme;

const isFocusVisible =
(!props.isDisabled && isFocusVisibleFromProps) ||
isFocusVisibleFromFocusRing;

/** Size, in pixels, of the dot that will be the switch */
const dotSize =
size === "normal" ? 12 : size === "large" ? 18 : assertUnreachable(size);
const borderSize =
size === "normal" ? 2 : size === "large" ? 3 : assertUnreachable(size);

return (
<ClassNames>
{({ css }) => (
<Label
className={className}
style={style}
theme={theme}
isDisabled={props.isDisabled}
>
<VisuallyHidden>
<input {...inputProps} {...focusProps} ref={ref} />
</VisuallyHidden>

<div
className={css({
flex: 1,
marginRight: showTextualState
? 12
: size === "large"
? 30
: size === "normal"
? undefined
: assertUnreachable(size),
})}
>
{props.children}
</div>

{showTextualState && (
<div
aria-hidden
className={css({
color:
theme === "light"
? props.isDisabled
? undefined
: state.isSelected
? undefined
: colors.grey.base
: theme === "midnight"
? props.isDisabled
? undefined
: state.isSelected
? colors.white
: colors.midnight.lighter
: assertUnreachable(theme),
fontWeight: state.isSelected ? 600 : 400,
marginRight: 8,
})}
>
{state.isSelected ? "ON" : "OFF"}
</div>
)}

<div
aria-hidden
key={props.isDisabled ? "disabled" : "enabled"}
className={css({
backgroundColor: state.isSelected
? getOffsetInPalette(props.isDisabled ? 2 : 0, "lighter", color)
: props.isDisabled
? colors.silver.dark
: colors.grey.light,
borderRadius: dotSize / 2 + borderSize,
boxShadow: [
isFocusVisible && `0 0 0 2px ${colors.blue.lighter}`,
!props.isDisabled && "inset 0 0 1px 0 rgba(18, 21, 26, 0.4)",
]
// The generic attached to the filter return type will indicate
// to TypeScript that we're stripping out all non-strings,
// meaning the booleans.
.filter((value): value is string => !!value)
.join(", "),
cursor: !props.isDisabled ? "pointer" : undefined,
height: dotSize + borderSize * 2,
padding: borderSize,
position: "relative",
width: (8 / 3) * dotSize,
})}
>
<motion.div
animate={{
x: state.isSelected ? dotSize + borderSize * 2 : 0,
}}
initial={false}
transition={{ duration: 0.5, type: "tween", ease: "easeOut" }}
className={css({
backgroundColor: colors.white,
borderRadius: "100%",
height: dotSize,
position: "absolute",
top: borderSize,
width: dotSize,
})}
/>
</div>
</Label>
)}
</ClassNames>
);
};

Toggle.Label = Label;
Loading

0 comments on commit 783c05b

Please sign in to comment.