From a2db8e2bb544cda706605c97b94fccb5048fb090 Mon Sep 17 00:00:00 2001 From: Justin Anastos Date: Fri, 24 Jul 2020 10:58:40 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20add=20`Switch=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + src/Switch/Switch.story.mdx | 167 ++++++++++++++++++++++++ src/Switch/index.tsx | 192 ++++++++++++++++++++++++++++ src/Switch/switch.story/Wrapper.tsx | 38 ++++++ src/Switch/switch/Label.tsx | 39 ++++++ 5 files changed, 437 insertions(+) create mode 100644 src/Switch/Switch.story.mdx create mode 100644 src/Switch/index.tsx create mode 100644 src/Switch/switch.story/Wrapper.tsx create mode 100644 src/Switch/switch/Label.tsx diff --git a/.gitignore b/.gitignore index eeef91f8..d0223d72 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ tsconfig.tsbuildinfo /Popover /shared /SpaceKitProvider +/Switch /Table /TextField /Tooltip diff --git a/src/Switch/Switch.story.mdx b/src/Switch/Switch.story.mdx new file mode 100644 index 00000000..ff730495 --- /dev/null +++ b/src/Switch/Switch.story.mdx @@ -0,0 +1,167 @@ +import { Wrapper as Switch } from "./switch.story/Wrapper"; +import { Switch as OriginalSwitch } from "../Switch"; +import { colors } from "../colors"; +import { Meta, Story, ArgsTable, Canvas } from "@storybook/addon-docs/blocks"; +import noop from "lodash/noop"; + + + +# Switch + +**Switchs** 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. + + + + Deslected + + + Selected + + + Disabled Deslected + + + + Disabled Selected + + + + +## Themed + +### Dark + + + + Deslected + + + + Selected + + + + + Disabled Deslected + + + + + Disabled Selected + + + + +## Controlled + +Pass `isSelectded` and `onChange` props to indicate these components are controlled; meaning their state held and controlled externally. + + + + + Deslected + + + + + Selected + + + + + Disabled Deslected + + + + + Disabled Selected + + + + +## Color + +You can customize the color of the checkbox's selected state with the `color` prop + + + + Deslected + + + + Selected + + + + + Disabled Deslected + + + + + Disabled Selected + + + + +## Focus + +Switchs will show a border when focused from keyboard navigation and not from touches or clicks. + + + + + Deslected + + + + + Selected + + + + + Disabled Deslected + + + + + Disabled Selected + + + + +## Size + +Switchs support several sizes + +### Large + + + + Deslected + + + + Selected + + + + + Disabled Deslected + + + + + Disabled Selected + + + + +## Props + + diff --git a/src/Switch/index.tsx b/src/Switch/index.tsx new file mode 100644 index 00000000..b0441e51 --- /dev/null +++ b/src/Switch/index.tsx @@ -0,0 +1,192 @@ +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 "./switch/Label"; + +type SwitchProps = { + /** + * `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" | "dark"; +} & Parameters[0] & + Parameters[0]; + +export const Switch: React.FC & { 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(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 ( + + {({ css }) => ( + + )} + + ); +}; + +Switch.Label = Label; diff --git a/src/Switch/switch.story/Wrapper.tsx b/src/Switch/switch.story/Wrapper.tsx new file mode 100644 index 00000000..44cd79ab --- /dev/null +++ b/src/Switch/switch.story/Wrapper.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { Switch } from "../../Switch"; +import { ClassNames } from "@emotion/core"; +import { colors } from "../../colors"; +import { mergeProps } from "@react-aria/utils"; +import { assertUnreachable } from "../../shared/assertUnreachable"; + +export const Wrapper: React.FC> = ( + props +) => { + return ( + + {({ css, cx }) => ( + + )} + + ); +}; diff --git a/src/Switch/switch/Label.tsx b/src/Switch/switch/Label.tsx new file mode 100644 index 00000000..97aa95bc --- /dev/null +++ b/src/Switch/switch/Label.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { ClassNames } from "@emotion/core"; + +type LabelProps = React.DetailedHTMLProps< + React.LabelHTMLAttributes, + HTMLLabelElement +>; + +/** + * Label used by `Toggle` + * + * This can be used to emulate what a label would look like if it were used by a + * `Toggle` in the even that you can't use the `Toggle`'s label. + */ +export const Label: React.FC = ({ + children, + className, + ...props +}) => { + return ( + + {({ css, cx }) => ( + + )} + + ); +};