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