lightswind
Version:
A collection of beautifully crafted React Components, Blocks & Templates for Modern Developers. Create stunning web applications effortlessly by using our 160+ professional and animated react components.
304 lines (303 loc) • 16.2 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
// @ts-nocheck
import * as React from "react";
import { createPortal } from "react-dom";
import { Check, ChevronDown } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { cn } from "../../lib/utils"; // Assuming you have this utility function
const SelectContext = React.createContext(undefined);
const Select = ({ children, defaultValue = "", value, onValueChange, defaultOpen = false, open, onOpenChange, disabled = false, }) => {
const [selectedValue, setSelectedValue] = React.useState(value || defaultValue);
const [isOpen, setIsOpen] = React.useState(open || defaultOpen);
const triggerRef = React.useRef(null);
const [searchQuery, setSearchQuery] = React.useState("");
React.useEffect(() => {
if (value !== undefined) {
setSelectedValue(value);
}
}, [value]);
React.useEffect(() => {
if (open !== undefined) {
setIsOpen(open);
}
}, [open]);
React.useEffect(() => {
if (!isOpen) {
setSearchQuery("");
}
}, [isOpen]);
const handleValueChange = React.useCallback((newValue) => {
if (value === undefined) {
setSelectedValue(newValue);
}
onValueChange?.(newValue);
}, [onValueChange, value]);
const handleOpenChange = React.useCallback((newOpen) => {
if (disabled)
return;
if (open === undefined) {
setIsOpen(newOpen);
}
onOpenChange?.(newOpen);
}, [onOpenChange, open, disabled]);
return (_jsx(SelectContext.Provider, { value: {
value: selectedValue,
onValueChange: handleValueChange,
open: isOpen,
setOpen: handleOpenChange,
triggerRef,
searchQuery,
setSearchQuery,
}, children: children }));
};
const SelectGroup = ({ children, ...props }) => {
return (_jsx("div", { className: "px-1 py-1.5", ...props, children: children }));
};
SelectGroup.displayName = "SelectGroup";
const SelectValue = React.forwardRef(({ className, placeholder, children, ...props }, ref) => {
const context = React.useContext(SelectContext);
if (!context) {
throw new Error("SelectValue must be used within a Select");
}
// This is a bit of a hack to get the children from the parent Select component.
// A better implementation would involve passing a map of values to display labels via context.
// For simplicity, we are assuming children are passed directly or can be inferred.
const parentChildren = context.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
?.children || children;
let displayValue = null;
const findDisplayValue = (nodes) => {
React.Children.forEach(nodes, (node) => {
if (!React.isValidElement(node))
return;
if (displayValue)
return;
// Check if it's a SelectItem
const props = node.props;
if (node.type.displayName === "SelectItem" &&
props.value === context.value) {
displayValue = props.children;
}
// Check if it's a SelectGroup and recurse
else if (node.type.displayName === "SelectGroup") {
findDisplayValue(props.children);
}
});
};
findDisplayValue(parentChildren);
const content = displayValue || context.value || placeholder;
return (_jsx("span", { ref: ref, className: cn("text-sm", className), ...props, children: content || (_jsx("span", { className: "text-muted-foreground", children: "Select an option" })) }));
});
SelectValue.displayName = "SelectValue";
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => {
const context = React.useContext(SelectContext);
if (!context) {
throw new Error("SelectTrigger must be used within a Select");
}
const { open, setOpen, triggerRef, searchQuery, setSearchQuery } = context;
const searchInputRef = React.useRef(null);
React.useEffect(() => {
if (open && searchInputRef.current) {
searchInputRef.current.focus();
}
}, [open]);
React.useImperativeHandle(ref, () => triggerRef.current, [triggerRef]);
return (_jsxs("button", { ref: triggerRef, type: "button", "data-state": open ? "open" : "closed", className: cn("flex h-10 w-full items-center justify-between rounded-md border bg-background px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", className), onClick: () => setOpen(!open), "aria-expanded": open, ...props, children: [open ? (_jsx("input", { ref: searchInputRef, type: "text", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), onClick: (e) => e.stopPropagation(), placeholder: "Search...", className: "w-full bg-transparent p-0 text-sm \r\n border-none outline-none ring-0 focus:outline-none focus:ring-0 \r\n active:outline-none active:ring-0", style: { boxShadow: "none" } })) : (children), _jsx(ChevronDown, { className: cn("h-4 w-4 opacity-50 transition-transform duration-200", open && "rotate-180") })] }));
});
SelectTrigger.displayName = "SelectTrigger";
const SelectScrollUpButton = (props) => _jsx("div", { ...props });
SelectScrollUpButton.displayName = "SelectScrollUpButton";
const SelectScrollDownButton = (props) => _jsx("div", { ...props });
SelectScrollDownButton.displayName = "SelectScrollDownButton";
const SelectContent = React.forwardRef(({ className, children, position = "popper", align = "start", sideOffset = 4, ...props }, ref) => {
const context = React.useContext(SelectContext);
if (!context) {
throw new Error("SelectContent must be used within a Select");
}
const { open, setOpen, triggerRef, searchQuery } = context;
const contentRef = React.useRef(null);
const [calculatedStyle, setCalculatedStyle] = React.useState({});
const [currentSide, setCurrentSide] = React.useState("bottom");
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
React.useEffect(() => {
if (!open || !triggerRef.current)
return;
const updatePosition = () => {
if (!triggerRef.current)
return; // Guard against null ref
const triggerRect = triggerRef.current.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
const spaceBelow = viewportHeight - triggerRect.bottom;
const spaceAbove = triggerRect.top;
const preferredMaxHeight = 224; // A common max-height for dropdowns (e.g., tailwind's h-56)
// Decide whether to show the dropdown below or above the trigger
const showBelow = spaceBelow >= preferredMaxHeight || spaceBelow > spaceAbove;
const newSide = showBelow ? "bottom" : "top";
setCurrentSide(newSide);
// Define the styles that will be applied
const newStyles = {
position: "absolute",
width: `${triggerRect.width}px`,
};
// --- START OF FIX ---
// This is the key change. We now calculate position differently
// for 'top' and 'bottom' to ensure it's always attached correctly.
if (newSide === "bottom") {
const availableHeight = spaceBelow - sideOffset - 8; // 8px for margin
newStyles.maxHeight = `${Math.min(preferredMaxHeight, Math.max(0, availableHeight))}px`;
newStyles.top = `${triggerRect.bottom + window.scrollY + sideOffset}px`;
}
else { // Position above the trigger
const availableHeight = spaceAbove - sideOffset - 8; // 8px for margin
newStyles.maxHeight = `${Math.min(preferredMaxHeight, Math.max(0, availableHeight))}px`;
// By setting `bottom`, we anchor the dropdown's bottom edge to the trigger's top edge.
// This solves the gap issue completely.
newStyles.bottom = `${viewportHeight - triggerRect.top - window.scrollY + sideOffset}px`;
}
// --- END OF FIX ---
// Handle horizontal alignment
let left = triggerRect.left;
if (align === "center") {
// This calculation was slightly off, corrected to center based on content width if known,
// but for a select, centering on trigger is usually sufficient.
left = triggerRect.left + (triggerRect.width / 2) - (triggerRect.width / 2); // Assumes content width = trigger width
}
else if (align === "end") {
left = triggerRect.right - triggerRect.width;
}
// Prevent overflow from the right edge of the viewport
if (left + triggerRect.width > viewportWidth) {
left = viewportWidth - triggerRect.width - 8; // 8px margin
}
// Prevent overflow from the left edge of the viewport
if (left < 0) {
left = 8; // 8px margin
}
newStyles.left = `${left + window.scrollX}px`;
setCalculatedStyle(newStyles);
};
updatePosition();
window.addEventListener("resize", updatePosition);
window.addEventListener("scroll", updatePosition, true);
return () => {
window.removeEventListener("resize", updatePosition);
window.removeEventListener("scroll", updatePosition, true);
};
}, [open, align, sideOffset, triggerRef]);
React.useEffect(() => {
if (!open)
return;
const handleClickOutside = (e) => {
if (contentRef.current &&
!contentRef.current.contains(e.target) &&
triggerRef.current &&
!triggerRef.current.contains(e.target)) {
setOpen(false);
}
};
const handleEscape = (e) => {
if (e.key === "Escape") {
setOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("keydown", handleEscape);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("keydown", handleEscape);
};
}, [open, setOpen, triggerRef]);
const combinedRef = React.useCallback((node) => {
contentRef.current = node;
if (typeof ref === "function") {
ref(node);
}
else if (ref) {
ref.current = node;
}
}, [ref]);
const filteredChildren = React.useMemo(() => {
if (!searchQuery) {
return children;
}
const lowerCaseQuery = searchQuery.toLowerCase();
const getChildText = (child) => {
if (typeof child === "string" || typeof child === "number") {
return child.toString();
}
if (React.isValidElement(child) && child.props.children) {
return React.Children.map(child.props.children, getChildText).join("");
}
return "";
};
return React.Children.map(children, (child) => {
if (!React.isValidElement(child)) {
return child;
}
if (child.type.displayName === "SelectGroup") {
const matchedItems = React.Children.toArray(child.props.children).filter((groupChild) => {
if (React.isValidElement(groupChild) &&
groupChild.type.displayName === "SelectItem") {
const text = getChildText(groupChild.props.children);
return text.toLowerCase().includes(lowerCaseQuery);
}
return false;
});
if (matchedItems.length > 0) {
return React.cloneElement(child, {
...child.props,
children: matchedItems,
});
}
return null;
}
if (child.type.displayName === "SelectItem") {
const text = getChildText(child.props.children);
return text.toLowerCase().includes(lowerCaseQuery) ? child : null;
}
return child;
});
}, [children, searchQuery]);
// Check if there are any children to render after filtering
const hasVisibleChildren = React.Children.count(filteredChildren) > 0;
if (!mounted)
return null;
return createPortal(_jsx(AnimatePresence, { children: open && (_jsxs(motion.div, { ref: combinedRef, style: calculatedStyle, className: cn("z-50 min-w-[var(--radix-select-trigger-width)] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md", position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1", className), initial: { opacity: 0, y: currentSide === "bottom" ? -10 : 10 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: currentSide === "bottom" ? -10 : 10 }, transition: { duration: 0.2 }, ...props, children: [_jsx(SelectScrollUpButton, {}), _jsx("div", { className: "p-1", style: {
maxHeight: calculatedStyle.maxHeight,
overflowY: "auto",
}, children: hasVisibleChildren ? (filteredChildren) : (_jsx("div", { className: "px-2 py-1.5 text-sm text-muted-foreground", children: "No results found." })) }), _jsx(SelectScrollDownButton, {})] })) }), document.body);
});
SelectContent.displayName = "SelectContent";
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (_jsx("span", { ref: ref, className: cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className), ...props })));
SelectLabel.displayName = "SelectLabel";
const SelectItem = React.forwardRef(({ className, children, value, disabled = false, ...props }, ref) => {
const context = React.useContext(SelectContext);
if (!context) {
throw new Error("SelectItem must be used within a Select");
}
const { value: selectedValue, onValueChange, setOpen } = context;
const isSelected = selectedValue === value;
const handleSelect = (e) => {
if (disabled)
return;
e.preventDefault();
e.stopPropagation();
onValueChange(value);
setTimeout(() => setOpen(false), 50); // Small delay to show selection
};
return (_jsxs("div", { ref: ref, className: cn("relative flex w-full select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground", isSelected
? "bg-accent text-accent-foreground"
: "hover:bg-accent hover:text-accent-foreground", disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer", className), onClick: handleSelect, onKeyDown: (e) => {
if (e.key === "Enter" || e.key === " ") {
handleSelect(e);
}
}, "aria-selected": isSelected, "data-disabled": disabled, role: "option", tabIndex: disabled ? -1 : 0, ...props, children: [_jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: isSelected && _jsx(Check, { className: "h-4 w-4" }) }), _jsx("span", { className: "text-sm", children: children })] }));
});
SelectItem.displayName = "SelectItem";
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (_jsx("div", { ref: ref, className: cn("-mx-1 my-1 h-px bg-muted", className), ...props })));
SelectSeparator.displayName = "SelectSeparator";
export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, SelectScrollUpButton, SelectScrollDownButton, };