UNPKG

@patreon/studio

Version:

Patreon Studio Design System

94 lines (92 loc) 3.96 kB
'use client'; import { autoUpdate, flip, offset, shift, useFloating } from '@floating-ui/react-dom'; import React, { useCallback, useId, useState } from 'react'; import { createPortal } from 'react-dom'; import { styled } from 'styled-components'; import { PortalPassthrough } from '~/components/PortalPassthrough'; import { tokens } from '~/tokens'; import { mediaForBreakpoint } from '~/utilities/breakpoints'; import { hasResizeObserver } from '~/utilities/feature-detection'; import { cssForBodyText } from '~/utilities/type-bundles'; export const TooltipContents = styled.div ` ${(props) => (props.multiline ? 'width: 240px;' : 'white-space: nowrap;')} background: ${tokens.global.constant.blackMuted.default}; color: ${tokens.global.constant.white.default}; border: ${tokens.global.borderWidth.thin} solid ${tokens.global.border.muted.default}; border-radius: ${tokens.global.radius.sm}; padding: 6px 10px; backdrop-filter: blur(${tokens.global.effects.md}); ${cssForBodyText({ size: 'sm' })}; @media ${mediaForBreakpoint('sm')} { padding: ${tokens.global.space.x4} ${tokens.global.space.x8}; } `; // We don't want to clobber event handlers passed to Tooltip-wrapped // components, so we dispatch to them when defined. function mergeHandlers(tooltipHandler, userlandHandler) { if (userlandHandler === undefined) { return tooltipHandler; } return (e) => { tooltipHandler(e); userlandHandler(e); }; } /** @deprecated use the `OverlayTriggerHover` + `OverlayTooltip` components instead */ export function Tooltip({ 'aria-hidden': ariaHidden = false, children, multiline = false, preferredPlacement = 'top', textContent, }) { const id = useId(); const [isVisible, setIsVisible] = useState(false); const handleKeyDown = useCallback((e) => { if (e.key === 'Esc' || e.key === 'Escape') { setIsVisible(false); } }, []); const show = useCallback(() => { setIsVisible(true); }, []); const hide = useCallback(() => { setIsVisible(false); }, []); const { floatingStyles, refs } = useFloating({ // Preferred placement. We default to 'top' as 'auto' is no // longer a thing. placement: preferredPlacement, // Some middleware for the positioning calculation middleware: [ // Offset the tooltip by 8px offset(8), // Flip the tooltip to the opposite side when it begins // to overflow the viewport. flip(), // If the tooltip is overflowing the viewport, allow for // shifting the placement to ensure it remains visible. shift(), ], // So long as the tooltip is visible, re-calc positioning on // scroll and resize. whileElementsMounted: hasResizeObserver ? autoUpdate : undefined, }); return (<> {React.cloneElement(React.Children.only(children), { 'aria-describedby': isVisible ? id : undefined, ref: refs.setReference, onKeyDown: mergeHandlers(handleKeyDown, children.props.onKeyDown), onMouseEnter: mergeHandlers(show, children.props.onMouseEnter), onMouseLeave: mergeHandlers(hide, children.props.onMouseLeave), onFocus: mergeHandlers(show, children.props.onFocus), onBlur: mergeHandlers(hide, children.props.onBlur), tabIndex: 0, })} {isVisible && createPortal(<div ref={refs.setFloating} role="tooltip" id={id} aria-hidden={ariaHidden} style={{ isolation: 'isolate', zIndex: 1200, ...floatingStyles, }}> <PortalPassthrough> <TooltipContents multiline={multiline}>{textContent}</TooltipContents> </PortalPassthrough> </div>, document.body)} </>); } //# sourceMappingURL=index.jsx.map