|
1 | 1 | "use client" |
2 | 2 |
|
3 | 3 | import { useId, useState } from "react" |
4 | | -import { Check, ChevronsUpDown } from "lucide-react" |
| 4 | +import { Check, ChevronsUpDown, X } from "lucide-react" |
5 | 5 |
|
6 | 6 | import { Button } from "@/components/ui/button" |
7 | 7 | import { Input } from "@/components/ui/input" |
@@ -40,6 +40,10 @@ export function SearchableSelect({ |
40 | 40 | const filteredOptions = normalizedQuery |
41 | 41 | ? options.filter((option) => option.toLowerCase().includes(normalizedQuery)) |
42 | 42 | : options |
| 43 | + const resultLabel = |
| 44 | + filteredOptions.length === 1 |
| 45 | + ? "1 result" |
| 46 | + : `${filteredOptions.length} results` |
43 | 47 |
|
44 | 48 | return ( |
45 | 49 | <Popover |
@@ -70,12 +74,36 @@ export function SearchableSelect({ |
70 | 74 | </PopoverTrigger> |
71 | 75 | <PopoverContent className="w-[220px] p-0" align="start"> |
72 | 76 | <div className="border-b p-2"> |
73 | | - <Input |
74 | | - value={query} |
75 | | - onChange={(event) => setQuery(event.target.value)} |
76 | | - placeholder={searchPlaceholder} |
77 | | - autoFocus |
78 | | - /> |
| 77 | + <div className="relative"> |
| 78 | + <Input |
| 79 | + value={query} |
| 80 | + onChange={(event) => setQuery(event.target.value)} |
| 81 | + onKeyDown={(event) => { |
| 82 | + if (event.key === "Escape" && query) { |
| 83 | + event.preventDefault() |
| 84 | + event.stopPropagation() |
| 85 | + setQuery("") |
| 86 | + } |
| 87 | + }} |
| 88 | + placeholder={searchPlaceholder} |
| 89 | + autoFocus |
| 90 | + className="pr-8" |
| 91 | + /> |
| 92 | + {query ? ( |
| 93 | + <button |
| 94 | + type="button" |
| 95 | + onClick={() => setQuery("")} |
| 96 | + className="absolute top-1/2 right-2 -translate-y-1/2 rounded-sm p-0.5 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" |
| 97 | + aria-label="Clear search" |
| 98 | + > |
| 99 | + <X className="size-4" /> |
| 100 | + </button> |
| 101 | + ) : null} |
| 102 | + </div> |
| 103 | + <div className="mt-2 flex items-center justify-between px-1 text-xs text-muted-foreground"> |
| 104 | + <span>{query ? resultLabel : `${options.length} options`}</span> |
| 105 | + {query ? <span>Esc to clear</span> : null} |
| 106 | + </div> |
79 | 107 | </div> |
80 | 108 | <div |
81 | 109 | id={`${inputId}-listbox`} |
@@ -108,7 +136,14 @@ export function SearchableSelect({ |
108 | 136 | }) |
109 | 137 | ) : ( |
110 | 138 | <div className="px-2 py-4 text-center text-sm text-muted-foreground"> |
111 | | - {emptyMessage} |
| 139 | + <div>{emptyMessage}</div> |
| 140 | + <button |
| 141 | + type="button" |
| 142 | + onClick={() => setQuery("")} |
| 143 | + className="mt-2 inline-flex items-center rounded-sm text-sm text-foreground underline underline-offset-4 transition-colors hover:text-primary" |
| 144 | + > |
| 145 | + Clear search |
| 146 | + </button> |
112 | 147 | </div> |
113 | 148 | )} |
114 | 149 | </div> |
|
0 commit comments