UNPKG

@atlaskit/modal-dialog

Version:

A modal dialog displays content that requires user interaction, in a layer above the page.

500 lines (476 loc) 23.9 kB
/* modal-wrapper.tsx generated by @compiled/babel-plugin v3.0.2 */ "use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _typeof3 = require("@babel/runtime/helpers/typeof"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; require("./modal-wrapper.compiled.css"); var _react = _interopRequireWildcard(require("react")); var React = _react; var _runtime = require("@compiled/react/runtime"); var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof")); var _bindEventListener = require("bind-event-listener"); var _reactFocusLock = _interopRequireDefault(require("react-focus-lock")); var _reactScrolllock = _interopRequireWildcard(require("react-scrolllock")); var _usePlatformLeafEventHandler = require("@atlaskit/analytics-next/usePlatformLeafEventHandler"); var _blanket = _interopRequireDefault(require("@atlaskit/blanket/blanket")); var _noop = _interopRequireDefault(require("@atlaskit/ds-lib/noop")); var _useAutoFocus = _interopRequireDefault(require("@atlaskit/ds-lib/use-auto-focus")); var _useId = require("@atlaskit/ds-lib/use-id"); var _layering = require("@atlaskit/layering/layering"); var _useNotifyOpenLayerObserver = require("@atlaskit/layering/use-notify-open-layer-observer"); var _motion = _interopRequireDefault(require("@atlaskit/motion/entering/motion")); var _useExitingPersistence = require("@atlaskit/motion/exiting-persistence/use-exiting-persistence"); var _fadeIn = _interopRequireDefault(require("@atlaskit/motion/fade-in")); var _fg = require("@atlaskit/platform-feature-flags/fg"); var _portal = _interopRequireDefault(require("@atlaskit/portal/portal")); var _combine = require("@atlaskit/pragmatic-drag-and-drop/utils/combine"); var _constants = require("@atlaskit/theme/constants"); var _createCloseEvent = require("@atlaskit/top-layer/create-close-event"); var _dialogContent = require("@atlaskit/top-layer/dialog-content"); var _dialogScrollLock = require("@atlaskit/top-layer/dialog-scroll-lock"); var _context = require("../context"); var _useModalStack = _interopRequireDefault(require("../hooks/use-modal-stack")); var _usePreventProgrammaticScroll = _interopRequireDefault(require("../hooks/use-prevent-programmatic-scroll")); var _element = require("../pragmatic-drag-and-drop/disable-dragging-to-cross-origin-iframes/element"); var _external = require("../pragmatic-drag-and-drop/disable-dragging-to-cross-origin-iframes/external"); var _textSelection = require("../pragmatic-drag-and-drop/disable-dragging-to-cross-origin-iframes/text-selection"); var _scrollContext = require("../scroll-context"); var _width = require("../width"); var _modalDialog = _interopRequireDefault(require("./modal-dialog")); 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" != _typeof3(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); } var fillScreenStyles = null; /** * Styles applied to the <dialog> wrapper, NOT the visual modal surface. */ var dialogStyles = { 'body-scroll': "_1reoewfl _18m9ewfl _19pkidpf _2hwxidpf _otyridpf _18u0idpf _1sg41wqb _maxs1wug _1jvh1wqb _1uca1wug", 'viewport-scroll': "_ln19p27b _1bsbauwl _4t3i1kxc", 'full-screen': "" }; // Visual styles for modal content inside native <dialog>. // Uses cssMap (not css) to avoid triggering no-nested-styles lint rule. var LOCAL_CURRENT_SURFACE_CSS_VAR = '--ds-elevation-surface-current'; /** * Styles applied to the visual modal surface. */ var surfaceStyles = { root: "_1e0c1txw _2lx21bp4 _bfhk1bhr _syazi7uo _1q1l1bhr _lcxv1wug _1mq81kw7 _m01u1kw7 _1dg11kw7 _mizu1v1w _1ah3dkaa _ra3xnqa1 _128mdkaa _zg7p130s", borderRadius: "_2rkoidpf _epkxpb1k" }; var topLayerScrollModeStyles = { 'full-screen': "_1bsbauwl _4t3i1kxc _kqsw1n9t _152tze3t _1e02ze3t _18m91wug _8am5i4x0", 'viewport-scroll': "_4t3i1kxc _1sg41wqb _maxs1wug _1jvh1wqb _1uca1wug _1mpv184x _1jyk17ks _g7116m9r _we1i18uh", 'body-scroll': "_1bsbauwl _4t3i1kxc _1jyk17ks _g7116m9r _we1i18uh" }; var topLayerAutoHeightStyles = null; var allowlistElements = function allowlistElements(element, callback) { // Allow focus outside modal when AUI dialog is visible // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage -- legacy FocusLock allowlist if (document.querySelector('.aui-blanket:not([hidden])')) { return false; } // Optional callback to let consumers exclude elements from focus lock if (typeof callback === 'function') { return callback(element); } return true; }; // Analytics-wrapped close handler. Extracted so both rendering paths build it // identically. function useModalCloseHandler(providedOnClose) { return (0, _usePlatformLeafEventHandler.usePlatformLeafEventHandler)({ fn: providedOnClose || _noop.default, action: 'closed', componentName: 'modalDialog', packageName: "@atlaskit/modal-dialog", packageVersion: "16.6.4" }); } function getScrollMode(_ref) { var shouldScrollInViewport = _ref.shouldScrollInViewport, isFullScreen = _ref.isFullScreen; if (isFullScreen) { return 'full-screen'; } if (shouldScrollInViewport) { return 'viewport-scroll'; } return 'body-scroll'; } function isWidthName(value) { return _width.width.values.includes(value); } function getTopLayerSurfaceWidth(input) { if (!input) { return 'auto'; } if (typeof input === 'number') { return "".concat(input, "px"); } if (isWidthName(input)) { return "".concat(_width.width.widths[input], "px"); } if (input.endsWith('%')) { // Percentage widths need special handling in the top layer. // In legacy, the percentage resolved against the Positioner's max-width // (100vw - 120px). In the top layer, the <dialog>'s containing block is the // viewport (100vw), so a raw percentage would produce a wider modal. // Transform e.g. '42%' → 'calc(42 * (100vw - 120px) / 100)' to match legacy. return "calc(".concat(parseFloat(input), " * (100vw - 120px) / 100)"); } return input; } function getTopLayerSurfaceHeight(input) { if (!input) { // Although this value would ordinarily fill the viewport height, // there is also a `max-height: max-content` applied to keep it constrained. // But we need to provide the modal with a fixed height (non-keyword, non-percentage) // in order for child elements with 100% height to work correctly. return 'calc(100vh - 120px)'; } if (typeof input === 'number') { return "".concat(input, "px"); } if (input.endsWith('%')) { // Percentage heights need special handling in the top layer. // In legacy, the percentage resolved against the Positioner's max-height // (100vh - 120px). In the top layer, the <dialog>'s containing block is the // viewport (100vh), so a raw percentage would produce a taller modal. // Transform e.g. '42%' → 'calc(42 * (100vh - 120px) / 100)' to match legacy. return "calc(".concat(parseFloat(input), " * (100vh - 120px) / 100)"); } return input; } function getDialogDismissedBy(_ref2) { var shouldCloseOnEscapePress = _ref2.shouldCloseOnEscapePress, shouldCloseOnOverlayClick = _ref2.shouldCloseOnOverlayClick; if (shouldCloseOnEscapePress && shouldCloseOnOverlayClick) { return 'escape-and-outside-click'; } if (shouldCloseOnEscapePress) { return 'escape'; } return 'none'; } /** * Top-layer rendering path (platform-dst-top-layer). * * Replaces Portal, FocusLock, ScrollLock, Blanket, Positioner, and z-index * management with native <dialog> via @atlaskit/top-layer/dialog. * * Key decisions: * - Animation: CSS transitions via @starting-style / allow-discrete. * - Close gating: Dialog only forwards allowed reasons. * - onClose event param: undefined - consumers should use close reason. * - Focus restoration: native <dialog> behavior replaces react-focus-lock's * returnFocus (see accessibility-criteria.md). * * Every hook here runs unconditionally: this component only mounts on the * top-layer path, so it never shares a hook sequence with the legacy path. */ function ModalWrapperTopLayer(props) { var autoFocus = props.autoFocus, _props$shouldCloseOnE = props.shouldCloseOnEscapePress, shouldCloseOnEscapePress = _props$shouldCloseOnE === void 0 ? true : _props$shouldCloseOnE, _props$shouldCloseOnO = props.shouldCloseOnOverlayClick, shouldCloseOnOverlayClick = _props$shouldCloseOnO === void 0 ? true : _props$shouldCloseOnO, _props$shouldScrollIn = props.shouldScrollInViewport, shouldScrollInViewport = _props$shouldScrollIn === void 0 ? false : _props$shouldScrollIn, _props$shouldReturnFo = props.shouldReturnFocus, shouldReturnFocus = _props$shouldReturnFo === void 0 ? true : _props$shouldReturnFo, providedOnClose = props.onClose, _props$onStackChange = props.onStackChange, onStackChange = _props$onStackChange === void 0 ? _noop.default : _props$onStackChange, isBlanketHidden = props.isBlanketHidden, children = props.children, height = props.height, _props$width = props.width, width = _props$width === void 0 ? 'medium' : _props$width, onCloseComplete = props.onCloseComplete, onOpenComplete = props.onOpenComplete, label = props.label, testId = props.testId, _props$isFullScreen = props.isFullScreen, isFullScreen = _props$isFullScreen === void 0 ? false : _props$isFullScreen; (0, _useModalStack.default)({ onStackChange: onStackChange }); var onCloseHandler = useModalCloseHandler(providedOnClose); var _useExitingPersistenc = (0, _useExitingPersistence.useExitingPersistence)(), isExiting = _useExitingPersistenc.isExiting, onExitFinish = _useExitingPersistenc.onFinish; // Native <dialog> always restores focus on close - no opt-out via shouldReturnFocus. var defaultTestId = testId || 'modal-dialog'; var id = (0, _useId.useId)(); var titleId = "modal-dialog-title-".concat(id); // Content container ref - used for onOpenComplete/onCloseComplete callbacks. var contentRef = (0, _react.useRef)(null); // Cache last content element for onCloseComplete after children unmount // (with reduced motion, contentRef clears before onExitFinish fires). var lastContentElRef = (0, _react.useRef)(null); if (contentRef.current) { lastContentElRef.current = contentRef.current; } // Native <dialog> ref - needed for ExitingPersistence to call dialog.close(). var dialogRef = (0, _react.useRef)(null); var modalDialogContext = (0, _react.useMemo)(function () { return { testId: defaultTestId, titleId: titleId, onClose: onCloseHandler, hasProvidedOnClose: Boolean(providedOnClose), isFullScreen: isFullScreen !== null && isFullScreen !== void 0 ? isFullScreen : false }; }, [defaultTestId, titleId, onCloseHandler, providedOnClose, isFullScreen]); // Dialog has already applied the close behavior configured by `dismissedBy`. // Pass a synthetic event to satisfy the KeyboardOrMouseEvent contract. var onDialogClose = (0, _react.useCallback)(function (_ref3) { var reason = _ref3.reason; onCloseHandler((0, _createCloseEvent.createCloseEvent)({ reason: reason })); }, [onCloseHandler]); var dismissedBy = getDialogDismissedBy({ shouldCloseOnEscapePress: shouldCloseOnEscapePress, shouldCloseOnOverlayClick: shouldCloseOnOverlayClick }); var shouldShimCloseOnOverlayClick = shouldCloseOnOverlayClick && !shouldCloseOnEscapePress; // Required until we fully remove `shouldCloseOnEscapePress`. // Dialog's `dismissedBy` options do not support outside click without Escape (because it is a bad pattern) (0, _react.useEffect)(function () { var dialog = dialogRef.current; if (!dialog || !shouldShimCloseOnOverlayClick) { return; } return (0, _bindEventListener.bind)(dialog, { type: 'click', listener: function listener(event) { if (event.target === event.currentTarget) { onDialogClose({ reason: 'overlay-click' }); } } }); }, [onDialogClose, shouldShimCloseOnOverlayClick]); // ExitingPersistence: isExiting → isOpen={false} → Dialog exit animation → // onExitFinish → onCloseComplete + unmount. var handleDialogExitFinish = (0, _react.useCallback)(function () { var _contentRef$current; var el = (_contentRef$current = contentRef.current) !== null && _contentRef$current !== void 0 ? _contentRef$current : lastContentElRef.current; if (onCloseComplete && el) { onCloseComplete(el); } lastContentElRef.current = null; onExitFinish === null || onExitFinish === void 0 || onExitFinish(); }, [onExitFinish, onCloseComplete]); var handleDialogEnterFinish = (0, _react.useCallback)(function () { if (onOpenComplete && contentRef.current) { onOpenComplete(contentRef.current, true); } }, [onOpenComplete]); // Honor `shouldReturnFocus={ref}` on unmount. // Native <dialog>.close() restores focus to the trigger that opened it, // but the consumer asked for focus to go to a specific element instead. // Run this in an unmount cleanup so it fires after dialog.close() // (which fires in the Dialog's effect cleanup). var shouldReturnFocusRef = (0, _react.useRef)(shouldReturnFocus); shouldReturnFocusRef.current = shouldReturnFocus; (0, _react.useEffect)(function () { return function () { var target = shouldReturnFocusRef.current; if ((0, _typeof2.default)(target) === 'object' && target.current) { target.current.focus(); } }; }, []); // Focus a ref-targeted element after mount (when autoFocus is a ref). // When true, native <dialog>.showModal() handles focus automatically. (0, _useAutoFocus.default)((0, _typeof2.default)(autoFocus) === 'object' ? autoFocus : undefined, (0, _typeof2.default)(autoFocus) === 'object'); // Chrome cross-origin iframe DnD workaround (crbug.com/362301053) (0, _react.useEffect)(function () { return (0, _combine.combine)((0, _element.disableDraggingToCrossOriginIFramesForElement)(), (0, _textSelection.disableDraggingToCrossOriginIFramesForTextSelection)(), (0, _external.disableDraggingToCrossOriginIFramesForExternal)()); }, []); var scrollMode = getScrollMode({ isFullScreen: isFullScreen, shouldScrollInViewport: shouldScrollInViewport }); return /*#__PURE__*/React.createElement(_dialogContent.Dialog, (0, _extends2.default)({ ref: dialogRef, onClose: onDialogClose, dismissedBy: dismissedBy, onExitFinish: handleDialogExitFinish, shouldAnimate: !isFullScreen, isOpen: !isExiting, onEnterFinish: handleDialogEnterFinish, shouldHideBackdrop: isBlanketHidden // Dialog requires at least one of `label` or `labelledBy` (string, not undefined). // Prefer the consumer-provided `label`; otherwise reference the internal `titleId`. }, label ? { label: label } : { labelledBy: titleId }, { testId: defaultTestId, xcss: dialogStyles[scrollMode] }), /*#__PURE__*/React.createElement(_dialogScrollLock.DialogScrollLock, { isOpen: true }), /*#__PURE__*/React.createElement("div", { ref: contentRef, style: { '--modal-dialog-height': getTopLayerSurfaceHeight(height), '--modal-dialog-width': getTopLayerSurfaceWidth(width) }, className: (0, _runtime.ax)([surfaceStyles.root, !isFullScreen && surfaceStyles.borderRadius, topLayerScrollModeStyles[scrollMode], !height && !isFullScreen && "_e8uo1ris"]) }, /*#__PURE__*/React.createElement(_context.ModalContext.Provider, { value: modalDialogContext }, /*#__PURE__*/React.createElement(_scrollContext.ScrollContext.Provider, { value: shouldScrollInViewport }, children)))); } /** * Legacy rendering path (Portal + FocusLock + ScrollLock + Blanket). * * Every hook here runs unconditionally: this component only mounts on the * legacy path, so it never shares a hook sequence with the top-layer path. */ var ModalWrapperLegacy = /*#__PURE__*/(0, _react.forwardRef)(function (props, ref) { var autoFocus = props.autoFocus, focusLockAllowlist = props.focusLockAllowlist, _props$shouldCloseOnE2 = props.shouldCloseOnEscapePress, shouldCloseOnEscapePress = _props$shouldCloseOnE2 === void 0 ? true : _props$shouldCloseOnE2, _props$shouldCloseOnO2 = props.shouldCloseOnOverlayClick, shouldCloseOnOverlayClick = _props$shouldCloseOnO2 === void 0 ? true : _props$shouldCloseOnO2, _props$shouldScrollIn2 = props.shouldScrollInViewport, shouldScrollInViewport = _props$shouldScrollIn2 === void 0 ? false : _props$shouldScrollIn2, _props$shouldReturnFo2 = props.shouldReturnFocus, shouldReturnFocus = _props$shouldReturnFo2 === void 0 ? true : _props$shouldReturnFo2, stackIndexOverride = props.stackIndex, providedOnClose = props.onClose, _props$onStackChange2 = props.onStackChange, onStackChange = _props$onStackChange2 === void 0 ? _noop.default : _props$onStackChange2, isBlanketHidden = props.isBlanketHidden, children = props.children, height = props.height, width = props.width, onCloseComplete = props.onCloseComplete, onOpenComplete = props.onOpenComplete, label = props.label, testId = props.testId, isFullScreen = props.isFullScreen, _props$UNSAFE_shouldD = props.UNSAFE_shouldDisableMotionUplift, UNSAFE_shouldDisableMotionUplift = _props$UNSAFE_shouldD === void 0 ? false : _props$UNSAFE_shouldD; var calculatedStackIndex = (0, _useModalStack.default)({ onStackChange: onStackChange }); var stackIndex = stackIndexOverride || calculatedStackIndex; var isForeground = stackIndex === 0; // If no ref is provided, autofocus on first element var autoFocusLock = !((0, _typeof2.default)(autoFocus) === 'object'); var onCloseHandler = useModalCloseHandler(providedOnClose); var onBlanketClicked = (0, _react.useCallback)(function (e) { if (shouldCloseOnOverlayClick) { onCloseHandler(e); } }, [shouldCloseOnOverlayClick, onCloseHandler]); // Stable callback to avoid re-renders when focusLockAllowlist is not provided. var allowListCallback = (0, _react.useCallback)(function (element) { return allowlistElements(element, focusLockAllowlist); }, [focusLockAllowlist]); // Prevent background scroll (top-layer path uses DialogScrollLock instead). (0, _usePreventProgrammaticScroll.default)(); // Register with the open layer observer. On the top-layer path the Dialog // primitive does this itself, which is why it lives in this legacy-only // component rather than the shared dispatcher. (0, _useNotifyOpenLayerObserver.useNotifyOpenLayerObserver)({ type: 'modal', // Always open — modal is conditionally rendered when visible. isOpen: true, // No-op: no current use case for programmatic close via OpenLayerObserver. onClose: _noop.default }); var modalDialogWithBlanket = /*#__PURE__*/React.createElement(_blanket.default, { isTinted: !isBlanketHidden, onBlanketClicked: onBlanketClicked, testId: testId && "".concat(testId, "--blanket") }, /*#__PURE__*/React.createElement(_modalDialog.default, { testId: testId, label: label, autoFocus: autoFocus, stackIndex: stackIndex, onClose: onCloseHandler, shouldCloseOnEscapePress: shouldCloseOnEscapePress && isForeground, shouldScrollInViewport: shouldScrollInViewport, height: height, width: width, onCloseComplete: onCloseComplete, onOpenComplete: onOpenComplete, hasProvidedOnClose: Boolean(providedOnClose), isFullScreen: isFullScreen, UNSAFE_shouldDisableMotionUplift: UNSAFE_shouldDisableMotionUplift, ref: ref }, children)); var returnFocus = true; var onDeactivation = _noop.default; if ('boolean' === typeof shouldReturnFocus) { returnFocus = shouldReturnFocus; } else { onDeactivation = function onDeactivation() { window.setTimeout(function () { var _shouldReturnFocus$cu; (_shouldReturnFocus$cu = shouldReturnFocus.current) === null || _shouldReturnFocus$cu === void 0 || _shouldReturnFocus$cu.focus(); }, 0); }; } return /*#__PURE__*/React.createElement(_layering.Layering, { isDisabled: false }, /*#__PURE__*/React.createElement(_portal.default, { zIndex: _constants.layers.modal() }, !UNSAFE_shouldDisableMotionUplift && (0, _fg.fg)('platform-dst-motion-uplift-modal') ? /*#__PURE__*/React.createElement(_motion.default, { enteringAnimation: "var(--ds-blanket-enter, 250ms cubic-bezier(0.4, 0, 0, 1) FadeIn0to100)", exitingAnimation: "var(--ds-blanket-exit, 200ms cubic-bezier(0.6, 0, 0.8, 0.6) FadeOut100to0)" }, /*#__PURE__*/React.createElement("div", { "aria-hidden": !isForeground, className: (0, _runtime.ax)(["_1bsbauwl _4t3i1kxc _kqsw1n9t _152tze3t _1e02ze3t _18m91wug _8am5i4x0"]) }, /*#__PURE__*/React.createElement(_reactFocusLock.default, { autoFocus: autoFocusLock, returnFocus: returnFocus, onDeactivation: onDeactivation, whiteList: allowListCallback }, /*#__PURE__*/React.createElement(_reactScrolllock.default, null), shouldScrollInViewport ? /*#__PURE__*/React.createElement(_reactScrolllock.TouchScrollable, null, modalDialogWithBlanket) : modalDialogWithBlanket))) : /*#__PURE__*/React.createElement(_fadeIn.default, null, function (fadeInProps) { return /*#__PURE__*/React.createElement("div", (0, _extends2.default)({}, fadeInProps, { // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop className: (0, _runtime.ax)(["_1bsbauwl _4t3i1kxc _kqsw1n9t _152tze3t _1e02ze3t _18m91wug _8am5i4x0", fadeInProps.className]), "aria-hidden": !isForeground }), /*#__PURE__*/React.createElement(_reactFocusLock.default, { autoFocus: autoFocusLock, returnFocus: returnFocus, onDeactivation: onDeactivation, whiteList: allowListCallback }, /*#__PURE__*/React.createElement(_reactScrolllock.default, null), shouldScrollInViewport ? /*#__PURE__*/React.createElement(_reactScrolllock.TouchScrollable, null, modalDialogWithBlanket) : modalDialogWithBlanket)); }))); }); // Choose the rendering implementation at the component boundary rather than // gating hooks inside a single component. Each implementation owns its own // hooks unconditionally, so a runtime feature-flag change swaps component types // (a clean remount) instead of changing the hook order of a mounted component. var InternalModalWrapper = /*#__PURE__*/(0, _react.forwardRef)(function (props, ref) { if ((0, _fg.fg)('platform-dst-top-layer')) { // TODO: the top-layer path does not forward the external `ref` (parity // with the pre-refactor behavior, where the top-layer branch never // consumed it). The public type still advertises `RefAttributes`, so a // consumer ref silently no-ops here. Follow up by forwarding `ref` to a // sensible element (for example the content `div`) once ref parity is // intentionally desired. return /*#__PURE__*/React.createElement(ModalWrapperTopLayer, props); } return /*#__PURE__*/React.createElement(ModalWrapperLegacy, (0, _extends2.default)({ ref: ref }, props)); }); // eslint-disable-next-line @repo/internal/react/require-jsdoc var _default = exports.default = InternalModalWrapper;