diff --git a/components/Combobox.tsx b/components/Combobox.tsx new file mode 100644 index 00000000..69d9b8d5 --- /dev/null +++ b/components/Combobox.tsx @@ -0,0 +1,152 @@ +import { createContext, useContext, useEffect, useRef, useState } from "react"; +import * as Popover from "@radix-ui/react-popover"; +import { NestedCardThemeProvider } from "components/ThemeManager/ThemeProvider"; +import { create } from "zustand"; + +export const useComboboxState = create(() => ({ + open: false, +})); + +export const Combobox = ({ + results, + onSelect, + children, + onOpenChange, + highlighted, + setHighlighted, + trigger, + triggerClassName, + sideOffset, +}: { + children: React.ReactNode; + trigger?: React.ReactNode; + triggerClassName?: string; + results: string[]; + onSelect?: () => void; + onOpenChange?: (open: boolean) => void; + highlighted: string | undefined; + setHighlighted: (h: string | undefined) => void; + sideOffset?: number; +}) => { + let ref = useRef(null); + + let open = useComboboxState((s) => s.open); + + useEffect(() => { + if (!highlighted || !results.find((result) => result === highlighted)) + setHighlighted(results[0]); + if (results.length === 1) { + setHighlighted(results[0]); + } + }, [results, setHighlighted, highlighted]); + + useEffect(() => { + let listener = async (e: KeyboardEvent) => { + let reverseDir = ref.current?.dataset.side === "top"; + let currentHighlightIndex = results.findIndex( + (result) => highlighted && result === highlighted, + ); + + if (reverseDir ? e.key === "ArrowUp" : e.key === "ArrowDown") { + setHighlighted( + results[ + currentHighlightIndex === results.length - 1 || + currentHighlightIndex === undefined + ? 0 + : currentHighlightIndex + 1 + ], + ); + return; + } + if (reverseDir ? e.key === "ArrowDown" : e.key === "ArrowUp") { + setHighlighted( + results[ + currentHighlightIndex === 0 || + currentHighlightIndex === undefined || + currentHighlightIndex === -1 + ? results.length - 1 + : currentHighlightIndex - 1 + ], + ); + return; + } + + // on enter, select the highlighted item + if (e.key === "Enter") { + onSelect?.(); + useComboboxState.setState({ + open: false, + }); + } + }; + + window.addEventListener("keydown", listener); + + return () => window.removeEventListener("keydown", listener); + }, [highlighted, setHighlighted, results]); + + return ( + { + useComboboxState.setState({ + open: newOpen, + }); + onOpenChange?.(newOpen); + }} + > + +
{trigger}
+
+ + e.preventDefault()} + className={` + commandMenuContent group/cmd-menu + z-20 w-[264px] + flex data-[side=top]:items-end items-start + `} + > + +
+ {children} +
+
+
+
+
+ ); +}; + +export const ComboboxResult = (props: { + result: string; + children: React.ReactNode; + onSelect: () => void; + highlighted: string | undefined; + setHighlighted: (state: string | undefined) => void; + className?: string; +}) => { + let isHighlighted = props.highlighted === props.result; + + return ( + + ); +};