@itwin/itwinui-react
Version:
A react component library for iTwinUI
240 lines (239 loc) • 5.91 kB
JavaScript
import * as React from 'react';
import cx from 'classnames';
import {
useFloating,
autoUpdate,
offset,
flip,
shift,
useHover,
useFocus,
useDismiss,
useInteractions,
safePolygon,
size,
autoPlacement,
hide,
inline,
useDelayGroup,
} from '@floating-ui/react';
import {
Box,
Portal,
cloneElementWithRef,
useControlledState,
useId,
useMergedRefs,
} from '../../utils/index.js';
export const defaultTooltipDelay = {
open: 200,
close: 200,
};
let useTooltip = (options = {}) => {
let uniqueId = useId();
let {
placement = 'top',
visible,
onVisibleChange,
middleware = {
flip: true,
shift: true,
},
autoUpdateOptions = {},
reference,
ariaStrategy = 'description',
id = uniqueId,
...props
} = options;
let [open, onOpenChange] = useControlledState(
false,
visible,
onVisibleChange,
);
let syncWithControlledState = React.useCallback(
(element) => {
queueMicrotask(() => {
try {
element?.togglePopover?.(open);
} catch {}
});
},
[open],
);
let floating = useFloating({
placement,
open,
onOpenChange,
strategy: 'fixed',
whileElementsMounted: React.useMemo(
() =>
open ? (...args) => autoUpdate(...args, autoUpdateOptions) : void 0,
[autoUpdateOptions, open],
),
middleware: React.useMemo(
() =>
[
void 0 !== middleware.offset ? offset(middleware.offset) : offset(4),
middleware.flip &&
flip({
padding: 4,
}),
middleware.shift &&
shift({
padding: 4,
}),
middleware.size &&
size({
padding: 4,
}),
middleware.autoPlacement &&
autoPlacement({
padding: 4,
}),
middleware.inline && inline(),
middleware.hide &&
hide({
padding: 4,
}),
].filter(Boolean),
[middleware],
),
...(reference && {
elements: {
reference,
},
}),
});
let ariaProps = React.useMemo(
() =>
'description' === ariaStrategy
? {
'aria-describedby': id,
}
: 'label' === ariaStrategy
? {
'aria-labelledby': id,
}
: {},
[ariaStrategy, id],
);
let { delay } = useDelayGroup(floating.context, {
id: useId(),
});
let interactions = useInteractions([
useHover(floating.context, {
delay: 0 !== delay ? delay : defaultTooltipDelay,
handleClose: safePolygon({
buffer: -1 / 0,
}),
move: false,
}),
useFocus(floating.context),
useDismiss(floating.context, {
referencePress: true,
referencePressEvent: 'click',
}),
]);
React.useEffect(() => {
if (!reference) return;
let domEventName = (e) => e.toLowerCase().substring(2);
let cleanupValues = {};
Object.entries({
...ariaProps,
...interactions.getReferenceProps(),
}).forEach(([key, value]) => {
if ('function' == typeof value) {
let patchedHandler = (event) => {
value({
...event,
nativeEvent: event,
});
};
reference.addEventListener(domEventName(key), patchedHandler);
cleanupValues[key] = patchedHandler;
} else if (value) {
cleanupValues[key] = reference.getAttribute(key);
reference.setAttribute(key, value);
}
});
return () => {
Object.entries(cleanupValues).forEach(([key, value]) => {
if ('function' == typeof value)
reference.removeEventListener(domEventName(key), value);
else if (value) reference.setAttribute(key, value);
else reference.removeAttribute(key);
});
};
}, [ariaProps, reference, interactions]);
let getReferenceProps = React.useCallback(
(userProps) =>
interactions.getReferenceProps({
...userProps,
...ariaProps,
}),
[interactions, ariaProps],
);
let floatingProps = React.useMemo(
() => ({
...interactions.getFloatingProps({
hidden: !open,
'aria-hidden': 'true',
...props,
id,
}),
popover: 'manual',
}),
[interactions, props, id, open],
);
return React.useMemo(
() => ({
getReferenceProps,
floatingProps,
...floating,
refs: {
...floating.refs,
setFloating: (element) => {
floating.refs.setFloating(element);
syncWithControlledState(element);
},
},
floatingStyles: floating.context.open ? floating.floatingStyles : {},
}),
[getReferenceProps, floatingProps, floating, syncWithControlledState],
);
};
export const Tooltip = React.forwardRef((props, forwardedRef) => {
let { content, children, portal = true, className, style, ...rest } = props;
let tooltip = useTooltip(rest);
let refs = useMergedRefs(tooltip.refs.setFloating, forwardedRef);
return React.createElement(
React.Fragment,
null,
cloneElementWithRef(children, (children) => ({
...tooltip.getReferenceProps(children.props),
ref: tooltip.refs.setReference,
})),
'none' !== props.ariaStrategy || tooltip.context.open
? React.createElement(
Portal,
{
portal: portal,
},
React.createElement(
Box,
{
className: cx('iui-tooltip', className),
ref: refs,
style: {
...tooltip.floatingStyles,
...style,
},
...tooltip.floatingProps,
},
content,
),
)
: null,
);
});
if ('development' === process.env.NODE_ENV) Tooltip.displayName = 'Tooltip';