diff --git a/crates/yew-hooks/Cargo.toml b/crates/yew-hooks/Cargo.toml index 290f2f1..27d40fa 100644 --- a/crates/yew-hooks/Cargo.toml +++ b/crates/yew-hooks/Cargo.toml @@ -27,31 +27,33 @@ js-sys = "0.3" version = "0.3" features = [ "BeforeUnloadEvent", - "Element", - "UrlSearchParams", - "WebSocket", - "MessageEvent", - "CloseEvent", "BinaryType", - "DomRectReadOnly", - "Navigator", - "Geolocation", + "Blob", + "CloseEvent", "Coordinates", - "Position", - "PositionError", - "PositionOptions", - "File", "DataTransfer", "DataTransferItem", "DataTransferItemList", + "DomRectReadOnly", + "Element", + "File", + "Geolocation", + "HtmlCollection", + "HtmlLinkElement", "HtmlMediaElement", + "IntersectionObserver", + "IntersectionObserverEntry", + "MessageEvent", + "Navigator", + "Position", + "PositionError", + "PositionOptions", + "StorageEvent", "TimeRanges", "Touch", "TouchList", - "HtmlLinkElement", - "HtmlCollection", - "Blob", - "StorageEvent", + "UrlSearchParams", + "WebSocket", ] [dev-dependencies] diff --git a/crates/yew-hooks/src/hooks/mod.rs b/crates/yew-hooks/src/hooks/mod.rs index 6532801..4c62fa0 100644 --- a/crates/yew-hooks/src/hooks/mod.rs +++ b/crates/yew-hooks/src/hooks/mod.rs @@ -49,6 +49,7 @@ mod use_title; mod use_toggle; mod use_unmount; mod use_update; +mod use_visible; mod use_websocket; mod use_window_scroll; mod use_window_size; @@ -104,6 +105,7 @@ pub use use_title::*; pub use use_toggle::*; pub use use_unmount::*; pub use use_update::*; +pub use use_visible::*; pub use use_websocket::*; pub use use_window_scroll::*; pub use use_window_size::*; diff --git a/crates/yew-hooks/src/hooks/use_visible.rs b/crates/yew-hooks/src/hooks/use_visible.rs new file mode 100644 index 0000000..b9da72a --- /dev/null +++ b/crates/yew-hooks/src/hooks/use_visible.rs @@ -0,0 +1,69 @@ +use wasm_bindgen::{ + closure::Closure, + JsCast, +}; +use web_sys::{IntersectionObserver, IntersectionObserverEntry}; +use yew::{NodeRef, functional::*}; +use crate::use_effect_once; + + +#[hook] +/// Check if an element is visible. Internally, it uses an [`IntersectionObserver`] to receive +/// notifications from the browser whenever the visibility state of the node changes. +/// +/// Setting the sticky bit makes this hook disconnect the observer once the element is visible, and +/// keep the visibility set to `true`, even when it becomes invisible. This is often desired +/// for lazy-loading components. +/// +/// # Example +/// +/// ```rust +/// use yew::prelude::*; +/// use yew_hooks::use_visible; +/// +/// #[function_component] +/// fn MyComponent() -> Html { +/// let node = use_node_ref(); +/// let visible = use_visible(node.clone(), false); +/// html! { +///
+/// if visible { +///

{"I'm visible!"}

+/// } else { +///

{"I'm invisible!"}

+/// } +///
+/// } +/// } +/// ``` +pub fn use_visible(node: NodeRef, sticky: bool) -> bool { + // code adapted from: + // https://stackoverflow.com/questions/1462138/event-listener-for-when-element-becomes-visible + let visible = use_state_eq(|| false); + let visible_clone = visible.clone(); + use_effect_once(move || { + let closure = Closure::, IntersectionObserver)>::new( + move |entries: Vec, observer: IntersectionObserver| { + // determine if any part of this node is visible. + let visible = entries.iter().any(|entry| entry.intersection_ratio() > 0.0); + + // if the visibility changed, update the state. + if (visible != *visible_clone) && (!sticky || !*visible_clone) { + visible_clone.set(visible); + } + + // if this is sticky and it is currently visible, disconnect the observer. + if visible && sticky { + observer.disconnect(); + } + }, + ) + .into_js_value(); + let observer = IntersectionObserver::new(&closure.dyn_ref().unwrap()).unwrap(); + if let Some(node) = node.get() { + observer.observe(&node.dyn_ref().unwrap()); + } + move || observer.disconnect() + }); + *visible +}