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}