@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
JavaScript
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;