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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 22 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import "./App.css";
import { theme } from "./styles/theme/theme";
import Tooltip from "./libs/tooltip/Tooltip";

function App() {
return (
<div
style={{
backgroundColor: theme.semantic.color.light.lightAlternative,
width: "100px",
height: "100px",
}}
>
시멘틱 컬러가 적용된 박스입니다.
<div style={{ padding: 40 }}>
<Tooltip content="왼쪽 툴팁" position="left">
<button style={{ minWidth: 100, minHeight: 48, fontSize: 18, borderRadius: 8 }}>
Left
</button>
</Tooltip>
<Tooltip content="오른쪽 툴팁" position="right">
<button style={{ minWidth: 100, minHeight: 48, fontSize: 18, borderRadius: 8 }}>
Right
</button>
</Tooltip>
<Tooltip content="상단 툴팁" position="top">
<button style={{ minWidth: 100, minHeight: 48, fontSize: 18, borderRadius: 8 }}>
Top
</button>
</Tooltip>
<Tooltip content="하단 툴팁" position="bottom">
<button style={{ minWidth: 100, minHeight: 48, fontSize: 18, borderRadius: 8 }}>
Bottom
</button>
</Tooltip>
</div>
);
}
Expand Down
126 changes: 126 additions & 0 deletions src/libs/tooltip/Tooltip.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
.tooltipWrapper {
position: relative;
display: inline-block;
}

.tooltip {
position: absolute;
z-index: 10;
padding: 14px 16px;
border-radius: 10px;
font-size: 14px;
font-weight: 400;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
white-space: nowrap;
transition: opacity 0.2s;
opacity: 0;
pointer-events: none;
}

.tooltipVisible {
opacity: 1;
pointer-events: auto;
}

.tooltipBlack {
background: #181f24;
color: #fff;
}

.tooltipWhite {
background: #fff;
color: #181f24;
border: 1px solid #e5e7eb;
}

.arrow {
position: absolute;
width: 0;
height: 0;
}


.tooltipTop {
bottom: 100%;
left: 50%;
transform: translateX(-50%);
margin-bottom: 12px;
}

.arrowTop {
top: 100%;
left: 50%;
transform: translateX(-50%);
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 12px solid #181f24;
}

.tooltipWhite .arrowTop {
border-top-color: #fff;
}


.tooltipBottom {
top: 100%;
left: 50%;
transform: translateX(-50%);
margin-top: 12px;
}

.arrowBottom {
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 12px solid #181f24;
}

.tooltipWhite .arrowBottom {
border-bottom-color: #fff;
}


.tooltipLeft {
right: 100%;
top: 50%;
transform: translateY(-50%);
margin-right: 12px;
}

.arrowLeft {
left: 100%;
top: 50%;
transform: translateY(-50%);
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-left: 12px solid #181F24;
Copy link

Copilot AI Jul 7, 2025

Choose a reason for hiding this comment

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

[nitpick] Hex color codes are mixed case; consider normalizing to lowercase (#181f24) to match the rest of the stylesheet.

Suggested change
border-left: 12px solid #181F24;
border-left: 12px solid #181f24;

Copilot uses AI. Check for mistakes.
margin-left: -1px;
}

.tooltipWhite .arrowLeft {
border-left-color: #fff;
}

/* right: 툴팁이 오른쪽에 위치 */
.tooltipRight {
left: 100%;
top: 50%;
transform: translateY(-50%);
margin-left: 12px;
}

.arrowRight {
right: 100%;
top: 50%;
transform: translateY(-50%);
border-top: 10px solid transparent;
border-bottom: 10px solid transparent;
border-right: 12px solid #181F24;
margin-right: -1px;
}

.tooltipWhite .arrowRight {
border-right-color: #fff;
}
109 changes: 109 additions & 0 deletions src/libs/tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { Tooltip } from './Tooltip';

const meta: Meta<typeof Tooltip> = {
title: 'libs/Tooltip',
component: Tooltip,
tags: ['autodocs'],
decorators: [
(Story) => (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', padding: '40px', minHeight: '200px' }}>
<Story />
</div>
),
],
argTypes: {
content: { control: 'text', description: '툴팁에 표시될 내용입니다.' },
position: {
control: 'select',
options: ['top', 'bottom', 'left', 'right'],
description: '툴팁의 위치를 선택합니다.'
},
color: {
control: 'radio',
options: ['black', 'white'],
description: '툴팁의 색상을 선택합니다.'
},
children: { control: false, description: '툴팁을 트리거할 요소입니다.' },
},
};

export default meta;

type Story = StoryObj<typeof Tooltip>;

export const Default: Story = {
args: {
content: '가장 기본적인 툴팁입니다.',
children: <button>기본 버튼</button>,
position: 'top',
color: 'black',
},
};

export const Positions: Story = {
// name 속성 제거
render: () => (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gridTemplateRows: '1fr 1fr', gap: '32px 80px', placeItems: 'center' }}>
<div style={{ gridColumn: '1 / -1' }}>
<Tooltip content="상단 툴팁입니다." position="top"><button>Top</button></Tooltip>
</div>
<Tooltip content="왼쪽 툴팁입니다." position="left"><button>Left</button></Tooltip>
<div />
<Tooltip content="오른쪽 툴팁입니다." position="right"><button>Right</button></Tooltip>
<div style={{ gridColumn: '1 / -1' }}>
<Tooltip content="하단 툴팁입니다." position="bottom"><button>Bottom</button></Tooltip>
</div>
</div>
),
};

export const Colors: Story = {
// name 속성 제거
render: () => (
<div style={{ display: 'flex', gap: 32 }}>
<Tooltip content="Black 타입 툴팁입니다." color="black"><button>Black</button></Tooltip>
<Tooltip content="White 타입 툴팁입니다." color="white"><button>White</button></Tooltip>
</div>
),
parameters: {
backgrounds: { default: 'dark' },
},
};

export const LongContent: Story = {
// name 속성 제거
args: {
content: '이것은 콘텐츠가 매우 길어질 경우 어떻게 보이는지 테스트하기 위한 툴팁입니다. white-space: nowrap 스타일 때문에 한 줄로 길게 표시됩니다.',
children: <button>긴 콘텐츠</button>,
},
};

export const RichContent: Story = {
// name 속성 제거
args: {
content: (
<div style={{ textAlign: 'center' }}>
<h4>안녕하세요!</h4>
<p>툴팁 안에 <b>HTML</b>과 <i>컴포넌트</i>를<br /> 자유롭게 넣을 수 있습니다.</p>
<a href="https://storybook.js.org/" target="_blank" rel="noopener noreferrer">여기를 클릭하세요</a>
</div>
),
children: <button>리치 콘텐츠</button>,
},
parameters: {
notes: '`TooltipProps`의 `content` 타입을 `string`에서 `React.ReactNode`로 변경해야 합니다.',
},
};

export const OnDisabledElement: Story = {
// name 속성 제거
render: () => (
<Tooltip content="이 버튼은 현재 비활성화 상태입니다.">
<span style={{ display: 'inline-block', cursor: 'not-allowed' }}>
<button disabled style={{ pointerEvents: 'none' }}>비활성화 버튼</button>
</span>
</Tooltip>
),
};
50 changes: 50 additions & 0 deletions src/libs/tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { useState, useRef } from 'react';
import styles from './Tooltip.module.css';
import type { TooltipProps } from './Tooltip.type';

export const Tooltip: React.FC<TooltipProps> = ({
content,
position = 'top',
color = 'black',
children,
className = '',
style,
}) => {
const [visible, setVisible] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
Copy link

Copilot AI Jul 7, 2025

Choose a reason for hiding this comment

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

[nitpick] The wrapperRef is created but never used in this component. Consider removing it or using it for positioning logic to keep the code clean.

Suggested change
const wrapperRef = useRef<HTMLDivElement>(null);
// Removed unused wrapperRef to clean up the code.

Copilot uses AI. Check for mistakes.

const tooltipClass = [
styles.tooltip,
styles[`tooltip${position.charAt(0).toUpperCase() + position.slice(1)}`],
color === 'white' ? styles.tooltipWhite : styles.tooltipBlack,
visible ? styles.tooltipVisible : '',
className,
].join(' ');

const arrowClass = [
styles.arrow,
styles[`arrow${position.charAt(0).toUpperCase() + position.slice(1)}`],
].join(' ');

return (
<div
className={styles.tooltipWrapper}
ref={wrapperRef}
onMouseEnter={() => setVisible(true)}
onMouseLeave={() => setVisible(false)}
onFocus={() => setVisible(true)}
onBlur={() => setVisible(false)}
tabIndex={0}
>
{children}
{visible && (
<div className={tooltipClass} style={style} role="tooltip">
Comment on lines +38 to +41
Copy link

Copilot AI Jul 7, 2025

Choose a reason for hiding this comment

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

For proper accessibility, add a unique id to this tooltip and set aria-describedby on the trigger element to link them.

Suggested change
>
{children}
{visible && (
<div className={tooltipClass} style={style} role="tooltip">
aria-describedby={visible ? tooltipId : undefined}
>
{children}
{visible && (
<div id={tooltipId} className={tooltipClass} style={style} role="tooltip">

Copilot uses AI. Check for mistakes.
{content}
<div className={arrowClass} />
</div>
)}
</div>
);
};

export default Tooltip;
11 changes: 11 additions & 0 deletions src/libs/tooltip/Tooltip.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
export type TooltipColor = 'black' | 'white';

export interface TooltipProps {
content: string;
Comment on lines +3 to +5
Copy link

Copilot AI Jul 7, 2025

Choose a reason for hiding this comment

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

The content prop is currently typed as string but the stories pass JSX (React.ReactNode). Change the type to React.ReactNode to allow rich content.

Suggested change
export interface TooltipProps {
content: string;
import React from 'react';
export interface TooltipProps {
content: React.ReactNode;

Copilot uses AI. Check for mistakes.
position?: TooltipPosition;
color?: TooltipColor;
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
}