diff --git a/app/(site)/docs/components/collapsible/page.tsx b/app/(site)/docs/components/collapsible/page.tsx new file mode 100644 index 0000000..18ed9b6 --- /dev/null +++ b/app/(site)/docs/components/collapsible/page.tsx @@ -0,0 +1,209 @@ +import { ComponentPreview } from "@/components/docs/component-preview"; + +export default function CollapsiblePage() { + return ( + \n Click me\n \n );\n}", + "language": "tsx" + } +]} + componentCode={`import * as React from "react"; +import { + View, + Pressable, + LayoutAnimation, + Platform, + UIManager, +} from "react-native"; +import { cn } from "@/lib/utils"; +import { Feather } from "@expo/vector-icons"; + +// Enable layout animation for Android +if (Platform.OS === "android") { + if (UIManager.setLayoutAnimationEnabledExperimental) { + UIManager.setLayoutAnimationEnabledExperimental(true); + } +} + +interface CollapsibleContextValue { + open: boolean; + toggle: () => void; +} + +const CollapsibleContext = React.createContext( + null +); + +interface CollapsibleProps { + children: React.ReactNode; + open?: boolean; + onOpenChange?: (open: boolean) => void; + defaultOpen?: boolean; + className?: string; + disabled?: boolean; +} + +const Collapsible = React.forwardRef( + ( + { + children, + className, + open, + onOpenChange, + defaultOpen = false, + disabled = false, + ...props + }, + ref + ) => { + const [isOpen, setIsOpen] = React.useState( + open !== undefined ? open : defaultOpen + ); + + const isControlled = open !== undefined; + const currentOpen = isControlled ? open : isOpen; + + const toggle = React.useCallback(() => { + if (!disabled) { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + + if (!isControlled) { + setIsOpen(!currentOpen); + } + + if (onOpenChange) { + onOpenChange(!currentOpen); + } + } + }, [currentOpen, isControlled, onOpenChange, disabled]); + + React.useEffect(() => { + if (isControlled) { + setIsOpen(open || false); + } + }, [open, isControlled]); + + return ( + + + {children} + + + ); + } +); + +Collapsible.displayName = "Collapsible"; + +interface CollapsibleTriggerProps { + children: React.ReactNode; + className?: string; + asChild?: boolean; + icon?: boolean; +} + +const CollapsibleTrigger = React.forwardRef( + ({ children, className, asChild, icon = true, ...props }, ref) => { + const context = React.useContext(CollapsibleContext); + + if (!context) { + throw new Error("CollapsibleTrigger must be used within a Collapsible"); + } + + const { open, toggle } = context; + + if (asChild && React.isValidElement(children)) { + return React.cloneElement(children, { + ...props, + onPress: toggle, + accessibilityRole: "button", + accessibilityState: { expanded: open }, + } as any); + } + + return ( + + {children} + {icon && ( + + + + )} + + ); + } +); + +CollapsibleTrigger.displayName = "CollapsibleTrigger"; + +interface CollapsibleContentProps { + children: React.ReactNode; + className?: string; +} + +const CollapsibleContent = React.forwardRef( + ({ children, className, ...props }, ref) => { + const context = React.useContext(CollapsibleContext); + + if (!context) { + throw new Error("CollapsibleContent must be used within a Collapsible"); + } + + const { open } = context; + + if (!open) { + return null; + } + + return ( + + {children} + + ); + } +); + +CollapsibleContent.displayName = "CollapsibleContent"; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; +`} + previewCode={`import { Collapsible } from "@nativeui/ui"; + +export default function CollapsibleDemo() { + return ( +
+ Default Collapsible + Delete + Outline + Secondary + Ghost + Link +
+ ); +}`} + registryName="collapsible" + packageName="@nativeui/ui" + /> + ); +} diff --git a/public/r/collapsible.json b/public/r/collapsible.json new file mode 100644 index 0000000..c6b4b31 --- /dev/null +++ b/public/r/collapsible.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "collapsible", + "type": "registry:component", + "title": "Collapsible", + "description": "A collapsible component for React Native applications.", + "dependencies": [ + "react-native", + "@expo/vector-icons" + ], + "registryDependencies": [], + "files": [ + { + "path": "registry/collapsible/collapsible.tsx", + "content": "import * as React from \"react\";\nimport {\n View,\n Pressable,\n LayoutAnimation,\n Platform,\n UIManager,\n} from \"react-native\";\nimport { cn } from \"@/lib/utils\";\nimport { Feather } from \"@expo/vector-icons\";\n\n// Enable layout animation for Android\nif (Platform.OS === \"android\") {\n if (UIManager.setLayoutAnimationEnabledExperimental) {\n UIManager.setLayoutAnimationEnabledExperimental(true);\n }\n}\n\ninterface CollapsibleContextValue {\n open: boolean;\n toggle: () => void;\n}\n\nconst CollapsibleContext = React.createContext(\n null\n);\n\ninterface CollapsibleProps {\n children: React.ReactNode;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n defaultOpen?: boolean;\n className?: string;\n disabled?: boolean;\n}\n\nconst Collapsible = React.forwardRef(\n (\n {\n children,\n className,\n open,\n onOpenChange,\n defaultOpen = false,\n disabled = false,\n ...props\n },\n ref\n ) => {\n const [isOpen, setIsOpen] = React.useState(\n open !== undefined ? open : defaultOpen\n );\n\n const isControlled = open !== undefined;\n const currentOpen = isControlled ? open : isOpen;\n\n const toggle = React.useCallback(() => {\n if (!disabled) {\n LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);\n\n if (!isControlled) {\n setIsOpen(!currentOpen);\n }\n\n if (onOpenChange) {\n onOpenChange(!currentOpen);\n }\n }\n }, [currentOpen, isControlled, onOpenChange, disabled]);\n\n React.useEffect(() => {\n if (isControlled) {\n setIsOpen(open || false);\n }\n }, [open, isControlled]);\n\n return (\n \n \n {children}\n \n \n );\n }\n);\n\nCollapsible.displayName = \"Collapsible\";\n\ninterface CollapsibleTriggerProps {\n children: React.ReactNode;\n className?: string;\n asChild?: boolean;\n icon?: boolean;\n}\n\nconst CollapsibleTrigger = React.forwardRef(\n ({ children, className, asChild, icon = true, ...props }, ref) => {\n const context = React.useContext(CollapsibleContext);\n\n if (!context) {\n throw new Error(\"CollapsibleTrigger must be used within a Collapsible\");\n }\n\n const { open, toggle } = context;\n\n if (asChild && React.isValidElement(children)) {\n return React.cloneElement(children, {\n ...props,\n onPress: toggle,\n accessibilityRole: \"button\",\n accessibilityState: { expanded: open },\n } as any);\n }\n\n return (\n \n {children}\n {icon && (\n \n \n \n )}\n \n );\n }\n);\n\nCollapsibleTrigger.displayName = \"CollapsibleTrigger\";\n\ninterface CollapsibleContentProps {\n children: React.ReactNode;\n className?: string;\n}\n\nconst CollapsibleContent = React.forwardRef(\n ({ children, className, ...props }, ref) => {\n const context = React.useContext(CollapsibleContext);\n\n if (!context) {\n throw new Error(\"CollapsibleContent must be used within a Collapsible\");\n }\n\n const { open } = context;\n\n if (!open) {\n return null;\n }\n\n return (\n \n {children}\n \n );\n }\n);\n\nCollapsibleContent.displayName = \"CollapsibleContent\";\n\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent };\n", + "type": "registry:component" + } + ] +} \ No newline at end of file diff --git a/registry.json b/registry.json index 5e82fac..58cba7d 100644 --- a/registry.json +++ b/registry.json @@ -357,6 +357,20 @@ ], "dependencies": ["react-native"], "registryDependencies": [] + }, + { + "name": "collapsible", + "type": "registry:component", + "title": "Collapsible", + "description": "A collapsible component for React Native applications.", + "files": [ + { + "path": "registry/collapsible/collapsible.tsx", + "type": "registry:component" + } + ], + "dependencies": ["react-native", "@expo/vector-icons"], + "registryDependencies": [] } ] } diff --git a/registry/collapsible/collapsible.tsx b/registry/collapsible/collapsible.tsx new file mode 100644 index 0000000..546dce5 --- /dev/null +++ b/registry/collapsible/collapsible.tsx @@ -0,0 +1,174 @@ +import * as React from "react"; +import { + View, + Pressable, + LayoutAnimation, + Platform, + UIManager, +} from "react-native"; +import { cn } from "@/lib/utils"; +import { Feather } from "@expo/vector-icons"; + +// Enable layout animation for Android +if (Platform.OS === "android") { + if (UIManager.setLayoutAnimationEnabledExperimental) { + UIManager.setLayoutAnimationEnabledExperimental(true); + } +} + +interface CollapsibleContextValue { + open: boolean; + toggle: () => void; +} + +const CollapsibleContext = React.createContext( + null +); + +interface CollapsibleProps { + children: React.ReactNode; + open?: boolean; + onOpenChange?: (open: boolean) => void; + defaultOpen?: boolean; + className?: string; + disabled?: boolean; +} + +const Collapsible = React.forwardRef( + ( + { + children, + className, + open, + onOpenChange, + defaultOpen = false, + disabled = false, + ...props + }, + ref + ) => { + const [isOpen, setIsOpen] = React.useState( + open !== undefined ? open : defaultOpen + ); + + const isControlled = open !== undefined; + const currentOpen = isControlled ? open : isOpen; + + const toggle = React.useCallback(() => { + if (!disabled) { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + + if (!isControlled) { + setIsOpen(!currentOpen); + } + + if (onOpenChange) { + onOpenChange(!currentOpen); + } + } + }, [currentOpen, isControlled, onOpenChange, disabled]); + + React.useEffect(() => { + if (isControlled) { + setIsOpen(open || false); + } + }, [open, isControlled]); + + return ( + + + {children} + + + ); + } +); + +Collapsible.displayName = "Collapsible"; + +interface CollapsibleTriggerProps { + children: React.ReactNode; + className?: string; + asChild?: boolean; + icon?: boolean; +} + +const CollapsibleTrigger = React.forwardRef( + ({ children, className, asChild, icon = true, ...props }, ref) => { + const context = React.useContext(CollapsibleContext); + + if (!context) { + throw new Error("CollapsibleTrigger must be used within a Collapsible"); + } + + const { open, toggle } = context; + + if (asChild && React.isValidElement(children)) { + return React.cloneElement(children, { + ...props, + onPress: toggle, + accessibilityRole: "button", + accessibilityState: { expanded: open }, + } as any); + } + + return ( + + {children} + {icon && ( + + + + )} + + ); + } +); + +CollapsibleTrigger.displayName = "CollapsibleTrigger"; + +interface CollapsibleContentProps { + children: React.ReactNode; + className?: string; +} + +const CollapsibleContent = React.forwardRef( + ({ children, className, ...props }, ref) => { + const context = React.useContext(CollapsibleContext); + + if (!context) { + throw new Error("CollapsibleContent must be used within a Collapsible"); + } + + const { open } = context; + + if (!open) { + return null; + } + + return ( + + {children} + + ); + } +); + +CollapsibleContent.displayName = "CollapsibleContent"; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent };