yewlish_roving_focus/
lib.rs1pub mod helpers;
2pub mod hooks;
3
4use helpers::*;
5use hooks::use_roving_iterator::*;
6use web_sys::{wasm_bindgen::JsCast, HtmlElement};
7use yew::prelude::*;
8use yewlish_utils::{
9 enums::{Dir, Orientation},
10 hooks::{use_children_as_html_collection, use_keydown},
11};
12
13#[derive(Clone, Debug, PartialEq, Properties)]
14pub struct RovingFocusProps {
15 pub children: Children,
16 #[prop_or_default]
17 pub class: Option<AttrValue>,
18 #[prop_or_default]
19 pub orientation: Orientation,
20 #[prop_or(Dir::Ltr)]
21 pub dir: Dir,
22 #[prop_or(true)]
23 pub r#loop: bool,
24 #[prop_or_default]
25 pub role: Option<AttrValue>,
26}
27
28#[function_component(RovingFocus)]
29pub fn roving_focus(props: &RovingFocusProps) -> Html {
30 let roving_iterator =
31 use_roving_iterator(props.children.len() as u32, props.r#loop, &props.dir);
32 let node_ref = use_node_ref();
33 let children_as_html_collection = use_children_as_html_collection(node_ref.clone());
34 let is_focus_entered = use_mut_ref(|| false);
35
36 let navigation_handler = {
37 let is_focus_entered = is_focus_entered.clone();
38 let children_as_html_collection = children_as_html_collection.clone();
39 let roving_iterator = roving_iterator.clone();
40 let orientation = props.orientation.clone();
41 let dir = props.dir.clone();
42
43 move |event: KeyboardEvent| {
44 let children_as_html_collection = children_as_html_collection.borrow();
45 let children = children_as_html_collection.as_ref();
46
47 if children.is_none() {
48 return;
49 }
50
51 let children = children.unwrap();
52
53 let next_index = match event.key().as_str() {
54 "ArrowDown" => match orientation {
55 Orientation::Vertical => roving_iterator.borrow_mut().next(&dir),
56 Orientation::Horizontal => roving_iterator.borrow_mut().prev(&dir),
57 },
58 "ArrowUp" => match orientation {
59 Orientation::Vertical => roving_iterator.borrow_mut().prev(&dir),
60 Orientation::Horizontal => roving_iterator.borrow_mut().next(&dir),
61 },
62 "ArrowLeft" => roving_iterator.borrow_mut().prev(&dir),
63 "ArrowRight" => roving_iterator.borrow_mut().next(&dir),
64 "Home" => roving_iterator.borrow_mut().first(&dir),
65 "End" => roving_iterator.borrow_mut().last(&dir),
66 "Tab" => {
67 let last_focusable_element_index = if event.shift_key() {
68 0
69 } else {
70 roving_iterator.borrow().length - 1
71 };
72
73 children
74 .item(last_focusable_element_index)
75 .and_then(|element| element.dyn_into::<HtmlElement>().ok())
76 .and_then(|html_element| get_focusable_element(&html_element))
77 .map(|html_element| {
78 if event.shift_key() {
79 get_prev_focusable_element(&html_element)
80 } else {
81 get_next_focusable_element(&html_element)
82 }
83 })
84 .and_then(|next_outside_focusable_element| {
85 next_outside_focusable_element.focus().err()
86 })
87 .map_or_else(
88 || {},
89 |error| log::error!("Failed to focus the element: {:?}", error),
90 );
91
92 *is_focus_entered.borrow_mut() = false;
93 None
94 }
95 _ => None,
96 };
97
98 if let Some(next_index) = next_index {
99 focus_child(children.item(next_index));
100 }
101 }
102 };
103
104 let navigate_through_children = use_keydown(
105 vec![
106 "ArrowDown".to_string(),
107 "ArrowUp".to_string(),
108 "ArrowLeft".to_string(),
109 "ArrowRight".to_string(),
110 "Home".to_string(),
111 "End".to_string(),
112 "Tab".to_string(),
113 ],
114 navigation_handler,
115 );
116
117 let focus_last_focused_child = use_callback(
118 (
119 roving_iterator.clone(),
120 children_as_html_collection.clone(),
121 is_focus_entered.clone(),
122 ),
123 move |_event: FocusEvent,
124 (roving_iterator, children_as_html_collection, is_focus_entered)| {
125 if *is_focus_entered.borrow() {
126 return;
127 }
128
129 let children_as_html_collection = children_as_html_collection.borrow();
130 let children = children_as_html_collection.as_ref();
131
132 if children.is_none() {
133 return;
134 }
135
136 let children = children.unwrap();
137 focus_child(children.item(roving_iterator.borrow().current));
138 *is_focus_entered.borrow_mut() = true;
139 },
140 );
141
142 html! {
143 <div role={props.role.clone()} class={&props.class} data-orientation={props.orientation.clone()} ref={node_ref} onfocusin={&focus_last_focused_child} onkeydown={&navigate_through_children}>
144 {for props.children.iter()}
145 </div>
146 }
147}