grommet
Version:
focus on the essential experience
371 lines (364 loc) • 18 kB
JavaScript
;
exports.__esModule = true;
exports.DropContainer = void 0;
var _react = _interopRequireWildcard(require("react"));
var _styledComponents = require("styled-components");
var _ContainerTargetContext = require("../../contexts/ContainerTargetContext");
var _FocusedContainer = require("../FocusedContainer");
var _utils = require("../../utils");
var _Keyboard = require("../Keyboard");
var _StyledDrop = require("./StyledDrop");
var _OptionsContext = require("../../contexts/OptionsContext");
var _useThemeValue2 = require("../../utils/useThemeValue");
var _excluded = ["a11yTitle", "aria-label", "align", "background", "onAlign", "children", "dropTarget", "elevation", "onClickOutside", "onEsc", "onKeyDown", "overflow", "plain", "responsive", "restrictFocus", "stretch", "trapFocus"];
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, "default": e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
// using react synthetic event to be able to stop propagation that
// would otherwise close the layer on ESC.
var preventLayerClose = function preventLayerClose(event) {
var key = event.keyCode ? event.keyCode : event.which;
if (key === 27) {
event.stopPropagation();
}
};
// Gets the closest ancestor positioned element
var getParentNode = function getParentNode(element) {
var _element$offsetParent;
return (_element$offsetParent = element.offsetParent) != null ? _element$offsetParent : element.parentNode;
};
// return the containing block
var getContainingBlock = function getContainingBlock(element) {
var currentNode = getParentNode(element);
while (currentNode instanceof window.HTMLElement && !['html', 'body'].includes(currentNode.nodeName.toLowerCase())) {
var _currentNode;
var css = window.getComputedStyle(currentNode);
// This is non-exhaustive but covers the most common CSS properties that
// create a containing block.
// https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
if ((css.transform ? css.transform !== 'none' : false) || (css.perspective ? css.perspective !== 'none' : false) || (css.backdropFilter ? css.backdropFilter !== 'none' : false) || css.contain === 'paint' || ['transform', 'perspective'].includes(css.willChange) || css.willChange === 'filter' || (css.filter ? css.filter !== 'none' : false)) {
return currentNode;
}
currentNode = (_currentNode = currentNode) == null ? void 0 : _currentNode.parentNode;
}
return null;
};
var defaultAlign = {
top: 'top',
left: 'left'
};
var DropContainer = exports.DropContainer = /*#__PURE__*/(0, _react.forwardRef)(function (_ref, ref) {
var a11yTitle = _ref.a11yTitle,
ariaLabel = _ref['aria-label'],
_ref$align = _ref.align,
align = _ref$align === void 0 ? defaultAlign : _ref$align,
background = _ref.background,
onAlign = _ref.onAlign,
children = _ref.children,
dropTarget = _ref.dropTarget,
elevation = _ref.elevation,
onClickOutside = _ref.onClickOutside,
onEsc = _ref.onEsc,
onKeyDown = _ref.onKeyDown,
_ref$overflow = _ref.overflow,
overflow = _ref$overflow === void 0 ? 'auto' : _ref$overflow,
plain = _ref.plain,
_ref$responsive = _ref.responsive,
responsive = _ref$responsive === void 0 ? true : _ref$responsive,
restrictFocus = _ref.restrictFocus,
_ref$stretch = _ref.stretch,
stretch = _ref$stretch === void 0 ? 'width' : _ref$stretch,
trapFocus = _ref.trapFocus,
rest = _objectWithoutPropertiesLoose(_ref, _excluded);
var containerTarget = (0, _react.useContext)(_ContainerTargetContext.ContainerTargetContext);
var _useThemeValue = (0, _useThemeValue2.useThemeValue)(),
theme = _useThemeValue.theme,
passThemeFlag = _useThemeValue.passThemeFlag;
// dropOptions was created to preserve backwards compatibility
var _useContext = (0, _react.useContext)(_OptionsContext.OptionsContext),
dropOptions = _useContext.drop;
var portalContext = (0, _react.useContext)(_utils.PortalContext);
var portalId = (0, _react.useMemo)(function () {
return portalContext.length;
}, [portalContext]);
var nextPortalContext = (0, _react.useMemo)(function () {
return [].concat(portalContext, [portalId]);
}, [portalContext, portalId]);
var dropRef = (0, _utils.useForwardedRef)(ref);
(0, _react.useEffect)(function () {
var onClickDocument = function onClickDocument(event) {
// determine which portal id the target is in, if any
var clickedPortalId = null;
var node = event.composed && event.composedPath()[0] || event.target;
while (clickedPortalId === null && node && node !== document && !(node instanceof ShadowRoot)) {
var attr = node.getAttribute('data-g-portal-id');
if (attr !== null) clickedPortalId = parseInt(attr, 10);
node = node.parentNode;
}
// Check if the click happened within the dropTarget
var clickInsideDropTarget = (dropTarget == null ? void 0 : dropTarget.current) && dropTarget.current.contains(event.target) || dropTarget && typeof dropTarget.contains === 'function' && dropTarget.contains(event.target);
if (!clickInsideDropTarget && clickedPortalId === null || portalContext.indexOf(clickedPortalId) !== -1) {
onClickOutside(event);
}
};
if (onClickOutside) {
document.addEventListener('mousedown', onClickDocument);
}
return function () {
if (onClickOutside) {
document.removeEventListener('mousedown', onClickDocument);
}
};
}, [onClickOutside, containerTarget, portalContext, dropTarget]);
(0, _react.useEffect)(function () {
var target = (dropTarget == null ? void 0 : dropTarget.current) || dropTarget;
var notifyAlign = function notifyAlign() {
var _dropRef$current;
var styleCurrent = dropRef == null || (_dropRef$current = dropRef.current) == null ? void 0 : _dropRef$current.style;
var alignControl = (styleCurrent == null ? void 0 : styleCurrent.top) !== '' ? 'top' : 'bottom';
onAlign(alignControl);
};
// We try to preserve the maxHeight as changing it causes any scroll
// position to be lost. We set the maxHeight on mount and if the window
// is resized.
var place = function place(preserveHeight) {
var windowWidth = window.innerWidth;
var windowHeight = window.innerHeight;
var container = dropRef.current;
if (container && target) {
var _containingBlockRect$, _containingBlockRect, _containingBlockRect$2, _containingBlockRect2, _containingBlockRect$3, _containingBlockRect3, _containingBlock$scro, _containingBlock2, _containingBlock$scro2, _containingBlock3;
// clear prior styling
container.style.left = '';
container.style.top = '';
container.style.bottom = '';
container.style.width = '';
if (!preserveHeight) {
container.style.maxHeight = '';
}
// get bounds
var targetRect = target.getBoundingClientRect();
var containerRect = container.getBoundingClientRect();
// determine width
var width;
if (stretch) {
width = Math.min(stretch === 'align' ? Math.min(targetRect.width, containerRect.width) : Math.max(targetRect.width, containerRect.width), windowWidth);
} else {
width = Math.min(containerRect.width, windowWidth);
}
// set left position
var left;
if (align.left) {
if (align.left === 'left') {
left = targetRect.left;
} else if (align.left === 'right') {
left = targetRect.left + targetRect.width;
}
} else if (align.right) {
if (align.right === 'left') {
left = targetRect.left - width;
} else if (align.right === 'right') {
left = targetRect.left + targetRect.width - width;
}
} else {
left = targetRect.left + targetRect.width / 2 - width / 2;
}
if (left + width > windowWidth) {
left -= left + width - windowWidth;
} else if (left < 0) {
left = 0;
}
// set top or bottom position
var top;
var bottom;
var maxHeight = containerRect.height;
/* If responsive is true and the Drop doesn't have enough room
to be fully visible and there is more room in the other
direction, change the Drop to display above/below. If there is
less room in the other direction leave the Drop in its current
position. */
if (responsive &&
// drop is above target
align.bottom === 'top' &&
// drop is overflowing above window
targetRect.top - containerRect.height <= 0 &&
// there is room to display the drop below the target
targetRect.bottom + containerRect.height < windowHeight) {
// top of drop is aligned to bottom of target
top = targetRect.bottom;
maxHeight = top;
} else if (responsive &&
// top of drop is aligned to top of target
align.top === 'top' &&
// drop is overflowing below window
targetRect.top + containerRect.height >= windowHeight &&
// height of the drop is larger than the target.
targetRect.top + containerRect.height > targetRect.bottom &&
// there is room to display the drop above the target
targetRect.bottom - containerRect.height > 0) {
// bottom of drop is aligned to bottom of target
bottom = targetRect.bottom;
maxHeight = top;
} else if (responsive &&
// top of drop is aligned to bottom of target
align.top === 'bottom' &&
// drop is overflowing below window
targetRect.bottom + containerRect.height >= windowHeight &&
// there is room to display the drop above the target
targetRect.top - containerRect.height > 0) {
// bottom of drop is aligned to top of target
bottom = targetRect.top;
maxHeight = bottom;
} else if (responsive &&
// bottom of drop is aligned to bottom of target
align.bottom === 'bottom' &&
// drop is overflowing above window
targetRect.bottom - containerRect.height <= 0 &&
// height of the drop is larger than the target.
targetRect.bottom - containerRect.height > targetRect.top &&
// there is room to display the drop below the target
targetRect.top + containerRect.height > 0) {
// top of drop is aligned to top of target
top = targetRect.top;
maxHeight = bottom;
} else if (align.top === 'top') {
top = targetRect.top;
maxHeight = windowHeight - top;
} else if (align.top === 'bottom') {
top = targetRect.bottom;
maxHeight = windowHeight - top;
} else if (align.bottom === 'top') {
bottom = targetRect.top;
maxHeight = bottom;
} else if (align.bottom === 'bottom') {
bottom = targetRect.bottom;
maxHeight = bottom;
} else {
top = targetRect.top + targetRect.height / 2 - containerRect.height / 2;
}
var containingBlock;
var containingBlockRect;
// dropOptions was created to preserve backwards compatibility
if (dropOptions != null && dropOptions.checkContainingBlock) {
var _containingBlock;
// return the containing block for absolute elements or `null`
// for fixed elements
containingBlock = getContainingBlock(container);
containingBlockRect = (_containingBlock = containingBlock) == null ? void 0 : _containingBlock.getBoundingClientRect();
}
// compute viewport offsets
var viewportOffsetLeft = (_containingBlockRect$ = (_containingBlockRect = containingBlockRect) == null ? void 0 : _containingBlockRect.left) != null ? _containingBlockRect$ : 0;
var viewportOffsetTop = (_containingBlockRect$2 = (_containingBlockRect2 = containingBlockRect) == null ? void 0 : _containingBlockRect2.top) != null ? _containingBlockRect$2 : 0;
var viewportOffsetBottom = (_containingBlockRect$3 = (_containingBlockRect3 = containingBlockRect) == null ? void 0 : _containingBlockRect3.bottom) != null ? _containingBlockRect$3 : windowHeight;
var containerOffsetLeft = (_containingBlock$scro = (_containingBlock2 = containingBlock) == null ? void 0 : _containingBlock2.scrollLeft) != null ? _containingBlock$scro : 0;
var containerOffsetTop = (_containingBlock$scro2 = (_containingBlock3 = containingBlock) == null ? void 0 : _containingBlock3.scrollTop) != null ? _containingBlock$scro2 : 0;
container.style.left = left - viewportOffsetLeft + containerOffsetLeft + "px";
if (stretch) {
// offset width by 0.1 to avoid a bug in ie11 that
// unnecessarily wraps the text if width is the same
// NOTE: turned off for now
container.style.width = width + 0.1 + "px";
}
// the (position:absolute + scrollTop)
// is presenting issues with desktop scroll flickering
if (top !== '') {
container.style.top = top - viewportOffsetTop + containerOffsetTop + "px";
}
if (bottom !== '') {
container.style.bottom = viewportOffsetBottom - bottom - containerOffsetTop + "px";
}
if (!preserveHeight) {
if (theme.drop && theme.drop.maxHeight) {
maxHeight = Math.min(maxHeight, (0, _utils.parseMetricToNum)(theme.drop.maxHeight));
}
container.style.maxHeight = maxHeight + "px";
}
}
if (onAlign) notifyAlign();
};
var scrollParents;
var addScrollListeners = function addScrollListeners() {
scrollParents = (0, _utils.findScrollParents)(target);
scrollParents.forEach(function (scrollParent) {
return scrollParent.addEventListener('scroll', place);
});
};
var removeScrollListeners = function removeScrollListeners() {
scrollParents.forEach(function (scrollParent) {
return scrollParent.removeEventListener('scroll', place);
});
scrollParents = [];
};
var onResize = function onResize() {
removeScrollListeners();
addScrollListeners();
place(false);
};
addScrollListeners();
window.addEventListener('resize', onResize);
place(false);
return function () {
removeScrollListeners();
window.removeEventListener('resize', onResize);
};
}, [align, containerTarget, onAlign, dropTarget, portalContext, portalId, responsive, restrictFocus, stretch, theme.drop, dropRef, dropOptions]);
// Once drop is open the focus will be put on the drop container
// if restrictFocus is true. If the caller put focus
// on an element already, we honor that. Otherwise, we put
// the focus on the drop container.
(0, _react.useEffect)(function () {
if (restrictFocus) {
var dropContainer = dropRef.current;
if (dropContainer) {
if (!dropContainer.contains(document.activeElement)) {
dropContainer.focus();
}
}
}
}, [dropRef, restrictFocus]);
var content = /*#__PURE__*/_react["default"].createElement(_StyledDrop.StyledDrop, _extends({
"aria-label": a11yTitle || ariaLabel,
ref: dropRef,
background: background,
plain: plain,
elevation: !plain ? elevation || theme.global.drop.elevation || theme.global.drop.shadowSize ||
// backward compatibility
'small' : undefined,
tabIndex: "-1",
alignProp: align,
overflow: overflow,
"data-g-portal-id": portalId
}, passThemeFlag, rest), children);
var themeContextValue = (0, _react.useMemo)(function () {
var dark;
if (background || theme.global.drop.background) {
dark = (0, _utils.backgroundIsDark)(background || theme.global.drop.background, theme);
}
return _extends({}, theme, {
dark: dark
});
}, [background, theme]);
var dark = themeContextValue.dark;
if (dark !== undefined && dark !== theme.dark) {
content = /*#__PURE__*/_react["default"].createElement(_styledComponents.ThemeContext.Provider, {
value: themeContextValue
}, content);
}
return /*#__PURE__*/_react["default"].createElement(_utils.PortalContext.Provider, {
value: nextPortalContext
}, /*#__PURE__*/_react["default"].createElement(_FocusedContainer.FocusedContainer, {
onKeyDown: onEsc && preventLayerClose,
trapFocus: trapFocus
}, /*#__PURE__*/_react["default"].createElement(_Keyboard.Keyboard
// should capture keyboard event before other elements,
// such as Layer
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
, {
capture: true,
onEsc: onEsc ? function (event) {
event.stopPropagation();
onEsc(event);
} : undefined,
onKeyDown: onKeyDown,
target: "document"
}, content)));
});