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