yew_hooks/hooks/use_idle.rs
1use gloo::events::EventListener;
2use gloo::utils::window;
3use std::cell::RefCell;
4use std::ops::Deref;
5use std::rc::Rc;
6use std::time::Duration;
7use yew::prelude::*;
8
9/// Configuration options for the [`use_idle`] hook.
10#[derive(Clone, Debug, PartialEq)]
11pub struct UseIdleOptions {
12 /// The time in milliseconds after which the user is considered idle.
13 /// Default: 60_000 (1 minute)
14 pub timeout: u32,
15 /// Whether to listen for mouse events. Default: true
16 pub listen_mouse: bool,
17 /// Whether to listen for keyboard events. Default: true
18 pub listen_keyboard: bool,
19 /// Whether to listen for scroll events. Default: true
20 pub listen_scroll: bool,
21 /// Whether to listen for visibility change events. Default: true
22 pub listen_visibility: bool,
23 /// Whether to start in idle state. Default: false
24 pub initial_idle: bool,
25 /// Additional events to listen for.
26 pub events: Vec<std::borrow::Cow<'static, str>>,
27}
28
29impl Default for UseIdleOptions {
30 fn default() -> Self {
31 Self {
32 timeout: 60_000,
33 listen_mouse: true,
34 listen_keyboard: true,
35 listen_scroll: true,
36 listen_visibility: true,
37 initial_idle: false,
38 events: Vec::new(),
39 }
40 }
41}
42
43/// State handle for the [`use_idle`] hook.
44#[derive(Clone, Debug, PartialEq)]
45pub struct UseIdleHandle {
46 idle: UseStateHandle<bool>,
47 last_active: UseStateHandle<Option<f64>>,
48 reset: Callback<()>,
49}
50
51impl UseIdleHandle {
52 /// Returns whether the user is currently idle.
53 pub fn is_idle(&self) -> bool {
54 *self.idle
55 }
56
57 /// Manually reset the idle timer, marking the user as active.
58 pub fn reset_idle(&self) {
59 self.reset.emit(());
60 }
61
62 /// Get the timestamp of the last user activity, if available.
63 pub fn last_active(&self) -> Option<f64> {
64 *self.last_active
65 }
66}
67
68impl Deref for UseIdleHandle {
69 type Target = bool;
70
71 fn deref(&self) -> &Self::Target {
72 &self.idle
73 }
74}
75
76/// This hook tracks whether the user is idle (not interacting with the page).
77/// It listens for various user events (mouse, keyboard, scroll, etc.) and resets
78/// an internal timer. When the timer expires, the user is considered idle.
79///
80/// # Example
81///
82/// ```rust
83/// # use yew::prelude::*;
84/// #
85/// use yew_hooks::prelude::*;
86/// use std::time::Duration;
87///
88/// #[function_component(UseIdle)]
89/// fn idle() -> Html {
90/// let idle = use_idle(Duration::from_secs(30));
91///
92/// html! {
93/// <div>
94/// <p>
95/// <b>{ "User is idle: " }</b>
96/// { *idle }
97/// </p>
98/// <p>
99/// { "Move your mouse or press a key to reset the idle timer." }
100/// </p>
101/// </div>
102/// }
103/// }
104/// ```
105#[hook]
106pub fn use_idle(timeout: Duration) -> UseIdleHandle {
107 let options = UseIdleOptions {
108 timeout: timeout.as_millis() as u32,
109 ..Default::default()
110 };
111 use_idle_with_options(options)
112}
113
114/// This hook tracks whether the user is idle with custom configuration options.
115///
116/// # Example
117///
118/// ```rust
119/// # use yew::prelude::*;
120/// #
121/// use yew_hooks::prelude::*;
122/// use std::time::Duration;
123///
124/// #[function_component(UseIdleWithOptions)]
125/// fn idle_with_options() -> Html {
126/// let idle = use_idle_with_options(UseIdleOptions {
127/// timeout: Duration::from_secs(10).as_millis() as u32,
128/// listen_scroll: false,
129/// events: vec!["touchstart".into(), "touchend".into()],
130/// ..Default::default()
131/// });
132///
133/// html! {
134/// <div>
135/// <p>
136/// <b>{ "User is idle: " }</b>
137/// { *idle }
138/// </p>
139/// <p>
140/// { "Touch the screen or move your mouse to reset the idle timer." }
141/// </p>
142/// </div>
143/// }
144/// }
145/// ```
146#[hook]
147pub fn use_idle_with_options(options: UseIdleOptions) -> UseIdleHandle {
148 let idle = use_state_eq(|| options.initial_idle);
149 let last_active = use_state_eq(|| None::<f64>);
150
151 // Store timeout in a ref so we can cancel it
152 let timeout_ref = use_mut_ref(|| None::<gloo::timers::callback::Timeout>);
153
154 // Function to restart the idle timeout
155 let restart_idle_timeout = {
156 let idle = idle.clone();
157 let timeout_ref = timeout_ref.clone();
158 let timeout = options.timeout;
159 Rc::new(move || {
160 // Cancel any existing timeout
161 *timeout_ref.borrow_mut() = None;
162
163 // Create new timeout
164 if timeout > 0 {
165 let idle_clone = idle.clone();
166 *timeout_ref.borrow_mut() =
167 Some(gloo::timers::callback::Timeout::new(timeout, move || {
168 idle_clone.set(true);
169 }));
170 }
171 })
172 };
173
174 // Function to handle user activity
175 let handle_activity = {
176 let idle = idle.clone();
177 let last_active = last_active.clone();
178 let restart_idle_timeout = restart_idle_timeout.clone();
179 Rc::new(move || {
180 idle.set(false);
181 last_active.set(Some(js_sys::Date::now()));
182 restart_idle_timeout();
183 })
184 };
185
186 // Create a callback to reset the idle state
187 let reset = {
188 let handle_activity = handle_activity.clone();
189 Callback::from(move |()| {
190 handle_activity();
191 })
192 };
193
194 // Set up event listeners and timeout management
195 {
196 let handle_activity = handle_activity.clone();
197 let _restart_idle_timeout = restart_idle_timeout.clone();
198 let last_active = last_active.clone();
199 let options_clone = options.clone();
200
201 // Store event listeners in Rc<RefCell> so they can be kept alive
202 let listeners = Rc::new(RefCell::new(Vec::<EventListener>::new()));
203
204 use_effect_with((), {
205 let listeners = listeners.clone();
206 let timeout_ref = timeout_ref.clone();
207 move |_| {
208 let window = window();
209
210 // Clear any existing listeners
211 listeners.borrow_mut().clear();
212
213 // Mouse events
214 if options_clone.listen_mouse {
215 let ha1 = handle_activity.clone();
216 listeners.borrow_mut().push(EventListener::new(
217 &window,
218 "mousemove",
219 move |_| {
220 ha1();
221 },
222 ));
223
224 let ha2 = handle_activity.clone();
225 listeners.borrow_mut().push(EventListener::new(
226 &window,
227 "mousedown",
228 move |_| {
229 ha2();
230 },
231 ));
232
233 let ha3 = handle_activity.clone();
234 listeners.borrow_mut().push(EventListener::new(
235 &window,
236 "mouseup",
237 move |_| {
238 ha3();
239 },
240 ));
241 }
242
243 // Keyboard events
244 if options_clone.listen_keyboard {
245 let ha4 = handle_activity.clone();
246 listeners.borrow_mut().push(EventListener::new(
247 &window,
248 "keydown",
249 move |_| {
250 ha4();
251 },
252 ));
253
254 let ha5 = handle_activity.clone();
255 listeners
256 .borrow_mut()
257 .push(EventListener::new(&window, "keyup", move |_| {
258 ha5();
259 }));
260
261 let ha6 = handle_activity.clone();
262 listeners.borrow_mut().push(EventListener::new(
263 &window,
264 "keypress",
265 move |_| {
266 ha6();
267 },
268 ));
269 }
270
271 // Scroll events
272 if options_clone.listen_scroll {
273 let ha7 = handle_activity.clone();
274 listeners
275 .borrow_mut()
276 .push(EventListener::new(&window, "scroll", move |_| {
277 ha7();
278 }));
279 }
280
281 // Visibility change events
282 if options_clone.listen_visibility {
283 let ha8 = handle_activity.clone();
284 listeners.borrow_mut().push(EventListener::new(
285 &window,
286 "visibilitychange",
287 move |_| {
288 ha8();
289 },
290 ));
291 }
292
293 // Additional custom events
294 for event_name in options_clone.events.clone() {
295 let ha_custom = handle_activity.clone();
296 listeners.borrow_mut().push(EventListener::new(
297 &window,
298 event_name.clone(),
299 move |_| {
300 ha_custom();
301 },
302 ));
303 }
304
305 // Initial setup - mark as active and start timeout
306 if !options_clone.initial_idle {
307 handle_activity();
308 } else {
309 // If starting idle, just set the timestamp
310 last_active.set(Some(js_sys::Date::now()));
311 }
312
313 // Cleanup function
314 move || {
315 // Cancel any pending timeout
316 *timeout_ref.borrow_mut() = None;
317 // Clear listeners - they will be dropped automatically
318 listeners.borrow_mut().clear();
319 }
320 }
321 });
322 }
323
324 UseIdleHandle {
325 idle,
326 last_active,
327 reset,
328 }
329}