diff --git a/client/src/components/Design/components/Checkbox/Checkbox.stories.tsx b/client/src/components/Design/components/Checkbox/Checkbox.stories.tsx index a48efe17..c7ebd21e 100644 --- a/client/src/components/Design/components/Checkbox/Checkbox.stories.tsx +++ b/client/src/components/Design/components/Checkbox/Checkbox.stories.tsx @@ -1,57 +1,99 @@ /** @jsxImportSource @emotion/react */ import type {Meta, StoryObj} from '@storybook/react'; -import {useEffect, useState} from 'react'; +import {useState} from 'react'; + +import Text from '../Text/Text'; import Checkbox from './Checkbox'; const meta = { title: 'Components/Checkbox', component: Checkbox, - tags: ['autodocs'], parameters: { layout: 'centered', + docs: { + description: { + component: ` +Checkbox 컴포넌트는 사용자가 여러 옵션 중에서 하나 이상을 선택할 수 있게 해주는 컴포넌트입니다. + +### 주요 기능 +- **체크 상태 관리**: checked prop으로 체크 상태를 제어할 수 있습니다. +- **우측 컨텐츠**: right prop으로 체크박스 우측에 텍스트나 컴포넌트를 추가할 수 있습니다. +- **접근성**: 키보드 탐색 및 스크린리더 지원 +- **비활성화**: disabled prop으로 체크박스를 비활성화할 수 있습니다. + +### 사용 예시 +\`\`\`jsx +// 기본 사용 + + +// 우측 텍스트 추가 +체크박스 라벨} +/> + +// 비활성화 상태 + +\`\`\` +`, + }, + }, }, + tags: ['autodocs'], argTypes: { - labelText: { - description: '', - control: {type: 'text'}, + checked: { + description: '체크박스의 체크 상태를 제어합니다.', + control: 'boolean', + defaultValue: false, }, - isChecked: { - description: '', - control: {type: 'boolean'}, + right: { + description: '체크박스 우측에 표시될 element입니다.', + }, + disabled: { + description: '체크박스의 비활성화 상태를 제어합니다.', + control: 'boolean', + defaultValue: false, }, onChange: { - description: '', - control: {type: 'object'}, + description: '체크박스 상태가 변경될 때 호출되는 콜백 함수입니다.', }, }, } satisfies Meta; export default meta; - type Story = StoryObj; -export const Playground: Story = { - args: { - isChecked: false, - onChange: () => {}, - labelText: '체크박스', - }, - render: ({isChecked, onChange, labelText, ...args}) => { - const [isCheckedState, setIsCheckedState] = useState(isChecked); - const [labelTextState, setLabelTextState] = useState(labelText); - - useEffect(() => { - setIsCheckedState(isChecked); - setLabelTextState(labelText); - }, [isChecked, labelText]); +const ControlledCheckbox = ({ + label, + disabled, + defaultChecked, +}: { + label: string; + disabled?: boolean; + defaultChecked?: boolean; +}) => { + const [checked, setChecked] = useState(defaultChecked); + return ( + setChecked(e.target.checked)} + right={{label}} + disabled={disabled} + /> + ); +}; - const handleToggle = () => { - setIsCheckedState(!isCheckedState); - onChange(); - }; +export const Default: Story = { + render: args => , +}; - return ; - }, +export const DisabledStates: Story = { + render: args => , }; diff --git a/client/src/components/Design/components/Checkbox/Checkbox.style.ts b/client/src/components/Design/components/Checkbox/Checkbox.style.ts index 7fe78129..f16d581e 100644 --- a/client/src/components/Design/components/Checkbox/Checkbox.style.ts +++ b/client/src/components/Design/components/Checkbox/Checkbox.style.ts @@ -3,7 +3,8 @@ import {css} from '@emotion/react'; import {WithTheme} from '@components/Design/type/withTheme'; interface CheckboxStyleProps { - isChecked: boolean; + checked: boolean; + disabled?: boolean; } export const checkboxStyle = () => @@ -15,24 +16,34 @@ export const checkboxStyle = () => cursor: 'pointer', }); -export const inputGroupStyle = ({theme, isChecked}: WithTheme) => +export const boxStyle = ({theme, checked, disabled}: WithTheme) => css({ - position: 'relative', - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - - '.check-icon': { - position: 'absolute', - }, + width: '1.375rem', + height: '1.375rem', + border: '1px solid', + borderRadius: '0.5rem', + borderColor: checked ? theme.colors.primary : theme.colors.tertiary, + backgroundColor: checked ? theme.colors.primary : theme.colors.white, - '.checkbox-input': { - width: '1.375rem', - height: '1.375rem', - border: '1px solid', + transition: 'all 0.2s', + transitionTimingFunction: 'cubic-bezier(0.7, 0, 0.3, 1)', + '&:focus-visible': { + outline: `2px solid ${theme.colors.primary}`, + outlineOffset: '2px', borderRadius: '0.5rem', - borderColor: isChecked ? theme.colors.primary : theme.colors.tertiary, - backgroundColor: isChecked ? theme.colors.primary : theme.colors.white, }, + opacity: disabled ? 0.4 : 1, + }); + +export const invisibleInputStyle = () => + css({ + position: 'absolute', + width: '1px', + height: '1px', + padding: 0, + margin: '-1px', + overflow: 'hidden', + clip: 'rect(0,0,0,0)', + whiteSpace: 'nowrap', + border: 0, }); diff --git a/client/src/components/Design/components/Checkbox/Checkbox.tsx b/client/src/components/Design/components/Checkbox/Checkbox.tsx index 2ee2e2c3..4b58be53 100644 --- a/client/src/components/Design/components/Checkbox/Checkbox.tsx +++ b/client/src/components/Design/components/Checkbox/Checkbox.tsx @@ -1,28 +1,67 @@ /** @jsxImportSource @emotion/react */ +import {forwardRef, useState} from 'react'; + import {useTheme} from '@components/Design/theme/HDesignProvider'; +import {ariaProps, nonAriaProps} from '@components/Design/utils/attribute'; -import Text from '../Text/Text'; import {IconCheck} from '../Icons/Icons/IconCheck'; -import {checkboxStyle, inputGroupStyle} from './Checkbox.style'; - -interface Props { - labelText?: string; - isChecked: boolean; - onChange: () => void; -} - -const Checkbox = ({labelText, isChecked = false, onChange}: Props) => { - const {theme} = useTheme(); - return ( - - - {isChecked ? : null} - - - {labelText && {labelText}} - - ); -}; +import {boxStyle, checkboxStyle, invisibleInputStyle} from './Checkbox.style'; +import {CheckboxProps} from './Checkbox.type'; + +const Checkbox = forwardRef( + ({right, checked: controlledChecked, onChange, defaultChecked = false, disabled, ...props}, ref) => { + const {theme} = useTheme(); + const [internalChecked, setInternalChecked] = useState(defaultChecked); + + const isControlled = controlledChecked !== undefined; + const checked = isControlled ? controlledChecked : internalChecked; + + const handleChange = (e: React.ChangeEvent) => { + if (!isControlled) { + setInternalChecked(e.target.checked); + } + onChange?.(e); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault(); + const input = e.currentTarget.querySelector('input'); + if (input) { + input.click(); + } + } + }; + + return ( + + + + {checked && } + + + + {right} + + ); + }, +); export default Checkbox; diff --git a/client/src/components/Design/components/Checkbox/Checkbox.type.ts b/client/src/components/Design/components/Checkbox/Checkbox.type.ts new file mode 100644 index 00000000..423e652d --- /dev/null +++ b/client/src/components/Design/components/Checkbox/Checkbox.type.ts @@ -0,0 +1,5 @@ +import {InputHTMLAttributes, ReactNode} from 'react'; + +export interface CheckboxProps extends Omit, 'type'> { + right?: ReactNode; +} diff --git a/client/src/components/Design/components/CreatedEvent/CreatedEvent.tsx b/client/src/components/Design/components/CreatedEvent/CreatedEvent.tsx index c32c1f93..bfa88810 100644 --- a/client/src/components/Design/components/CreatedEvent/CreatedEvent.tsx +++ b/client/src/components/Design/components/CreatedEvent/CreatedEvent.tsx @@ -50,7 +50,7 @@ export function CreatedEventItem({isEditMode, setEditMode, isChecked, onChange, return ( - {isEditMode && onChange(createdEvent)} />} + {isEditMode && onChange(createdEvent)} />} ) => { + const ariaAttributes = Object.entries(props).reduce( + (acc, [key, value]) => { + if (key.startsWith('aria-')) { + acc[key] = value; + } + return acc; + }, + {} as Record, + ); + + return ariaAttributes; +}; + +export const nonAriaProps = (props: React.HTMLAttributes) => { + const nonAriaAttributes = Object.entries(props).reduce( + (acc, [key, value]) => { + if (!key.startsWith('aria-')) { + acc[key] = value; + } + return acc; + }, + {} as Record, + ); + return nonAriaAttributes; +}; diff --git a/client/src/pages/mypage/withdraw/NotUseServiceStep.tsx b/client/src/pages/mypage/withdraw/NotUseServiceStep.tsx index 3af08793..b5a1236b 100644 --- a/client/src/pages/mypage/withdraw/NotUseServiceStep.tsx +++ b/client/src/pages/mypage/withdraw/NotUseServiceStep.tsx @@ -2,7 +2,7 @@ import {css} from '@emotion/react'; import {WithdrawStep} from '@hooks/useWithdrawFunnel'; -import {Top, Checkbox, FixedButton, Flex} from '@components/Design'; +import {Top, Checkbox, FixedButton, Flex, Text} from '@components/Design'; const NotUseServiceStep = ({handleMoveStep}: {handleMoveStep: (nextStep: WithdrawStep) => void}) => { return ( @@ -22,14 +22,15 @@ const NotUseServiceStep = ({handleMoveStep}: {handleMoveStep: (nextStep: Withdra {/* TODO: (@soha) 백엔드와 어떻게 관리할 지 논의 후에 기능(hook) 추가 예정 */} - {}} labelText="예상했던 서비스가 아님" /> - {}} labelText="디자인이 별로임" /> - {}} labelText="사용하기 불편함" /> - {}} labelText="원하는 기능이 없음" /> - {}} labelText="기타" /> + {}} right={예상했던 서비스가 아님} /> + {}} right={디자인이 별로임} /> + {}} right={사용하기 불편함} /> + {}} right={원하는 기능이 없음} /> + {}} right={기타} /> - {/* TODO: (@soha) checkbox를 하나라도 해야 탈퇴하기 버튼 활성화 */} + {/* TODO: (@soha) checkbox를 하나라도 해야 탈퇴하기 버튼 활성화 */ + /* TODO: (@todari) 현재 회원탈퇴 완료 페이지에서 뒤로가기 가능한 오류 있음!!**/} handleMoveStep('checkBeforeWithdrawing')} onBackClick={() => handleMoveStep('withdrawReason')} diff --git a/client/src/pages/mypage/withdraw/UnableToUseDueToError.tsx b/client/src/pages/mypage/withdraw/UnableToUseDueToError.tsx index 46190b28..25f19c16 100644 --- a/client/src/pages/mypage/withdraw/UnableToUseDueToError.tsx +++ b/client/src/pages/mypage/withdraw/UnableToUseDueToError.tsx @@ -2,7 +2,7 @@ import {css} from '@emotion/react'; import {WithdrawStep} from '@hooks/useWithdrawFunnel'; -import {Top, Checkbox, FixedButton, Flex} from '@components/Design'; +import {Top, Checkbox, FixedButton, Flex, Text} from '@components/Design'; const UnableToUseDueToError = ({handleMoveStep}: {handleMoveStep: (nextStep: WithdrawStep) => void}) => { return ( @@ -21,11 +21,11 @@ const UnableToUseDueToError = ({handleMoveStep}: {handleMoveStep: (nextStep: Wit {/* TODO: (@soha) 백엔드와 어떻게 관리할 지 논의 후에 기능(hook) 추가 예정 */} - {}} labelText="행사 생성" /> - {}} labelText="지출 내역 추가" /> - {}} labelText="정산 초대하기" /> - {}} labelText="관리자에게 송금" /> - {}} labelText="기타" /> + {}} right={행사 생성} /> + {}} right={지출 내역 추가} /> + {}} right={정산 초대하기} /> + {}} right={관리자에게 송금} /> + {}} right={기타} /> {/* TODO: (@soha) checkbox를 하나라도 해야 탈퇴하기 버튼 활성화 */}