freya_core/accessibility/
focus.rs

1use keyboard_types::{
2    Key,
3    Modifiers,
4};
5
6use crate::{
7    accessibility::id::AccessibilityId,
8    integration::{
9        ACCESSIBILITY_ROOT_ID,
10        AccessibilityGenerator,
11    },
12    platform::{
13        NavigationMode,
14        Platform,
15    },
16    prelude::{
17        AccessibilityFocusStrategy,
18        KeyboardEventData,
19        Memo,
20        ScreenReader,
21        UserEvent,
22        consume_root_context,
23        use_hook,
24        use_memo,
25    },
26};
27
28#[derive(Clone, Copy)]
29pub struct Focus {
30    a11y_id: AccessibilityId,
31}
32
33impl Focus {
34    pub fn create() -> Self {
35        Self::new_for_id(Self::new_id())
36    }
37
38    pub fn new_for_id(a11y_id: AccessibilityId) -> Self {
39        Self { a11y_id }
40    }
41
42    pub fn new_id() -> AccessibilityId {
43        let accessibility_generator = consume_root_context::<AccessibilityGenerator>();
44        AccessibilityId(accessibility_generator.new_id())
45    }
46
47    pub fn a11y_id(&self) -> AccessibilityId {
48        self.a11y_id
49    }
50
51    pub fn is_focused(&self) -> bool {
52        let platform = Platform::get();
53        *platform.focused_accessibility_id.peek() == self.a11y_id
54    }
55
56    pub fn is_focused_with_keyboard(&self) -> bool {
57        let platform = Platform::get();
58        *platform.focused_accessibility_id.peek() == self.a11y_id
59            && *platform.navigation_mode.peek() == NavigationMode::Keyboard
60    }
61
62    pub fn request_focus(&self) {
63        Platform::get().send(UserEvent::FocusAccessibilityNode(
64            AccessibilityFocusStrategy::Node(self.a11y_id),
65        ));
66    }
67
68    pub fn request_unfocus(&self) {
69        Platform::get().send(UserEvent::FocusAccessibilityNode(
70            AccessibilityFocusStrategy::Node(ACCESSIBILITY_ROOT_ID),
71        ));
72    }
73
74    pub fn is_pressed(event: &KeyboardEventData) -> bool {
75        let is_space = matches!(event.key, Key::Character(ref s) if s == " ");
76        let is_enter = event.key == Key::Enter;
77
78        if cfg!(target_os = "macos") {
79            let screen_reader = ScreenReader::get();
80            if screen_reader.is_on() {
81                is_space
82                    && event.modifiers.contains(Modifiers::CONTROL)
83                    && event.modifiers.contains(Modifiers::ALT)
84            } else {
85                is_enter || is_space
86            }
87        } else {
88            is_enter || is_space
89        }
90    }
91}
92
93pub fn use_focus() -> Focus {
94    use_hook(Focus::create)
95}
96
97#[derive(Clone, Copy, Debug, PartialEq)]
98pub enum FocusStatus {
99    Not,
100    Pointer,
101    Keyboard,
102}
103
104pub fn use_focus_status(focus: Focus) -> Memo<FocusStatus> {
105    use_memo(move || {
106        let platform = Platform::get();
107        let is_focused = *platform.focused_accessibility_id.read() == focus.a11y_id;
108        let is_keyboard = *platform.navigation_mode.read() == NavigationMode::Keyboard;
109
110        match (is_focused, is_keyboard) {
111            (true, false) => FocusStatus::Pointer,
112            (true, true) => FocusStatus::Keyboard,
113            _ => FocusStatus::Not,
114        }
115    })
116}