@atlaskit/modal-dialog 16.2.2 → 16.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @atlaskit/modal-dialog
2
2
 
3
+ ## 16.2.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [`3da013cc133e5`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/3da013cc133e5) -
8
+ Internal refactor for safer feature flag usage.
9
+ - Updated dependencies
10
+
3
11
  ## 16.2.2
4
12
 
5
13
  ### Patch Changes
@@ -74,9 +74,38 @@ var allowlistElements = function allowlistElements(element, callback) {
74
74
  }
75
75
  return true;
76
76
  };
77
- var InternalModalWrapper = /*#__PURE__*/(0, _react.forwardRef)(function (props, ref) {
77
+
78
+ // Analytics-wrapped close handler. Extracted so both rendering paths build it
79
+ // identically.
80
+ function useModalCloseHandler(providedOnClose) {
81
+ return (0, _analyticsNext.usePlatformLeafEventHandler)({
82
+ fn: providedOnClose || _noop.default,
83
+ action: 'closed',
84
+ componentName: 'modalDialog',
85
+ packageName: "@atlaskit/modal-dialog",
86
+ packageVersion: "16.2.2"
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Top-layer rendering path (platform-dst-top-layer).
92
+ *
93
+ * Replaces Portal, FocusLock, ScrollLock, Blanket, Positioner, and z-index
94
+ * management with native <dialog> via @atlaskit/top-layer/dialog.
95
+ *
96
+ * Key decisions:
97
+ * - Animation: CSS transitions via @starting-style / allow-discrete.
98
+ * - Close gating: onDialogClose only forwards allowed reasons
99
+ * (see notes/guides/dialog-close-flow.md).
100
+ * - onClose event param: undefined - consumers should use close reason.
101
+ * - Focus restoration: native <dialog> behavior replaces react-focus-lock's
102
+ * returnFocus (see accessibility-criteria.md).
103
+ *
104
+ * Every hook here runs unconditionally: this component only mounts on the
105
+ * top-layer path, so it never shares a hook sequence with the legacy path.
106
+ */
107
+ function ModalWrapperTopLayer(props) {
78
108
  var autoFocus = props.autoFocus,
79
- focusLockAllowlist = props.focusLockAllowlist,
80
109
  _props$shouldCloseOnE = props.shouldCloseOnEscapePress,
81
110
  shouldCloseOnEscapePress = _props$shouldCloseOnE === void 0 ? true : _props$shouldCloseOnE,
82
111
  _props$shouldCloseOnO = props.shouldCloseOnOverlayClick,
@@ -97,253 +126,244 @@ var InternalModalWrapper = /*#__PURE__*/(0, _react.forwardRef)(function (props,
97
126
  onOpenComplete = props.onOpenComplete,
98
127
  label = props.label,
99
128
  testId = props.testId,
100
- isFullScreen = props.isFullScreen,
101
- _props$UNSAFE_shouldD = props.UNSAFE_shouldDisableMotionUplift,
102
- UNSAFE_shouldDisableMotionUplift = _props$UNSAFE_shouldD === void 0 ? false : _props$UNSAFE_shouldD;
129
+ isFullScreen = props.isFullScreen;
103
130
  var calculatedStackIndex = (0, _useModalStack.default)({
104
131
  onStackChange: onStackChange
105
132
  });
106
133
  var stackIndex = stackIndexOverride || calculatedStackIndex;
107
- var isForeground = stackIndex === 0;
108
-
109
- // If no ref is provided, autofocus on first element
110
- var autoFocusLock = !((0, _typeof2.default)(autoFocus) === 'object');
111
- var onCloseHandler = (0, _analyticsNext.usePlatformLeafEventHandler)({
112
- fn: providedOnClose || _noop.default,
113
- action: 'closed',
114
- componentName: 'modalDialog',
115
- packageName: "@atlaskit/modal-dialog",
116
- packageVersion: "16.2.1"
117
- });
118
- var onBlanketClicked = (0, _react.useCallback)(function (e) {
119
- if (shouldCloseOnOverlayClick) {
120
- onCloseHandler(e);
121
- }
122
- }, [shouldCloseOnOverlayClick, onCloseHandler]);
123
-
124
- // Stable callback to avoid re-renders when focusLockAllowlist is not provided.
125
- var allowListCallback = (0, _react.useCallback)(function (element) {
126
- return allowlistElements(element, focusLockAllowlist);
127
- }, [focusLockAllowlist]);
128
-
129
- // Called outside the feature-flag branch to keep hook order stable.
130
- // Legacy path: FadeIn calls onFinish. Top-layer path: called directly.
134
+ var onCloseHandler = useModalCloseHandler(providedOnClose);
131
135
  var _useExitingPersistenc = (0, _exitingPersistence.useExitingPersistence)(),
132
136
  isExiting = _useExitingPersistenc.isExiting,
133
137
  onExitFinish = _useExitingPersistenc.onFinish;
134
138
 
135
- // Prevent background scroll (top-layer path uses DialogScrollLock instead).
136
- // Safe conditional hook: feature flags are resolved once at startup.
137
- if (!(0, _platformFeatureFlags.fg)('platform-dst-top-layer')) {
138
- // eslint-disable-next-line react-hooks/rules-of-hooks
139
- (0, _usePreventProgrammaticScroll.default)();
140
- }
141
-
142
- // On the top-layer path, the Dialog primitive registers with the observer
143
- // directly, so we skip registration here to avoid double-counting.
144
- // Safe conditional hook: feature flags are resolved once at startup.
145
- if (!(0, _platformFeatureFlags.fg)('platform-dst-top-layer')) {
146
- // eslint-disable-next-line react-hooks/rules-of-hooks
147
- (0, _useNotifyOpenLayerObserver.useNotifyOpenLayerObserver)({
148
- type: 'modal',
149
- // Always open — modal is conditionally rendered when visible.
150
- isOpen: true,
151
- // No-op: no current use case for programmatic close via OpenLayerObserver.
152
- onClose: _noop.default
153
- });
154
- }
139
+ // Native <dialog> always restores focus on close - no opt-out via shouldReturnFocus.
140
+ var defaultTestId = testId || 'modal-dialog';
141
+ var id = (0, _useId.useId)();
142
+ var titleId = "modal-dialog-title-".concat(id);
155
143
 
156
- /**
157
- * Top-layer path (platform-dst-top-layer).
158
- *
159
- * Replaces Portal, FocusLock, ScrollLock, Blanket, Positioner, and z-index
160
- * management with native <dialog> via @atlaskit/top-layer/dialog.
161
- *
162
- * Key decisions:
163
- * - Animation: CSS transitions via @starting-style / allow-discrete.
164
- * - Close gating: onDialogClose only forwards allowed reasons
165
- * (see notes/guides/dialog-close-flow.md).
166
- * - onClose event param: undefined - consumers should use close reason.
167
- * - Focus restoration: native <dialog> behavior replaces react-focus-lock's
168
- * returnFocus (see accessibility-criteria.md).
169
- */
170
- if ((0, _platformFeatureFlags.fg)('platform-dst-top-layer')) {
171
- // Native <dialog> always restores focus on close - no opt-out via shouldReturnFocus.
172
- var defaultTestId = testId || 'modal-dialog';
144
+ // Content container ref - used for onOpenComplete/onCloseComplete callbacks.
145
+ var contentRef = (0, _react.useRef)(null);
173
146
 
174
- // eslint-disable-next-line react-hooks/rules-of-hooks
175
- var id = (0, _useId.useId)();
176
- var titleId = "modal-dialog-title-".concat(id);
147
+ // Cache last content element for onCloseComplete after children unmount
148
+ // (with reduced motion, contentRef clears before onExitFinish fires).
149
+ var lastContentElRef = (0, _react.useRef)(null);
150
+ if (contentRef.current) {
151
+ lastContentElRef.current = contentRef.current;
152
+ }
177
153
 
178
- // Content container ref - used for onOpenComplete/onCloseComplete callbacks.
179
- // eslint-disable-next-line react-hooks/rules-of-hooks
180
- var contentRef = (0, _react.useRef)(null);
154
+ // Native <dialog> ref - needed for ExitingPersistence to call dialog.close().
155
+ var dialogRef = (0, _react.useRef)(null);
156
+ var modalDialogContext = (0, _react.useMemo)(function () {
157
+ return {
158
+ testId: defaultTestId,
159
+ titleId: titleId,
160
+ onClose: onCloseHandler,
161
+ hasProvidedOnClose: Boolean(providedOnClose),
162
+ isFullScreen: isFullScreen !== null && isFullScreen !== void 0 ? isFullScreen : false
163
+ };
164
+ }, [defaultTestId, titleId, onCloseHandler, providedOnClose, isFullScreen]);
181
165
 
182
- // Cache last content element for onCloseComplete after children unmount
183
- // (with reduced motion, contentRef clears before onExitFinish fires).
184
- // eslint-disable-next-line react-hooks/rules-of-hooks
185
- var lastContentElRef = (0, _react.useRef)(null);
186
- if (contentRef.current) {
187
- lastContentElRef.current = contentRef.current;
166
+ // Only forward close when the reason is allowed by props.
167
+ // Passes a synthetic event to satisfy the KeyboardOrMouseEvent contract.
168
+ var onDialogClose = (0, _react.useCallback)(function (_ref) {
169
+ var reason = _ref.reason;
170
+ if (reason === 'escape' && shouldCloseOnEscapePress) {
171
+ onCloseHandler((0, _dialog.createCloseEvent)({
172
+ reason: reason
173
+ }));
174
+ }
175
+ if (reason === 'overlay-click' && shouldCloseOnOverlayClick) {
176
+ onCloseHandler((0, _dialog.createCloseEvent)({
177
+ reason: reason
178
+ }));
188
179
  }
180
+ }, [onCloseHandler, shouldCloseOnEscapePress, shouldCloseOnOverlayClick]);
189
181
 
190
- // Native <dialog> ref - needed for ExitingPersistence to call dialog.close().
191
- // eslint-disable-next-line react-hooks/rules-of-hooks
192
- var dialogRef = (0, _react.useRef)(null);
182
+ // ExitingPersistence: isExiting → isOpen={false} → Dialog exit animation →
183
+ // onExitFinish → onCloseComplete + unmount.
184
+ var handleDialogExitFinish = (0, _react.useCallback)(function () {
185
+ var _contentRef$current;
186
+ var el = (_contentRef$current = contentRef.current) !== null && _contentRef$current !== void 0 ? _contentRef$current : lastContentElRef.current;
187
+ if (onCloseComplete && el) {
188
+ onCloseComplete(el);
189
+ }
190
+ lastContentElRef.current = null;
191
+ onExitFinish === null || onExitFinish === void 0 || onExitFinish();
192
+ }, [onExitFinish, onCloseComplete]);
193
193
 
194
- // eslint-disable-next-line react-hooks/rules-of-hooks
195
- var modalDialogContext = (0, _react.useMemo)(function () {
196
- return {
197
- testId: defaultTestId,
198
- titleId: titleId,
199
- onClose: onCloseHandler,
200
- hasProvidedOnClose: Boolean(providedOnClose),
201
- isFullScreen: isFullScreen !== null && isFullScreen !== void 0 ? isFullScreen : false
202
- };
203
- }, [defaultTestId, titleId, onCloseHandler, providedOnClose, isFullScreen]);
194
+ // Fire onOpenComplete after mount.
195
+ (0, _react.useEffect)(function () {
196
+ if (onOpenComplete && contentRef.current) {
197
+ onOpenComplete(contentRef.current, true);
198
+ }
199
+ // eslint-disable-next-line react-hooks/exhaustive-deps
200
+ }, []);
204
201
 
205
- // Only forward close when the reason is allowed by props.
206
- // Passes a synthetic event to satisfy the KeyboardOrMouseEvent contract.
207
- // eslint-disable-next-line react-hooks/rules-of-hooks
208
- var onDialogClose = (0, _react.useCallback)(function (_ref) {
209
- var reason = _ref.reason;
210
- if (reason === 'escape' && shouldCloseOnEscapePress) {
211
- onCloseHandler((0, _dialog.createCloseEvent)({
212
- reason: reason
213
- }));
214
- }
215
- if (reason === 'overlay-click' && shouldCloseOnOverlayClick) {
216
- onCloseHandler((0, _dialog.createCloseEvent)({
217
- reason: reason
218
- }));
202
+ // Honor `shouldReturnFocus={ref}` on unmount.
203
+ // Native <dialog>.close() restores focus to the trigger that opened it,
204
+ // but the consumer asked for focus to go to a specific element instead.
205
+ // Run this in an unmount cleanup so it fires after dialog.close()
206
+ // (which fires in the Dialog's effect cleanup).
207
+ var shouldReturnFocusRef = (0, _react.useRef)(shouldReturnFocus);
208
+ shouldReturnFocusRef.current = shouldReturnFocus;
209
+ (0, _react.useEffect)(function () {
210
+ return function () {
211
+ var target = shouldReturnFocusRef.current;
212
+ if ((0, _typeof2.default)(target) === 'object' && target.current) {
213
+ target.current.focus();
219
214
  }
220
- }, [onCloseHandler, shouldCloseOnEscapePress, shouldCloseOnOverlayClick]);
215
+ };
216
+ }, []);
221
217
 
222
- // ExitingPersistence: isExiting → isOpen={false} → Dialog exit animation →
223
- // onExitFinish → onCloseComplete + unmount.
224
- // eslint-disable-next-line react-hooks/rules-of-hooks
225
- var handleDialogExitFinish = (0, _react.useCallback)(function () {
226
- var _contentRef$current;
227
- var el = (_contentRef$current = contentRef.current) !== null && _contentRef$current !== void 0 ? _contentRef$current : lastContentElRef.current;
228
- if (onCloseComplete && el) {
229
- onCloseComplete(el);
230
- }
231
- lastContentElRef.current = null;
232
- onExitFinish === null || onExitFinish === void 0 || onExitFinish();
233
- }, [onExitFinish, onCloseComplete]);
218
+ // Focus a ref-targeted element after mount (when autoFocus is a ref).
219
+ // When true, native <dialog>.showModal() handles focus automatically.
220
+ (0, _useAutoFocus.default)((0, _typeof2.default)(autoFocus) === 'object' ? autoFocus : undefined, (0, _typeof2.default)(autoFocus) === 'object');
234
221
 
235
- // Fire onOpenComplete after mount.
236
- // eslint-disable-next-line react-hooks/rules-of-hooks
237
- (0, _react.useEffect)(function () {
238
- if (onOpenComplete && contentRef.current) {
239
- onOpenComplete(contentRef.current, true);
240
- }
241
- // eslint-disable-next-line react-hooks/exhaustive-deps
242
- }, []);
222
+ // Chrome cross-origin iframe DnD workaround (crbug.com/362301053)
223
+ (0, _react.useEffect)(function () {
224
+ return (0, _combine.combine)((0, _element.disableDraggingToCrossOriginIFramesForElement)(), (0, _textSelection.disableDraggingToCrossOriginIFramesForTextSelection)(), (0, _external.disableDraggingToCrossOriginIFramesForExternal)());
225
+ }, []);
243
226
 
244
- // Honor `shouldReturnFocus={ref}` on unmount.
245
- // Native <dialog>.close() restores focus to the trigger that opened it,
246
- // but the consumer asked for focus to go to a specific element instead.
247
- // Run this in an unmount cleanup so it fires after dialog.close()
248
- // (which fires in the Dialog's effect cleanup).
249
- // eslint-disable-next-line react-hooks/rules-of-hooks
250
- var shouldReturnFocusRef = (0, _react.useRef)(shouldReturnFocus);
251
- shouldReturnFocusRef.current = shouldReturnFocus;
252
- // eslint-disable-next-line react-hooks/rules-of-hooks
253
- (0, _react.useEffect)(function () {
254
- return function () {
255
- var target = shouldReturnFocusRef.current;
256
- if ((0, _typeof2.default)(target) === 'object' && target.current) {
257
- target.current.focus();
258
- }
259
- };
260
- }, []);
227
+ // Responsive layout via ID-scoped <style> (same pattern as Dialog's hideBackdrop).
228
+ // ID selector beats Compiled atomic classes without !important and supports @media.
229
+ var namedWidth = (0, _dialogWidth.dialogWidth)(width !== null && width !== void 0 ? width : 'medium');
230
+ var dialogId = "modal-dialog-".concat(id);
231
+ var escapedDialogId = CSS.escape(dialogId);
261
232
 
262
- // Focus a ref-targeted element after mount (when autoFocus is a ref).
263
- // When true, native <dialog>.showModal() handles focus automatically.
264
- // eslint-disable-next-line react-hooks/rules-of-hooks
265
- (0, _useAutoFocus.default)((0, _typeof2.default)(autoFocus) === 'object' ? autoFocus : undefined, (0, _typeof2.default)(autoFocus) === 'object');
233
+ // Percentage widths need special handling in the top layer.
234
+ // In legacy, the percentage resolved against the Positioner's max-width
235
+ // (100vw - 120px). In the top layer, the <dialog>'s containing block is the
236
+ // viewport (100vw), so a raw percentage would produce a wider modal.
237
+ // Transform e.g. '42%' → 'calc(42 * (100vw - 120px) / 100)' to match legacy.
238
+ var resolvedWidth = namedWidth.endsWith('%') ? "calc(".concat(parseFloat(namedWidth), " * (100vw - 120px) / 100)") : namedWidth;
239
+ var dialogStyle = isFullScreen ? {
240
+ width: '100vw',
241
+ height: '100vh',
242
+ margin: '0'
243
+ } : {
244
+ width: "min(".concat(resolvedWidth, ", 100vw)")
245
+ };
266
246
 
267
- // Chrome cross-origin iframe DnD workaround (crbug.com/362301053)
268
- // eslint-disable-next-line react-hooks/rules-of-hooks
269
- (0, _react.useEffect)(function () {
270
- return (0, _combine.combine)((0, _element.disableDraggingToCrossOriginIFramesForElement)(), (0, _textSelection.disableDraggingToCrossOriginIFramesForTextSelection)(), (0, _external.disableDraggingToCrossOriginIFramesForExternal)());
271
- }, []);
247
+ // Shift stacked background modals down by space.100 (8px) per level.
248
+ if (stackIndex > 0) {
249
+ dialogStyle['transform'] = "translateY(calc(".concat(stackIndex, "px * ", "var(--ds-space-100, 8px)", "))");
250
+ }
272
251
 
273
- // Responsive layout via ID-scoped <style> (same pattern as Dialog's hideBackdrop).
274
- // ID selector beats Compiled atomic classes without !important and supports @media.
275
- var namedWidth = (0, _dialogWidth.dialogWidth)(width !== null && width !== void 0 ? width : 'medium');
276
- var dialogId = "modal-dialog-".concat(id);
277
- var escapedDialogId = CSS.escape(dialogId);
252
+ // Mobile: viewport fill. Desktop (≥ 30rem): gutter margins, auto height.
253
+ // Content-div height set via #id > div to beat Compiled's atomic specificity.
254
+ var desktopMargin = shouldScrollInViewport ? '60px auto' : '60px auto auto';
255
+ var resolvedHeight = (0, _dialogHeight.dialogHeight)(height);
256
+ // Body-scroll: specified height or auto. Viewport-scroll: uses min-height.
257
+ var desktopContentHeight = shouldScrollInViewport ? 'auto' : resolvedHeight;
258
+ var desktopContentMinHeight = shouldScrollInViewport ? resolvedHeight : 'auto';
259
+ // Viewport-scroll: the legacy Positioner was a fixed 100vh container that
260
+ // scrolled internally, so the modal section could fill (100vh - 60px top gutter).
261
+ // In the top layer the <dialog> sizes to content with height:auto, so we need
262
+ // an explicit min-height to ensure the dialog stretches to the same visible area.
263
+ var desktopDialogMinHeight = shouldScrollInViewport ? 'min-height:calc(100vh - 60px);' : '';
264
+ // Doubled-ID selector (#id#id > div) at specificity (2,0,1) beats
265
+ // Compiled atomic classes at (0,1,0) (increaseSpecificity is disabled).
266
+ var dialogPositionStyles = isFullScreen ? '' : // Mobile: edge-to-edge. Desktop (≥ 30rem): 60px gutters, max-width.
267
+ "#".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, "}}");
268
+ return /*#__PURE__*/React.createElement(_dialog.Dialog, (0, _extends2.default)({
269
+ ref: dialogRef,
270
+ id: dialogId,
271
+ onClose: onDialogClose,
272
+ onExitFinish: handleDialogExitFinish,
273
+ shouldAnimate: !isFullScreen,
274
+ isOpen: !isExiting,
275
+ shouldHideBackdrop: stackIndex > 0 || Boolean(isBlanketHidden)
276
+ // Dialog requires at least one of `label` or `labelledBy` (string, not undefined).
277
+ // Prefer the consumer-provided `label`; otherwise reference the internal `titleId`.
278
+ }, label ? {
279
+ label: label
280
+ } : {
281
+ labelledBy: titleId
282
+ }, {
283
+ testId: defaultTestId
284
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop
285
+ ,
286
+ style: dialogStyle
287
+ }), /*#__PURE__*/React.createElement(_dialogScrollLock.DialogScrollLock, {
288
+ isOpen: true
289
+ }), dialogPositionStyles &&
290
+ /*#__PURE__*/
291
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-global-styles
292
+ React.createElement("style", null, dialogPositionStyles), /*#__PURE__*/React.createElement("div", {
293
+ ref: contentRef,
294
+ 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"])
295
+ }, /*#__PURE__*/React.createElement(_context.ModalContext.Provider, {
296
+ value: modalDialogContext
297
+ }, /*#__PURE__*/React.createElement(_scrollContext.ScrollContext.Provider, {
298
+ value: shouldScrollInViewport
299
+ }, children))));
300
+ }
278
301
 
279
- // Percentage widths need special handling in the top layer.
280
- // In legacy, the percentage resolved against the Positioner's max-width
281
- // (100vw - 120px). In the top layer, the <dialog>'s containing block is the
282
- // viewport (100vw), so a raw percentage would produce a wider modal.
283
- // Transform e.g. '42%' → 'calc(42 * (100vw - 120px) / 100)' to match legacy.
284
- var resolvedWidth = namedWidth.endsWith('%') ? "calc(".concat(parseFloat(namedWidth), " * (100vw - 120px) / 100)") : namedWidth;
285
- var dialogStyle = isFullScreen ? {
286
- width: '100vw',
287
- height: '100vh',
288
- margin: '0'
289
- } : {
290
- width: "min(".concat(resolvedWidth, ", 100vw)")
291
- };
302
+ /**
303
+ * Legacy rendering path (Portal + FocusLock + ScrollLock + Blanket).
304
+ *
305
+ * Every hook here runs unconditionally: this component only mounts on the
306
+ * legacy path, so it never shares a hook sequence with the top-layer path.
307
+ */
308
+ var ModalWrapperLegacy = /*#__PURE__*/(0, _react.forwardRef)(function (props, ref) {
309
+ var autoFocus = props.autoFocus,
310
+ focusLockAllowlist = props.focusLockAllowlist,
311
+ _props$shouldCloseOnE2 = props.shouldCloseOnEscapePress,
312
+ shouldCloseOnEscapePress = _props$shouldCloseOnE2 === void 0 ? true : _props$shouldCloseOnE2,
313
+ _props$shouldCloseOnO2 = props.shouldCloseOnOverlayClick,
314
+ shouldCloseOnOverlayClick = _props$shouldCloseOnO2 === void 0 ? true : _props$shouldCloseOnO2,
315
+ _props$shouldScrollIn2 = props.shouldScrollInViewport,
316
+ shouldScrollInViewport = _props$shouldScrollIn2 === void 0 ? false : _props$shouldScrollIn2,
317
+ _props$shouldReturnFo2 = props.shouldReturnFocus,
318
+ shouldReturnFocus = _props$shouldReturnFo2 === void 0 ? true : _props$shouldReturnFo2,
319
+ stackIndexOverride = props.stackIndex,
320
+ providedOnClose = props.onClose,
321
+ _props$onStackChange2 = props.onStackChange,
322
+ onStackChange = _props$onStackChange2 === void 0 ? _noop.default : _props$onStackChange2,
323
+ isBlanketHidden = props.isBlanketHidden,
324
+ children = props.children,
325
+ height = props.height,
326
+ width = props.width,
327
+ onCloseComplete = props.onCloseComplete,
328
+ onOpenComplete = props.onOpenComplete,
329
+ label = props.label,
330
+ testId = props.testId,
331
+ isFullScreen = props.isFullScreen,
332
+ _props$UNSAFE_shouldD = props.UNSAFE_shouldDisableMotionUplift,
333
+ UNSAFE_shouldDisableMotionUplift = _props$UNSAFE_shouldD === void 0 ? false : _props$UNSAFE_shouldD;
334
+ var calculatedStackIndex = (0, _useModalStack.default)({
335
+ onStackChange: onStackChange
336
+ });
337
+ var stackIndex = stackIndexOverride || calculatedStackIndex;
338
+ var isForeground = stackIndex === 0;
292
339
 
293
- // Shift stacked background modals down by space.100 (8px) per level.
294
- if (stackIndex > 0) {
295
- dialogStyle['transform'] = "translateY(calc(".concat(stackIndex, "px * ", "var(--ds-space-100, 8px)", "))");
340
+ // If no ref is provided, autofocus on first element
341
+ var autoFocusLock = !((0, _typeof2.default)(autoFocus) === 'object');
342
+ var onCloseHandler = useModalCloseHandler(providedOnClose);
343
+ var onBlanketClicked = (0, _react.useCallback)(function (e) {
344
+ if (shouldCloseOnOverlayClick) {
345
+ onCloseHandler(e);
296
346
  }
347
+ }, [shouldCloseOnOverlayClick, onCloseHandler]);
297
348
 
298
- // Mobile: viewport fill. Desktop (≥ 30rem): gutter margins, auto height.
299
- // Content-div height set via #id > div to beat Compiled's atomic specificity.
300
- var desktopMargin = shouldScrollInViewport ? '60px auto' : '60px auto auto';
301
- var resolvedHeight = (0, _dialogHeight.dialogHeight)(height);
302
- // Body-scroll: specified height or auto. Viewport-scroll: uses min-height.
303
- var desktopContentHeight = shouldScrollInViewport ? 'auto' : resolvedHeight;
304
- var desktopContentMinHeight = shouldScrollInViewport ? resolvedHeight : 'auto';
305
- // Viewport-scroll: the legacy Positioner was a fixed 100vh container that
306
- // scrolled internally, so the modal section could fill (100vh - 60px top gutter).
307
- // In the top layer the <dialog> sizes to content with height:auto, so we need
308
- // an explicit min-height to ensure the dialog stretches to the same visible area.
309
- var desktopDialogMinHeight = shouldScrollInViewport ? 'min-height:calc(100vh - 60px);' : '';
310
- // Doubled-ID selector (#id#id > div) at specificity (2,0,1) beats
311
- // Compiled atomic classes at (0,1,0) (increaseSpecificity is disabled).
312
- var dialogPositionStyles = isFullScreen ? '' : // Mobile: edge-to-edge. Desktop (≥ 30rem): 60px gutters, max-width.
313
- "#".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, "}}");
314
- return /*#__PURE__*/React.createElement(_dialog.Dialog, (0, _extends2.default)({
315
- ref: dialogRef,
316
- id: dialogId,
317
- onClose: onDialogClose,
318
- onExitFinish: handleDialogExitFinish,
319
- shouldAnimate: !isFullScreen,
320
- isOpen: !isExiting,
321
- shouldHideBackdrop: stackIndex > 0 || Boolean(isBlanketHidden)
322
- // Dialog requires at least one of `label` or `labelledBy` (string, not undefined).
323
- // Prefer the consumer-provided `label`; otherwise reference the internal `titleId`.
324
- }, label ? {
325
- label: label
326
- } : {
327
- labelledBy: titleId
328
- }, {
329
- testId: defaultTestId
330
- // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop
331
- ,
332
- style: dialogStyle
333
- }), /*#__PURE__*/React.createElement(_dialogScrollLock.DialogScrollLock, {
334
- isOpen: true
335
- }), dialogPositionStyles &&
336
- /*#__PURE__*/
337
- // eslint-disable-next-line @atlaskit/ui-styling-standard/no-global-styles
338
- React.createElement("style", null, dialogPositionStyles), /*#__PURE__*/React.createElement("div", {
339
- ref: contentRef,
340
- 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"])
341
- }, /*#__PURE__*/React.createElement(_context.ModalContext.Provider, {
342
- value: modalDialogContext
343
- }, /*#__PURE__*/React.createElement(_scrollContext.ScrollContext.Provider, {
344
- value: shouldScrollInViewport
345
- }, children))));
346
- }
349
+ // Stable callback to avoid re-renders when focusLockAllowlist is not provided.
350
+ var allowListCallback = (0, _react.useCallback)(function (element) {
351
+ return allowlistElements(element, focusLockAllowlist);
352
+ }, [focusLockAllowlist]);
353
+
354
+ // Prevent background scroll (top-layer path uses DialogScrollLock instead).
355
+ (0, _usePreventProgrammaticScroll.default)();
356
+
357
+ // Register with the open layer observer. On the top-layer path the Dialog
358
+ // primitive does this itself, which is why it lives in this legacy-only
359
+ // component rather than the shared dispatcher.
360
+ (0, _useNotifyOpenLayerObserver.useNotifyOpenLayerObserver)({
361
+ type: 'modal',
362
+ // Always open — modal is conditionally rendered when visible.
363
+ isOpen: true,
364
+ // No-op: no current use case for programmatic close via OpenLayerObserver.
365
+ onClose: _noop.default
366
+ });
347
367
  var modalDialogWithBlanket = /*#__PURE__*/React.createElement(_blanket.default, {
348
368
  isTinted: !isBlanketHidden,
349
369
  onBlanketClicked: onBlanketClicked,
@@ -406,5 +426,24 @@ var InternalModalWrapper = /*#__PURE__*/(0, _react.forwardRef)(function (props,
406
426
  })));
407
427
  });
408
428
 
429
+ // Choose the rendering implementation at the component boundary rather than
430
+ // gating hooks inside a single component. Each implementation owns its own
431
+ // hooks unconditionally, so a runtime feature-flag change swaps component types
432
+ // (a clean remount) instead of changing the hook order of a mounted component.
433
+ var InternalModalWrapper = /*#__PURE__*/(0, _react.forwardRef)(function (props, ref) {
434
+ if ((0, _platformFeatureFlags.fg)('platform-dst-top-layer')) {
435
+ // TODO: the top-layer path does not forward the external `ref` (parity
436
+ // with the pre-refactor behavior, where the top-layer branch never
437
+ // consumed it). The public type still advertises `RefAttributes`, so a
438
+ // consumer ref silently no-ops here. Follow up by forwarding `ref` to a
439
+ // sensible element (for example the content `div`) once ref parity is
440
+ // intentionally desired.
441
+ return /*#__PURE__*/React.createElement(ModalWrapperTopLayer, props);
442
+ }
443
+ return /*#__PURE__*/React.createElement(ModalWrapperLegacy, (0, _extends2.default)({
444
+ ref: ref
445
+ }, props));
446
+ });
447
+
409
448
  // eslint-disable-next-line @repo/internal/react/require-jsdoc
410
449
  var _default = exports.default = InternalModalWrapper;