1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
use std::sync::Arc;

use dioxus_core::{
    prelude::consume_context,
    use_hook,
    AttributeValue,
};
use dioxus_hooks::{
    use_context,
    use_memo,
};
use dioxus_signals::{
    Memo,
    ReadOnlySignal,
    Readable,
    Signal,
    Writable,
};
use freya_core::{
    accessibility::{
        AccessibilityFocusStrategy,
        AccessibilityGenerator,
        ACCESSIBILITY_ROOT_ID,
    },
    custom_attributes::CustomAttributeValues,
    event_loop_messages::EventLoopMessage,
    platform_state::NavigationMode,
    types::{
        AccessibilityId,
        AccessibilityNode,
    },
};
use freya_elements::events::{
    keyboard::Code,
    KeyboardEvent,
};

use crate::{
    use_platform,
    NavigationMark,
    UsePlatform,
};

/// Manage the focus operations of given Node
#[derive(Clone, Copy)]
pub struct UseFocus {
    id: AccessibilityId,
    is_focused_with_keyboard: Memo<bool>,
    is_focused: Memo<bool>,
    navigation_mode: Signal<NavigationMode>,
    navigation_mark: Signal<NavigationMark>,
    platform: UsePlatform,
    focused_id: Signal<AccessibilityId>,
    focused_node: Signal<AccessibilityNode>,
}

impl UseFocus {
    pub fn new_id() -> AccessibilityId {
        let accessibility_generator = consume_context::<Arc<AccessibilityGenerator>>();

        AccessibilityId(accessibility_generator.new_id())
    }

    /// Focus this node
    pub fn focus(&mut self) {
        if !*self.is_focused.peek() {
            self.platform
                .focus(AccessibilityFocusStrategy::Node(self.id));
        }
    }

    /// Get the node focus ID
    pub fn id(&self) -> AccessibilityId {
        self.id
    }

    /// Create a node focus ID attribute
    pub fn attribute(&self) -> AttributeValue {
        Self::attribute_for_id(self.id)
    }

    /// Create a node focus ID attribute
    pub fn attribute_for_id(id: AccessibilityId) -> AttributeValue {
        AttributeValue::any_value(CustomAttributeValues::AccessibilityId(id))
    }

    /// Subscribe to focus changes where this node was involved.
    pub fn is_focused(&self) -> bool {
        *self.is_focused.read()
    }

    /// Subscribe to focus changes where this node was involved and the keyboard was used.
    pub fn is_focused_with_keyboard(&self) -> bool {
        *self.is_focused_with_keyboard.read()
            && *self.navigation_mode.read() == NavigationMode::Keyboard
    }

    /// Unfocus the currently focused node.
    pub fn unfocus(&mut self) {
        self.platform
            .send(EventLoopMessage::FocusAccessibilityNode(
                AccessibilityFocusStrategy::Node(ACCESSIBILITY_ROOT_ID),
            ))
            .ok();
    }

    /// Useful if you want to trigger an action when `Enter` or `Space` is pressed and this Node was focused with the keyboard.
    pub fn validate_keydown(&self, e: &KeyboardEvent) -> bool {
        (e.data.code == Code::Enter || e.data.code == Code::Space)
            && self.is_focused_with_keyboard()
    }

    /// Prevent navigating the accessible nodes with the keyboard.
    /// You must use this this inside of a `onglobalkeydown` event handler.
    pub fn prevent_navigation(&mut self) {
        self.navigation_mark.write().set_allowed(false);
    }

    /// Get a readable of the currently focused Node Id.
    pub fn focused_id(&self) -> ReadOnlySignal<AccessibilityId> {
        self.focused_id.into()
    }

    /// Get a readable of the currently focused Node.
    pub fn focused_node(&self) -> ReadOnlySignal<AccessibilityNode> {
        self.focused_node.into()
    }
}

/// Create a focus manager for a node.
///
/// With this you can focus this node whenever you want or subscribe to any focus change,
/// this way you can style your element based on its focus state.
///
/// ### Simple example
///
/// ```rust
/// # use freya::prelude::*;
/// fn app() -> Element {
///     // Create a focus instance
///     let mut my_focus = use_focus();
///
///     rsx!(
///         rect {
///             // Bind the focus to this `rect`
///             a11y_id: my_focus.attribute(),
///             // This will focus this element and effectively cause a rerender updating the returned value of `is_focused()`
///             onclick: move |_| my_focus.focus(),
///             label {
///                 "Am I focused? {my_focus.is_focused()}"
///             }
///         }
///     )
/// }
/// ```
///
/// ### Style based on state
///
/// ```rust
/// # use freya::prelude::*;
/// fn app() -> Element {
///     let mut my_focus = use_focus();
///
///     let background = if my_focus.is_focused() {
///         "red"
///     } else {
///         "blue"
///     };
///
///     rsx!(
///         rect {
///             background,
///             a11y_id: my_focus.attribute(),
///             onclick: move |_| my_focus.focus(),
///             label {
///                 "Focus me!"
///             }
///         }
///     )
/// }
/// ```
///
/// ### Keyboard navigation
///
/// Elements can also be selected with the keyboard, for those cases you can also subscribe by calling [UseFocus::is_focused_with_keyboard].
///
/// ```rust
/// # use freya::prelude::*;
/// fn app() -> Element {
///     let mut my_focus = use_focus();
///
///     let background = if my_focus.is_focused_with_keyboard() {
///         "red"
///     } else {
///         "blue"
///     };
///
///     rsx!(
///         rect {
///             background,
///             a11y_id: my_focus.attribute(),
///             label {
///                 "Focus me!"
///             }
///         }
///     )
/// }
/// ```
pub fn use_focus() -> UseFocus {
    let id = use_hook(UseFocus::new_id);

    use_focus_from_id(id)
}

/// Same as [use_focus] but providing a Node instead of generating a new one.
///
/// This is an advance hook so you probably just want to use [use_focus].
pub fn use_focus_from_id(id: AccessibilityId) -> UseFocus {
    let focused_id = use_context::<Signal<AccessibilityId>>();
    let focused_node = use_context::<Signal<AccessibilityNode>>();
    let navigation_mode = use_context::<Signal<NavigationMode>>();
    let navigation_mark = use_context::<Signal<NavigationMark>>();
    let platform = use_platform();

    let is_focused = use_memo(move || id == *focused_id.read());

    let is_focused_with_keyboard =
        use_memo(move || *is_focused.read() && *navigation_mode.read() == NavigationMode::Keyboard);

    use_hook(move || UseFocus {
        id,
        is_focused,
        is_focused_with_keyboard,
        navigation_mode,
        navigation_mark,
        platform,
        focused_id,
        focused_node,
    })
}