leptos_helios/accessibility/
screen_reader.rs

1//! Screen Reader Support
2//!
3//! This module provides screen reader support and ARIA integration.
4
5use super::{AccessibilityError, DataTable, ScreenReaderSupport};
6use crate::chart::{ChartSpec, MarkType};
7use polars::prelude::DataFrame;
8use std::collections::HashMap;
9
10/// Screen reader manager
11pub struct ScreenReaderManager {
12    config: ScreenReaderSupport,
13    live_regions: HashMap<String, String>,
14    announcements: Vec<String>,
15}
16
17impl ScreenReaderManager {
18    /// Create a new screen reader manager
19    pub fn new(config: ScreenReaderSupport) -> Self {
20        Self {
21            config,
22            live_regions: HashMap::new(),
23            announcements: Vec::new(),
24        }
25    }
26
27    /// Generate screen reader description for chart
28    pub fn generate_description(
29        &self,
30        spec: &ChartSpec,
31        data: &DataFrame,
32    ) -> Result<String, AccessibilityError> {
33        if !self.config.enabled {
34            return Ok(String::new());
35        }
36
37        let mut description = String::new();
38
39        // Add chart title and type
40        if !spec.config.title.is_empty() {
41            description.push_str(&format!("Chart titled: {}. ", spec.config.title));
42        }
43
44        description.push_str(&format!(
45            "This is a {} chart. ",
46            self.get_chart_type_description(&spec.mark)
47        ));
48
49        // Add data summary
50        if let Ok(summary) = self.generate_data_summary(data) {
51            description.push_str(&summary);
52        }
53
54        // Add interaction instructions
55        description.push_str(" Use arrow keys to navigate data points. Press Enter to select.");
56
57        Ok(description)
58    }
59
60    /// Create data table for screen readers
61    pub fn create_data_table(
62        &self,
63        spec: &ChartSpec,
64        data: &DataFrame,
65    ) -> Result<DataTable, AccessibilityError> {
66        if !self.config.create_data_tables {
67            return Err(AccessibilityError::ScreenReaderError(
68                "Data table creation is disabled".to_string(),
69            ));
70        }
71
72        let title = if spec.config.title.is_empty() {
73            "Chart Data".to_string()
74        } else {
75            format!("{} Data", spec.config.title)
76        };
77
78        let summary = self
79            .generate_data_summary(data)
80            .unwrap_or_else(|_| "Chart data table".to_string());
81
82        // Extract headers from data
83        let headers = data.get_column_names();
84
85        // Convert data to string rows
86        let mut rows = Vec::new();
87        for i in 0..data.height() {
88            let mut row = Vec::new();
89            for col in &headers {
90                if let Ok(series) = data.column(col) {
91                    let value = series
92                        .get(i)
93                        .map(|v| format!("{}", v))
94                        .unwrap_or_else(|_| "N/A".to_string());
95                    row.push(value);
96                }
97            }
98            rows.push(row);
99        }
100
101        Ok(DataTable {
102            title: title.clone(),
103            summary,
104            headers: headers.into_iter().map(|h| h.to_string()).collect(),
105            rows,
106            caption: Some(format!("Data table for {}", title)),
107            scope_attributes: HashMap::new(),
108        })
109    }
110
111    /// Generate ARIA labels for chart elements
112    pub fn generate_aria_labels(
113        &self,
114        spec: &ChartSpec,
115        data: &DataFrame,
116    ) -> Result<HashMap<String, String>, AccessibilityError> {
117        if !self.config.aria_labels {
118            return Ok(HashMap::new());
119        }
120
121        let mut labels = HashMap::new();
122
123        // Chart container
124        labels.insert(
125            "chart-container".to_string(),
126            format!("Interactive {} chart", self.get_chart_type_name(&spec.mark)),
127        );
128
129        // Data points
130        for (i, row) in data.iter().enumerate() {
131            let row_values: Vec<_> = row.iter().map(|v| v.clone()).collect();
132            let point_label = self.generate_point_label(&spec.mark, &row_values, i);
133            labels.insert(format!("data-point-{}", i), point_label);
134        }
135
136        // Legend items
137        if let Some(legend) = &spec.config.legend {
138            if let Some(title) = &legend.title {
139                labels.insert("legend-title".to_string(), format!("Legend: {}", title));
140            }
141        }
142
143        Ok(labels)
144    }
145
146    /// Announce update to screen readers
147    pub fn announce_update(&mut self, message: &str) -> Result<(), AccessibilityError> {
148        if !self.config.announce_updates {
149            return Ok(());
150        }
151
152        self.announcements.push(message.to_string());
153        Ok(())
154    }
155
156    /// Get pending announcements
157    pub fn get_pending_announcements(&mut self) -> Vec<String> {
158        self.announcements.drain(..).collect()
159    }
160
161    /// Generate structured navigation
162    pub fn generate_navigation_structure(
163        &self,
164        spec: &ChartSpec,
165        _data: &DataFrame,
166    ) -> Result<NavigationStructure, AccessibilityError> {
167        if !self.config.structured_navigation {
168            return Err(AccessibilityError::ScreenReaderError(
169                "Structured navigation is disabled".to_string(),
170            ));
171        }
172
173        let mut structure = NavigationStructure {
174            landmarks: Vec::new(),
175            headings: Vec::new(),
176            regions: Vec::new(),
177        };
178
179        // Add main chart landmark
180        structure.landmarks.push(Landmark {
181            role: "main".to_string(),
182            label: "Chart Content".to_string(),
183            id: "chart-main".to_string(),
184        });
185
186        // Add chart heading
187        structure.headings.push(Heading {
188            level: 1,
189            text: spec.config.title.clone(),
190            id: "chart-title".to_string(),
191        });
192
193        // Add data region
194        structure.regions.push(Region {
195            role: "region".to_string(),
196            label: "Chart Data".to_string(),
197            id: "chart-data".to_string(),
198            aria_live: Some("polite".to_string()),
199        });
200
201        Ok(structure)
202    }
203
204    /// Generate data summary
205    fn generate_data_summary(&self, data: &DataFrame) -> Result<String, AccessibilityError> {
206        let row_count = data.height();
207        let col_count = data.width();
208
209        Ok(format!(
210            "The chart contains {} data points across {} categories. ",
211            row_count, col_count
212        ))
213    }
214
215    /// Get chart type description
216    fn get_chart_type_description(&self, mark: &MarkType) -> &'static str {
217        match mark {
218            MarkType::Line { .. } => "line",
219            MarkType::Bar { .. } => "bar",
220            MarkType::Point { .. } => "scatter plot",
221            MarkType::Area { .. } => "area",
222            MarkType::Text { .. } => "text",
223            _ => "data visualization",
224        }
225    }
226
227    /// Get chart type name
228    fn get_chart_type_name(&self, mark: &MarkType) -> &'static str {
229        match mark {
230            MarkType::Line { .. } => "line",
231            MarkType::Bar { .. } => "bar",
232            MarkType::Point { .. } => "scatter",
233            MarkType::Area { .. } => "area",
234            MarkType::Text { .. } => "text",
235            _ => "data",
236        }
237    }
238
239    /// Generate point label
240    fn generate_point_label(
241        &self,
242        mark: &MarkType,
243        _row: &[polars::prelude::AnyValue],
244        index: usize,
245    ) -> String {
246        match mark {
247            MarkType::Point { .. } => format!("Data point {}", index + 1),
248            MarkType::Bar { .. } => format!("Bar {}", index + 1),
249            MarkType::Line { .. } => format!("Line point {}", index + 1),
250            _ => format!("Data element {}", index + 1),
251        }
252    }
253}
254
255/// Navigation structure for screen readers
256#[derive(Debug, Clone)]
257pub struct NavigationStructure {
258    pub landmarks: Vec<Landmark>,
259    pub headings: Vec<Heading>,
260    pub regions: Vec<Region>,
261}
262
263/// Landmark for navigation
264#[derive(Debug, Clone)]
265pub struct Landmark {
266    pub role: String,
267    pub label: String,
268    pub id: String,
269}
270
271/// Heading for navigation
272#[derive(Debug, Clone)]
273pub struct Heading {
274    pub level: u32,
275    pub text: String,
276    pub id: String,
277}
278
279/// Region for navigation
280#[derive(Debug, Clone)]
281pub struct Region {
282    pub role: String,
283    pub label: String,
284    pub id: String,
285    pub aria_live: Option<String>,
286}