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}