mirror of
https://github.com/acid-info/lsd.git
synced 2025-01-13 10:34:49 +00:00
Merge pull request #18 from acid-info/topic-implement-radio-button
Implement radio button
This commit is contained in:
commit
b71623a84f
@ -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 }> = ({
|
||||
|
@ -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
|
1
packages/lsd-react/src/components/RadioButton/index.ts
Normal file
1
packages/lsd-react/src/components/RadioButton/index.ts
Normal file
@ -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'
|
@ -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'
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user