impulse_thaw/auto_complete/
mod.rs

1mod auto_complete_option;
2mod types;
3
4pub use auto_complete_option::AutoCompleteOption;
5pub use types::*;
6
7use crate::{
8    combobox::listbox::{listbox_keyboard_event, Listbox},
9    ComponentRef, Input, InputPrefix, InputRef, InputSuffix,
10    _aria::use_active_descendant,
11};
12use leptos::{context::Provider, either::Either, html, prelude::*};
13use std::collections::HashMap;
14use thaw_components::{Follower, FollowerPlacement, FollowerWidth};
15use thaw_utils::{class_list, mount_style, ArcOneCallback, BoxOneCallback, Model};
16
17#[component]
18pub fn AutoComplete(
19    #[prop(optional, into)] class: MaybeProp<String>,
20    /// Input of autocomplete.
21    #[prop(optional, into)]
22    value: Model<String>,
23    /// Autocomplete's placeholder.
24    #[prop(optional, into)]
25    placeholder: MaybeProp<String>,
26    // Whether to clear after selection.
27    #[prop(optional, into)] clear_after_select: Signal<bool>,
28    /// Whether to blur after selection.
29    #[prop(optional, into)]
30    blur_after_select: Signal<bool>,
31    // On select callback function.
32    #[prop(optional, into)] on_select: Option<BoxOneCallback<String>>,
33    /// Whether the input is disabled.
34    #[prop(optional, into)]
35    disabled: Signal<bool>,
36    /// Size of the input.
37    #[prop(optional, into)]
38    size: Signal<AutoCompleteSize>,
39    #[prop(optional)] auto_complete_prefix: Option<AutoCompletePrefix>,
40    #[prop(optional)] auto_complete_suffix: Option<AutoCompleteSuffix>,
41    #[prop(optional)] comp_ref: ComponentRef<AutoCompleteRef>,
42    #[prop(optional)] children: Option<Children>,
43) -> impl IntoView {
44    mount_style("auto-complete", include_str!("./auto-complete.css"));
45    let input_ref = ComponentRef::<InputRef>::new();
46    let listbox_ref = NodeRef::<html::Div>::new();
47    let open_listbox = RwSignal::new(false);
48    let options = StoredValue::new(HashMap::<String, String>::new());
49
50    let allow_value = move |_| {
51        if !open_listbox.get_untracked() {
52            open_listbox.set(true);
53        }
54        true
55    };
56
57    let select_option = ArcOneCallback::new(move |option_value: String| {
58        if clear_after_select.get_untracked() {
59            value.set(String::new());
60        } else {
61            value.set(option_value.clone());
62        }
63        if let Some(on_select) = on_select.as_ref() {
64            on_select(option_value);
65        }
66
67        open_listbox.set(false);
68        if blur_after_select.get_untracked() {
69            if let Some(input_ref) = input_ref.get_untracked() {
70                input_ref.blur();
71            }
72        }
73    });
74
75    let (set_listbox, active_descendant_controller) =
76        use_active_descendant(move |el| el.class_list().contains("thaw-auto-complete-option"));
77    let on_blur = {
78        let active_descendant_controller = active_descendant_controller.clone();
79        move |_| {
80            active_descendant_controller.blur();
81            open_listbox.set(false);
82        }
83    };
84    let on_keydown = {
85        let select_option = select_option.clone();
86        move |e| {
87            let select_option = select_option.clone();
88            listbox_keyboard_event(
89                e,
90                open_listbox,
91                false,
92                &active_descendant_controller,
93                move |option| {
94                    options.with_value(|options| {
95                        if let Some(value) = options.get(&option.id()) {
96                            select_option(value.clone());
97                        }
98                    });
99                },
100            );
101        }
102    };
103
104    comp_ref.load(AutoCompleteRef { input_ref });
105
106    view! {
107        <crate::_binder::Binder>
108            <div class=class_list!["thaw-auto-complete", class] on:keydown=on_keydown>
109                <Input
110                    value
111                    placeholder
112                    disabled
113                    on_focus=move |_| open_listbox.set(true)
114                    on_blur=on_blur
115                    allow_value
116                    size=Signal::derive(move || size.get().into())
117                    comp_ref=input_ref
118                >
119                    <InputPrefix if_=auto_complete_prefix.is_some() slot>
120
121                        {if let Some(auto_complete_prefix) = auto_complete_prefix {
122                            Some((auto_complete_prefix.children)())
123                        } else {
124                            None
125                        }}
126
127                    </InputPrefix>
128                    <InputSuffix if_=auto_complete_suffix.is_some() slot>
129
130                        {if let Some(auto_complete_suffix) = auto_complete_suffix {
131                            Some((auto_complete_suffix.children)())
132                        } else {
133                            None
134                        }}
135
136                    </InputSuffix>
137                </Input>
138            </div>
139            <Follower
140                slot
141                show=open_listbox
142                placement=FollowerPlacement::BottomStart
143                width=FollowerWidth::Target
144                auto_height=true
145            >
146                <Provider value=AutoCompleteInjection {
147                    value,
148                    select_option,
149                    options,
150                }>
151                    <Listbox set_listbox listbox_ref class="thaw-auto-complete__listbox">
152                        {if let Some(children) = children {
153                            Either::Left(children())
154                        } else {
155                            Either::Right(())
156                        }}
157                    </Listbox>
158                </Provider>
159            </Follower>
160        </crate::_binder::Binder>
161    }
162}