diff --git a/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx b/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx index f36c9e3..c5f38f5 100644 --- a/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx +++ b/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx @@ -7,6 +7,7 @@ import { ButtonStyles } from '../Button/Button.styles' import { CardStyles } from '../Card/Card.styles' import { CardBodyStyles } from '../CardBody/CardBody.styles' import { CardHeaderStyles } from '../CardHeader/CardHeader.styles' +import { CheckboxStyles } from '../Checkbox/Checkbox.styles' import { CollapseStyles } from '../Collapse/Collapse.styles' import { CollapseHeaderStyles } from '../CollapseHeader/CollapseHeader.styles' import { DropdownStyles } from '../Dropdown/Dropdown.styles' @@ -40,6 +41,7 @@ const componentStyles: Array | SerializedStyles> = CardBodyStyles, TagStyles, TextFieldStyles, + CheckboxStyles, AutocompleteStyles, QuoteStyles, CollapseStyles, diff --git a/packages/lsd-react/src/components/Checkbox/Checkbox.classes.ts b/packages/lsd-react/src/components/Checkbox/Checkbox.classes.ts new file mode 100644 index 0000000..8d224d5 --- /dev/null +++ b/packages/lsd-react/src/components/Checkbox/Checkbox.classes.ts @@ -0,0 +1,15 @@ +export const checkboxClasses = { + root: `lsd-checkbox`, + + input: `lsd-checkbox__input`, + icon: `lsd-checkbox__icon`, + label: `lsd-checkbox__label`, + + focused: `lsd-checkbox--focused`, + disabled: `lsd-checkbox--disabled`, + indeterminate: 'lsd-checkbox--indeterminate', + + large: `lsd-checkbox--large`, + medium: `lsd-checkbox--medium`, + small: 'lsd-checkbox--small', +} diff --git a/packages/lsd-react/src/components/Checkbox/Checkbox.stories.tsx b/packages/lsd-react/src/components/Checkbox/Checkbox.stories.tsx new file mode 100644 index 0000000..0bed9b3 --- /dev/null +++ b/packages/lsd-react/src/components/Checkbox/Checkbox.stories.tsx @@ -0,0 +1,28 @@ +import { Meta, Story } from '@storybook/react' +import { Checkbox, CheckboxProps } from './Checkbox' + +export default { + title: 'Checkbox', + component: Checkbox, + argTypes: { + size: { + type: { + name: 'enum', + value: ['small', 'medium', 'large'], + }, + defaultValue: 'large', + }, + }, +} as Meta + +export const Root: Story = (args) => ( + Checkbox +) + +Root.args = { + size: 'large', + disabled: false, + indeterminate: false, + checked: undefined, + onChange: undefined, +} diff --git a/packages/lsd-react/src/components/Checkbox/Checkbox.styles.ts b/packages/lsd-react/src/components/Checkbox/Checkbox.styles.ts new file mode 100644 index 0000000..bde6ed5 --- /dev/null +++ b/packages/lsd-react/src/components/Checkbox/Checkbox.styles.ts @@ -0,0 +1,59 @@ +import { css } from '@emotion/react' +import { checkboxClasses } from './Checkbox.classes' + +export const CheckboxStyles = css` + .${checkboxClasses.root} { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + } + + .${checkboxClasses.input} { + opacity: 0; + position: absolute; + left: 0; + top: 0; + padding: 0; + margin: 0; + width: 100%; + height: 100%; + } + + .${checkboxClasses.root}:not(.${checkboxClasses.disabled}) { + &:hover, + &.${checkboxClasses.focused} { + text-decoration: underline; + } + + .${checkboxClasses.input} { + cursor: pointer; + } + } + + .${checkboxClasses.disabled} { + opacity: 0.34; + } + + .${checkboxClasses.label} { + margin-left: 18px; + } + + .${checkboxClasses.large} { + .${checkboxClasses.label} { + margin-left: 18px; + } + } + + .${checkboxClasses.medium} { + .${checkboxClasses.label} { + margin-left: 14px; + } + } + + .${checkboxClasses.small} { + .${checkboxClasses.label} { + margin-left: 12px; + } + } +` diff --git a/packages/lsd-react/src/components/Checkbox/Checkbox.tsx b/packages/lsd-react/src/components/Checkbox/Checkbox.tsx new file mode 100644 index 0000000..fca6fc7 --- /dev/null +++ b/packages/lsd-react/src/components/Checkbox/Checkbox.tsx @@ -0,0 +1,97 @@ +import clsx from 'clsx' +import React, { useEffect, useRef, useState } from 'react' +import { useInput } from '../../utils/useInput' +import { CheckboxFilledIcon, CheckboxIcon } from '../Icons' +import { CheckboxIndeterminateIcon } from '../Icons/CheckboxIndeterminate' +import { Typography } from '../Typography' +import { checkboxClasses } from './Checkbox.classes' + +export type CheckboxProps = Omit< + React.LabelHTMLAttributes, + 'onChange' | 'value' | 'color' +> & + Pick< + React.InputHTMLAttributes, + 'onChange' | 'checked' | 'defaultChecked' + > & { + disabled?: boolean + indeterminate?: boolean + size?: 'small' | 'medium' | 'large' + inputProps?: React.InputHTMLAttributes + } + +export const Checkbox: React.FC & { + classes: typeof checkboxClasses +} = ({ + size = 'large', + onChange, + checked, + defaultChecked, + disabled = false, + indeterminate = false, + inputProps = {}, + children, + ...props +}) => { + const ref = useRef(null) + const [focused, setFocused] = useState(false) + const input = useInput({ + value: checked, + defaultValue: defaultChecked ?? false, + onChange, + ref, + }) + + useEffect(() => { + if (!ref.current) return + + const onFocus = () => setFocused(true) + const onBlur = () => setFocused(false) + + ref.current.addEventListener('focus', onFocus) + ref.current.addEventListener('blur', onBlur) + + return () => { + ref.current?.removeEventListener('focus', onFocus) + ref.current?.removeEventListener('blur', onBlur) + } + }, [ref.current]) + + return ( + + + {indeterminate ? ( + + ) : input.value ? ( + + ) : ( + + )} + {children} + + ) +} + +Checkbox.classes = checkboxClasses diff --git a/packages/lsd-react/src/components/Checkbox/index.ts b/packages/lsd-react/src/components/Checkbox/index.ts new file mode 100644 index 0000000..0dab115 --- /dev/null +++ b/packages/lsd-react/src/components/Checkbox/index.ts @@ -0,0 +1 @@ +export * from './Checkbox' diff --git a/packages/lsd-react/src/components/Icons/CheckboxIndeterminate/CheckboxIndeterminateIcon.tsx b/packages/lsd-react/src/components/Icons/CheckboxIndeterminate/CheckboxIndeterminateIcon.tsx new file mode 100644 index 0000000..dad4ef2 --- /dev/null +++ b/packages/lsd-react/src/components/Icons/CheckboxIndeterminate/CheckboxIndeterminateIcon.tsx @@ -0,0 +1,24 @@ +import { LsdIcon } from '../LsdIcon' + +export const CheckboxIndeterminateIcon = LsdIcon( + (props) => ( + + + + ), + { + filled: true, + }, +) diff --git a/packages/lsd-react/src/components/Icons/CheckboxIndeterminate/index.ts b/packages/lsd-react/src/components/Icons/CheckboxIndeterminate/index.ts new file mode 100644 index 0000000..1775ffb --- /dev/null +++ b/packages/lsd-react/src/components/Icons/CheckboxIndeterminate/index.ts @@ -0,0 +1 @@ +export * from './CheckboxIndeterminateIcon' diff --git a/packages/lsd-react/src/index.ts b/packages/lsd-react/src/index.ts index 94a2dc2..36c389e 100644 --- a/packages/lsd-react/src/index.ts +++ b/packages/lsd-react/src/index.ts @@ -5,6 +5,7 @@ export * from './components/Button' export * from './components/Card' export * from './components/CardBody' export * from './components/CardHeader' +export * from './components/Checkbox' export * from './components/Collapse' export * from './components/CollapseHeader' export * from './components/Dropdown' diff --git a/packages/lsd-react/src/utils/useInput.ts b/packages/lsd-react/src/utils/useInput.ts index 9a8925e..8b2fd41 100644 --- a/packages/lsd-react/src/utils/useInput.ts +++ b/packages/lsd-react/src/utils/useInput.ts @@ -1,22 +1,23 @@ import React, { useEffect, useState } from 'react' export type InputValueType = - React.InputHTMLAttributes['value'] + | React.InputHTMLAttributes['value'] + | boolean export type InputOnChangeType = React.InputHTMLAttributes['onChange'] -export type InputProps = { - value?: InputValueType - defaultValue?: InputValueType +export type InputProps = { + value?: T + defaultValue: T onChange?: InputOnChangeType ref?: React.RefObject } -export const useInput = (props: InputProps) => { - const [value, setValue] = useState( - props.value ?? props.defaultValue ?? '', - ) +export const useInput = ( + props: InputProps, +) => { + const [value, setValue] = useState(props.value ?? props.defaultValue) const uncontrolled = typeof props.value === 'undefined' const filled = @@ -27,7 +28,9 @@ export const useInput = (props: InputProps) => { : value.toString().length > 0 const onChange: InputOnChangeType = (event) => { - if (uncontrolled) return setValue(event.target.value) + const type = event.target.type + const value = event.target[type === 'checkbox' ? 'checked' : 'value'] + if (uncontrolled) return setValue(value as T) props.onChange && props.onChange(event) } @@ -46,7 +49,7 @@ export const useInput = (props: InputProps) => { } useEffect(() => { - !uncontrolled && setValue(props.value) + !uncontrolled && setValue(props.value as T) }, [uncontrolled, props.value]) return {