1use crate::components::config_provider::{ComponentSize, use_config};
2use crate::components::control::{ControlStatus, push_status_class};
3use crate::components::form::{form_value_to_string, use_form_item_control};
4use crate::components::select_base::{
5 DropdownLayer, SelectOption, filter_options_by_query, use_dropdown_layer,
6};
7use dioxus::events::KeyboardEvent;
8use dioxus::prelude::*;
9
10#[derive(Props, Clone, PartialEq)]
18pub struct AutoCompleteProps {
19 #[props(optional)]
21 pub options: Option<Vec<SelectOption>>,
22 #[props(optional)]
24 pub value: Option<String>,
25 #[props(optional)]
27 pub default_value: Option<String>,
28 #[props(optional)]
30 pub placeholder: Option<String>,
31 #[props(default)]
33 pub allow_clear: bool,
34 #[props(default)]
36 pub disabled: bool,
37 #[props(optional)]
39 pub status: Option<ControlStatus>,
40 #[props(optional)]
42 pub size: Option<ComponentSize>,
43 #[props(optional)]
45 pub class: Option<String>,
46 #[props(optional)]
47 pub style: Option<String>,
48 #[props(optional)]
50 pub dropdown_class: Option<String>,
51 #[props(optional)]
52 pub dropdown_style: Option<String>,
53 #[props(optional)]
55 pub on_change: Option<EventHandler<String>>,
56 #[props(optional)]
58 pub on_search: Option<EventHandler<String>>,
59 #[props(optional)]
61 pub on_select: Option<EventHandler<String>>,
62}
63
64#[component]
66pub fn AutoComplete(props: AutoCompleteProps) -> Element {
67 let AutoCompleteProps {
68 options,
69 value,
70 default_value,
71 placeholder,
72 allow_clear,
73 disabled,
74 status,
75 size,
76 class,
77 style,
78 dropdown_class,
79 dropdown_style,
80 on_change,
81 on_search,
82 on_select,
83 } = props;
84
85 let config = use_config();
86 let form_control = use_form_item_control();
87
88 let final_size = size.unwrap_or(config.size);
89
90 let is_disabled =
91 disabled || config.disabled || form_control.as_ref().is_some_and(|ctx| ctx.is_disabled());
92
93 let initial_inner = default_value.clone().unwrap_or_default();
95 let inner_value: Signal<String> = use_signal(|| initial_inner);
96
97 let has_form = form_control.is_some();
98 let prop_value = value.clone();
99 let controlled_by_prop = has_form || prop_value.is_some();
100
101 let current_value: String = if let Some(ctx) = form_control.as_ref() {
103 form_value_to_string(ctx.value())
104 } else if let Some(v) = prop_value {
105 v
106 } else {
107 inner_value.read().clone()
108 };
109
110 let open_state: Signal<bool> = use_signal(|| false);
112 let internal_click_flag: Signal<bool> = use_signal(|| false);
113
114 #[cfg(target_arch = "wasm32")]
115 {
116 let mut open_for_global = open_state;
117 let mut internal_flag = internal_click_flag;
118 use_effect(move || {
119 use wasm_bindgen::{JsCast, closure::Closure};
120
121 if let Some(window) = web_sys::window() {
122 if let Some(document) = window.document() {
123 let target: web_sys::EventTarget = document.into();
124 let handler = Closure::<dyn FnMut(web_sys::MouseEvent)>::wrap(Box::new(
125 move |_evt: web_sys::MouseEvent| {
126 let mut flag = internal_flag;
127 if *flag.read() {
128 flag.set(false);
129 return;
130 }
131 let mut open_signal = open_for_global;
132 if *open_signal.read() {
133 open_signal.set(false);
134 }
135 },
136 ));
137 let _ = target.add_event_listener_with_callback(
138 "click",
139 handler.as_ref().unchecked_ref(),
140 );
141 handler.forget();
142 }
143 }
144 });
145 }
146
147 let placeholder_str = placeholder.unwrap_or_default();
148
149 let has_any_options = options
150 .as_ref()
151 .map(|opts| !opts.is_empty())
152 .unwrap_or(false);
153
154 let filtered_options: Vec<SelectOption> = if let Some(opts) = options.as_ref() {
156 if current_value.is_empty() {
157 opts.clone()
158 } else {
159 filter_options_by_query(opts, ¤t_value)
160 }
161 } else {
162 Vec::new()
163 };
164
165 let open_flag = *open_state.read();
166 let DropdownLayer { z_index, .. } = use_dropdown_layer(open_flag);
167 let current_z = *z_index.read();
168
169 let on_change_cb = on_change;
171 let on_search_cb = on_search;
172 let on_select_cb = on_select;
173 let inner_for_change = inner_value;
174 let form_for_handlers = form_control.clone();
175 let open_for_toggle = open_state;
176 let internal_click_for_toggle = internal_click_flag;
177
178 let dropdown_class_attr = {
179 let mut list = vec!["adui-select-dropdown".to_string()];
180 if let Some(extra) = dropdown_class {
181 list.push(extra);
182 }
183 list.join(" ")
184 };
185 let dropdown_style_attr = format!(
186 "position: absolute; top: 100%; left: 0; min-width: 100%; z-index: {}; {}",
187 current_z,
188 dropdown_style.unwrap_or_default()
189 );
190
191 let mut class_list = vec!["adui-select".to_string()];
193 if is_disabled {
194 class_list.push("adui-select-disabled".into());
195 }
196 if open_flag {
197 class_list.push("adui-select-open".into());
198 }
199 match final_size {
200 ComponentSize::Small => class_list.push("adui-select-sm".into()),
201 ComponentSize::Large => class_list.push("adui-select-lg".into()),
202 ComponentSize::Middle => {}
203 }
204 push_status_class(&mut class_list, status);
205 if let Some(extra) = class {
206 class_list.push(extra);
207 }
208 let class_attr = class_list.join(" ");
209 let style_attr = style.unwrap_or_default();
210
211 let handle_input_change = move |next: String| {
213 if let Some(ctx) = form_for_handlers.as_ref() {
214 ctx.set_string(next.clone());
215 } else if !controlled_by_prop {
216 let mut inner = inner_for_change;
217 inner.set(next.clone());
218 }
219 if let Some(cb) = on_change_cb {
220 cb.call(next.clone());
221 }
222 if let Some(cb) = on_search_cb {
223 cb.call(next);
224 }
225 };
226
227 rsx! {
228 div {
229 class: "adui-select-root",
230 style: "position: relative; display: inline-block;",
231 div {
232 class: "{class_attr}",
233 style: "{style_attr}",
234 role: "combobox",
235 "aria-expanded": open_flag,
236 "aria-disabled": is_disabled,
237 onclick: move |_| {
238 if is_disabled {
239 return;
240 }
241 let mut flag = internal_click_for_toggle;
242 flag.set(true);
243 let mut open_signal = open_for_toggle;
244 let current = *open_signal.read();
245 if has_any_options {
247 open_signal.set(!current);
248 }
249 },
250 input {
252 class: "adui-input",
253 disabled: is_disabled || config.disabled,
254 value: "{current_value}",
255 placeholder: "{placeholder_str}",
256 onfocus: move |_| {
257 if is_disabled || !has_any_options {
258 return;
259 }
260 let mut flag = internal_click_for_toggle;
261 flag.set(true);
262 let mut open_signal = open_for_toggle;
263 open_signal.set(true);
264 },
265 oninput: {
266 let handle_input_change = handle_input_change.clone();
267 move |evt| {
268 if is_disabled {
269 return;
270 }
271 let mut flag = internal_click_for_toggle;
272 flag.set(true);
273 let next = evt.value();
274 handle_input_change(next);
275 if has_any_options {
276 let mut open_signal = open_for_toggle;
277 open_signal.set(true);
278 }
279 }
280 },
281 onkeydown: move |evt: KeyboardEvent| {
282 if is_disabled {
283 return;
284 }
285 use dioxus::prelude::Key;
286 if matches!(evt.key(), Key::Escape) {
287 let mut open_signal = open_for_toggle;
288 open_signal.set(false);
289 }
290 }
291 }
292 if allow_clear && !current_value.is_empty() && !is_disabled {
293 {
294 let handle_input_change = handle_input_change.clone();
295 rsx! {
296 span {
297 class: "adui-select-clear",
298 onclick: move |_| {
299 handle_input_change(String::new());
300 let mut open_signal = open_for_toggle;
301 open_signal.set(false);
302 },
303 "×"
304 }
305 }
306 }
307 }
308 }
309 if open_flag && !filtered_options.is_empty() {
310 div {
311 class: "{dropdown_class_attr}",
312 style: "{dropdown_style_attr}",
313 role: "listbox",
314 ul { class: "adui-select-item-list",
315 {filtered_options.iter().map(|opt| {
316 let key = opt.key.clone();
317 let label = opt.label.clone();
318 let disabled_opt = opt.disabled || is_disabled;
319 let internal_click_for_item = internal_click_flag;
320 let form_for_click = form_control.clone();
321 let inner_for_click = inner_value;
322 rsx! {
323 li {
324 class: {
325 let mut classes = vec!["adui-select-item".to_string()];
326 if disabled_opt {
327 classes.push("adui-select-item-option-disabled".into());
328 }
329 classes.join(" ")
330 },
331 role: "option",
332 onclick: move |_| {
333 if disabled_opt {
334 return;
335 }
336 let mut flag = internal_click_for_item;
337 flag.set(true);
338
339 let next_text = label.clone();
341 if let Some(ctx) = form_for_click.as_ref() {
342 ctx.set_string(next_text.clone());
343 } else if !controlled_by_prop {
344 let mut inner = inner_for_click;
345 inner.set(next_text.clone());
346 }
347 if let Some(cb) = on_change_cb {
348 cb.call(next_text.clone());
349 }
350 if let Some(cb) = on_select_cb {
351 cb.call(key.clone());
352 }
353
354 let mut open_signal = open_for_toggle;
355 open_signal.set(false);
356 },
357 "{label}"
358 }
359 }
360 })}
361 }
362 }
363 }
364 }
365 }
366}
367
368#[cfg(test)]
369mod auto_complete_tests {
370 use super::*;
371 use crate::components::select_base::SelectOption;
372
373 #[test]
374 fn auto_complete_props_defaults() {
375 assert_eq!(false, false); assert_eq!(false, false); }
381
382 #[test]
383 fn auto_complete_props_optional_fields() {
384 let _options: Option<Vec<SelectOption>> = None;
387 let _value: Option<String> = None;
388 let _default_value: Option<String> = None;
389 let _placeholder: Option<String> = None;
390 let _status: Option<ControlStatus> = None;
391 let _size: Option<ComponentSize> = None;
392 let _class: Option<String> = None;
393 let _style: Option<String> = None;
394 let _dropdown_class: Option<String> = None;
395 let _dropdown_style: Option<String> = None;
396 assert!(true);
398 }
399
400 #[test]
401 fn auto_complete_props_with_values() {
402 let options = Some(vec![
404 SelectOption {
405 key: "1".to_string(),
406 label: "Option 1".to_string(),
407 disabled: false,
408 },
409 SelectOption {
410 key: "2".to_string(),
411 label: "Option 2".to_string(),
412 disabled: false,
413 },
414 ]);
415 assert!(options.is_some());
416 assert_eq!(options.as_ref().unwrap().len(), 2);
417
418 let value = Some("test".to_string());
419 assert_eq!(value.as_ref().unwrap(), "test");
420
421 let status = Some(ControlStatus::Error);
422 assert_eq!(status.unwrap(), ControlStatus::Error);
423
424 let size = Some(ComponentSize::Large);
425 assert_eq!(size.unwrap(), ComponentSize::Large);
426 }
427
428 #[test]
429 fn auto_complete_props_boolean_defaults() {
430 let allow_clear_default = false;
432 let disabled_default = false;
433 assert_eq!(allow_clear_default, false);
434 assert_eq!(disabled_default, false);
435 }
436
437 #[test]
438 fn filter_options_by_query_used_by_autocomplete() {
439 let options = vec![
441 SelectOption {
442 key: "1".to_string(),
443 label: "Apple".to_string(),
444 disabled: false,
445 },
446 SelectOption {
447 key: "2".to_string(),
448 label: "Banana".to_string(),
449 disabled: false,
450 },
451 SelectOption {
452 key: "3".to_string(),
453 label: "Cherry".to_string(),
454 disabled: false,
455 },
456 ];
457
458 let filtered = filter_options_by_query(&options, "app");
459 assert_eq!(filtered.len(), 1);
460 assert_eq!(filtered[0].label, "Apple");
461
462 let filtered_empty = filter_options_by_query(&options, "");
463 assert_eq!(filtered_empty.len(), 3);
464
465 let filtered_case_insensitive = filter_options_by_query(&options, "BANANA");
466 assert_eq!(filtered_case_insensitive.len(), 1);
467 assert_eq!(filtered_case_insensitive[0].label, "Banana");
468 }
469}