use accesskit::NodeId;
use oxiui_text::{TextArea, TextInput};
use crate::{
text_a11y::{TextPosition, TextSelection},
tree::{A11yNode, WidgetRole},
};
#[derive(Debug, Default, Clone)]
pub struct TextInputA11yParams {
pub label: Option<String>,
pub description: Option<String>,
pub id: Option<NodeId>,
pub disabled: bool,
}
pub fn text_input_to_a11y(input: &TextInput, params: &TextInputA11yParams) -> A11yNode {
let id = params.id.unwrap_or(NodeId(0));
let sel = input.selection();
let a11y_sel = TextSelection {
anchor: TextPosition(sel.anchor),
active: TextPosition(sel.focus),
};
let description = params
.description
.clone()
.unwrap_or_else(|| build_selection_description(a11y_sel));
let mut node = A11yNode::simple(id, WidgetRole::TextInput, params.label.clone());
node.text_content = Some(input.text().to_string());
node.props.description = Some(description);
node.props.disabled = params.disabled;
node
}
pub fn text_area_to_a11y(area: &TextArea, params: &TextInputA11yParams) -> A11yNode {
let id = params.id.unwrap_or(NodeId(0));
let (row, col) = area.cursor();
let description = params
.description
.clone()
.unwrap_or_else(|| build_cursor_description(row, col));
let mut node = A11yNode::simple(id, WidgetRole::TextInput, params.label.clone());
node.text_content = Some(area.text());
node.props.description = Some(description);
node.props.disabled = params.disabled;
node
}
fn build_selection_description(sel: TextSelection) -> String {
if sel.is_collapsed() {
format!("cursor at position {}", sel.active.0)
} else {
format!("selected text: bytes {}..{}", sel.start(), sel.end())
}
}
fn build_cursor_description(row: usize, col: usize) -> String {
format!("cursor at row {}, column {}", row + 1, col + 1)
}
#[cfg(test)]
mod tests {
use super::*;
use accesskit::NodeId;
use oxiui_text::{TextArea, TextInput, WrapMode};
#[test]
fn test_text_input_to_a11y_label_and_content() {
let input = TextInput::with_text("hello");
let params = TextInputA11yParams {
label: Some("Username".to_string()),
id: Some(NodeId(1)),
..Default::default()
};
let node = text_input_to_a11y(&input, ¶ms);
assert_eq!(node.id, NodeId(1));
assert_eq!(node.role, WidgetRole::TextInput);
assert_eq!(node.label.as_deref(), Some("Username"));
assert_eq!(node.text_content.as_deref(), Some("hello"));
}
#[test]
fn test_text_input_to_a11y_cursor_at_end() {
let input = TextInput::with_text("abc");
let params = TextInputA11yParams::default();
let node = text_input_to_a11y(&input, ¶ms);
let desc = node.props.description.as_deref().unwrap_or("");
assert!(
desc.contains("cursor at position 3"),
"expected cursor description, got: {desc}"
);
}
#[test]
fn test_text_input_to_a11y_description_override() {
let input = TextInput::with_text("data");
let params = TextInputA11yParams {
description: Some("Enter your login name".to_string()),
..Default::default()
};
let node = text_input_to_a11y(&input, ¶ms);
assert_eq!(
node.props.description.as_deref(),
Some("Enter your login name"),
"params.description should override auto-generated description"
);
}
#[test]
fn test_text_input_to_a11y_disabled_flag() {
let input = TextInput::new();
let params = TextInputA11yParams {
disabled: true,
..Default::default()
};
let node = text_input_to_a11y(&input, ¶ms);
assert!(node.props.disabled, "disabled flag should be propagated");
}
#[test]
fn test_text_input_to_a11y_empty_input() {
let input = TextInput::new();
let params = TextInputA11yParams::default();
let node = text_input_to_a11y(&input, ¶ms);
assert_eq!(node.text_content.as_deref(), Some(""));
let desc = node.props.description.as_deref().unwrap_or("");
assert!(
desc.contains("cursor at position 0"),
"empty input should have cursor at 0, got: {desc}"
);
}
#[test]
fn test_text_input_to_a11y_default_id_is_zero() {
let input = TextInput::new();
let params = TextInputA11yParams::default();
let node = text_input_to_a11y(&input, ¶ms);
assert_eq!(node.id, NodeId(0));
}
#[test]
fn test_text_area_to_a11y_label_and_content() {
let area = TextArea::new("line one\nline two", WrapMode::Hard);
let params = TextInputA11yParams {
label: Some("Notes".to_string()),
id: Some(NodeId(7)),
..Default::default()
};
let node = text_area_to_a11y(&area, ¶ms);
assert_eq!(node.id, NodeId(7));
assert_eq!(node.role, WidgetRole::TextInput);
assert_eq!(node.label.as_deref(), Some("Notes"));
let content = node.text_content.as_deref().unwrap_or("");
assert!(
content.contains("line one"),
"content should include first line, got: {content}"
);
}
#[test]
fn test_text_area_to_a11y_cursor_description() {
let area = TextArea::new("hello", WrapMode::Hard);
let params = TextInputA11yParams::default();
let node = text_area_to_a11y(&area, ¶ms);
let desc = node.props.description.as_deref().unwrap_or("");
assert!(
desc.contains("row"),
"description should mention row, got: {desc}"
);
assert!(
desc.contains("column"),
"description should mention column, got: {desc}"
);
}
#[test]
fn test_text_area_to_a11y_description_override() {
let area = TextArea::new("text", WrapMode::Hard);
let params = TextInputA11yParams {
description: Some("Multi-line notes field".to_string()),
..Default::default()
};
let node = text_area_to_a11y(&area, ¶ms);
assert_eq!(
node.props.description.as_deref(),
Some("Multi-line notes field"),
);
}
#[test]
fn test_text_area_to_a11y_disabled_flag() {
let area = TextArea::new("", WrapMode::Hard);
let params = TextInputA11yParams {
disabled: true,
..Default::default()
};
let node = text_area_to_a11y(&area, ¶ms);
assert!(node.props.disabled);
}
#[test]
fn test_build_selection_description_cursor() {
let sel = TextSelection::cursor(5);
let desc = build_selection_description(sel);
assert_eq!(desc, "cursor at position 5");
}
#[test]
fn test_build_selection_description_range() {
let sel = TextSelection::range(2, 7);
let desc = build_selection_description(sel);
assert!(desc.contains("2..7"), "expected 2..7 in: {desc}");
}
#[test]
fn test_build_cursor_description_first_cell() {
let desc = build_cursor_description(0, 0);
assert_eq!(desc, "cursor at row 1, column 1");
}
#[test]
fn test_build_cursor_description_third_row() {
let desc = build_cursor_description(2, 4);
assert_eq!(desc, "cursor at row 3, column 5");
}
}