From a389652d6d5fc20fe720c93df28ca94e40f91447 Mon Sep 17 00:00:00 2001 From: Sanghyeok Park Date: Sun, 11 Aug 2024 03:45:29 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20RadioGroup=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RadioGroup/RadioGroup.context.ts | 16 ++ src/components/RadioGroup/RadioGroup.mdx | 14 ++ .../RadioGroup/RadioGroup.stories.tsx | 55 ++++++ src/components/RadioGroup/RadioGroup.style.ts | 165 ++++++++++++++++++ src/components/RadioGroup/RadioGroup.type.ts | 20 +++ .../RadioGroup/hooks/useRadioGroup.tsx | 102 +++++++++++ .../color/semanticColor/semanticColor.type.ts | 3 + .../semanticColor/semanticColorPalette.ts | 2 + 8 files changed, 377 insertions(+) create mode 100644 src/components/RadioGroup/RadioGroup.context.ts create mode 100644 src/components/RadioGroup/RadioGroup.mdx create mode 100644 src/components/RadioGroup/RadioGroup.stories.tsx create mode 100644 src/components/RadioGroup/RadioGroup.style.ts create mode 100644 src/components/RadioGroup/RadioGroup.type.ts create mode 100644 src/components/RadioGroup/hooks/useRadioGroup.tsx diff --git a/src/components/RadioGroup/RadioGroup.context.ts b/src/components/RadioGroup/RadioGroup.context.ts new file mode 100644 index 0000000..f81d6ad --- /dev/null +++ b/src/components/RadioGroup/RadioGroup.context.ts @@ -0,0 +1,16 @@ +import { createContext } from 'react'; + +import { + RadioGroupSizeType, + RadioGroupOrientationType, +} from '@/components/RadioGroup/RadioGroup.type'; + +interface RadioGroupContextProps { + orientation?: RadioGroupOrientationType; + size?: RadioGroupSizeType; +} + +export const RadioGroupContext = createContext({ + size: undefined, + orientation: undefined, +}); diff --git a/src/components/RadioGroup/RadioGroup.mdx b/src/components/RadioGroup/RadioGroup.mdx new file mode 100644 index 0000000..e964d53 --- /dev/null +++ b/src/components/RadioGroup/RadioGroup.mdx @@ -0,0 +1,14 @@ +import { Canvas, Meta, Controls } from '@storybook/blocks'; +import * as RadioGroupStories from './RadioGroup.stories'; + + + +# RadioGroup + +RadioGroup은 단일 선택을 나타낼 수 있는 요소인 Radio Button을 그룹화하여 사용할 수 있도록 도와주는 컴포넌트입니다. + + + + +
+
diff --git a/src/components/RadioGroup/RadioGroup.stories.tsx b/src/components/RadioGroup/RadioGroup.stories.tsx new file mode 100644 index 0000000..c7e6d73 --- /dev/null +++ b/src/components/RadioGroup/RadioGroup.stories.tsx @@ -0,0 +1,55 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { RadioGroupItemProps, RadioGroupProps } from '@/components/RadioGroup/RadioGroup.type'; +import { useRadioGroup } from '@/components/RadioGroup/hooks/useRadioGroup'; + +const meta: Meta & RadioGroupItemProps> = { + title: 'Components/RadioGroup', + parameters: { + layout: 'centered', + }, + args: { + size: 'medium', + orientation: 'vertical', + }, + argTypes: { + size: { + description: 'RadioGroup.Item의 크기를 정합니다.', + control: { + type: 'radio', + }, + options: ['small', 'medium', 'large'], + }, + onValueChange: { + description: '선택된 RadioGroup.Item이 바뀔 때 호출되는 함수입니다.', + }, + orientation: { + description: 'RadioGroup.Item이 나열되는 방향을 정합니다.', + control: { + type: 'radio', + }, + table: { + defaultValue: { summary: 'vertical' }, + }, + options: ['vertical', 'horizontal'], + }, + }, +}; + +export default meta; + +const ControlComponent = (args: object) => { + const RadioGroup = useRadioGroup<'a' | 'b' | 'c'>('b'); + + return ( + + Option A + Option B + Option C + + ); +}; + +export const Control: StoryObj = { + render: ControlComponent, +}; diff --git a/src/components/RadioGroup/RadioGroup.style.ts b/src/components/RadioGroup/RadioGroup.style.ts new file mode 100644 index 0000000..93de6f9 --- /dev/null +++ b/src/components/RadioGroup/RadioGroup.style.ts @@ -0,0 +1,165 @@ +import { css, styled } from 'styled-components'; + +import { + RadioGroupSizeType, + RadioGroupOrientationType, +} from '@/components/RadioGroup/RadioGroup.type'; + +interface StyledRadioGroupFieldsetProps { + $orientation: RadioGroupOrientationType; +} + +interface StyledRadioItemProps { + $size: RadioGroupSizeType; +} + +const getRadioInnerStyle = ($size: RadioGroupSizeType) => { + switch ($size) { + case 'small': + return css` + width: 9.5px; + height: 9.5px; + `; + case 'medium': + return css` + width: 12px; + height: 12px; + `; + case 'large': + return css` + width: 14px; + height: 14px; + `; + default: + return ''; + } +}; + +const getRadioButtonStyle = ($size: RadioGroupSizeType) => { + switch ($size) { + case 'small': + return css` + width: 16px; + height: 16px; + border-width: 1px; + `; + case 'medium': + return css` + width: 20px; + height: 20px; + border-width: 1.25px; + `; + case 'large': + return css` + width: 24px; + height: 24px; + border-width: 1.5px; + `; + default: + return ''; + } +}; + +const getDisabledRadioButtonStyle = ($size: RadioGroupSizeType) => { + switch ($size) { + case 'small': + return css` + border-width: 3.25px; + `; + case 'medium': + return css` + border-width: 4px; + `; + case 'large': + return css` + border-width: 5px; + `; + default: + return ''; + } +}; + +export const StyledRadioGroupFieldset = styled.fieldset` + min-width: 0; + padding: 0; + margin: 0; + border: 0; + + display: flex; + flex-wrap: wrap; + flex-direction: ${({ $orientation }) => ($orientation === 'horizontal' ? 'row' : 'column')}; + gap: 16px; +`; + +export const StyledRadioItemLabel = styled.label` + position: relative; + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + + input[type='radio'] { + appearance: none; + outline: 0; + position: absolute; + width: 0; + height: 0; + } + + &:has([type='radio']:disabled) { + cursor: not-allowed; + } + + button { + display: flex; + justify-content: center; + align-items: center; + + ${({ $size }) => getRadioButtonStyle($size)} + border-style: solid; + border-color: ${({ theme }) => theme.semantic.color.lineBasicMedium}; + border-radius: 50%; + + pointer-events: none; + } + + [data-radio-content='true'] { + color: ${({ theme }) => theme.semantic.color.textBasicSecondary}; + ${({ $size, theme }) => $size === 'small' && theme.typo.C2_Rg_12} + ${({ $size, theme }) => $size === 'medium' && theme.typo.B3_Rg_14} + ${({ $size, theme }) => $size === 'large' && theme.typo.B1_Rg_16} + } + + input[type='radio']:checked ~ button { + border-color: ${({ theme }) => theme.semantic.color.lineBrandPrimary}; + } + + input[type='radio']:checked ~ [data-radio-content='true'] { + color: ${({ theme }) => theme.semantic.color.textBasicPrimary}; + } + + input[type='radio']:disabled ~ button { + border-color: ${({ theme }) => theme.semantic.color.lineBasicMedium}; + ${({ $size }) => getDisabledRadioButtonStyle($size)} + } + + input[type='radio']:disabled ~ [data-radio-content='true'] { + color: ${({ theme }) => theme.semantic.color.textBasicDisabled}; + } + + .inner { + ${({ $size }) => getRadioInnerStyle($size)} + + border-radius: 50%; + background-color: ${({ theme }) => theme.semantic.color.buttonRadioUnselected}; + } + + input[type='radio']:checked ~ button .inner { + background-color: ${({ theme }) => theme.semantic.color.buttonRadioSelected}; + } + + input[type='radio']:disabled ~ button .inner { + opacity: 0; + visibility: hidden; + } +`; diff --git a/src/components/RadioGroup/RadioGroup.type.ts b/src/components/RadioGroup/RadioGroup.type.ts new file mode 100644 index 0000000..b810d7c --- /dev/null +++ b/src/components/RadioGroup/RadioGroup.type.ts @@ -0,0 +1,20 @@ +export type RadioGroupOrientationType = 'horizontal' | 'vertical'; +export type RadioGroupSizeType = 'small' | 'medium' | 'large'; + +export interface RadioGroupValueChangeEvent { + value: Values; + event: React.ChangeEvent; +} + +export interface RadioGroupProps { + size: RadioGroupSizeType; + children: React.ReactNode; + orientation?: RadioGroupOrientationType; + onValueChange?: (e: RadioGroupValueChangeEvent) => void; +} + +export interface RadioGroupItemProps { + value: Values; + children: React.ReactNode; + disabled?: boolean; +} diff --git a/src/components/RadioGroup/hooks/useRadioGroup.tsx b/src/components/RadioGroup/hooks/useRadioGroup.tsx new file mode 100644 index 0000000..fa43536 --- /dev/null +++ b/src/components/RadioGroup/hooks/useRadioGroup.tsx @@ -0,0 +1,102 @@ +import { useContext, useId, useRef } from 'react'; + +import { RadioGroupContext } from '@/components/RadioGroup/RadioGroup.context'; +import { + StyledRadioGroupFieldset, + StyledRadioItemLabel, +} from '@/components/RadioGroup/RadioGroup.style'; +import { RadioGroupItemProps, RadioGroupProps } from '@/components/RadioGroup/RadioGroup.type'; + +export const useRadioGroup = (initialValue?: Values) => { + const radioGroupName = useId(); + const groupRef = useRef(null); + + const RadioGroupItemComponent = ({ + value, + children, + disabled = false, + }: RadioGroupItemProps) => { + const ref = useRef(null); + const { orientation, size } = useContext(RadioGroupContext); + + const getButtonElementFrom = (node: ChildNode | null) => { + if (node instanceof HTMLLabelElement) return node.querySelector('button'); + return null; + }; + + const onKeyDown = (e: React.KeyboardEvent) => { + if (!ref.current || !groupRef.current || !orientation) return; + + const previousButton = getButtonElementFrom(ref.current.previousSibling); + const nextButton = getButtonElementFrom(ref.current.nextSibling); + const firstButton = getButtonElementFrom(groupRef.current.firstChild); + const lastButton = getButtonElementFrom(groupRef.current.lastChild); + + if (!firstButton || !lastButton) return; + + const previousKey = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'; + const nextKey = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'; + + if (e.code === previousKey) { + const button = previousButton || lastButton; + button.click(); + button.focus(); + } + + if (e.code === nextKey) { + const button = nextButton || firstButton; + button.click(); + button.focus(); + } + + if (e.code === 'Space' || e.code === 'Enter') { + e.preventDefault(); + e.currentTarget.closest('label')?.click(); + e.currentTarget.focus(); + } + }; + + if (!size) return null; + + return ( + + + + + +
{children}
+
+ ); + }; + + const RadioGroupComponent = ({ + size, + children, + orientation = 'vertical', + onValueChange, + }: RadioGroupProps) => { + const onInput = (e: React.ChangeEvent) => { + const { target } = e; + if (!(target instanceof HTMLInputElement)) return; + onValueChange?.({ value: target.value as Values, event: e }); + }; + + return ( + + + {children} + + + ); + }; + + return Object.assign(RadioGroupComponent, { Item: RadioGroupItemComponent }); +}; diff --git a/src/style/foundation/color/semanticColor/semanticColor.type.ts b/src/style/foundation/color/semanticColor/semanticColor.type.ts index 000ab15..fb81c97 100644 --- a/src/style/foundation/color/semanticColor/semanticColor.type.ts +++ b/src/style/foundation/color/semanticColor/semanticColor.type.ts @@ -25,6 +25,8 @@ export type SemanticTextStatusColor = MergeVariants<'text', 'status', StatusVari export type SemanticLineBasicColor = MergeVariants<'line', 'basic', 'light' | 'medium' | 'strong'>; +export type SemanticLineBrandColor = MergeVariants<'line', 'brand', 'primary'>; + export type SemanticLineStatusColor = MergeVariants<'line', 'status', StatusVariant>; export type SemanticButtonBoxPrimaryColor = MergeVariants< @@ -102,6 +104,7 @@ export type SemanticColorType = | SemanticTextBrandColor | SemanticTextStatusColor | SemanticLineBasicColor + | SemanticLineBrandColor | SemanticLineStatusColor | SemanticButtonBoxPrimaryColor | SemanticButtonBoxSecondaryColor diff --git a/src/style/foundation/color/semanticColor/semanticColorPalette.ts b/src/style/foundation/color/semanticColor/semanticColorPalette.ts index 161a15f..6799f83 100644 --- a/src/style/foundation/color/semanticColor/semanticColorPalette.ts +++ b/src/style/foundation/color/semanticColor/semanticColorPalette.ts @@ -29,6 +29,8 @@ export const semanticColorPalette: SemanticColorPalette = { lineBasicMedium: primitiveColorPalette.gray200, lineBasicStrong: primitiveColorPalette.gray300, + lineBrandPrimary: primitiveColorPalette.violet500, + lineStatusNegative: primitiveColorPalette.statusRedMain, lineStatusPositive: primitiveColorPalette.violet500, From c5c44865c58431557ce37c8f115fbd3572ff67e3 Mon Sep 17 00:00:00 2001 From: Sanghyeok Park Date: Sun, 11 Aug 2024 23:57:26 +0900 Subject: [PATCH 02/10] =?UTF-8?q?docs:=20=EC=9E=84=EC=8B=9C=EC=A0=80?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/RadioGroup/RadioGroup.mdx | 2 ++ src/components/RadioGroup/RadioGroup.stories.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/RadioGroup/RadioGroup.mdx b/src/components/RadioGroup/RadioGroup.mdx index e964d53..ba13620 100644 --- a/src/components/RadioGroup/RadioGroup.mdx +++ b/src/components/RadioGroup/RadioGroup.mdx @@ -12,3 +12,5 @@ RadioGroup은 단일 선택을 나타낼 수 있는 요소인 Radio Button을

+ +## RadioGroup.Item diff --git a/src/components/RadioGroup/RadioGroup.stories.tsx b/src/components/RadioGroup/RadioGroup.stories.tsx index c7e6d73..19387d9 100644 --- a/src/components/RadioGroup/RadioGroup.stories.tsx +++ b/src/components/RadioGroup/RadioGroup.stories.tsx @@ -39,13 +39,13 @@ const meta: Meta & RadioGroupItemProps> = { export default meta; const ControlComponent = (args: object) => { - const RadioGroup = useRadioGroup<'a' | 'b' | 'c'>('b'); + const RadioGroup = useRadioGroup<'item-1' | 'item-2' | 'item-3'>('item-1'); return ( - Option A - Option B - Option C + Item1 + Item2 + Item3 ); }; From 38209a6f945042f461da94ab6eb8a8685a86226d Mon Sep 17 00:00:00 2001 From: Sanghyeok Park Date: Mon, 12 Aug 2024 00:29:28 +0900 Subject: [PATCH 03/10] =?UTF-8?q?docs:=20RadioGroup=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/RadioGroup/RadioGroup.mdx | 130 ++++++++++++++++- .../RadioGroup/RadioGroup.stories.tsx | 137 +++++++++++++++++- 2 files changed, 265 insertions(+), 2 deletions(-) diff --git a/src/components/RadioGroup/RadioGroup.mdx b/src/components/RadioGroup/RadioGroup.mdx index ba13620..56924bb 100644 --- a/src/components/RadioGroup/RadioGroup.mdx +++ b/src/components/RadioGroup/RadioGroup.mdx @@ -13,4 +13,132 @@ RadioGroup은 단일 선택을 나타낼 수 있는 요소인 Radio Button을

-## RadioGroup.Item +## 개발 시 참고하면 좋을 내용 + +RadioGroup은 단일 선택을 제공할 때에만 사용합니다. +다중 선택을 제공해야한다면 Checkbox 혹은 Switch를 사용합니다. + +
+
+ +## 사용법 + +RadioGroup 컴포넌트는 `useRadioGroup` 훅을 불러와 사용합니다. + +1. 안전한 typing을 위해 RadioGroup의 value를 나열한 타입을 정의합니다. + +```tsx +type RadioGroupValues = '한국어' | '영어' | '일본어'; +``` + +2. `useRadioGroup` 훅을 호출합니다. + +```tsx +import { useRadioGroup } from '@yourssu/design-system-react'; + +const RadioGroup = useRadioGroup(); +``` + +3. 반환된 RadioGroup 컴포넌트를 통해 원하는 콘텐츠를 구성합니다. + +```tsx + + 한국어 + 영어 + 일본어 + +``` + + + +4. 필요하다면 `useRadioGroup` 훅에 기본으로 선택될 value를 전달합니다. + +```tsx +const RadioGroup = useRadioGroup('한국어'); +``` + + + +
+
+ +### RadioGroup: size (필수) + +RadioGroup.Item 컴포넌트의 크기를 정합니다. +가능한 값은 `small`, `medium`, `large` 입니다. + +```tsx +... +... +... +``` + + + +
+
+ +### RadioGroup: orientation (선택) + +RadioGroup.Item 컴포넌트들의 정렬 방향을 정합니다. +가능한 값은 `vertical`, `horizontal` 이며, 기본값은 `vertical` 입니다. + +```tsx +... +... +... +``` + + + +
+
+ +## 예시 + +### RadioGroup.Item: disabled + +RadioGroup.Item 컴포넌트를 비활성화 상태로 만듭니다. + +```tsx + + 한국어 + + 영어 + + 일본어 + +``` + + + +
+
+ +### 변경 감지 이벤트 할당 + +RadioGroup 컴포넌트에 `onValueChange` 이벤트를 할당하여 변경 감지 이벤트를 처리할 수 있습니다. + +```tsx +import { RadioGroupValueChangeEvent } from '@yourssu/design-system-react'; +``` + +```tsx +const RadioGroup = useRadioGroup('한국어'); + +const onValueChange = (e: RadioGroupValueChangeEvent) => { + const { value, event } = e; + alert(`선택된 값: ${value}`); + console.log(event); +}; + +return ( + + 한국어입니다 + 영어입니다 + 일본어입니다 + +); +``` + + diff --git a/src/components/RadioGroup/RadioGroup.stories.tsx b/src/components/RadioGroup/RadioGroup.stories.tsx index 19387d9..fecdd2a 100644 --- a/src/components/RadioGroup/RadioGroup.stories.tsx +++ b/src/components/RadioGroup/RadioGroup.stories.tsx @@ -1,6 +1,10 @@ import { Meta, StoryObj } from '@storybook/react'; -import { RadioGroupItemProps, RadioGroupProps } from '@/components/RadioGroup/RadioGroup.type'; +import { + RadioGroupItemProps, + RadioGroupProps, + RadioGroupValueChangeEvent, +} from '@/components/RadioGroup/RadioGroup.type'; import { useRadioGroup } from '@/components/RadioGroup/hooks/useRadioGroup'; const meta: Meta & RadioGroupItemProps> = { @@ -50,6 +54,137 @@ const ControlComponent = (args: object) => { ); }; +const UsageComponent = () => { + const RadioGroup = useRadioGroup<'한국어' | '영어' | '일본어'>(); + + return ( + + 한국어 + 영어 + 일본어 + + ); +}; + +const UsageDefaultComponent = () => { + const RadioGroup = useRadioGroup<'한국어' | '영어' | '일본어'>('한국어'); + + return ( + + 한국어 + 영어 + 일본어 + + ); +}; + +const SizeComponent = () => { + const RadioGroup = useRadioGroup<'한국어' | '영어' | '일본어'>(); + + return ( +
+ +
- small
+ 한국어 + 영어 + 일본어 +
+ +
- medium
+ 한국어 + 영어 + 일본어 +
+ +
- large
+ 한국어 + 영어 + 일본어 +
+
+ ); +}; + +const OrientationComponent = () => { + const RadioGroup = useRadioGroup<'한국어' | '영어' | '일본어'>(); + + return ( +
+ + 한국어 + 영어 + 일본어 + + + 한국어 + 영어 + 일본어 + + + 한국어 + 영어 + 일본어 + +
+ ); +}; + +const EventComponent = () => { + const RadioGroup = useRadioGroup<'한국어' | '영어' | '일본어'>('한국어'); + + const onValueChange = (e: RadioGroupValueChangeEvent<'한국어' | '영어' | '일본어'>) => { + const { value, event } = e; + alert(`선택된 값: ${value}`); + console.log(event); + }; + + return ( + + 한국어입니다 + 영어입니다 + 일본어입니다 + + ); +}; + +const DisabledComponent = () => { + const RadioGroup = useRadioGroup<'한국어' | '영어' | '일본어'>(); + + return ( + + 한국어 + + 영어 + + 일본어 + + ); +}; + export const Control: StoryObj = { render: ControlComponent, }; + +export const Usage: StoryObj = { + render: UsageComponent, +}; + +export const UsageDefault: StoryObj = { + render: UsageDefaultComponent, +}; + +export const Size: StoryObj = { + render: SizeComponent, +}; + +export const Orientation: StoryObj = { + render: OrientationComponent, +}; + +export const Disabled: StoryObj = { + render: DisabledComponent, +}; + +export const Event: StoryObj = { + render: EventComponent, +}; From 164a566030b3379c85f16c6a58bc18ddd6bb33b2 Mon Sep 17 00:00:00 2001 From: Sanghyeok Park Date: Mon, 12 Aug 2024 00:30:42 +0900 Subject: [PATCH 04/10] =?UTF-8?q?docs:=20=EC=A0=9C=EB=84=A4=EB=A6=AD=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/RadioGroup/RadioGroup.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RadioGroup/RadioGroup.mdx b/src/components/RadioGroup/RadioGroup.mdx index 56924bb..aba4f29 100644 --- a/src/components/RadioGroup/RadioGroup.mdx +++ b/src/components/RadioGroup/RadioGroup.mdx @@ -126,7 +126,7 @@ import { RadioGroupValueChangeEvent } from '@yourssu/design-system-react'; ```tsx const RadioGroup = useRadioGroup('한국어'); -const onValueChange = (e: RadioGroupValueChangeEvent) => { +const onValueChange = (e: RadioGroupValueChangeEvent) => { const { value, event } = e; alert(`선택된 값: ${value}`); console.log(event); From 2fb3851ca1239ebe344e5740a8f1eb376eec7df4 Mon Sep 17 00:00:00 2001 From: Sanghyeok Park Date: Mon, 12 Aug 2024 00:42:28 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat:=20index.ts=20=EC=97=90=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20export=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/RadioGroup/index.ts | 2 ++ src/components/index.ts | 9 +++++++++ 2 files changed, 11 insertions(+) create mode 100644 src/components/RadioGroup/index.ts diff --git a/src/components/RadioGroup/index.ts b/src/components/RadioGroup/index.ts new file mode 100644 index 0000000..7342522 --- /dev/null +++ b/src/components/RadioGroup/index.ts @@ -0,0 +1,2 @@ +export { useRadioGroup } from './hooks/useRadioGroup'; +export type * from './RadioGroup.type'; diff --git a/src/components/index.ts b/src/components/index.ts index 856bc63..f89dc40 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -30,3 +30,12 @@ export type { TabsProps, TabListProps, TabProps, TabPanelProps } from './Tabs'; export { Fab } from './Fab'; export type { FabHierarchy, FabProps, FabSize } from './Fab'; + +export { useRadioGroup } from './RadioGroup'; +export type { + RadioGroupProps, + RadioGroupItemProps, + RadioGroupSizeType, + RadioGroupOrientationType, + RadioGroupValueChangeEvent, +} from './RadioGroup'; From d1f310b6e1df50cb6adcd65b139e63fb4d9c6fd3 Mon Sep 17 00:00:00 2001 From: Sanghyeok Park Date: Mon, 12 Aug 2024 00:55:08 +0900 Subject: [PATCH 06/10] =?UTF-8?q?fix:=20=EB=B0=B0=EA=B2=BD=20=EC=83=89?= =?UTF-8?q?=EC=83=81=20=ED=88=AC=EB=AA=85=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/RadioGroup/RadioGroup.style.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/RadioGroup/RadioGroup.style.ts b/src/components/RadioGroup/RadioGroup.style.ts index 93de6f9..81d61cc 100644 --- a/src/components/RadioGroup/RadioGroup.style.ts +++ b/src/components/RadioGroup/RadioGroup.style.ts @@ -119,6 +119,7 @@ export const StyledRadioItemLabel = styled.label` border-style: solid; border-color: ${({ theme }) => theme.semantic.color.lineBasicMedium}; border-radius: 50%; + background-color: transparent; pointer-events: none; } From c2569785cea11affa6cdca54f611105094648db6 Mon Sep 17 00:00:00 2001 From: fecapark Date: Sun, 18 Aug 2024 13:15:43 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20radio=20focus=EC=8B=9C=20outline?= =?UTF-8?q?=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=9E=84=EC=9D=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/RadioGroup/RadioGroup.style.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/RadioGroup/RadioGroup.style.ts b/src/components/RadioGroup/RadioGroup.style.ts index 81d61cc..1952d15 100644 --- a/src/components/RadioGroup/RadioGroup.style.ts +++ b/src/components/RadioGroup/RadioGroup.style.ts @@ -122,6 +122,10 @@ export const StyledRadioItemLabel = styled.label` background-color: transparent; pointer-events: none; + + &:focus { + outline: 1px solid black; + } } [data-radio-content='true'] { From cbf2b92aa6ea14c4904cdd83f259ad0ddae89e9c Mon Sep 17 00:00:00 2001 From: fecapark Date: Sun, 18 Aug 2024 13:15:58 +0900 Subject: [PATCH 08/10] =?UTF-8?q?feat:=20=ED=98=84=EC=9E=AC=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=EB=90=9C=20radio=EB=A5=BC=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=ED=95=98=EB=8A=94=20context=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/RadioGroup/RadioGroup.context.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/RadioGroup/RadioGroup.context.ts b/src/components/RadioGroup/RadioGroup.context.ts index f81d6ad..7494be5 100644 --- a/src/components/RadioGroup/RadioGroup.context.ts +++ b/src/components/RadioGroup/RadioGroup.context.ts @@ -8,9 +8,11 @@ import { interface RadioGroupContextProps { orientation?: RadioGroupOrientationType; size?: RadioGroupSizeType; + currentRadioValue?: string; } export const RadioGroupContext = createContext({ size: undefined, orientation: undefined, + currentRadioValue: undefined, }); From fc82e42d10c351fb05cecaf4707d012fcaf24799 Mon Sep 17 00:00:00 2001 From: fecapark Date: Sun, 18 Aug 2024 13:16:34 +0900 Subject: [PATCH 09/10] =?UTF-8?q?feat:=20radiogroup=20focus=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RadioGroup/hooks/useRadioGroup.tsx | 61 +++++++++++++------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/src/components/RadioGroup/hooks/useRadioGroup.tsx b/src/components/RadioGroup/hooks/useRadioGroup.tsx index fa43536..e24674e 100644 --- a/src/components/RadioGroup/hooks/useRadioGroup.tsx +++ b/src/components/RadioGroup/hooks/useRadioGroup.tsx @@ -1,4 +1,4 @@ -import { useContext, useId, useRef } from 'react'; +import { useContext, useId, useRef, useState } from 'react'; import { RadioGroupContext } from '@/components/RadioGroup/RadioGroup.context'; import { @@ -17,13 +17,26 @@ export const useRadioGroup = (initialValue?: Values) => { disabled = false, }: RadioGroupItemProps) => { const ref = useRef(null); - const { orientation, size } = useContext(RadioGroupContext); + const { orientation, size, currentRadioValue } = useContext(RadioGroupContext); + const thisChecked = currentRadioValue === value; const getButtonElementFrom = (node: ChildNode | null) => { if (node instanceof HTMLLabelElement) return node.querySelector('button'); return null; }; + const getKeysetByOrientation = () => { + const previousKey = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'; + const nextKey = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'; + return { previousKey, nextKey }; + }; + + const handleFocusKeyAction = (buttonElement: HTMLButtonElement | null) => { + if (!buttonElement) return; + buttonElement.closest('label')?.click(); + buttonElement.focus(); + }; + const onKeyDown = (e: React.KeyboardEvent) => { if (!ref.current || !groupRef.current || !orientation) return; @@ -31,28 +44,21 @@ export const useRadioGroup = (initialValue?: Values) => { const nextButton = getButtonElementFrom(ref.current.nextSibling); const firstButton = getButtonElementFrom(groupRef.current.firstChild); const lastButton = getButtonElementFrom(groupRef.current.lastChild); - - if (!firstButton || !lastButton) return; - - const previousKey = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'; - const nextKey = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'; + const { previousKey, nextKey } = getKeysetByOrientation(); if (e.code === previousKey) { - const button = previousButton || lastButton; - button.click(); - button.focus(); + e.preventDefault(); + handleFocusKeyAction(previousButton || lastButton); } if (e.code === nextKey) { - const button = nextButton || firstButton; - button.click(); - button.focus(); + e.preventDefault(); + handleFocusKeyAction(nextButton || firstButton); } if (e.code === 'Space' || e.code === 'Enter') { e.preventDefault(); - e.currentTarget.closest('label')?.click(); - e.currentTarget.focus(); + handleFocusKeyAction(e.currentTarget); } }; @@ -66,9 +72,16 @@ export const useRadioGroup = (initialValue?: Values) => { name={radioGroupName} defaultChecked={initialValue === value} disabled={disabled} + tabIndex={-1} + aria-hidden /> - @@ -83,15 +96,25 @@ export const useRadioGroup = (initialValue?: Values) => { orientation = 'vertical', onValueChange, }: RadioGroupProps) => { + const [currentRadioValue, setCurrentRadioValue] = useState(initialValue); + const onInput = (e: React.ChangeEvent) => { const { target } = e; if (!(target instanceof HTMLInputElement)) return; - onValueChange?.({ value: target.value as Values, event: e }); + + const value = target.value as Values; + setCurrentRadioValue(value); + onValueChange?.({ value, event: e }); }; return ( - - + + {children} From e8571878f8503620746b4b22f9d53b65f7056827 Mon Sep 17 00:00:00 2001 From: fecapark Date: Sun, 18 Aug 2024 13:20:24 +0900 Subject: [PATCH 10/10] =?UTF-8?q?feat:=20disabled=EC=8B=9C=20radio=20focus?= =?UTF-8?q?=20=EB=A7=89=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/RadioGroup/hooks/useRadioGroup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RadioGroup/hooks/useRadioGroup.tsx b/src/components/RadioGroup/hooks/useRadioGroup.tsx index e24674e..cf05800 100644 --- a/src/components/RadioGroup/hooks/useRadioGroup.tsx +++ b/src/components/RadioGroup/hooks/useRadioGroup.tsx @@ -18,7 +18,7 @@ export const useRadioGroup = (initialValue?: Values) => { }: RadioGroupItemProps) => { const ref = useRef(null); const { orientation, size, currentRadioValue } = useContext(RadioGroupContext); - const thisChecked = currentRadioValue === value; + const thisChecked = currentRadioValue === value && !disabled; const getButtonElementFrom = (node: ChildNode | null) => { if (node instanceof HTMLLabelElement) return node.querySelector('button');