@hanzogui/dialog 2.0.0
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/LICENSE +21 -0
- package/dist/cjs/Dialog.cjs +618 -0
- package/dist/cjs/Dialog.native.js +691 -0
- package/dist/cjs/Dialog.native.js.map +1 -0
- package/dist/cjs/index.cjs +18 -0
- package/dist/cjs/index.native.js +21 -0
- package/dist/cjs/index.native.js.map +1 -0
- package/dist/esm/Dialog.mjs +571 -0
- package/dist/esm/Dialog.mjs.map +1 -0
- package/dist/esm/Dialog.native.js +641 -0
- package/dist/esm/Dialog.native.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/index.mjs +2 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/index.native.js +2 -0
- package/dist/esm/index.native.js.map +1 -0
- package/dist/jsx/Dialog.mjs +571 -0
- package/dist/jsx/Dialog.mjs.map +1 -0
- package/dist/jsx/Dialog.native.js +691 -0
- package/dist/jsx/Dialog.native.js.map +1 -0
- package/dist/jsx/index.js +2 -0
- package/dist/jsx/index.js.map +1 -0
- package/dist/jsx/index.mjs +2 -0
- package/dist/jsx/index.mjs.map +1 -0
- package/dist/jsx/index.native.js +21 -0
- package/dist/jsx/index.native.js.map +1 -0
- package/package.json +68 -0
- package/src/Dialog.tsx +1009 -0
- package/src/index.ts +1 -0
- package/types/Dialog.d.ts +409 -0
- package/types/index.d.ts +2 -0
package/src/Dialog.tsx
ADDED
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Adapt,
|
|
3
|
+
AdaptParent,
|
|
4
|
+
AdaptPortalContents,
|
|
5
|
+
ProvideAdaptContext,
|
|
6
|
+
useAdaptContext,
|
|
7
|
+
useAdaptIsActive,
|
|
8
|
+
} from '@hanzogui/adapt'
|
|
9
|
+
import { Animate } from '@hanzogui/animate'
|
|
10
|
+
import { composeRefs, useComposedRefs } from '@hanzogui/compose-refs'
|
|
11
|
+
import { isWeb, useIsomorphicLayoutEffect } from '@hanzogui/constants'
|
|
12
|
+
import type { GetProps, GuiElement, ViewProps } from '@hanzogui/core'
|
|
13
|
+
import {
|
|
14
|
+
createStyledContext,
|
|
15
|
+
getExpandedShorthand,
|
|
16
|
+
LayoutMeasurementController,
|
|
17
|
+
styled,
|
|
18
|
+
Theme,
|
|
19
|
+
useThemeName,
|
|
20
|
+
View,
|
|
21
|
+
} from '@hanzogui/core'
|
|
22
|
+
import { createContext } from '@hanzogui/create-context'
|
|
23
|
+
import type { DismissableProps } from '@hanzogui/dismissable'
|
|
24
|
+
import { Dismissable } from '@hanzogui/dismissable'
|
|
25
|
+
import type { FocusScopeProps } from '@hanzogui/focus-scope'
|
|
26
|
+
import { FocusScope, FocusScopeController } from '@hanzogui/focus-scope'
|
|
27
|
+
import { composeEventHandlers, withStaticProperties } from '@hanzogui/helpers'
|
|
28
|
+
import {
|
|
29
|
+
needsPortalRepropagation,
|
|
30
|
+
Portal,
|
|
31
|
+
PortalItem,
|
|
32
|
+
resolveViewZIndex,
|
|
33
|
+
} from '@hanzogui/portal'
|
|
34
|
+
import { RemoveScroll } from '@hanzogui/remove-scroll'
|
|
35
|
+
import { SheetController } from '@hanzogui/sheet/controller'
|
|
36
|
+
import type { YStackProps } from '@hanzogui/stacks'
|
|
37
|
+
import { ButtonNestingContext, ThemeableStack, YStack } from '@hanzogui/stacks'
|
|
38
|
+
import { H2, Paragraph } from '@hanzogui/text'
|
|
39
|
+
import { useControllableState } from '@hanzogui/use-controllable-state'
|
|
40
|
+
import { StackZIndexContext } from '@hanzogui/z-index-stack'
|
|
41
|
+
import * as React from 'react'
|
|
42
|
+
|
|
43
|
+
export type DialogScopes = string
|
|
44
|
+
|
|
45
|
+
type ScopedProps<P> = P & { scope?: DialogScopes }
|
|
46
|
+
|
|
47
|
+
type DialogProps = ScopedProps<{
|
|
48
|
+
children?: React.ReactNode
|
|
49
|
+
open?: boolean
|
|
50
|
+
defaultOpen?: boolean
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* When true, children never un-mount, otherwise they mount on open.
|
|
54
|
+
*
|
|
55
|
+
* @default false
|
|
56
|
+
*/
|
|
57
|
+
keepChildrenMounted?: boolean
|
|
58
|
+
onOpenChange?(open: boolean): void
|
|
59
|
+
modal?: boolean
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Used to disable the remove scroll functionality when open
|
|
63
|
+
*/
|
|
64
|
+
disableRemoveScroll?: boolean
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Called when the dialog open/close animation completes.
|
|
68
|
+
*/
|
|
69
|
+
onAnimationComplete?: (info: { open: boolean }) => void
|
|
70
|
+
}>
|
|
71
|
+
|
|
72
|
+
type NonNull<A> = Exclude<A, void | null>
|
|
73
|
+
|
|
74
|
+
type DialogContextValue = {
|
|
75
|
+
forceMount?: boolean
|
|
76
|
+
keepChildrenMounted?: boolean
|
|
77
|
+
disableRemoveScroll?: boolean
|
|
78
|
+
triggerRef: React.RefObject<GuiElement | null>
|
|
79
|
+
contentRef: React.RefObject<GuiElement | null>
|
|
80
|
+
contentId: string
|
|
81
|
+
titleId: string
|
|
82
|
+
descriptionId: string
|
|
83
|
+
onOpenToggle(): void
|
|
84
|
+
open: NonNull<DialogProps['open']>
|
|
85
|
+
onOpenChange: NonNull<DialogProps['onOpenChange']>
|
|
86
|
+
modal: NonNull<DialogProps['modal']>
|
|
87
|
+
dialogScope: DialogScopes
|
|
88
|
+
adaptScope: string
|
|
89
|
+
onAnimationComplete?: DialogProps['onAnimationComplete']
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const DialogContext = createStyledContext<DialogContextValue>(
|
|
93
|
+
// since we always provide this we can avoid setting here
|
|
94
|
+
{} as DialogContextValue,
|
|
95
|
+
'Dialog__'
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
export const { useStyledContext: useDialogContext, Provider: DialogProvider } =
|
|
99
|
+
DialogContext
|
|
100
|
+
|
|
101
|
+
/* -------------------------------------------------------------------------------------------------
|
|
102
|
+
* DialogTrigger
|
|
103
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
104
|
+
|
|
105
|
+
const DialogTriggerFrame = styled(View, {
|
|
106
|
+
name: 'DialogTrigger',
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
type DialogTriggerProps = ScopedProps<ViewProps>
|
|
110
|
+
|
|
111
|
+
const DialogTrigger = DialogTriggerFrame.styleable<ScopedProps<{}>>(
|
|
112
|
+
function DialogTrigger(props, forwardedRef) {
|
|
113
|
+
const { scope, ...triggerProps } = props
|
|
114
|
+
const isInsideButton = React.useContext(ButtonNestingContext)
|
|
115
|
+
const context = useDialogContext(scope)
|
|
116
|
+
const composedTriggerRef = useComposedRefs(forwardedRef, context.triggerRef)
|
|
117
|
+
return (
|
|
118
|
+
<ButtonNestingContext.Provider value={true}>
|
|
119
|
+
<DialogTriggerFrame
|
|
120
|
+
render={isInsideButton ? 'span' : 'button'}
|
|
121
|
+
aria-haspopup="dialog"
|
|
122
|
+
aria-expanded={context.open}
|
|
123
|
+
aria-controls={context.contentId}
|
|
124
|
+
data-state={getState(context.open)}
|
|
125
|
+
{...triggerProps}
|
|
126
|
+
ref={composedTriggerRef}
|
|
127
|
+
onPress={composeEventHandlers(props.onPress as any, context.onOpenToggle)}
|
|
128
|
+
/>
|
|
129
|
+
</ButtonNestingContext.Provider>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
/* -------------------------------------------------------------------------------------------------
|
|
135
|
+
* DialogPortal
|
|
136
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
137
|
+
|
|
138
|
+
type DialogPortalProps = ScopedProps<
|
|
139
|
+
YStackProps & {
|
|
140
|
+
/**
|
|
141
|
+
* Used to force mounting when more control is needed. Useful when
|
|
142
|
+
* controlling animation with React animation libraries.
|
|
143
|
+
*/
|
|
144
|
+
forceMount?: boolean
|
|
145
|
+
}
|
|
146
|
+
>
|
|
147
|
+
|
|
148
|
+
export const DialogPortalFrame = styled(YStack, {
|
|
149
|
+
pointerEvents: 'none',
|
|
150
|
+
render: 'dialog',
|
|
151
|
+
|
|
152
|
+
variants: {
|
|
153
|
+
unstyled: {
|
|
154
|
+
false: {
|
|
155
|
+
alignItems: 'center',
|
|
156
|
+
justifyContent: 'center',
|
|
157
|
+
fullscreen: true,
|
|
158
|
+
|
|
159
|
+
'$platform-web': {
|
|
160
|
+
// undo dialog styles
|
|
161
|
+
borderWidth: 0,
|
|
162
|
+
backgroundColor: 'transparent',
|
|
163
|
+
color: 'inherit',
|
|
164
|
+
maxInlineSize: 'none',
|
|
165
|
+
margin: 0,
|
|
166
|
+
width: 'auto',
|
|
167
|
+
height: 'auto',
|
|
168
|
+
// ensure always in frame and right height
|
|
169
|
+
maxHeight: '100vh',
|
|
170
|
+
position: 'fixed',
|
|
171
|
+
// ensure dialog inherits stacking context from portal wrapper
|
|
172
|
+
zIndex: 1,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
} as const,
|
|
177
|
+
|
|
178
|
+
defaultVariants: {
|
|
179
|
+
unstyled: process.env.HANZO_GUI_HEADLESS === '1',
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const needsRepropagation = needsPortalRepropagation()
|
|
184
|
+
|
|
185
|
+
const DialogPortalItem = ({
|
|
186
|
+
context,
|
|
187
|
+
children,
|
|
188
|
+
}: {
|
|
189
|
+
context: DialogContextValue
|
|
190
|
+
children: React.ReactNode
|
|
191
|
+
}) => {
|
|
192
|
+
const themeName = useThemeName()
|
|
193
|
+
const isAdapted = useAdaptIsActive(context.adaptScope)
|
|
194
|
+
const adaptContext = useAdaptContext(context.adaptScope)
|
|
195
|
+
|
|
196
|
+
let content = <Theme name={themeName}>{children}</Theme>
|
|
197
|
+
|
|
198
|
+
// not just adapted - both sheet and portal for modal need it
|
|
199
|
+
if (needsRepropagation) {
|
|
200
|
+
content = (
|
|
201
|
+
<ProvideAdaptContext {...adaptContext}>
|
|
202
|
+
<DialogProvider {...context}>{content}</DialogProvider>
|
|
203
|
+
</ProvideAdaptContext>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// until we can use react-native portals natively
|
|
208
|
+
// have to re-propogate context, sketch
|
|
209
|
+
// when adapted we portal to the adapt, when not we portal to root modal if needed
|
|
210
|
+
return isAdapted ? (
|
|
211
|
+
<AdaptPortalContents scope={context.adaptScope}>{content}</AdaptPortalContents>
|
|
212
|
+
) : context.modal ? (
|
|
213
|
+
<PortalItem hostName={context.modal ? 'root' : context.adaptScope}>
|
|
214
|
+
{content}
|
|
215
|
+
</PortalItem>
|
|
216
|
+
) : (
|
|
217
|
+
content
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const DialogPortal = React.forwardRef<GuiElement, DialogPortalProps>(
|
|
222
|
+
(props, forwardRef) => {
|
|
223
|
+
const { scope, forceMount, children, ...frameProps } = props
|
|
224
|
+
const dialogRef = React.useRef<GuiElement>(null)
|
|
225
|
+
const ref = composeRefs(dialogRef, forwardRef)
|
|
226
|
+
|
|
227
|
+
const context = useDialogContext(scope)
|
|
228
|
+
const keepMounted = forceMount || context.keepChildrenMounted
|
|
229
|
+
const isAdapted = useAdaptIsActive(context.adaptScope)
|
|
230
|
+
const [isFullyHidden, setIsFullyHidden] = React.useState(!context.open)
|
|
231
|
+
|
|
232
|
+
if (context.open && isFullyHidden) {
|
|
233
|
+
setIsFullyHidden(false)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const isVisible = context.open ? true : !isFullyHidden
|
|
237
|
+
|
|
238
|
+
if (isWeb) {
|
|
239
|
+
useIsomorphicLayoutEffect(() => {
|
|
240
|
+
const node = dialogRef.current
|
|
241
|
+
if (!(node instanceof HTMLDialogElement)) return
|
|
242
|
+
// optional chaining for JSDOM compatibility (doesn't implement show/close)
|
|
243
|
+
if (isVisible) {
|
|
244
|
+
node.show?.()
|
|
245
|
+
} else {
|
|
246
|
+
node.close?.()
|
|
247
|
+
}
|
|
248
|
+
}, [isVisible])
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const onAnimationCompleteRef = React.useRef(context.onAnimationComplete)
|
|
252
|
+
onAnimationCompleteRef.current = context.onAnimationComplete
|
|
253
|
+
|
|
254
|
+
const handleExitComplete = React.useCallback(() => {
|
|
255
|
+
setIsFullyHidden(true)
|
|
256
|
+
onAnimationCompleteRef.current?.({ open: false })
|
|
257
|
+
}, [])
|
|
258
|
+
|
|
259
|
+
React.useEffect(() => {
|
|
260
|
+
if (context.open && !isAdapted && onAnimationCompleteRef.current) {
|
|
261
|
+
const tm = setTimeout(() => {
|
|
262
|
+
onAnimationCompleteRef.current?.({ open: true })
|
|
263
|
+
}, 350)
|
|
264
|
+
return () => clearTimeout(tm)
|
|
265
|
+
}
|
|
266
|
+
}, [context.open, isAdapted])
|
|
267
|
+
|
|
268
|
+
const zIndex = getExpandedShorthand('zIndex', props)
|
|
269
|
+
|
|
270
|
+
const contents = (
|
|
271
|
+
<StackZIndexContext zIndex={resolveViewZIndex(zIndex)}>
|
|
272
|
+
<Animate
|
|
273
|
+
type="presence"
|
|
274
|
+
present={Boolean(context.open)}
|
|
275
|
+
keepChildrenMounted={Boolean(keepMounted)}
|
|
276
|
+
onExitComplete={handleExitComplete}
|
|
277
|
+
passThrough={isAdapted}
|
|
278
|
+
>
|
|
279
|
+
{children}
|
|
280
|
+
</Animate>
|
|
281
|
+
</StackZIndexContext>
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
const framedContents =
|
|
285
|
+
isFullyHidden && !keepMounted && !isAdapted ? null : (
|
|
286
|
+
<LayoutMeasurementController disable={!context.open}>
|
|
287
|
+
<DialogPortalFrame
|
|
288
|
+
ref={ref}
|
|
289
|
+
{...(isWeb &&
|
|
290
|
+
context.open && {
|
|
291
|
+
'aria-modal': true,
|
|
292
|
+
})}
|
|
293
|
+
pointerEvents={context.open ? 'auto' : 'none'}
|
|
294
|
+
{...frameProps}
|
|
295
|
+
className={`_no_backdrop ` + (frameProps.className || '')}
|
|
296
|
+
>
|
|
297
|
+
{contents}
|
|
298
|
+
</DialogPortalFrame>
|
|
299
|
+
</LayoutMeasurementController>
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if (isWeb) {
|
|
303
|
+
return (
|
|
304
|
+
<Portal
|
|
305
|
+
zIndex={zIndex}
|
|
306
|
+
// set to 100000 which ensures dialogs are above most fixed UI (headers, navs)
|
|
307
|
+
// this makes sure its above typical stacking contexts
|
|
308
|
+
stackZIndex={100000}
|
|
309
|
+
passThrough={isAdapted}
|
|
310
|
+
>
|
|
311
|
+
<PassthroughTheme passThrough={isAdapted}>{framedContents}</PassthroughTheme>
|
|
312
|
+
</Portal>
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return isAdapted ? (
|
|
317
|
+
framedContents
|
|
318
|
+
) : (
|
|
319
|
+
<DialogPortalItem context={context}>{framedContents}</DialogPortalItem>
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
const PassthroughTheme = ({
|
|
325
|
+
children,
|
|
326
|
+
passThrough,
|
|
327
|
+
}: {
|
|
328
|
+
passThrough?: boolean
|
|
329
|
+
children?: React.ReactNode
|
|
330
|
+
}) => {
|
|
331
|
+
const themeName = useThemeName()
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
<Theme passThrough={passThrough} name={themeName} forceClassName>
|
|
335
|
+
{children}
|
|
336
|
+
</Theme>
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/* -------------------------------------------------------------------------------------------------
|
|
341
|
+
* DialogOverlay
|
|
342
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
343
|
+
|
|
344
|
+
const OVERLAY_NAME = 'DialogOverlay'
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* exported for internal use with extractable()
|
|
348
|
+
*/
|
|
349
|
+
export const DialogOverlayFrame = styled(YStack, {
|
|
350
|
+
name: OVERLAY_NAME,
|
|
351
|
+
zIndex: 1,
|
|
352
|
+
|
|
353
|
+
variants: {
|
|
354
|
+
open: {
|
|
355
|
+
true: {
|
|
356
|
+
pointerEvents: 'auto',
|
|
357
|
+
},
|
|
358
|
+
false: {
|
|
359
|
+
pointerEvents: 'none',
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
unstyled: {
|
|
364
|
+
false: {
|
|
365
|
+
fullscreen: true,
|
|
366
|
+
position: 'absolute',
|
|
367
|
+
backgroundColor: '$background',
|
|
368
|
+
pointerEvents: 'auto',
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
} as const,
|
|
372
|
+
|
|
373
|
+
defaultVariants: {
|
|
374
|
+
unstyled: process.env.HANZO_GUI_HEADLESS === '1',
|
|
375
|
+
},
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
export type DialogOverlayExtraProps = ScopedProps<{
|
|
379
|
+
/**
|
|
380
|
+
* Used to force mounting when more control is needed. Useful when
|
|
381
|
+
* controlling animation with React animation libraries.
|
|
382
|
+
*/
|
|
383
|
+
forceMount?: boolean
|
|
384
|
+
}>
|
|
385
|
+
|
|
386
|
+
type DialogOverlayProps = YStackProps & DialogOverlayExtraProps
|
|
387
|
+
|
|
388
|
+
const DialogOverlay = DialogOverlayFrame.styleable<DialogOverlayExtraProps>(
|
|
389
|
+
function DialogOverlay({ scope, ...props }, forwardedRef) {
|
|
390
|
+
const context = useDialogContext(scope)
|
|
391
|
+
const { forceMount = context.forceMount, ...overlayProps } = props
|
|
392
|
+
const isAdapted = useAdaptIsActive(context.adaptScope)
|
|
393
|
+
|
|
394
|
+
if (!forceMount) {
|
|
395
|
+
if (!context.modal || isAdapted) {
|
|
396
|
+
return null
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Make sure `Content` is scrollable even when it doesn't live inside `RemoveScroll`
|
|
401
|
+
// ie. when `Overlay` and `Content` are siblings
|
|
402
|
+
return (
|
|
403
|
+
<DialogOverlayFrame
|
|
404
|
+
data-state={getState(context.open)}
|
|
405
|
+
// TODO: this will be apply for v2
|
|
406
|
+
// onPress={() => {
|
|
407
|
+
// // if the overlay is pressed, close the dialog
|
|
408
|
+
// context.onOpenChange(false)
|
|
409
|
+
// }}
|
|
410
|
+
// We re-enable pointer-events prevented by `Dialog.Content` to allow scrolling the overlay.
|
|
411
|
+
pointerEvents={context.open ? 'auto' : 'none'}
|
|
412
|
+
{...overlayProps}
|
|
413
|
+
ref={forwardedRef}
|
|
414
|
+
/>
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
/* -------------------------------------------------------------------------------------------------
|
|
420
|
+
* DialogContent
|
|
421
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
422
|
+
|
|
423
|
+
const CONTENT_NAME = 'DialogContent'
|
|
424
|
+
|
|
425
|
+
const DialogContentFrame = styled(ThemeableStack, {
|
|
426
|
+
name: CONTENT_NAME,
|
|
427
|
+
zIndex: 2,
|
|
428
|
+
|
|
429
|
+
variants: {
|
|
430
|
+
size: {
|
|
431
|
+
'...size': (val, extras) => {
|
|
432
|
+
return {}
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
unstyled: {
|
|
437
|
+
false: {
|
|
438
|
+
position: 'relative',
|
|
439
|
+
backgroundColor: '$background',
|
|
440
|
+
borderWidth: 1,
|
|
441
|
+
borderColor: '$borderColor',
|
|
442
|
+
padding: '$true',
|
|
443
|
+
borderRadius: '$true',
|
|
444
|
+
elevate: true,
|
|
445
|
+
// Ensure content receives pointer events (fixes React 19 + display:contents inheritance)
|
|
446
|
+
pointerEvents: 'auto',
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
} as const,
|
|
450
|
+
|
|
451
|
+
defaultVariants: {
|
|
452
|
+
size: '$true',
|
|
453
|
+
unstyled: process.env.HANZO_GUI_HEADLESS === '1',
|
|
454
|
+
},
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
type DialogContentFrameProps = GetProps<typeof DialogContentFrame>
|
|
458
|
+
|
|
459
|
+
type DialogContentExtraProps = ScopedProps<
|
|
460
|
+
Omit<DialogContentTypeProps, 'context' | 'onPointerDownCapture'>
|
|
461
|
+
>
|
|
462
|
+
|
|
463
|
+
type DialogContentProps = DialogContentFrameProps & DialogContentExtraProps
|
|
464
|
+
|
|
465
|
+
const DialogContent = DialogContentFrame.styleable<DialogContentExtraProps>(
|
|
466
|
+
function DialogContent({ scope, ...props }, forwardedRef) {
|
|
467
|
+
const context = useDialogContext(scope)
|
|
468
|
+
|
|
469
|
+
const contents = (
|
|
470
|
+
<>
|
|
471
|
+
{context.modal ? (
|
|
472
|
+
<DialogContentModal context={context} {...props} ref={forwardedRef} />
|
|
473
|
+
) : (
|
|
474
|
+
<DialogContentNonModal context={context} {...props} ref={forwardedRef} />
|
|
475
|
+
)}
|
|
476
|
+
</>
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
if (!isWeb || context.disableRemoveScroll) {
|
|
480
|
+
return contents
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return (
|
|
484
|
+
<RemoveScroll enabled={context.open}>
|
|
485
|
+
<div data-remove-scroll-container className="_dsp_contents">
|
|
486
|
+
{contents}
|
|
487
|
+
</div>
|
|
488
|
+
</RemoveScroll>
|
|
489
|
+
)
|
|
490
|
+
}
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
/* -----------------------------------------------------------------------------------------------*/
|
|
494
|
+
|
|
495
|
+
type DialogContentTypeProps = DialogContentImplProps & {
|
|
496
|
+
context: DialogContextValue
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const DialogContentModal = React.forwardRef<GuiElement, DialogContentTypeProps>(
|
|
500
|
+
({ children, context, ...props }, forwardedRef) => {
|
|
501
|
+
const contentRef = React.useRef<GuiElement>(null)
|
|
502
|
+
const composedRefs = useComposedRefs(forwardedRef, context.contentRef, contentRef)
|
|
503
|
+
|
|
504
|
+
return (
|
|
505
|
+
<DialogContentImpl
|
|
506
|
+
{...props}
|
|
507
|
+
context={context}
|
|
508
|
+
ref={composedRefs}
|
|
509
|
+
// we make sure focus isn't trapped once `DialogContent` has been closed
|
|
510
|
+
// (closed !== unmounted when animating out)
|
|
511
|
+
trapFocus={context.open}
|
|
512
|
+
disableOutsidePointerEvents
|
|
513
|
+
onCloseAutoFocus={composeEventHandlers(props.onCloseAutoFocus, (event) => {
|
|
514
|
+
event.preventDefault()
|
|
515
|
+
context.triggerRef.current?.focus()
|
|
516
|
+
})}
|
|
517
|
+
onPointerDownOutside={composeEventHandlers(
|
|
518
|
+
props.onPointerDownOutside,
|
|
519
|
+
(event) => {
|
|
520
|
+
const originalEvent = event['detail'].originalEvent
|
|
521
|
+
const ctrlLeftClick =
|
|
522
|
+
originalEvent.button === 0 && originalEvent.ctrlKey === true
|
|
523
|
+
const isRightClick = originalEvent.button === 2 || ctrlLeftClick
|
|
524
|
+
// If the event is a right-click, we shouldn't close because
|
|
525
|
+
// it is effectively as if we right-clicked the `Overlay`.
|
|
526
|
+
if (isRightClick) event.preventDefault()
|
|
527
|
+
}
|
|
528
|
+
)}
|
|
529
|
+
// When focus is trapped, a `focusout` event may still happen.
|
|
530
|
+
// We make sure we don't trigger our `onDismiss` in such case.
|
|
531
|
+
onFocusOutside={composeEventHandlers(props.onFocusOutside, (event) =>
|
|
532
|
+
event.preventDefault()
|
|
533
|
+
)}
|
|
534
|
+
{...(!props.unstyled && {
|
|
535
|
+
outlineStyle: 'none',
|
|
536
|
+
})}
|
|
537
|
+
>
|
|
538
|
+
{children}
|
|
539
|
+
</DialogContentImpl>
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
/* -----------------------------------------------------------------------------------------------*/
|
|
545
|
+
|
|
546
|
+
const DialogContentNonModal = React.forwardRef<GuiElement, DialogContentTypeProps>(
|
|
547
|
+
(props, forwardedRef) => {
|
|
548
|
+
const hasInteractedOutsideRef = React.useRef(false)
|
|
549
|
+
|
|
550
|
+
return (
|
|
551
|
+
<DialogContentImpl
|
|
552
|
+
{...props}
|
|
553
|
+
ref={forwardedRef}
|
|
554
|
+
trapFocus={false}
|
|
555
|
+
disableOutsidePointerEvents={false}
|
|
556
|
+
onCloseAutoFocus={(event) => {
|
|
557
|
+
props.onCloseAutoFocus?.(event)
|
|
558
|
+
|
|
559
|
+
if (!event.defaultPrevented) {
|
|
560
|
+
if (!hasInteractedOutsideRef.current) {
|
|
561
|
+
props.context.triggerRef.current?.focus()
|
|
562
|
+
}
|
|
563
|
+
// Always prevent auto focus because we either focus manually or want user agent focus
|
|
564
|
+
event.preventDefault()
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
hasInteractedOutsideRef.current = false
|
|
568
|
+
}}
|
|
569
|
+
onInteractOutside={(event) => {
|
|
570
|
+
props.onInteractOutside?.(event)
|
|
571
|
+
|
|
572
|
+
if (!event.defaultPrevented) hasInteractedOutsideRef.current = true
|
|
573
|
+
|
|
574
|
+
// Prevent dismissing when clicking the trigger.
|
|
575
|
+
// As the trigger is already setup to close, without doing so would
|
|
576
|
+
// cause it to close and immediately open.
|
|
577
|
+
//
|
|
578
|
+
// We use `onInteractOutside` as some browsers also
|
|
579
|
+
// focus on pointer down, creating the same issue.
|
|
580
|
+
const target = event.target as HTMLElement
|
|
581
|
+
const trigger = props.context.triggerRef.current
|
|
582
|
+
if (!(trigger instanceof HTMLElement)) return
|
|
583
|
+
const targetIsTrigger = trigger.contains(target)
|
|
584
|
+
if (targetIsTrigger) event.preventDefault()
|
|
585
|
+
}}
|
|
586
|
+
/>
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
/* -----------------------------------------------------------------------------------------------*/
|
|
592
|
+
|
|
593
|
+
type DialogContentImplExtraProps = Omit<DismissableProps, 'onDismiss'> & {
|
|
594
|
+
/**
|
|
595
|
+
* When `true`, focus cannot escape the `Content` via keyboard,
|
|
596
|
+
* pointer, or a programmatic focus.
|
|
597
|
+
* @defaultValue false
|
|
598
|
+
*/
|
|
599
|
+
trapFocus?: FocusScopeProps['trapped']
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Event handler called when auto-focusing on open.
|
|
603
|
+
* Can be prevented.
|
|
604
|
+
*/
|
|
605
|
+
onOpenAutoFocus?: FocusScopeProps['onMountAutoFocus']
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Event handler called when auto-focusing on close.
|
|
609
|
+
* Can be prevented.
|
|
610
|
+
*/
|
|
611
|
+
onCloseAutoFocus?: FocusScopeProps['onUnmountAutoFocus']
|
|
612
|
+
|
|
613
|
+
context: DialogContextValue
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
type DialogContentImplProps = DialogContentFrameProps & DialogContentImplExtraProps
|
|
617
|
+
|
|
618
|
+
const DialogContentImpl = React.forwardRef<GuiElement, DialogContentImplProps>(
|
|
619
|
+
(props, forwardedRef) => {
|
|
620
|
+
const {
|
|
621
|
+
trapFocus,
|
|
622
|
+
onOpenAutoFocus,
|
|
623
|
+
onCloseAutoFocus,
|
|
624
|
+
disableOutsidePointerEvents,
|
|
625
|
+
onEscapeKeyDown,
|
|
626
|
+
onPointerDownOutside,
|
|
627
|
+
onFocusOutside,
|
|
628
|
+
onInteractOutside,
|
|
629
|
+
context,
|
|
630
|
+
...contentProps
|
|
631
|
+
} = props
|
|
632
|
+
|
|
633
|
+
const contentRef = React.useRef<GuiElement>(null)
|
|
634
|
+
const composedRefs = useComposedRefs(forwardedRef, contentRef)
|
|
635
|
+
const isAdapted = useAdaptIsActive(context.adaptScope)
|
|
636
|
+
|
|
637
|
+
// TODO this will re-parent, ideally we would not change tree structure
|
|
638
|
+
|
|
639
|
+
if (isAdapted) {
|
|
640
|
+
if (!isWeb && !context.open) {
|
|
641
|
+
return null
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return (
|
|
645
|
+
<DialogPortalItem context={context}>{contentProps.children}</DialogPortalItem>
|
|
646
|
+
)
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const contents = (
|
|
650
|
+
<DialogContentFrame
|
|
651
|
+
ref={composedRefs}
|
|
652
|
+
id={context.contentId}
|
|
653
|
+
role="dialog"
|
|
654
|
+
aria-modal={context.modal}
|
|
655
|
+
aria-describedby={context.descriptionId}
|
|
656
|
+
aria-labelledby={context.titleId}
|
|
657
|
+
data-state={getState(context.open)}
|
|
658
|
+
// allow clicking through content during exit animation
|
|
659
|
+
pointerEvents={context.open ? 'auto' : 'none'}
|
|
660
|
+
{...contentProps}
|
|
661
|
+
/>
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
if (!isWeb) {
|
|
665
|
+
return contents
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return (
|
|
669
|
+
<>
|
|
670
|
+
<Dismissable
|
|
671
|
+
disableOutsidePointerEvents={context.open && disableOutsidePointerEvents}
|
|
672
|
+
forceUnmount={!context.open}
|
|
673
|
+
onEscapeKeyDown={onEscapeKeyDown}
|
|
674
|
+
onPointerDownOutside={onPointerDownOutside}
|
|
675
|
+
onFocusOutside={onFocusOutside}
|
|
676
|
+
onInteractOutside={onInteractOutside}
|
|
677
|
+
onDismiss={() => context?.onOpenChange?.(false)}
|
|
678
|
+
>
|
|
679
|
+
<FocusScope
|
|
680
|
+
loop
|
|
681
|
+
enabled={context.open}
|
|
682
|
+
trapped={trapFocus}
|
|
683
|
+
onMountAutoFocus={onOpenAutoFocus}
|
|
684
|
+
forceUnmount={!context.open}
|
|
685
|
+
onUnmountAutoFocus={onCloseAutoFocus}
|
|
686
|
+
>
|
|
687
|
+
{contents}
|
|
688
|
+
</FocusScope>
|
|
689
|
+
</Dismissable>
|
|
690
|
+
|
|
691
|
+
{process.env.NODE_ENV === 'development' && (
|
|
692
|
+
<>
|
|
693
|
+
<TitleWarning titleId={context.titleId} />
|
|
694
|
+
<DescriptionWarning
|
|
695
|
+
contentRef={contentRef as any}
|
|
696
|
+
descriptionId={context.descriptionId}
|
|
697
|
+
/>
|
|
698
|
+
</>
|
|
699
|
+
)}
|
|
700
|
+
</>
|
|
701
|
+
)
|
|
702
|
+
}
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
/* -------------------------------------------------------------------------------------------------
|
|
706
|
+
* DialogTitle
|
|
707
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
708
|
+
|
|
709
|
+
const DialogTitleFrame = styled(H2, {
|
|
710
|
+
name: 'DialogTitle',
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
type DialogTitleExtraProps = ScopedProps<{}>
|
|
714
|
+
type DialogTitleProps = DialogTitleExtraProps & GetProps<typeof DialogTitleFrame>
|
|
715
|
+
|
|
716
|
+
const DialogTitle = DialogTitleFrame.styleable<DialogTitleExtraProps>(
|
|
717
|
+
function DialogTitle(props, forwardedRef) {
|
|
718
|
+
const { scope, ...titleProps } = props
|
|
719
|
+
const context = useDialogContext(scope)
|
|
720
|
+
return <DialogTitleFrame id={context.titleId} {...titleProps} ref={forwardedRef} />
|
|
721
|
+
}
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
/* -------------------------------------------------------------------------------------------------
|
|
725
|
+
* DialogDescription
|
|
726
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
727
|
+
|
|
728
|
+
const DialogDescriptionFrame = styled(Paragraph, {
|
|
729
|
+
name: 'DialogDescription',
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
type DialogDescriptionExtraProps = ScopedProps<{}>
|
|
733
|
+
type DialogDescriptionProps = DialogDescriptionExtraProps &
|
|
734
|
+
GetProps<typeof DialogDescriptionFrame>
|
|
735
|
+
|
|
736
|
+
const DialogDescription = DialogDescriptionFrame.styleable<DialogDescriptionExtraProps>(
|
|
737
|
+
function DialogDescription(props, forwardedRef) {
|
|
738
|
+
const { scope, ...descriptionProps } = props
|
|
739
|
+
const context = useDialogContext(scope)
|
|
740
|
+
return (
|
|
741
|
+
<DialogDescriptionFrame
|
|
742
|
+
id={context.descriptionId}
|
|
743
|
+
{...descriptionProps}
|
|
744
|
+
ref={forwardedRef}
|
|
745
|
+
/>
|
|
746
|
+
)
|
|
747
|
+
}
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
/* -------------------------------------------------------------------------------------------------
|
|
751
|
+
* DialogClose
|
|
752
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
753
|
+
|
|
754
|
+
const CLOSE_NAME = 'DialogClose'
|
|
755
|
+
|
|
756
|
+
const DialogCloseFrame = styled(View, {
|
|
757
|
+
name: CLOSE_NAME,
|
|
758
|
+
render: 'button',
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
export type DialogCloseExtraProps = ScopedProps<{
|
|
762
|
+
displayWhenAdapted?: boolean
|
|
763
|
+
}>
|
|
764
|
+
|
|
765
|
+
type DialogCloseProps = GetProps<typeof DialogCloseFrame> & DialogCloseExtraProps
|
|
766
|
+
|
|
767
|
+
const DialogClose = DialogCloseFrame.styleable<DialogCloseExtraProps>(
|
|
768
|
+
(props, forwardedRef) => {
|
|
769
|
+
const { scope, displayWhenAdapted, ...closeProps } = props
|
|
770
|
+
const context = useDialogContext(scope)
|
|
771
|
+
const isAdapted = useAdaptIsActive(context.adaptScope)
|
|
772
|
+
const isInsideButton = React.useContext(ButtonNestingContext)
|
|
773
|
+
|
|
774
|
+
if (isAdapted && !displayWhenAdapted) {
|
|
775
|
+
return null
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return (
|
|
779
|
+
<DialogCloseFrame
|
|
780
|
+
aria-label="Dialog Close"
|
|
781
|
+
render={isInsideButton ? 'span' : 'button'}
|
|
782
|
+
{...closeProps}
|
|
783
|
+
ref={forwardedRef}
|
|
784
|
+
onPress={composeEventHandlers(props.onPress as any, () => {
|
|
785
|
+
context.onOpenChange(false)
|
|
786
|
+
})}
|
|
787
|
+
/>
|
|
788
|
+
)
|
|
789
|
+
}
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
/* -----------------------------------------------------------------------------------------------*/
|
|
793
|
+
|
|
794
|
+
function getState(open: boolean) {
|
|
795
|
+
return open ? 'open' : 'closed'
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const TITLE_WARNING_NAME = 'DialogTitleWarning'
|
|
799
|
+
|
|
800
|
+
const [DialogWarningProvider, useWarningContext] = createContext(TITLE_WARNING_NAME, {
|
|
801
|
+
contentName: CONTENT_NAME,
|
|
802
|
+
titleName: 'DialogTitle',
|
|
803
|
+
docsSlug: 'dialog',
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
type TitleWarningProps = { titleId?: string }
|
|
807
|
+
|
|
808
|
+
const TitleWarning: React.FC<TitleWarningProps> = ({ titleId }) => {
|
|
809
|
+
if (process.env.NODE_ENV === 'development') {
|
|
810
|
+
const titleWarningContext = useWarningContext(TITLE_WARNING_NAME)
|
|
811
|
+
|
|
812
|
+
const MESSAGE = `\`${titleWarningContext.contentName}\` wants a \`${titleWarningContext.titleName}\` to be accessible. If you want to hide the \`${titleWarningContext.titleName}\`, wrap it with <VisuallyHidden />.`
|
|
813
|
+
|
|
814
|
+
React.useEffect(() => {
|
|
815
|
+
if (!isWeb) return
|
|
816
|
+
if (titleId) {
|
|
817
|
+
const hasTitle = document.getElementById(titleId)
|
|
818
|
+
if (!hasTitle) {
|
|
819
|
+
console.warn(MESSAGE)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}, [MESSAGE, titleId])
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return null
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const DESCRIPTION_WARNING_NAME = 'DialogDescriptionWarning'
|
|
829
|
+
|
|
830
|
+
type DescriptionWarningProps = {
|
|
831
|
+
contentRef: React.RefObject<GuiElement>
|
|
832
|
+
descriptionId?: string
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const DescriptionWarning: React.FC<DescriptionWarningProps> = ({
|
|
836
|
+
contentRef,
|
|
837
|
+
descriptionId,
|
|
838
|
+
}) => {
|
|
839
|
+
if (process.env.NODE_ENV === 'development') {
|
|
840
|
+
const descriptionWarningContext = useWarningContext(DESCRIPTION_WARNING_NAME)
|
|
841
|
+
const MESSAGE = `Warning: Missing \`Description\` or \`aria-describedby={undefined}\` for {${descriptionWarningContext.contentName}}.`
|
|
842
|
+
|
|
843
|
+
React.useEffect(() => {
|
|
844
|
+
if (!isWeb) return
|
|
845
|
+
const contentNode = contentRef.current
|
|
846
|
+
if (!(contentNode instanceof HTMLElement)) {
|
|
847
|
+
return
|
|
848
|
+
}
|
|
849
|
+
const describedById = contentNode.getAttribute('aria-describedby')
|
|
850
|
+
// if we have an id and the user hasn't set aria-describedby={undefined}
|
|
851
|
+
if (descriptionId && describedById) {
|
|
852
|
+
const hasDescription = document.getElementById(descriptionId)
|
|
853
|
+
if (!hasDescription) {
|
|
854
|
+
console.warn(MESSAGE)
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}, [MESSAGE, contentRef, descriptionId])
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return null
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/* -------------------------------------------------------------------------------------------------
|
|
864
|
+
* Dialog
|
|
865
|
+
* -----------------------------------------------------------------------------------------------*/
|
|
866
|
+
|
|
867
|
+
export type DialogHandle = {
|
|
868
|
+
open: (val: boolean) => void
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const Dialog = withStaticProperties(
|
|
872
|
+
React.forwardRef<{ open: (val: boolean) => void }, DialogProps>(
|
|
873
|
+
function Dialog(props, ref) {
|
|
874
|
+
const {
|
|
875
|
+
scope = '',
|
|
876
|
+
children,
|
|
877
|
+
open: openProp,
|
|
878
|
+
defaultOpen = false,
|
|
879
|
+
onOpenChange,
|
|
880
|
+
modal = true,
|
|
881
|
+
keepChildrenMounted,
|
|
882
|
+
disableRemoveScroll = false,
|
|
883
|
+
onAnimationComplete,
|
|
884
|
+
} = props
|
|
885
|
+
|
|
886
|
+
const baseId = React.useId()
|
|
887
|
+
const dialogId = `Dialog-${scope}-${baseId}`
|
|
888
|
+
const contentId = `${dialogId}-content`
|
|
889
|
+
const titleId = `${dialogId}-title`
|
|
890
|
+
const descriptionId = `${dialogId}-description`
|
|
891
|
+
|
|
892
|
+
const triggerRef = React.useRef<GuiElement>(null)
|
|
893
|
+
const contentRef = React.useRef<GuiElement>(null)
|
|
894
|
+
|
|
895
|
+
const [open, setOpen] = useControllableState({
|
|
896
|
+
prop: openProp,
|
|
897
|
+
defaultProp: defaultOpen,
|
|
898
|
+
onChange: onOpenChange,
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
const onOpenToggle = React.useCallback(() => {
|
|
902
|
+
setOpen((prevOpen) => !prevOpen)
|
|
903
|
+
}, [setOpen])
|
|
904
|
+
|
|
905
|
+
const adaptScope = `DialogAdapt${scope}`
|
|
906
|
+
|
|
907
|
+
const context = {
|
|
908
|
+
dialogScope: scope,
|
|
909
|
+
adaptScope,
|
|
910
|
+
triggerRef,
|
|
911
|
+
contentRef,
|
|
912
|
+
contentId,
|
|
913
|
+
titleId,
|
|
914
|
+
descriptionId,
|
|
915
|
+
open,
|
|
916
|
+
onOpenChange: setOpen,
|
|
917
|
+
onOpenToggle,
|
|
918
|
+
modal,
|
|
919
|
+
keepChildrenMounted,
|
|
920
|
+
disableRemoveScroll,
|
|
921
|
+
onAnimationComplete,
|
|
922
|
+
} satisfies DialogContextValue
|
|
923
|
+
|
|
924
|
+
React.useImperativeHandle(
|
|
925
|
+
ref,
|
|
926
|
+
() => ({
|
|
927
|
+
open: setOpen,
|
|
928
|
+
}),
|
|
929
|
+
[setOpen]
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
return (
|
|
933
|
+
<AdaptParent
|
|
934
|
+
scope={adaptScope}
|
|
935
|
+
portal={{
|
|
936
|
+
forwardProps: props,
|
|
937
|
+
}}
|
|
938
|
+
>
|
|
939
|
+
<DialogProvider scope={scope} {...context}>
|
|
940
|
+
<DialogSheetController onOpenChange={setOpen} scope={scope}>
|
|
941
|
+
{children}
|
|
942
|
+
</DialogSheetController>
|
|
943
|
+
</DialogProvider>
|
|
944
|
+
</AdaptParent>
|
|
945
|
+
)
|
|
946
|
+
}
|
|
947
|
+
),
|
|
948
|
+
{
|
|
949
|
+
Trigger: DialogTrigger,
|
|
950
|
+
Portal: DialogPortal,
|
|
951
|
+
Overlay: DialogOverlay,
|
|
952
|
+
Content: DialogContent,
|
|
953
|
+
Title: DialogTitle,
|
|
954
|
+
Description: DialogDescription,
|
|
955
|
+
Close: DialogClose,
|
|
956
|
+
FocusScope: FocusScopeController,
|
|
957
|
+
Adapt,
|
|
958
|
+
}
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
const getAdaptScope = (dialogScope: string) => `DialogAdapt${dialogScope}`
|
|
962
|
+
|
|
963
|
+
const DialogSheetController = (
|
|
964
|
+
props: ScopedProps<{
|
|
965
|
+
children: React.ReactNode
|
|
966
|
+
onOpenChange: React.Dispatch<React.SetStateAction<boolean>>
|
|
967
|
+
}>
|
|
968
|
+
) => {
|
|
969
|
+
const context = useDialogContext(props.scope)
|
|
970
|
+
const isAdapted = useAdaptIsActive(context.adaptScope)
|
|
971
|
+
|
|
972
|
+
return (
|
|
973
|
+
<SheetController
|
|
974
|
+
onOpenChange={(val) => {
|
|
975
|
+
if (isAdapted) {
|
|
976
|
+
props.onOpenChange?.(val)
|
|
977
|
+
}
|
|
978
|
+
}}
|
|
979
|
+
open={context.open}
|
|
980
|
+
hidden={!isAdapted}
|
|
981
|
+
>
|
|
982
|
+
{props.children}
|
|
983
|
+
</SheetController>
|
|
984
|
+
)
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
export {
|
|
988
|
+
//
|
|
989
|
+
Dialog,
|
|
990
|
+
DialogClose,
|
|
991
|
+
DialogContent,
|
|
992
|
+
DialogDescription,
|
|
993
|
+
DialogOverlay,
|
|
994
|
+
DialogPortal,
|
|
995
|
+
DialogTitle,
|
|
996
|
+
DialogTrigger,
|
|
997
|
+
//
|
|
998
|
+
DialogWarningProvider,
|
|
999
|
+
}
|
|
1000
|
+
export type {
|
|
1001
|
+
DialogCloseProps,
|
|
1002
|
+
DialogContentProps,
|
|
1003
|
+
DialogDescriptionProps,
|
|
1004
|
+
DialogOverlayProps,
|
|
1005
|
+
DialogPortalProps,
|
|
1006
|
+
DialogProps,
|
|
1007
|
+
DialogTitleProps,
|
|
1008
|
+
DialogTriggerProps,
|
|
1009
|
+
}
|