react-elegant-ui
Version:
Elegant UI components, made by BEM best practices for react
143 lines • 5.36 kB
JavaScript
// Imported from yandex-ui. Source: https://github.com/bem/yandex-ui/
import React, { useEffect, useRef, useMemo, useCallback } from 'react';
import { useImmutableCallback } from '../../hooks/useImmutableCallback';
import { usePrevious } from '../../hooks/usePrevious';
import { useUniqueId } from '../../hooks/useUniqueId';
import { isKeyCode, Keys } from '../../lib/keyboard';
// TODO: replace global object to context
var LayerRegistry = {
stack: []
};
function removeLayerById(layerId) {
LayerRegistry.stack = LayerRegistry.stack.filter(function (_a) {
var id = _a.id;
return id !== layerId;
});
}
/**
* Component to manage layers of pop-up components like `Popup` or `Modal`
*
* It allow close elements in that order what it did open
*
* @param {LayerManagerProps}
*/
export var LayerManager = function (_a) {
var visible = _a.visible,
onClose = _a.onClose,
children = _a.children,
essentialRefs = _a.essentialRefs;
var id = useUniqueId('layer');
var prevOnClose = usePrevious(onClose);
var mouseDownRef = useRef(null);
// Collect ShadowRoot nodes from essential refs
var shadowRoots = useMemo(function () {
return essentialRefs.reduce(function (acc, ref) {
var node = ref.current;
// Push to acc unique shadow roots
if (node !== null) {
var root = node.getRootNode();
if (root instanceof ShadowRoot && acc.indexOf(root) === -1) {
acc.push(root);
}
}
return acc;
}, []);
}, [essentialRefs]);
var isEssentialShadowRootHost = useCallback(function (node) {
return node === null ? false : shadowRoots.some(function (root) {
return root.host === node;
});
}, [shadowRoots]);
var onDocumentKeyUp = useImmutableCallback(function (event) {
if (isKeyCode(event.code, Keys.ESC)) {
var _a = LayerRegistry.stack[LayerRegistry.stack.length - 1] || {},
layerId = _a.id,
layerOnClose = _a.onClose;
// Check id cuz we just take last item and should verify
if (layerId === id && layerOnClose !== undefined) {
layerOnClose(event, 'esc');
}
}
}, [id]);
// Remember mouse down target
var onDocumentMouseDown = useImmutableCallback(function (event) {
// Skip click on ShadowRoot. It will handle in next callback
if (isEssentialShadowRootHost(event.target)) return;
mouseDownRef.current = event.target;
}, [isEssentialShadowRootHost]);
var onDocumentClick = useImmutableCallback(function (event) {
var _a = LayerRegistry.stack[LayerRegistry.stack.length - 1] || {},
layerId = _a.id,
layerOnClose = _a.onClose,
refs = _a.essentialRefs;
// Skip click on ShadowRoot. It will handle in next callback
if (isEssentialShadowRootHost(event.target)) return;
// Check that target is same as in last mouse down event
// It need to prevent close by dragging the cursor (for example while select text)
if (mouseDownRef.current !== event.target) return;
// Check id cuz we just take last item and should verify
if (layerId === id && layerOnClose !== undefined && refs !== undefined) {
var isEssentionalClick = refs.some(function (ref) {
return ref.current !== null && ref.current instanceof HTMLElement && ref.current.contains(event.target);
});
if (!isEssentionalClick) {
layerOnClose(event, 'click');
}
}
}, [id, isEssentialShadowRootHost]);
// Toggle event handlers
useEffect(function () {
// Skip invisible
if (!visible) return;
// Global events
document.addEventListener('keyup', onDocumentKeyUp);
document.addEventListener('mousedown', onDocumentMouseDown, true);
document.addEventListener('click', onDocumentClick, true);
// Events on ShadowRoot nodes
shadowRoots.forEach(function (root) {
root.addEventListener('mousedown', onDocumentMouseDown, true);
root.addEventListener('click', onDocumentClick, true);
});
return function () {
// Global events
document.removeEventListener('keyup', onDocumentKeyUp);
document.removeEventListener('mousedown', onDocumentMouseDown, true);
document.removeEventListener('click', onDocumentClick, true);
// Events on ShadowRoot nodes
shadowRoots.forEach(function (root) {
root.removeEventListener('mousedown', onDocumentMouseDown, true);
root.removeEventListener('click', onDocumentClick, true);
});
};
}, [visible, onDocumentKeyUp, onDocumentMouseDown, onDocumentClick, shadowRoots]);
// Update stack
useEffect(function () {
var cleanup = function () {
return removeLayerById(id);
};
if (visible) {
LayerRegistry.stack.push({
id: id,
onClose: onClose,
essentialRefs: essentialRefs
});
} else {
cleanup();
}
return cleanup;
// must update only by change `visible`
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible]);
// Update callback in stack
useEffect(function () {
if (onClose !== prevOnClose) {
LayerRegistry.stack.forEach(function (layer) {
if (layer.onClose === prevOnClose) {
layer.onClose = onClose;
}
});
}
}, [onClose, prevOnClose]);
return /*#__PURE__*/React.createElement(React.Fragment, null, children);
};
LayerManager.displayName = 'LayerManager';