Skip to content

Commit

Permalink
Merge pull request #93 from neko113/feat/#92/tooltip-component
Browse files Browse the repository at this point in the history
Feat/#92/tooltip component
  • Loading branch information
alstn113 authored May 14, 2023
2 parents 6d047d2 + d397e68 commit 7dcd4aa
Show file tree
Hide file tree
Showing 9 changed files with 504 additions and 1 deletion.
31 changes: 31 additions & 0 deletions docs/components/Tooltip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# `[Component] Tooltip`

## `type`

```ts
export interface TooltipProps {
children: React.ReactNode;
placement?: Placement;
content?: React.ReactNode;
offset?: number;
color?: NormalColorType;
}
```

## `example`

```tsx
import { Tooltip } from '@wap-ui/react';

const App = () => {
return (
<>
<Tooltip placement="right" content="Boooooom!" color="primary">
<Button shadow size="md" color="primary">
Hover me
</Button>
</Tooltip>
</>
);
};
```
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@wap-ui/react",
"version": "1.4.0",
"version": "2.0.0",
"description": "WAP UI / React UI Components Library",
"repository": "https://github.com/pknu-wap/2022_2_WAP_WEB_TEAM1.git",
"author": "neko113 <[email protected]>",
Expand Down
103 changes: 103 additions & 0 deletions packages/react/src/components/Tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import styled from '@emotion/styled';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import React from 'react';
import { Tooltip, TooltipProps } from './Tooltip';
import { Button } from '../Button';

export default {
title: 'Components/Tooltip',
component: Tooltip,
} as ComponentMeta<typeof Tooltip>;

const Template: ComponentStory<typeof Tooltip> = (args: TooltipProps) => (
<Container>
<Tooltip {...args}>
<Button shadow size="md">
Hover Here
</Button>
</Tooltip>
</Container>
);

export const Default = Template.bind({});

Default.args = {
placement: 'top',
content: 'Boooooom!',
};

export const Placement = () => {
return (
<Container>
<Tooltip placement="left" content="Boooooom!">
<Button shadow size="md">
Left
</Button>
</Tooltip>
<Tooltip placement="top" content="Boooooom!">
<Button shadow size="md">
Top
</Button>
</Tooltip>
<Tooltip placement="bottom" content="Boooooom!">
<Button shadow size="md">
Bottom
</Button>
</Tooltip>
<Tooltip placement="right" content="Boooooom!">
<Button shadow size="md">
Right
</Button>
</Tooltip>
</Container>
);
};

export const Color = () => {
return (
<FlexColumn>
<Tooltip placement="right" content="Boooooom!" color="primary">
<Button shadow size="md" color="primary">
Hover me
</Button>
</Tooltip>
<Tooltip placement="right" content="Boooooom!" color="success">
<Button shadow size="md" color="success">
Hover me
</Button>
</Tooltip>
<Tooltip placement="right" content="Boooooom!" color="secondary">
<Button shadow size="md" color="secondary">
Hover me
</Button>
</Tooltip>
<Tooltip placement="right" content="Boooooom!" color="warning">
<Button shadow size="md" color="warning">
Hover me
</Button>
</Tooltip>
<Tooltip placement="right" content="Boooooom!" color="error">
<Button shadow size="md" color="error">
Hover me
</Button>
</Tooltip>
</FlexColumn>
);
};

const FlexColumn = styled.div`
display: flex;
flex-direction: column;
gap: 2rem;
margin-left: 5rem;
margin-top: 2rem;
`;

const Container = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 300px;
gap: 5rem;
margin-top: 5rem;
`;
71 changes: 71 additions & 0 deletions packages/react/src/components/Tooltip/Tooltip.styles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { NormalColorType, palette } from '../../theme';
import { TooltipIconPlacement, TooltipPlacement } from './placement';

export const StyledTooltipTrigger = styled.div`
width: max-content;
display: inherit;
`;

export const StyledTooltipArrow = styled.div<{
tooltipIconPlacement: TooltipIconPlacement;
}>`
border-radius: 0 0 2px 0;
position: absolute;
width: 12px;
height: 12px;
background-color: #ccc;
opacity: 0;
transition: 0.1s ease-in-out;
${({ tooltipIconPlacement }) => css`
top: ${tooltipIconPlacement.top};
left: ${tooltipIconPlacement.left};
right: ${tooltipIconPlacement.right};
bottom: ${tooltipIconPlacement.bottom};
transform: ${tooltipIconPlacement.transform};
`};
`;

export const StyledTooltipContent = styled.div<{
visible: boolean;
color: NormalColorType;
tooltipPlacement: TooltipPlacement;
}>`
position: absolute;
border-radius: 14px;
padding: 8px 12px;
opacity: 0;
transition: 0.1s ease-in-out;
${({ tooltipPlacement }) => css`
top: calc(${tooltipPlacement.top} + 6px);
left: ${tooltipPlacement.left};
transform: ${tooltipPlacement.transform};
`};
${({ visible, tooltipPlacement }) =>
visible &&
css`
opacity: 1;
top: ${tooltipPlacement.top};
${StyledTooltipArrow} {
opacity: 1;
}
`};
${({ color }) => css`
background-color: ${palette[color]};
${StyledTooltipArrow} {
background-color: ${palette[color]};
}
`};
`;

export const StyledTooltip = styled.div`
position: relative;
font-size: 14px;
color: #fff;
line-height: 1.5;
`;
76 changes: 76 additions & 0 deletions packages/react/src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { TooltipContent } from './TooltipContent';
import * as S from './Tooltip.styles';
import React, { useRef, useState } from 'react';
import { Placement } from './placement';
import { NormalColorType } from '../../theme';

export interface TooltipProps {
children: React.ReactNode;
placement?: Placement;
content?: React.ReactNode;
offset?: number;
color?: NormalColorType;
}

/**
* @example
* ```tsx
* <Tooltip content="Tooltip Content" color="error" placement="left">
* <Button>Button</Button>
* </Tooltip>
* ```
*/
export const Tooltip = ({
children,
placement = 'top',
content,
offset,
color = 'primary',
...props
}: TooltipProps) => {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
const timer = useRef<number>();

const contentProps = {
placement,
parent: ref,
visible,
offset,
color,
};

const handleChangeVisible = (nextState: boolean) => {
const clear = () => {
clearTimeout(timer.current);
timer.current = undefined;
};
const handler = (nextState: boolean) => {
setVisible(nextState);
clear();
};

clear();
if (nextState) {
timer.current = window.setTimeout(() => handler(true), 100);

return;
}
timer.current = window.setTimeout(() => handler(false), 100);
};

return (
<S.StyledTooltipTrigger
ref={ref}
onMouseEnter={() => handleChangeVisible(true)}
onMouseLeave={() => handleChangeVisible(false)}
{...props}
>
{children}
{content ? (
<TooltipContent {...contentProps}>{content}</TooltipContent>
) : null}
{/* null을 사용하지 않으면, TooltipContent가 렌더링되지 않는다. */}
</S.StyledTooltipTrigger>
);
};
81 changes: 81 additions & 0 deletions packages/react/src/components/Tooltip/TooltipContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { MutableRefObject, useEffect, useMemo, useState } from 'react';
import * as S from './Tooltip.styles';
import { createPortal } from 'react-dom';
import usePortal from '../../hooks/usePortal';
import {
Placement,
TooltipPlacement,
defaultTooltipPlacement,
getIconPlacement,
getPlacement,
getRect,
} from './placement';
import { NormalColorType } from '../../theme';

interface TooltipContentProps {
placement?: Placement;
visible: boolean;
children?: React.ReactNode;
parent?: MutableRefObject<HTMLElement | null> | undefined;
offset?: number;
color?: NormalColorType;
}

export const TooltipContent = ({
placement = 'top',
visible,
children,
offset = 12,
color = 'primary',
parent,
}: TooltipContentProps) => {
const el = usePortal('tooltip');
const [rect, setRect] = useState<TooltipPlacement>(defaultTooltipPlacement);

if (!parent) return null;

const updateRect = () => {
const pos = getPlacement(placement, getRect(parent), offset);

setRect(pos);
};

// eslint-disable-next-line react-hooks/rules-of-hooks
const { transform, top, left, right, bottom } = useMemo(
() => getIconPlacement(placement, 5),
[placement],
);

// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
updateRect();
}, [visible]);

if (!el) return null;

return createPortal(
<S.StyledTooltipContent
visible={visible}
tooltipPlacement={{
left: rect.left,
top: rect.top,
transform: rect.transform,
}}
color={color}
>
<S.StyledTooltip>
<S.StyledTooltipArrow
tooltipIconPlacement={{
left,
top,
right,
bottom,
transform,
}}
/>
{children}
</S.StyledTooltip>
</S.StyledTooltipContent>,
el,
);
};
1 change: 1 addition & 0 deletions packages/react/src/components/Tooltip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Tooltip } from './Tooltip';
Loading

0 comments on commit 7dcd4aa

Please sign in to comment.