diff --git a/README.md b/README.md index 6c85e7f..d5fbc8e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,31 @@ simple hook to change input components(uncontroll component) to controll compone ``` +### useRadio + +A hook to use the uncontrolled Radio component as a controlled component. + +#### Generic + +By using generics, you can configure the Radio component in a type-safe manner when using the hook. + +```typescript +type RadioType = 'πŸ•' | 'πŸ”' | '🍟' | '🌭'; +const { value, Radio, RadioGroup } = useRadio('πŸ•'); +``` + +#### Function Arguments + +You can set a `defaultValue`. + +#### Return Values + +`value`: The currently selected value among the Radios. + +`Radio`: The Radio component. You can set the value in a type-safe manner through the hook. You can change the displayed value using children. + +`RadioGroup`: A group that wraps multiple Radios. + ### useInterval simple hook to setInterval with React Component diff --git a/src/stories/useRadio/Docs.mdx b/src/stories/useRadio/Docs.mdx new file mode 100644 index 0000000..2efe79c --- /dev/null +++ b/src/stories/useRadio/Docs.mdx @@ -0,0 +1,31 @@ +import { Canvas, Meta, Description } from '@storybook/blocks'; +import * as Radio from './Radio.stories'; + + + +# useRadio + +λΉ„μ œμ–΄ μ»΄ν¬λ„ŒνŠΈ Radioλ₯Ό μ œμ–΄ μ»΄ν¬λ„ŒνŠΈλ‘œ μ‚¬μš©ν•˜κΈ° μœ„ν•œ ν›…μž…λ‹ˆλ‹€. + +## μ œλ„€λ¦­ + +ν›… μ‚¬μš©μ‹œ μ œλ„€λ¦­μ„ μ‚¬μš©ν•˜μ—¬ type safeν•˜κ²Œ Radioλ₯Ό ꡬ성할 수 μžˆμŠ΅λ‹ˆλ‹€. + +```typescript +type RadioType = 'πŸ•' | 'πŸ”' | '🍟' | '🌭'; +const { value, Radio, RadioGroup } = useRadio('πŸ•'); +``` + +## ν•¨μˆ˜ 인자 + +`defaultValue`λ₯Ό μ„€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€. + +## λ°˜ν™˜κ°’ + +`value` : ν˜„μž¬ Radioλ“€ 쀑 μ„ νƒλœ κ°’μž…λ‹ˆλ‹€. + +`Radio`: Radio μ»΄ν¬λ„ŒνŠΈμž…λ‹ˆλ‹€. 훅을 톡해 type safeν•˜κ²Œ valueλ₯Ό μ„€μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€. children으둜 화면에 ν‘œκΈ°ν•  값을 λ³€κ²½ν•  수 μžˆμŠ΅λ‹ˆλ‹€. + +`RadioGroup`: Radio듀을 묢어쀄 ν•˜λ‚˜μ˜ κ·Έλ£Ήμž…λ‹ˆλ‹€. + + diff --git a/src/stories/useRadio/Radio.stories.ts b/src/stories/useRadio/Radio.stories.ts new file mode 100644 index 0000000..b420314 --- /dev/null +++ b/src/stories/useRadio/Radio.stories.ts @@ -0,0 +1,21 @@ +import { Meta, StoryObj } from '@storybook/react'; +import Radio from './Radio'; + +const meta = { + title: 'hooks/useRadio', + component: Radio, + parameters: { + layout: 'centered', + docs: { + canvas: {}, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const defaultStory: Story = { + args: {}, +}; diff --git a/src/stories/useRadio/Radio.tsx b/src/stories/useRadio/Radio.tsx new file mode 100644 index 0000000..d0b1cc5 --- /dev/null +++ b/src/stories/useRadio/Radio.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import useRadio from '../../useRadio/useRadio'; + +type RadioType = 'πŸ•' | 'πŸ”' | '🍟' | '🌭'; + +export default function Radio() { + const { value, Radio, RadioGroup } = useRadio('πŸ•'); + return ( +
+ + πŸ• + πŸ” + 🍟 + 🌭 + +
+ 였늘 점심은 {value} +
+
+ ); +} diff --git a/src/useRadio/useRadio.test.tsx b/src/useRadio/useRadio.test.tsx new file mode 100644 index 0000000..ed05b30 --- /dev/null +++ b/src/useRadio/useRadio.test.tsx @@ -0,0 +1,61 @@ +import useRadio from './useRadio'; +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; + +type RadioType = '1' | '2' | '3' | '4'; + +const RadioTestComponent = () => { + const { value, Radio, RadioGroup } = useRadio('1'); + return ( +
+ + 1 + 2 + 3 + 4 + +
{value}
+
+ ); +}; + +const RadioNonDefaultTestComponent = () => { + const { value, Radio, RadioGroup } = useRadio(); + return ( +
+ + 1 + 2 + 3 + 4 + +
{value}
+
+ ); +}; +describe('useRadio κΈ°λŠ₯ν…ŒμŠ€νŠΈ', () => { + it('Radioκ°€ μ„ νƒλ˜λ©΄ 값을 λ³€κ²½ν•  수 μžˆλ‹€.', async () => { + render(); + fireEvent.click(await screen.findByText('3')); + const [result] = await screen.findAllByRole('result'); + expect(result.textContent).toBe('3'); + + fireEvent.click(await screen.findByText('1')); + expect(result.textContent).toBe('1'); + + fireEvent.click(await screen.findByText('2')); + expect(result.textContent).toBe('2'); + }); + + it('Default 값이 μ—†λŠ” 경우, checkλ˜μ§€ μ•ŠλŠ” μƒνƒœλ‘œ λ Œλ”λ§λœλ‹€.', async () => { + const { container } = render(); + const result = await screen.findByRole('result'); + expect(result.textContent).toBe(''); + + const labels = container.querySelectorAll('label'); + labels.forEach((label) => { + const input = label.querySelector('input'); + expect(input!.checked).toBeFalsy(); + }); + }); +}); diff --git a/src/useRadio/useRadio.tsx b/src/useRadio/useRadio.tsx new file mode 100644 index 0000000..3d8dfeb --- /dev/null +++ b/src/useRadio/useRadio.tsx @@ -0,0 +1,71 @@ +import React, { CSSProperties, ReactNode, createContext, useContext, useState } from 'react'; + +type RadioValue = string | number; +const RadioContext = createContext<[RadioValue | undefined, React.Dispatch>]>([ + undefined, + () => {}, +]); + +const RadioGroup = ({ + radioState, + className, + style, + children, +}: { + radioState: [T | undefined, React.Dispatch>]; + className?: string; + style?: CSSProperties; + children: ReactNode; +}) => { + return ( +
+ {children} +
+ ); +}; + +const Radio = ({ + value, + className, + style, + children, +}: { + value: T; + className?: string; + style?: CSSProperties; + children: ReactNode; +}) => { + const [radioValue, setRadioValue] = useContext(RadioContext); + + return ( + + ); +}; + +export default function useRadio(defaultValue?: T) { + const radioState = useState(defaultValue); + + const RadioGroupWrapper = ({ className, style, children }: { className?: string; style?: CSSProperties; children: ReactNode }) => { + return ( + + {children} + + ); + }; + + return { + RadioGroup: RadioGroupWrapper, + Radio: Radio, + value: radioState[0], + }; +}