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 Jul 24, 2020
1 parent ba463ca commit dd62c7d
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ tsconfig.tsbuildinfo
/SpaceKitProvider
/Table
/TextField
/Toggle
/Tooltip
/typography

Expand Down
137 changes: 137 additions & 0 deletions src/Toggle/Toggle.story.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Toggle as ToggleComponent } from "../Toggle";
import { ClassNames } from "@emotion/core";
import { colors } from "../colors";
import {
Description,
Meta,
Story,
Props,
Preview,
} from "@storybook/addon-docs/blocks";
import { mergeProps } from "@react-aria/utils";
import noop from "lodash/noop";

export function Toggle(props) {
return (
<ClassNames>
{({ css }) => (
<ToggleComponent
{...mergeProps(props, { className: css({ whiteSpace: "nowrap" }) })}
/>
)}
</ClassNames>
);
}

<Meta title="Components|Toggle" />

# Toggle

<Description
of={ToggleComponent}
markdown="**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.

<Preview>
<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>
</Preview>

## Controlled

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

<Preview>
<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>
</Preview>

## Color

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

<Preview>
<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>
</Preview>

## Focus

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

<Preview>
<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>
</Preview>

## Props

<Props of={ToggleComponent} />
118 changes: 118 additions & 0 deletions src/Toggle/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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";

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;
} & Parameters<typeof useSwitch>[0];

export const Toggle: React.FC<ToggleProps> = ({
className,
style,
color = colors.blue.base,
isFocusVisible: isFocusVisibleFromProps,
...props
}) => {
const state = useToggleState(props);
const ref = React.useRef<HTMLInputElement | null>(null);
const { inputProps } = useSwitch(props, state, ref);
const {
isFocusVisible: isFocusVisibleFromFocusRing,
focusProps,
} = useFocusRing(props);

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

return (
<ClassNames>
{({ css, cx }) => (
<label
className={cx(
css({
alignItems: "center",
color: props.isDisabled ? colors.silver.darker : undefined,
display: "flex",
}),
className
)}
style={style}
>
<div
className={css({
backgroundColor: state.isSelected
? getOffsetInPalette(props.isDisabled ? 2 : 0, "lighter", color)
: props.isDisabled
? colors.silver.dark
: colors.grey.light,
borderRadius: 8,
boxShadow: [
isFocusVisible && `0 0 0 2px ${colors.blue.lighter}`,
!props.isDisabled && "inset 0 0 1px 0 rgba(18, 21, 26, 0.4)",
]
.filter((value): value is string => !!value)
.join(", "),
height: 16,
marginRight: 12,
padding: 2,
position: "relative",
width: 32,
})}
aria-hidden="true"
>
<div
className={css({
backgroundColor: colors.white,
borderRadius: "100%",
height: 12,
left: state.isSelected ? 2 : 18,
position: "absolute",
top: 2,
width: 12,
})}
/>
</div>
<VisuallyHidden>
<input {...inputProps} {...focusProps} ref={ref} />
</VisuallyHidden>

{props.children}
</label>
)}
</ClassNames>
);
};

0 comments on commit dd62c7d

Please sign in to comment.