@atlaskit/modal-dialog
Version:
A modal dialog displays content that requires user interaction, in a layer above the page.
403 lines (376 loc) • 21.9 kB
JavaScript
/* modal-wrapper.tsx generated by @compiled/babel-plugin v0.39.1 */
"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 _reactFocusLock = _interopRequireDefault(require("react-focus-lock"));
var _reactScrolllock = _interopRequireWildcard(require("react-scrolllock"));
var _analyticsNext = require("@atlaskit/analytics-next");
var _blanket = _interopRequireDefault(require("@atlaskit/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");
var _openLayerObserver = require("@atlaskit/layering/experimental/open-layer-observer");
var _motion = require("@atlaskit/motion");
var _exitingPersistence = require("@atlaskit/motion/exiting-persistence");
var _fadeIn = _interopRequireDefault(require("@atlaskit/motion/fade-in"));
var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
var _portal = _interopRequireDefault(require("@atlaskit/portal"));
var _combine = require("@atlaskit/pragmatic-drag-and-drop/combine");
var _constants = require("@atlaskit/theme/constants");
var _animations = require("@atlaskit/top-layer/animations");
var _createCloseEvent = require("@atlaskit/top-layer/create-close-event");
var _dialog = require("@atlaskit/top-layer/dialog");
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 _modalDialog = _interopRequireWildcard(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 modalAnimation = (0, _animations.dialogSlideUpAndFade)();
var fillScreenStyles = null;
// 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';
var topLayerStyles = {
content: "_1e0c1txw _4t3i1osq _2lx21bp4 _bfhk1bhr _syazi7uo _1q1l1bhr _lcxv1wug _1mq81kw7 _m01u1kw7 _1dg11kw7 _mizu1v1w _1ah3dkaa _ra3xnqa1 _128mdkaa _zg7p130s",
borderRadius: "_epkxfajl",
borderRadiusT26: "_epkxpb1k"
};
// Scroll-mode styles for the content div.
// Height overrides use ID-scoped <style> (see dialogPositionStyles) because
// Compiled atomic classes have specificity (0,1,0) (increaseSpecificity is disabled).
// The doubled-ID selector (#id#id > div) at (2,0,1) reliably wins.
// Only non-height properties needing the && boost remain here.
var topLayerBodyScrollStyles = null;
var topLayerViewportScrollStyles = 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 (Boolean(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;
};
var InternalModalWrapper = /*#__PURE__*/(0, _react.forwardRef)(function (props, ref) {
var autoFocus = props.autoFocus,
focusLockAllowlist = props.focusLockAllowlist,
_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,
stackIndexOverride = props.stackIndex,
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,
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 = (0, _analyticsNext.usePlatformLeafEventHandler)({
fn: providedOnClose || _noop.default,
action: 'closed',
componentName: 'modalDialog',
packageName: "@atlaskit/modal-dialog",
packageVersion: "15.1.3"
});
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]);
// Called outside the feature-flag branch to keep hook order stable.
// Legacy path: FadeIn calls onFinish. Top-layer path: called directly.
var _useExitingPersistenc = (0, _exitingPersistence.useExitingPersistence)(),
isExiting = _useExitingPersistenc.isExiting,
onExitFinish = _useExitingPersistenc.onFinish;
// Prevent background scroll (top-layer path uses DialogScrollLock instead).
// Safe conditional hook: feature flags are resolved once at startup.
if (!(0, _platformFeatureFlags.fg)('platform-dst-top-layer')) {
// eslint-disable-next-line react-hooks/rules-of-hooks
(0, _usePreventProgrammaticScroll.default)();
}
// On the top-layer path, the Dialog primitive registers with the observer
// directly, so we skip registration here to avoid double-counting.
// Safe conditional hook: feature flags are resolved once at startup.
if (!(0, _platformFeatureFlags.fg)('platform-dst-top-layer')) {
// eslint-disable-next-line react-hooks/rules-of-hooks
(0, _openLayerObserver.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
});
}
/**
* Top-layer 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: onDialogClose only forwards allowed reasons
* (see notes/guides/dialog-close-flow.md).
* - onClose event param: undefined - consumers should use close reason.
* - Focus restoration: native <dialog> behavior replaces react-focus-lock's
* returnFocus (see accessibility-criteria.md).
*/
if ((0, _platformFeatureFlags.fg)('platform-dst-top-layer')) {
// Native <dialog> always restores focus on close - no opt-out via shouldReturnFocus.
var defaultTestId = testId || 'modal-dialog';
// eslint-disable-next-line react-hooks/rules-of-hooks
var id = (0, _useId.useId)();
var titleId = "modal-dialog-title-".concat(id);
// Content container ref - used for onOpenComplete/onCloseComplete callbacks.
// eslint-disable-next-line react-hooks/rules-of-hooks
var contentRef = (0, _react.useRef)(null);
// Cache last content element for onCloseComplete after children unmount
// (with reduced motion, contentRef clears before onExitFinish fires).
// eslint-disable-next-line react-hooks/rules-of-hooks
var lastContentElRef = (0, _react.useRef)(null);
if (contentRef.current) {
lastContentElRef.current = contentRef.current;
}
// Native <dialog> ref - needed for ExitingPersistence to call dialog.close().
// eslint-disable-next-line react-hooks/rules-of-hooks
var dialogRef = (0, _react.useRef)(null);
// eslint-disable-next-line react-hooks/rules-of-hooks
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]);
// Only forward close when the reason is allowed by props.
// Passes a synthetic event to satisfy the KeyboardOrMouseEvent contract.
// eslint-disable-next-line react-hooks/rules-of-hooks
var onDialogClose = (0, _react.useCallback)(function (_ref) {
var reason = _ref.reason;
if (reason === 'escape' && shouldCloseOnEscapePress) {
onCloseHandler((0, _createCloseEvent.createCloseEvent)({
reason: reason
}));
}
if (reason === 'overlay-click' && shouldCloseOnOverlayClick) {
onCloseHandler((0, _createCloseEvent.createCloseEvent)({
reason: reason
}));
}
}, [onCloseHandler, shouldCloseOnEscapePress, shouldCloseOnOverlayClick]);
// ExitingPersistence: isExiting → isOpen={false} → Dialog exit animation →
// onExitFinish → onCloseComplete + unmount.
// eslint-disable-next-line react-hooks/rules-of-hooks
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]);
// Fire onOpenComplete after mount.
// eslint-disable-next-line react-hooks/rules-of-hooks
(0, _react.useEffect)(function () {
if (onOpenComplete && contentRef.current) {
onOpenComplete(contentRef.current, true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 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).
// eslint-disable-next-line react-hooks/rules-of-hooks
var shouldReturnFocusRef = (0, _react.useRef)(shouldReturnFocus);
shouldReturnFocusRef.current = shouldReturnFocus;
// eslint-disable-next-line react-hooks/rules-of-hooks
(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.
// eslint-disable-next-line react-hooks/rules-of-hooks
(0, _useAutoFocus.default)((0, _typeof2.default)(autoFocus) === 'object' ? autoFocus : undefined, (0, _typeof2.default)(autoFocus) === 'object');
// Chrome cross-origin iframe DnD workaround (crbug.com/362301053)
// eslint-disable-next-line react-hooks/rules-of-hooks
(0, _react.useEffect)(function () {
return (0, _combine.combine)((0, _element.disableDraggingToCrossOriginIFramesForElement)(), (0, _textSelection.disableDraggingToCrossOriginIFramesForTextSelection)(), (0, _external.disableDraggingToCrossOriginIFramesForExternal)());
}, []);
// Responsive layout via ID-scoped <style> (same pattern as Dialog's hideBackdrop).
// ID selector beats Compiled atomic classes without !important and supports @media.
var namedWidth = (0, _modalDialog.dialogWidth)(width !== null && width !== void 0 ? width : 'medium');
var dialogId = "modal-dialog-".concat(id);
var escapedDialogId = CSS.escape(dialogId);
// 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.
var resolvedWidth = namedWidth.endsWith('%') ? "calc(".concat(parseFloat(namedWidth), " * (100vw - 120px) / 100)") : namedWidth;
var dialogStyle = isFullScreen ? {
width: '100vw',
height: '100vh',
margin: '0'
} : {
width: "min(".concat(resolvedWidth, ", 100vw)")
};
// Shift stacked background modals down by space.100 (8px) per level.
if (stackIndex > 0) {
dialogStyle['transform'] = "translateY(calc(".concat(stackIndex, "px * ", "var(--ds-space-100, 8px)", "))");
}
// Mobile: viewport fill. Desktop (≥ 30rem): gutter margins, auto height.
// Content-div height set via #id > div to beat Compiled's atomic specificity.
var desktopMargin = shouldScrollInViewport ? '60px auto' : '60px auto auto';
var resolvedHeight = (0, _modalDialog.dialogHeight)(height);
// Body-scroll: specified height or auto. Viewport-scroll: uses min-height.
var desktopContentHeight = shouldScrollInViewport ? 'auto' : resolvedHeight;
var desktopContentMinHeight = shouldScrollInViewport ? resolvedHeight : 'auto';
// Viewport-scroll: the legacy Positioner was a fixed 100vh container that
// scrolled internally, so the modal section could fill (100vh - 60px top gutter).
// In the top layer the <dialog> sizes to content with height:auto, so we need
// an explicit min-height to ensure the dialog stretches to the same visible area.
var desktopDialogMinHeight = shouldScrollInViewport ? 'min-height:calc(100vh - 60px);' : '';
// Doubled-ID selector (#id#id > div) at specificity (2,0,1) beats
// Compiled atomic classes at (0,1,0) (increaseSpecificity is disabled).
var dialogPositionStyles = isFullScreen ? '' : // Mobile: edge-to-edge. Desktop (≥ 30rem): 60px gutters, max-width.
"#".concat(escapedDialogId, "#").concat(escapedDialogId, "{margin:0;height:100vh}#").concat(escapedDialogId, "#").concat(escapedDialogId, ">div{height:100%}@media(min-width:30rem){#").concat(escapedDialogId, "#").concat(escapedDialogId, "{margin:").concat(desktopMargin, ";height:auto;").concat(desktopDialogMinHeight, "max-width:calc(100vw - 120px)}#").concat(escapedDialogId, "#").concat(escapedDialogId, ">div{height:").concat(desktopContentHeight, ";min-height:").concat(desktopContentMinHeight, "}}");
return /*#__PURE__*/React.createElement(_dialog.Dialog, {
ref: dialogRef,
id: dialogId,
onClose: onDialogClose,
onExitFinish: handleDialogExitFinish,
animate: isFullScreen ? false : modalAnimation,
isOpen: !isExiting,
shouldHideBackdrop: stackIndex > 0 || Boolean(isBlanketHidden),
label: label,
labelledBy: label ? undefined : titleId,
testId: defaultTestId
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop
,
style: dialogStyle
}, /*#__PURE__*/React.createElement(_dialogScrollLock.DialogScrollLock, null), dialogPositionStyles &&
/*#__PURE__*/
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-global-styles
React.createElement("style", null, dialogPositionStyles), /*#__PURE__*/React.createElement("div", {
ref: contentRef,
className: (0, _runtime.ax)([topLayerStyles.content, !isFullScreen && topLayerStyles.borderRadius, !isFullScreen && (0, _platformFeatureFlags.fg)('platform-dst-shape-theme-default') && topLayerStyles.borderRadiusT26, !isFullScreen && !shouldScrollInViewport && "_bolhzwhf", !isFullScreen && shouldScrollInViewport && "_1tke1kxc _c71lglyw"])
}, /*#__PURE__*/React.createElement(_context.ModalContext.Provider, {
value: modalDialogContext
}, /*#__PURE__*/React.createElement(_context.ScrollContext.Provider, {
value: shouldScrollInViewport
}, children))));
}
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, _platformFeatureFlags.fg)('platform-dst-motion-uplift-modal') ? /*#__PURE__*/React.createElement(_motion.Motion, {
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));
})));
});
// eslint-disable-next-line @repo/internal/react/require-jsdoc
var _default = exports.default = InternalModalWrapper;