Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/green-weeks-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@channel.io/bezier-react": minor
---

Re-implement `Radio` component.

BREAKING CHANGES

- Legacy `Radio` component is now exported with namespace `LEGACY__Radio`. It will be deprecated.
- New `Radio` component must be used with the new `RadioGroup` component. See below.

```tsx
// AS-IS
<Radio
value={value}
watchingValue={watchingValue}
onClick={() => setWatchingValue(value)}
/>

// TO-BE
<RadioGroup
value={watchingValue}
onChangeValue={setWatchingValue}
>
<Radio value={value} />
</RadioGroup>
```
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
/packages/bezier-react/src/components/Forms/ @sungik-choi
/packages/bezier-react/src/components/Forms/Slider/
/packages/bezier-react/src/components/Forms/Slider/ @Dogdriip
/packages/bezier-react/src/components/Forms/RadioGroup/ @sungik-choi
/packages/bezier-react/src/components/Help/ @dinohan
/packages/bezier-react/src/components/Icon/ @sungik-choi
/packages/bezier-react/src/components/KeyValueListItem/ @sungik-choi
Expand Down
1 change: 1 addition & 0 deletions packages/bezier-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"@channel.io/react-docgen-typescript-plugin": "^1.0.0",
"@mdx-js/react": "^1.6.22",
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-radio-group": "^1.1.0",
"@radix-ui/react-visually-hidden": "^1.0.1",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^19.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { Story, Meta } from '@storybook/react'
/* Internal dependencies */
import { getTitle } from 'Utils/storyUtils'
import { SegmentedControl } from 'Components/Forms/SegmentedControl'
import { Radio } from 'Components/Forms/Radio'
import { RadioGroup, Radio } from 'Components/Forms/RadioGroup'
import { Checkbox } from 'Components/Forms/Checkbox'
import { Text } from 'Components/Text'
import { FormGroup } from 'Components/Forms/FormGroup'
Expand Down Expand Up @@ -41,6 +41,7 @@ Primary.args = {
hasError: false,
disabled: false,
readOnly: false,
required: false,
}

const WithMultiFormTemplate: Story<FormControlProps> = (args) => (
Expand Down Expand Up @@ -94,31 +95,12 @@ const WithMultiFormTemplate: Story<FormControlProps> = (args) => (
</FormControl>

<FormControl {...args}>
<FormLabel help="Lorem Ipsum">Label</FormLabel>
<FormGroup direction="horizontal" spacing={20}>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 10,
}}
>
<Radio>Immediately</Radio>
<Radio>Year(s)</Radio>
<Radio>Seasons</Radio>
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 10,
}}
>
<Radio>Hour(s)</Radio>
<Radio>Day(s)</Radio>
<Radio>Minute(s)</Radio>
</div>
</FormGroup>
<FormLabel>Theme Setting</FormLabel>
<RadioGroup>
<Radio value="system">System Preference</Radio>
<Radio value="light">Light Theme</Radio>
<Radio value="dark">Dark Theme</Radio>
</RadioGroup>
<FormHelperText>Description</FormHelperText>
<FormErrorMessage>Error!</FormErrorMessage>
</FormControl>
Expand All @@ -139,4 +121,5 @@ WithMultiForm.args = {
hasError: false,
disabled: false,
readOnly: false,
required: false,
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@
import type { BezierComponentProps, ChildrenProps } from 'Types/ComponentProps'
import type { StackProps } from 'Components/Stack'

interface FormGroupOptions {
role?: string
}

interface FormGroupProps extends
BezierComponentProps,
ChildrenProps,
Partial<Pick<StackProps, 'direction' | 'spacing'>>,
FormGroupOptions {}
React.HTMLAttributes<HTMLDivElement> {}

export default FormGroupProps
32 changes: 32 additions & 0 deletions packages/bezier-react/src/components/Forms/RadioGroup/Radio.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* External dependencies */
import React, { forwardRef } from 'react'

/* Internal dependencies */
import useId from 'Hooks/useId'
import { RadioProps } from './RadioGroup.types'
import * as Styled from './RadioGroup.styled'

/**
* `Radio` is a checkable button, known as a radio button.
* It is must be a child of `RadioGroup`.
*/
export const Radio = forwardRef(function Radio({
children,
id: idProp,
...rest
}: RadioProps, forwardedRef: React.Ref<HTMLButtonElement>) {
const id = useId(idProp, 'bezier-radio')

return (
<Styled.RadioGroupPrimitiveItem
ref={forwardedRef}
id={id}
{...rest}
>
{ /* @ts-ignore FIXME(@ed): Delete after applying polymorphic props */ }
<Styled.Label htmlFor={id}>
{ children }
</Styled.Label>
</Styled.RadioGroupPrimitiveItem>
)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* External dependencies */
import React, { useState, useEffect } from 'react'
import { base } from 'paths.macro'
import { Meta, Story } from '@storybook/react'

/* Internal dependencies */
import { getTitle } from 'Utils/storyUtils'
import { RadioGroup } from './RadioGroup'
import { Radio } from './Radio'
import { RadioGroupProps } from './RadioGroup.types'

export default {
title: getTitle(base),
component: RadioGroup,
subcomponents: { Radio },
argTypes: {
direction: {
control: {
type: 'radio',
options: ['vertical', 'horizontal'],
},
},
},
} as Meta<RadioGroupProps>

const Template: Story<RadioGroupProps> = ({
value: valueProp,
...props
}) => {
const [value, setValue] = useState(() => valueProp)

useEffect(function watchValueToChange() {
setValue(valueProp)
}, [valueProp])

return (
<RadioGroup
value={value}
onValueChange={setValue}
{...props}
>
<Radio value="System">System</Radio>
<Radio value="Light">Light</Radio>
<Radio value="Dark">Dark</Radio>
</RadioGroup>
)
}

export const Primary = Template.bind({})
Primary.args = {
value: 'System',
disabled: false,
required: false,
direction: 'vertical',
spacing: 0,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/* External dependencies */
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'

/* Internal dependencies */
import { styled, css, Typography } from 'Foundation'
import DisabledOpacity from 'Constants/DisabledOpacity'
import { touchableHover } from 'Utils/styleUtils'
import { Text } from 'Components/Text'
import { FormFieldSize } from 'Components/Forms'
import { focusedInputWrapperStyle } from 'Components/Forms/Inputs/mixins'
import { RadioProps } from './RadioGroup.types'

const OUTER_INDICATOR_SIZE = 18
const OUTER_INDICATOR_MARGIN = 1
const INNER_INDICATOR_SIZE = 8

export const RadioGroupPrimitiveItem = styled(RadioGroupPrimitive.Item)<RadioProps>`
all: unset;
position: relative;
display: flex;
align-items: center;
width: 100%;
height: ${FormFieldSize.M}px;
cursor: pointer;

/* Outer Indicator */
&::before {
position: relative;
box-sizing: border-box;
width: ${OUTER_INDICATOR_SIZE}px;
height: ${OUTER_INDICATOR_SIZE}px;
margin: ${OUTER_INDICATOR_MARGIN}px;
content: '';
background-color: var(--bg-white-normal);
border-radius: 50%;
box-shadow: inset 0 0 0 2px var(--bdr-black-dark);
}

/* Inner Indicator */
&::after {
position: absolute;
top: 50%;
left: ${(OUTER_INDICATOR_SIZE / 2) + OUTER_INDICATOR_MARGIN}px;
width: ${INNER_INDICATOR_SIZE}px;
height: ${INNER_INDICATOR_SIZE}px;
content: '';
border-radius: 50%;
transform: translate(-50%, -50%);
}

&[data-disabled] {
cursor: not-allowed;
opacity: ${DisabledOpacity};

&::before {
background-color: var(--bg-black-dark);
box-shadow: none;
}
}

&[data-state='checked'] {
&::before {
background-color: var(--bgtxt-green-normal);
box-shadow: none;
}

&::after {
background-color: var(--bgtxt-absolute-white-dark);
}
}

${touchableHover(css`
&:not([data-disabled])[data-state='checked']::before {
background-color: var(--bgtxt-green-dark);
}

&:not([data-disabled]):not([data-state='checked'])::after {
background-color: var(--bg-black-dark);
}
`)}

&:focus-visible {
&::before {
${focusedInputWrapperStyle}
}
}

${({ interpolation }) => interpolation}
`

export const Label = styled(Text).attrs({
forwardedAs: 'label',
typo: Typography.Size14,
color: 'txt-black-darkest',
})`
padding-left: 12px;
pointer-events: none;
`
Loading