Merge pull request #18 from acid-info/topic-implement-radio-button

Implement radio button
This commit is contained in:
jeangovil 2023-03-09 18:38:58 +03:30 committed by GitHub
commit b71623a84f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 382 additions and 1 deletions

View File

@ -16,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'
@ -46,6 +48,8 @@ const componentStyles: Array<ReturnType<typeof withTheme> | SerializedStyles> =
QuoteStyles,
CollapseStyles,
CollapseHeaderStyles,
RadioButtonStyles,
RadioButtonGroupStyles,
]
export const CSSBaseline: React.FC<{ theme?: Theme }> = ({

View File

@ -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,
},
)

View File

@ -0,0 +1 @@
export * from './RadioButtonFilledIcon'

View File

@ -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,
},
)

View File

@ -0,0 +1 @@
export * from './RadioButtonIcon'

View File

@ -17,3 +17,5 @@ export * from './NavigateNextIcon'
export * from './NewPageIcon'
export * from './SearchIcon'
export * from './PickIcon'
export * from './RadioButtonIcon'
export * from './RadioButtonFilledIcon'

View File

@ -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',
}

View File

@ -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',
}

View File

@ -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;
}
}
`

View File

@ -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

View File

@ -0,0 +1 @@
export * from './RadioButton'

View File

@ -0,0 +1,4 @@
export const radioButtonGroupClasses = {
root: `lsd-radio-button-group`,
label: `lsd-radio-button-group__label`,
}

View File

@ -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)

View File

@ -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',
}

View File

@ -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;
}
`

View File

@ -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

View File

@ -0,0 +1 @@
export * from './RadioButtonGroup'

View File

@ -18,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'

View File

@ -29,7 +29,10 @@ export const useInput = <T extends InputValueType = InputValueType>(
const onChange: InputOnChangeType = (event) => {
const type = event.target.type
const value = event.target[type === 'checkbox' ? 'checked' : 'value']
const value =
event.target[
type === 'checkbox' || type === 'radio' ? 'checked' : 'value'
]
if (uncontrolled) return setValue(value as T)
props.onChange && props.onChange(event)
}