Skip to content
Merged
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
158 changes: 82 additions & 76 deletions libs/ui/lib/tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,110 +1,116 @@
import {
FloatingPortal,
arrow,
autoPlacement,
autoUpdate,
flip,
offset,
useDismiss,
useFloating,
useFocus,
useHover,
useInteractions,
useRole,
} from '@floating-ui/react-dom-interactions'
import type { Placement } from '@floating-ui/react-dom-interactions'
import cn from 'classnames'
import { useCallback, useEffect, useRef, useState } from 'react'
import { usePopper } from 'react-popper'
import { useRef, useState } from 'react'

import { KEYS } from '../../util/keys'
import './tooltip.css'

/**
* This component allows either auto or manual placement of the tooltip. When `auto` is used, the
* tooltip will be placed in the best position based on the available space. When any other placement
* is used, the tooltip will be placed in that position but will also be flipped if there is not enough
* space for it to be displayed in that position.
*/
type PlacementOrAuto = Placement | 'auto'

export interface TooltipProps {
id: string
children?: React.ReactNode
/** The text to appear on hover/focus */
content: string | React.ReactNode
onClick?: React.MouseEventHandler<HTMLButtonElement>
definition?: boolean
/** Defaults to auto if not supplied */
placement?: PlacementOrAuto
}

const ARROW_SIZE = 12

export const Tooltip = ({
id,
children,
content,
onClick,
placement = 'auto',
definition = false,
}: TooltipProps) => {
const referenceElement = useRef(null)
const popperElement = useRef(null)
const arrowElement = useRef(null)
const [isOpen, setIsOpen] = useState(false)

const { attributes, styles, update } = usePopper(
referenceElement.current,
popperElement.current,
{
modifiers: [
{ name: 'arrow', options: { element: arrowElement.current } },
{
name: 'offset',
options: {
offset: [0, ARROW_SIZE],
},
},
// disable eventListeners when closed for optimization
// (could make difference with many Tooltips on a single page)
{ name: 'eventListeners', enabled: isOpen },
],
}
)

const openTooltip = () => {
setIsOpen(true)
if (update) {
// Update popper position
// (position will need to update after scrolling, for example)
update()
}
}
const closeTooltip = useCallback(() => setIsOpen(false), [setIsOpen])
const [open, setOpen] = useState(false)
const arrowRef = useRef(null)

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
const { key } = event
switch (key) {
case KEYS.escape:
event.preventDefault()
// Close tooltip on escape
closeTooltip()
break
}
}
const {
x,
y,
reference,
floating,
strategy,
context,
placement: finalPlacement,
middlewareData,
} = useFloating({
open,
onOpenChange: setOpen,
placement: placement === 'auto' ? undefined : placement,
whileElementsMounted: autoUpdate,
middleware: [
/**
* `autoPlacement` and `flip` are mututally excusive behaviors. If we manually provide a placement we want to make sure
* it flips to the other side if there is not enough space for it to be displayed in that position.
*/
placement === 'auto' ? autoPlacement() : flip(),
offset(12),
arrow({ element: arrowRef, padding: 12 }),
],
})

window.addEventListener('keydown', handleKeyDown)
const { x: arrowX, y: arrowY } = middlewareData.arrow || {}

return function cleanup() {
window.removeEventListener('keydown', handleKeyDown)
}
}, [closeTooltip])
const { getReferenceProps, getFloatingProps } = useInteractions([
useHover(context, { move: false }),
useFocus(context),
useDismiss(context),
useRole(context, { role: 'tooltip' }),
])

return (
<>
<button
type="button"
ref={referenceElement}
onClick={onClick}
onMouseEnter={openTooltip}
onMouseLeave={closeTooltip}
onFocus={openTooltip}
onBlur={closeTooltip}
className={cn('h-4 svg:pointer-events-none svg:align-top', {
ref={reference}
{...getReferenceProps()}
className={cn('svg:pointer-events-none', {
'dashed-underline': definition,
})}
>
{children}
</button>
<div
className={cn('TooltipContainer', isOpen ? 'block' : 'hidden')}
ref={popperElement}
role="tooltip"
id={id}
style={styles.popper}
{...attributes.popper}
>
<div className="max-w-xs rounded border py-1 px-2 text-sans-sm text-default bg-raise border-secondary">
{content}
</div>
<div className="TooltipArrow" ref={arrowElement} style={styles.arrow} />
</div>
<FloatingPortal>
{open && (
<div
ref={floating}
style={{ position: strategy, top: y ?? 0, left: x ?? 0 }}
className={cn('ox-tooltip max-content')}
/** Used to ensure the arrow is styled correctly */
data-placement={finalPlacement}
{...getFloatingProps()}
>
{content}
<div
className="ox-tooltip-arrow"
ref={arrowRef}
style={{ left: arrowX, top: arrowY }}
/>
</div>
)}
</FloatingPortal>
</>
)
}
50 changes: 22 additions & 28 deletions libs/ui/lib/tooltip/tooltip.css
Original file line number Diff line number Diff line change
@@ -1,35 +1,29 @@
.TooltipArrow {
visibility: hidden;

&,
&:before {
position: absolute;
height: 12px;
width: 12px;
}
.ox-tooltip {
@apply rounded border p-2 border-secondary elevation-1;
}

&:before {
content: '';
transform: rotate(45deg);
visibility: visible;
@apply border-t border-l bg-raise border-secondary;
}
.ox-tooltip-arrow {
position: absolute;
height: 12px;
width: 12px;
transform: rotate(-135deg);
@apply bg-raise border-secondary;
}

.TooltipContainer {
&[data-popper-placement^='top'] > .TooltipArrow {
bottom: -6px;
}
.ox-tooltip[data-placement^='top'] .ox-tooltip-arrow {
@apply border-t border-l;
margin-top: 2.5px;
}

&[data-popper-placement^='right'] > .TooltipArrow {
left: -6px;
}
.ox-tooltip[data-placement^='bottom'] .ox-tooltip-arrow {
@apply -top-[6.5px] border-b border-r;
margin-top: 0px;
}

&[data-popper-placement^='bottom'] > .TooltipArrow {
top: -6px;
}
.ox-tooltip[data-placement^='right'] .ox-tooltip-arrow {
@apply -left-[6.5px] border-r border-t;
}

&[data-popper-placement^='left'] > .TooltipArrow {
right: -6px;
}
.ox-tooltip[data-placement^='left'] .ox-tooltip-arrow {
@apply -right-[6.5px] border-b border-l;
}
Loading