use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FocusManager {
pub strategy: FocusStrategy,
container_id: Option<String>,
restore_target: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FocusStrategy {
Auto,
Trap,
Restore,
Initial,
TrapAndRestore,
}
impl FocusManager {
pub fn new(strategy: FocusStrategy) -> Self {
Self {
strategy,
container_id: None,
restore_target: None,
}
}
pub fn dialog() -> Self {
Self::new(FocusStrategy::TrapAndRestore)
}
pub fn menu() -> Self {
Self::new(FocusStrategy::Initial)
}
pub fn popover() -> Self {
Self::new(FocusStrategy::Restore)
}
pub fn with_container(mut self, id: impl Into<String>) -> Self {
self.container_id = Some(id.into());
self
}
pub fn container_id(&self) -> Option<&str> {
self.container_id.as_deref()
}
pub fn save_restore_target(&mut self, element_id: impl Into<String>) {
self.restore_target = Some(element_id.into());
}
pub fn restore_target(&self) -> Option<&str> {
self.restore_target.as_deref()
}
pub fn trap(&self, container_id: &str) -> FocusInstruction {
FocusInstruction::Trap {
container_id: container_id.to_string(),
}
}
pub fn release(&self) -> FocusInstruction {
FocusInstruction::Release
}
pub fn restore(&self) -> FocusInstruction {
match &self.restore_target {
Some(id) => FocusInstruction::FocusElement {
element_id: id.clone(),
},
None => FocusInstruction::Release,
}
}
pub fn focus_first(&self, container_id: &str) -> FocusInstruction {
FocusInstruction::FocusFirst {
container_id: container_id.to_string(),
}
}
pub fn focus_last(&self, container_id: &str) -> FocusInstruction {
FocusInstruction::FocusLast {
container_id: container_id.to_string(),
}
}
pub fn focus_next(&self) -> FocusInstruction {
FocusInstruction::FocusNext
}
pub fn focus_prev(&self) -> FocusInstruction {
FocusInstruction::FocusPrev
}
pub fn is_trapping(&self) -> bool {
matches!(
self.strategy,
FocusStrategy::Trap | FocusStrategy::TrapAndRestore
)
}
pub fn should_restore(&self) -> bool {
matches!(
self.strategy,
FocusStrategy::Restore | FocusStrategy::TrapAndRestore
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FocusInstruction {
Trap { container_id: String },
Release,
FocusElement { element_id: String },
FocusFirst { container_id: String },
FocusLast { container_id: String },
FocusNext,
FocusPrev,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn focus_manager_dialog() {
let fm = FocusManager::dialog();
assert_eq!(fm.strategy, FocusStrategy::TrapAndRestore);
assert!(fm.is_trapping());
assert!(fm.should_restore());
}
#[test]
fn focus_manager_menu() {
let fm = FocusManager::menu();
assert_eq!(fm.strategy, FocusStrategy::Initial);
assert!(!fm.is_trapping());
assert!(!fm.should_restore());
}
#[test]
fn focus_manager_popover() {
let fm = FocusManager::popover();
assert_eq!(fm.strategy, FocusStrategy::Restore);
assert!(!fm.is_trapping());
assert!(fm.should_restore());
}
#[test]
fn focus_manager_save_restore_target() {
let mut fm = FocusManager::dialog();
assert!(fm.restore_target().is_none());
fm.save_restore_target("btn-trigger");
assert_eq!(fm.restore_target(), Some("btn-trigger"));
}
#[test]
fn focus_manager_with_container() {
let fm = FocusManager::dialog().with_container("dialog-1");
assert_eq!(fm.container_id(), Some("dialog-1"));
}
#[test]
fn focus_instruction_trap() {
let fm = FocusManager::dialog();
let instruction = fm.trap("container-1");
assert_eq!(
instruction,
FocusInstruction::Trap {
container_id: "container-1".to_string()
}
);
}
#[test]
fn focus_instruction_restore() {
let mut fm = FocusManager::dialog();
fm.save_restore_target("trigger-btn");
let instruction = fm.restore();
assert_eq!(
instruction,
FocusInstruction::FocusElement {
element_id: "trigger-btn".to_string()
}
);
}
#[test]
fn focus_instruction_restore_no_target() {
let fm = FocusManager::dialog();
let instruction = fm.restore();
assert_eq!(instruction, FocusInstruction::Release);
}
}