@tamagui/dialog 2.7.7 → 3.0.0-beta.643.1
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/dist/cjs/Dialog.cjs +582 -638
- package/dist/cjs/Dialog.native.cjs +684 -0
- package/dist/cjs/Dialog.native.js +616 -684
- package/dist/cjs/Dialog.native.js.map +1 -1
- package/dist/cjs/index.cjs +10 -11
- package/dist/cjs/index.native.cjs +21 -0
- package/dist/cjs/index.native.js +10 -10
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/Dialog.mjs +557 -605
- package/dist/esm/Dialog.mjs.map +1 -1
- package/dist/esm/Dialog.native.js +591 -652
- package/dist/esm/Dialog.native.js.map +1 -1
- package/dist/esm/index.js +1 -2
- package/dist/esm/index.mjs +1 -2
- package/dist/esm/index.native.js +1 -2
- package/dist/jsx/Dialog.mjs +557 -605
- package/dist/jsx/Dialog.mjs.map +1 -1
- package/dist/jsx/Dialog.native.cjs +684 -0
- package/dist/jsx/Dialog.native.js +616 -684
- package/dist/jsx/Dialog.native.js.map +1 -1
- package/dist/jsx/index.js +1 -2
- package/dist/jsx/index.mjs +1 -2
- package/dist/jsx/index.native.cjs +21 -0
- package/dist/jsx/index.native.js +10 -10
- package/dist/jsx/index.native.js.map +1 -1
- package/package.json +21 -21
- package/src/Dialog.tsx +369 -326
- package/types/Dialog.d.ts +134 -171
- package/types/Dialog.d.ts.map +1 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/index.mjs.map +0 -1
- package/dist/esm/index.native.js.map +0 -1
- package/dist/jsx/index.js.map +0 -1
- package/dist/jsx/index.mjs.map +0 -1
package/src/Dialog.tsx
CHANGED
|
@@ -9,9 +9,11 @@ import {
|
|
|
9
9
|
import { Animate } from '@tamagui/animate'
|
|
10
10
|
import { composeRefs, useComposedRefs } from '@tamagui/compose-refs'
|
|
11
11
|
import { isWeb, useIsomorphicLayoutEffect } from '@tamagui/constants'
|
|
12
|
-
import type { GetProps, TamaguiElement, ViewProps } from '@tamagui/core'
|
|
12
|
+
import type { GetProps, OnTransition, TamaguiElement, ViewProps } from '@tamagui/core'
|
|
13
13
|
import {
|
|
14
|
+
createStyledHOC,
|
|
14
15
|
createStyledContext,
|
|
16
|
+
createRefComponent,
|
|
15
17
|
getExpandedShorthand,
|
|
16
18
|
LayoutMeasurementController,
|
|
17
19
|
styled,
|
|
@@ -32,9 +34,8 @@ import {
|
|
|
32
34
|
resolveViewZIndex,
|
|
33
35
|
} from '@tamagui/portal'
|
|
34
36
|
import { RemoveScroll } from '@tamagui/remove-scroll'
|
|
35
|
-
import { SheetController } from '@tamagui/sheet/controller'
|
|
36
37
|
import type { YStackProps } from '@tamagui/stacks'
|
|
37
|
-
import { ButtonNestingContext,
|
|
38
|
+
import { ButtonNestingContext, YStack } from '@tamagui/stacks'
|
|
38
39
|
import { H2, Paragraph } from '@tamagui/text'
|
|
39
40
|
import { useControllableState } from '@tamagui/use-controllable-state'
|
|
40
41
|
import { StackZIndexContext } from '@tamagui/z-index-stack'
|
|
@@ -69,21 +70,21 @@ type DialogProps = ScopedProps<{
|
|
|
69
70
|
onAnimationComplete?: (info: { open: boolean }) => void
|
|
70
71
|
}>
|
|
71
72
|
|
|
72
|
-
type NonNull<A> = Exclude<A, void | null>
|
|
73
|
-
|
|
74
73
|
type DialogContextValue = {
|
|
75
74
|
forceMount?: boolean
|
|
76
75
|
keepChildrenMounted?: boolean
|
|
77
76
|
disableRemoveScroll?: boolean
|
|
77
|
+
hasPresentParts: boolean
|
|
78
|
+
setPartPresence(id: string, present: boolean): void
|
|
78
79
|
triggerRef: React.RefObject<TamaguiElement | null>
|
|
79
80
|
contentRef: React.RefObject<TamaguiElement | null>
|
|
80
81
|
contentId: string
|
|
81
82
|
titleId: string
|
|
82
83
|
descriptionId: string
|
|
83
84
|
onOpenToggle(): void
|
|
84
|
-
open:
|
|
85
|
-
onOpenChange:
|
|
86
|
-
modal:
|
|
85
|
+
open: Exclude<DialogProps['open'], void | null>
|
|
86
|
+
onOpenChange: Exclude<DialogProps['onOpenChange'], void | null>
|
|
87
|
+
modal: Exclude<DialogProps['modal'], void | null>
|
|
87
88
|
dialogScope: DialogScopes
|
|
88
89
|
adaptScope: string
|
|
89
90
|
onAnimationComplete?: DialogProps['onAnimationComplete']
|
|
@@ -91,43 +92,38 @@ type DialogContextValue = {
|
|
|
91
92
|
|
|
92
93
|
export const DialogContext = createStyledContext<DialogContextValue>(
|
|
93
94
|
// since we always provide this we can avoid setting here
|
|
94
|
-
{}
|
|
95
|
+
{},
|
|
95
96
|
'Dialog__'
|
|
96
97
|
)
|
|
97
98
|
|
|
98
99
|
export const { useStyledContext: useDialogContext, Provider: DialogProvider } =
|
|
99
100
|
DialogContext
|
|
100
101
|
|
|
101
|
-
/**
|
|
102
|
-
* Tracks whether the Sheet adapted from a Dialog has finished its slide-out
|
|
103
|
-
* animation. DialogSheetController owns the state; DialogContent reads it so
|
|
104
|
-
* it can hold adapted children mounted until the sheet is fully off-screen,
|
|
105
|
-
* instead of tearing them down the moment Dialog.open flips false.
|
|
106
|
-
*
|
|
107
|
-
* Default true so the value is "safe to unmount" if no provider is mounted
|
|
108
|
-
* (matches the historical behavior where adapted children unmount immediately
|
|
109
|
-
* on close in native).
|
|
110
|
-
*/
|
|
111
|
-
const DialogAdaptHiddenContext = React.createContext(true)
|
|
112
|
-
|
|
113
102
|
/* -------------------------------------------------------------------------------------------------
|
|
114
103
|
* DialogTrigger
|
|
115
104
|
* -----------------------------------------------------------------------------------------------*/
|
|
116
105
|
|
|
117
106
|
const DialogTriggerFrame = styled(View, {
|
|
118
|
-
|
|
107
|
+
displayName: 'DialogTrigger',
|
|
119
108
|
})
|
|
120
109
|
|
|
121
110
|
type DialogTriggerProps = ScopedProps<ViewProps>
|
|
122
111
|
|
|
123
|
-
const DialogTrigger =
|
|
124
|
-
|
|
112
|
+
const DialogTrigger = createStyledHOC(
|
|
113
|
+
DialogTriggerFrame,
|
|
114
|
+
function DialogTrigger(props: DialogTriggerProps, forwardedRef) {
|
|
125
115
|
const { scope, ...triggerProps } = props
|
|
126
116
|
const isInsideButton = React.useContext(ButtonNestingContext)
|
|
127
117
|
const context = useDialogContext(scope)
|
|
128
118
|
const composedTriggerRef = useComposedRefs(forwardedRef, context.triggerRef)
|
|
119
|
+
// only mark descendants as button-nested when this trigger renders its OWN
|
|
120
|
+
// <button>. with asChild the child element BECOMES the trigger (it receives
|
|
121
|
+
// our composed onPress directly), so it is the control, not a decorative
|
|
122
|
+
// button nested inside one - forcing nesting here would strip its
|
|
123
|
+
// interactivity (Button's nested branch neutralizes role/tabIndex/press).
|
|
124
|
+
// preserve whatever nesting the trigger itself sits in. matches Popover.Trigger.
|
|
129
125
|
return (
|
|
130
|
-
<ButtonNestingContext.Provider value={true}>
|
|
126
|
+
<ButtonNestingContext.Provider value={props.asChild ? isInsideButton : true}>
|
|
131
127
|
<DialogTriggerFrame
|
|
132
128
|
render={isInsideButton ? 'span' : 'button'}
|
|
133
129
|
aria-haspopup="dialog"
|
|
@@ -159,37 +155,20 @@ type DialogPortalProps = ScopedProps<
|
|
|
159
155
|
|
|
160
156
|
export const DialogPortalFrame = styled(YStack, {
|
|
161
157
|
pointerEvents: 'none',
|
|
158
|
+
alignItems: 'center',
|
|
159
|
+
justifyContent: 'center',
|
|
160
|
+
position: 'absolute web:fixed',
|
|
161
|
+
inset: 0,
|
|
162
|
+
borderWidth: 'web:0px',
|
|
163
|
+
backgroundColor: 'web:transparent',
|
|
164
|
+
color: 'web:inherit',
|
|
165
|
+
maxInlineSize: 'web:none',
|
|
166
|
+
margin: 'web:0px',
|
|
167
|
+
width: 'web:auto',
|
|
168
|
+
height: 'web:auto',
|
|
169
|
+
maxHeight: 'web:100vh',
|
|
170
|
+
zIndex: 'web:1',
|
|
162
171
|
render: 'dialog',
|
|
163
|
-
|
|
164
|
-
variants: {
|
|
165
|
-
unstyled: {
|
|
166
|
-
false: {
|
|
167
|
-
alignItems: 'center',
|
|
168
|
-
justifyContent: 'center',
|
|
169
|
-
fullscreen: true,
|
|
170
|
-
|
|
171
|
-
'$platform-web': {
|
|
172
|
-
// undo dialog styles
|
|
173
|
-
borderWidth: 0,
|
|
174
|
-
backgroundColor: 'transparent',
|
|
175
|
-
color: 'inherit',
|
|
176
|
-
maxInlineSize: 'none',
|
|
177
|
-
margin: 0,
|
|
178
|
-
width: 'auto',
|
|
179
|
-
height: 'auto',
|
|
180
|
-
// ensure always in frame and right height
|
|
181
|
-
maxHeight: '100vh',
|
|
182
|
-
position: 'fixed',
|
|
183
|
-
// ensure dialog inherits stacking context from portal wrapper
|
|
184
|
-
zIndex: 1,
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
} as const,
|
|
189
|
-
|
|
190
|
-
defaultVariants: {
|
|
191
|
-
unstyled: process.env.TAMAGUI_HEADLESS === '1',
|
|
192
|
-
},
|
|
193
172
|
})
|
|
194
173
|
|
|
195
174
|
const needsRepropagation = needsPortalRepropagation()
|
|
@@ -218,34 +197,27 @@ const DialogPortalItem = ({
|
|
|
218
197
|
|
|
219
198
|
// until we can use react-native portals natively
|
|
220
199
|
// have to re-propogate context, sketch
|
|
221
|
-
// when adapted we
|
|
200
|
+
// when adapted we publish to the Adapt live slot, otherwise we portal to root modal if needed
|
|
222
201
|
return isAdapted ? (
|
|
223
202
|
<AdaptPortalContents scope={context.adaptScope}>{content}</AdaptPortalContents>
|
|
224
203
|
) : context.modal ? (
|
|
225
|
-
<PortalItem hostName={
|
|
226
|
-
{content}
|
|
227
|
-
</PortalItem>
|
|
204
|
+
<PortalItem hostName="root">{content}</PortalItem>
|
|
228
205
|
) : (
|
|
229
206
|
content
|
|
230
207
|
)
|
|
231
208
|
}
|
|
232
209
|
|
|
233
|
-
const DialogPortal =
|
|
234
|
-
(props,
|
|
210
|
+
const DialogPortal = createRefComponent<TamaguiElement, DialogPortalProps>(
|
|
211
|
+
(props, forwardedRef) => {
|
|
235
212
|
const { scope, forceMount, children, ...frameProps } = props
|
|
236
213
|
const dialogRef = React.useRef<TamaguiElement>(null)
|
|
237
|
-
const ref = composeRefs(dialogRef,
|
|
214
|
+
const ref = composeRefs(dialogRef, forwardedRef)
|
|
238
215
|
|
|
239
216
|
const context = useDialogContext(scope)
|
|
217
|
+
const portalContext = forceMount ? { ...context, forceMount: true } : context
|
|
240
218
|
const keepMounted = forceMount || context.keepChildrenMounted
|
|
241
219
|
const isAdapted = useAdaptIsActive(context.adaptScope)
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
if (context.open && isFullyHidden) {
|
|
245
|
-
setIsFullyHidden(false)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const isVisible = context.open ? true : !isFullyHidden
|
|
220
|
+
const isVisible = context.open || context.hasPresentParts
|
|
249
221
|
|
|
250
222
|
if (isWeb) {
|
|
251
223
|
useIsomorphicLayoutEffect(() => {
|
|
@@ -260,49 +232,27 @@ const DialogPortal = React.forwardRef<TamaguiElement, DialogPortalProps>(
|
|
|
260
232
|
}, [isVisible])
|
|
261
233
|
}
|
|
262
234
|
|
|
263
|
-
const onAnimationCompleteRef = React.useRef(context.onAnimationComplete)
|
|
264
|
-
onAnimationCompleteRef.current = context.onAnimationComplete
|
|
265
|
-
|
|
266
|
-
const handleExitComplete = React.useCallback(() => {
|
|
267
|
-
setIsFullyHidden(true)
|
|
268
|
-
onAnimationCompleteRef.current?.({ open: false })
|
|
269
|
-
}, [])
|
|
270
|
-
|
|
271
|
-
React.useEffect(() => {
|
|
272
|
-
if (context.open && !isAdapted && onAnimationCompleteRef.current) {
|
|
273
|
-
const tm = setTimeout(() => {
|
|
274
|
-
onAnimationCompleteRef.current?.({ open: true })
|
|
275
|
-
}, 350)
|
|
276
|
-
return () => clearTimeout(tm)
|
|
277
|
-
}
|
|
278
|
-
}, [context.open, isAdapted])
|
|
279
|
-
|
|
280
235
|
const zIndex = getExpandedShorthand('zIndex', props)
|
|
281
236
|
|
|
282
237
|
const contents = (
|
|
283
238
|
<StackZIndexContext zIndex={resolveViewZIndex(zIndex)}>
|
|
284
|
-
<
|
|
285
|
-
type="presence"
|
|
286
|
-
present={Boolean(context.open)}
|
|
287
|
-
keepChildrenMounted={Boolean(keepMounted)}
|
|
288
|
-
onExitComplete={handleExitComplete}
|
|
289
|
-
passThrough={isAdapted}
|
|
290
|
-
>
|
|
239
|
+
<DialogProvider scope={context.dialogScope} {...portalContext}>
|
|
291
240
|
{children}
|
|
292
|
-
</
|
|
241
|
+
</DialogProvider>
|
|
293
242
|
</StackZIndexContext>
|
|
294
243
|
)
|
|
295
244
|
|
|
296
245
|
const framedContents =
|
|
297
|
-
|
|
246
|
+
!isVisible && !keepMounted && !isAdapted ? null : (
|
|
298
247
|
<LayoutMeasurementController disable={!context.open}>
|
|
299
248
|
<DialogPortalFrame
|
|
300
249
|
ref={ref}
|
|
301
250
|
{...(isWeb &&
|
|
302
|
-
context.open &&
|
|
251
|
+
context.open &&
|
|
252
|
+
context.modal && {
|
|
303
253
|
'aria-modal': true,
|
|
304
254
|
})}
|
|
305
|
-
pointerEvents={context.open ? 'auto' : 'none'}
|
|
255
|
+
pointerEvents={context.open && context.modal ? 'auto' : 'none'}
|
|
306
256
|
{...frameProps}
|
|
307
257
|
className={`_no_backdrop ` + (frameProps.className || '')}
|
|
308
258
|
>
|
|
@@ -319,6 +269,9 @@ const DialogPortal = React.forwardRef<TamaguiElement, DialogPortalProps>(
|
|
|
319
269
|
// this makes sure its above typical stacking contexts
|
|
320
270
|
stackZIndex={100000}
|
|
321
271
|
passThrough={isAdapted}
|
|
272
|
+
// hide the host while closed (covers keepChildrenMounted content, see
|
|
273
|
+
// Portal.tsx); isVisible stays true through the exit animation
|
|
274
|
+
hidden={!isVisible}
|
|
322
275
|
>
|
|
323
276
|
<PassthroughTheme passThrough={isAdapted}>{framedContents}</PassthroughTheme>
|
|
324
277
|
</Portal>
|
|
@@ -328,7 +281,7 @@ const DialogPortal = React.forwardRef<TamaguiElement, DialogPortalProps>(
|
|
|
328
281
|
return isAdapted ? (
|
|
329
282
|
framedContents
|
|
330
283
|
) : (
|
|
331
|
-
<DialogPortalItem context={
|
|
284
|
+
<DialogPortalItem context={portalContext}>{framedContents}</DialogPortalItem>
|
|
332
285
|
)
|
|
333
286
|
}
|
|
334
287
|
)
|
|
@@ -349,18 +302,92 @@ const PassthroughTheme = ({
|
|
|
349
302
|
)
|
|
350
303
|
}
|
|
351
304
|
|
|
305
|
+
function useDialogAnimationReporter(context: DialogContextValue) {
|
|
306
|
+
const onAnimationCompleteRef = React.useRef(context.onAnimationComplete)
|
|
307
|
+
onAnimationCompleteRef.current = context.onAnimationComplete
|
|
308
|
+
|
|
309
|
+
const openRef = React.useRef(context.open)
|
|
310
|
+
const pendingTransitionRef = React.useRef<boolean | null>(context.open ? true : null)
|
|
311
|
+
|
|
312
|
+
if (openRef.current !== context.open) {
|
|
313
|
+
openRef.current = context.open
|
|
314
|
+
pendingTransitionRef.current = context.open
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const reportComplete = React.useCallback((open: boolean) => {
|
|
318
|
+
if (pendingTransitionRef.current !== open) return
|
|
319
|
+
if (openRef.current !== open) return
|
|
320
|
+
|
|
321
|
+
pendingTransitionRef.current = null
|
|
322
|
+
onAnimationCompleteRef.current?.({ open })
|
|
323
|
+
}, [])
|
|
324
|
+
|
|
325
|
+
return React.useMemo(
|
|
326
|
+
() => ({
|
|
327
|
+
onEnterComplete: () => reportComplete(true),
|
|
328
|
+
onExitComplete: () => reportComplete(false),
|
|
329
|
+
}),
|
|
330
|
+
[reportComplete]
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function useDialogPartPresence(
|
|
335
|
+
context: DialogContextValue,
|
|
336
|
+
options: {
|
|
337
|
+
disabled?: boolean
|
|
338
|
+
forceMount?: boolean
|
|
339
|
+
id: string
|
|
340
|
+
onExitComplete?: () => void
|
|
341
|
+
}
|
|
342
|
+
) {
|
|
343
|
+
const [isFullyHidden, setIsFullyHidden] = React.useState(!context.open)
|
|
344
|
+
const reactId = React.useId()
|
|
345
|
+
const partPresenceId = `${context.contentId}-${options.id}-${reactId}`
|
|
346
|
+
const keepMounted = options.forceMount || context.keepChildrenMounted
|
|
347
|
+
const isPresent = context.open || !isFullyHidden
|
|
348
|
+
|
|
349
|
+
useIsomorphicLayoutEffect(() => {
|
|
350
|
+
if (context.open && isFullyHidden) {
|
|
351
|
+
setIsFullyHidden(false)
|
|
352
|
+
}
|
|
353
|
+
}, [context.open, isFullyHidden])
|
|
354
|
+
|
|
355
|
+
useIsomorphicLayoutEffect(() => {
|
|
356
|
+
if (options.disabled) return
|
|
357
|
+
|
|
358
|
+
context.setPartPresence(partPresenceId, isPresent)
|
|
359
|
+
return () => {
|
|
360
|
+
context.setPartPresence(partPresenceId, false)
|
|
361
|
+
}
|
|
362
|
+
}, [context.setPartPresence, isPresent, options.disabled, partPresenceId])
|
|
363
|
+
|
|
364
|
+
const onExitComplete = React.useCallback(() => {
|
|
365
|
+
setIsFullyHidden(true)
|
|
366
|
+
options.onExitComplete?.()
|
|
367
|
+
}, [options.onExitComplete])
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
keepMounted,
|
|
371
|
+
onExitComplete,
|
|
372
|
+
shouldRender: Boolean(keepMounted || context.open || !isFullyHidden),
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
352
376
|
/* -------------------------------------------------------------------------------------------------
|
|
353
377
|
* DialogOverlay
|
|
354
378
|
* -----------------------------------------------------------------------------------------------*/
|
|
355
379
|
|
|
356
380
|
const OVERLAY_NAME = 'DialogOverlay'
|
|
357
381
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
382
|
+
// Unstyled overlay frame: positioning + pointer-event bookkeeping only. The
|
|
383
|
+
// dim/scrim background lives in the tamagui skin
|
|
384
|
+
// (code/ui/tamagui/src/components/Dialog.tsx).
|
|
361
385
|
export const DialogOverlayFrame = styled(YStack, {
|
|
362
|
-
|
|
386
|
+
displayName: OVERLAY_NAME,
|
|
363
387
|
zIndex: 1,
|
|
388
|
+
inset: 0,
|
|
389
|
+
position: 'absolute',
|
|
390
|
+
pointerEvents: 'auto',
|
|
364
391
|
|
|
365
392
|
variants: {
|
|
366
393
|
open: {
|
|
@@ -371,20 +398,7 @@ export const DialogOverlayFrame = styled(YStack, {
|
|
|
371
398
|
pointerEvents: 'none',
|
|
372
399
|
},
|
|
373
400
|
},
|
|
374
|
-
|
|
375
|
-
unstyled: {
|
|
376
|
-
false: {
|
|
377
|
-
fullscreen: true,
|
|
378
|
-
position: 'absolute',
|
|
379
|
-
backgroundColor: '$background',
|
|
380
|
-
pointerEvents: 'auto',
|
|
381
|
-
},
|
|
382
|
-
},
|
|
383
401
|
} as const,
|
|
384
|
-
|
|
385
|
-
defaultVariants: {
|
|
386
|
-
unstyled: process.env.TAMAGUI_HEADLESS === '1',
|
|
387
|
-
},
|
|
388
402
|
})
|
|
389
403
|
|
|
390
404
|
export type DialogOverlayExtraProps = ScopedProps<{
|
|
@@ -397,33 +411,46 @@ export type DialogOverlayExtraProps = ScopedProps<{
|
|
|
397
411
|
|
|
398
412
|
type DialogOverlayProps = YStackProps & DialogOverlayExtraProps
|
|
399
413
|
|
|
400
|
-
const DialogOverlay =
|
|
401
|
-
|
|
414
|
+
const DialogOverlay = createStyledHOC(
|
|
415
|
+
DialogOverlayFrame,
|
|
416
|
+
function DialogOverlay({ scope, ...props }: DialogOverlayProps, forwardedRef) {
|
|
402
417
|
const context = useDialogContext(scope)
|
|
403
418
|
const { forceMount = context.forceMount, ...overlayProps } = props
|
|
404
419
|
const isAdapted = useAdaptIsActive(context.adaptScope)
|
|
420
|
+
const presence = useDialogPartPresence(context, {
|
|
421
|
+
disabled: isAdapted,
|
|
422
|
+
forceMount,
|
|
423
|
+
id: 'overlay',
|
|
424
|
+
})
|
|
405
425
|
|
|
406
|
-
if (!forceMount) {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
426
|
+
if (!forceMount && isAdapted) {
|
|
427
|
+
return null
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!presence.shouldRender) {
|
|
431
|
+
return null
|
|
410
432
|
}
|
|
411
433
|
|
|
412
434
|
// Make sure `Content` is scrollable even when it doesn't live inside `RemoveScroll`
|
|
413
435
|
// ie. when `Overlay` and `Content` are siblings
|
|
414
436
|
return (
|
|
415
|
-
<
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
437
|
+
<Animate
|
|
438
|
+
type="presence"
|
|
439
|
+
present={Boolean(context.open)}
|
|
440
|
+
keepChildrenMounted={Boolean(presence.keepMounted)}
|
|
441
|
+
onExitComplete={presence.onExitComplete}
|
|
442
|
+
passThrough={isAdapted}
|
|
443
|
+
>
|
|
444
|
+
<DialogOverlayFrame
|
|
445
|
+
key={`${context.contentId}-overlay`}
|
|
446
|
+
data-state={getState(context.open)}
|
|
447
|
+
// Presence freezes the exiting clone with its open props. The exit
|
|
448
|
+
// clause releases the full-screen overlay during that frozen frame.
|
|
449
|
+
pointerEvents={context.open && context.modal ? 'auto exit:none' : 'none'}
|
|
450
|
+
{...overlayProps}
|
|
451
|
+
ref={forwardedRef}
|
|
452
|
+
/>
|
|
453
|
+
</Animate>
|
|
427
454
|
)
|
|
428
455
|
}
|
|
429
456
|
)
|
|
@@ -434,36 +461,34 @@ const DialogOverlay = DialogOverlayFrame.styleable<DialogOverlayExtraProps>(
|
|
|
434
461
|
|
|
435
462
|
const CONTENT_NAME = 'DialogContent'
|
|
436
463
|
|
|
437
|
-
|
|
438
|
-
|
|
464
|
+
// Unstyled content frame: stacking + pointer events + the opt-in elevate/bordered
|
|
465
|
+
// v2-compat variants (formerly inherited from ThemeableStack, off by default).
|
|
466
|
+
// Background, padding, and radius live in the tamagui skin. The variants stay on
|
|
467
|
+
// this frame (not a skin wrapper) because the frame is the animated node — a
|
|
468
|
+
// variant block on a wrapper around it breaks the native animation driver's
|
|
469
|
+
// interpolation.
|
|
470
|
+
const DialogContentFrame = styled(YStack, {
|
|
471
|
+
displayName: CONTENT_NAME,
|
|
439
472
|
zIndex: 2,
|
|
473
|
+
position: 'relative',
|
|
474
|
+
// Ensure content receives pointer events (fixes React 19 + display:contents inheritance)
|
|
475
|
+
pointerEvents: 'auto',
|
|
440
476
|
|
|
441
477
|
variants: {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
478
|
+
elevate: {
|
|
479
|
+
true: {
|
|
480
|
+
shadowColor: 'shadow-color',
|
|
481
|
+
shadowRadius: 24,
|
|
482
|
+
shadowOffset: { width: 0, height: 12 },
|
|
445
483
|
},
|
|
446
484
|
},
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
false: {
|
|
450
|
-
position: 'relative',
|
|
451
|
-
backgroundColor: '$background',
|
|
485
|
+
bordered: {
|
|
486
|
+
true: {
|
|
452
487
|
borderWidth: 1,
|
|
453
|
-
borderColor: '
|
|
454
|
-
padding: '$true',
|
|
455
|
-
borderRadius: '$true',
|
|
456
|
-
elevate: true,
|
|
457
|
-
// Ensure content receives pointer events (fixes React 19 + display:contents inheritance)
|
|
458
|
-
pointerEvents: 'auto',
|
|
488
|
+
borderColor: 'border-color',
|
|
459
489
|
},
|
|
460
490
|
},
|
|
461
491
|
} as const,
|
|
462
|
-
|
|
463
|
-
defaultVariants: {
|
|
464
|
-
size: '$true',
|
|
465
|
-
unstyled: process.env.TAMAGUI_HEADLESS === '1',
|
|
466
|
-
},
|
|
467
492
|
})
|
|
468
493
|
|
|
469
494
|
type DialogContentFrameProps = GetProps<typeof DialogContentFrame>
|
|
@@ -474,28 +499,89 @@ type DialogContentExtraProps = ScopedProps<
|
|
|
474
499
|
|
|
475
500
|
type DialogContentProps = DialogContentFrameProps & DialogContentExtraProps
|
|
476
501
|
|
|
477
|
-
const DialogContent =
|
|
478
|
-
|
|
502
|
+
const DialogContent = createStyledHOC(
|
|
503
|
+
DialogContentFrame,
|
|
504
|
+
function DialogContent({ scope, ...props }: DialogContentExtraProps, forwardedRef) {
|
|
479
505
|
const context = useDialogContext(scope)
|
|
506
|
+
const isAdapted = useAdaptIsActive(context.adaptScope)
|
|
507
|
+
const reporter = useDialogAnimationReporter(context)
|
|
508
|
+
const onTransitionProp = props.onTransition
|
|
509
|
+
const onTransition = React.useCallback<OnTransition>(
|
|
510
|
+
(event) => {
|
|
511
|
+
if (event.phase === 'end' && event.cause === 'enter') {
|
|
512
|
+
reporter.onEnterComplete()
|
|
513
|
+
}
|
|
514
|
+
onTransitionProp?.(event)
|
|
515
|
+
},
|
|
516
|
+
[reporter.onEnterComplete, onTransitionProp]
|
|
517
|
+
)
|
|
518
|
+
const presence = useDialogPartPresence(context, {
|
|
519
|
+
disabled: isAdapted,
|
|
520
|
+
forceMount: context.forceMount,
|
|
521
|
+
id: 'content',
|
|
522
|
+
onExitComplete: reporter.onExitComplete,
|
|
523
|
+
})
|
|
480
524
|
|
|
481
525
|
const contents = (
|
|
482
526
|
<>
|
|
483
527
|
{context.modal ? (
|
|
484
|
-
<DialogContentModal
|
|
528
|
+
<DialogContentModal
|
|
529
|
+
context={context}
|
|
530
|
+
{...props}
|
|
531
|
+
onTransition={onTransition}
|
|
532
|
+
ref={forwardedRef}
|
|
533
|
+
/>
|
|
485
534
|
) : (
|
|
486
|
-
<DialogContentNonModal
|
|
535
|
+
<DialogContentNonModal
|
|
536
|
+
context={context}
|
|
537
|
+
{...props}
|
|
538
|
+
onTransition={onTransition}
|
|
539
|
+
ref={forwardedRef}
|
|
540
|
+
/>
|
|
487
541
|
)}
|
|
488
542
|
</>
|
|
489
543
|
)
|
|
490
544
|
|
|
545
|
+
if (isAdapted) {
|
|
546
|
+
if (!isWeb || context.disableRemoveScroll) {
|
|
547
|
+
return contents
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return (
|
|
551
|
+
<RemoveScroll enabled={context.open && context.modal}>
|
|
552
|
+
<div data-remove-scroll-container className="_dsp_contents">
|
|
553
|
+
{contents}
|
|
554
|
+
</div>
|
|
555
|
+
</RemoveScroll>
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (!presence.shouldRender) {
|
|
560
|
+
return null
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const animated = (
|
|
564
|
+
<Animate
|
|
565
|
+
type="presence"
|
|
566
|
+
present={Boolean(context.open)}
|
|
567
|
+
keepChildrenMounted={Boolean(presence.keepMounted)}
|
|
568
|
+
onExitComplete={presence.onExitComplete}
|
|
569
|
+
>
|
|
570
|
+
<React.Fragment key={context.contentId}>{contents}</React.Fragment>
|
|
571
|
+
</Animate>
|
|
572
|
+
)
|
|
573
|
+
|
|
491
574
|
if (!isWeb || context.disableRemoveScroll) {
|
|
492
|
-
return
|
|
575
|
+
return animated
|
|
493
576
|
}
|
|
494
577
|
|
|
578
|
+
// RemoveScroll must stay OUTSIDE the presence boundary: the exiting clone
|
|
579
|
+
// freezes props, so an enabled={open} baked inside it would keep the body
|
|
580
|
+
// pointer-events lock for the whole exit animation
|
|
495
581
|
return (
|
|
496
|
-
<RemoveScroll enabled={context.open}>
|
|
582
|
+
<RemoveScroll enabled={context.open && context.modal}>
|
|
497
583
|
<div data-remove-scroll-container className="_dsp_contents">
|
|
498
|
-
{
|
|
584
|
+
{animated}
|
|
499
585
|
</div>
|
|
500
586
|
</RemoveScroll>
|
|
501
587
|
)
|
|
@@ -508,8 +594,13 @@ type DialogContentTypeProps = DialogContentImplProps & {
|
|
|
508
594
|
context: DialogContextValue
|
|
509
595
|
}
|
|
510
596
|
|
|
511
|
-
const DialogContentModal =
|
|
512
|
-
({ children, context, ...props }, forwardedRef) => {
|
|
597
|
+
const DialogContentModal = createRefComponent<TamaguiElement, DialogContentTypeProps>(
|
|
598
|
+
({ children, context: contextProp, ...props }, forwardedRef) => {
|
|
599
|
+
// re-read context via hook: this component renders inside the presence
|
|
600
|
+
// boundary, whose exiting clone freezes props at open=true — a hook
|
|
601
|
+
// subscription still receives fresh context so open-derived behavior
|
|
602
|
+
// (body pointer-events lock, focus trap) releases as soon as close starts
|
|
603
|
+
const context = useDialogContext(contextProp.dialogScope)
|
|
513
604
|
const contentRef = React.useRef<TamaguiElement>(null)
|
|
514
605
|
const composedRefs = useComposedRefs(forwardedRef, context.contentRef, contentRef)
|
|
515
606
|
|
|
@@ -523,29 +614,28 @@ const DialogContentModal = React.forwardRef<TamaguiElement, DialogContentTypePro
|
|
|
523
614
|
trapFocus={context.open}
|
|
524
615
|
disableOutsidePointerEvents
|
|
525
616
|
onCloseAutoFocus={composeEventHandlers(props.onCloseAutoFocus, (event) => {
|
|
526
|
-
event.
|
|
617
|
+
event.cancel()
|
|
527
618
|
context.triggerRef.current?.focus()
|
|
528
619
|
})}
|
|
529
620
|
onPointerDownOutside={composeEventHandlers(
|
|
530
621
|
props.onPointerDownOutside,
|
|
531
622
|
(event) => {
|
|
532
|
-
const originalEvent = event
|
|
623
|
+
const originalEvent = event.event
|
|
624
|
+
if (!originalEvent) return
|
|
533
625
|
const ctrlLeftClick =
|
|
534
626
|
originalEvent.button === 0 && originalEvent.ctrlKey === true
|
|
535
627
|
const isRightClick = originalEvent.button === 2 || ctrlLeftClick
|
|
536
628
|
// If the event is a right-click, we shouldn't close because
|
|
537
629
|
// it is effectively as if we right-clicked the `Overlay`.
|
|
538
|
-
if (isRightClick) event.
|
|
630
|
+
if (isRightClick) event.cancel()
|
|
539
631
|
}
|
|
540
632
|
)}
|
|
541
633
|
// When focus is trapped, a `focusout` event may still happen.
|
|
542
634
|
// We make sure we don't trigger our `onDismiss` in such case.
|
|
543
635
|
onFocusOutside={composeEventHandlers(props.onFocusOutside, (event) =>
|
|
544
|
-
event.
|
|
636
|
+
event.cancel()
|
|
545
637
|
)}
|
|
546
|
-
|
|
547
|
-
outlineStyle: 'none',
|
|
548
|
-
})}
|
|
638
|
+
outlineStyle="none"
|
|
549
639
|
>
|
|
550
640
|
{children}
|
|
551
641
|
</DialogContentImpl>
|
|
@@ -555,25 +645,28 @@ const DialogContentModal = React.forwardRef<TamaguiElement, DialogContentTypePro
|
|
|
555
645
|
|
|
556
646
|
/* -----------------------------------------------------------------------------------------------*/
|
|
557
647
|
|
|
558
|
-
const DialogContentNonModal =
|
|
648
|
+
const DialogContentNonModal = createRefComponent<TamaguiElement, DialogContentTypeProps>(
|
|
559
649
|
(props, forwardedRef) => {
|
|
650
|
+
// fresh context read for the same presence-freeze reason as DialogContentModal
|
|
651
|
+
const context = useDialogContext(props.context.dialogScope)
|
|
560
652
|
const hasInteractedOutsideRef = React.useRef(false)
|
|
561
653
|
|
|
562
654
|
return (
|
|
563
655
|
<DialogContentImpl
|
|
564
656
|
{...props}
|
|
657
|
+
context={context}
|
|
565
658
|
ref={forwardedRef}
|
|
566
659
|
trapFocus={false}
|
|
567
660
|
disableOutsidePointerEvents={false}
|
|
568
661
|
onCloseAutoFocus={(event) => {
|
|
569
662
|
props.onCloseAutoFocus?.(event)
|
|
570
663
|
|
|
571
|
-
if (!event.
|
|
664
|
+
if (!event.isCanceled) {
|
|
572
665
|
if (!hasInteractedOutsideRef.current) {
|
|
573
666
|
props.context.triggerRef.current?.focus()
|
|
574
667
|
}
|
|
575
668
|
// Always prevent auto focus because we either focus manually or want user agent focus
|
|
576
|
-
event.
|
|
669
|
+
event.cancel()
|
|
577
670
|
}
|
|
578
671
|
|
|
579
672
|
hasInteractedOutsideRef.current = false
|
|
@@ -581,7 +674,7 @@ const DialogContentNonModal = React.forwardRef<TamaguiElement, DialogContentType
|
|
|
581
674
|
onInteractOutside={(event) => {
|
|
582
675
|
props.onInteractOutside?.(event)
|
|
583
676
|
|
|
584
|
-
if (!event.
|
|
677
|
+
if (!event.isCanceled) hasInteractedOutsideRef.current = true
|
|
585
678
|
|
|
586
679
|
// Prevent dismissing when clicking the trigger.
|
|
587
680
|
// As the trigger is already setup to close, without doing so would
|
|
@@ -589,11 +682,11 @@ const DialogContentNonModal = React.forwardRef<TamaguiElement, DialogContentType
|
|
|
589
682
|
//
|
|
590
683
|
// We use `onInteractOutside` as some browsers also
|
|
591
684
|
// focus on pointer down, creating the same issue.
|
|
592
|
-
const target = event.target as HTMLElement
|
|
685
|
+
const target = event.event?.target as HTMLElement | null
|
|
593
686
|
const trigger = props.context.triggerRef.current
|
|
594
|
-
if (!(trigger instanceof HTMLElement)) return
|
|
687
|
+
if (!target || !(trigger instanceof HTMLElement)) return
|
|
595
688
|
const targetIsTrigger = trigger.contains(target)
|
|
596
|
-
if (targetIsTrigger) event.
|
|
689
|
+
if (targetIsTrigger) event.cancel()
|
|
597
690
|
}}
|
|
598
691
|
/>
|
|
599
692
|
)
|
|
@@ -612,22 +705,23 @@ type DialogContentImplExtraProps = Omit<DismissableProps, 'onDismiss'> & {
|
|
|
612
705
|
|
|
613
706
|
/**
|
|
614
707
|
* Event handler called when auto-focusing on open.
|
|
615
|
-
* Can be
|
|
708
|
+
* Can be canceled.
|
|
616
709
|
*/
|
|
617
710
|
onOpenAutoFocus?: FocusScopeProps['onMountAutoFocus']
|
|
618
711
|
|
|
619
712
|
/**
|
|
620
713
|
* Event handler called when auto-focusing on close.
|
|
621
|
-
* Can be
|
|
714
|
+
* Can be canceled.
|
|
622
715
|
*/
|
|
623
716
|
onCloseAutoFocus?: FocusScopeProps['onUnmountAutoFocus']
|
|
624
717
|
|
|
625
718
|
context: DialogContextValue
|
|
719
|
+
onTransition?: OnTransition
|
|
626
720
|
}
|
|
627
721
|
|
|
628
722
|
type DialogContentImplProps = DialogContentFrameProps & DialogContentImplExtraProps
|
|
629
723
|
|
|
630
|
-
const DialogContentImpl =
|
|
724
|
+
const DialogContentImpl = createRefComponent<TamaguiElement, DialogContentImplProps>(
|
|
631
725
|
(props, forwardedRef) => {
|
|
632
726
|
const {
|
|
633
727
|
trapFocus,
|
|
@@ -639,24 +733,23 @@ const DialogContentImpl = React.forwardRef<TamaguiElement, DialogContentImplProp
|
|
|
639
733
|
onFocusOutside,
|
|
640
734
|
onInteractOutside,
|
|
641
735
|
context,
|
|
736
|
+
onTransition,
|
|
642
737
|
...contentProps
|
|
643
738
|
} = props
|
|
644
739
|
|
|
645
740
|
const contentRef = React.useRef<TamaguiElement>(null)
|
|
646
741
|
const composedRefs = useComposedRefs(forwardedRef, contentRef)
|
|
647
742
|
const isAdapted = useAdaptIsActive(context.adaptScope)
|
|
743
|
+
const adaptContext = useAdaptContext(context.adaptScope)
|
|
648
744
|
|
|
649
745
|
// TODO this will re-parent, ideally we would not change tree structure
|
|
650
746
|
|
|
651
|
-
// when adapted, the dialog's content is portaled into the Sheet via
|
|
652
|
-
// Adapt.Contents. hold children mounted until the sheet's slide-out
|
|
653
|
-
// animation is fully complete (DialogAdaptHiddenContext is flipped by
|
|
654
|
-
// DialogSheetController via SheetController.onAnimationComplete), then
|
|
655
|
-
// unmount. opt out with `keepChildrenMounted` if you need the old
|
|
656
|
-
// permanent-mount behavior.
|
|
657
|
-
const isAdaptFullyHidden = React.useContext(DialogAdaptHiddenContext)
|
|
658
747
|
if (isAdapted) {
|
|
659
|
-
if (
|
|
748
|
+
if (
|
|
749
|
+
!context.open &&
|
|
750
|
+
!context.keepChildrenMounted &&
|
|
751
|
+
adaptContext.targetFullyHidden
|
|
752
|
+
) {
|
|
660
753
|
return null
|
|
661
754
|
}
|
|
662
755
|
|
|
@@ -676,6 +769,7 @@ const DialogContentImpl = React.forwardRef<TamaguiElement, DialogContentImplProp
|
|
|
676
769
|
data-state={getState(context.open)}
|
|
677
770
|
// allow clicking through content during exit animation
|
|
678
771
|
pointerEvents={context.open ? 'auto' : 'none'}
|
|
772
|
+
onTransition={onTransition}
|
|
679
773
|
{...contentProps}
|
|
680
774
|
/>
|
|
681
775
|
)
|
|
@@ -726,14 +820,15 @@ const DialogContentImpl = React.forwardRef<TamaguiElement, DialogContentImplProp
|
|
|
726
820
|
* -----------------------------------------------------------------------------------------------*/
|
|
727
821
|
|
|
728
822
|
const DialogTitleFrame = styled(H2, {
|
|
729
|
-
|
|
823
|
+
displayName: 'DialogTitle',
|
|
730
824
|
})
|
|
731
825
|
|
|
732
826
|
type DialogTitleExtraProps = ScopedProps<{}>
|
|
733
827
|
type DialogTitleProps = DialogTitleExtraProps & GetProps<typeof DialogTitleFrame>
|
|
734
828
|
|
|
735
|
-
const DialogTitle =
|
|
736
|
-
|
|
829
|
+
const DialogTitle = createStyledHOC(
|
|
830
|
+
DialogTitleFrame,
|
|
831
|
+
function DialogTitle(props: DialogTitleExtraProps, forwardedRef) {
|
|
737
832
|
const { scope, ...titleProps } = props
|
|
738
833
|
const context = useDialogContext(scope)
|
|
739
834
|
return <DialogTitleFrame id={context.titleId} {...titleProps} ref={forwardedRef} />
|
|
@@ -745,15 +840,16 @@ const DialogTitle = DialogTitleFrame.styleable<DialogTitleExtraProps>(
|
|
|
745
840
|
* -----------------------------------------------------------------------------------------------*/
|
|
746
841
|
|
|
747
842
|
const DialogDescriptionFrame = styled(Paragraph, {
|
|
748
|
-
|
|
843
|
+
displayName: 'DialogDescription',
|
|
749
844
|
})
|
|
750
845
|
|
|
751
846
|
type DialogDescriptionExtraProps = ScopedProps<{}>
|
|
752
847
|
type DialogDescriptionProps = DialogDescriptionExtraProps &
|
|
753
848
|
GetProps<typeof DialogDescriptionFrame>
|
|
754
849
|
|
|
755
|
-
const DialogDescription =
|
|
756
|
-
|
|
850
|
+
const DialogDescription = createStyledHOC(
|
|
851
|
+
DialogDescriptionFrame,
|
|
852
|
+
function DialogDescription(props: DialogDescriptionExtraProps, forwardedRef) {
|
|
757
853
|
const { scope, ...descriptionProps } = props
|
|
758
854
|
const context = useDialogContext(scope)
|
|
759
855
|
return (
|
|
@@ -773,7 +869,7 @@ const DialogDescription = DialogDescriptionFrame.styleable<DialogDescriptionExtr
|
|
|
773
869
|
const CLOSE_NAME = 'DialogClose'
|
|
774
870
|
|
|
775
871
|
const DialogCloseFrame = styled(View, {
|
|
776
|
-
|
|
872
|
+
displayName: CLOSE_NAME,
|
|
777
873
|
render: 'button',
|
|
778
874
|
})
|
|
779
875
|
|
|
@@ -783,8 +879,9 @@ export type DialogCloseExtraProps = ScopedProps<{
|
|
|
783
879
|
|
|
784
880
|
type DialogCloseProps = GetProps<typeof DialogCloseFrame> & DialogCloseExtraProps
|
|
785
881
|
|
|
786
|
-
const DialogClose =
|
|
787
|
-
|
|
882
|
+
const DialogClose = createStyledHOC(
|
|
883
|
+
DialogCloseFrame,
|
|
884
|
+
(props: DialogCloseProps, forwardedRef) => {
|
|
788
885
|
const { scope, displayWhenAdapted, ...closeProps } = props
|
|
789
886
|
const context = useDialogContext(scope)
|
|
790
887
|
const isAdapted = useAdaptIsActive(context.adaptScope)
|
|
@@ -883,87 +980,83 @@ const DescriptionWarning: React.FC<DescriptionWarningProps> = ({
|
|
|
883
980
|
* Dialog
|
|
884
981
|
* -----------------------------------------------------------------------------------------------*/
|
|
885
982
|
|
|
886
|
-
export type DialogHandle = {
|
|
887
|
-
open: (val: boolean) => void
|
|
888
|
-
}
|
|
889
|
-
|
|
890
983
|
const Dialog = withStaticProperties(
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
} = props
|
|
904
|
-
|
|
905
|
-
const baseId = React.useId()
|
|
906
|
-
const dialogId = `Dialog-${scope}-${baseId}`
|
|
907
|
-
const contentId = `${dialogId}-content`
|
|
908
|
-
const titleId = `${dialogId}-title`
|
|
909
|
-
const descriptionId = `${dialogId}-description`
|
|
910
|
-
|
|
911
|
-
const triggerRef = React.useRef<TamaguiElement>(null)
|
|
912
|
-
const contentRef = React.useRef<TamaguiElement>(null)
|
|
913
|
-
|
|
914
|
-
const [open, setOpen] = useControllableState({
|
|
915
|
-
prop: openProp,
|
|
916
|
-
defaultProp: defaultOpen,
|
|
917
|
-
onChange: onOpenChange,
|
|
918
|
-
})
|
|
919
|
-
|
|
920
|
-
const onOpenToggle = React.useCallback(() => {
|
|
921
|
-
setOpen((prevOpen) => !prevOpen)
|
|
922
|
-
}, [setOpen])
|
|
923
|
-
|
|
924
|
-
const adaptScope = `DialogAdapt${scope}`
|
|
925
|
-
|
|
926
|
-
const context = {
|
|
927
|
-
dialogScope: scope,
|
|
928
|
-
adaptScope,
|
|
929
|
-
triggerRef,
|
|
930
|
-
contentRef,
|
|
931
|
-
contentId,
|
|
932
|
-
titleId,
|
|
933
|
-
descriptionId,
|
|
934
|
-
open,
|
|
935
|
-
onOpenChange: setOpen,
|
|
936
|
-
onOpenToggle,
|
|
937
|
-
modal,
|
|
938
|
-
keepChildrenMounted,
|
|
939
|
-
disableRemoveScroll,
|
|
940
|
-
onAnimationComplete,
|
|
941
|
-
} satisfies DialogContextValue
|
|
942
|
-
|
|
943
|
-
React.useImperativeHandle(
|
|
944
|
-
ref,
|
|
945
|
-
() => ({
|
|
946
|
-
open: setOpen,
|
|
947
|
-
}),
|
|
948
|
-
[setOpen]
|
|
949
|
-
)
|
|
984
|
+
createRefComponent<TamaguiElement, DialogProps>(function Dialog(props) {
|
|
985
|
+
const {
|
|
986
|
+
scope = '',
|
|
987
|
+
children,
|
|
988
|
+
open: openProp,
|
|
989
|
+
defaultOpen = false,
|
|
990
|
+
onOpenChange,
|
|
991
|
+
modal = true,
|
|
992
|
+
keepChildrenMounted,
|
|
993
|
+
disableRemoveScroll = false,
|
|
994
|
+
onAnimationComplete,
|
|
995
|
+
} = props
|
|
950
996
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
997
|
+
const baseId = React.useId()
|
|
998
|
+
const dialogId = `Dialog-${scope}-${baseId}`
|
|
999
|
+
const contentId = `${dialogId}-content`
|
|
1000
|
+
const titleId = `${dialogId}-title`
|
|
1001
|
+
const descriptionId = `${dialogId}-description`
|
|
1002
|
+
|
|
1003
|
+
const triggerRef = React.useRef<TamaguiElement>(null)
|
|
1004
|
+
const contentRef = React.useRef<TamaguiElement>(null)
|
|
1005
|
+
const presentPartIdsRef = React.useRef(new Set<string>())
|
|
1006
|
+
const [presentPartCount, setPresentPartCount] = React.useState(0)
|
|
1007
|
+
|
|
1008
|
+
const [open, setOpen] = useControllableState({
|
|
1009
|
+
prop: openProp,
|
|
1010
|
+
defaultProp: defaultOpen,
|
|
1011
|
+
onChange: onOpenChange,
|
|
1012
|
+
})
|
|
1013
|
+
|
|
1014
|
+
const onOpenToggle = React.useCallback(() => {
|
|
1015
|
+
setOpen((prevOpen) => !prevOpen)
|
|
1016
|
+
}, [setOpen])
|
|
1017
|
+
|
|
1018
|
+
const adaptScope = `DialogAdapt${scope}`
|
|
1019
|
+
|
|
1020
|
+
const setPartPresence = React.useCallback((id: string, present: boolean) => {
|
|
1021
|
+
const presentPartIds = presentPartIdsRef.current
|
|
1022
|
+
const hasPart = presentPartIds.has(id)
|
|
1023
|
+
|
|
1024
|
+
if (present && !hasPart) {
|
|
1025
|
+
presentPartIds.add(id)
|
|
1026
|
+
setPresentPartCount(presentPartIds.size)
|
|
1027
|
+
} else if (!present && hasPart) {
|
|
1028
|
+
presentPartIds.delete(id)
|
|
1029
|
+
setPresentPartCount(presentPartIds.size)
|
|
1030
|
+
}
|
|
1031
|
+
}, [])
|
|
1032
|
+
|
|
1033
|
+
const context = {
|
|
1034
|
+
dialogScope: scope,
|
|
1035
|
+
adaptScope,
|
|
1036
|
+
triggerRef,
|
|
1037
|
+
contentRef,
|
|
1038
|
+
contentId,
|
|
1039
|
+
titleId,
|
|
1040
|
+
descriptionId,
|
|
1041
|
+
open,
|
|
1042
|
+
onOpenChange: setOpen,
|
|
1043
|
+
onOpenToggle,
|
|
1044
|
+
modal,
|
|
1045
|
+
keepChildrenMounted,
|
|
1046
|
+
disableRemoveScroll,
|
|
1047
|
+
hasPresentParts: presentPartCount > 0,
|
|
1048
|
+
setPartPresence,
|
|
1049
|
+
onAnimationComplete,
|
|
1050
|
+
} satisfies DialogContextValue
|
|
1051
|
+
|
|
1052
|
+
return (
|
|
1053
|
+
<AdaptParent scope={adaptScope} open={open} onOpenChange={setOpen} state={context}>
|
|
1054
|
+
<DialogProvider scope={scope} {...context}>
|
|
1055
|
+
{children}
|
|
1056
|
+
</DialogProvider>
|
|
1057
|
+
</AdaptParent>
|
|
1058
|
+
)
|
|
1059
|
+
}),
|
|
967
1060
|
{
|
|
968
1061
|
Trigger: DialogTrigger,
|
|
969
1062
|
Portal: DialogPortal,
|
|
@@ -977,56 +1070,6 @@ const Dialog = withStaticProperties(
|
|
|
977
1070
|
}
|
|
978
1071
|
)
|
|
979
1072
|
|
|
980
|
-
const getAdaptScope = (dialogScope: string) => `DialogAdapt${dialogScope}`
|
|
981
|
-
|
|
982
|
-
const DialogSheetController = (
|
|
983
|
-
props: ScopedProps<{
|
|
984
|
-
children: React.ReactNode
|
|
985
|
-
onOpenChange: React.Dispatch<React.SetStateAction<boolean>>
|
|
986
|
-
}>
|
|
987
|
-
) => {
|
|
988
|
-
const context = useDialogContext(props.scope)
|
|
989
|
-
const isAdapted = useAdaptIsActive(context.adaptScope)
|
|
990
|
-
|
|
991
|
-
// tracks whether the adapted Sheet has finished its slide-out animation.
|
|
992
|
-
// starts true (= safe to unmount) when the dialog is closed; flips to
|
|
993
|
-
// false the moment the dialog opens; flips back to true when the sheet
|
|
994
|
-
// signals onAnimationComplete with open=false (i.e. slide-out finished).
|
|
995
|
-
const [isAdaptFullyHidden, setIsAdaptFullyHidden] = React.useState(!context.open)
|
|
996
|
-
// mirror context.open into the hidden flag during render — an opening
|
|
997
|
-
// dialog must immediately mark its children as not-hidden so they render
|
|
998
|
-
// for the enter animation.
|
|
999
|
-
if (context.open && isAdaptFullyHidden) {
|
|
1000
|
-
setIsAdaptFullyHidden(false)
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
const handleSheetAnimationComplete = React.useCallback(
|
|
1004
|
-
({ open }: { open: boolean }) => {
|
|
1005
|
-
if (!open) {
|
|
1006
|
-
setIsAdaptFullyHidden(true)
|
|
1007
|
-
}
|
|
1008
|
-
},
|
|
1009
|
-
[]
|
|
1010
|
-
)
|
|
1011
|
-
|
|
1012
|
-
return (
|
|
1013
|
-
<SheetController
|
|
1014
|
-
onOpenChange={(val) => {
|
|
1015
|
-
if (isAdapted) {
|
|
1016
|
-
props.onOpenChange?.(val)
|
|
1017
|
-
}
|
|
1018
|
-
}}
|
|
1019
|
-
onAnimationComplete={handleSheetAnimationComplete}
|
|
1020
|
-
open={context.open}
|
|
1021
|
-
hidden={!isAdapted}
|
|
1022
|
-
>
|
|
1023
|
-
<DialogAdaptHiddenContext.Provider value={isAdaptFullyHidden}>
|
|
1024
|
-
{props.children}
|
|
1025
|
-
</DialogAdaptHiddenContext.Provider>
|
|
1026
|
-
</SheetController>
|
|
1027
|
-
)
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
1073
|
export {
|
|
1031
1074
|
//
|
|
1032
1075
|
Dialog,
|