Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 33 additions & 13 deletions apps/docs/src/stories/Button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { IconPlus, IconChevronRight } from '@sopt-makers/icons';

interface ButtonOwnProps {
size?: 'sm' | 'md' | 'lg';
theme?: 'white' | 'black' | 'blue' | 'red';
rounded?: 'md' | 'lg';
variant?: 'fill' | 'outlined';
intent?: 'primary' | 'secondary' | 'success' | 'danger';
shape?: 'rect' | 'pill';
variant?: 'fill' | 'outlined' | 'text' | 'floating';
disabled?: boolean;
LeftIcon?: React.ComponentType;
RightIcon?: React.ComponentType;
Expand All @@ -20,8 +20,8 @@ export default {
tags: ['autodocs'],
argTypes: {
size: { control: 'radio', options: ['sm', 'md', 'lg'] },
theme: { control: 'radio', options: ['white', 'black', 'blue', 'red'] },
rounded: { control: 'radio', options: ['md', 'lg'] },
intent: { control: 'radio', options: ['primary', 'secondary', 'success', 'danger'] },
shape: { control: 'radio', options: ['rect', 'pill'] },
variant: { control: 'radio', options: ['fill', 'outlined'] },
LeftIcon: { control: false },
RightIcon: { control: false },
Expand All @@ -33,8 +33,8 @@ export const Default: StoryObj<ButtonStoryProps> = {
args: {
children: 'Default Button',
size: 'md',
theme: 'white',
rounded: 'md',
intent: 'primary',
shape: 'rect',
disabled: false,
},
};
Expand All @@ -44,8 +44,8 @@ export const Outlined: StoryObj<ButtonStoryProps> = {
args: {
children: 'Outlined Button',
size: 'md',
theme: 'white',
rounded: 'md',
intent: 'primary',
shape: 'rect',
variant: 'outlined',
disabled: false,
},
Expand All @@ -56,8 +56,8 @@ export const LeftIcon: StoryObj<ButtonStoryProps> = {
args: {
children: 'LeftIcon Button',
size: 'sm',
theme: 'red',
rounded: 'lg',
intent: 'danger',
shape: 'pill',
disabled: false,
LeftIcon: IconPlus,
},
Expand All @@ -67,9 +67,29 @@ export const RightIcon: StoryObj<ButtonStoryProps> = {
args: {
children: 'RightIcon Button',
size: 'lg',
theme: 'blue',
rounded: 'lg',
intent: 'success',
shape: 'pill',
disabled: false,
RightIcon: IconChevronRight,
},
};

// text 버튼 스토리
export const Text: StoryObj<ButtonStoryProps> = {
args: {
children: 'Text Button',
variant: 'text',
disabled: false,
RightIcon: IconChevronRight,
},
};

// floating 버튼 스토리
export const Floating: StoryObj<ButtonStoryProps> = {
args: {
children: '글쓰기',
variant: 'floating',
disabled: false,
LeftIcon: IconPlus,
},
};
33 changes: 25 additions & 8 deletions packages/ui/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React, { type ButtonHTMLAttributes } from 'react';
import React, { useEffect, useState, type ButtonHTMLAttributes } from 'react';
import * as S from './style.css';
import createButtonVariant from './utils';
import { iconSizes } from './constants';
import { ButtonIntent, ButtonShape, ButtonVariant } from './types';
import { useResolvedProps, useScrollDirection } from './hooks';

interface IconProps {
color?: string;
Expand All @@ -13,33 +15,48 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children?: React.ReactNode;
className?: string;
size?: 'sm' | 'md' | 'lg';
theme?: 'white' | 'black' | 'blue' | 'red';
rounded?: 'md' | 'lg';
variant?: 'fill' | 'outlined';
theme?: 'white' | 'black' | 'blue' | 'red'; // @deprecated - `intent` prop 사용
rounded?: 'md' | 'lg'; // @deprecated - `shape` prop 사용
variant?: ButtonVariant;
LeftIcon?: React.ComponentType<IconProps>;
RightIcon?: React.ComponentType<IconProps>;
shape?: ButtonShape;
intent?: ButtonIntent;
}

function Button({
children,
className,
size = 'md',
theme = 'white',
rounded = 'md',
theme,
rounded,
LeftIcon,
RightIcon,
variant = 'fill',
shape = 'rect',
intent = 'primary',
Copy link
Contributor

Choose a reason for hiding this comment

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

단순 궁금증인데 intent는 어떤 의미인가요?

...buttonElementProps
}: ButtonProps) {
const style = createButtonVariant(theme, rounded, size, variant);
const { finalIntent, finalShape } = useResolvedProps({ intent, shape, theme, rounded });
const isFloating = variant === 'floating';
const scrollDirection = isFloating ? useScrollDirection() : null;
Copy link
Contributor

Choose a reason for hiding this comment

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

hook은 조건에 따라 호출되는 것이 아닌 최상위에서 호출이 되어야 할 것 같아요! 차라리 일단 무조건 값을 return 받고 값으로 분기처리 해야 할 것 같아요.

const scrollDirection = useScrollDirection(); 
const finalDirection = isFloating ? scrollDirection : null; 

근데 이렇게 하면 useScrollDirection에서 useEffect 내부 이벤트 리스너 등 로직을 isFloatingfalse일 때도 불필요하게 실행이 되니, useScrollDirectionenabled 파라미터를 뚫어서 isFloating에 따라 useEffect에 early return으로 실행 분기를 내부적으로 추가하면 어떨까요?

Copy link
Member

Choose a reason for hiding this comment

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

가장 최선은 디자이너와 논의하여 FloatingButton 정도의 별도 컴포넌트를 만드는 것이라고 봐요.. 하나의 베리언트가 이렇게 많은 파생 분기 코드들을 만들어내니, 자연스럽게 분리를 고려하기 좋은 타이밍이라고 생각합니다.

그게 안된다면 차선책으로 진혁님이 말씀해주신 것들 챙기면 좋을 것 같네요

Copy link
Contributor

Choose a reason for hiding this comment

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

저도 이부분에 대해서 동의합니다!

const [isExpanded, setIsExpanded] = useState(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

isExtended로 피그마와 통일하는 것은 어떨까요?
image


useEffect(() => {
if (!isFloating) return;
setIsExpanded(scrollDirection === 'down');
}, [scrollDirection, isFloating]);

const style = createButtonVariant(finalIntent, finalShape, size, variant, isExpanded);
const iconSize = iconSizes[size];
return (
<button className={`${S.root} ${style} ${className}`} type='button' {...buttonElementProps}>
{LeftIcon ? <LeftIcon height={iconSize} width={iconSize} /> : null}
<span>{children}</span>
{(!isFloating || isExpanded) && <span>{children}</span>}
{RightIcon && !LeftIcon ? <RightIcon height={iconSize} width={iconSize} /> : null}
</button>
);
}

Button.displayName = 'Button';
export default Button;
Loading