Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions app/(site)/docs/components/collapsible/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { ComponentPreview } from "@/components/docs/component-preview";

export default function CollapsiblePage() {
return (
<ComponentPreview
name="Collapsible"
description="A collapsible component for React Native applications."
examples={[
{
"title": "Default",
"value": "default",
"content": "import { Collapsible } from \"@nativeui/ui\";\n\nexport default function CollapsibleDemo() {\n return (\n <Collapsible>\n Click me\n </Collapsible>\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<CollapsibleContextValue | null>(
null
);

interface CollapsibleProps {
children: React.ReactNode;
open?: boolean;
onOpenChange?: (open: boolean) => void;
defaultOpen?: boolean;
className?: string;
disabled?: boolean;
}

const Collapsible = React.forwardRef<View, CollapsibleProps>(
(
{
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 (
<CollapsibleContext.Provider value={{ open: currentOpen, toggle }}>
<View
ref={ref}
className={cn("overflow-hidden", disabled && "opacity-50", className)}
{...props}
>
{children}
</View>
</CollapsibleContext.Provider>
);
}
);

Collapsible.displayName = "Collapsible";

interface CollapsibleTriggerProps {
children: React.ReactNode;
className?: string;
asChild?: boolean;
icon?: boolean;
}

const CollapsibleTrigger = React.forwardRef<View, CollapsibleTriggerProps>(
({ 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 (
<Pressable
ref={ref as any}
className={cn(
"flex-row items-center justify-between p-4 active:opacity-70",
className
)}
onPress={toggle}
accessibilityRole="button"
accessibilityState={{ expanded: open }}
accessibilityHint="Toggle collapsible section"
{...props}
>
<View className="flex-row items-center flex-1">{children}</View>
{icon && (
<View style={{ transform: [{ rotate: open ? "180deg" : "0deg" }] }}>
<Feather name="chevron-down" size={20} color="#888" />
</View>
)}
</Pressable>
);
}
);

CollapsibleTrigger.displayName = "CollapsibleTrigger";

interface CollapsibleContentProps {
children: React.ReactNode;
className?: string;
}

const CollapsibleContent = React.forwardRef<View, CollapsibleContentProps>(
({ 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 (
<View ref={ref} className={cn("overflow-hidden", className)} {...props}>
{children}
</View>
);
}
);

CollapsibleContent.displayName = "CollapsibleContent";

export { Collapsible, CollapsibleTrigger, CollapsibleContent };
`}
previewCode={`import { Collapsible } from "@nativeui/ui";

export default function CollapsibleDemo() {
return (
<div className="flex flex-col gap-4">
<Collapsible>Default Collapsible</Collapsible>
<Collapsible variant="destructive">Delete</Collapsible>
<Collapsible variant="outline">Outline</Collapsible>
<Collapsible variant="secondary">Secondary</Collapsible>
<Collapsible variant="ghost">Ghost</Collapsible>
<Collapsible variant="link">Link</Collapsible>
</div>
);
}`}
registryName="collapsible"
packageName="@nativeui/ui"
/>
);
}
19 changes: 19 additions & 0 deletions public/r/collapsible.json
Original file line number Diff line number Diff line change
@@ -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<CollapsibleContextValue | null>(\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<View, CollapsibleProps>(\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 <CollapsibleContext.Provider value={{ open: currentOpen, toggle }}>\n <View\n ref={ref}\n className={cn(\"overflow-hidden\", disabled && \"opacity-50\", className)}\n {...props}\n >\n {children}\n </View>\n </CollapsibleContext.Provider>\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<View, CollapsibleTriggerProps>(\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 <Pressable\n ref={ref as any}\n className={cn(\n \"flex-row items-center justify-between p-4 active:opacity-70\",\n className\n )}\n onPress={toggle}\n accessibilityRole=\"button\"\n accessibilityState={{ expanded: open }}\n accessibilityHint=\"Toggle collapsible section\"\n {...props}\n >\n <View className=\"flex-row items-center flex-1\">{children}</View>\n {icon && (\n <View style={{ transform: [{ rotate: open ? \"180deg\" : \"0deg\" }] }}>\n <Feather name=\"chevron-down\" size={20} color=\"#888\" />\n </View>\n )}\n </Pressable>\n );\n }\n);\n\nCollapsibleTrigger.displayName = \"CollapsibleTrigger\";\n\ninterface CollapsibleContentProps {\n children: React.ReactNode;\n className?: string;\n}\n\nconst CollapsibleContent = React.forwardRef<View, CollapsibleContentProps>(\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 <View ref={ref} className={cn(\"overflow-hidden\", className)} {...props}>\n {children}\n </View>\n );\n }\n);\n\nCollapsibleContent.displayName = \"CollapsibleContent\";\n\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent };\n",
"type": "registry:component"
}
]
}
14 changes: 14 additions & 0 deletions registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": []
}
]
}
Loading