1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct FocusManager {
14 pub strategy: FocusStrategy,
16 container_id: Option<String>,
18 restore_target: Option<String>,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub enum FocusStrategy {
25 Auto,
27 Trap,
30 Restore,
32 Initial,
34 TrapAndRestore,
36}
37
38impl FocusManager {
39 pub fn new(strategy: FocusStrategy) -> Self {
41 Self {
42 strategy,
43 container_id: None,
44 restore_target: None,
45 }
46 }
47
48 pub fn dialog() -> Self {
50 Self::new(FocusStrategy::TrapAndRestore)
51 }
52
53 pub fn menu() -> Self {
55 Self::new(FocusStrategy::Initial)
56 }
57
58 pub fn popover() -> Self {
60 Self::new(FocusStrategy::Restore)
61 }
62
63 pub fn with_container(mut self, id: impl Into<String>) -> Self {
65 self.container_id = Some(id.into());
66 self
67 }
68
69 pub fn container_id(&self) -> Option<&str> {
71 self.container_id.as_deref()
72 }
73
74 pub fn save_restore_target(&mut self, element_id: impl Into<String>) {
76 self.restore_target = Some(element_id.into());
77 }
78
79 pub fn restore_target(&self) -> Option<&str> {
81 self.restore_target.as_deref()
82 }
83
84 pub fn trap(&self, container_id: &str) -> FocusInstruction {
88 FocusInstruction::Trap {
89 container_id: container_id.to_string(),
90 }
91 }
92
93 pub fn release(&self) -> FocusInstruction {
95 FocusInstruction::Release
96 }
97
98 pub fn restore(&self) -> FocusInstruction {
100 match &self.restore_target {
101 Some(id) => FocusInstruction::FocusElement {
102 element_id: id.clone(),
103 },
104 None => FocusInstruction::Release,
105 }
106 }
107
108 pub fn focus_first(&self, container_id: &str) -> FocusInstruction {
110 FocusInstruction::FocusFirst {
111 container_id: container_id.to_string(),
112 }
113 }
114
115 pub fn focus_last(&self, container_id: &str) -> FocusInstruction {
117 FocusInstruction::FocusLast {
118 container_id: container_id.to_string(),
119 }
120 }
121
122 pub fn focus_next(&self) -> FocusInstruction {
124 FocusInstruction::FocusNext
125 }
126
127 pub fn focus_prev(&self) -> FocusInstruction {
129 FocusInstruction::FocusPrev
130 }
131
132 pub fn is_trapping(&self) -> bool {
134 matches!(
135 self.strategy,
136 FocusStrategy::Trap | FocusStrategy::TrapAndRestore
137 )
138 }
139
140 pub fn should_restore(&self) -> bool {
142 matches!(
143 self.strategy,
144 FocusStrategy::Restore | FocusStrategy::TrapAndRestore
145 )
146 }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum FocusInstruction {
156 Trap { container_id: String },
158 Release,
160 FocusElement { element_id: String },
162 FocusFirst { container_id: String },
164 FocusLast { container_id: String },
166 FocusNext,
168 FocusPrev,
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn focus_manager_dialog() {
178 let fm = FocusManager::dialog();
179 assert_eq!(fm.strategy, FocusStrategy::TrapAndRestore);
180 assert!(fm.is_trapping());
181 assert!(fm.should_restore());
182 }
183
184 #[test]
185 fn focus_manager_menu() {
186 let fm = FocusManager::menu();
187 assert_eq!(fm.strategy, FocusStrategy::Initial);
188 assert!(!fm.is_trapping());
189 assert!(!fm.should_restore());
190 }
191
192 #[test]
193 fn focus_manager_popover() {
194 let fm = FocusManager::popover();
195 assert_eq!(fm.strategy, FocusStrategy::Restore);
196 assert!(!fm.is_trapping());
197 assert!(fm.should_restore());
198 }
199
200 #[test]
201 fn focus_manager_save_restore_target() {
202 let mut fm = FocusManager::dialog();
203 assert!(fm.restore_target().is_none());
204 fm.save_restore_target("btn-trigger");
205 assert_eq!(fm.restore_target(), Some("btn-trigger"));
206 }
207
208 #[test]
209 fn focus_manager_with_container() {
210 let fm = FocusManager::dialog().with_container("dialog-1");
211 assert_eq!(fm.container_id(), Some("dialog-1"));
212 }
213
214 #[test]
215 fn focus_instruction_trap() {
216 let fm = FocusManager::dialog();
217 let instruction = fm.trap("container-1");
218 assert_eq!(
219 instruction,
220 FocusInstruction::Trap {
221 container_id: "container-1".to_string()
222 }
223 );
224 }
225
226 #[test]
227 fn focus_instruction_restore() {
228 let mut fm = FocusManager::dialog();
229 fm.save_restore_target("trigger-btn");
230 let instruction = fm.restore();
231 assert_eq!(
232 instruction,
233 FocusInstruction::FocusElement {
234 element_id: "trigger-btn".to_string()
235 }
236 );
237 }
238
239 #[test]
240 fn focus_instruction_restore_no_target() {
241 let fm = FocusManager::dialog();
242 let instruction = fm.restore();
243 assert_eq!(instruction, FocusInstruction::Release);
244 }
245}