1use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
15pub struct AnimationConfig {
16 pub enabled: bool,
18 pub speed: f32,
20 pub reduce_motion: bool,
22}
23
24impl Default for AnimationConfig {
25 fn default() -> Self {
26 Self {
27 enabled: true,
28 speed: 1.0,
29 reduce_motion: false,
30 }
31 }
32}
33
34impl AnimationConfig {
35 pub fn duration_ms(&self, base_ms: u32) -> u32 {
37 if !self.enabled || self.reduce_motion {
38 return 0;
39 }
40 ((base_ms as f32) / self.speed) as u32
41 }
42
43 pub fn should_animate(&self) -> bool {
45 self.enabled && !self.reduce_motion
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct AccessibilityConfig {
52 pub screen_reader_enabled: bool,
54 pub high_contrast_enabled: bool,
56 pub animations_disabled: bool,
58 pub announcements_enabled: bool,
60 pub focus_indicator: FocusIndicatorStyle,
62 #[serde(default)]
64 pub animations: AnimationConfig,
65}
66
67impl Default for AccessibilityConfig {
68 fn default() -> Self {
69 Self {
70 screen_reader_enabled: false,
71 high_contrast_enabled: false,
72 animations_disabled: false,
73 announcements_enabled: true,
74 focus_indicator: FocusIndicatorStyle::Bracket,
75 animations: AnimationConfig::default(),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82pub enum FocusIndicatorStyle {
83 Bracket,
85 Asterisk,
87 Underline,
89 Arrow,
91}
92
93impl FocusIndicatorStyle {
94 pub fn prefix(&self) -> &'static str {
96 match self {
97 FocusIndicatorStyle::Bracket => "[",
98 FocusIndicatorStyle::Asterisk => "*",
99 FocusIndicatorStyle::Underline => "_",
100 FocusIndicatorStyle::Arrow => "> ",
101 }
102 }
103
104 pub fn suffix(&self) -> &'static str {
106 match self {
107 FocusIndicatorStyle::Bracket => "]",
108 FocusIndicatorStyle::Asterisk => "*",
109 FocusIndicatorStyle::Underline => "_",
110 FocusIndicatorStyle::Arrow => "",
111 }
112 }
113}
114
115#[derive(Debug, Clone)]
117pub struct TextAlternative {
118 pub id: String,
120 pub short_description: String,
122 pub long_description: Option<String>,
124 pub element_type: ElementType,
126}
127
128impl TextAlternative {
129 pub fn new(
131 id: impl Into<String>,
132 short_desc: impl Into<String>,
133 element_type: ElementType,
134 ) -> Self {
135 Self {
136 id: id.into(),
137 short_description: short_desc.into(),
138 long_description: None,
139 element_type,
140 }
141 }
142
143 pub fn with_long_description(mut self, desc: impl Into<String>) -> Self {
145 self.long_description = Some(desc.into());
146 self
147 }
148
149 pub fn full_description(&self) -> String {
151 match &self.long_description {
152 Some(long) => format!("{}: {}", self.short_description, long),
153 None => self.short_description.clone(),
154 }
155 }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum ElementType {
161 Button,
163 Input,
165 List,
167 ListItem,
169 Tab,
171 TabPanel,
173 Dialog,
175 Text,
177 Heading,
179 CodeBlock,
181 Message,
183 Status,
185}
186
187impl ElementType {
188 pub fn role(&self) -> &'static str {
190 match self {
191 ElementType::Button => "button",
192 ElementType::Input => "textbox",
193 ElementType::List => "list",
194 ElementType::ListItem => "listitem",
195 ElementType::Tab => "tab",
196 ElementType::TabPanel => "tabpanel",
197 ElementType::Dialog => "dialog",
198 ElementType::Text => "text",
199 ElementType::Heading => "heading",
200 ElementType::CodeBlock => "code",
201 ElementType::Message => "article",
202 ElementType::Status => "status",
203 }
204 }
205}
206
207#[derive(Debug, Clone)]
209pub struct ScreenReaderAnnouncer {
210 enabled: bool,
212 history: Vec<Announcement>,
214}
215
216#[derive(Debug, Clone)]
218pub struct Announcement {
219 pub text: String,
221 pub priority: AnnouncementPriority,
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
227pub enum AnnouncementPriority {
228 Low,
230 Normal,
232 High,
234}
235
236impl ScreenReaderAnnouncer {
237 pub fn new(enabled: bool) -> Self {
239 Self {
240 enabled,
241 history: Vec::new(),
242 }
243 }
244
245 pub fn announce(&mut self, text: impl Into<String>, priority: AnnouncementPriority) {
247 if !self.enabled {
248 return;
249 }
250
251 let announcement = Announcement {
252 text: text.into(),
253 priority,
254 };
255
256 self.history.push(announcement);
257 }
258
259 pub fn announce_state_change(&mut self, element: &str, state: &str) {
261 self.announce(
262 format!("{} {}", element, state),
263 AnnouncementPriority::Normal,
264 );
265 }
266
267 pub fn announce_error(&mut self, message: impl Into<String>) {
269 self.announce(message, AnnouncementPriority::High);
270 }
271
272 pub fn announce_success(&mut self, message: impl Into<String>) {
274 self.announce(message, AnnouncementPriority::Normal);
275 }
276
277 pub fn last_announcement(&self) -> Option<&Announcement> {
279 self.history.last()
280 }
281
282 pub fn announcements(&self) -> &[Announcement] {
284 &self.history
285 }
286
287 pub fn clear_history(&mut self) {
289 self.history.clear();
290 }
291
292 pub fn enable(&mut self) {
294 self.enabled = true;
295 }
296
297 pub fn disable(&mut self) {
299 self.enabled = false;
300 }
301
302 pub fn is_enabled(&self) -> bool {
304 self.enabled
305 }
306}
307
308#[derive(Debug, Clone)]
310pub struct StateChangeEvent {
311 pub component: String,
313 pub previous_state: String,
315 pub new_state: String,
317 pub priority: AnnouncementPriority,
319}
320
321impl StateChangeEvent {
322 pub fn new(
324 component: impl Into<String>,
325 previous: impl Into<String>,
326 new: impl Into<String>,
327 priority: AnnouncementPriority,
328 ) -> Self {
329 Self {
330 component: component.into(),
331 previous_state: previous.into(),
332 new_state: new.into(),
333 priority,
334 }
335 }
336
337 pub fn announcement_text(&self) -> String {
339 format!(
340 "{} changed from {} to {}",
341 self.component, self.previous_state, self.new_state
342 )
343 }
344}
345
346#[derive(Debug, Clone, Default)]
348pub struct FocusManager {
349 pub focused_element: Option<String>,
351 pub focus_history: Vec<String>,
353}
354
355impl FocusManager {
356 pub fn new() -> Self {
358 Self::default()
359 }
360
361 pub fn set_focus(&mut self, element_id: impl Into<String>) {
363 let id = element_id.into();
364 if let Some(current) = &self.focused_element {
365 self.focus_history.push(current.clone());
366 }
367 self.focused_element = Some(id);
368 }
369
370 pub fn restore_focus(&mut self) -> Option<String> {
372 self.focus_history.pop()
373 }
374
375 pub fn clear_focus(&mut self) {
377 self.focused_element = None;
378 }
379}
380
381#[derive(Debug, Clone, Default)]
383pub struct KeyboardNavigationManager {
384 pub focused_element: Option<String>,
386 pub tab_order: Vec<String>,
388 pub element_descriptions: HashMap<String, TextAlternative>,
390}
391
392impl KeyboardNavigationManager {
393 pub fn new() -> Self {
395 Self::default()
396 }
397
398 pub fn register_element(&mut self, alternative: TextAlternative) {
400 self.tab_order.push(alternative.id.clone());
401 self.element_descriptions
402 .insert(alternative.id.clone(), alternative);
403 }
404
405 pub fn focus(&mut self, element_id: &str) -> bool {
407 if self.element_descriptions.contains_key(element_id) {
408 self.focused_element = Some(element_id.to_string());
409 true
410 } else {
411 false
412 }
413 }
414
415 pub fn focus_next(&mut self) -> Option<&TextAlternative> {
417 if self.tab_order.is_empty() {
418 return None;
419 }
420
421 let next_index = match &self.focused_element {
422 None => 0,
423 Some(current) => {
424 let current_index = self.tab_order.iter().position(|id| id == current)?;
425 (current_index + 1) % self.tab_order.len()
426 }
427 };
428
429 let next_id = self.tab_order[next_index].clone();
430 self.focused_element = Some(next_id.clone());
431 self.element_descriptions.get(&next_id)
432 }
433
434 pub fn focus_previous(&mut self) -> Option<&TextAlternative> {
436 if self.tab_order.is_empty() {
437 return None;
438 }
439
440 let prev_index = match &self.focused_element {
441 None => self.tab_order.len() - 1,
442 Some(current) => {
443 let current_index = self.tab_order.iter().position(|id| id == current)?;
444 if current_index == 0 {
445 self.tab_order.len() - 1
446 } else {
447 current_index - 1
448 }
449 }
450 };
451
452 let prev_id = self.tab_order[prev_index].clone();
453 self.focused_element = Some(prev_id.clone());
454 self.element_descriptions.get(&prev_id)
455 }
456
457 pub fn current_focus(&self) -> Option<&TextAlternative> {
459 self.focused_element
460 .as_ref()
461 .and_then(|id| self.element_descriptions.get(id))
462 }
463
464 pub fn clear(&mut self) {
466 self.focused_element = None;
467 self.tab_order.clear();
468 self.element_descriptions.clear();
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 #[test]
477 fn test_accessibility_config_default() {
478 let config = AccessibilityConfig::default();
479 assert!(!config.screen_reader_enabled);
480 assert!(!config.high_contrast_enabled);
481 assert!(!config.animations_disabled);
482 assert!(config.announcements_enabled);
483 }
484
485 #[test]
486 fn test_focus_indicator_style() {
487 assert_eq!(FocusIndicatorStyle::Bracket.prefix(), "[");
488 assert_eq!(FocusIndicatorStyle::Bracket.suffix(), "]");
489 assert_eq!(FocusIndicatorStyle::Arrow.prefix(), "> ");
490 assert_eq!(FocusIndicatorStyle::Arrow.suffix(), "");
491 }
492
493 #[test]
494 fn test_text_alternative() {
495 let alt = TextAlternative::new("btn1", "Submit button", ElementType::Button)
496 .with_long_description("Click to submit the form");
497 assert_eq!(alt.id, "btn1");
498 assert_eq!(alt.short_description, "Submit button");
499 assert!(alt.long_description.is_some());
500 }
501
502 #[test]
503 fn test_element_type_role() {
504 assert_eq!(ElementType::Button.role(), "button");
505 assert_eq!(ElementType::Input.role(), "textbox");
506 assert_eq!(ElementType::List.role(), "list");
507 }
508
509 #[test]
510 fn test_screen_reader_announcer() {
511 let mut announcer = ScreenReaderAnnouncer::new(true);
512 announcer.announce("Test announcement", AnnouncementPriority::Normal);
513 assert_eq!(announcer.announcements().len(), 1);
514 assert_eq!(
515 announcer.last_announcement().unwrap().text,
516 "Test announcement"
517 );
518 }
519
520 #[test]
521 fn test_screen_reader_announcer_disabled() {
522 let mut announcer = ScreenReaderAnnouncer::new(false);
523 announcer.announce("Test", AnnouncementPriority::Normal);
524 assert_eq!(announcer.announcements().len(), 0);
525 }
526
527 #[test]
528 fn test_keyboard_navigation_manager() {
529 let mut manager = KeyboardNavigationManager::new();
530 let alt1 = TextAlternative::new("btn1", "Button 1", ElementType::Button);
531 let alt2 = TextAlternative::new("btn2", "Button 2", ElementType::Button);
532
533 manager.register_element(alt1);
534 manager.register_element(alt2);
535
536 assert!(manager.focus("btn1"));
537 assert_eq!(manager.focused_element, Some("btn1".to_string()));
538
539 let next = manager.focus_next();
540 assert!(next.is_some());
541 assert_eq!(manager.focused_element, Some("btn2".to_string()));
542 }
543
544 #[test]
545 fn test_keyboard_navigation_wrap_around() {
546 let mut manager = KeyboardNavigationManager::new();
547 manager.register_element(TextAlternative::new(
548 "btn1",
549 "Button 1",
550 ElementType::Button,
551 ));
552 manager.register_element(TextAlternative::new(
553 "btn2",
554 "Button 2",
555 ElementType::Button,
556 ));
557
558 manager.focus("btn2");
559 let _next = manager.focus_next();
560 assert_eq!(manager.focused_element, Some("btn1".to_string()));
561 }
562
563 #[test]
564 fn test_animation_config_default() {
565 let config = AnimationConfig::default();
566 assert!(config.enabled);
567 assert_eq!(config.speed, 1.0);
568 assert!(!config.reduce_motion);
569 }
570
571 #[test]
572 fn test_animation_duration_calculation() {
573 let config = AnimationConfig {
574 enabled: true,
575 speed: 2.0,
576 reduce_motion: false,
577 };
578 assert_eq!(config.duration_ms(100), 50);
580 }
581
582 #[test]
583 fn test_animation_disabled() {
584 let config = AnimationConfig {
585 enabled: false,
586 speed: 1.0,
587 reduce_motion: false,
588 };
589 assert_eq!(config.duration_ms(100), 0);
591 }
592
593 #[test]
594 fn test_animation_reduce_motion() {
595 let config = AnimationConfig {
596 enabled: true,
597 speed: 1.0,
598 reduce_motion: true,
599 };
600 assert_eq!(config.duration_ms(100), 0);
602 assert!(!config.should_animate());
603 }
604
605 #[test]
606 fn test_accessibility_config_animations() {
607 let config = AccessibilityConfig::default();
608 assert!(config.animations.enabled);
609 assert!(config.animations.should_animate());
610 }
611
612 #[test]
613 fn test_state_change_event() {
614 let event = StateChangeEvent::new(
615 "button",
616 "disabled",
617 "enabled",
618 AnnouncementPriority::Normal,
619 );
620 assert_eq!(event.component, "button");
621 assert_eq!(event.previous_state, "disabled");
622 assert_eq!(event.new_state, "enabled");
623 assert!(event.announcement_text().contains("button"));
624 }
625
626 #[test]
627 fn test_focus_manager() {
628 let mut manager = FocusManager::new();
629 assert!(manager.focused_element.is_none());
630
631 manager.set_focus("btn1");
632 assert_eq!(manager.focused_element, Some("btn1".to_string()));
633
634 manager.set_focus("btn2");
635 assert_eq!(manager.focused_element, Some("btn2".to_string()));
636
637 let restored = manager.restore_focus();
638 assert_eq!(restored, Some("btn1".to_string()));
639 }
640
641 #[test]
642 fn test_focus_manager_clear() {
643 let mut manager = FocusManager::new();
644 manager.set_focus("btn1");
645 manager.clear_focus();
646 assert!(manager.focused_element.is_none());
647 }
648}