-
Notifications
You must be signed in to change notification settings - Fork 2
feat: 디자인 시스템 - Radio 컴포넌트 구현 #236
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rkdcodus
wants to merge
25
commits into
dev
Choose a base branch
from
feat/221-radio
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
a9b39ed
feat: Radio(basic) 컴포넌트를 구현합니다
rkdcodus 4b32e07
fix: interaction 함수에 borderRadius inherit 속성을 추가합니다
rkdcodus 207c47b
docs: Radio(basic) 컴포넌트 스토리북을 작성합니다
rkdcodus 17ea064
refactor: Radio 컴포넌트에서 ComponentPropsWithoutRef를 확장하여 구현합니다.
rkdcodus 4b0271f
move: Radio 컴포넌트를 radioBasic 폴더로 이동합니다
rkdcodus 12c73cd
feat: RadioContent(empty, left, non-checked) 컴포넌트 스타일을 구현합니다
rkdcodus 27d75c0
fix: Radio(basic) 컴포넌트에서 checked, disabled 조건 여부에 따른 색상을 수정합니다
rkdcodus 576b1bd
docs: Radio(basic) 컴포넌트 checked 스토리를 작성합니다
rkdcodus 9869c91
feat: RadioContent 컴포넌트 (empty, left, checked) 스타일을 구현합니다
rkdcodus 47e3cc6
feat: RadioContent에 left, right 속성에 따른 레이블 위치, 서브 레이블을 구현합니다
rkdcodus 61a8cfc
fix: SUB_LABEL_SIZE 임포트 경로로 인한 버그를 수정합니다
rkdcodus 1181295
fix: align이 right일 때의 서브 레이블 위치를 수정합니다
rkdcodus df2df93
feat: outline RadioContent 컴포넌트 스타일을 추가합니다
rkdcodus e5b4d5c
fix: RADIO_CONTAINER_SIZE에 borderRadius 속성을 추가합니다
rkdcodus 7091720
docs: outline RadioContent를 스토리를 추가합니다
rkdcodus 57fcc46
feat: outline RadioContent에서 disabled 일 때 border Color를 수정합니다
rkdcodus 66805da
refactor: style 컴포넌트명을 수정합니다
rkdcodus bdfebff
fix: interaction 스프레드 시 발생하는 타입에러를 해결합니다
rkdcodus a517759
feat: scheme 토큰화 및 반응형으로 구현합니다
rkdcodus 7256538
feat: Radio 컴포넌트에 cursor pointer 효과를 추가합니다
rkdcodus 5ca8b1f
feat: interaction에 pointerEvents none 속성을 추가합니다
rkdcodus ce95916
fix: subLabel에 하이퍼링크가 들어올 경우, 커서 모양으로 링크를 분별하기 위해 cursor 스타일을 default…
rkdcodus 3842fa9
docs: RadioContent의 하이퍼링크가 달린 레이블 스토리를 작성합니다
rkdcodus 6e80840
refactor: Radio 컴포넌트에 반응형 토큰을 사용합니다
rkdcodus 399dcf1
fix: Radio 컴포넌트 체크 스타일 버그를 수정합니다
rkdcodus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './radioBasic/Radio'; |
63 changes: 63 additions & 0 deletions
63
packages/jds/src/components/Radio/radioBasic/Radio.stories.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import React, { useState } from 'react'; | ||
| import { Meta, StoryObj } from '@storybook/react'; | ||
| import { Radio, RadioProps, RadioSize } from './Radio'; | ||
|
|
||
| const meta: Meta<typeof Radio> = { | ||
| title: 'Components/Radio', | ||
| component: Radio, | ||
| parameters: { | ||
| layout: 'centered', | ||
| }, | ||
| argTypes: { | ||
| radioSize: { | ||
| control: { type: 'radio' }, | ||
| options: ['lg', 'md', 'sm', 'xs'], | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| export default meta; | ||
|
|
||
| type Story = StoryObj<typeof Radio>; | ||
|
|
||
| export const Default: Story = { | ||
| args: { | ||
| radioSize: 'lg', | ||
| }, | ||
| }; | ||
|
|
||
| export const Checked: Story = { | ||
| render: () => <Radio radioSize='md' name='disabledGroup' value='1' checked={true} />, | ||
| }; | ||
|
|
||
| export const Disabled: Story = { | ||
| render: () => ( | ||
| <div style={{ display: 'flex', gap: 20 }}> | ||
| <Radio radioSize='md' name='disabledGroup' value='2' checked={false} disabled={true} /> | ||
| <Radio radioSize='md' name='disabledGroup' value='1' checked={true} disabled={true} /> | ||
| </div> | ||
| ), | ||
| }; | ||
|
|
||
| export const Sizes: Story = { | ||
| render: () => { | ||
| const [checkedSize, setCheckedSize] = useState<RadioSize | undefined>('md'); | ||
|
|
||
| const sizes: RadioProps['radioSize'][] = ['lg', 'md', 'sm', 'xs']; | ||
|
|
||
| return ( | ||
| <div style={{ display: 'flex', gap: 20, alignItems: 'center' }}> | ||
| {sizes.map(radioSize => ( | ||
| <Radio | ||
| key={radioSize} | ||
| radioSize={radioSize} | ||
| name='sizeGroup' | ||
| value={radioSize} | ||
| checked={checkedSize === radioSize} | ||
| onChange={() => setCheckedSize(radioSize)} | ||
| /> | ||
| ))} | ||
| </div> | ||
| ); | ||
| }, | ||
| }; |
80 changes: 80 additions & 0 deletions
80
packages/jds/src/components/Radio/radioBasic/Radio.style.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import styled from '@emotion/styled'; | ||
| import { RadioSize } from './Radio'; | ||
| import { RADIO_SIZE } from './radio.variants'; | ||
| import { interaction, pxToRem } from 'utils'; | ||
|
|
||
| interface RadioStyledProps { | ||
| radioSize: RadioSize; | ||
| } | ||
|
|
||
| export const RadioLabel = styled.label<RadioStyledProps>(({ theme, radioSize }) => { | ||
| const borderSize = RADIO_SIZE[radioSize] | ||
| .border as keyof typeof theme.scheme.desktop.stroke.weight; | ||
|
|
||
| return { | ||
| display: 'inline-flex', | ||
| position: 'relative', | ||
|
|
||
| [`input[type="radio"]:not(:disabled):checked + .visual`]: { | ||
| backgroundColor: theme.color.surface.static.standard, | ||
| border: `${pxToRem(theme.scheme.desktop.stroke.weight[borderSize])} solid ${theme.color.accent.neutral}`, | ||
| [theme.breakPoint.tablet]: { | ||
| border: `${pxToRem(theme.scheme.tablet.stroke.weight[borderSize])} solid ${theme.color.accent.neutral}`, | ||
| }, | ||
| [theme.breakPoint.mobile]: { | ||
| border: `${pxToRem(theme.scheme.mobile.stroke.weight[borderSize])} solid ${theme.color.accent.neutral}`, | ||
| }, | ||
| }, | ||
|
|
||
| [`input[type="radio"]:not(:checked):disabled + .visual`]: { | ||
| backgroundColor: theme.color.surface.standard, | ||
| borderColor: theme.color.stroke.alpha.subtler, | ||
| cursor: 'default', | ||
| ...interaction(theme, 'normal', 'normal', 'default', 'disabled'), | ||
| }, | ||
|
|
||
| [`input[type="radio"]:checked:disabled + .visual`]: { | ||
| backgroundColor: theme.color.fill.subtle, | ||
| border: `${pxToRem(theme.scheme.desktop.stroke.weight[borderSize])}px solid ${theme.color.stroke.subtler}`, | ||
| cursor: 'default', | ||
| ...interaction(theme, 'normal', 'normal', 'default', 'disabled'), | ||
|
|
||
| [theme.breakPoint.tablet]: { | ||
| border: `${pxToRem(theme.scheme.tablet.stroke.weight[borderSize])} solid ${theme.color.accent.neutral}`, | ||
| }, | ||
| [theme.breakPoint.mobile]: { | ||
| border: `${pxToRem(theme.scheme.mobile.stroke.weight[borderSize])} solid ${theme.color.accent.neutral}`, | ||
| }, | ||
| }, | ||
|
|
||
| [`input[type="radio"]:focus-visible + .visual`]: { | ||
| boxShadow: `0 0 0 3px ${theme.color.interaction.focus}`, | ||
| }, | ||
| }; | ||
| }); | ||
|
|
||
| export const RadioInput = styled.input({ | ||
| position: 'absolute', | ||
| width: 1, | ||
| height: 1, | ||
| padding: 0, | ||
| margin: -1, | ||
| border: 0, | ||
| overflow: 'hidden', | ||
| clipPath: 'inset(50%)', | ||
| whiteSpace: 'nowrap', | ||
| }); | ||
|
|
||
| export const RadioSpan = styled.span<RadioStyledProps>(({ theme, radioSize }) => { | ||
| const sizeValue = pxToRem(RADIO_SIZE[radioSize].radioSize); | ||
|
|
||
| return { | ||
| borderRadius: theme.scheme.desktop.radius.max, | ||
| width: sizeValue, | ||
| height: sizeValue, | ||
| border: `1px solid ${theme.color.stroke.alpha.assistive}`, | ||
| backgroundColor: theme.color.surface.shallow, | ||
| cursor: 'pointer', | ||
| ...interaction(theme, 'normal', 'normal', 'default'), | ||
| }; | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { ComponentPropsWithoutRef, forwardRef } from 'react'; | ||
| import { RadioInput, RadioLabel, RadioSpan } from './Radio.style'; | ||
|
|
||
| export type RadioSize = 'lg' | 'md' | 'sm' | 'xs'; | ||
|
|
||
| export interface RadioProps extends ComponentPropsWithoutRef<'input'> { | ||
| radioSize?: RadioSize; | ||
| } | ||
|
|
||
| export const Radio = forwardRef<HTMLInputElement, RadioProps>( | ||
| ({ radioSize = 'md', ...props }, ref) => { | ||
| return ( | ||
| <RadioLabel radioSize={radioSize}> | ||
| <RadioInput ref={ref} type='radio' {...props} /> | ||
| <RadioSpan className='visual' aria-hidden='true' radioSize={radioSize} /> | ||
| </RadioLabel> | ||
|
Comment on lines
+13
to
+16
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 설명해 주신대로 태그 구조가 잘 설정되어있네요~ |
||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| Radio.displayName = 'Radio'; | ||
26 changes: 26 additions & 0 deletions
26
packages/jds/src/components/Radio/radioBasic/radio.variants.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { RadioSize } from './Radio'; | ||
|
|
||
| export const RADIO_SIZE: Record< | ||
| RadioSize, | ||
| { | ||
| radioSize: number; | ||
| border: string; | ||
| } | ||
| > = { | ||
| lg: { | ||
| radioSize: 20, | ||
| border: '6', | ||
| }, | ||
| md: { | ||
| radioSize: 18, | ||
| border: '5', | ||
| }, | ||
| sm: { | ||
| radioSize: 16, | ||
| border: '5', | ||
| }, | ||
| xs: { | ||
| radioSize: 14, | ||
| border: '4', | ||
| }, | ||
| }; |
142 changes: 142 additions & 0 deletions
142
packages/jds/src/components/Radio/radioContent/RadioContent.stories.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| import { ChangeEvent, FormEvent, useState } from 'react'; | ||
| import { Meta, StoryObj } from '@storybook/react'; | ||
| import { RadioContent } from './RadioContent'; | ||
|
|
||
| const meta: Meta<typeof RadioContent> = { | ||
| title: 'Components/RadioContent', | ||
| component: RadioContent, | ||
| parameters: { | ||
| layout: 'centered', | ||
| }, | ||
| argTypes: { | ||
| radioSize: { | ||
| control: { type: 'radio' }, | ||
| options: ['lg', 'md', 'sm', 'xs'], | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| export default meta; | ||
|
|
||
| type Story = StoryObj<typeof RadioContent>; | ||
|
|
||
| export const Default: Story = { | ||
| args: { | ||
| radioSize: 'lg', | ||
| radioStyle: 'empty', | ||
| align: 'left', | ||
| disabled: false, | ||
| subLabelVisible: false, | ||
| subLabel: '레이블', | ||
| children: '레이블', | ||
| }, | ||
| }; | ||
|
|
||
| export const OutlineRadio: Story = { | ||
| render: () => ( | ||
| <div style={{ display: 'flex', gap: 20, alignItems: 'flex-start' }}> | ||
| <RadioContent radioSize='md' radioStyle='outline' value='1'> | ||
| 레이블 | ||
| </RadioContent> | ||
| <RadioContent | ||
| radioSize='md' | ||
| radioStyle='outline' | ||
| subLabelVisible={true} | ||
| subLabel='레이블' | ||
| value='2' | ||
| > | ||
| 레이블 | ||
| </RadioContent> | ||
| <RadioContent radioSize='md' radioStyle='outline' align='right' value='3'> | ||
| 레이블 | ||
| </RadioContent> | ||
| <RadioContent | ||
| radioSize='md' | ||
| radioStyle='outline' | ||
| align='right' | ||
| subLabelVisible={true} | ||
| subLabel='레이블' | ||
| value='4' | ||
| > | ||
| 레이블 | ||
| </RadioContent> | ||
| </div> | ||
| ), | ||
| }; | ||
|
|
||
| export const SubLabelWithHyperlink: Story = { | ||
| render: () => ( | ||
| <RadioContent | ||
| radioSize='md' | ||
| radioStyle='outline' | ||
| subLabelVisible={true} | ||
| subLabel={ | ||
| <> | ||
| 하이퍼링크 | ||
| <a href='https://www.naver.com/' style={{ textDecoration: 'underline' }}> | ||
| (네이버 바로가기) | ||
| </a> | ||
| </> | ||
| } | ||
| value='1' | ||
| > | ||
| 레이블 | ||
| </RadioContent> | ||
| ), | ||
| }; | ||
|
|
||
| export const controlledRadio: Story = { | ||
| render: () => { | ||
| const [checked, setChecked] = useState('korea'); | ||
|
|
||
| const handleGenderChange = (e: ChangeEvent<HTMLInputElement>) => { | ||
| setChecked(e.target.value); | ||
| }; | ||
|
|
||
| const items = ['korea', 'japan', 'us', 'uk']; | ||
|
|
||
| return ( | ||
| <div style={{ display: 'flex', gap: 20 }}> | ||
| {items.map(item => ( | ||
| <RadioContent | ||
| key={item} | ||
| radioSize='md' | ||
| name='radioGroup' | ||
| value={item} | ||
| checked={checked === item} | ||
| onChange={handleGenderChange} | ||
| > | ||
| {item} | ||
| </RadioContent> | ||
| ))} | ||
| <p>결과: {checked}</p> | ||
| </div> | ||
| ); | ||
| }, | ||
| }; | ||
|
|
||
| export const uncontrolledRadio: Story = { | ||
| render: () => ( | ||
| <div> | ||
| <form | ||
| style={{ display: 'flex', gap: 20 }} | ||
| onSubmit={(e: FormEvent<HTMLFormElement>) => { | ||
| e.preventDefault(); | ||
| const form = e.target as HTMLFormElement; | ||
| alert(`${form.namedItem('groupName').value} 확인!`); | ||
| }} | ||
| > | ||
| <RadioContent radioSize='md' name='groupName' value='apple' defaultChecked> | ||
| apple | ||
| </RadioContent> | ||
| <RadioContent radioSize='md' name='groupName' value='banana'> | ||
| banana | ||
| </RadioContent> | ||
| <RadioContent radioSize='md' name='groupName' value='orange'> | ||
| orange | ||
| </RadioContent> | ||
| <button type='submit'>제출 버튼</button> | ||
| </form> | ||
| </div> | ||
| ), | ||
| }; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
radioContent의 스토리북을 보면 비제어형 컴포넌트, 제어형 컴포넌트로 사용될 때의 경우를 모두 표현해주셨는데, 이미 ComponentPropsWithoutRef<'input'> 에 포함되어 있는 값이지만 checked, onChange, defaultChecked(비제어형까지 고려 시) 정도는 명시적으로 표현을 해줘도 좋을 거 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JDS 라디오 설명 란에
사용자는 동시에 하나의 옵션만 선택할 수 있다.고 되어있는데, 해당 부분을 고려 + 라디오 버튼의 주 케이스를 고려했을 때 Controlled Component로 사용되는 케이스가 더 많을 거 같아요.이런 경우, Radio의 상태를 관리할 수 있는 Context까지 생성하는 것도 좋을 거 같아요!
++) 요거는 구현하기에 따라서 어느정도 제어형 컴포넌트의 리렌더링 문제도 해소 가능할 거 같습니다.