PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 2.0.10 All 83 releases
yatra / resources / js / components / ui / searchable-select.tsx

searchable-select.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.3, at resources/js/components/ui/searchable-select.tsx

196 lines 6.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useState, useRef, useEffect } from "react";
2 import { ChevronDown, X, Search } from "lucide-react";
3 import { __ } from "../../lib/i18n";
4 import { Input } from "./input";
5
6 export interface SearchableSelectOption {
7 value: string;
8 label: string;
9 icon?: string;
10 }
11
12 export interface SearchableSelectProps {
13 value: string;
14 onChange: (value: string) => void;
15 options: SearchableSelectOption[];
16 placeholder?: string;
17 searchPlaceholder?: string;
18 className?: string;
19 error?: boolean;
20 required?: boolean;
21 disabled?: boolean;
22 showValueId?: boolean; // Show "ID: value" next to label
23 }
24
25 export const SearchableSelect: React.FC<SearchableSelectProps> = ({
26 value,
27 onChange,
28 options,
29 placeholder = __("Select an option...", "yatra"),
30 searchPlaceholder = __("Search...", "yatra"),
31 className = "",
32 error = false,
33 disabled = false,
34 showValueId = true,
35 }) => {
36 const [isOpen, setIsOpen] = useState(false);
37 const [searchTerm, setSearchTerm] = useState("");
38 const containerRef = useRef<HTMLDivElement>(null);
39 const searchInputRef = useRef<HTMLInputElement>(null);
40
41 // Filter options based on search term (search by both label and value)
42 const filteredOptions = options.filter(
43 (option) =>
44 option.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
45 option.value.toLowerCase().includes(searchTerm.toLowerCase()),
46 );
47
48 // Get selected option
49 const selectedOption = options.find((opt) => opt.value === value);
50
51 // Close dropdown when clicking outside
52 useEffect(() => {
53 const handleClickOutside = (event: MouseEvent) => {
54 if (
55 containerRef.current &&
56 !containerRef.current.contains(event.target as Node)
57 ) {
58 setIsOpen(false);
59 setSearchTerm("");
60 }
61 };
62
63 if (isOpen) {
64 document.addEventListener("mousedown", handleClickOutside);
65 // Focus search input when dropdown opens
66 setTimeout(() => {
67 searchInputRef.current?.focus();
68 }, 100);
69 }
70
71 return () => {
72 document.removeEventListener("mousedown", handleClickOutside);
73 };
74 }, [isOpen]);
75
76 const handleSelect = (optionValue: string) => {
77 onChange(optionValue);
78 setIsOpen(false);
79 setSearchTerm("");
80 };
81
82 const handleClear = (e: React.MouseEvent) => {
83 e.stopPropagation();
84 onChange("");
85 setSearchTerm("");
86 };
87
88 return (
89 <div
90 ref={containerRef}
91 className={`relative ${className}`}
92 style={{ zIndex: isOpen ? 9999 : "auto" }}
93 >
94 <div
95 className={`flex h-11 w-full rounded-md border-2 ${
96 error ? "border-red-500" : "border-gray-300 dark:border-gray-600"
97 } bg-white dark:bg-gray-800 px-4 py-2.5 text-base ring-offset-white focus-within:outline-none focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:ring-offset-gray-900 dark:focus-within:ring-blue-400 transition-colors`}
98 >
99 <div
100 onClick={() => !disabled && setIsOpen(!isOpen)}
101 className={`flex-1 flex items-center justify-between text-left ${disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`}
102 >
103 <span
104 className={
105 selectedOption
106 ? "text-gray-900 dark:text-white"
107 : "text-gray-500 dark:text-gray-400"
108 }
109 >
110 {selectedOption ? (
111 <div className="flex items-center justify-between w-full">
112 <span>{selectedOption.label}</span>
113 {showValueId &&
114 selectedOption.value &&
115 selectedOption.value !== "" && (
116 <span className="text-xs text-gray-500 dark:text-gray-400 ml-2 font-medium">
117 ID: {selectedOption.value}
118 </span>
119 )}
120 </div>
121 ) : (
122 placeholder
123 )}
124 </span>
125 <div className="flex items-center gap-1">
126 {value && !disabled && (
127 <button
128 type="button"
129 onClick={handleClear}
130 className="p-0.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
131 aria-label={__("Clear selection", "yatra")}
132 >
133 <X className="w-4 h-4 text-gray-400" />
134 </button>
135 )}
136 <ChevronDown
137 className={`w-4 h-4 text-gray-400 transition-transform ${isOpen ? "rotate-180" : ""}`}
138 />
139 </div>
140 </div>
141 </div>
142
143 {isOpen && (
144 <div className="absolute z-[9999] w-full mt-1 bg-white dark:bg-gray-800 border-2 border-gray-300 dark:border-gray-600 rounded-md shadow-lg max-h-60 overflow-visible">
145 {/* Search Input */}
146 <div className="p-2 border-b border-gray-200 dark:border-gray-700">
147 <div className="relative">
148 <Search className="absolute left-2.5 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
149 <Input
150 ref={searchInputRef}
151 type="text"
152 placeholder={searchPlaceholder}
153 value={searchTerm}
154 onChange={(e) => setSearchTerm(e.target.value)}
155 className="pl-8 h-8"
156 onClick={(e) => e.stopPropagation()}
157 />
158 </div>
159 </div>
160
161 {/* Options List */}
162 <div className="max-h-48 overflow-y-auto overflow-x-visible">
163 {filteredOptions.length === 0 ? (
164 <div className="px-3 py-2 text-sm text-gray-500 dark:text-gray-400 text-center">
165 {__("No options found", "yatra")}
166 </div>
167 ) : (
168 filteredOptions.map((option) => (
169 <button
170 key={option.value}
171 type="button"
172 onClick={() => handleSelect(option.value)}
173 className={`w-full px-3 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${
174 value === option.value
175 ? "bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400"
176 : "text-gray-900 dark:text-white"
177 }`}
178 >
179 <div className="flex items-center justify-between">
180 <span>{option.label}</span>
181 {showValueId && option.value && option.value !== "" && (
182 <span className="text-xs text-gray-500 dark:text-gray-400 ml-2 font-medium">
183 ID: {option.value}
184 </span>
185 )}
186 </div>
187 </button>
188 ))
189 )}
190 </div>
191 </div>
192 )}
193 </div>
194 );
195 };
196