@patreon/studio
Version:
Patreon Studio Design System
95 lines (93 loc) • 4.03 kB
JSX
'use client';
import { autoUpdate, flip, offset, shift, useFloating } from '@floating-ui/react-dom';
import React, { useCallback, useState } from 'react';
import { createPortal } from 'react-dom';
import styled from 'styled-components';
import { useSequentialId } from '../../hooks/useSequentialId';
import { tokens } from '../../tokens';
import { mediaForBreakpoint } from '../../utilities/breakpoints';
import { hasResizeObserver } from '../../utilities/feature-detection';
import { cssForBodyText } from '../../utilities/type-bundles';
import { PortalPassthrough } from '../PortalPassthrough';
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 = useSequentialId('Tooltip');
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