use js_sys::{Function, Object, Reflect};
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{Element, FocusEvent, HtmlElement, KeyboardEvent, Node};
pub const FOCUSABLE_SELECTOR: &str = concat!(
"a[href], area[href], button:not([disabled]), ",
"input:not([disabled]):not([type=hidden]), ",
"select:not([disabled]), textarea:not([disabled]), ",
"[tabindex]:not([tabindex=\"-1\"])"
);
pub struct Saved(Option<HtmlElement>);
pub fn save() -> Saved {
Saved(active_html_element())
}
pub fn restore(saved: Saved) {
let Some(el) = saved.0 else { return };
let node: &Node = el.as_ref();
if !node.is_connected() {
return;
}
focus_no_scroll(&el);
}
pub fn focus_no_scroll(el: &HtmlElement) {
let opts = Object::new();
let _ = Reflect::set(&opts, &JsValue::from_str("preventScroll"), &JsValue::TRUE);
let Ok(v) = Reflect::get(el.as_ref(), &JsValue::from_str("focus")) else {
let _ = el.focus();
return;
};
let Ok(f) = v.dyn_into::<Function>() else {
let _ = el.focus();
return;
};
let _ = f.call1(el.as_ref(), &opts);
}
pub fn focus_element_no_scroll(el: &Element) {
let Ok(html) = el.clone().dyn_into::<HtmlElement>() else {
return;
};
focus_no_scroll(&html);
}
pub fn blur() {
let Some(window) = web_sys::window() else {
return;
};
let Some(doc) = window.document() else { return };
let Some(active) = doc.active_element() else {
return;
};
if let Some(body) = doc.body() {
let body_el: &Element = body.as_ref();
if active == *body_el {
return;
}
}
if let Ok(html) = active.dyn_into::<HtmlElement>() {
let _ = html.blur();
}
}
pub fn auto_focus_first(container: &Element) -> bool {
let Ok(Some(first)) = container.query_selector(FOCUSABLE_SELECTOR) else {
return false;
};
if let Ok(html) = first.dyn_into::<HtmlElement>() {
focus_no_scroll(&html);
return true;
}
false
}
pub fn trap(container: &Element) -> TrapHandle {
let Some(doc) = web_sys::window().and_then(|w| w.document()) else {
return TrapHandle::empty();
};
let container_for_key = container.clone();
let keydown: Closure<dyn FnMut(KeyboardEvent)> =
Closure::wrap(Box::new(move |ev: KeyboardEvent| {
if ev.key() != "Tab" {
return;
}
let container = &container_for_key;
let node: &Node = container.as_ref();
if !node.is_connected() {
return;
}
let focusables = collect_focusables(container);
if focusables.is_empty() {
ev.prevent_default();
return;
}
let last_idx = focusables.len() - 1;
let cur_idx =
active_html_element().and_then(|a| focusables.iter().position(|f| *f == a));
let next_idx = match (ev.shift_key(), cur_idx) {
(false, None) => 0,
(false, Some(i)) if i >= last_idx => 0,
(false, Some(i)) => i + 1,
(true, None) => last_idx,
(true, Some(0)) => last_idx,
(true, Some(i)) => i - 1,
};
ev.prevent_default();
focus_no_scroll(&focusables[next_idx]);
}) as Box<dyn FnMut(KeyboardEvent)>);
let container_for_in = container.clone();
let focusin: Closure<dyn FnMut(FocusEvent)> = Closure::wrap(Box::new(move |ev: FocusEvent| {
let container = &container_for_in;
let node: &Node = container.as_ref();
if !node.is_connected() {
return;
}
let Some(target) = ev.target() else { return };
let Ok(target_node) = target.dyn_into::<Node>() else {
return;
};
if container.contains(Some(&target_node)) {
return;
}
let focusables = collect_focusables(container);
if let Some(first) = focusables.first() {
focus_no_scroll(first);
}
})
as Box<dyn FnMut(FocusEvent)>);
let target: &web_sys::EventTarget = doc.as_ref();
let _ = target.add_event_listener_with_callback("keydown", keydown.as_ref().unchecked_ref());
let _ = target.add_event_listener_with_callback("focusin", focusin.as_ref().unchecked_ref());
TrapHandle {
inner: Some(TrapInner {
doc,
keydown,
focusin,
}),
}
}
pub struct TrapHandle {
inner: Option<TrapInner>,
}
struct TrapInner {
doc: web_sys::Document,
keydown: Closure<dyn FnMut(KeyboardEvent)>,
focusin: Closure<dyn FnMut(FocusEvent)>,
}
impl TrapHandle {
fn empty() -> Self {
Self { inner: None }
}
pub fn release(mut self) {
self.tear_down();
}
fn tear_down(&mut self) {
let Some(inner) = self.inner.take() else {
return;
};
let target: &web_sys::EventTarget = inner.doc.as_ref();
let _ = target
.remove_event_listener_with_callback("keydown", inner.keydown.as_ref().unchecked_ref());
let _ = target
.remove_event_listener_with_callback("focusin", inner.focusin.as_ref().unchecked_ref());
}
}
impl Drop for TrapHandle {
fn drop(&mut self) {
self.tear_down();
}
}
fn active_html_element() -> Option<HtmlElement> {
let window = web_sys::window()?;
let doc = window.document()?;
let active = doc.active_element()?;
active.dyn_into::<HtmlElement>().ok()
}
fn collect_focusables(container: &Element) -> Vec<HtmlElement> {
let Ok(list) = container.query_selector_all(FOCUSABLE_SELECTOR) else {
return Vec::new();
};
let len = list.length();
let mut out = Vec::with_capacity(len as usize);
for i in 0..len {
let Some(node) = list.item(i) else { continue };
if let Ok(el) = node.dyn_into::<HtmlElement>() {
out.push(el);
}
}
out
}
#[doc(hidden)]
pub fn __focusable_count(container: &Element) -> usize {
collect_focusables(container).len()
}