@atlaskit/popup
Version:
A popup displays brief content in an overlay.
249 lines (245 loc) • 10.4 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useCloseManager = void 0;
var _react = require("react");
var _bindEventListener = require("bind-event-listener");
var _noop = _interopRequireDefault(require("@atlaskit/ds-lib/noop"));
var _layering = require("@atlaskit/layering");
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
var _isElementInteractive = require("./utils/is-element-interactive");
var _useAnimationFrame2 = require("./utils/use-animation-frame");
// eslint-disable-next-line no-duplicate-imports
var useCloseManager = exports.useCloseManager = function useCloseManager(_ref) {
var isOpen = _ref.isOpen,
onClose = _ref.onClose,
popupRef = _ref.popupRef,
triggerRef = _ref.triggerRef,
autoFocus = _ref.autoFocus,
shouldDisableFocusTrap = _ref.shouldDisableFocusTrap,
capture = _ref.shouldUseCaptureOnOutsideClick,
shouldCloseOnTab = _ref.shouldCloseOnTab,
shouldRenderToParent = _ref.shouldRenderToParent;
var _useLayering = (0, _layering.useLayering)(),
isLayerDisabled = _useLayering.isLayerDisabled,
currentLevel = _useLayering.currentLevel;
var _useAnimationFrame = (0, _useAnimationFrame2.useAnimationFrame)(),
requestFrame = _useAnimationFrame.requestFrame,
cancelAllFrames = _useAnimationFrame.cancelAllFrames;
var mouseDownTarget = (0, _react.useRef)(null);
(0, _react.useEffect)(function () {
if (!isOpen || !popupRef) {
return _noop.default;
}
var closePopup = function closePopup(event) {
if (onClose) {
var _currentLevel = null;
if (event.target instanceof HTMLElement) {
var _event$target$closest, _event$target$closest2;
_currentLevel = (_event$target$closest = (_event$target$closest2 = event.target.closest("[data-ds--level]")) === null || _event$target$closest2 === void 0 ? void 0 : _event$target$closest2.getAttribute('data-ds--level')) !== null && _event$target$closest !== void 0 ? _event$target$closest : null;
}
_currentLevel ? onClose(event, Number(_currentLevel)) : onClose(event);
}
if (shouldDisableFocusTrap && (0, _platformFeatureFlags.fg)('platform_dst_popup-disable-focuslock')) {
// Restoring the normal focus order for trigger.
requestFrame(function () {
triggerRef === null || triggerRef === void 0 || triggerRef.setAttribute('tabindex', '0');
if (popupRef && autoFocus) {
popupRef.setAttribute('tabindex', '0');
}
});
}
};
// This check is required for cases where components like
// Select or DDM are placed inside a Popup. A click
// on a MenuItem or Option would close the Popup, without registering
// a click on DDM/Select.
// Users would have to call `onClose` manually to close the Popup in these cases.
// You can see the bug in action here:
// https://codesandbox.io/s/atlaskitpopup-default-forked-2eb87?file=/example.tsx:0-1788
var onClick = function onClick(event) {
var target = event.target;
var doesDomNodeExist = document.body.contains(target);
if (!doesDomNodeExist) {
return;
}
if (isLayerDisabled()) {
if (target instanceof HTMLElement) {
var _target$closest;
var layeredElement = (_target$closest = target.closest) === null || _target$closest === void 0 ? void 0 : _target$closest.call(target, "[data-ds--level]");
if (layeredElement) {
var closeType = layeredElement.getAttribute('[data-ds--close--type]');
if (closeType === 'single') {
// if the close type is single, we won't close other disabled layers when clicking outside
return;
}
var levelOfClickedLayer = layeredElement.getAttribute('data-ds--level');
if (levelOfClickedLayer && Number(levelOfClickedLayer) > currentLevel) {
// won't trigger onClick event when we click in a higher layer.
return;
}
}
}
}
var isClickOnPopup = popupRef && popupRef.contains(target);
var isClickOnTrigger = triggerRef && triggerRef.contains(target);
var didClickStartInsidePopup = popupRef && mouseDownTarget.current instanceof Node && popupRef.contains(mouseDownTarget.current);
if (!isClickOnPopup && !isClickOnTrigger && !didClickStartInsidePopup) {
closePopup(event);
// If there was an outside click on a non-interactive element, the focus should be on the trigger.
if (document.activeElement && !(0, _isElementInteractive.isInteractiveElement)(document.activeElement) && (0, _platformFeatureFlags.fg)('platform_dst_popup-disable-focuslock')) {
triggerRef === null || triggerRef === void 0 || triggerRef.focus();
}
}
};
var onMouseDown = function onMouseDown(event) {
/**
* Tracking the target of the mouse down event.
* This is used to prevent the popup from closing when the user mouses down inside the popup, but then
* moves the mouse outside the popup before releasing the mouse button.
*
* This is a common user interaction - users may have mistakenly clicked on something, or changed their mind,
* so they try to cancel their click by moving their mouse away from what they had moused down on.
*
* Blanket uses the same technique.
*/
mouseDownTarget.current = event.target;
};
var onKeyDown = function onKeyDown(event) {
var key = event.key;
if ((key === 'Escape' || key === 'Esc') && (0, _platformFeatureFlags.fg)('platform_dst_nested_escape')) {
var _eventTarget$closest;
var eventTarget = event.target instanceof HTMLElement ? event.target : null;
var layeredAncestor = eventTarget === null || eventTarget === void 0 || (_eventTarget$closest = eventTarget.closest) === null || _eventTarget$closest === void 0 ? void 0 : _eventTarget$closest.call(eventTarget, '[data-ds--level]');
var levelStr = layeredAncestor === null || layeredAncestor === void 0 ? void 0 : layeredAncestor.getAttribute('data-ds--level');
if (levelStr && Number(levelStr) > currentLevel) {
return;
}
}
if ((0, _platformFeatureFlags.fg)('platform_dst_popup-disable-focuslock')) {
var shiftKey = event.shiftKey;
if (shiftKey && key === 'Tab' && !shouldRenderToParent) {
if (isLayerDisabled()) {
return;
}
// We need to move the focus to the popup trigger when the popup is displayed in React.Portal.
requestFrame(function () {
var isPopupFocusOut = popupRef && !popupRef.contains(document.activeElement);
if (isPopupFocusOut) {
closePopup(event);
if (currentLevel === 1) {
triggerRef === null || triggerRef === void 0 || triggerRef.focus();
}
}
});
return;
}
if (key === 'Tab') {
var _document$activeEleme;
// We have cases where we need to close the Popup on Tab press.
// Example: DropdownMenu
if (shouldCloseOnTab) {
if (isLayerDisabled()) {
return;
}
closePopup(event);
return;
}
if (isLayerDisabled() && (_document$activeEleme = document.activeElement) !== null && _document$activeEleme !== void 0 && _document$activeEleme.closest('[aria-modal]')) {
return;
}
if (shouldDisableFocusTrap) {
if (shouldRenderToParent) {
// We need to move the focus to the previous interactive element before popup trigger
requestFrame(function () {
var isPopupFocusOut = popupRef && !popupRef.contains(document.activeElement);
if (isPopupFocusOut) {
closePopup(event);
}
});
} else {
requestFrame(function () {
if (!document.hasFocus()) {
closePopup(event);
}
});
}
return;
}
}
if (isLayerDisabled()) {
return;
}
if (key === 'Escape' || key === 'Esc') {
if (triggerRef && autoFocus) {
triggerRef.focus();
}
closePopup(event);
}
} else {
if (isLayerDisabled()) {
return;
}
if (key === 'Escape' || key === 'Esc' || shouldCloseOnTab && key === 'Tab') {
closePopup(event);
}
}
};
var unbind = _noop.default;
if ((0, _platformFeatureFlags.fg)('popup-onclose-fix')) {
setTimeout(function () {
unbind = (0, _bindEventListener.bindAll)(window, [{
type: 'click',
listener: onClick,
options: {
capture: capture
}
}, {
type: 'keydown',
listener: onKeyDown
}, {
type: 'mousedown',
listener: onMouseDown
}]);
}, 0);
} else {
unbind = (0, _bindEventListener.bindAll)(window, [{
type: 'click',
listener: onClick,
options: {
capture: capture
}
}, {
type: 'keydown',
listener: onKeyDown
}, {
type: 'mousedown',
listener: onMouseDown
}]);
}
// bind onBlur event listener to fix popup not close when clicking on iframe outside
var unbindBlur = _noop.default;
unbindBlur = (0, _bindEventListener.bind)(window, {
type: 'blur',
listener: function onBlur(e) {
if (isLayerDisabled() || !(document.activeElement instanceof HTMLIFrameElement)) {
return;
}
closePopup(e);
}
});
return function () {
if ((0, _platformFeatureFlags.fg)('popup-onclose-fix')) {
setTimeout(function () {
unbind();
}, 0);
} else {
unbind();
}
cancelAllFrames();
unbindBlur();
};
}, [isOpen, onClose, popupRef, triggerRef, autoFocus, shouldDisableFocusTrap, capture, isLayerDisabled, shouldCloseOnTab, currentLevel, shouldRenderToParent, requestFrame, cancelAllFrames]);
};