Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
63fdbe5
feat: 툴팁의 기본 구조를 구현합니다.
WonJuneKim Nov 21, 2025
5b50dbc
feat: 툴팁의 Context 구독 방식을 이원화 합니다.
WonJuneKim Nov 21, 2025
ef5b324
test: 스토리북을 이용해 툴팁의 액션을 테스트합니다.
WonJuneKim Nov 22, 2025
7327576
refactor: Icon 컴포넌트에 ref를 forwardRef로 래핑합니다.
WonJuneKim Nov 22, 2025
a358768
refactor: Label 컴포넌트에 display 속성을 제거합니다.
WonJuneKim Nov 22, 2025
498929b
feat: 스토리북에 GlobalStyles를 주입합니다.
WonJuneKim Nov 22, 2025
2f92bb8
test: Tooltip 테스트 케이스를 구체화합니다.
WonJuneKim Nov 22, 2025
4ed7812
feat: 구현한 컴포넌트를 export합니다.
WonJuneKim Nov 22, 2025
e008dd1
feat: Tooltip의 Side를 결정짓는 props를 별도로 선언하지 않고 radix에서 제공하는 인터페이스를 사용합니다.
WonJuneKim Nov 25, 2025
34d11d1
feat: Tooltip이 자체 Context를 가지지 않고 radix의 설계를 따르도록 합니다.
WonJuneKim Nov 25, 2025
9882f21
feat: radix가 제공하는 avoidCollisions prop을 사용하여 툴팁 요소의 방향성을 결정합니다.
WonJuneKim Nov 25, 2025
bdf8011
test: 변경된 Tooltip 구조에 맞춰 스토리북의 테스트 케이스를 변경합니다.
WonJuneKim Nov 25, 2025
cb505c4
feat: 툴팁의 추가 구현 방향성에 대한 Todo를 작성합니다.
WonJuneKim Nov 25, 2025
714456d
Merge branch 'dev' into feat/263-tooltip-design-system
WonJuneKim Nov 25, 2025
5e1fd34
refactor: Icon의 Wrapper의 타입을 실제 컴포넌트의 타입을 직접 참조하도록 변경합니다.
WonJuneKim Nov 25, 2025
3a0449d
Merge branch 'feat/263-tooltip-design-system' of github.com:JECT-Stud…
WonJuneKim Nov 25, 2025
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
26 changes: 16 additions & 10 deletions packages/jds/src/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { type ElementRef, forwardRef } from 'react';

import { StyledIconWrapper } from './Icon.styles';
import type { IconProps } from './Icon.types';
import { iconMap, sizeMap } from './IconMap';

export const Icon = ({ name, size = 'md', color = 'currentColor', ...props }: IconProps) => {
const IconComponent = iconMap[name];
export const Icon = forwardRef<ElementRef<typeof StyledIconWrapper>, IconProps>(
({ name, size = 'md', color = 'currentColor', ...props }, ref) => {
const IconComponent = iconMap[name];

if (!IconComponent) return null;

if (!IconComponent) return null;
const pixelSize = sizeMap[size];

const pixelSize = sizeMap[size];
return (
<StyledIconWrapper ref={ref}>
<IconComponent width={pixelSize} height={pixelSize} color={color} {...props} />
</StyledIconWrapper>
);
},
);

return (
<StyledIconWrapper>
<IconComponent width={pixelSize} height={pixelSize} color={color} {...props} />
</StyledIconWrapper>
);
};
Icon.displayName = 'Icon';
13 changes: 2 additions & 11 deletions packages/jds/src/components/Label/Label.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,8 @@ import isPropValid from '@emotion/is-prop-valid';
import type { Theme } from '@emotion/react';
import styled from '@emotion/styled';

const TEXT_ALIGN_MAPPING = {
center: 'center',
left: 'flex-start',
right: 'flex-end',
} as const;

export type LabelSize = 'lg' | 'md' | 'sm' | 'xs';
export type LabelTextAlign = keyof typeof TEXT_ALIGN_MAPPING;
export type LabelTextAlign = 'left' | 'center' | 'right';
export type LabelWeight = 'bold' | 'normal' | 'subtle';

interface LabelStyledProps {
Expand All @@ -32,12 +26,9 @@ export const LabelStyled = styled('label', {
shouldForwardProp: prop => isPropValid(prop) && !prop.startsWith('$'),
})<LabelStyledProps>(({ theme, $size, $textAlign, $weight, $color }) => {
const tokenKey = getLabelTokenKey($size, $weight);
const justifyContent = TEXT_ALIGN_MAPPING[$textAlign];

return {
display: 'flex',
justifyContent,
alignItems: 'center',
textAlign: $textAlign,
color: $color ?? theme.color.semantic.object.bold,
cursor: 'inherit',
...theme.textStyle[tokenKey],
Expand Down
231 changes: 231 additions & 0 deletions packages/jds/src/components/Tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import type { Meta, StoryObj } from '@storybook/react';
import { FlexColumn, FlexRow, Label } from '@storybook-utils/layout';
import { Icon, IconButton, Input, Tooltip, BlockButton } from 'components';
import { Label as LabelComponent } from 'components';

const meta = {
title: 'Components/Tooltip',
component: Tooltip.Content,
decorators: [
Story => (
<Tooltip.Provider delayDuration={0} skipDelayDuration={0}>
<Story />
</Tooltip.Provider>
),
],
parameters: {
layout: 'centered',
},
argTypes: {
side: {
control: 'select',
options: ['top', 'right', 'bottom', 'left'],
description: '툴팁 표시 위치',
table: {
defaultValue: { summary: 'top' },
},
},
sideOffset: {
control: 'number',
description: '트리거 요소와의 간격 (px)',
table: {
defaultValue: { summary: '8' },
},
},
avoidCollisions: {
control: 'boolean',
description: '뷰포트 충돌 방지 자동 위치 조정',
table: {
defaultValue: { summary: 'true' },
},
},
},
} satisfies Meta<typeof Tooltip.Content>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
render: args => (
<Tooltip.Root>
<Tooltip.Trigger>
<IconButton.Basic icon='information-line' />
</Tooltip.Trigger>
<Tooltip.Content {...args}>툴팁 테스트 레이블</Tooltip.Content>
</Tooltip.Root>
),
args: {
side: 'top',
sideOffset: 8,
avoidCollisions: true,
},
};

export const AllSides: Story = {
args: { children: undefined },
render: () => (
<FlexColumn gap='60px'>
<FlexRow gap='24px'>
<FlexColumn gap='12px'>
<Label>Top (기본값)</Label>
<Tooltip.Root>
<Tooltip.Trigger>
<IconButton.Basic icon='information-line' />
</Tooltip.Trigger>
<Tooltip.Content side='top'>툴팁 상단</Tooltip.Content>
</Tooltip.Root>
</FlexColumn>

<FlexColumn gap='12px'>
<Label>Right</Label>
<Tooltip.Root>
<Tooltip.Trigger>
<IconButton.Basic icon='information-line' />
</Tooltip.Trigger>
<Tooltip.Content side='right'>툴팁 우측</Tooltip.Content>
</Tooltip.Root>
</FlexColumn>

<FlexColumn gap='12px'>
<Label>Bottom</Label>
<Tooltip.Root>
<Tooltip.Trigger>
<IconButton.Basic icon='information-line' />
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>툴팁 아래</Tooltip.Content>
</Tooltip.Root>
</FlexColumn>

<FlexColumn gap='12px'>
<Label>Left</Label>
<Tooltip.Root>
<Tooltip.Trigger>
<IconButton.Basic icon='information-line' />
</Tooltip.Trigger>
<Tooltip.Content side='left'>툴팁 좌측</Tooltip.Content>
</Tooltip.Root>
</FlexColumn>
</FlexRow>
</FlexColumn>
),
parameters: {
docs: {
description: {
story:
'툴팁은 네 가지 방향(top, right, bottom, left)으로 표시할 수 있습니다. avoidCollisions가 활성화되어 공간이 부족하면 자동으로 다른 방향으로 전환됩니다.',
},
},
},
};

export const LongContent: Story = {
args: { children: undefined },
render: () => (
<Tooltip.Root>
<Tooltip.Trigger>
<BlockButton.Basic hierarchy='accent'>표시되는 요소가 길 경우</BlockButton.Basic>
</Tooltip.Trigger>
<Tooltip.Content>
아주 아주 아주 아주 긴 요소입니다. 모바일 환경에서도 정상적인 툴팁 내용이 표시되어야하기
때문에 이러한 처리를 하였습니다. 최대 길이는 320px이며 이 후 자동으로 줄바꿈 됩니다.
</Tooltip.Content>
</Tooltip.Root>
),
parameters: {
docs: {
description: {
story: '긴 텍스트는 자동으로 줄바꿈되며, 최대 너비는 320px입니다.',
},
},
},
};

export const WithCustomOffset: Story = {
args: { children: undefined },
render: () => (
<FlexRow gap='24px'>
<FlexColumn gap='12px'>
<Label>오프셋 기본값(8px)</Label>
<Tooltip.Root>
<Tooltip.Trigger>
<BlockButton.Basic hierarchy='accent'>기본 오프셋</BlockButton.Basic>
</Tooltip.Trigger>
<Tooltip.Content sideOffset={8}>트리거 요소로 부터 8px</Tooltip.Content>
</Tooltip.Root>
</FlexColumn>

<FlexColumn gap='12px'>
<Label>오프셋 커스텀(24px)</Label>
<Tooltip.Root>
<Tooltip.Trigger>
<BlockButton.Basic hierarchy='accent'>커스텀(확장) 오프셋</BlockButton.Basic>
</Tooltip.Trigger>
<Tooltip.Content sideOffset={24}>트리거 요소로 부터 24px</Tooltip.Content>
</Tooltip.Root>
</FlexColumn>
</FlexRow>
),
parameters: {
docs: {
description: {
story: 'sideOffset prop으로 트리거 요소와 툴팁 사이의 간격을 조절할 수 있습니다.',
},
},
},
};

export const CustomTrigger: Story = {
args: { children: undefined },
render: () => (
<FlexColumn gap='24px'>
<div>
<Tooltip.Root>
<Tooltip.Trigger>
<LabelComponent as='span'>텍스트 레이블입니다.</LabelComponent>
</Tooltip.Trigger>
<Tooltip.Content>레이블</Tooltip.Content>
</Tooltip.Root>
</div>

<FlexColumn gap='12px'>
<Label>Icon 컴포넌트</Label>
<Tooltip.Root>
<Tooltip.Trigger>
<Icon name='information-line' size='lg' />
</Tooltip.Trigger>
<Tooltip.Content>아이콘 툴팁</Tooltip.Content>
</Tooltip.Root>
</FlexColumn>

<FlexColumn gap='12px'>
<Label>Icon 버튼</Label>
<Tooltip.Root>
<Tooltip.Trigger>
<IconButton.Basic icon='alert-line' />
</Tooltip.Trigger>
<Tooltip.Content>아이콘 버튼 툴팁</Tooltip.Content>
</Tooltip.Root>
</FlexColumn>

<FlexColumn gap='12px'>
<Label>Input 예시</Label>
<Tooltip.Root>
<Tooltip.Trigger>
<Input.TextField value='레이블 명' onChange={e => e.preventDefault()} />
</Tooltip.Trigger>
<Tooltip.Content side='top' sideOffset={12}>
안녕하세요? 툴팁입니다.
</Tooltip.Content>
</Tooltip.Root>
</FlexColumn>
</FlexColumn>
),
parameters: {
docs: {
description: {
story:
'툴팁은 다양한 Interactive한 요소에 적용할 수 있습니다. 다만, forwardRef 로 래핑된 요소나 Content 자체에 Html Element를 가진 요소이어야 합니다. 버튼, 텍스트, 아이콘, 입력 필드 등 어떤 요소든 트리거로 사용할 수 있습니다.',
},
},
},
};
57 changes: 57 additions & 0 deletions packages/jds/src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Tooltip as TooltipPrimitive } from 'radix-ui';

import { StyledTooltipContent } from './tooltip.styles';
import type { TooltipContentProps, TooltipProps, TooltipTriggerProps } from './tooltip.types';

const TooltipRoot = ({ children, delayDuration = 0, ...radixProps }: TooltipProps) => {
return (
<TooltipPrimitive.Root delayDuration={delayDuration} {...radixProps}>
{children}
</TooltipPrimitive.Root>
);
};

TooltipRoot.displayName = 'Tooltip.Root';

//Todo: avoidCollisions로 제어되고 있는 위치 감지를 디자인 에셋에서 요구하는 감지 플로우로 변경 시 내부 Context 활용 필요 가능성
const TooltipTrigger = ({ children, asChild = true, ...restProps }: TooltipTriggerProps) => {
return (
<TooltipPrimitive.Trigger asChild={asChild} {...restProps}>
{children}
</TooltipPrimitive.Trigger>
);
};

TooltipTrigger.displayName = 'Tooltip.Trigger';

const TooltipContent = ({
children,
side = 'top',
sideOffset = 8,
collisionPadding = 0,
avoidCollisions = true,
...restProps
}: TooltipContentProps) => {
return (
<TooltipPrimitive.Portal>
<StyledTooltipContent
side={side}
sideOffset={sideOffset}
collisionPadding={collisionPadding}
avoidCollisions={avoidCollisions}
{...restProps}
>
{children}
</StyledTooltipContent>
</TooltipPrimitive.Portal>
);
};

TooltipContent.displayName = 'Tooltip.Content';

export const Tooltip = {
Provider: TooltipPrimitive.Provider,
Root: TooltipRoot,
Trigger: TooltipTrigger,
Content: TooltipContent,
};
2 changes: 2 additions & 0 deletions packages/jds/src/components/Tooltip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Tooltip';
export type { TooltipProps, TooltipTriggerProps, TooltipContentProps } from './tooltip.types';
Loading