Skip to content

Commit

Permalink
refactor(tooltip) : add asChild props
Browse files Browse the repository at this point in the history
  • Loading branch information
Huinno-ParkJinHyun committed Dec 1, 2024
1 parent 5332884 commit 7d61448
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 21 deletions.
65 changes: 64 additions & 1 deletion packages/tooltip/src/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export default {
description: '툴팁에 적용할 클래스명',
},
gap: { control: 'number', description: '툴팁과 트리거 사이의 간격' },
asChild: {
control: { type: 'boolean' },
description: '`true`일 경우, 자식 요소를 그대로 사용합니다.',
},
},
} as Meta<TooltipProps>;

Expand Down Expand Up @@ -51,7 +55,7 @@ Default.args = {
export const ClickTrigger = Template.bind({});
ClickTrigger.args = {
tooltipContent: 'Click to toggle this tooltip',
placement: 'top',
placement: 'bottom',
trigger: 'click',
tooltipStyle: {
backgroundColor: '#000',
Expand All @@ -76,3 +80,62 @@ CustomStyles.args = {
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.2)',
},
};

export const AsChildExample = () => (
<div
style={{
display: 'flex',
gap: '20px',
padding: '50px',
textAlign: 'center',
}}
>
<Tooltip tooltipContent="This is a tooltip" asChild placement="top">
<h1>Hover me (H1 element)</h1>
</Tooltip>
<Tooltip tooltipContent="Styled Tooltip" asChild placement="bottom">
<button type="button" style={{ color: 'red', fontSize: '16px' }}>
Hover me (Styled Button)
</button>
</Tooltip>
</div>
);

export const AllPlacements = () => (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: '20px',
padding: '50px',
}}
>
<Tooltip tooltipContent="Top" placement="top">
<button type="button">Top</button>
</Tooltip>
<Tooltip tooltipContent="Top" placement="top" asChild={false}>
<button type="button">Top</button>
</Tooltip>

<Tooltip tooltipContent="Left" placement="left">
<h1>Left</h1>
</Tooltip>
<Tooltip tooltipContent="Left" placement="left" asChild={false}>
<h1>Left</h1>
</Tooltip>

<Tooltip tooltipContent="Right" placement="right">
<button type="button">Right</button>
</Tooltip>
<Tooltip tooltipContent="Right" placement="right" asChild={false}>
<button type="button">Right</button>
</Tooltip>

<Tooltip tooltipContent="Bottom" placement="bottom">
<button type="button">Bottom</button>
</Tooltip>
<Tooltip tooltipContent="Bottom" placement="bottom" asChild={false}>
<button type="button">Bottom</button>
</Tooltip>
</div>
);
74 changes: 73 additions & 1 deletion packages/tooltip/src/Tooltip.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useEffect, useState } from 'react';
import { describe, expect, test } from 'vitest';
Expand Down Expand Up @@ -223,3 +223,75 @@ describe('Tooltip 비동기 데이터 테스트', () => {
expect(screen.getByText('Fetched Content')).toBeInTheDocument();
});
});

describe('Tooltip asChild 속성 테스트', () => {
test('asChild가 true일 경우 자식 요소의 태그가 유지된다.', async () => {
render(
<Tooltip tooltipContent="This is a tooltip" asChild={true}>
<h1>Hover me</h1>
</Tooltip>,
);

const childElement = screen.getByText('Hover me');
expect(childElement).toHaveProperty('tagName', 'H1');

await userEvent.hover(childElement);
const tooltip = await screen.findByText('This is a tooltip');
expect(tooltip).toBeInTheDocument();
});

test('asChild 속성을 주지 않았을 경우, 기본 값으로 동작한다.', async () => {
render(
<Tooltip tooltipContent="This is a tooltip">
<h1>Hover me</h1>
</Tooltip>,
);

const childElement = screen.getByText('Hover me');
expect(childElement).toHaveProperty('tagName', 'H1');

await userEvent.hover(childElement);
const tooltip = await screen.findByText('This is a tooltip');
expect(tooltip).toBeInTheDocument();

await userEvent.unhover(childElement);
await waitFor(() => {
expect(screen.queryByText('This is a tooltip')).not.toBeInTheDocument();
});
});

test('asChild가 false일 경우 기본 div로 감싸진다.', async () => {
render(
<Tooltip tooltipContent="This is a tooltip" asChild={false}>
<button type="button">Hover me</button>
</Tooltip>,
);

const wrapperElement = screen.getByRole('tooltip');
expect(wrapperElement.tagName).toBe('DIV');

const childElement = screen.getByText('Hover me');
await userEvent.hover(childElement);
const tooltip = await screen.findByText('This is a tooltip');
expect(tooltip).toBeInTheDocument();
});

test('asChild가 true일 경우 이벤트 핸들러가 자식 요소에 적용된다.', async () => {
render(
<Tooltip tooltipContent="This is a tooltip" asChild={true}>
<button type="button">Hover me</button>
</Tooltip>,
);

const childElement = screen.getByText('Hover me');

await userEvent.hover(childElement);
const tooltip = await screen.findByText('This is a tooltip');
expect(tooltip).toBeInTheDocument();

await userEvent.unhover(childElement);
await waitFor(() => {
expect(screen.queryByText('This is a tooltip')).not.toBeInTheDocument();
});
});
});
44 changes: 27 additions & 17 deletions packages/tooltip/src/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Slot } from '@radix-ui/react-slot';

import clsx from 'clsx';
import {
type CSSProperties,
Expand All @@ -9,13 +11,14 @@ import {
} from 'react';
import { createPortal } from 'react-dom';
import styles from './Tooltip.module.css';
import { useTooltip } from './hooks/useTooltip/useTooltip';
import { useTooltip } from './hooks/useTooltip';

export type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';

export interface TooltipProps extends ComponentProps<'div'> {
tooltipContent: ReactNode;
placement?: TooltipPosition;
asChild?: boolean;
trigger?: 'hover' | 'click';
tooltipStyle?: CSSProperties;
tooltipClassName?: string;
Expand All @@ -27,6 +30,7 @@ export const Tooltip = forwardRef(function Tooltip(
tooltipContent,
placement = 'top',
trigger = 'hover',
asChild = true,
children,
tooltipStyle,
tooltipClassName,
Expand All @@ -49,22 +53,28 @@ export const Tooltip = forwardRef(function Tooltip(

useImperativeHandle(ref, () => wrapperRef.current as HTMLElement);

const Component = asChild ? Slot : 'div';

return (
<div
ref={wrapperRef}
role="tooltip"
className={styles.wrapper}
onMouseEnter={trigger === 'hover' ? () => toggleTooltip(true) : undefined}
onMouseLeave={
trigger === 'hover' ? () => toggleTooltip(false) : undefined
}
onClick={
trigger === 'click' ? () => toggleTooltip(!isVisible) : undefined
}
onKeyDown={handleKeyDown}
tabIndex={trigger === 'click' ? 0 : undefined}
>
{children}
<>
<Component
ref={wrapperRef}
role="tooltip"
onMouseEnter={
trigger === 'hover' ? () => toggleTooltip(true) : undefined
}
onMouseLeave={
trigger === 'hover' ? () => toggleTooltip(false) : undefined
}
onClick={
trigger === 'click' ? () => toggleTooltip(!isVisible) : undefined
}
onKeyDown={handleKeyDown}
tabIndex={trigger === 'click' ? 0 : undefined}
className={clsx(styles.trigger, { [styles.asChild]: asChild })}
>
{children}
</Component>
{isVisible &&
createPortal(
<div
Expand All @@ -81,6 +91,6 @@ export const Tooltip = forwardRef(function Tooltip(
</div>,
document.body,
)}
</div>
</>
);
});
3 changes: 1 addition & 2 deletions packages/tooltip/src/hooks/useTooltip/useTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import {
useRef,
useState,
} from 'react';

export type TooltipPosition = 'top' | 'bottom' | 'left' | 'right';
import type { TooltipPosition } from '../../Tooltip';

interface useTooltipProps {
placement: TooltipPosition;
Expand Down

0 comments on commit 7d61448

Please sign in to comment.