Skip to main content

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}