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
360 changes: 360 additions & 0 deletions app/(site)/docs/components/select/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
import { ComponentPreview } from "@/components/docs/component-preview";

export default function SelectPage() {
return (
<ComponentPreview
name="Select"
description="A select component for React Native applications."
examples={[
{
"title": "Default",
"value": "default",
"content": "import { Select } from \"@nativeui/ui\";\n\nexport default function SelectDemo() {\n return (\n <Select>\n Click me\n </Select>\n );\n}",
"language": "tsx"
}
]}
componentCode={`import * as React from "react";
import {
View,
Text,
Pressable,
Modal,
ScrollView,
TouchableWithoutFeedback,
Platform,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { cn } from "@/lib/utils";

interface SelectProps {
value?: string;
onValueChange?: (value: string) => void;
placeholder?: string;
disabled?: boolean;
className?: string;
triggerClassName?: string;
contentClassName?: string;
children: React.ReactNode;
}

interface SelectItemProps {
value: string;
children: React.ReactNode;
disabled?: boolean;
className?: string;
onSelect?: (value: string, label: React.ReactNode) => void;
selectedValue?: string;
}

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

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

interface SelectSeparatorProps {
className?: string;
}

// To help TypeScript with React.Children
type ChildType = {
type: React.JSXElementConstructor<any>;
props: any;
};

const Select = React.forwardRef<View, SelectProps>(
(
{
value,
onValueChange,
placeholder,
disabled = false,
className,
triggerClassName,
contentClassName,
children,
},
ref
) => {
const [open, setOpen] = React.useState(false);
const [selectedValue, setSelectedValue] = React.useState(value);
const [selectedLabel, setSelectedLabel] =
React.useState<React.ReactNode>("");

// Find the label from children for the current value
React.useEffect(() => {
if (value === undefined) return;

React.Children.forEach(children, (child) => {
if (!React.isValidElement(child)) return;

const childElement = child as React.ReactElement<any>;

// Handle SelectItem direct child
if (
childElement.type === SelectItem &&
childElement.props.value === value
) {
setSelectedLabel(childElement.props.children);
setSelectedValue(value);
return;
}

// Handle SelectGroup > SelectItem
if (childElement.type === SelectGroup) {
React.Children.forEach(childElement.props.children, (groupChild) => {
if (
React.isValidElement(groupChild) &&
(groupChild as React.ReactElement<any>).type === SelectItem &&
(groupChild as React.ReactElement<any>).props.value === value
) {
setSelectedLabel(
(groupChild as React.ReactElement<any>).props.children
);
setSelectedValue(value);
}
});
}
});
}, [value, children]);

const handleSelect = (value: string, label: React.ReactNode) => {
setSelectedValue(value);
setSelectedLabel(label);
if (onValueChange) {
onValueChange(value);
}
setOpen(false);
};

// Clone children to inject the handleSelect
const enhancedChildren = React.Children.map(children, (child) => {
if (!React.isValidElement(child)) return child;

const childElement = child as React.ReactElement<any>;

if (childElement.type === SelectItem) {
return React.cloneElement(childElement, {
onSelect: handleSelect,
selectedValue,
});
}

if (childElement.type === SelectGroup) {
const groupChildren = React.Children.map(
childElement.props.children,
(groupChild) => {
if (
React.isValidElement(groupChild) &&
(groupChild as React.ReactElement<any>).type === SelectItem
) {
return React.cloneElement(groupChild as React.ReactElement<any>, {
onSelect: handleSelect,
selectedValue,
});
}
return groupChild;
}
);
return React.cloneElement(childElement, {}, groupChildren);
}

return child;
});

return (
<View ref={ref} className={cn("w-full", className)}>
<Pressable
disabled={disabled}
onPress={() => setOpen(true)}
className={cn(
"flex-row h-12 items-center justify-between rounded-md border border-input bg-transparent px-3 py-2",
"shadow-sm",
"active:opacity-70",
disabled && "opacity-50",
Platform.OS === "ios"
? "ios:shadow-sm ios:shadow-foreground/10"
: "android:elevation-1",
triggerClassName
)}
>
<Text
className={cn(
"text-base flex-1",
!selectedValue && "text-muted-foreground",
"text-foreground"
)}
numberOfLines={1}
>
{selectedValue ? selectedLabel : placeholder || "Select an option"}
</Text>

<Ionicons
name="chevron-down"
size={16}
color="#9CA3AF"
style={{ marginLeft: 8, opacity: 0.7 }}
/>
</Pressable>

<Modal
visible={open}
transparent
animationType="fade"
onRequestClose={() => setOpen(false)}
>
<TouchableWithoutFeedback onPress={() => setOpen(false)}>
<View className="flex-1 bg-black/25 justify-center items-center p-4">
<TouchableWithoutFeedback>
<View
className={cn(
"bg-popover rounded-lg overflow-hidden w-full max-w-[90%] max-h-[70%] shadow-xl",
Platform.OS === "ios"
? "ios:shadow-xl"
: "android:elevation-8",
contentClassName
)}
>
<View className="p-1 border-b border-border">
<Text className="text-xl font-medium text-center py-2 text-foreground">
{placeholder || "Select an option"}
</Text>
</View>

<ScrollView className="p-1 max-h-[300px]">
{enhancedChildren}
</ScrollView>

<Pressable
onPress={() => setOpen(false)}
className="border-t border-border p-3"
>
<Text className="text-center text-primary font-medium">
Cancel
</Text>
</Pressable>
</View>
</TouchableWithoutFeedback>
</View>
</TouchableWithoutFeedback>
</Modal>
</View>
);
}
);

Select.displayName = "Select";

const SelectGroup = React.forwardRef<View, SelectGroupProps>(
({ className, children, ...props }, ref) => {
return (
<View ref={ref} className={cn("", className)} {...props}>
{children}
</View>
);
}
);

SelectGroup.displayName = "SelectGroup";

const SelectItem = React.forwardRef<typeof Pressable, SelectItemProps>(
(
{ className, children, value, disabled, onSelect, selectedValue, ...props },
ref
) => {
const isSelected = selectedValue === value;

return (
<Pressable
ref={ref as any}
disabled={disabled}
onPress={() => {
if (onSelect) {
onSelect(value, children);
}
}}
className={cn(
"flex-row h-14 items-center justify-between px-4 py-2 active:bg-accent/50",
isSelected ? "bg-accent" : "",
disabled && "opacity-50",
className
)}
{...props}
>
<Text
className={cn(
"text-base",
isSelected
? "text-accent-foreground font-medium"
: "text-foreground"
)}
>
{children}
</Text>

{isSelected && <Ionicons name="checkmark" size={20} color="#4F46E5" />}
</Pressable>
);
}
);

SelectItem.displayName = "SelectItem";

const SelectLabel = React.forwardRef<Text, SelectLabelProps>(
({ className, children, ...props }, ref) => {
return (
<Text
ref={ref}
className={cn(
"px-3 py-2 text-sm font-semibold text-foreground",
className
)}
{...props}
>
{children}
</Text>
);
}
);

SelectLabel.displayName = "SelectLabel";

const SelectSeparator = React.forwardRef<View, SelectSeparatorProps>(
({ className, ...props }, ref) => {
return (
<View
ref={ref}
className={cn("h-px bg-muted mx-2 my-1", className)}
{...props}
/>
);
}
);

SelectSeparator.displayName = "SelectSeparator";

export { Select, SelectGroup, SelectItem, SelectLabel, SelectSeparator };
`}
previewCode={`import { Select } from "@nativeui/ui";

export default function SelectDemo() {
return (
<div className="flex flex-col gap-4">
<Select>Default Select</Select>
<Select variant="destructive">Delete</Select>
<Select variant="outline">Outline</Select>
<Select variant="secondary">Secondary</Select>
<Select variant="ghost">Ghost</Select>
<Select variant="link">Link</Select>
</div>
);
}`}
registryName="select"
packageName="@nativeui/ui"
/>
);
}
19 changes: 19 additions & 0 deletions public/r/select.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": "select",
"type": "registry:component",
"title": "Select",
"description": "A select component for React Native applications.",
"dependencies": [
"react-native",
"@expo/vector-icons"
],
"registryDependencies": [],
"files": [
{
"path": "registry/select/select.tsx",
"content": "import * as React from \"react\";\nimport {\n View,\n Text,\n Pressable,\n Modal,\n ScrollView,\n TouchableWithoutFeedback,\n Platform,\n} from \"react-native\";\nimport { Ionicons } from \"@expo/vector-icons\";\nimport { cn } from \"@/lib/utils\";\n\ninterface SelectProps {\n value?: string;\n onValueChange?: (value: string) => void;\n placeholder?: string;\n disabled?: boolean;\n className?: string;\n triggerClassName?: string;\n contentClassName?: string;\n children: React.ReactNode;\n}\n\ninterface SelectItemProps {\n value: string;\n children: React.ReactNode;\n disabled?: boolean;\n className?: string;\n onSelect?: (value: string, label: React.ReactNode) => void;\n selectedValue?: string;\n}\n\ninterface SelectLabelProps {\n children: React.ReactNode;\n className?: string;\n}\n\ninterface SelectGroupProps {\n children: React.ReactNode;\n className?: string;\n}\n\ninterface SelectSeparatorProps {\n className?: string;\n}\n\n// To help TypeScript with React.Children\ntype ChildType = {\n type: React.JSXElementConstructor<any>;\n props: any;\n};\n\nconst Select = React.forwardRef<View, SelectProps>(\n (\n {\n value,\n onValueChange,\n placeholder,\n disabled = false,\n className,\n triggerClassName,\n contentClassName,\n children,\n },\n ref\n ) => {\n const [open, setOpen] = React.useState(false);\n const [selectedValue, setSelectedValue] = React.useState(value);\n const [selectedLabel, setSelectedLabel] =\n React.useState<React.ReactNode>(\"\");\n\n // Find the label from children for the current value\n React.useEffect(() => {\n if (value === undefined) return;\n\n React.Children.forEach(children, (child) => {\n if (!React.isValidElement(child)) return;\n\n const childElement = child as React.ReactElement<any>;\n\n // Handle SelectItem direct child\n if (\n childElement.type === SelectItem &&\n childElement.props.value === value\n ) {\n setSelectedLabel(childElement.props.children);\n setSelectedValue(value);\n return;\n }\n\n // Handle SelectGroup > SelectItem\n if (childElement.type === SelectGroup) {\n React.Children.forEach(childElement.props.children, (groupChild) => {\n if (\n React.isValidElement(groupChild) &&\n (groupChild as React.ReactElement<any>).type === SelectItem &&\n (groupChild as React.ReactElement<any>).props.value === value\n ) {\n setSelectedLabel(\n (groupChild as React.ReactElement<any>).props.children\n );\n setSelectedValue(value);\n }\n });\n }\n });\n }, [value, children]);\n\n const handleSelect = (value: string, label: React.ReactNode) => {\n setSelectedValue(value);\n setSelectedLabel(label);\n if (onValueChange) {\n onValueChange(value);\n }\n setOpen(false);\n };\n\n // Clone children to inject the handleSelect\n const enhancedChildren = React.Children.map(children, (child) => {\n if (!React.isValidElement(child)) return child;\n\n const childElement = child as React.ReactElement<any>;\n\n if (childElement.type === SelectItem) {\n return React.cloneElement(childElement, {\n onSelect: handleSelect,\n selectedValue,\n });\n }\n\n if (childElement.type === SelectGroup) {\n const groupChildren = React.Children.map(\n childElement.props.children,\n (groupChild) => {\n if (\n React.isValidElement(groupChild) &&\n (groupChild as React.ReactElement<any>).type === SelectItem\n ) {\n return React.cloneElement(groupChild as React.ReactElement<any>, {\n onSelect: handleSelect,\n selectedValue,\n });\n }\n return groupChild;\n }\n );\n return React.cloneElement(childElement, {}, groupChildren);\n }\n\n return child;\n });\n\n return (\n <View ref={ref} className={cn(\"w-full\", className)}>\n <Pressable\n disabled={disabled}\n onPress={() => setOpen(true)}\n className={cn(\n \"flex-row h-12 items-center justify-between rounded-md border border-input bg-transparent px-3 py-2\",\n \"shadow-sm\",\n \"active:opacity-70\",\n disabled && \"opacity-50\",\n Platform.OS === \"ios\"\n ? \"ios:shadow-sm ios:shadow-foreground/10\"\n : \"android:elevation-1\",\n triggerClassName\n )}\n >\n <Text\n className={cn(\n \"text-base flex-1\",\n !selectedValue && \"text-muted-foreground\",\n \"text-foreground\"\n )}\n numberOfLines={1}\n >\n {selectedValue ? selectedLabel : placeholder || \"Select an option\"}\n </Text>\n\n <Ionicons\n name=\"chevron-down\"\n size={16}\n color=\"#9CA3AF\"\n style={{ marginLeft: 8, opacity: 0.7 }}\n />\n </Pressable>\n\n <Modal\n visible={open}\n transparent\n animationType=\"fade\"\n onRequestClose={() => setOpen(false)}\n >\n <TouchableWithoutFeedback onPress={() => setOpen(false)}>\n <View className=\"flex-1 bg-black/25 justify-center items-center p-4\">\n <TouchableWithoutFeedback>\n <View\n className={cn(\n \"bg-popover rounded-lg overflow-hidden w-full max-w-[90%] max-h-[70%] shadow-xl\",\n Platform.OS === \"ios\"\n ? \"ios:shadow-xl\"\n : \"android:elevation-8\",\n contentClassName\n )}\n >\n <View className=\"p-1 border-b border-border\">\n <Text className=\"text-xl font-medium text-center py-2 text-foreground\">\n {placeholder || \"Select an option\"}\n </Text>\n </View>\n\n <ScrollView className=\"p-1 max-h-[300px]\">\n {enhancedChildren}\n </ScrollView>\n\n <Pressable\n onPress={() => setOpen(false)}\n className=\"border-t border-border p-3\"\n >\n <Text className=\"text-center text-primary font-medium\">\n Cancel\n </Text>\n </Pressable>\n </View>\n </TouchableWithoutFeedback>\n </View>\n </TouchableWithoutFeedback>\n </Modal>\n </View>\n );\n }\n);\n\nSelect.displayName = \"Select\";\n\nconst SelectGroup = React.forwardRef<View, SelectGroupProps>(\n ({ className, children, ...props }, ref) => {\n return (\n <View ref={ref} className={cn(\"\", className)} {...props}>\n {children}\n </View>\n );\n }\n);\n\nSelectGroup.displayName = \"SelectGroup\";\n\nconst SelectItem = React.forwardRef<typeof Pressable, SelectItemProps>(\n (\n { className, children, value, disabled, onSelect, selectedValue, ...props },\n ref\n ) => {\n const isSelected = selectedValue === value;\n\n return (\n <Pressable\n ref={ref as any}\n disabled={disabled}\n onPress={() => {\n if (onSelect) {\n onSelect(value, children);\n }\n }}\n className={cn(\n \"flex-row h-14 items-center justify-between px-4 py-2 active:bg-accent/50\",\n isSelected ? \"bg-accent\" : \"\",\n disabled && \"opacity-50\",\n className\n )}\n {...props}\n >\n <Text\n className={cn(\n \"text-base\",\n isSelected\n ? \"text-accent-foreground font-medium\"\n : \"text-foreground\"\n )}\n >\n {children}\n </Text>\n\n {isSelected && <Ionicons name=\"checkmark\" size={20} color=\"#4F46E5\" />}\n </Pressable>\n );\n }\n);\n\nSelectItem.displayName = \"SelectItem\";\n\nconst SelectLabel = React.forwardRef<Text, SelectLabelProps>(\n ({ className, children, ...props }, ref) => {\n return (\n <Text\n ref={ref}\n className={cn(\n \"px-3 py-2 text-sm font-semibold text-foreground\",\n className\n )}\n {...props}\n >\n {children}\n </Text>\n );\n }\n);\n\nSelectLabel.displayName = \"SelectLabel\";\n\nconst SelectSeparator = React.forwardRef<View, SelectSeparatorProps>(\n ({ className, ...props }, ref) => {\n return (\n <View\n ref={ref}\n className={cn(\"h-px bg-muted mx-2 my-1\", className)}\n {...props}\n />\n );\n }\n);\n\nSelectSeparator.displayName = \"SelectSeparator\";\n\nexport { Select, SelectGroup, SelectItem, SelectLabel, SelectSeparator };\n",
"type": "registry:component"
}
]
}
Loading