freya_hooks/
use_focus.rs

1use std::sync::Arc;
2
3use dioxus_core::{
4    prelude::consume_context,
5    use_hook,
6    AttributeValue,
7};
8use dioxus_hooks::{
9    use_context,
10    use_memo,
11};
12use dioxus_signals::{
13    Memo,
14    ReadOnlySignal,
15    Readable,
16    Signal,
17    Writable,
18};
19use freya_core::{
20    accessibility::{
21        AccessibilityFocusStrategy,
22        AccessibilityGenerator,
23        ACCESSIBILITY_ROOT_ID,
24    },
25    custom_attributes::CustomAttributeValues,
26    event_loop_messages::EventLoopMessage,
27    platform_state::NavigationMode,
28    types::{
29        AccessibilityId,
30        AccessibilityNode,
31    },
32};
33use freya_elements::events::{
34    keyboard::Code,
35    KeyboardEvent,
36};
37
38use crate::{
39    use_platform,
40    NavigationMark,
41    UsePlatform,
42};
43
44/// Manage the focus operations of given Node
45#[derive(Clone, Copy)]
46pub struct UseFocus {
47    id: AccessibilityId,
48    is_focused_with_keyboard: Memo<bool>,
49    is_focused: Memo<bool>,
50    navigation_mode: Signal<NavigationMode>,
51    navigation_mark: Signal<NavigationMark>,
52    platform: UsePlatform,
53    focused_id: Signal<AccessibilityId>,
54    focused_node: Signal<AccessibilityNode>,
55}
56
57impl UseFocus {
58    pub fn new_id() -> AccessibilityId {
59        let accessibility_generator = consume_context::<Arc<AccessibilityGenerator>>();
60
61        AccessibilityId(accessibility_generator.new_id())
62    }
63
64    /// Request to **focus** accessibility node. This will not immediately update [Self::is_focused].
65    pub fn request_focus(&mut self) {
66        if !*self.is_focused.peek() {
67            self.platform
68                .focus(AccessibilityFocusStrategy::Node(self.id));
69        }
70    }
71
72    /// Request to **unfocus** accessibility node. This will not immediately update [Self::is_focused].
73    pub fn request_unfocus(&mut self) {
74        self.platform
75            .send(EventLoopMessage::FocusAccessibilityNode(
76                AccessibilityFocusStrategy::Node(ACCESSIBILITY_ROOT_ID),
77            ))
78            .ok();
79    }
80
81    /// Focus a given [AccessibilityId].
82    pub fn focus_id(id: AccessibilityId) {
83        UsePlatform::current().focus(AccessibilityFocusStrategy::Node(id));
84    }
85
86    /// Get [AccessibilityId] of this accessibility node.
87    pub fn id(&self) -> AccessibilityId {
88        self.id
89    }
90
91    /// Create a [freya_elements::elements::rect::a11y_id] attribute value for this accessibility node.
92    pub fn attribute(&self) -> AttributeValue {
93        Self::attribute_for_id(self.id)
94    }
95
96    /// Create a [freya_elements::elements::rect::a11y_id] attribute value for a given [AccessibilityId].
97    pub fn attribute_for_id(id: AccessibilityId) -> AttributeValue {
98        AttributeValue::any_value(CustomAttributeValues::AccessibilityId(id))
99    }
100
101    /// Subscribe to focus changes where this node was involved.
102    pub fn is_focused(&self) -> bool {
103        *self.is_focused.read()
104    }
105
106    /// Subscribe to focus changes where this node was involved and the keyboard was used.
107    pub fn is_focused_with_keyboard(&self) -> bool {
108        *self.is_focused_with_keyboard.read()
109            && *self.navigation_mode.read() == NavigationMode::Keyboard
110    }
111
112    /// Useful if you want to trigger an action when `Enter` or `Space` is pressed and this Node was focused with the keyboard.
113    pub fn validate_keydown(&self, e: &KeyboardEvent) -> bool {
114        (e.data.code == Code::Enter || e.data.code == Code::Space)
115            && self.is_focused_with_keyboard()
116    }
117
118    /// Prevent navigating the accessible nodes with the keyboard.
119    /// You must use this this inside of a `onglobalkeydown` event handler.
120    pub fn prevent_navigation(&mut self) {
121        self.navigation_mark.write().set_allowed(false);
122    }
123
124    /// Get a readable of the currently focused Node Id.
125    pub fn focused_id(&self) -> ReadOnlySignal<AccessibilityId> {
126        self.focused_id.into()
127    }
128
129    /// Get a readable of the currently focused Node.
130    pub fn focused_node(&self) -> ReadOnlySignal<AccessibilityNode> {
131        self.focused_node.into()
132    }
133}
134
135/// Create a focus manager for a node.
136///
137/// With this you can focus this node whenever you want or subscribe to any focus change,
138/// this way you can style your element based on its focus state.
139///
140/// ### Simple example
141///
142/// ```rust
143/// # use freya::prelude::*;
144/// fn app() -> Element {
145///     // Create a focus instance
146///     let mut my_focus = use_focus();
147///
148///     rsx!(
149///         rect {
150///             // Bind the focus to this `rect`
151///             a11y_id: my_focus.attribute(),
152///             // This will focus this element and effectively cause a rerender updating the returned value of `is_focused()`
153///             onclick: move |_| my_focus.request_focus(),
154///             label {
155///                 "Am I focused? {my_focus.is_focused()}"
156///             }
157///         }
158///     )
159/// }
160/// ```
161///
162/// ### Style based on state
163///
164/// ```rust
165/// # use freya::prelude::*;
166/// fn app() -> Element {
167///     let mut my_focus = use_focus();
168///
169///     let background = if my_focus.is_focused() {
170///         "red"
171///     } else {
172///         "blue"
173///     };
174///
175///     rsx!(
176///         rect {
177///             background,
178///             a11y_id: my_focus.attribute(),
179///             onclick: move |_| my_focus.request_focus(),
180///             label {
181///                 "Focus me!"
182///             }
183///         }
184///     )
185/// }
186/// ```
187///
188/// ### Keyboard navigation
189///
190/// Elements can also be selected with the keyboard, for those cases you can also subscribe by calling [UseFocus::is_focused_with_keyboard].
191///
192/// ```rust
193/// # use freya::prelude::*;
194/// fn app() -> Element {
195///     let mut my_focus = use_focus();
196///
197///     let background = if my_focus.is_focused_with_keyboard() {
198///         "red"
199///     } else {
200///         "blue"
201///     };
202///
203///     rsx!(
204///         rect {
205///             background,
206///             a11y_id: my_focus.attribute(),
207///             label {
208///                 "Focus me!"
209///             }
210///         }
211///     )
212/// }
213/// ```
214pub fn use_focus() -> UseFocus {
215    let id = use_hook(UseFocus::new_id);
216
217    use_focus_for_id(id)
218}
219
220/// Same as [use_focus] but providing a Node instead of generating a new one.
221///
222/// This is an advance hook so you probably just want to use [use_focus].
223pub fn use_focus_for_id(id: AccessibilityId) -> UseFocus {
224    let focused_id = use_context::<Signal<AccessibilityId>>();
225    let focused_node = use_context::<Signal<AccessibilityNode>>();
226    let navigation_mode = use_context::<Signal<NavigationMode>>();
227    let navigation_mark = use_context::<Signal<NavigationMark>>();
228    let platform = use_platform();
229
230    let is_focused = use_memo(move || id == *focused_id.read());
231
232    let is_focused_with_keyboard =
233        use_memo(move || *is_focused.read() && *navigation_mode.read() == NavigationMode::Keyboard);
234
235    use_hook(move || UseFocus {
236        id,
237        is_focused,
238        is_focused_with_keyboard,
239        navigation_mode,
240        navigation_mark,
241        platform,
242        focused_id,
243        focused_node,
244    })
245}