UNPKG

@atlaskit/popup

Version:

A popup displays brief content in an overlay.

248 lines (246 loc) 9.42 kB
// eslint-disable-next-line no-duplicate-imports import { useEffect, useRef } from 'react'; import { bind, bindAll } from 'bind-event-listener'; import noop from '@atlaskit/ds-lib/noop'; import { useLayering } from '@atlaskit/layering'; import { fg } from '@atlaskit/platform-feature-flags'; import { isInteractiveElement } from './utils/is-element-interactive'; import { useAnimationFrame } from './utils/use-animation-frame'; export const useCloseManager = ({ isOpen, onClose, popupRef, triggerRef, autoFocus, shouldDisableFocusTrap, shouldUseCaptureOnOutsideClick: capture, shouldCloseOnTab, shouldRenderToParent }) => { const { isLayerDisabled, currentLevel } = useLayering(); const { requestFrame, cancelAllFrames } = useAnimationFrame(); const mouseDownTarget = useRef(null); useEffect(() => { if (!isOpen || !popupRef) { return noop; } const closePopup = event => { if (onClose) { let 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 && fg('platform_dst_popup-disable-focuslock')) { // Restoring the normal focus order for trigger. requestFrame(() => { triggerRef === null || triggerRef === void 0 ? 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 const onClick = event => { const { target } = event; const doesDomNodeExist = document.body.contains(target); if (!doesDomNodeExist) { return; } if (isLayerDisabled()) { if (target instanceof HTMLElement) { var _target$closest; const layeredElement = (_target$closest = target.closest) === null || _target$closest === void 0 ? void 0 : _target$closest.call(target, `[data-ds--level]`); if (layeredElement) { const 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; } const levelOfClickedLayer = layeredElement.getAttribute('data-ds--level'); if (levelOfClickedLayer && Number(levelOfClickedLayer) > currentLevel) { // won't trigger onClick event when we click in a higher layer. return; } } } } const isClickOnPopup = popupRef && popupRef.contains(target); const isClickOnTrigger = triggerRef && triggerRef.contains(target); const 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 && !isInteractiveElement(document.activeElement) && fg('platform_dst_popup-disable-focuslock')) { triggerRef === null || triggerRef === void 0 ? void 0 : triggerRef.focus(); } } }; const 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; }; const onKeyDown = event => { const key = event.key; if ((key === 'Escape' || key === 'Esc') && fg('platform_dst_nested_escape')) { var _eventTarget$closest; const eventTarget = event.target instanceof HTMLElement ? event.target : null; const layeredAncestor = eventTarget === null || eventTarget === void 0 ? void 0 : (_eventTarget$closest = eventTarget.closest) === null || _eventTarget$closest === void 0 ? void 0 : _eventTarget$closest.call(eventTarget, '[data-ds--level]'); const levelStr = layeredAncestor === null || layeredAncestor === void 0 ? void 0 : layeredAncestor.getAttribute('data-ds--level'); if (levelStr && Number(levelStr) > currentLevel) { return; } } if (fg('platform_dst_popup-disable-focuslock')) { const { shiftKey } = event; 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(() => { const isPopupFocusOut = popupRef && !popupRef.contains(document.activeElement); if (isPopupFocusOut) { closePopup(event); if (currentLevel === 1) { triggerRef === null || triggerRef === void 0 ? 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(() => { const isPopupFocusOut = popupRef && !popupRef.contains(document.activeElement); if (isPopupFocusOut) { closePopup(event); } }); } else { requestFrame(() => { 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); } } }; let unbind = noop; if (fg('popup-onclose-fix')) { setTimeout(() => { unbind = bindAll(window, [{ type: 'click', listener: onClick, options: { capture } }, { type: 'keydown', listener: onKeyDown }, { type: 'mousedown', listener: onMouseDown }]); }, 0); } else { unbind = bindAll(window, [{ type: 'click', listener: onClick, options: { capture } }, { type: 'keydown', listener: onKeyDown }, { type: 'mousedown', listener: onMouseDown }]); } // bind onBlur event listener to fix popup not close when clicking on iframe outside let unbindBlur = noop; unbindBlur = bind(window, { type: 'blur', listener: function onBlur(e) { if (isLayerDisabled() || !(document.activeElement instanceof HTMLIFrameElement)) { return; } closePopup(e); } }); return () => { if (fg('popup-onclose-fix')) { setTimeout(() => { unbind(); }, 0); } else { unbind(); } cancelAllFrames(); unbindBlur(); }; }, [isOpen, onClose, popupRef, triggerRef, autoFocus, shouldDisableFocusTrap, capture, isLayerDisabled, shouldCloseOnTab, currentLevel, shouldRenderToParent, requestFrame, cancelAllFrames]); };