Skip to content
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

[FE] Checkbox component 변경 #960

Open
wants to merge 13 commits into
base: fe-dev-v3.1.4
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/** @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';

Expand All @@ -13,17 +15,16 @@ const meta = {
layout: 'centered',
},
argTypes: {
labelText: {
right: {
description: '',
control: {type: 'text'},
},
isChecked: {
checked: {
description: '',
control: {type: 'boolean'},
},
onChange: {
description: '',
control: {type: 'object'},
},
},
} satisfies Meta<typeof Checkbox>;
Expand All @@ -32,26 +33,18 @@ export default meta;

type Story = StoryObj<typeof meta>;

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 = () => {
const [checked, setChecked] = useState(false);

const handleToggle = () => {
setIsCheckedState(!isCheckedState);
onChange();
};
return (
<Checkbox
checked={checked}
onChange={e => setChecked(e.target.checked)}
right={<Text size="bodyBold">체크박스</Text>}
/>
);
};

return <Checkbox {...args} isChecked={isCheckedState} onChange={handleToggle} labelText={labelTextState} />;
},
export const Playground: Story = {
render: () => <ControlledCheckbox />,
};
43 changes: 26 additions & 17 deletions client/src/components/Design/components/Checkbox/Checkbox.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {css} from '@emotion/react';
import {WithTheme} from '@components/Design/type/withTheme';

interface CheckboxStyleProps {
isChecked: boolean;
checked: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isChecked보다 checked가 더 자연스럽다고 생각해서 is 키워드를 제거하신 걸까요!?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

애초에 기본적으로 input 태그의 prop으로 있는 이름이 checked 에요
기존에는 input props를 extends하지 않고, isChecked, onChange, labelText 세개의 prop만 받았는데, 이렇게 구현하면 나중에 접근성 태그나 disabled 등 input(Checkbox)를 커스터마이징 하는데 어렵다고 생각해서 input props를 extends 하면서 자연스럽게 isChecked를 input의 기본 prop인 checked로 바꿨습니다

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇군요. 기본 태그를 활용해서 checked로 변경하는 것 동의합니다.

}

export const checkboxStyle = () =>
Expand All @@ -15,24 +15,33 @@ export const checkboxStyle = () =>
cursor: 'pointer',
});

export const inputGroupStyle = ({theme, isChecked}: WithTheme<CheckboxStyleProps>) =>
export const boxStyle = ({theme, checked}: WithTheme<CheckboxStyleProps>) =>
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',
Comment on lines +28 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

와우... 애니메이션의 신 토다리

borderRadius: '0.5rem',
borderColor: isChecked ? theme.colors.primary : theme.colors.tertiary,
backgroundColor: isChecked ? theme.colors.primary : theme.colors.white,
},
});

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,
});
70 changes: 49 additions & 21 deletions client/src/components/Design/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,56 @@
/** @jsxImportSource @emotion/react */
import {forwardRef, useState} from 'react';

import {useTheme} from '@components/Design/theme/HDesignProvider';

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 (
<label css={checkboxStyle}>
<div css={inputGroupStyle({theme, isChecked})}>
{isChecked ? <IconCheck size={20} color="onPrimary" className="check-icon" /> : null}
<input type="checkbox" checked={isChecked} onChange={onChange} className="checkbox-input" />
</div>
{labelText && <Text size="bodyBold">{labelText}</Text>}
</label>
);
};
import {boxStyle, checkboxStyle, invisibleInputStyle} from './Checkbox.style';
import {CheckboxProps} from './Checkbox.type';

const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
({right, checked: controlledChecked, onChange, defaultChecked = false, ...props}, ref) => {
const {theme} = useTheme();
const [internalChecked, setInternalChecked] = useState(defaultChecked);

const isControlled = controlledChecked !== undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

비제어 컴포넌트를 고려하신 이유가 궁금해요!

제가 생각하는 비제어 컴포넌트의 장점은 state의 prop drilling 없이 form submit 시 name으로 값을 가져올 수 있다는 것이에요. 혹시 이점을 고려해서 만든 것인지 아니면 토다리가 생각하는 다른 장점이 있어 비제어 컴포넌트 방식을 고려했는지 궁금합니다~

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제어 컴포넌트와 비제어 컴포넌트 각각의 장점이 있겠지만, 대부분 상황에서 제어 컴포넌트를 사용할 것 같아요. 그렇지만, 비제어 컴포넌트의 가능성을 아에 닫아두는것 보단, 일단 열어두고 사용할 수 있다면 사용할 수 있는게 맞다고 판단했습니다

Copy link
Contributor

@jinhokim98 jinhokim98 Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추후에 비제어로 접근할 수도 있으니 열어둔다는 의미였군요. Checkbox는 공통 컴포넌트이니깐 사용성을 열어둔다.. 좋은 것 같아요!

const checked = isControlled ? controlledChecked : internalChecked;

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!isControlled) {
setInternalChecked(e.target.checked);
}
Comment on lines +20 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나중을 위해 비제어까지 열어서 구현하신게 정말..👍

onChange?.(e);
};

const handleKeyDown = (e: React.KeyboardEvent) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

접근성까지...! 굿굿스입니다 👍

if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
const input = e.currentTarget.querySelector('input');
if (input) {
input.click();
Comment on lines +29 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 내부의 input을 찾아서 클릭 좋네요

}
}
};

return (
<label css={checkboxStyle} role="checkbox" aria-checked={checked}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

접근성까지 야무지게 챙겨가는 토다리 좋습니다~

<div css={boxStyle({theme, checked})} aria-hidden="true" tabIndex={0} onKeyDown={handleKeyDown}>
{checked && <IconCheck size={20} color="onPrimary" />}
<input
ref={ref}
type="checkbox"
checked={checked}
onChange={handleChange}
css={invisibleInputStyle}
aria-label="체크박스"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어떤 체크박스인지 aria-label에 넣어주면 더 좋을 것 같아요!

예를 들어, [x] 소하
라는 체크박스가 존재할 경우, aria-labe이 '소하 체크박스'라고 읽을 수 있게요!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스토리북에서 확인했을 때는 '소하 체크박스' 이런식으로 잘 읽히네요! 대신에 '체크박스 소하 체크박스' 이런식으로 두번 읽히네욤!
아, 그리고 스토리북이랑 실제 서비스에 사용될때랑 스크린리더가 읽는게 조금 다른 것 같아서 서비스에서도 잘 되는지 확인해봐야 할 것 같습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그러면, aria-label이 있으면, 그대로 내보내고 없다면 체크박스 가 되는 식으로 하는게 나을 것 같네요!

{...props}
/>
</div>
{right ?? null}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 널리쉬를 안쓰면 문제가 생기나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right가 없이 undefined 상태에서도 그대로 없는 상태로 렌더 됩니다
빼는게 좋을까요? 타입 없이 마크업 부분만 보고도, right가 없으면 렌더되지 않는구나 라고 보여주고 싶은 의도긴 했습니다~!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4d2f7e6
지우는게 대부분이고, 깔끔해 보여서 지웠습니다!

</label>
);
},
);

export default Checkbox;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {InputHTMLAttributes, ReactNode} from 'react';

export interface CheckboxProps extends InputHTMLAttributes<HTMLInputElement> {
right?: ReactNode;
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function CreatedEventItem({isEditMode, setEditMode, isChecked, onChange,

return (
<Flex>
{isEditMode && <Checkbox isChecked={isChecked} onChange={() => onChange(createdEvent)} />}
{isEditMode && <Checkbox checked={isChecked} onChange={() => onChange(createdEvent)} />}
<Flex
justifyContent="spaceBetween"
alignItems="center"
Expand Down
12 changes: 6 additions & 6 deletions client/src/pages/mypage/withdraw/NotUseServiceStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -22,11 +22,11 @@ const NotUseServiceStep = ({handleMoveStep}: {handleMoveStep: (nextStep: Withdra

<Flex flexDirection="column" gap="1rem">
{/* TODO: (@soha) 백엔드와 어떻게 관리할 지 논의 후에 기능(hook) 추가 예정 */}
<Checkbox isChecked={false} onChange={() => {}} labelText="예상했던 서비스가 아님" />
<Checkbox isChecked={false} onChange={() => {}} labelText="디자인이 별로임" />
<Checkbox isChecked={false} onChange={() => {}} labelText="사용하기 불편함" />
<Checkbox isChecked={false} onChange={() => {}} labelText="원하는 기능이 없음" />
<Checkbox isChecked={false} onChange={() => {}} labelText="기타" />
<Checkbox checked={false} onChange={() => {}} right={<Text size="bodyBold">예상했던 서비스가 아님</Text>} />
<Checkbox checked={false} onChange={() => {}} right={<Text size="bodyBold">디자인이 별로임</Text>} />
<Checkbox checked={false} onChange={() => {}} right={<Text size="bodyBold">사용하기 불편함</Text>} />
<Checkbox checked={false} onChange={() => {}} right={<Text size="bodyBold">원하는 기능이 없음</Text>} />
<Checkbox checked={false} onChange={() => {}} right={<Text size="bodyBold">기타</Text>} />
</Flex>
</div>
{/* TODO: (@soha) checkbox를 하나라도 해야 탈퇴하기 버튼 활성화 */}
Expand Down
12 changes: 6 additions & 6 deletions client/src/pages/mypage/withdraw/UnableToUseDueToError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -21,11 +21,11 @@ const UnableToUseDueToError = ({handleMoveStep}: {handleMoveStep: (nextStep: Wit

<Flex flexDirection="column" gap="1rem">
{/* TODO: (@soha) 백엔드와 어떻게 관리할 지 논의 후에 기능(hook) 추가 예정 */}
<Checkbox isChecked={false} onChange={() => {}} labelText="행사 생성" />
<Checkbox isChecked={false} onChange={() => {}} labelText="지출 내역 추가" />
<Checkbox isChecked={false} onChange={() => {}} labelText="정산 초대하기" />
<Checkbox isChecked={false} onChange={() => {}} labelText="관리자에게 송금" />
<Checkbox isChecked={false} onChange={() => {}} labelText="기타" />
<Checkbox checked={false} onChange={() => {}} right={<Text size="bodyBold">행사 생성</Text>} />
<Checkbox checked={false} onChange={() => {}} right={<Text size="bodyBold">지출 내역 추가</Text>} />
<Checkbox checked={false} onChange={() => {}} right={<Text size="bodyBold">정산 초대하기</Text>} />
<Checkbox checked={false} onChange={() => {}} right={<Text size="bodyBold">관리자에게 송금</Text>} />
<Checkbox checked={false} onChange={() => {}} right={<Text size="bodyBold">기타</Text>} />
</Flex>
</div>
{/* TODO: (@soha) checkbox를 하나라도 해야 탈퇴하기 버튼 활성화 */}
Expand Down
Loading