@tamagui/dialog 1.0.1-beta.100

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/src/Dialog.tsx ADDED
@@ -0,0 +1,669 @@
1
+ import { AnimatePresence } from '@tamagui/animate-presence'
2
+ import { useComposedRefs } from '@tamagui/compose-refs'
3
+ import {
4
+ GetProps,
5
+ Slot,
6
+ Theme,
7
+ composeEventHandlers,
8
+ isWeb,
9
+ styled,
10
+ useId,
11
+ useThemeName,
12
+ withStaticProperties,
13
+ } from '@tamagui/core'
14
+ import { Scope, createContext, createContextScope } from '@tamagui/create-context'
15
+ import { Dismissable, DismissableProps } from '@tamagui/dismissable'
16
+ import { FocusScope, FocusScopeProps } from '@tamagui/focus-scope'
17
+ import { Portal, PortalProps } from '@tamagui/portal'
18
+ import { ThemeableStack, YStack, YStackProps } from '@tamagui/stacks'
19
+ import { H2, Paragraph } from '@tamagui/text'
20
+ import { useControllableState } from '@tamagui/use-controllable-state'
21
+ import { hideOthers } from 'aria-hidden'
22
+ import * as React from 'react'
23
+ import { View } from 'react-native'
24
+ import { RemoveScroll } from 'react-remove-scroll'
25
+
26
+ const DIALOG_NAME = 'Dialog'
27
+
28
+ type ScopedProps<P> = P & { __scopeDialog?: Scope }
29
+ type TamaguiElement = HTMLElement | View
30
+
31
+ const [createDialogContext, createDialogScope] = createContextScope(DIALOG_NAME)
32
+
33
+ type DialogContextValue = {
34
+ triggerRef: React.RefObject<TamaguiElement>
35
+ contentRef: React.RefObject<TamaguiElement>
36
+ contentId: string
37
+ titleId: string
38
+ descriptionId: string
39
+ open: boolean
40
+ onOpenChange(open: boolean): void
41
+ onOpenToggle(): void
42
+ modal: boolean
43
+ allowPinchZoom: boolean
44
+ }
45
+
46
+ const [DialogProvider, useDialogContext] = createDialogContext<DialogContextValue>(DIALOG_NAME)
47
+
48
+ type RemoveScrollProps = React.ComponentProps<typeof RemoveScroll>
49
+
50
+ interface DialogProps {
51
+ children?: React.ReactNode
52
+ open?: boolean
53
+ defaultOpen?: boolean
54
+ onOpenChange?(open: boolean): void
55
+ modal?: boolean
56
+
57
+ /**
58
+ * @see https://github.com/theKashey/react-remove-scroll#usage
59
+ */
60
+ allowPinchZoom?: RemoveScrollProps['allowPinchZoom']
61
+ }
62
+
63
+ /* -------------------------------------------------------------------------------------------------
64
+ * DialogTrigger
65
+ * -----------------------------------------------------------------------------------------------*/
66
+
67
+ const TRIGGER_NAME = 'DialogTrigger'
68
+
69
+ const DialogTriggerFrame = styled(YStack, {
70
+ name: TRIGGER_NAME,
71
+ })
72
+
73
+ interface DialogTriggerProps extends YStackProps {}
74
+
75
+ const DialogTrigger = React.forwardRef<TamaguiElement, DialogTriggerProps>(
76
+ (props: ScopedProps<DialogTriggerProps>, forwardedRef) => {
77
+ const { __scopeDialog, ...triggerProps } = props
78
+ const context = useDialogContext(TRIGGER_NAME, __scopeDialog)
79
+ const composedTriggerRef = useComposedRefs(forwardedRef, context.triggerRef)
80
+ return (
81
+ <DialogTriggerFrame
82
+ tag="button"
83
+ aria-haspopup="dialog"
84
+ aria-expanded={context.open}
85
+ aria-controls={context.contentId}
86
+ data-state={getState(context.open)}
87
+ {...triggerProps}
88
+ ref={composedTriggerRef}
89
+ onPress={composeEventHandlers(props.onPress, context.onOpenToggle)}
90
+ />
91
+ )
92
+ }
93
+ )
94
+
95
+ DialogTrigger.displayName = TRIGGER_NAME
96
+
97
+ /* -------------------------------------------------------------------------------------------------
98
+ * DialogPortal
99
+ * -----------------------------------------------------------------------------------------------*/
100
+
101
+ const PORTAL_NAME = 'DialogPortal'
102
+
103
+ type PortalContextValue = { forceMount?: true }
104
+ const [PortalProvider, usePortalContext] = createDialogContext<PortalContextValue>(PORTAL_NAME, {
105
+ forceMount: undefined,
106
+ })
107
+
108
+ interface DialogPortalProps extends Omit<PortalProps, 'asChild'> {
109
+ children?: React.ReactNode
110
+ /**
111
+ * Used to force mounting when more control is needed. Useful when
112
+ * controlling animation with React animation libraries.
113
+ */
114
+ forceMount?: true
115
+ }
116
+
117
+ const DialogPortal: React.FC<DialogPortalProps> = (props: ScopedProps<DialogPortalProps>) => {
118
+ const { __scopeDialog, forceMount, children, ...rest } = props
119
+ const themeName = useThemeName()
120
+ const context = useDialogContext(PORTAL_NAME, __scopeDialog)
121
+ const isShowing = forceMount || context.open
122
+ const contents = <AnimatePresence>{isShowing ? children : null}</AnimatePresence>
123
+ if (!context.modal) {
124
+ return contents
125
+ }
126
+ if (!isWeb && !isShowing) {
127
+ return contents
128
+ }
129
+ return (
130
+ <PortalProvider scope={__scopeDialog} forceMount={forceMount}>
131
+ <Portal
132
+ alignItems="center"
133
+ justifyContent="center"
134
+ zIndex={100}
135
+ pointerEvents={isShowing ? 'auto' : 'none'}
136
+ {...(isWeb && {
137
+ maxHeight: '100vh',
138
+ })}
139
+ {...rest}
140
+ >
141
+ <Theme name={themeName}>{contents}</Theme>
142
+ </Portal>
143
+ </PortalProvider>
144
+ )
145
+ }
146
+
147
+ DialogPortal.displayName = PORTAL_NAME
148
+
149
+ /* -------------------------------------------------------------------------------------------------
150
+ * DialogOverlay
151
+ * -----------------------------------------------------------------------------------------------*/
152
+
153
+ const OVERLAY_NAME = 'DialogOverlay'
154
+
155
+ const DialogOverlayFrame = styled(ThemeableStack, {
156
+ name: OVERLAY_NAME,
157
+ pointerEvents: 'auto',
158
+ backgrounded: true,
159
+ fullscreen: true,
160
+ })
161
+
162
+ interface DialogOverlayProps extends YStackProps {
163
+ /**
164
+ * Used to force mounting when more control is needed. Useful when
165
+ * controlling animation with React animation libraries.
166
+ */
167
+ forceMount?: true
168
+ }
169
+
170
+ const DialogOverlay = React.forwardRef<TamaguiElement, DialogOverlayProps>(
171
+ ({ __scopeDialog, ...props }: ScopedProps<DialogOverlayProps>, forwardedRef) => {
172
+ const portalContext = usePortalContext(OVERLAY_NAME, __scopeDialog)
173
+ const { forceMount = portalContext.forceMount, ...overlayProps } = props
174
+ const context = useDialogContext(OVERLAY_NAME, __scopeDialog)
175
+
176
+ if (!context.modal) {
177
+ return null
178
+ }
179
+
180
+ // <AnimatePresence>
181
+ return <DialogOverlayImpl {...overlayProps} ref={forwardedRef} />
182
+ // </AnimatePresence>
183
+ }
184
+ )
185
+
186
+ DialogOverlay.displayName = OVERLAY_NAME
187
+
188
+ type DialogOverlayImplProps = GetProps<typeof DialogOverlayFrame>
189
+
190
+ const DialogOverlayImpl = React.forwardRef<TamaguiElement, DialogOverlayImplProps>(
191
+ (props: ScopedProps<DialogOverlayImplProps>, forwardedRef) => {
192
+ const { __scopeDialog, ...overlayProps } = props
193
+ const context = useDialogContext(OVERLAY_NAME, __scopeDialog)
194
+ const content = (
195
+ <DialogOverlayFrame
196
+ data-state={getState(context.open)}
197
+ // We re-enable pointer-events prevented by `Dialog.Content` to allow scrolling the overlay.
198
+ pointerEvents="auto"
199
+ {...overlayProps}
200
+ ref={forwardedRef}
201
+ />
202
+ )
203
+
204
+ if (!isWeb) {
205
+ return content
206
+ }
207
+
208
+ return (
209
+ // Make sure `Content` is scrollable even when it doesn't live inside `RemoveScroll`
210
+ // ie. when `Overlay` and `Content` are siblings
211
+ <RemoveScroll as={Slot} allowPinchZoom={context.allowPinchZoom} shards={[context.contentRef]}>
212
+ {content}
213
+ </RemoveScroll>
214
+ )
215
+ }
216
+ )
217
+
218
+ /* -------------------------------------------------------------------------------------------------
219
+ * DialogContent
220
+ * -----------------------------------------------------------------------------------------------*/
221
+
222
+ const CONTENT_NAME = 'DialogContent'
223
+
224
+ const DialogContentFrame = styled(ThemeableStack, {
225
+ name: CONTENT_NAME,
226
+ tag: 'dialog',
227
+ pointerEvents: 'auto',
228
+ position: 'relative',
229
+ backgrounded: true,
230
+ padded: true,
231
+ radiused: true,
232
+ elevate: true,
233
+
234
+ variants: {
235
+ size: {
236
+ '...size': (val, extras) => {
237
+ return {}
238
+ },
239
+ },
240
+ },
241
+
242
+ defaultVariants: {
243
+ size: '$4',
244
+ },
245
+ })
246
+
247
+ type DialogContentFrameProps = GetProps<typeof DialogContentFrame>
248
+
249
+ interface DialogContentProps extends DialogContentFrameProps, DialogContentTypeProps {
250
+ /**
251
+ * Used to force mounting when more control is needed. Useful when
252
+ * controlling animation with React animation libraries.
253
+ */
254
+ forceMount?: true
255
+ }
256
+
257
+ const DialogContent = React.forwardRef<TamaguiElement, DialogContentProps>(
258
+ ({ __scopeDialog, ...props }: ScopedProps<DialogContentProps>, forwardedRef) => {
259
+ const portalContext = usePortalContext(CONTENT_NAME, __scopeDialog)
260
+ const { forceMount = portalContext.forceMount, ...contentProps } = props
261
+ const context = useDialogContext(CONTENT_NAME, __scopeDialog)
262
+ return (
263
+ <>
264
+ {context.modal ? (
265
+ <DialogContentModal {...contentProps} ref={forwardedRef} />
266
+ ) : (
267
+ <DialogContentNonModal {...contentProps} ref={forwardedRef} />
268
+ )}
269
+ </>
270
+ )
271
+ }
272
+ )
273
+
274
+ DialogContent.displayName = CONTENT_NAME
275
+
276
+ /* -----------------------------------------------------------------------------------------------*/
277
+
278
+ interface DialogContentTypeProps
279
+ extends Omit<DialogContentImplProps, 'trapFocus' | 'disableOutsidePointerEvents'> {}
280
+
281
+ const DialogContentModal = React.forwardRef<TamaguiElement, DialogContentTypeProps>(
282
+ ({ __scopeDialog, ...props }: ScopedProps<DialogContentTypeProps>, forwardedRef) => {
283
+ const context = useDialogContext(CONTENT_NAME, __scopeDialog)
284
+ const contentRef = React.useRef<HTMLDivElement>(null)
285
+ const composedRefs = useComposedRefs(forwardedRef, context.contentRef, contentRef)
286
+
287
+ // aria-hide everything except the content (better supported equivalent to setting aria-modal)
288
+ React.useEffect(() => {
289
+ if (!context.open) return
290
+ const content = contentRef.current
291
+ if (content) return hideOthers(content)
292
+ }, [context.open])
293
+
294
+ return (
295
+ <DialogContentImpl
296
+ {...props}
297
+ ref={composedRefs}
298
+ // we make sure focus isn't trapped once `DialogContent` has been closed
299
+ // (closed !== unmounted when animating out)
300
+ trapFocus={context.open}
301
+ disableOutsidePointerEvents
302
+ onCloseAutoFocus={composeEventHandlers(props.onCloseAutoFocus, (event) => {
303
+ event.preventDefault()
304
+ context.triggerRef.current?.focus()
305
+ })}
306
+ onPointerDownOutside={composeEventHandlers(props.onPointerDownOutside, (event) => {
307
+ const originalEvent = event['detail'].originalEvent
308
+ const ctrlLeftClick = originalEvent.button === 0 && originalEvent.ctrlKey === true
309
+ const isRightClick = originalEvent.button === 2 || ctrlLeftClick
310
+ // If the event is a right-click, we shouldn't close because
311
+ // it is effectively as if we right-clicked the `Overlay`.
312
+ if (isRightClick) event.preventDefault()
313
+ })}
314
+ // When focus is trapped, a `focusout` event may still happen.
315
+ // We make sure we don't trigger our `onDismiss` in such case.
316
+ onFocusOutside={composeEventHandlers(props.onFocusOutside, (event) =>
317
+ event.preventDefault()
318
+ )}
319
+ />
320
+ )
321
+ }
322
+ )
323
+
324
+ /* -----------------------------------------------------------------------------------------------*/
325
+
326
+ const DialogContentNonModal = React.forwardRef<TamaguiElement, DialogContentTypeProps>(
327
+ (props: ScopedProps<DialogContentTypeProps>, forwardedRef) => {
328
+ const context = useDialogContext(CONTENT_NAME, props.__scopeDialog)
329
+ const hasInteractedOutsideRef = React.useRef(false)
330
+
331
+ return (
332
+ <DialogContentImpl
333
+ {...props}
334
+ ref={forwardedRef}
335
+ trapFocus={false}
336
+ disableOutsidePointerEvents={false}
337
+ onCloseAutoFocus={(event) => {
338
+ props.onCloseAutoFocus?.(event)
339
+
340
+ if (!event.defaultPrevented) {
341
+ if (!hasInteractedOutsideRef.current) context.triggerRef.current?.focus()
342
+ // Always prevent auto focus because we either focus manually or want user agent focus
343
+ event.preventDefault()
344
+ }
345
+
346
+ hasInteractedOutsideRef.current = false
347
+ }}
348
+ onInteractOutside={(event) => {
349
+ props.onInteractOutside?.(event)
350
+
351
+ if (!event.defaultPrevented) hasInteractedOutsideRef.current = true
352
+
353
+ // Prevent dismissing when clicking the trigger.
354
+ // As the trigger is already setup to close, without doing so would
355
+ // cause it to close and immediately open.
356
+ //
357
+ // We use `onInteractOutside` as some browsers also
358
+ // focus on pointer down, creating the same issue.
359
+ const target = event.target as HTMLElement
360
+ const trigger = context.triggerRef.current
361
+ if (!(trigger instanceof HTMLElement)) return
362
+ const targetIsTrigger = trigger.contains(target)
363
+ if (targetIsTrigger) event.preventDefault()
364
+ }}
365
+ />
366
+ )
367
+ }
368
+ )
369
+
370
+ /* -----------------------------------------------------------------------------------------------*/
371
+
372
+ type DialogContentImplProps = DialogContentFrameProps &
373
+ Omit<DismissableProps, 'onDismiss'> & {
374
+ /**
375
+ * When `true`, focus cannot escape the `Content` via keyboard,
376
+ * pointer, or a programmatic focus.
377
+ * @defaultValue false
378
+ */
379
+ trapFocus?: FocusScopeProps['trapped']
380
+
381
+ /**
382
+ * Event handler called when auto-focusing on open.
383
+ * Can be prevented.
384
+ */
385
+ onOpenAutoFocus?: FocusScopeProps['onMountAutoFocus']
386
+
387
+ /**
388
+ * Event handler called when auto-focusing on close.
389
+ * Can be prevented.
390
+ */
391
+ onCloseAutoFocus?: FocusScopeProps['onUnmountAutoFocus']
392
+ }
393
+
394
+ const DialogContentImpl = React.forwardRef<TamaguiElement, DialogContentImplProps>(
395
+ (props: ScopedProps<DialogContentImplProps>, forwardedRef) => {
396
+ const {
397
+ __scopeDialog,
398
+ trapFocus,
399
+ onOpenAutoFocus,
400
+ onCloseAutoFocus,
401
+ disableOutsidePointerEvents,
402
+ onEscapeKeyDown,
403
+ onPointerDownOutside,
404
+ onFocusOutside,
405
+ onInteractOutside,
406
+ ...contentProps
407
+ } = props
408
+ const context = useDialogContext(CONTENT_NAME, __scopeDialog)
409
+ const contentRef = React.useRef<HTMLDivElement>(null)
410
+ const composedRefs = useComposedRefs(forwardedRef, contentRef)
411
+
412
+ return (
413
+ <>
414
+ <FocusScope
415
+ loop
416
+ trapped={trapFocus}
417
+ onMountAutoFocus={onOpenAutoFocus}
418
+ onUnmountAutoFocus={onCloseAutoFocus}
419
+ >
420
+ <Dismissable
421
+ disableOutsidePointerEvents={disableOutsidePointerEvents}
422
+ onEscapeKeyDown={onEscapeKeyDown}
423
+ onPointerDownOutside={onPointerDownOutside}
424
+ onFocusOutside={onFocusOutside}
425
+ onInteractOutside={onInteractOutside}
426
+ // @ts-ignore
427
+ ref={composedRefs}
428
+ onDismiss={() => context.onOpenChange(false)}
429
+ >
430
+ <DialogContentFrame
431
+ id={context.contentId}
432
+ aria-describedby={context.descriptionId}
433
+ aria-labelledby={context.titleId}
434
+ data-state={getState(context.open)}
435
+ {...contentProps}
436
+ />
437
+ </Dismissable>
438
+ </FocusScope>
439
+ {process.env.NODE_ENV === 'development' && (
440
+ <>
441
+ <TitleWarning titleId={context.titleId} />
442
+ <DescriptionWarning contentRef={contentRef} descriptionId={context.descriptionId} />
443
+ </>
444
+ )}
445
+ </>
446
+ )
447
+ }
448
+ )
449
+
450
+ /* -------------------------------------------------------------------------------------------------
451
+ * DialogTitle
452
+ * -----------------------------------------------------------------------------------------------*/
453
+
454
+ const TITLE_NAME = 'DialogTitle'
455
+ const DialogTitleFrame = styled(H2, {
456
+ name: TITLE_NAME,
457
+ })
458
+
459
+ type DialogTitleProps = GetProps<typeof DialogTitleFrame>
460
+
461
+ const DialogTitle = React.forwardRef<TamaguiElement, DialogTitleProps>(
462
+ (props: ScopedProps<DialogTitleProps>, forwardedRef) => {
463
+ const { __scopeDialog, ...titleProps } = props
464
+ const context = useDialogContext(TITLE_NAME, __scopeDialog)
465
+ return <DialogTitleFrame id={context.titleId} {...titleProps} ref={forwardedRef} />
466
+ }
467
+ )
468
+
469
+ DialogTitle.displayName = TITLE_NAME
470
+
471
+ /* -------------------------------------------------------------------------------------------------
472
+ * DialogDescription
473
+ * -----------------------------------------------------------------------------------------------*/
474
+
475
+ const DialogDescriptionFrame = styled(Paragraph, {
476
+ name: 'DialogDescription',
477
+ })
478
+
479
+ type DialogDescriptionProps = GetProps<typeof DialogDescriptionFrame>
480
+
481
+ const DESCRIPTION_NAME = 'DialogDescription'
482
+
483
+ const DialogDescription = React.forwardRef<TamaguiElement, DialogDescriptionProps>(
484
+ (props: ScopedProps<DialogDescriptionProps>, forwardedRef) => {
485
+ const { __scopeDialog, ...descriptionProps } = props
486
+ const context = useDialogContext(DESCRIPTION_NAME, __scopeDialog)
487
+ return (
488
+ <DialogDescriptionFrame id={context.descriptionId} {...descriptionProps} ref={forwardedRef} />
489
+ )
490
+ }
491
+ )
492
+
493
+ DialogDescription.displayName = DESCRIPTION_NAME
494
+
495
+ /* -------------------------------------------------------------------------------------------------
496
+ * DialogClose
497
+ * -----------------------------------------------------------------------------------------------*/
498
+
499
+ const CLOSE_NAME = 'DialogClose'
500
+
501
+ type DialogCloseProps = YStackProps
502
+
503
+ const DialogClose = React.forwardRef<TamaguiElement, DialogCloseProps>(
504
+ (props: ScopedProps<DialogCloseProps>, forwardedRef) => {
505
+ const { __scopeDialog, ...closeProps } = props
506
+ const context = useDialogContext(CLOSE_NAME, __scopeDialog)
507
+ return (
508
+ <YStack
509
+ tag="button"
510
+ {...closeProps}
511
+ ref={forwardedRef}
512
+ onPress={composeEventHandlers(props.onPress, () => context.onOpenChange(false))}
513
+ />
514
+ )
515
+ }
516
+ )
517
+
518
+ DialogClose.displayName = CLOSE_NAME
519
+
520
+ /* -----------------------------------------------------------------------------------------------*/
521
+
522
+ function getState(open: boolean) {
523
+ return open ? 'open' : 'closed'
524
+ }
525
+
526
+ const TITLE_WARNING_NAME = 'DialogTitleWarning'
527
+
528
+ const [WarningProvider, useWarningContext] = createContext(TITLE_WARNING_NAME, {
529
+ contentName: CONTENT_NAME,
530
+ titleName: TITLE_NAME,
531
+ docsSlug: 'dialog',
532
+ })
533
+
534
+ type TitleWarningProps = { titleId?: string }
535
+
536
+ const TitleWarning: React.FC<TitleWarningProps> = ({ titleId }) => {
537
+ const titleWarningContext = useWarningContext(TITLE_WARNING_NAME)
538
+
539
+ const MESSAGE = `\`${titleWarningContext.contentName}\` requires a \`${titleWarningContext.titleName}\` for the component to be accessible for screen reader users.
540
+
541
+ If you want to hide the \`${titleWarningContext.titleName}\`, you can wrap it with our VisuallyHidden component.
542
+
543
+ For more information, see https://radix-ui.com/primitives/docs/components/${titleWarningContext.docsSlug}`
544
+
545
+ React.useEffect(() => {
546
+ if (!isWeb) return
547
+ if (titleId) {
548
+ const hasTitle = document.getElementById(titleId)
549
+ if (!hasTitle) throw new Error(MESSAGE)
550
+ }
551
+ }, [MESSAGE, titleId])
552
+
553
+ return null
554
+ }
555
+
556
+ const DESCRIPTION_WARNING_NAME = 'DialogDescriptionWarning'
557
+
558
+ type DescriptionWarningProps = {
559
+ contentRef: React.RefObject<TamaguiElement>
560
+ descriptionId?: string
561
+ }
562
+
563
+ const DescriptionWarning: React.FC<DescriptionWarningProps> = ({ contentRef, descriptionId }) => {
564
+ const descriptionWarningContext = useWarningContext(DESCRIPTION_WARNING_NAME)
565
+ const MESSAGE = `Warning: Missing \`Description\` or \`aria-describedby={undefined}\` for {${descriptionWarningContext.contentName}}.`
566
+
567
+ React.useEffect(() => {
568
+ if (!isWeb) return
569
+ const contentNode = contentRef.current
570
+ if (!(contentNode instanceof HTMLElement)) {
571
+ return
572
+ }
573
+ const describedById = contentNode.getAttribute('aria-describedby')
574
+ // if we have an id and the user hasn't set aria-describedby={undefined}
575
+ if (descriptionId && describedById) {
576
+ const hasDescription = document.getElementById(descriptionId)
577
+ if (!hasDescription) console.warn(MESSAGE)
578
+ }
579
+ }, [MESSAGE, contentRef, descriptionId])
580
+
581
+ return null
582
+ }
583
+
584
+ /* -------------------------------------------------------------------------------------------------
585
+ * Dialog
586
+ * -----------------------------------------------------------------------------------------------*/
587
+
588
+ const DialogInner = React.forwardRef<{ open: (val: boolean) => void }, DialogProps>(function Dialog(
589
+ props: ScopedProps<DialogProps>,
590
+ ref
591
+ ) {
592
+ const {
593
+ __scopeDialog,
594
+ children,
595
+ open: openProp,
596
+ defaultOpen = false,
597
+ onOpenChange,
598
+ modal = true,
599
+ allowPinchZoom = false,
600
+ } = props
601
+ const triggerRef = React.useRef<HTMLButtonElement>(null)
602
+ const contentRef = React.useRef<TamaguiElement>(null)
603
+ const [open, setOpen] = useControllableState({
604
+ prop: openProp,
605
+ defaultProp: defaultOpen,
606
+ onChange: onOpenChange,
607
+ })
608
+
609
+ React.useImperativeHandle(
610
+ ref,
611
+ () => ({
612
+ open: setOpen,
613
+ }),
614
+ [setOpen]
615
+ )
616
+
617
+ return (
618
+ <DialogProvider
619
+ scope={__scopeDialog}
620
+ triggerRef={triggerRef}
621
+ contentRef={contentRef}
622
+ contentId={useId() || ''}
623
+ titleId={useId() || ''}
624
+ descriptionId={useId() || ''}
625
+ open={open}
626
+ onOpenChange={setOpen}
627
+ onOpenToggle={React.useCallback(() => setOpen((prevOpen) => !prevOpen), [setOpen])}
628
+ modal={modal}
629
+ allowPinchZoom={allowPinchZoom}
630
+ >
631
+ {children}
632
+ </DialogProvider>
633
+ )
634
+ })
635
+
636
+ const Dialog = withStaticProperties(DialogInner, {
637
+ Trigger: DialogTrigger,
638
+ Portal: DialogPortal,
639
+ Overlay: DialogOverlay,
640
+ Content: DialogContent,
641
+ Title: DialogTitle,
642
+ Description: DialogDescription,
643
+ Close: DialogClose,
644
+ })
645
+
646
+ export {
647
+ createDialogScope,
648
+ //
649
+ Dialog,
650
+ DialogTrigger,
651
+ DialogPortal,
652
+ DialogOverlay,
653
+ DialogContent,
654
+ DialogTitle,
655
+ DialogDescription,
656
+ DialogClose,
657
+ //
658
+ WarningProvider,
659
+ }
660
+ export type {
661
+ DialogProps,
662
+ DialogTriggerProps,
663
+ DialogPortalProps,
664
+ DialogOverlayProps,
665
+ DialogContentProps,
666
+ DialogTitleProps,
667
+ DialogDescriptionProps,
668
+ DialogCloseProps,
669
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './Dialog'