a tool for shared writing and social publishing

oops forgot to push a file

+152
+152
components/Combobox.tsx
···
··· 1 + import { createContext, useContext, useEffect, useRef, useState } from "react"; 2 + import * as Popover from "@radix-ui/react-popover"; 3 + import { NestedCardThemeProvider } from "components/ThemeManager/ThemeProvider"; 4 + import { create } from "zustand"; 5 + 6 + export const useComboboxState = create(() => ({ 7 + open: false, 8 + })); 9 + 10 + export const Combobox = ({ 11 + results, 12 + onSelect, 13 + children, 14 + onOpenChange, 15 + highlighted, 16 + setHighlighted, 17 + trigger, 18 + triggerClassName, 19 + sideOffset, 20 + }: { 21 + children: React.ReactNode; 22 + trigger?: React.ReactNode; 23 + triggerClassName?: string; 24 + results: string[]; 25 + onSelect?: () => void; 26 + onOpenChange?: (open: boolean) => void; 27 + highlighted: string | undefined; 28 + setHighlighted: (h: string | undefined) => void; 29 + sideOffset?: number; 30 + }) => { 31 + let ref = useRef<HTMLDivElement>(null); 32 + 33 + let open = useComboboxState((s) => s.open); 34 + 35 + useEffect(() => { 36 + if (!highlighted || !results.find((result) => result === highlighted)) 37 + setHighlighted(results[0]); 38 + if (results.length === 1) { 39 + setHighlighted(results[0]); 40 + } 41 + }, [results, setHighlighted, highlighted]); 42 + 43 + useEffect(() => { 44 + let listener = async (e: KeyboardEvent) => { 45 + let reverseDir = ref.current?.dataset.side === "top"; 46 + let currentHighlightIndex = results.findIndex( 47 + (result) => highlighted && result === highlighted, 48 + ); 49 + 50 + if (reverseDir ? e.key === "ArrowUp" : e.key === "ArrowDown") { 51 + setHighlighted( 52 + results[ 53 + currentHighlightIndex === results.length - 1 || 54 + currentHighlightIndex === undefined 55 + ? 0 56 + : currentHighlightIndex + 1 57 + ], 58 + ); 59 + return; 60 + } 61 + if (reverseDir ? e.key === "ArrowDown" : e.key === "ArrowUp") { 62 + setHighlighted( 63 + results[ 64 + currentHighlightIndex === 0 || 65 + currentHighlightIndex === undefined || 66 + currentHighlightIndex === -1 67 + ? results.length - 1 68 + : currentHighlightIndex - 1 69 + ], 70 + ); 71 + return; 72 + } 73 + 74 + // on enter, select the highlighted item 75 + if (e.key === "Enter") { 76 + onSelect?.(); 77 + useComboboxState.setState({ 78 + open: false, 79 + }); 80 + } 81 + }; 82 + 83 + window.addEventListener("keydown", listener); 84 + 85 + return () => window.removeEventListener("keydown", listener); 86 + }, [highlighted, setHighlighted, results]); 87 + 88 + return ( 89 + <Popover.Root 90 + open={open} 91 + onOpenChange={(newOpen) => { 92 + useComboboxState.setState({ 93 + open: newOpen, 94 + }); 95 + onOpenChange?.(newOpen); 96 + }} 97 + > 98 + <Popover.Trigger asChild className={`${triggerClassName}`}> 99 + <div>{trigger}</div> 100 + </Popover.Trigger> 101 + <Popover.Portal> 102 + <Popover.Content 103 + align="start" 104 + sideOffset={sideOffset ? sideOffset : 16} 105 + collisionPadding={16} 106 + ref={ref} 107 + onOpenAutoFocus={(e) => e.preventDefault()} 108 + className={` 109 + commandMenuContent group/cmd-menu 110 + z-20 w-[264px] 111 + flex data-[side=top]:items-end items-start 112 + `} 113 + > 114 + <NestedCardThemeProvider> 115 + <div className="commandMenuResults w-full max-h-(--radix-popover-content-available-height) overflow-auto flex flex-col group-data-[side=top]/cmd-menu:flex-col-reverse bg-bg-page py-1 gap-0.5 border border-border rounded-md shadow-md"> 116 + {children} 117 + </div> 118 + </NestedCardThemeProvider> 119 + </Popover.Content> 120 + </Popover.Portal> 121 + </Popover.Root> 122 + ); 123 + }; 124 + 125 + export const ComboboxResult = (props: { 126 + result: string; 127 + children: React.ReactNode; 128 + onSelect: () => void; 129 + highlighted: string | undefined; 130 + setHighlighted: (state: string | undefined) => void; 131 + className?: string; 132 + }) => { 133 + let isHighlighted = props.highlighted === props.result; 134 + 135 + return ( 136 + <button 137 + className={`comboboxResult menuItem text-secondary font-normal! py-0.5! mx-1 ${props.className} ${isHighlighted && "bg-[var(--accent-light)]!"}`} 138 + onMouseOver={() => { 139 + props.setHighlighted(props.result); 140 + }} 141 + onMouseDown={(e) => { 142 + e.preventDefault(); 143 + props.onSelect(); 144 + useComboboxState.setState({ 145 + open: false, 146 + }); 147 + }} 148 + > 149 + <div className="truncate">{props.children}</div> 150 + </button> 151 + ); 152 + };