UNPKG

@adaptabletools/adaptable

Version:

Powerful data-agnostic HTML5 AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements

222 lines (221 loc) 8.94 kB
import * as React from 'react'; import { useRef, useEffect } from 'react'; import debounce from 'lodash/debounce'; import batchUpdate from '../utils/batchUpdate'; import selectParent from '../utils/selectParent'; import useProperty from '../utils/useProperty'; import Overlay from './Overlay'; import join from '../utils/join'; import usePrevious from '../utils/usePrevious'; import { getDocRect, getRect } from './utils'; import useAgGridClassName from './useAgGridClassName'; import contains from '../utils/contains'; import { isBrowserDocumentAvailable } from '../../View/UIHelper'; import { useOverlay } from '../InfiniteTable'; import { useAdaptable } from '../../View/AdaptableContext'; export const getConstrainElement = (target, constrainTo) => { let el = null; if (typeof constrainTo === 'string') { el = selectParent(constrainTo, target); } if (typeof constrainTo === 'function') { el = constrainTo(target); } return el; }; export const getConstrainRect = (target, constrainTo) => { let el = getConstrainElement(target, constrainTo); if (el && el.tagName) { return getRect(el); } return getDocRect(); }; let portalElement; const ensurePortalElement = () => { if (!isBrowserDocumentAvailable()) { return; } if (portalElement) { return; } portalElement = document.createElement('div'); portalElement.style.position = 'absolute'; portalElement.style.zIndex = '999999'; portalElement.style.top = '0px'; portalElement.style.left = '0px'; document.body.appendChild(portalElement); }; const defaultProps = { showEvent: 'mouseenter', hideEvent: 'mouseleave', anchor: 'vertical', targetOffset: 10, defaultZIndex: 1000000, opacityTransitionDuration: '250ms', }; const OverlayTrigger = React.forwardRef((givenProps, ref) => { const props = { ...defaultProps, ...givenProps }; const adaptable = useAdaptable(); let { visible: _, showTriangle, showEvent, hideEvent, render, targetOffset, preventPortalEventPropagation = false, defaultZIndex, anchor, hideDelay = 0, opacityTransitionDuration, onVisibleChange, alignPosition = [ // overlay - target ['TopLeft', 'BottomLeft'], ['TopRight', 'BottomRight'], ['TopCenter', 'BottomCenter'], ['BottomCenter', 'TopCenter'], ['TopRight', 'BottomCenter'], ['TopRight', 'BottomLeft'], ['TopRight', 'BottomLeft'], ['BottomLeft', 'TopLeft'], ['BottomRight', 'TopRight'], ['BottomRight', 'TopCenter'], ['BottomRight', 'TopLeft'], ['TopRight', 'CenterLeft'], ['TopRight', 'TopLeft'], ['TopLeft', 'TopRight'], ['CenterRight', 'CenterLeft'], ['CenterLeft', 'CenterRight'], ], constrainTo, target: targetProp, ...domProps } = props; const { showOverlay, clearAll: clearAllOverlays, hideOverlay, portal, } = useOverlay({ portalContainer: portalElement, }); const domRef = useRef(null); const targetRef = useRef(null); const overlayRef = useRef(null); const [visible, doSetVisible] = useProperty(props, 'visible', false); const hideTimeoutRef = useRef(null); const setVisible = React.useCallback( // visible state may quickly change from true -> false -> true // when moving the mouse cursor from the trigger to the overlay // for this case we debounce the visible change for a very small amount of time debounce((visible) => { onVisibleChange?.(visible); if (!visible) { hideTimeoutRef.current = setTimeout(() => { hideTimeoutRef.current = null; doSetVisible(false); }, hideDelay); return; } if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); hideTimeoutRef.current = null; } doSetVisible(true); }, 50), []); const prevVisible = usePrevious(visible, false); ensurePortalElement(); const onShow = React.useCallback((event) => { const target = targetRef.current; if (event && preventPortalEventPropagation && !contains(target, event.target)) { // because of how React portals behave - see https://github.com/facebook/react/issues/11387 // after the portal is rendered, even though an item is in a separate dom parent, if it's in the portal, // events from it are propagated to the components in the portal and thus break some stuff return; } batchUpdate(() => { setVisible(true); }).commit(); }, [constrainTo, preventPortalEventPropagation]); const onHide = React.useCallback((_event) => { const shouldHide = props.shouldHide ? props.shouldHide(_event) : true; if (shouldHide) { setVisible(false); } }, [setVisible]); useEffect(() => { if (ref) { const api = { show: onShow, hide: onHide, }; if (typeof ref === 'function') { ref(api); } else { ref.current = api; } } }, [ref]); useEffect(() => { let target = domRef.current.previousSibling; if (targetProp) { target = targetProp(target); } if (!target) { adaptable.logger.warn('No OverlayTrigger target - make sure you render a child inside the OverlayTrigger, which will be the overlay target'); return; } targetRef.current = target; let attached = false; let onShowFn = onShow; let onHideFn = onHide; if (props.visible === undefined) { attached = true; target.addEventListener(showEvent, onShowFn); target.addEventListener(hideEvent, onHideFn); } if (props.visible && !prevVisible) { onShowFn(); } return () => { if (attached) { target.removeEventListener(showEvent, onShowFn); target.removeEventListener(hideEvent, onHideFn); } }; }, [props.visible, showEvent, hideEvent, onShow, onHide]); React.useEffect(() => { const target = targetRef.current; if (!target) { return; } const targetWidth = target.getBoundingClientRect().width; if ((prevVisible && !visible) || visible) { const overlayContent = (React.createElement(Overlay, { ...domProps, ref: (node) => { if (overlayRef.current && overlayRef.current != node) { overlayRef.current.removeEventListener(showEvent, onShow); overlayRef.current.removeEventListener(hideEvent, onHide); } overlayRef.current = node; if (node) { node.addEventListener(showEvent, onShow); node.addEventListener(hideEvent, onHide); } }, className: join('ab-Overlay', showTriangle ? 'ab-Overlay--show-triangle' : '', agGridClassName, domProps.className), style: { transition: `opacity ${opacityTransitionDuration}` }, visible: visible, onTransitionEnd: () => { if (!visible) { clearAllOverlays(); hideOverlay('overlay-trigger'); } } }, props.render({ targetWidth: targetWidth }))); let preparedConstrainTo; if (constrainTo) { preparedConstrainTo = getConstrainElement(targetRef.current, constrainTo); } // show only if visible or if it was visible and now it is invisible const alignToRect = getRect(target, targetOffset ?? 0); const showOverlayOptions = { id: 'overlay-trigger', // add id on props so component is not unmounted alignPosition, constrainTo: preparedConstrainTo?.getBoundingClientRect?.() ?? true, // at least constrain to document alignTo: alignToRect, }; showOverlay(() => overlayContent, showOverlayOptions); } else { clearAllOverlays(); } }, [visible, props.render]); const agGridClassName = useAgGridClassName([visible]); return (React.createElement(React.Fragment, null, React.Children.only(props.children), React.createElement("div", { "data-name": "OverlayTrigger", "data-visible": visible, ref: domRef, style: { visibility: 'hidden', flex: 'none', width: 0, height: 0, pointerEvents: 'none', display: 'inline-flex', } }), portal)); }); export default OverlayTrigger;