@itwin/itwinui-react
Version:
A react component library for iTwinUI
355 lines (354 loc) • 9.47 kB
JavaScript
import * as React from 'react';
import cx from 'classnames';
import {
useFloating,
useClick,
useDismiss,
useInteractions,
size,
autoUpdate,
offset,
flip,
shift,
autoPlacement,
inline,
hide,
FloatingFocusManager,
useHover,
useFocus,
safePolygon,
useRole,
FloatingPortal,
useFloatingTree,
} from '@floating-ui/react';
import {
Box,
ShadowRoot,
cloneElementWithRef,
isUnitTest,
mergeEventHandlers,
useControlledState,
useId,
useLayoutEffect,
useMergedRefs,
} from '../../utils/index.js';
import {
PortalContainerContext,
usePortalTo,
} from '../../utils/components/Portal.js';
import { ThemeProvider } from '../ThemeProvider/ThemeProvider.js';
export const PopoverOpenContext = React.createContext(void 0);
export const PopoverInitialFocusContext = React.createContext(void 0);
export const usePopover = (options) => {
let {
placement = 'bottom-start',
visible,
onVisibleChange,
closeOnOutsideClick,
autoUpdateOptions,
matchWidth,
interactions: interactionsProp,
role,
...rest
} = options;
let mergedInteractions = React.useMemo(
() => ({
...interactionsProp,
...{
click: interactionsProp?.click ?? true,
dismiss: interactionsProp?.dismiss ?? true,
hover: interactionsProp?.hover ?? false,
focus: interactionsProp?.focus ?? false,
},
}),
[interactionsProp],
);
let tree = useFloatingTree();
let middleware = React.useMemo(
() => ({
...options.middleware,
flip: options.middleware?.flip ?? true,
shift: options.middleware?.shift ?? true,
size: options.middleware?.size ?? true,
hide: options.middleware?.hide || !isUnitTest,
}),
[options.middleware],
);
let maxHeight =
'boolean' == typeof middleware.size ? '400px' : middleware.size?.maxHeight;
let [open, onOpenChange] = useControlledState(
false,
visible,
onVisibleChange,
);
let floating = useFloating({
placement,
open,
onOpenChange,
strategy: 'fixed',
whileElementsMounted: React.useMemo(
() =>
open ? (...args) => autoUpdate(...args, autoUpdateOptions) : void 0,
[autoUpdateOptions, open],
),
...rest,
middleware: React.useMemo(
() =>
[
void 0 !== middleware.offset && offset(middleware.offset),
middleware.flip &&
flip({
padding: 5,
}),
middleware.shift &&
shift({
padding: 4,
}),
(matchWidth || middleware.size) &&
size({
padding: 4,
apply: ({ rects, availableHeight }) => {
if (middleware.size)
setAvailableHeight(Math.round(availableHeight));
if (matchWidth) setReferenceWidth(rects.reference.width);
},
}),
middleware.autoPlacement &&
autoPlacement({
padding: 4,
}),
middleware.inline && inline(),
middleware.hide &&
hide({
padding: 4,
}),
].filter(Boolean),
[matchWidth, middleware],
),
});
let interactions = useInteractions([
useClick(floating.context, {
enabled: !!mergedInteractions.click,
...mergedInteractions.click,
}),
useDismiss(floating.context, {
enabled: !!mergedInteractions.dismiss,
outsidePress: closeOnOutsideClick,
bubbles: null != tree,
...mergedInteractions.dismiss,
}),
useHover(floating.context, {
enabled: !!mergedInteractions.hover,
delay: 100,
handleClose: safePolygon({
buffer: 1,
blockPointerEvents: true,
}),
move: false,
...mergedInteractions.hover,
}),
useFocus(floating.context, {
enabled: !!mergedInteractions.focus,
...mergedInteractions.focus,
}),
useRole(floating.context, {
role: 'dialog',
enabled: !!role,
}),
]);
let [referenceWidth, setReferenceWidth] = React.useState();
let [availableHeight, setAvailableHeight] = React.useState();
let getFloatingProps = React.useCallback(
(userProps) =>
interactions.getFloatingProps({
...userProps,
style: {
...floating.floatingStyles,
...(middleware.size &&
availableHeight && {
maxBlockSize: `min(${availableHeight}px, ${maxHeight})`,
}),
zIndex: 999,
...(matchWidth && referenceWidth
? {
minInlineSize: `${referenceWidth}px`,
maxInlineSize: `min(${2 * referenceWidth}px, 90vw)`,
}
: {}),
...(middleware.hide &&
floating.middlewareData.hide?.referenceHidden && {
visibility: 'hidden',
}),
...userProps?.style,
},
}),
[
interactions,
floating.floatingStyles,
floating.middlewareData.hide?.referenceHidden,
middleware.size,
middleware.hide,
availableHeight,
maxHeight,
matchWidth,
referenceWidth,
],
);
let getReferenceProps = React.useCallback(
(userProps) =>
interactions.getReferenceProps({
...userProps,
onClick: mergeEventHandlers(userProps?.onClick, () => {
if (!!mergedInteractions.click && visible) onOpenChange(false);
}),
}),
[interactions, mergedInteractions.click, visible, onOpenChange],
);
return React.useMemo(
() => ({
open,
onOpenChange,
getReferenceProps,
getFloatingProps,
...floating,
}),
[open, onOpenChange, getFloatingProps, floating, getReferenceProps],
);
};
export const Popover = React.forwardRef((props, forwardedRef) => {
let {
portal = true,
visible,
placement = 'bottom-start',
onVisibleChange,
closeOnOutsideClick = true,
middleware,
positionReference,
className,
children,
content,
applyBackground = false,
...rest
} = props;
let popover = usePopover({
visible,
placement,
onVisibleChange,
closeOnOutsideClick,
role: 'dialog',
middleware,
transform: false,
});
let [popoverElement, setPopoverElement] = React.useState(null);
let popoverRef = useMergedRefs(
popover.refs.setFloating,
forwardedRef,
setPopoverElement,
);
let triggerId = `${useId()}-trigger`;
let hasAriaLabel = !!props['aria-labelledby'] || !!props['aria-label'];
useLayoutEffect(() => {
if (!positionReference) return;
popover.refs.setPositionReference(positionReference);
return () => void popover.refs.setPositionReference(null);
}, [popover.refs, positionReference]);
let [initialFocus, setInitialFocus] = React.useState();
let initialFocusContextValue = React.useMemo(
() => ({
setInitialFocus,
}),
[],
);
return React.createElement(
React.Fragment,
null,
React.createElement(
PopoverOpenContext.Provider,
{
value: popover.open,
},
cloneElementWithRef(children, (children) => ({
id: children.props.id || triggerId,
...popover.getReferenceProps(children.props),
ref: popover.refs.setReference,
})),
),
popover.open
? React.createElement(
PopoverInitialFocusContext.Provider,
{
value: initialFocusContextValue,
},
React.createElement(
PopoverPortal,
{
portal: portal,
},
React.createElement(
ThemeProvider,
null,
React.createElement(
PortalContainerContext.Provider,
{
value: popoverElement,
},
React.createElement(DisplayContents, null),
React.createElement(
FloatingFocusManager,
{
context: popover.context,
modal: false,
initialFocus: initialFocus,
},
React.createElement(
Box,
{
className: cx(
'iui-popover',
{
'iui-popover-surface': applyBackground,
},
className,
),
'aria-labelledby': hasAriaLabel
? void 0
: popover.refs.domReference.current?.id,
...popover.getFloatingProps(rest),
ref: popoverRef,
},
content,
),
),
),
),
),
)
: null,
);
});
if ('development' === process.env.NODE_ENV) Popover.displayName = 'Popover';
let PopoverPortal = ({ children, portal = true }) => {
let portalTo = usePortalTo(portal);
return React.createElement(
FloatingPortal,
{
key: portalTo?.id,
root: portalTo ?? void 0,
},
React.createElement(DisplayContents, null),
children,
);
};
let DisplayContents = React.memo(() =>
React.createElement(
ShadowRoot,
{
css: `
:host {
display: contents;
}
`,
},
React.createElement('slot', null),
),
);