import * as listbox from "@zag-js/listbox"; import { normalizeProps, type PropTypes, useMachine } from "@zag-js/react"; import * as React from "react"; import { SuffixContext, useSuffixContext } from "../../contexts"; import { useGetKey, useGetSet } from "../../hooks"; type Context = { api: listbox.Api; items: T[]; }; const ListBoxContext = React.createContext(null!); const useListBoxContext = () => React.use>(ListBoxContext); type CoreProps = { children?: React.ReactNode; itemToValue: (item: T) => string; itemToString: (item: T) => string; items: T[]; loopFocus?: boolean; typeahead?: boolean; __length?: number; "data-id": string; "data-label"?: string; }; type MultipleProps = CoreProps & { multiple: true; onValueChange?: (items: T[]) => void; initialValue?: T | null; }; type SingleProps = CoreProps & { multiple: false; onValueChange?: (item: T[]) => void; initialValue?: T | null; }; const DEFAULT_VALUE = { value: [] }; export function ListBox({ items = [], children, initialValue, itemToValue = defaultItemToValue, itemToString = (item: T) => String(item), onValueChange, multiple = false, loopFocus = false, typeahead = true, __length, ...rest }: MultipleProps | SingleProps) { const key = useGetKey(rest); const [{ value }, setValue] = useGetSet( key, initialValue ? { value: Array.isArray(initialValue) ? (initialValue.map(itemToValue) as T[]) : ([itemToValue(initialValue as T)] as T[]), } : DEFAULT_VALUE, ); const service = useMachine(listbox.machine as any, { id: key, collection: listbox.collection({ items: items || [], itemToValue, itemToString, }), typeahead, loopFocus, value: value as any, selectionMode: multiple ? "multiple" : "single", onValueChange: (details) => { setValue({ value: details.items || [] }); onValueChange?.(details.items || []); }, }) as listbox.Service; const api = listbox.connect(service, normalizeProps); return (
{children}
); } function ListInput(props: { "data-id"?: string }) { const key = useGetKey(props); const initialValue = React.useMemo(() => ({ value: "" }), []); const [{ value }, setState] = useGetSet<{ value: string }>(key, initialValue); const { api } = useListBoxContext(); return ( setState({ value: e.target.value })} /> ); } ListBox.ListInput = ListInput; function ListContent({ children, ...props }: { children?: (value: T, index: number) => React.ReactElement | null; }) { const suffix = useSuffixContext(); const { api, items } = useListBoxContext(); return (
{items.map((item, idx) => { const itemKey = `${(item as any)?.id ?? idx}`; const key = suffix ? `${itemKey}-${suffix}` : itemKey; return ( {children ? children(item, idx) : null} ); })}
); } ListBox.ListContent = ListContent as any; const ListBoxItemContext = React.createContext(null!); const useListBoxItemContext = () => React.use(ListBoxItemContext); function ListItem({ children, ...props }: { children?: React.ReactNode; }) { const { api } = useListBoxContext(); const item = useListBoxItemContext(); return (
{children}
); } ListContent.ListItem = ListItem; function ItemText(props: any) { const { api } = useListBoxContext(); const item = useListBoxItemContext(); const label = api.collection.stringifyItem(item); return ( {label} ); } ListItem.ListItemText = ItemText; function ItemIndicator({ children, ...props }) { const { api } = useListBoxContext(); const item = useListBoxItemContext(); return ( {children} ); } ListItem.ListItemIndicator = ItemIndicator; function defaultItemToValue(item: T): string { if (!item) return ""; if (typeof item !== "object") { return String(item); } return (item as any)?.id ?? JSON.stringify(item); }