UNPKG

@itwin/itwinui-react

Version:

A react component library for iTwinUI

355 lines (354 loc) 9.47 kB
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), ), );