Skip to content

Commit

Permalink
feat-fe: 범용 Tooltip 컴포넌트 구현, 대시보드 사이드바 버그 수정 및 리팩토링 (#986)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Seongjin Hong <[email protected]>
  • Loading branch information
2 people authored and lurgi committed Feb 10, 2025
1 parent 40c86af commit 90899d4
Show file tree
Hide file tree
Showing 20 changed files with 1,023 additions and 246 deletions.
269 changes: 269 additions & 0 deletions frontend/src/components/_common/molecules/Tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
import type { Meta, StoryObj } from '@storybook/react';
import styled from '@emotion/styled';
import Tooltip from '.';

const meta = {
title: 'Common/Molecules/Tooltip',
component: Tooltip,
parameters: {
layout: 'centered',
docs: {
description: {
component:
'Tooltip 컴포넌트입니다. placement 설정값에 따라 노출 위치가 달라집니다. 스크롤, 리사이징, 뷰포트에 모두 대응합니다.',
},
},
},

tags: ['autodocs'],

argTypes: {
content: {
description: '툴팁 안에 표시되는 텍스트입니다.',
control: { type: 'text' },
},
placement: {
description: '툴팁을 표시할 방향입니다.',
},
zIndex: {
description: '툴팁을 위치시킬 zIndex 값입니다.',
control: { type: 'number' },
},
distanceFromTarget: {
description: '툴팁과 대상 요소 사이의 거리(px)를 정합니다.',
control: { type: 'number' },
},
maxWidth: {
description: '툴팁의 최대 너비(px)를 정합니다.',
control: { type: 'number' },
},
},
} satisfies Meta<typeof Tooltip>;

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

const Container = styled.div`
padding: 32px;
display: flex;
gap: 32px;
flex-wrap: wrap;
`;

const PlacementContainer = styled.div`
position: relative;
width: 300px;
height: 300px;
& > *:nth-child(1) {
position: absolute;
left: 0;
top: 0;
}
& > *:nth-child(2) {
position: absolute;
left: 50%;
top: 0;
transform: translateX(-50%);
}
& > *:nth-child(3) {
position: absolute;
right: 0;
top: 0;
}
& > *:nth-child(4) {
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
}
& > *:nth-child(5) {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
& > *:nth-child(6) {
position: absolute;
left: 0;
bottom: 0;
}
& > *:nth-child(7) {
position: absolute;
left: 50%;
bottom: 0;
transform: translateX(-50%);
}
& > *:nth-child(8) {
position: absolute;
right: 0;
bottom: 0;
}
`;

const ScrollContainer = styled.div`
height: 300px;
overflow-y: scroll;
padding: 200px 100px;
border: 1px solid #e2e8f0;
background: #f8fafc;
`;

const TriggerButton = styled.button`
padding: 8px 16px;
background-color: #e2e8f0;
border: none;
border-radius: 4px;
cursor: pointer;
white-space: nowrap;
&:hover {
background-color: #cbd5e1;
}
`;

export const Default: Story = {
name: '기본 툴팁',
args: {
content: '기본 툴팁입니다.',
placement: 'top',
},
render: ({ content, placement }) => (
<Container>
<Tooltip
content={content}
placement={placement}
>
<TriggerButton>마우스를 올려보세요</TriggerButton>
</Tooltip>
</Container>
),
};

export const TooltipsByPlacement: Story = {
name: '방향별 툴팁 표시',
render: () => (
<Container>
<PlacementContainer>
<Tooltip
content="좌측 상단으로 표시되는 툴팁입니다"
placement="topLeft"
>
<TriggerButton>좌측 상단</TriggerButton>
</Tooltip>
<Tooltip
content="상단으로 표시되는 툴팁입니다"
placement="top"
>
<TriggerButton>상단</TriggerButton>
</Tooltip>
<Tooltip
content="우측 상단으로 표시되는 툴팁입니다"
placement="topRight"
>
<TriggerButton>우측 상단</TriggerButton>
</Tooltip>
<Tooltip
content="좌측으로 표시되는 툴팁입니다"
placement="left"
>
<TriggerButton>좌측</TriggerButton>
</Tooltip>
<Tooltip
content="우측으로 표시되는 툴팁입니다"
placement="right"
>
<TriggerButton>우측</TriggerButton>
</Tooltip>
<Tooltip
content="좌측 하단으로 표시되는 툴팁입니다"
placement="bottomLeft"
>
<TriggerButton>좌측 하단</TriggerButton>
</Tooltip>
<Tooltip
content="하단으로 표시되는 툴팁입니다"
placement="bottom"
>
<TriggerButton>하단</TriggerButton>
</Tooltip>
<Tooltip
content="우측 하단으로 표시되는 툴팁입니다"
placement="bottomRight"
>
<TriggerButton>우측 하단</TriggerButton>
</Tooltip>
</PlacementContainer>
</Container>
),
};

export const TooltipsByDistance: Story = {
name: '트리거 요소와의 거리 조절',
render: () => (
<Container>
<Tooltip
content="기본 거리 (8px)"
placement="top"
>
<TriggerButton>기본 거리</TriggerButton>
</Tooltip>
<Tooltip
content="중간 거리 (16px)"
placement="top"
distanceFromTarget={16}
>
<TriggerButton>중간 거리</TriggerButton>
</Tooltip>
<Tooltip
content="긴 거리 (24px)"
placement="top"
distanceFromTarget={24}
>
<TriggerButton>긴 거리</TriggerButton>
</Tooltip>
</Container>
),
};

export const TooltipsByMaxWidth: Story = {
name: '툴팁 너비 조절',
render: () => (
<Container>
<Tooltip
content="기본 너비 설정입니다. 컨텐츠의 길이에 따라 자동으로 조절됩니다."
placement="top"
>
<TriggerButton>기본 너비</TriggerButton>
</Tooltip>
<Tooltip
content="최대 너비가 200px로 제한됩니다. 내용이 길어지면 자동으로 줄바꿈이 됩니다."
placement="top"
maxWidth={200}
>
<TriggerButton>최대 너비 200px</TriggerButton>
</Tooltip>
</Container>
),
};

export const ScrollTest: Story = {
name: '스크롤 시 동작 테스트',
render: () => (
<ScrollContainer>
<Tooltip
content="스크롤을 움직여보세요!"
placement="right"
>
<TriggerButton>스크롤 테스트 버튼</TriggerButton>
</Tooltip>
</ScrollContainer>
),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { forwardRef } from 'react';

import { TooltipPlacement } from './types';
import S from './style';

interface TooltipContentProps {
content: string;
placement: TooltipPlacement;
zIndex?: number;
maxWidth?: number;
isVisible: boolean;
positionX: number;
positionY: number;
}

export const TooltipContent = forwardRef<HTMLDivElement, TooltipContentProps>(
({ content, placement, zIndex, maxWidth, isVisible, positionX, positionY }, ref) => {
const style: React.CSSProperties = {
left: `${positionX}px`,
top: `${positionY}px`,
maxWidth: maxWidth ? `${maxWidth}px` : undefined,
};

return (
<S.ContentContainer
ref={ref}
placement={placement}
zIndex={zIndex}
data-visible={isVisible}
style={style}
>
{content}
</S.ContentContainer>
);
},
);

export default TooltipContent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { PropsWithChildren, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';

export default function TooltipPortal({ children }: PropsWithChildren) {
const [portalElement, setPortalElement] = useState<HTMLElement | null>(null);

useEffect(() => {
let element = document.getElementById('tooltip-portal-container');

if (!element) {
element = document.createElement('div');
element.id = 'tooltip-portal-container';
document.body.appendChild(element);
}

setPortalElement(element);

return () => {
if (element && element.childNodes.length === 0) {
element.remove();
}
};
}, []);

if (!portalElement) return null;

return createPortal(children, portalElement);
}
14 changes: 14 additions & 0 deletions frontend/src/components/_common/molecules/Tooltip/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { TooltipPlacement } from './types';

export const WINDOW_PADDING = 10;

export const OPPOSITE_PLACEMENT: Record<TooltipPlacement, TooltipPlacement> = {
top: 'bottom',
topRight: 'bottomLeft',
right: 'left',
bottomRight: 'topLeft',
bottom: 'top',
bottomLeft: 'topRight',
left: 'right',
topLeft: 'bottomRight',
} as const;
Loading

0 comments on commit 90899d4

Please sign in to comment.