diff --git a/apps/expo-nativewind/app/(components)/dialog.tsx b/apps/expo-nativewind/app/(components)/dialog.tsx index b48ae521..c863fed5 100644 --- a/apps/expo-nativewind/app/(components)/dialog.tsx +++ b/apps/expo-nativewind/app/(components)/dialog.tsx @@ -30,10 +30,8 @@ export default function DialogScreen() { - - + + OK diff --git a/apps/expo-nativewind/components/ui/dialog.tsx b/apps/expo-nativewind/components/ui/dialog.tsx index 75648fb8..da4fd404 100644 --- a/apps/expo-nativewind/components/ui/dialog.tsx +++ b/apps/expo-nativewind/components/ui/dialog.tsx @@ -1,7 +1,10 @@ +import { Platform, View } from '@rn-primitives/core'; import * as DialogPrimitive from '@rn-primitives/dialog'; +import { mergeProps } from '@rn-primitives/utils'; import * as React from 'react'; -import { Platform, StyleSheet, View } from 'react-native'; -import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; +import { FadeIn, FadeOut, ZoomIn, ZoomOut } from 'react-native-reanimated'; +import { buttonTextVariants, buttonVariants } from '~/components/ui/button'; +import { TextClassContext } from '~/components/ui/text'; import { X } from '~/lib/icons/X'; import { cn } from '~/lib/utils'; @@ -11,98 +14,83 @@ const DialogTrigger = DialogPrimitive.Trigger; const DialogPortal = DialogPrimitive.Portal; -const DialogClose = DialogPrimitive.Close; +const OVERLAY_NATIVE_PROPS = { + isAnimated: true, + entering: FadeIn, + exiting: FadeOut.duration(150), +}; -const DialogOverlayWeb = ({ - ref, - className, - ...props -}: React.ComponentPropsWithoutRef & { - ref?: React.RefObject>; -}) => { - const { open } = DialogPrimitive.useRootContext(); +function DialogOverlay({ className, native, ...props }: DialogPrimitive.OverlayProps) { return ( ); -}; +} -DialogOverlayWeb.displayName = 'DialogOverlayWeb'; +const CONTENT_NATIVE_PROPS = { + isAnimated: true, + entering: ZoomIn.duration(200).withInitialValues({ transform: [{ scale: 0.85 }] }), + exiting: ZoomOut.duration(400), +}; -const DialogOverlayNative = ({ - ref, +function DialogContent({ className, children, + native: { portalHost, ...nativeProp } = {}, ...props -}: React.ComponentPropsWithoutRef & { - ref?: React.RefObject>; -}) => { - return ( - - - <>{children} - - - ); -}; - -DialogOverlayNative.displayName = 'DialogOverlayNative'; - -const DialogOverlay = Platform.select({ - web: DialogOverlayWeb, - default: DialogOverlayNative, -}); - -const DialogContent = ({ ref, className, children, portalHost, ...props }) => { +}: Omit & { + native?: DialogPrimitive.ContentProps['native'] & { portalHost?: string }; +}) { const { open } = DialogPrimitive.useRootContext(); return ( - + - - {children} - + {/* DialogPrimitive.Content uses `nativeID` for accessibility, so it prevents the entering animation from working https://docs.swmansion.com/react-native-reanimated/docs/layout-animations/entering-exiting-animations/#remarks */} + - - + {children} + + + + ); -}; -DialogContent.displayName = DialogPrimitive.Content.displayName; +} const DialogHeader = ({ className, ...props }: React.ComponentPropsWithoutRef) => ( ); -DialogHeader.displayName = 'DialogHeader'; const DialogFooter = ({ className, ...props }: React.ComponentPropsWithoutRef) => ( ); -DialogFooter.displayName = 'DialogFooter'; -const DialogTitle = ({ - ref, - className, - ...props -}: React.ComponentPropsWithoutRef & { - ref?: React.RefObject>; -}) => ( - -); -DialogTitle.displayName = DialogPrimitive.Title.displayName; +function DialogTitle({ className, ...props }: DialogPrimitive.TitleProps) { + return ( + + ); +} -const DialogDescription = ({ - ref, - className, - ...props -}: React.ComponentPropsWithoutRef & { - ref?: React.RefObject>; -}) => ( - +function DialogDescription({ className, ...props }: DialogPrimitive.DescriptionProps) { + return ( + + ); +} + +const DialogClose = ({ className, ...props }: DialogPrimitive.CloseProps) => ( + + + ); -DialogDescription.displayName = DialogPrimitive.Description.displayName; export { Dialog, diff --git a/apps/nextjs-nativewind/src/app/page.tsx b/apps/nextjs-nativewind/src/app/page.tsx index 1fc47cc3..5d550962 100644 --- a/apps/nextjs-nativewind/src/app/page.tsx +++ b/apps/nextjs-nativewind/src/app/page.tsx @@ -1,4 +1,5 @@ import { View } from '@rn-primitives/core'; +import { CollapsibleExample } from '~/components/CollapsibleExample'; import { Core } from '~/components/core'; import { ToggleExample } from '~/components/ToggleExample'; import { ToggleGroupExample } from '~/components/ToggleGroupExample'; @@ -22,7 +23,6 @@ import { import { AspectRatio } from '~/components/ui/aspect-ratio'; import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar'; import { Button } from '~/components/ui/button'; -import { CollapsibleExample } from '~/components/CollapsibleExample'; import { Dialog, DialogClose, @@ -103,6 +103,7 @@ export default function Home() { + @@ -110,7 +111,6 @@ export default function Home() { {/* - @@ -243,10 +243,8 @@ function DialogExample() { - - + + OK diff --git a/apps/nextjs-nativewind/src/components/ui/dialog.tsx b/apps/nextjs-nativewind/src/components/ui/dialog.tsx index 59b86507..f30608b8 100644 --- a/apps/nextjs-nativewind/src/components/ui/dialog.tsx +++ b/apps/nextjs-nativewind/src/components/ui/dialog.tsx @@ -1,9 +1,12 @@ 'use client'; +import { Platform, View } from '@rn-primitives/core'; import * as DialogPrimitive from '@rn-primitives/dialog'; +import { mergeProps } from '@rn-primitives/utils'; import * as React from 'react'; -import { Platform, StyleSheet, View } from 'react-native'; -import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; +import { FadeIn, FadeOut, ZoomIn, ZoomOut } from 'react-native-reanimated'; +import { buttonTextVariants, buttonVariants } from '~/components/ui/button'; +import { TextClassContext } from '~/components/ui/text'; import { X } from '~/lib/icons/X'; import { cn } from '~/lib/utils'; @@ -13,98 +16,83 @@ const DialogTrigger = DialogPrimitive.Trigger; const DialogPortal = DialogPrimitive.Portal; -const DialogClose = DialogPrimitive.Close; +const OVERLAY_NATIVE_PROPS = { + isAnimated: true, + entering: FadeIn, + exiting: FadeOut.duration(150), +}; -const DialogOverlayWeb = ({ - ref, - className, - ...props -}: React.ComponentPropsWithoutRef & { - ref?: React.RefObject>; -}) => { - const { open } = DialogPrimitive.useRootContext(); +function DialogOverlay({ className, native, ...props }: DialogPrimitive.OverlayProps) { return ( ); -}; +} -DialogOverlayWeb.displayName = 'DialogOverlayWeb'; +const CONTENT_NATIVE_PROPS = { + isAnimated: true, + entering: ZoomIn.duration(200).withInitialValues({ transform: [{ scale: 0.85 }] }), + exiting: ZoomOut.duration(400), +}; -const DialogOverlayNative = ({ - ref, +function DialogContent({ className, children, + native: { portalHost, ...nativeProp } = {}, ...props -}: React.ComponentPropsWithoutRef & { - ref?: React.RefObject>; -}) => { - return ( - - - <>{children} - - - ); -}; - -DialogOverlayNative.displayName = 'DialogOverlayNative'; - -const DialogOverlay = Platform.select({ - web: DialogOverlayWeb, - default: DialogOverlayNative, -}); - -const DialogContent = ({ ref, className, children, portalHost, ...props }) => { +}: Omit & { + native?: DialogPrimitive.ContentProps['native'] & { portalHost?: string }; +}) { const { open } = DialogPrimitive.useRootContext(); return ( - + - - {children} - + {/* DialogPrimitive.Content uses `nativeID` for accessibility, so it prevents the entering animation from working https://docs.swmansion.com/react-native-reanimated/docs/layout-animations/entering-exiting-animations/#remarks */} + - - + {children} + + + + ); -}; -DialogContent.displayName = DialogPrimitive.Content.displayName; +} const DialogHeader = ({ className, ...props }: React.ComponentPropsWithoutRef) => ( ); -DialogHeader.displayName = 'DialogHeader'; const DialogFooter = ({ className, ...props }: React.ComponentPropsWithoutRef) => ( ); -DialogFooter.displayName = 'DialogFooter'; -const DialogTitle = ({ - ref, - className, - ...props -}: React.ComponentPropsWithoutRef & { - ref?: React.RefObject>; -}) => ( - -); -DialogTitle.displayName = DialogPrimitive.Title.displayName; +function DialogTitle({ className, ...props }: DialogPrimitive.TitleProps) { + return ( + + ); +} -const DialogDescription = ({ - ref, - className, - ...props -}: React.ComponentPropsWithoutRef & { - ref?: React.RefObject>; -}) => ( - +function DialogDescription({ className, ...props }: DialogPrimitive.DescriptionProps) { + return ( + + ); +} + +const DialogClose = ({ className, ...props }: DialogPrimitive.CloseProps) => ( + + + ); -DialogDescription.displayName = DialogPrimitive.Description.displayName; export { Dialog, diff --git a/apps/nextjs-no-rn/package.json b/apps/nextjs-no-rn/package.json index 3f375236..2e1aa9b0 100644 --- a/apps/nextjs-no-rn/package.json +++ b/apps/nextjs-no-rn/package.json @@ -17,6 +17,7 @@ "@rn-primitives/checkbox": "workspace:*", "@rn-primitives/collapsible": "workspace:*", "@rn-primitives/core": "workspace:*", + "@rn-primitives/dialog": "workspace:*", "@rn-primitives/alert-dialog": "workspace:*", "@rn-primitives/aspect-ratio": "workspace:*", "@rn-primitives/avatar": "workspace:*", diff --git a/apps/nextjs-no-rn/src/app/page.tsx b/apps/nextjs-no-rn/src/app/page.tsx index ce591189..77eb5702 100644 --- a/apps/nextjs-no-rn/src/app/page.tsx +++ b/apps/nextjs-no-rn/src/app/page.tsx @@ -23,6 +23,16 @@ import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar'; import { Button } from '~/components/ui/button'; import { Checkbox } from '~/components/ui/checkbox'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '~/components/ui/collapsible'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '~/components/ui/dialog'; import { Label } from '~/components/ui/label'; import { Progress } from '~/components/ui/progress'; import { Separator } from '~/components/ui/separator'; @@ -47,6 +57,7 @@ export default function Home() { + @@ -196,6 +207,32 @@ function CollapsibleExample() { ); } +function DialogExample() { + return ( + + + + + + + Edit profile + + Make changes to your profile here. Click save when you're done. + + + + + + OK + + + + + ); +} + function LabelExample() { return ( diff --git a/apps/nextjs-no-rn/src/components/ui/alert-dialog.tsx b/apps/nextjs-no-rn/src/components/ui/alert-dialog.tsx index e34efc39..b5155ac4 100644 --- a/apps/nextjs-no-rn/src/components/ui/alert-dialog.tsx +++ b/apps/nextjs-no-rn/src/components/ui/alert-dialog.tsx @@ -3,9 +3,9 @@ import * as AlertDialogPrimitive from '@rn-primitives/alert-dialog'; import { Platform, View } from '@rn-primitives/core'; import { FadeIn, FadeOut, ZoomIn, ZoomOut } from '@rn-primitives/core/native-only-reanimated'; +import { mergeProps } from '@rn-primitives/utils'; import * as React from 'react'; import { Button } from '~/components/ui/button'; -import { mergeProps } from '@rn-primitives/utils'; import { cn } from '~/lib/utils'; const AlertDialog = AlertDialogPrimitive.Root; diff --git a/apps/nextjs-no-rn/src/components/ui/dialog.tsx b/apps/nextjs-no-rn/src/components/ui/dialog.tsx new file mode 100644 index 00000000..9091143c --- /dev/null +++ b/apps/nextjs-no-rn/src/components/ui/dialog.tsx @@ -0,0 +1,141 @@ +'use client'; + +import { Platform, View } from '@rn-primitives/core'; +import { FadeIn, FadeOut, ZoomIn, ZoomOut } from '@rn-primitives/core/native-only-reanimated'; +import * as DialogPrimitive from '@rn-primitives/dialog'; +import { mergeProps } from '@rn-primitives/utils'; +import { X } from 'lucide-react'; +import * as React from 'react'; +import { Button } from '~/components/ui/button'; +import { cn } from '~/lib/utils'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const OVERLAY_NATIVE_PROPS = { + isAnimated: true, + entering: FadeIn, + exiting: FadeOut.duration(150), +}; + +function DialogOverlay({ className, native, ...props }: DialogPrimitive.OverlayProps) { + return ( + + ); +} + +const CONTENT_NATIVE_PROPS = { + isAnimated: true, + entering: ZoomIn.duration(200).withInitialValues({ transform: [{ scale: 0.85 }] }), + exiting: ZoomOut.duration(400), +}; + +function DialogContent({ + className, + children, + native: { portalHost, ...nativeProp } = {}, + ...props +}: Omit & { + native?: DialogPrimitive.ContentProps['native'] & { portalHost?: string }; +}) { + const { open } = DialogPrimitive.useRootContext(); + return ( + + + + {/* DialogPrimitive.Content uses `nativeID` for accessibility, so it prevents the entering animation from working https://docs.swmansion.com/react-native-reanimated/docs/layout-animations/entering-exiting-animations/#remarks */} + + {children} + + + + + + + + ); +} + +const DialogHeader = ({ className, ...props }: React.ComponentPropsWithoutRef) => ( + +); + +const DialogFooter = ({ className, ...props }: React.ComponentPropsWithoutRef) => ( + +); + +function DialogTitle({ className, ...props }: DialogPrimitive.TitleProps) { + return ( + + ); +} + +function DialogDescription({ className, ...props }: DialogPrimitive.DescriptionProps) { + return ( + + ); +} + +const DialogClose = ({ className, ...props }: DialogPrimitive.CloseProps) => ( + +); + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/apps/vite-tanstack-router/package.json b/apps/vite-tanstack-router/package.json index 18aa04f1..541afd0a 100644 --- a/apps/vite-tanstack-router/package.json +++ b/apps/vite-tanstack-router/package.json @@ -18,6 +18,7 @@ "@rn-primitives/avatar": "workspace:*", "@rn-primitives/checkbox": "workspace:*", "@rn-primitives/collapsible": "workspace:*", + "@rn-primitives/dialog": "workspace:*", "@rn-primitives/label": "workspace:*", "@rn-primitives/progress": "workspace:*", "@rn-primitives/separator": "workspace:*", diff --git a/apps/vite-tanstack-router/src/components/ui/dialog.tsx b/apps/vite-tanstack-router/src/components/ui/dialog.tsx new file mode 100644 index 00000000..90840eb6 --- /dev/null +++ b/apps/vite-tanstack-router/src/components/ui/dialog.tsx @@ -0,0 +1,141 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import { Platform, View } from '@rn-primitives/core'; +import { FadeIn, FadeOut, ZoomIn, ZoomOut } from '@rn-primitives/core/native-only-reanimated'; +import * as DialogPrimitive from '@rn-primitives/dialog'; +import { mergeProps } from '@rn-primitives/utils'; +import { X } from 'lucide-react'; +import * as React from 'react'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const OVERLAY_NATIVE_PROPS = { + isAnimated: true, + entering: FadeIn, + exiting: FadeOut.duration(150), +}; + +function DialogOverlay({ className, native, ...props }: DialogPrimitive.OverlayProps) { + return ( + + ); +} + +const CONTENT_NATIVE_PROPS = { + isAnimated: true, + entering: ZoomIn.duration(200).withInitialValues({ transform: [{ scale: 0.85 }] }), + exiting: ZoomOut.duration(400), +}; + +function DialogContent({ + className, + children, + native: { portalHost, ...nativeProp } = {}, + ...props +}: Omit & { + native?: DialogPrimitive.ContentProps['native'] & { portalHost?: string }; +}) { + const { open } = DialogPrimitive.useRootContext(); + return ( + + + + {/* DialogPrimitive.Content uses `nativeID` for accessibility, so it prevents the entering animation from working https://docs.swmansion.com/react-native-reanimated/docs/layout-animations/entering-exiting-animations/#remarks */} + + {children} + + + + + + + + ); +} + +const DialogHeader = ({ className, ...props }: React.ComponentPropsWithoutRef) => ( + +); + +const DialogFooter = ({ className, ...props }: React.ComponentPropsWithoutRef) => ( + +); + +function DialogTitle({ className, ...props }: DialogPrimitive.TitleProps) { + return ( + + ); +} + +function DialogDescription({ className, ...props }: DialogPrimitive.DescriptionProps) { + return ( + + ); +} + +const DialogClose = ({ className, ...props }: DialogPrimitive.CloseProps) => ( + +); + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/apps/vite-tanstack-router/src/routes/index.tsx b/apps/vite-tanstack-router/src/routes/index.tsx index 37291c50..1a80ab8d 100644 --- a/apps/vite-tanstack-router/src/routes/index.tsx +++ b/apps/vite-tanstack-router/src/routes/index.tsx @@ -18,6 +18,16 @@ import { } from '@/components/ui/card'; import { Checkbox } from '@/components/ui/checkbox'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; import { Label } from '@/components/ui/label'; import { Progress } from '@/components/ui/progress'; import { Separator } from '@/components/ui/separator'; @@ -51,6 +61,7 @@ export function App() { + @@ -197,6 +208,32 @@ function CollapsibleExample() { ); } +function DialogExample() { + return ( + + + + + + + Edit profile + + Make changes to your profile here. Click save when you're done. + + + + + + OK + + + + + ); +} + function LabelExample() { return ( diff --git a/packages/dialog/package.json b/packages/dialog/package.json index 5b8918b1..aeba3a2b 100644 --- a/packages/dialog/package.json +++ b/packages/dialog/package.json @@ -1,6 +1,6 @@ { "name": "@rn-primitives/dialog", - "version": "1.1.0", + "version": "2.0.0-alpha.1", "description": "Primitive dialog", "license": "MIT", "main": "dist/index.js", @@ -13,11 +13,17 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, - "./dist/dialog": { - "import": "./dist/dialog.mjs", - "require": "./dist/dialog.js", - "types": "./dist/dialog.d.ts", - "default": "./dist/dialog.js" + "./native": { + "import": "./dist/native/index.mjs", + "require": "./dist/native/index.js", + "types": "./dist/native/index.d.ts", + "default": "./dist/native/index.js" + }, + "./web": { + "import": "./dist/web/index.mjs", + "require": "./dist/web/index.js", + "types": "./dist/web/index.d.ts", + "default": "./dist/web/index.js" } }, "files": [ @@ -32,10 +38,12 @@ "pub:release": "pnpm publish --access public" }, "dependencies": { - "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.14", + "@rn-primitives/core": "workspace:*", "@rn-primitives/hooks": "workspace:*", - "@rn-primitives/slot": "workspace:*", - "@rn-primitives/types": "workspace:*" + "@rn-primitives/portal": "workspace:*", + "@rn-primitives/types": "workspace:*", + "@rn-primitives/utils": "workspace:*" }, "devDependencies": { "@rn-primitives/portal": "workspace:*", diff --git a/packages/dialog/src/base-types.ts b/packages/dialog/src/base-types.ts new file mode 100644 index 00000000..dbbf2386 --- /dev/null +++ b/packages/dialog/src/base-types.ts @@ -0,0 +1,37 @@ +import type { + DialogContentProps, + DialogOverlayProps, + DialogPortalProps, + DialogProps, +} from '@radix-ui/react-dialog'; +import type { Prettify } from '@rn-primitives/types'; + +type BaseDialogRootProps = Omit, 'children'>; + +type BaseDialogTriggerProps = {}; + +type BaseDialogPortalProps = Pick; + +type BaseDialogOverlayProps = Pick; + +type BaseDialogCloseProps = {}; + +type BaseDialogTitleProps = {}; + +type BaseDialogDescriptionProps = {}; + +type BaseDialogContentProps = Pick; + +type BaseDialogRootContext = Required> | null; + +export type { + BaseDialogCloseProps, + BaseDialogContentProps, + BaseDialogDescriptionProps, + BaseDialogOverlayProps, + BaseDialogPortalProps, + BaseDialogRootContext, + BaseDialogRootProps, + BaseDialogTitleProps, + BaseDialogTriggerProps, +}; diff --git a/packages/dialog/src/dialog.tsx b/packages/dialog/src/dialog.tsx deleted file mode 100644 index 153214f2..00000000 --- a/packages/dialog/src/dialog.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import { useControllableState } from '@rn-primitives/hooks'; -import { Portal as RNPPortal } from '@rn-primitives/portal'; -import { Slot } from '@rn-primitives/slot'; -import * as React from 'react'; -import { BackHandler, GestureResponderEvent, Pressable, Text, View } from 'react-native'; -import type { - CloseProps, - CloseRef, - ContentProps, - ContentRef, - DescriptionProps, - DescriptionRef, - OverlayProps, - OverlayRef, - PortalProps, - RootContext, - RootProps, - RootRef, - TitleProps, - TitleRef, - TriggerProps, - TriggerRef, -} from './types'; - -const DialogContext = React.createContext<(RootContext & { nativeID: string }) | null>(null); - -const Root = ( - { - ref, - asChild, - open: openProp, - defaultOpen, - onOpenChange: onOpenChangeProp, - ...viewProps - }: RootProps & { - ref: React.RefObject; - } -) => { - const nativeID = React.useId(); - const [open = false, onOpenChange] = useControllableState({ - prop: openProp, - defaultProp: defaultOpen, - onChange: onOpenChangeProp, - }); - - const Component = asChild ? Slot : View; - return ( - - - - ); -}; - -Root.displayName = 'RootNativeDialog'; - -function useRootContext() { - const context = React.useContext(DialogContext); - if (!context) { - throw new Error('Dialog compound components cannot be rendered outside the Dialog component'); - } - return context; -} - -const Trigger = ( - { - ref, - asChild, - onPress: onPressProp, - disabled = false, - ...props - }: TriggerProps & { - ref: React.RefObject; - } -) => { - const { open, onOpenChange } = useRootContext(); - - function onPress(ev: GestureResponderEvent) { - if (disabled) return; - const newValue = !open; - onOpenChange(newValue); - onPressProp?.(ev); - } - - const Component = asChild ? Slot : Pressable; - return ( - - ); -}; - -Trigger.displayName = 'TriggerNativeDialog'; - -/** - * @warning when using a custom ``, you might have to adjust the Content's sideOffset to account for nav elements like headers. - */ -function Portal({ forceMount, hostName, children }: PortalProps) { - const value = useRootContext(); - - if (!forceMount) { - if (!value.open) { - return null; - } - } - - return ( - - {children} - - ); -} - -const Overlay = ( - { - ref, - asChild, - forceMount, - closeOnPress = true, - onPress: OnPressProp, - ...props - }: OverlayProps & { - ref: React.RefObject; - } -) => { - const { open, onOpenChange } = useRootContext(); - - function onPress(ev: GestureResponderEvent) { - if (closeOnPress) { - onOpenChange(!open); - } - OnPressProp?.(ev); - } - - if (!forceMount) { - if (!open) { - return null; - } - } - - const Component = asChild ? Slot : Pressable; - return ; -}; - -Overlay.displayName = 'OverlayNativeDialog'; - -const Content = ( - { - ref, - asChild, - forceMount, - ...props - }: ContentProps & { - ref: React.RefObject; - } -) => { - const { open, nativeID, onOpenChange } = useRootContext(); - - React.useEffect(() => { - const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { - onOpenChange(false); - return true; - }); - - return () => { - backHandler.remove(); - }; - }, []); - - if (!forceMount) { - if (!open) { - return null; - } - } - - const Component = asChild ? Slot : View; - return ( - - ); -}; - -Content.displayName = 'ContentNativeDialog'; - -const Close = ( - { - ref, - asChild, - onPress: onPressProp, - disabled = false, - ...props - }: CloseProps & { - ref: React.RefObject; - } -) => { - const { onOpenChange } = useRootContext(); - - function onPress(ev: GestureResponderEvent) { - if (disabled) return; - onOpenChange(false); - onPressProp?.(ev); - } - - const Component = asChild ? Slot : Pressable; - return ( - - ); -}; - -Close.displayName = 'CloseNativeDialog'; - -const Title = ( - { - ref, - ...props - }: TitleProps & { - ref: React.RefObject; - } -) => { - const { nativeID } = useRootContext(); - return ; -}; - -Title.displayName = 'TitleNativeDialog'; - -const Description = ( - { - ref, - ...props - }: DescriptionProps & { - ref: React.RefObject; - } -) => { - const { nativeID } = useRootContext(); - return ; -}; - -Description.displayName = 'DescriptionNativeDialog'; - -export { Close, Content, Description, Overlay, Portal, Root, Title, Trigger, useRootContext }; - -function onStartShouldSetResponder() { - return true; -} diff --git a/packages/dialog/src/dialog.web.tsx b/packages/dialog/src/dialog.web.tsx deleted file mode 100644 index ed688126..00000000 --- a/packages/dialog/src/dialog.web.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import * as Dialog from '@radix-ui/react-dialog'; -import { - useAugmentedRef, - useControllableState, - useIsomorphicLayoutEffect, -} from '@rn-primitives/hooks'; -import { Slot } from '@rn-primitives/slot'; -import * as React from 'react'; -import { Pressable, Text, View, type GestureResponderEvent } from 'react-native'; -import type { - CloseProps, - CloseRef, - ContentProps, - ContentRef, - DescriptionProps, - DescriptionRef, - OverlayProps, - OverlayRef, - PortalProps, - RootContext, - RootProps, - RootRef, - TitleProps, - TitleRef, - TriggerProps, - TriggerRef, -} from './types'; - -const DialogContext = React.createContext(null); - -const Root = ( - { - ref, - asChild, - open: openProp, - defaultOpen, - onOpenChange: onOpenChangeProp, - ...viewProps - }: RootProps & { - ref: React.RefObject; - } -) => { - const [open = false, onOpenChange] = useControllableState({ - prop: openProp, - defaultProp: defaultOpen, - onChange: onOpenChangeProp, - }); - const Component = asChild ? Slot : View; - return ( - - - - - - ); -}; - -Root.displayName = 'RootWebDialog'; - -function useRootContext() { - const context = React.useContext(DialogContext); - if (!context) { - throw new Error('Dialog compound components cannot be rendered outside the Dialog component'); - } - return context; -} - -const Trigger = ( - { - ref, - asChild, - onPress: onPressProp, - role: _role, - disabled, - ...props - }: TriggerProps & { - ref: React.RefObject; - } -) => { - const augmentedRef = useAugmentedRef({ ref }); - const { onOpenChange, open } = useRootContext(); - function onPress(ev: GestureResponderEvent) { - if (onPressProp) { - onPressProp(ev); - } - onOpenChange(!open); - } - - useIsomorphicLayoutEffect(() => { - if (augmentedRef.current) { - const augRef = augmentedRef.current as unknown as HTMLButtonElement; - augRef.dataset.state = open ? 'open' : 'closed'; - augRef.type = 'button'; - } - }, [open]); - - const Component = asChild ? Slot : Pressable; - return ( - - - - ); -}; - -Trigger.displayName = 'TriggerWebDialog'; - -function Portal({ forceMount, container, children }: PortalProps) { - return ; -} - -const Overlay = ( - { - ref, - asChild, - forceMount, - ...props - }: OverlayProps & { - ref: React.RefObject; - } -) => { - const Component = asChild ? Slot : Pressable; - return ( - - - - ); -}; - -Overlay.displayName = 'OverlayWebDialog'; - -const Content = ( - { - ref, - asChild, - forceMount, - onOpenAutoFocus, - onCloseAutoFocus, - onEscapeKeyDown, - onInteractOutside, - onPointerDownOutside, - ...props - }: ContentProps & { - ref: React.RefObject; - } -) => { - const Component = asChild ? Slot : View; - return ( - - - - ); -}; - -Content.displayName = 'ContentWebDialog'; - -const Close = ( - { - ref, - asChild, - onPress: onPressProp, - disabled, - ...props - }: CloseProps & { - ref: React.RefObject; - } -) => { - const augmentedRef = useAugmentedRef({ ref }); - const { onOpenChange, open } = useRootContext(); - - function onPress(ev: GestureResponderEvent) { - if (onPressProp) { - onPressProp(ev); - } - onOpenChange(!open); - } - - useIsomorphicLayoutEffect(() => { - if (augmentedRef.current) { - const augRef = augmentedRef.current as unknown as HTMLButtonElement; - augRef.type = 'button'; - } - }, []); - - const Component = asChild ? Slot : Pressable; - return ( - <> - - - - - ); -}; - -Close.displayName = 'CloseWebDialog'; - -const Title = ( - { - ref, - asChild, - ...props - }: TitleProps & { - ref: React.RefObject; - } -) => { - const Component = asChild ? Slot : Text; - return ( - - - - ); -}; - -Title.displayName = 'TitleWebDialog'; - -const Description = ( - { - ref, - asChild, - ...props - }: DescriptionProps & { - ref: React.RefObject; - } -) => { - const Component = asChild ? Slot : Text; - return ( - - - - ); -}; - -Description.displayName = 'DescriptionWebDialog'; - -export { Close, Content, Description, Overlay, Portal, Root, Title, Trigger, useRootContext }; diff --git a/packages/dialog/src/index.ts b/packages/dialog/src/index.ts index 67979f9d..9470bad0 100644 --- a/packages/dialog/src/index.ts +++ b/packages/dialog/src/index.ts @@ -1,2 +1,22 @@ -export * from './dialog'; -export * from './types'; +export { + Close, + Content, + Description, + Overlay, + Portal, + Root, + Title, + Trigger, + useRootContext, +} from './universal'; + +export type { + CloseProps, + ContentProps, + DescriptionProps, + OverlayProps, + PortalProps, + RootProps, + TitleProps, + TriggerProps, +} from './universal'; diff --git a/packages/dialog/src/native/dialog-native.native.tsx b/packages/dialog/src/native/dialog-native.native.tsx new file mode 100644 index 00000000..6ec06a3b --- /dev/null +++ b/packages/dialog/src/native/dialog-native.native.tsx @@ -0,0 +1,196 @@ +import { Pressable, Text, View } from '@rn-primitives/core/dist/native'; +import { useControllableState } from '@rn-primitives/hooks'; +import { Portal as RNPPortal } from '@rn-primitives/portal'; +import * as React from 'react'; +import { BackHandler, type GestureResponderEvent } from 'react-native'; +import { RootContext, useRootContext } from '../utils/contexts'; +import type { + CloseProps, + ContentProps, + DescriptionProps, + OverlayProps, + PortalProps, + RootProps, + TitleProps, + TriggerProps, +} from './types'; + +const RootInternalContext = React.createContext<{ nativeID: string } | null>(null); + +function Root({ + open: openProp, + defaultOpen, + onOpenChange: onOpenChangeProp, + children, +}: RootProps) { + const nativeID = React.useId(); + const [open = false, onOpenChange] = useControllableState({ + prop: openProp, + defaultProp: defaultOpen, + onChange: onOpenChangeProp, + }); + return ( + + + <>{children} + + + ); +} + +function useRootInternalContext() { + const context = React.useContext(RootInternalContext); + if (!context) { + throw new Error( + 'Dialog Internal compound components cannot be rendered outside the Dialog component' + ); + } + return context; +} + +function Trigger({ onPress: onPressProp, disabled, ...props }: TriggerProps) { + const { open: value, onOpenChange } = useRootContext(); + + const onPress = React.useCallback( + (ev: GestureResponderEvent) => { + onOpenChange(!value); + if (typeof onPressProp === 'function') { + onPressProp(ev); + } + }, + [onOpenChange, onPressProp, value] + ); + + return ( + + ); +} + +function Portal({ forceMount, hostName, children }: PortalProps) { + const internalValue = useRootInternalContext(); + const value = useRootContext(); + + if (!forceMount) { + if (!value.open) { + return null; + } + } + + return ( + + + {children} + + + ); +} + +function Overlay({ + forceMount, + onAccessibilityEscape: onAccessibilityEscapeProp, + ...props +}: OverlayProps) { + const { open: value, onOpenChange } = useRootContext(); + + const onAccessibilityEscape = React.useCallback(() => { + if (typeof onAccessibilityEscape === 'function') { + onAccessibilityEscape(); + } + onOpenChange(false); + }, [onAccessibilityEscapeProp, onOpenChange]); + + if (!forceMount) { + if (!value) { + return null; + } + } + + return ; +} + +function Content({ + forceMount, + onAccessibilityEscape: onAccessibilityEscapeProp, + ...props +}: ContentProps) { + const { open: value, onOpenChange } = useRootContext(); + const { nativeID } = useRootInternalContext(); + + React.useEffect(() => { + const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { + onOpenChange(false); + return true; + }); + + return () => { + backHandler.remove(); + }; + }, []); + + const onAccessibilityEscape = React.useCallback(() => { + if (typeof onAccessibilityEscapeProp === 'function') { + onAccessibilityEscapeProp(); + } + onOpenChange(false); + }, [onAccessibilityEscapeProp, onOpenChange]); + + if (!forceMount) { + if (!value) { + return null; + } + } + + return ( + + ); +} + +function Close({ onPress: onPressProp, disabled, ...props }: CloseProps) { + const { onOpenChange } = useRootContext(); + + const onPress = React.useCallback( + (ev: GestureResponderEvent) => { + onOpenChange(false); + if (typeof onPressProp === 'function') { + onPressProp(ev); + } + }, + [onOpenChange, onPressProp] + ); + + return ( + + ); +} + +function Title(props: TitleProps) { + const { nativeID } = useRootInternalContext(); + return ; +} + +function Description({ ...props }: DescriptionProps) { + const { nativeID } = useRootInternalContext(); + return ; +} + +export { Close, Content, Description, Overlay, Portal, Root, Title, Trigger, useRootContext }; diff --git a/packages/dialog/src/native/dialog-native.tsx b/packages/dialog/src/native/dialog-native.tsx new file mode 100644 index 00000000..3841fd57 --- /dev/null +++ b/packages/dialog/src/native/dialog-native.tsx @@ -0,0 +1,76 @@ +import { RootContextReturnType } from '../utils/contexts'; +import type { + CloseProps, + ContentProps, + DescriptionProps, + OverlayProps, + PortalProps, + RootProps, + TitleProps, + TriggerProps, +} from './types'; + +function Root(props: RootProps) { + if (process.env.NODE_ENV === 'development') { + console.log('`Root` from @rn-primitives/dialog/native is only supported on native.'); + } + return null; +} + +function Trigger(props: TriggerProps) { + if (process.env.NODE_ENV === 'development') { + console.log('`Trigger` from @rn-primitives/dialog/native is only supported on native.'); + } + return null; +} + +function Portal(props: PortalProps) { + if (process.env.NODE_ENV === 'development') { + console.log('`Portal` from @rn-primitives/dialog/native is only supported on native.'); + } + return null; +} + +function Overlay(props: OverlayProps) { + if (process.env.NODE_ENV === 'development') { + console.log('`Overlay` from @rn-primitives/dialog/native is only supported on native.'); + } + return null; +} + +function Content(props: ContentProps) { + if (process.env.NODE_ENV === 'development') { + console.log('`Content` from @rn-primitives/dialog/native is only supported on native.'); + } + return null; +} + +function Close(props: CloseProps) { + if (process.env.NODE_ENV === 'development') { + console.log('`Close` from @rn-primitives/dialog/native is only supported on native.'); + } + return null; +} + +function Title(props: TitleProps) { + if (process.env.NODE_ENV === 'development') { + console.log('`Title` from @rn-primitives/dialog/native is only supported on native.'); + } + return null; +} + +function Description(props: DescriptionProps) { + if (process.env.NODE_ENV === 'development') { + console.log('`Description` from @rn-primitives/dialog/native is only supported on native.'); + } + return null; +} + +const useRootContext = () => { + throw new Error( + 'Cannot access the native useRootContext on the web. Please import from `@rn-primitives/dialog` or `@rn-primitives/dialog/web`' + ); + return {} as RootContextReturnType; +}; + +export { Close, Content, Description, Overlay, Portal, Root, Title, Trigger, useRootContext }; diff --git a/packages/dialog/src/native/index.ts b/packages/dialog/src/native/index.ts new file mode 100644 index 00000000..58d11be3 --- /dev/null +++ b/packages/dialog/src/native/index.ts @@ -0,0 +1,22 @@ +export { + Close, + Content, + Description, + Overlay, + Portal, + Root, + Title, + Trigger, + useRootContext, +} from './dialog-native'; + +export type { + CloseProps, + ContentProps, + DescriptionProps, + OverlayProps, + PortalProps, + RootProps, + TitleProps, + TriggerProps, +} from './types'; diff --git a/packages/dialog/src/native/types.ts b/packages/dialog/src/native/types.ts new file mode 100644 index 00000000..53176fb6 --- /dev/null +++ b/packages/dialog/src/native/types.ts @@ -0,0 +1,49 @@ +import type { PressableProps, TextProps, ViewProps } from '@rn-primitives/core/dist/native'; +import type { + BaseDialogCloseProps, + BaseDialogContentProps, + BaseDialogDescriptionProps, + BaseDialogOverlayProps, + BaseDialogPortalProps, + BaseDialogRootProps, + BaseDialogTitleProps, + BaseDialogTriggerProps, +} from '../base-types'; + +type ClosePropsNativeOnly = PressableProps; +type ContentPropsNativeOnly = ViewProps; +type DescriptionPropsNativeOnly = TextProps; +type OverlayPropsNativeOnly = ViewProps; +type PortalPropsNativeOnly = { + hostName?: string; + children?: React.ReactNode; +}; +type TitlePropsNativeOnly = TextProps; +type TriggerPropsNativeOnly = PressableProps; + +type RootProps = BaseDialogRootProps & { children?: React.ReactNode }; +type CloseProps = ClosePropsNativeOnly & BaseDialogCloseProps; +type ContentProps = ContentPropsNativeOnly & BaseDialogContentProps; +type DescriptionProps = DescriptionPropsNativeOnly & BaseDialogDescriptionProps; +type OverlayProps = OverlayPropsNativeOnly & BaseDialogOverlayProps; +type PortalProps = PortalPropsNativeOnly & BaseDialogPortalProps; +type TitleProps = TitlePropsNativeOnly & BaseDialogTitleProps; +type TriggerProps = TriggerPropsNativeOnly & BaseDialogTriggerProps; + +export type { + CloseProps, + ClosePropsNativeOnly, + ContentProps, + ContentPropsNativeOnly, + DescriptionProps, + DescriptionPropsNativeOnly, + OverlayProps, + OverlayPropsNativeOnly, + PortalProps, + PortalPropsNativeOnly, + RootProps, + TitleProps, + TitlePropsNativeOnly, + TriggerProps, + TriggerPropsNativeOnly, +}; diff --git a/packages/dialog/src/types.ts b/packages/dialog/src/types.ts deleted file mode 100644 index 5cbf6bab..00000000 --- a/packages/dialog/src/types.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { - ForceMountable, - PressableRef, - SlottablePressableProps, - SlottableTextProps, - SlottableViewProps, - TextRef, - ViewRef, -} from '@rn-primitives/types'; - -type RootContext = { - open: boolean; - onOpenChange: (value: boolean) => void; -}; - -type RootProps = SlottableViewProps & { - open?: boolean; - defaultOpen?: boolean; - onOpenChange?: (value: boolean) => void; -}; - -interface PortalProps extends ForceMountable { - children: React.ReactNode; - /** - * Platform: NATIVE ONLY - */ - hostName?: string; - /** - * Platform: WEB ONLY - */ - container?: HTMLElement | null | undefined; -} -type OverlayProps = ForceMountable & - SlottablePressableProps & { - /** - * Platform: NATIVE ONLY - default: true - */ - closeOnPress?: boolean; - }; -type ContentProps = ForceMountable & - SlottableViewProps & { - /** - * Platform: WEB ONLY - */ - onOpenAutoFocus?: (ev: Event) => void; - /** - * Platform: WEB ONLY - */ - onCloseAutoFocus?: (ev: Event) => void; - /** - * Platform: WEB ONLY - */ - onEscapeKeyDown?: (ev: Event) => void; - /** - * Platform: WEB ONLY - */ - onInteractOutside?: (ev: Event) => void; - /** - * Platform: WEB ONLY - */ - onPointerDownOutside?: (ev: Event) => void; - }; - -type TriggerProps = SlottablePressableProps; -type CloseProps = SlottablePressableProps; -type TitleProps = SlottableTextProps; -type DescriptionProps = SlottableTextProps; - -type CloseRef = PressableRef; -type ContentRef = ViewRef; -type DescriptionRef = TextRef; -type OverlayRef = PressableRef; -type RootRef = ViewRef; -type TitleRef = TextRef; -type TriggerRef = PressableRef; - -export type { - CloseProps, - CloseRef, - ContentProps, - ContentRef, - DescriptionProps, - DescriptionRef, - OverlayProps, - OverlayRef, - PortalProps, - RootContext, - RootProps, - RootRef, - TitleProps, - TitleRef, - TriggerProps, - TriggerRef, -}; diff --git a/packages/dialog/src/universal/dialog.tsx b/packages/dialog/src/universal/dialog.tsx new file mode 100644 index 00000000..b0008ba8 --- /dev/null +++ b/packages/dialog/src/universal/dialog.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import { + Close as CloseNative, + type CloseProps as ClosePropsNative, + Content as ContentNative, + Description as DescriptionNative, + Overlay as OverlayNative, + Portal as PortalNative, + Root as RootNative, + Title as TitleNative, + Trigger as TriggerNative, + type TriggerProps as TriggerPropsNative, + useRootContext, +} from '../native'; +import type { + CloseProps, + ContentProps, + DescriptionProps, + OverlayProps, + PortalProps, + RootProps, + TitleProps, + TriggerProps, +} from './types'; + +function Root(props: RootProps) { + return ; +} + +function Content({ web: _web, native, ...props }: ContentProps) { + return ; +} + +function Description({ web: _web, native, ...props }: DescriptionProps) { + return ; +} + +function Overlay({ web: _web, native, ...props }: OverlayProps) { + return ; +} + +function Portal({ web: _web, native, ...props }: PortalProps) { + return ; +} + +function Title({ web: _web, native, ...props }: TitleProps) { + return ; +} + +function Trigger({ ref, web: _web, native, ...props }: TriggerProps) { + return ; +} + +function Close({ ref, web: _web, native, ...props }: CloseProps) { + return ; +} + +export { Close, Content, Description, Overlay, Portal, Root, Title, Trigger, useRootContext }; diff --git a/packages/dialog/src/universal/dialog.web.tsx b/packages/dialog/src/universal/dialog.web.tsx new file mode 100644 index 00000000..842f74e0 --- /dev/null +++ b/packages/dialog/src/universal/dialog.web.tsx @@ -0,0 +1,97 @@ +import { Pressable, Text, View } from '@rn-primitives/core'; +import { mergeProps } from '@rn-primitives/utils'; +import * as React from 'react'; +import { + Close as CloseWeb, + Content as ContentWeb, + Description as DescriptionWeb, + Overlay as OverlayWeb, + Portal as PortalWeb, + Root as RootWeb, + Title as TitleWeb, + Trigger as TriggerWeb, + useRootContext, +} from '../web'; +import type { + CloseProps, + ContentProps, + DescriptionProps, + OverlayProps, + PortalProps, + RootProps, + TitleProps, + TriggerProps, +} from './types'; + +function Root(props: RootProps) { + return ; +} + +function Content({ web, native: _native, style, ...props }: ContentProps) { + if (style) { + return ( + + + + ); + } + return ; +} + +function Description({ web, native: _native, style, ...props }: DescriptionProps) { + if (style) { + return ( + + + + ); + } + + return ; +} + +function Overlay({ web, native: _native, style, ...props }: OverlayProps) { + if (style) { + return ( + + + + ); + } + return ; +} + +function Portal({ web, native: _native, ...props }: PortalProps) { + return ; +} + +function Title({ web, native: _native, style, ...props }: TitleProps) { + if (style) { + return ( + + + + ); + } + return ; +} + +const DEFAULT_PRESSABLE_WEB = { as: 'button' } as const; + +function Trigger({ native: _native, web, ...props }: TriggerProps) { + return ( + + + + ); +} + +function Close({ native: _native, web, ...props }: CloseProps) { + return ( + + + + ); +} + +export { Close, Content, Description, Overlay, Portal, Root, Title, Trigger, useRootContext }; diff --git a/packages/dialog/src/universal/index.ts b/packages/dialog/src/universal/index.ts new file mode 100644 index 00000000..4f0ee069 --- /dev/null +++ b/packages/dialog/src/universal/index.ts @@ -0,0 +1,22 @@ +export { + Close, + Content, + Description, + Overlay, + Portal, + Root, + Title, + Trigger, + useRootContext, +} from './dialog'; + +export type { + CloseProps, + ContentProps, + DescriptionProps, + OverlayProps, + PortalProps, + RootProps, + TitleProps, + TriggerProps, +} from './types'; diff --git a/packages/dialog/src/universal/types.ts b/packages/dialog/src/universal/types.ts new file mode 100644 index 00000000..1d38df82 --- /dev/null +++ b/packages/dialog/src/universal/types.ts @@ -0,0 +1,106 @@ +import type { + PressablePropsUniversal, + TextPropsUniversal, + ViewPropsUniversal, +} from '@rn-primitives/core'; +import type { Prettify } from '@rn-primitives/types'; +import type { + BaseDialogCloseProps, + BaseDialogContentProps, + BaseDialogDescriptionProps, + BaseDialogOverlayProps, + BaseDialogPortalProps, + BaseDialogRootProps, + BaseDialogTitleProps, + BaseDialogTriggerProps, +} from '../base-types'; +import type { + ClosePropsNativeOnly, + ContentPropsNativeOnly, + DescriptionPropsNativeOnly, + OverlayPropsNativeOnly, + PortalPropsNativeOnly, + TitlePropsNativeOnly, + TriggerPropsNativeOnly, +} from '../native/types'; +import type { + ClosePropsWebOnly, + ContentPropsWebOnly, + DescriptionPropsWebOnly, + OverlayPropsWebOnly, + PortalPropsWebOnly, + TitlePropsWebOnly, + TriggerPropsWebOnly, +} from '../web/types'; + +type ContentProps = Prettify< + BaseDialogContentProps & + ViewPropsUniversal & { + native?: ContentPropsNativeOnly; + web?: ContentPropsWebOnly; + } +>; + +type RootProps = Prettify< + BaseDialogRootProps & { + children?: React.ReactNode; + } +>; + +type TriggerProps = Prettify< + BaseDialogTriggerProps & + PressablePropsUniversal & { + native?: TriggerPropsNativeOnly; + web?: TriggerPropsWebOnly; + } +>; + +type CloseProps = Prettify< + BaseDialogCloseProps & + PressablePropsUniversal & { + native?: ClosePropsNativeOnly; + web?: ClosePropsWebOnly; + } +>; + +type DescriptionProps = Prettify< + BaseDialogDescriptionProps & + TextPropsUniversal & { + native?: DescriptionPropsNativeOnly; + web?: DescriptionPropsWebOnly; + } +>; + +type OverlayProps = Prettify< + BaseDialogOverlayProps & + ViewPropsUniversal & { + native?: OverlayPropsNativeOnly; + web?: OverlayPropsWebOnly; + } +>; + +type PortalProps = Prettify< + BaseDialogPortalProps & { + native?: PortalPropsNativeOnly; + web?: PortalPropsWebOnly; + } +>; + +type TitleProps = Prettify< + BaseDialogTitleProps & + TextPropsUniversal & { + native?: TitlePropsNativeOnly; + web?: TitlePropsWebOnly; + } +>; + +export type { + CloseProps, + ContentProps, + DescriptionProps, + OverlayProps, + PortalProps, + RootProps, + TitleProps, + TriggerProps, +}; diff --git a/packages/dialog/src/utils/contexts.ts b/packages/dialog/src/utils/contexts.ts new file mode 100644 index 00000000..d18a8854 --- /dev/null +++ b/packages/dialog/src/utils/contexts.ts @@ -0,0 +1,17 @@ +import * as React from 'react'; +import type { BaseDialogRootContext } from '../base-types'; + +const RootContext = React.createContext(null); +function useRootContext() { + const context = React.useContext(RootContext); + if (!context) { + throw new Error('Dialog compound components cannot be rendered outside the Dialog component'); + } + return context; +} + +type RootContextReturnType = ReturnType; + +export { RootContext, useRootContext }; + +export type { RootContextReturnType }; diff --git a/packages/dialog/src/web/dialog-web.tsx b/packages/dialog/src/web/dialog-web.tsx new file mode 100644 index 00000000..ec6765df --- /dev/null +++ b/packages/dialog/src/web/dialog-web.tsx @@ -0,0 +1,76 @@ +import type { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} from '@radix-ui/react-dialog'; +import type { RootContextReturnType } from '../utils/contexts'; + +const Root = (() => { + if (process.env.NODE_ENV === 'development') { + console.log('`Root` from @rn-primitives/dialog/web is only supported on web.'); + } + return null; +}) as typeof Dialog; + +const Trigger = (() => { + if (process.env.NODE_ENV === 'development') { + console.log('`Trigger` from @rn-primitives/dialog/web is only supported on web.'); + } + return null; +}) as unknown as typeof DialogTrigger; + +const Content = (() => { + if (process.env.NODE_ENV === 'development') { + console.log('`Content` from @rn-primitives/dialog/web is only supported on web.'); + } + return null; +}) as unknown as typeof DialogContent; + +const Close = (() => { + if (process.env.NODE_ENV === 'development') { + console.log('`Close` from @rn-primitives/dialog/web is only supported on web.'); + } + return null; +}) as unknown as typeof DialogClose; + +const Description = (() => { + if (process.env.NODE_ENV === 'development') { + console.log('`Description` from @rn-primitives/dialog/web is only supported on web.'); + } + return null; +}) as unknown as typeof DialogDescription; + +const Overlay = (() => { + if (process.env.NODE_ENV === 'development') { + console.log('`Overlay` from @rn-primitives/dialog/web is only supported on web.'); + } + return null; +}) as unknown as typeof DialogOverlay; + +const Portal = (() => { + if (process.env.NODE_ENV === 'development') { + console.log('`Portal` from @rn-primitives/dialog/web is only supported on web.'); + } + return null; +}) as unknown as typeof DialogPortal; + +const Title = (() => { + if (process.env.NODE_ENV === 'development') { + console.log('`Title` from @rn-primitives/dialog/web is only supported on web.'); + } + return null; +}) as unknown as typeof DialogTitle; + +const useRootContext = () => { + throw new Error( + 'Cannot access the web useRootContext on a web platform. Please import from `@rn-primitives/dialog` or `@rn-primitives/dialog/web`' + ); + return {} as RootContextReturnType; +}; + +export { Close, Content, Description, Overlay, Portal, Root, Title, Trigger, useRootContext }; diff --git a/packages/dialog/src/web/dialog-web.web.tsx b/packages/dialog/src/web/dialog-web.web.tsx new file mode 100644 index 00000000..12e7a218 --- /dev/null +++ b/packages/dialog/src/web/dialog-web.web.tsx @@ -0,0 +1,48 @@ +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogOverlay, + DialogTitle, + DialogTrigger, + Portal, +} from '@radix-ui/react-dialog'; +import { useControllableState } from '@rn-primitives/hooks'; +import { withRNPrimitives } from '@rn-primitives/utils'; +import * as React from 'react'; +import { RootContext, useRootContext } from '../utils/contexts'; +import type { RootProps } from './types'; + +function Root({ + children, + defaultOpen, + onOpenChange: onOpenChangeProp, + open: openProp, +}: RootProps) { + const [open = false, onOpenChange] = useControllableState({ + prop: openProp, + defaultProp: defaultOpen, + onChange: onOpenChangeProp, + }); + + return ( + + + + ); +} + +const Close = withRNPrimitives(DialogClose, 'pressable'); +const Content = withRNPrimitives(DialogContent, 'view'); +const Description = withRNPrimitives(DialogDescription, 'text'); +const Overlay = withRNPrimitives(DialogOverlay, 'view'); +const Title = withRNPrimitives(DialogTitle, 'text'); +const Trigger = withRNPrimitives(DialogTrigger, 'pressable'); + +export { Close, Content, Description, Overlay, Portal, Root, Title, Trigger, useRootContext }; diff --git a/packages/dialog/src/web/index.ts b/packages/dialog/src/web/index.ts new file mode 100644 index 00000000..0a5fef56 --- /dev/null +++ b/packages/dialog/src/web/index.ts @@ -0,0 +1,22 @@ +export { + Close, + Content, + Description, + Overlay, + Portal, + Root, + Title, + Trigger, + useRootContext, +} from './dialog-web'; + +export type { + CloseProps, + ContentProps, + DescriptionProps, + OverlayProps, + PortalProps, + RootProps, + TitleProps, + TriggerProps, +} from './types'; diff --git a/packages/dialog/src/web/types.ts b/packages/dialog/src/web/types.ts new file mode 100644 index 00000000..2d249ad6 --- /dev/null +++ b/packages/dialog/src/web/types.ts @@ -0,0 +1,59 @@ +import type { + DialogCloseProps, + DialogContentProps, + DialogDescriptionProps, + DialogOverlayProps, + DialogPortalProps, + DialogProps, + DialogTitleProps, + DialogTriggerProps, + Portal, +} from '@radix-ui/react-dialog'; + +type ContentPropsWebOnly = React.ComponentProps<'div'>; + +type TriggerPropsWebOnly = React.ComponentProps<'button'>; + +type ClosePropsWebOnly = React.ComponentProps<'button'>; + +type DescriptionPropsWebOnly = React.ComponentProps<'p'>; + +type OverlayPropsWebOnly = React.ComponentProps<'div'>; + +type PortalPropsWebOnly = Pick, 'container'>; + +type TitlePropsWebOnly = React.ComponentProps<'h1'>; + +type RootProps = DialogProps; + +type CloseProps = DialogCloseProps; + +type DescriptionProps = DialogDescriptionProps; + +type OverlayProps = DialogOverlayProps; + +type PortalProps = DialogPortalProps; + +type TitleProps = DialogTitleProps; + +type TriggerProps = DialogTriggerProps; + +type ContentProps = DialogContentProps; + +export type { + CloseProps, + ClosePropsWebOnly, + ContentProps, + ContentPropsWebOnly, + DescriptionProps, + DescriptionPropsWebOnly, + OverlayProps, + OverlayPropsWebOnly, + PortalProps, + PortalPropsWebOnly, + RootProps, + TitleProps, + TitlePropsWebOnly, + TriggerProps, + TriggerPropsWebOnly, +}; diff --git a/packages/dialog/tsup.config.ts b/packages/dialog/tsup.config.ts index a7bca0fb..f4e04d29 100644 --- a/packages/dialog/tsup.config.ts +++ b/packages/dialog/tsup.config.ts @@ -1,13 +1,32 @@ import { defineConfig, Options } from 'tsup'; export default defineConfig((options: Options) => ({ - entry: ['src/index.ts', 'src/dialog.tsx', 'src/dialog.web.tsx'], + entry: [ + 'src/index.ts', + 'src/universal/index.ts', + 'src/universal/dialog.tsx', + 'src/universal/dialog.web.tsx', + 'src/native/index.ts', + 'src/native/dialog-native.tsx', + 'src/native/dialog-native.native.tsx', + 'src/web/index.ts', + 'src/web/dialog-web.tsx', + 'src/web/dialog-web.web.tsx', + ], banner: { js: "'use client'", }, clean: true, format: ['cjs', 'esm'], - external: ['react', './dialog'], + external: [ + 'react', + './universal', + './dialog', + '../native', + './dialog-native', + '../web', + './dialog-web', + ], dts: true, ...options, esbuildOptions(options) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f3620b8..f0f52fba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -713,6 +713,9 @@ importers: '@rn-primitives/core': specifier: workspace:* version: link:../../packages/core + '@rn-primitives/dialog': + specifier: workspace:* + version: link:../../packages/dialog '@rn-primitives/label': specifier: workspace:* version: link:../../packages/label @@ -804,6 +807,9 @@ importers: '@rn-primitives/core': specifier: workspace:* version: link:../../packages/core + '@rn-primitives/dialog': + specifier: workspace:* + version: link:../../packages/dialog '@rn-primitives/label': specifier: workspace:* version: link:../../packages/label @@ -1189,24 +1195,27 @@ importers: packages/dialog: dependencies: '@radix-ui/react-dialog': - specifier: ^1.1.1 - version: 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^1.1.14 + version: 1.1.14(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@rn-primitives/core': + specifier: workspace:* + version: link:../core '@rn-primitives/hooks': specifier: workspace:* version: link:../hooks - '@rn-primitives/slot': + '@rn-primitives/portal': specifier: workspace:* - version: link:../slot + version: link:../portal '@rn-primitives/types': specifier: workspace:* version: link:../types + '@rn-primitives/utils': + specifier: workspace:* + version: link:../utils react-native-web: specifier: '*' version: 0.20.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) devDependencies: - '@rn-primitives/portal': - specifier: workspace:* - version: link:../portal '@tsconfig/react-native': specifier: ^1.0.1 version: 1.0.5 @@ -3335,6 +3344,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-dialog@1.1.14': + resolution: {integrity: sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: 19.0.0 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dialog@1.1.7': resolution: {integrity: sha512-EIdma8C0C/I6kL6sO02avaCRqi3fmWJpxH6mqbVScorW6nNktzKJT/le7VPho3o/7wCsyRg3z0+Q+Obr0Gy/VQ==} peerDependencies: @@ -3357,6 +3379,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-dismissable-layer@1.1.10': + resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: 19.0.0 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dismissable-layer@1.1.6': resolution: {integrity: sha512-7gpgMT2gyKym9Jz2ZhlRXSg2y6cNQIK8d/cqBZ0RBCaps8pFryCWXiUKI+uHGFrhMrbGUP7U6PWgiXzIxoyF3Q==} peerDependencies: @@ -3405,6 +3440,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: 19.0.0 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-hover-card@1.1.7': resolution: {integrity: sha512-HwM03kP8psrv21J1+9T/hhxi0f5rARVbqIZl9+IAq13l4j4fX+oGIuxisukZZmebO7J35w9gpoILvtG8bbph0w==} peerDependencies: @@ -3518,6 +3566,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: 19.0.0 + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-presence@1.1.3': resolution: {integrity: sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==} peerDependencies: @@ -10290,6 +10351,28 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 + '@radix-ui/react-dialog@1.1.14(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.10)(react@19.0.0) + aria-hidden: 1.2.4 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + react-remove-scroll: 2.6.3(@types/react@19.0.10)(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-dialog@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -10318,6 +10401,19 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 + '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-dismissable-layer@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -10363,6 +10459,17 @@ snapshots: '@types/react': 19.0.10 '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-hover-card@1.1.7(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -10513,6 +10620,16 @@ snapshots: '@types/react': 19.0.10 '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.10)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.10 + '@types/react-dom': 19.0.4(@types/react@19.0.10) + '@radix-ui/react-presence@1.1.3(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.10)(react@19.0.0)