mirror of https://github.com/acid-info/lsd.git
Merge pull request #17 from acid-info/topic-implement-checkbox
feat: implement Checkbox component
This commit is contained in:
commit
8835bbbc7c
|
@ -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'
|
||||
|
@ -15,6 +16,8 @@ import { IconButtonStyles } from '../IconButton/IconButton.styles'
|
|||
import { LsdIconStyles } from '../Icons/LsdIcon/LsdIcon.styles'
|
||||
import { ListBoxStyles } from '../ListBox/ListBox.styles'
|
||||
import { QuoteStyles } from '../Quote/Quote.styles'
|
||||
import { RadioButtonStyles } from '../RadioButton/RadioButton.styles'
|
||||
import { RadioButtonGroupStyles } from '../RadioButtonGroup/RadioButtonGroup.styles'
|
||||
import { TabItemStyles } from '../TabItem/TabItem.styles'
|
||||
import { TabsStyles } from '../Tabs/Tabs.styles'
|
||||
import { TagStyles } from '../Tag/Tag.styles'
|
||||
|
@ -40,10 +43,13 @@ const componentStyles: Array<ReturnType<typeof withTheme> | SerializedStyles> =
|
|||
CardBodyStyles,
|
||||
TagStyles,
|
||||
TextFieldStyles,
|
||||
CheckboxStyles,
|
||||
AutocompleteStyles,
|
||||
QuoteStyles,
|
||||
CollapseStyles,
|
||||
CollapseHeaderStyles,
|
||||
RadioButtonStyles,
|
||||
RadioButtonGroupStyles,
|
||||
]
|
||||
|
||||
export const CSSBaseline: React.FC<{ theme?: Theme }> = ({
|
||||
|
|
|
@ -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',
|
||||
}
|
|
@ -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<CheckboxProps> = (args) => (
|
||||
<Checkbox {...args}>Checkbox</Checkbox>
|
||||
)
|
||||
|
||||
Root.args = {
|
||||
size: 'large',
|
||||
disabled: false,
|
||||
indeterminate: false,
|
||||
checked: undefined,
|
||||
onChange: undefined,
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
`
|
|
@ -0,0 +1,100 @@
|
|||
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<HTMLLabelElement>,
|
||||
'onChange' | 'value' | 'color'
|
||||
> &
|
||||
Pick<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
'name' | 'onChange' | 'checked' | 'defaultChecked'
|
||||
> & {
|
||||
disabled?: boolean
|
||||
indeterminate?: boolean
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||
}
|
||||
|
||||
export const Checkbox: React.FC<CheckboxProps> & {
|
||||
classes: typeof checkboxClasses
|
||||
} = ({
|
||||
name,
|
||||
size = 'large',
|
||||
onChange,
|
||||
checked,
|
||||
defaultChecked,
|
||||
disabled = false,
|
||||
indeterminate = false,
|
||||
inputProps = {},
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const ref = useRef<HTMLInputElement>(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 (
|
||||
<Typography
|
||||
color="primary"
|
||||
variant={size === 'large' ? 'label1' : 'label2'}
|
||||
component="label"
|
||||
aria-disabled={disabled ? 'true' : 'false'}
|
||||
{...props}
|
||||
className={clsx(
|
||||
props.className,
|
||||
checkboxClasses.root,
|
||||
checkboxClasses[size],
|
||||
focused && checkboxClasses.focused,
|
||||
disabled && checkboxClasses.disabled,
|
||||
indeterminate && checkboxClasses.indeterminate,
|
||||
)}
|
||||
>
|
||||
<input
|
||||
ref={ref}
|
||||
name={name}
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
checked={input.value}
|
||||
onChange={input.onChange}
|
||||
defaultChecked={defaultChecked}
|
||||
className={clsx(inputProps.className, checkboxClasses.input)}
|
||||
{...inputProps}
|
||||
/>
|
||||
{indeterminate ? (
|
||||
<CheckboxIndeterminateIcon color="primary" focusable={false} />
|
||||
) : input.value ? (
|
||||
<CheckboxFilledIcon color="primary" focusable={false} />
|
||||
) : (
|
||||
<CheckboxIcon color="primary" focusable={false} />
|
||||
)}
|
||||
<span className={checkboxClasses.label}>{children}</span>
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
|
||||
Checkbox.classes = checkboxClasses
|
|
@ -0,0 +1 @@
|
|||
export * from './Checkbox'
|
|
@ -0,0 +1,24 @@
|
|||
import { LsdIcon } from '../LsdIcon'
|
||||
|
||||
export const CheckboxIndeterminateIcon = LsdIcon(
|
||||
(props) => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.91667 1.75C2.27233 1.75 1.75 2.27233 1.75 2.91667V11.0833C1.75 11.7277 2.27233 12.25 2.91667 12.25H11.0833C11.7277 12.25 12.25 11.7277 12.25 11.0833V2.91667C12.25 2.27233 11.7277 1.75 11.0833 1.75H2.91667ZM9.91667 6.41667H4.08333V7.58333H9.91667V6.41667Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
{
|
||||
filled: true,
|
||||
},
|
||||
)
|
|
@ -0,0 +1 @@
|
|||
export * from './CheckboxIndeterminateIcon'
|
|
@ -0,0 +1,26 @@
|
|||
import { LsdIcon } from '../LsdIcon'
|
||||
|
||||
export const RadioButtonFilledIcon = LsdIcon(
|
||||
(props) => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M7.0013 1.16669C3.7813 1.16669 1.16797 3.78002 1.16797 7.00002C1.16797 10.22 3.7813 12.8334 7.0013 12.8334C10.2213 12.8334 12.8346 10.22 12.8346 7.00002C12.8346 3.78002 10.2213 1.16669 7.0013 1.16669ZM7.0013 11.6667C4.42297 11.6667 2.33464 9.57835 2.33464 7.00002C2.33464 4.42169 4.42297 2.33335 7.0013 2.33335C9.57964 2.33335 11.668 4.42169 11.668 7.00002C11.668 9.57835 9.57964 11.6667 7.0013 11.6667Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M7.0013 9.91669C8.61213 9.91669 9.91797 8.61085 9.91797 7.00002C9.91797 5.38919 8.61213 4.08335 7.0013 4.08335C5.39047 4.08335 4.08464 5.38919 4.08464 7.00002C4.08464 8.61085 5.39047 9.91669 7.0013 9.91669Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
{
|
||||
filled: true,
|
||||
},
|
||||
)
|
|
@ -0,0 +1 @@
|
|||
export * from './RadioButtonFilledIcon'
|
|
@ -0,0 +1,22 @@
|
|||
import { LsdIcon } from '../LsdIcon'
|
||||
|
||||
export const RadioButtonIcon = LsdIcon(
|
||||
(props) => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M7.0013 1.16669C3.7813 1.16669 1.16797 3.78002 1.16797 7.00002C1.16797 10.22 3.7813 12.8334 7.0013 12.8334C10.2213 12.8334 12.8346 10.22 12.8346 7.00002C12.8346 3.78002 10.2213 1.16669 7.0013 1.16669ZM7.0013 11.6667C4.42297 11.6667 2.33464 9.57835 2.33464 7.00002C2.33464 4.42169 4.42297 2.33335 7.0013 2.33335C9.57964 2.33335 11.668 4.42169 11.668 7.00002C11.668 9.57835 9.57964 11.6667 7.0013 11.6667Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
{
|
||||
filled: true,
|
||||
},
|
||||
)
|
|
@ -0,0 +1 @@
|
|||
export * from './RadioButtonIcon'
|
|
@ -17,3 +17,5 @@ export * from './NavigateNextIcon'
|
|||
export * from './NewPageIcon'
|
||||
export * from './SearchIcon'
|
||||
export * from './PickIcon'
|
||||
export * from './RadioButtonIcon'
|
||||
export * from './RadioButtonFilledIcon'
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
export const radioButtonClasses = {
|
||||
root: `lsd-radio-button`,
|
||||
|
||||
input: `lsd-radio-button__input`,
|
||||
label: `lsd-radio-button__label`,
|
||||
|
||||
disabled: `lsd-radio-button--disabled`,
|
||||
|
||||
large: `lsd-radio-button--large`,
|
||||
medium: `lsd-radio-button--medium`,
|
||||
small: 'lsd-radio-button--small',
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { Meta, Story } from '@storybook/react'
|
||||
import { RadioButton, RadioButtonProps } from './RadioButton'
|
||||
|
||||
export default {
|
||||
title: 'RadioButton',
|
||||
component: RadioButton,
|
||||
argTypes: {
|
||||
size: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
value: ['small', 'medium', 'large'],
|
||||
},
|
||||
defaultValue: 'large',
|
||||
},
|
||||
},
|
||||
} as Meta
|
||||
|
||||
export const Root: Story<RadioButtonProps> = (args) => (
|
||||
<RadioButton {...args}>RadioButton label</RadioButton>
|
||||
)
|
||||
|
||||
Root.args = {
|
||||
size: 'large',
|
||||
disabled: false,
|
||||
checked: undefined,
|
||||
onChange: undefined,
|
||||
value: '1',
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import { css } from '@emotion/react'
|
||||
import { radioButtonClasses } from './RadioButton.classes'
|
||||
|
||||
export const RadioButtonStyles = css`
|
||||
.${radioButtonClasses.root} {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.${radioButtonClasses.input} {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.${radioButtonClasses.root}:not(.${radioButtonClasses.disabled}) {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.${radioButtonClasses.input} {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.${radioButtonClasses.disabled} {
|
||||
opacity: 0.34;
|
||||
}
|
||||
|
||||
.${radioButtonClasses.label} {
|
||||
margin-left: 18px;
|
||||
}
|
||||
|
||||
.${radioButtonClasses.large} {
|
||||
.${radioButtonClasses.label} {
|
||||
margin-left: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.${radioButtonClasses.medium} {
|
||||
.${radioButtonClasses.label} {
|
||||
margin-left: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.${radioButtonClasses.small} {
|
||||
.${radioButtonClasses.label} {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
`
|
|
@ -0,0 +1,95 @@
|
|||
import clsx from 'clsx'
|
||||
import React, { useRef } from 'react'
|
||||
import { useInput } from '../../utils/useInput'
|
||||
import { RadioButtonFilledIcon, RadioButtonIcon } from '../Icons'
|
||||
import { useRadioButtonGroupContext } from '../RadioButtonGroup/RadioButtonGroup.context'
|
||||
import { Typography } from '../Typography'
|
||||
import { radioButtonClasses } from './RadioButton.classes'
|
||||
|
||||
export type RadioButtonProps = Omit<
|
||||
React.LabelHTMLAttributes<HTMLLabelElement>,
|
||||
'onChange' | 'value' | 'color'
|
||||
> &
|
||||
Pick<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
'onChange' | 'checked' | 'defaultChecked'
|
||||
> & {
|
||||
disabled?: boolean
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
name?: string
|
||||
value: string
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||
}
|
||||
|
||||
export const RadioButton: React.FC<RadioButtonProps> & {
|
||||
classes: typeof radioButtonClasses
|
||||
} = ({
|
||||
size: _size = 'large',
|
||||
onChange,
|
||||
checked: _checked,
|
||||
defaultChecked,
|
||||
disabled = false,
|
||||
value,
|
||||
name: _name,
|
||||
inputProps = {},
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const ref = useRef<HTMLInputElement>(null)
|
||||
|
||||
const radioButtonGroup = useRadioButtonGroupContext()
|
||||
const size = radioButtonGroup?.size ?? _size
|
||||
const name = radioButtonGroup?.name ?? _name ?? ''
|
||||
const selected = radioButtonGroup
|
||||
? radioButtonGroup.value === value
|
||||
: _checked
|
||||
|
||||
const input = useInput({
|
||||
value: selected,
|
||||
defaultValue: defaultChecked ?? false,
|
||||
onChange,
|
||||
ref,
|
||||
})
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
radioButtonGroup
|
||||
? radioButtonGroup.setActiveRadioButton(event.target.value)
|
||||
: input.onChange(event)
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography
|
||||
color="primary"
|
||||
variant={size === 'large' ? 'label1' : 'label2'}
|
||||
component="label"
|
||||
aria-disabled={disabled ? 'true' : 'false'}
|
||||
{...props}
|
||||
className={clsx(
|
||||
props.className,
|
||||
radioButtonClasses.root,
|
||||
radioButtonClasses[size],
|
||||
disabled && radioButtonClasses.disabled,
|
||||
)}
|
||||
>
|
||||
<input
|
||||
ref={ref}
|
||||
name={name}
|
||||
value={value}
|
||||
type="radio"
|
||||
checked={input.value}
|
||||
onChange={handleChange}
|
||||
defaultChecked={defaultChecked}
|
||||
className={clsx(inputProps.className, radioButtonClasses.input)}
|
||||
{...inputProps}
|
||||
/>
|
||||
{input.value ? (
|
||||
<RadioButtonFilledIcon color="primary" focusable={false} />
|
||||
) : (
|
||||
<RadioButtonIcon color="primary" focusable={false} />
|
||||
)}
|
||||
<span className={radioButtonClasses.label}>{children}</span>
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
|
||||
RadioButton.classes = radioButtonClasses
|
|
@ -0,0 +1 @@
|
|||
export * from './RadioButton'
|
|
@ -0,0 +1,4 @@
|
|||
export const radioButtonGroupClasses = {
|
||||
root: `lsd-radio-button-group`,
|
||||
label: `lsd-radio-button-group__label`,
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react'
|
||||
import {
|
||||
ActiveRadioButtonType,
|
||||
RadioButtonGroupProps,
|
||||
} from './RadioButtonGroup'
|
||||
|
||||
export type RadioButtonGroupContextType = {
|
||||
value?: ActiveRadioButtonType | null
|
||||
name?: string | null
|
||||
setActiveRadioButton: (value: ActiveRadioButtonType) => void
|
||||
|
||||
size?: RadioButtonGroupProps['size']
|
||||
}
|
||||
|
||||
export const RadioButtonGroupContext =
|
||||
React.createContext<RadioButtonGroupContextType>(null as any)
|
||||
|
||||
export const useRadioButtonGroupContext = () =>
|
||||
React.useContext(RadioButtonGroupContext)
|
|
@ -0,0 +1,35 @@
|
|||
import { Meta, Story } from '@storybook/react'
|
||||
import { RadioButton } from '../RadioButton'
|
||||
import { RadioButtonGroup, RadioButtonGroupProps } from './RadioButtonGroup'
|
||||
|
||||
export default {
|
||||
title: 'RadioButtonGroup',
|
||||
component: RadioButtonGroup,
|
||||
argTypes: {
|
||||
size: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
value: ['small', 'medium', 'large'],
|
||||
},
|
||||
defaultValue: 'large',
|
||||
},
|
||||
},
|
||||
} as Meta
|
||||
|
||||
export const Root: Story<RadioButtonGroupProps> = (args) => (
|
||||
<RadioButtonGroup name="name" {...args}>
|
||||
<RadioButton value="1">RadioButton label</RadioButton>
|
||||
<RadioButton value="2">RadioButton label</RadioButton>
|
||||
<RadioButton value="3">RadioButton label</RadioButton>
|
||||
<RadioButton value="4">RadioButton label</RadioButton>
|
||||
<RadioButton value="5">RadioButton label</RadioButton>
|
||||
<RadioButton value="6">RadioButton label</RadioButton>
|
||||
<RadioButton value="7">RadioButton label</RadioButton>
|
||||
<RadioButton value="8">RadioButton label</RadioButton>
|
||||
</RadioButtonGroup>
|
||||
)
|
||||
|
||||
Root.args = {
|
||||
size: 'large',
|
||||
label: 'Label',
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { css } from '@emotion/react'
|
||||
import { radioButtonGroupClasses } from './RadioButtonGroup.classes'
|
||||
|
||||
export const RadioButtonGroupStyles = css`
|
||||
.${radioButtonGroupClasses.root} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.${radioButtonGroupClasses.label} {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
`
|
|
@ -0,0 +1,52 @@
|
|||
import clsx from 'clsx'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { RadioButtonGroupContext } from './RadioButtonGroup.context'
|
||||
import { radioButtonGroupClasses } from './RadioButtonGroup.classes'
|
||||
import { Typography } from '../Typography'
|
||||
|
||||
export type ActiveRadioButtonType = string | readonly string[]
|
||||
|
||||
export type RadioButtonGroupProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
value: ActiveRadioButtonType | null
|
||||
name?: string | null
|
||||
onChange?: (value: ActiveRadioButtonType) => void
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
label?: string
|
||||
}
|
||||
|
||||
export const RadioButtonGroup: React.FC<RadioButtonGroupProps> & {
|
||||
classes: typeof radioButtonGroupClasses
|
||||
} = ({ size = 'large', label, value, name, onChange, children, ...props }) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [activeValue, setActiveValue] = useState(value)
|
||||
|
||||
const setActiveRadioButton = (value: ActiveRadioButtonType) => {
|
||||
if (onChange) onChange(value)
|
||||
else setActiveValue(value)
|
||||
}
|
||||
|
||||
useEffect(() => setActiveValue(value), [value])
|
||||
|
||||
return (
|
||||
<RadioButtonGroupContext.Provider
|
||||
value={{ value: activeValue, setActiveRadioButton, name, size }}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={clsx(props.className, radioButtonGroupClasses.root)}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
variant={size === 'small' ? 'label2' : 'label1'}
|
||||
className={radioButtonGroupClasses.label}
|
||||
>
|
||||
{label && label}
|
||||
</Typography>
|
||||
{children}
|
||||
</div>
|
||||
</RadioButtonGroupContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
RadioButtonGroup.classes = radioButtonGroupClasses
|
|
@ -0,0 +1 @@
|
|||
export * from './RadioButtonGroup'
|
|
@ -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'
|
||||
|
@ -17,3 +18,5 @@ export * from './components/TabItem'
|
|||
export * from './components/Tabs'
|
||||
export * from './components/Tag'
|
||||
export * from './components/Theme'
|
||||
export * from './components/RadioButton'
|
||||
export * from './components/RadioButtonGroup'
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
export type InputValueType =
|
||||
React.InputHTMLAttributes<HTMLInputElement>['value']
|
||||
| React.InputHTMLAttributes<HTMLInputElement>['value']
|
||||
| boolean
|
||||
|
||||
export type InputOnChangeType =
|
||||
React.InputHTMLAttributes<HTMLInputElement>['onChange']
|
||||
|
||||
export type InputProps = {
|
||||
value?: InputValueType
|
||||
defaultValue?: InputValueType
|
||||
export type InputProps<T extends InputValueType = InputValueType> = {
|
||||
value?: T
|
||||
defaultValue: T
|
||||
onChange?: InputOnChangeType
|
||||
ref?: React.RefObject<HTMLInputElement>
|
||||
}
|
||||
|
||||
export const useInput = (props: InputProps) => {
|
||||
const [value, setValue] = useState<InputValueType>(
|
||||
props.value ?? props.defaultValue ?? '',
|
||||
)
|
||||
export const useInput = <T extends InputValueType = InputValueType>(
|
||||
props: InputProps<T>,
|
||||
) => {
|
||||
const [value, setValue] = useState<T>(props.value ?? props.defaultValue)
|
||||
|
||||
const uncontrolled = typeof props.value === 'undefined'
|
||||
const filled =
|
||||
|
@ -27,7 +28,12 @@ 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' || type === 'radio' ? 'checked' : 'value'
|
||||
]
|
||||
if (uncontrolled) return setValue(value as T)
|
||||
props.onChange && props.onChange(event)
|
||||
}
|
||||
|
||||
|
@ -46,7 +52,7 @@ export const useInput = (props: InputProps) => {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
!uncontrolled && setValue(props.value)
|
||||
!uncontrolled && setValue(props.value as T)
|
||||
}, [uncontrolled, props.value])
|
||||
|
||||
return {
|
||||
|
|
Loading…
Reference in New Issue