Skip to content

Commit

Permalink
feat: Create new tooltip component (#3152)
Browse files Browse the repository at this point in the history
  • Loading branch information
RulaKhaled authored Sep 3, 2024
1 parent 8e7daaa commit 6cc0e88
Show file tree
Hide file tree
Showing 6 changed files with 343 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.2",
"@sentry/react": "^8.24.0",
"@stripe/react-stripe-js": "^2.7.1",
"@stripe/stripe-js": "^3.4.0",
Expand Down
119 changes: 119 additions & 0 deletions src/ui/Tooltip/Tooltip.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import ResizeObserver from 'resize-observer-polyfill'

import { Tooltip } from './Tooltip'

global.ResizeObserver = ResizeObserver

describe('Tooltip', () => {
it('renders Tooltip correctly', () => {
render(
<Tooltip>
<Tooltip.Root>
<Tooltip.Trigger>
<button>Hover me</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content>Tooltip Content</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip>
)

const triggerElement = screen.getByText('Hover me')
expect(triggerElement).toBeInTheDocument()
})

it('displays the Tooltip Content on hover', async () => {
render(
<Tooltip>
<Tooltip.Root>
<Tooltip.Trigger>
<button>Hover me</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content data-state="instant-open">
Tooltip Content
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip>
)

const triggerElement = await screen.findByText('Hover me')
await userEvent.hover(triggerElement)

const contentElement = await screen.findByText('Tooltip Content', {
selector: 'div',
})
expect(contentElement).toBeInTheDocument()
})

it('hides the Tooltip Content when not hovered', async () => {
render(
<Tooltip>
<Tooltip.Root>
<Tooltip.Trigger>
<button>Hover me</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content>Tooltip Content</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip>
)

const contentElement = screen.queryByText('Tooltip Content')
expect(contentElement).not.toBeInTheDocument()
})

it('applies custom className to Tooltip Content', async () => {
render(
<Tooltip>
<Tooltip.Root>
<Tooltip.Trigger>
<button>Hover me</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="border-black" data-state="instant-open">
Tooltip Content
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip>
)

const triggerElement = await screen.findByText('Hover me')
await userEvent.hover(triggerElement)

const contentElement = await screen.findByText('Tooltip Content', {
selector: 'div',
})
expect(contentElement).toHaveClass('border-black')
})

it('displays arrow element when Tooltip Content is displayed', async () => {
render(
<Tooltip>
<Tooltip.Root>
<Tooltip.Trigger>
<button>Hover me</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content data-state="instant-open">
Tooltip Content
<Tooltip.Arrow data-testid="tooltip-arrow" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip>
)

const triggerElement = await screen.findByText('Hover me')
await userEvent.hover(triggerElement)

const arrowElement = await screen.findByTestId('tooltip-arrow')
expect(arrowElement).toBeInTheDocument()
})
})
94 changes: 94 additions & 0 deletions src/ui/Tooltip/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Meta, StoryObj } from '@storybook/react'
import React, { useState } from 'react'

import Icon from 'ui/Icon'

import { Tooltip } from './Tooltip'

const meta: Meta<typeof Tooltip> = {
title: 'Components/Tooltip',
component: Tooltip,
}
export default meta

type Story = StoryObj<typeof Tooltip>

const DefaultStory: React.FC = () => (
<Tooltip delayDuration={0} skipDelayDuration={100}>
<div className="flex h-screen items-center justify-center">
<Tooltip.Root>
<Tooltip.Trigger>
<p>hover over me</p>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="top">
<p>This is the tooltip content with plain text.</p>
<Tooltip.Arrow />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</div>
</Tooltip>
)

const WithHtmlContentStory: React.FC = () => (
<Tooltip delayDuration={0} skipDelayDuration={500}>
<div className="flex h-screen items-center justify-center">
<Tooltip.Root>
<Tooltip.Trigger>
<button className="rounded border-2 border-red-700 p-4 text-red-700">
<Icon name="informationCircle" size="lg" />
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="left" className="bg-gray-100 text-black">
<div>
<p>This is the tooltip content with HTML.</p>
<p>
It can contain HTML elements like <strong>bold text</strong> and{' '}
<em>italic text</em>.
</p>
</div>
<Tooltip.Arrow />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</div>
</Tooltip>
)

const BottomTooltipStory: React.FC = () => {
const [isOpen, setIsOpen] = useState(false)

return (
<Tooltip delayDuration={0} skipDelayDuration={500}>
<div className="flex h-screen items-center justify-center">
<Tooltip.Root onOpenChange={setIsOpen} open={isOpen}>
<Tooltip.Trigger>
<button className="p-4 text-blue-700">
<Icon name={isOpen ? 'eye' : 'eyeOff'} size="lg" />
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content side="bottom">
<p>This is the tooltip content with plain text.</p>
<Tooltip.Arrow className="fill-blue-700" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</div>
</Tooltip>
)
}

export const Default: Story = {
render: () => <DefaultStory />,
}

export const WithHtmlContent: Story = {
render: () => <WithHtmlContentStory />,
}

export const BottomTooltip: Story = {
render: () => <BottomTooltipStory />,
}
78 changes: 78 additions & 0 deletions src/ui/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//https://www.radix-ui.com/primitives/docs/components/tooltip
import * as RadixTooltip from '@radix-ui/react-tooltip'
import React, { forwardRef } from 'react'

import { cn } from 'shared/utils/cn'

const TooltipProvider: React.FC<RadixTooltip.TooltipProviderProps> = ({
children,
...props
}) => <RadixTooltip.Provider {...props}>{children}</RadixTooltip.Provider>

TooltipProvider.displayName = 'TooltipProvider'

const TooltipRoot: React.FC<RadixTooltip.TooltipProps> = ({
children,
...props
}) => <RadixTooltip.Root {...props}>{children}</RadixTooltip.Root>

TooltipRoot.displayName = 'TooltipRoot'

const TooltipTrigger = forwardRef<
React.ElementRef<typeof RadixTooltip.Trigger>,
React.ComponentPropsWithoutRef<typeof RadixTooltip.Trigger>
>(({ children, className, ...props }, ref) => (
<RadixTooltip.Trigger ref={ref} {...props} className={className}>
{children}
</RadixTooltip.Trigger>
))

TooltipTrigger.displayName = 'TooltipTrigger'

interface TooltipContentProps
extends React.ComponentPropsWithoutRef<typeof RadixTooltip.Content> {
sideOffset?: number
}

const TooltipContent = forwardRef<
React.ElementRef<typeof RadixTooltip.Content>,
TooltipContentProps
>(({ children, className, sideOffset = 5, ...props }, ref) => (
<RadixTooltip.Content
ref={ref}
sideOffset={sideOffset}
{...props}
className={cn(
'bg-gray-800 px-3 py-2 text-sm text-white shadow-md',
className
)}
>
{children}
</RadixTooltip.Content>
))

TooltipContent.displayName = 'TooltipContent'

const TooltipArrow = forwardRef<
React.ElementRef<typeof RadixTooltip.Arrow>,
React.ComponentPropsWithoutRef<typeof RadixTooltip.Arrow>
>(({ className, ...props }, ref) => (
<RadixTooltip.Arrow ref={ref} {...props} className={className} />
))

TooltipArrow.displayName = 'TooltipArrow'

const TooltipPortal: React.FC<RadixTooltip.TooltipPortalProps> = ({
children,
...props
}) => <RadixTooltip.Portal {...props}>{children}</RadixTooltip.Portal>

TooltipPortal.displayName = 'TooltipPortal'

export const Tooltip = Object.assign(TooltipProvider, {
Root: TooltipRoot,
Trigger: TooltipTrigger,
Content: TooltipContent,
Arrow: TooltipArrow,
Portal: TooltipPortal,
})
1 change: 1 addition & 0 deletions src/ui/Tooltip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Tooltip } from './Tooltip'
50 changes: 50 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3573,6 +3573,36 @@ __metadata:
languageName: node
linkType: hard

"@radix-ui/react-tooltip@npm:^1.1.2":
version: 1.1.2
resolution: "@radix-ui/react-tooltip@npm:1.1.2"
dependencies:
"@radix-ui/primitive": "npm:1.1.0"
"@radix-ui/react-compose-refs": "npm:1.1.0"
"@radix-ui/react-context": "npm:1.1.0"
"@radix-ui/react-dismissable-layer": "npm:1.1.0"
"@radix-ui/react-id": "npm:1.1.0"
"@radix-ui/react-popper": "npm:1.2.0"
"@radix-ui/react-portal": "npm:1.1.1"
"@radix-ui/react-presence": "npm:1.1.0"
"@radix-ui/react-primitive": "npm:2.0.0"
"@radix-ui/react-slot": "npm:1.1.0"
"@radix-ui/react-use-controllable-state": "npm:1.1.0"
"@radix-ui/react-visually-hidden": "npm:1.1.0"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 10c0/76f3abcd27f7f673612631abc340a17e6ab0e5d20b901fe4828400de05d4d8a8711392417b028be86a3053a0881b80d0ed41c4e027eb64c1af9fe74db70d3786
languageName: node
linkType: hard

"@radix-ui/react-use-callback-ref@npm:1.1.0":
version: 1.1.0
resolution: "@radix-ui/react-use-callback-ref@npm:1.1.0"
Expand Down Expand Up @@ -3672,6 +3702,25 @@ __metadata:
languageName: node
linkType: hard

"@radix-ui/react-visually-hidden@npm:1.1.0":
version: 1.1.0
resolution: "@radix-ui/react-visually-hidden@npm:1.1.0"
dependencies:
"@radix-ui/react-primitive": "npm:2.0.0"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 10c0/db138dd5f3c94958a9f836740d4408c89c4a73e770eaba5ead921e69b3c0d196c5cd58323d82829a9bc05a74873c299195dfd8366b9808e53a9a3dbca5a1e5fe
languageName: node
linkType: hard

"@radix-ui/rect@npm:1.1.0":
version: 1.1.0
resolution: "@radix-ui/rect@npm:1.1.0"
Expand Down Expand Up @@ -10513,6 +10562,7 @@ __metadata:
"@radix-ui/react-label": "npm:^2.0.2"
"@radix-ui/react-popover": "npm:^1.0.6"
"@radix-ui/react-radio-group": "npm:^1.1.3"
"@radix-ui/react-tooltip": "npm:^1.1.2"
"@sentry/react": "npm:^8.24.0"
"@sentry/webpack-plugin": "npm:^2.22.2"
"@storybook/addon-a11y": "npm:^8.2.6"
Expand Down

0 comments on commit 6cc0e88

Please sign in to comment.