yewlish_roving_focus/
lib.rs

1pub 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}