"use client";
import { Children, isValidElement, useMemo } from "react";
import { Select as SelectPrimitive } from "@base-ui/react/select";
import { cn } from "./cn";
/*
* Inline rather than imported, matching Checkbox. The package takes no icon
* dependency, so a product's icon set and version stay entirely its own.
*/
function ChevronDownIcon({ className }: { className?: string }) {
return (
);
}
function CheckIcon() {
return (
);
}
/*
* No new tokens. A select trigger is a field, so it reads the `--input-*`
* geometry; the popup is a menu-like surface, so it reads `--menu-*`. Reusing
* both means a brand that has styled Input and Menu gets a coherent Select for
* free, and there is one less thing to keep in sync.
*
* `container` is here for the reason it is on Dialog and Menu: a portal
* re-parents to document.body and would otherwise escape the `data-brand`
* subtree that supplies the theme.
*/
/*
* Base UI's `Select.Value` renders the raw value, not the matching item's
* text: it resolves labels only when the root is handed an `items` structure
* up front. Products write `
* America/New_York` and expect the trigger to read
* "America/New_York", so the labels are read straight off the JSX.
*
* Off the JSX, and not from the mounted items -- the popup is not in the DOM
* until it is first opened, so anything that waits for items to mount is a
* commit too late and the trigger paints the raw value. The element tree is
* available on the very first render, which is when the trigger needs it.
*
* Items produced by a component that hides them behind its own boundary are
* invisible to this walk; those fall back to the raw value, exactly as they
* would without it.
*/
function collectItems(
node: React.ReactNode,
out: Array<{ label: React.ReactNode; value: string }>,
) {
Children.forEach(node, (child) => {
if (!isValidElement(child)) return;
const childProps = child.props as {
value?: unknown;
children?: React.ReactNode;
};
if (child.type === SelectItem && childProps.value != null) {
out.push({ value: String(childProps.value), label: childProps.children });
return;
}
if (childProps.children != null) collectItems(childProps.children, out);
});
}
/*
* Declared against `string` rather than re-exported. Base UI's root is
* generic over any value type, so a consumer writing
* `React.ComponentProps` collapses that generic to `unknown` --
* every `onValueChange={(v) => ...}` at the call site silently loses its
* parameter type. RadioGroup hit this first.
*
* Constraining the parameter to `string` keeps that collapse harmless: it
* yields `string`, the safe default, rather than `unknown`. Keeping it generic
* at all is what lets a call site pass a handler narrowed to a string union
* -- `(value: PeriodOption) => void` -- without a contravariance error, which
* a plain `(value: string) => void` would reject. Products that genuinely
* need non-string values can reach for the primitive directly.
*/
export interface SelectProps
extends Omit<
SelectPrimitive.Root.Props,
"value" | "defaultValue" | "onValueChange" | "multiple"
> {
value?: T | null;
defaultValue?: T | null;
onValueChange?: (value: T) => void;
}
function Select({
onValueChange,
...props
}: SelectProps) {
const items = useMemo(() => {
const collected: Array<{ label: React.ReactNode; value: string }> = [];
collectItems(props.children, collected);
return collected.length > 0 ? collected : undefined;
}, [props.children]);
return (
onValueChange((value ?? "") as T))
}
/>
);
}
const SelectGroup = SelectPrimitive.Group;
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
return (
);
}
export interface SelectTriggerProps
extends Omit {
className?: string;
size?: "sm" | "md" | "lg";
}
function SelectTrigger({ className, size = "md", children, ...props }: SelectTriggerProps) {
return (
{children}
}
/>
);
}
export interface SelectContentProps
extends Omit,
Pick<
SelectPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
> {
className?: string;
container?: SelectPrimitive.Portal.Props["container"];
}
function SelectContent({
className,
container,
side = "bottom",
sideOffset = 4,
align = "start",
alignOffset = 0,
alignItemWithTrigger = false,
children,
...props
}: SelectContentProps) {
return (
{children}
);
}
function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Props) {
return (
{children}
}
>
);
}
function SelectLabel({ className, ...props }: SelectPrimitive.GroupLabel.Props) {
return (
);
}
function SelectSeparator({
className,
...props
}: SelectPrimitive.Separator.Props) {
return (
);
}
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectItem,
SelectLabel,
SelectSeparator,
};