Spaces:
Running
Running
File size: 6,300 Bytes
5c05829 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | import React, { useState, useRef, useEffect } from 'react';
import { ChevronDown, Check } from 'lucide-react';
import { cn } from '@/lib/utils';
const Select = ({ children, value, onValueChange, disabled = false }) => {
const [open, setOpen] = useState(false);
const ref = useRef(null);
useEffect(() => {
const handleClickOutside = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
setOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// Find the selected item's label
let selectedLabel = null;
// Process all children to find the selected item
React.Children.forEach(children, child => {
if (React.isValidElement(child) && child.type === SelectContent) {
// Check direct SelectItem children
React.Children.forEach(child.props.children, contentChild => {
if (React.isValidElement(contentChild)) {
if (contentChild.type === SelectItem && contentChild.props.value === value) {
selectedLabel = contentChild.props.children;
} else if (contentChild.type === SelectGroup) {
// Check SelectItems inside SelectGroups
React.Children.forEach(contentChild.props.children, groupChild => {
if (React.isValidElement(groupChild) &&
groupChild.type === SelectItem &&
groupChild.props.value === value) {
selectedLabel = groupChild.props.children;
}
});
}
}
});
}
});
// No need for context as we're passing values directly
return (
<div ref={ref} className="relative w-full">
{React.Children.map(children, child => {
if (React.isValidElement(child)) {
if (child.type === SelectTrigger) {
// Clone the trigger with our props
return React.cloneElement(child, {
onClick: () => !disabled && setOpen(!open),
open,
disabled,
value: selectedLabel,
},
// Map the children of the trigger
React.Children.map(child.props.children, triggerChild => {
// If it's a SelectValue, replace its placeholder with the selected value
if (React.isValidElement(triggerChild) && triggerChild.type === SelectValue) {
return React.cloneElement(triggerChild, {
placeholder: selectedLabel || triggerChild.props.placeholder
});
}
return triggerChild;
}));
}
if (child.type === SelectContent) {
return open ? React.cloneElement(child, {
onSelect: (val) => {
onValueChange?.(val);
setOpen(false);
},
}) : null;
}
}
return child;
})}
</div>
);
};
const SelectTrigger = ({ className, children, open, disabled, onClick, value }) => (
<button
type="button"
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
open && "ring-2 ring-ring ring-offset-2",
className
)}
disabled={disabled}
onClick={onClick}
>
<span className="truncate">{value || children}</span>
<ChevronDown className={cn("h-4 w-4 opacity-50 transition-transform ml-2 flex-shrink-0", open && "rotate-180")} />
</button>
);
const SelectValue = ({ placeholder, className }) => {
return <span className={cn("text-muted-foreground", className)}>{placeholder}</span>;
};
const SelectContent = ({ className, children, onSelect }) => {
return (
<div
className={cn(
"absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
className
)}
>
{React.Children.map(children, child => {
if (React.isValidElement(child)) {
if (child.type === SelectItem) {
return React.cloneElement(child, {
onSelect
});
} else if (child.type === SelectGroup) {
// Handle SelectGroup by recursively processing its children
return React.cloneElement(child, {
children: React.Children.map(child.props.children, groupChild => {
if (React.isValidElement(groupChild) && groupChild.type === SelectItem) {
return React.cloneElement(groupChild, {
onSelect
});
}
return groupChild;
})
});
}
}
return child;
})}
</div>
);
};
const SelectItem = ({ className, children, value, onSelect, disabled }) => {
return (
<div
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
onClick={() => !disabled && onSelect?.(value)}
data-disabled={disabled || undefined}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<Check className="h-4 w-4 opacity-0" />
</span>
{children}
</div>
);
};
const SelectGroup = ({ className, children }) => {
return <div className={cn("p-1", className)}>{children}</div>;
};
const SelectLabel = ({ className, children }) => {
return <div className={cn("px-2 py-1.5 text-sm font-semibold", className)}>{children}</div>;
};
const SelectSeparator = ({ className }) => {
return <div className={cn("-mx-1 my-1 h-px bg-muted", className)} />;
};
// Export Select as default and named export
export default Select;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
}; |