Skip to main content

euv_ui/component/input/hook/
impl.rs

1use crate::*;
2
3/// Implementation of input functionality.
4impl UseEuvInput {
5    /// Creates a click event handler that toggles a boolean signal.
6    ///
7    /// Produces a `NativeEventHandler` that flips the value of the given
8    /// boolean signal on each click. Useful for toggle buttons, visibility
9    /// switches, and drawer open/close patterns.
10    ///
11    /// # Arguments
12    ///
13    /// - `Signal<bool>` - The boolean signal to toggle.
14    ///
15    /// # Returns
16    ///
17    /// - `Option<Rc<dyn Fn(Event)>>` - A click event handler that toggles the signal.
18    pub fn use_toggle(signal: Signal<bool>) -> Option<Rc<dyn Fn(Event)>> {
19        Some(Rc::new(move |_: Event| {
20            let current: bool = signal.get();
21            signal.set(!current);
22        }))
23    }
24
25    /// Creates an input event handler that updates a string signal.
26    ///
27    /// # Arguments
28    ///
29    /// - `Signal<String>` - The signal to update with the input value.
30    ///
31    /// # Returns
32    ///
33    /// - `Option<Rc<dyn Fn(Event)>>` - An input handler.
34    pub fn on_input_value(signal: Signal<String>) -> Option<Rc<dyn Fn(Event)>> {
35        Some(Rc::new(move |event: Event| {
36            let value: Option<String> = event.target().and_then(|target: EventTarget| {
37                if let Ok(input) = target.clone().dyn_into::<HtmlInputElement>() {
38                    return Some(input.value());
39                }
40                if let Ok(textarea) = target.clone().dyn_into::<HtmlTextAreaElement>() {
41                    return Some(textarea.value());
42                }
43                if let Ok(select) = target.clone().dyn_into::<HtmlSelectElement>() {
44                    return Some(select.value());
45                }
46                None
47            });
48            if let Some(value) = value {
49                signal.set(value);
50            }
51        }))
52    }
53
54    /// Creates a change event handler that updates a string signal.
55    ///
56    /// # Arguments
57    ///
58    /// - `Signal<String>` - The signal to update with the change value.
59    ///
60    /// # Returns
61    ///
62    /// - `Option<Rc<dyn Fn(Event)>>` - A change handler.
63    pub fn on_change_value(signal: Signal<String>) -> Option<Rc<dyn Fn(Event)>> {
64        Some(Rc::new(move |event: Event| {
65            let value: Option<String> = event.target().and_then(|target: EventTarget| {
66                if let Ok(input) = target.clone().dyn_into::<HtmlInputElement>() {
67                    return Some(input.value());
68                }
69                if let Ok(select) = target.clone().dyn_into::<HtmlSelectElement>() {
70                    return Some(select.value());
71                }
72                if let Ok(textarea) = target.clone().dyn_into::<HtmlTextAreaElement>() {
73                    return Some(textarea.value());
74                }
75                None
76            });
77            if let Some(value) = value {
78                signal.set(value);
79            }
80        }))
81    }
82
83    /// Creates a change event handler that updates a boolean signal from checkbox.
84    ///
85    /// # Arguments
86    ///
87    /// - `Signal<bool>` - The signal to update with the checked state.
88    ///
89    /// # Returns
90    ///
91    /// - `Option<Rc<dyn Fn(Event)>>` - A change handler.
92    pub fn on_change_checked(signal: Signal<bool>) -> Option<Rc<dyn Fn(Event)>> {
93        Some(Rc::new(move |event: Event| {
94            if let Some(target) = event.target()
95                && let Ok(input) = target.clone().dyn_into::<HtmlInputElement>()
96            {
97                signal.set(input.checked());
98            }
99        }))
100    }
101
102    /// Creates a focus event handler that scrolls the focused input into view.
103    ///
104    /// On mobile devices, focusing an input can open the virtual keyboard and obscure
105    /// the field. This handler waits briefly for the keyboard to appear, then checks
106    /// whether the input's bottom edge is below the visible viewport (using
107    /// `VisualViewport` when available). If so, it scrolls the page just enough to
108    /// leave a small gap between the input and the keyboard.
109    ///
110    /// # Returns
111    ///
112    /// - `Option<Rc<dyn Fn(Event)>>` - A focus handler.
113    pub fn on_focus_scroll_into_view() -> Option<Rc<dyn Fn(Event)>> {
114        Some(Rc::new(move |event: Event| {
115            let Some(target) = event.target() else {
116                return;
117            };
118            let Ok(element) = target.dyn_into::<HtmlElement>() else {
119                return;
120            };
121            let window: Window = window().expect("no global window exists");
122            let element_clone: HtmlElement = element.clone();
123            let window_clone: Window = window.clone();
124            if let Ok(Some(main_el)) = element.closest("main")
125                && let Ok(main) = main_el.dyn_into::<HtmlElement>()
126            {
127                let _ = main
128                    .style()
129                    .set_property("padding-bottom", KEYBOARD_EXTRA_PADDING);
130            }
131            let closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
132                let rect: DomRect = element_clone.get_bounding_client_rect();
133                let input_bottom: f64 = rect.bottom();
134                let viewport_height: f64 = window_clone
135                    .visual_viewport()
136                    .map(|viewport: VisualViewport| viewport.height())
137                    .unwrap_or_else(|| {
138                        window_clone
139                            .inner_height()
140                            .map(|height: JsValue| height.as_f64().unwrap_or(0.0))
141                            .unwrap_or(0.0)
142                    });
143                let visible_bottom: f64 = viewport_height - KEYBOARD_FOCUS_GAP;
144                if input_bottom > visible_bottom {
145                    let scroll_amount: f64 = input_bottom - visible_bottom;
146                    window_clone.scroll_by_with_x_and_y(0.0, scroll_amount);
147                }
148            }));
149            let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
150                closure.as_ref().unchecked_ref::<Function>(),
151                FOCUS_SCROLL_DELAY_MILLIS,
152            );
153            closure.forget();
154        }))
155    }
156
157    /// Creates a blur event handler that restores the page height after input focus.
158    ///
159    /// Removes the temporary `padding-bottom` injected by `on_focus_scroll_into_view`
160    /// so that the page returns to its original height once the virtual keyboard
161    /// is dismissed.
162    ///
163    /// # Returns
164    ///
165    /// - `Option<Rc<dyn Fn(Event)>>` - A blur handler.
166    pub fn on_blur_restore_height() -> Option<Rc<dyn Fn(Event)>> {
167        Some(Rc::new(move |event: Event| {
168            let Some(target) = event.target() else {
169                return;
170            };
171            let Ok(element) = target.dyn_into::<HtmlElement>() else {
172                return;
173            };
174            if let Ok(Some(main_el)) = element.closest("main")
175                && let Ok(main) = main_el.dyn_into::<HtmlElement>()
176            {
177                let _ = main.style().remove_property("padding-bottom");
178            }
179        }))
180    }
181}