Skip to main content

use_focus_order/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive focus-order helpers.
3//!
4//! The crate stays generic and does not depend on a DOM or UI framework.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use use_focus_order::{
10//!     FocusItem, enabled_focus_items, has_duplicate_focus_order, is_focus_order_valid,
11//!     sorted_focus_order,
12//! };
13//!
14//! let items = [
15//!     FocusItem {
16//!         id: String::from("submit"),
17//!         order: 2,
18//!         enabled: true,
19//!     },
20//!     FocusItem {
21//!         id: String::from("email"),
22//!         order: 1,
23//!         enabled: true,
24//!     },
25//!     FocusItem {
26//!         id: String::from("help"),
27//!         order: 3,
28//!         enabled: false,
29//!     },
30//! ];
31//!
32//! assert_eq!(sorted_focus_order(&items)[0].id, "email");
33//! assert_eq!(enabled_focus_items(&items).len(), 2);
34//! assert!(!has_duplicate_focus_order(&items));
35//! assert!(is_focus_order_valid(&items));
36//! ```
37
38use std::collections::HashSet;
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct FocusItem {
42    pub id: String,
43    pub order: usize,
44    pub enabled: bool,
45}
46
47#[must_use]
48pub fn sorted_focus_order(items: &[FocusItem]) -> Vec<FocusItem> {
49    let mut sorted = items.to_vec();
50    sorted.sort_by(|left, right| {
51        left.order
52            .cmp(&right.order)
53            .then_with(|| left.id.cmp(&right.id))
54    });
55    sorted
56}
57
58#[must_use]
59pub fn enabled_focus_items(items: &[FocusItem]) -> Vec<FocusItem> {
60    items.iter().filter(|item| item.enabled).cloned().collect()
61}
62
63#[must_use]
64pub fn has_duplicate_focus_order(items: &[FocusItem]) -> bool {
65    let mut seen = HashSet::with_capacity(items.len());
66    items.iter().any(|item| !seen.insert(item.order))
67}
68
69#[must_use]
70pub fn has_empty_focus_id(items: &[FocusItem]) -> bool {
71    items.iter().any(|item| item.id.trim().is_empty())
72}
73
74#[must_use]
75pub fn is_focus_order_valid(items: &[FocusItem]) -> bool {
76    let enabled = enabled_focus_items(items);
77    !has_duplicate_focus_order(&enabled) && !has_empty_focus_id(&enabled)
78}
79
80#[cfg(test)]
81mod tests {
82    use super::{
83        FocusItem, enabled_focus_items, has_duplicate_focus_order, has_empty_focus_id,
84        is_focus_order_valid, sorted_focus_order,
85    };
86
87    #[test]
88    fn sorts_focus_order_and_filters_enabled_items() {
89        let items = [
90            FocusItem {
91                id: String::from("submit"),
92                order: 2,
93                enabled: true,
94            },
95            FocusItem {
96                id: String::from("email"),
97                order: 1,
98                enabled: true,
99            },
100            FocusItem {
101                id: String::from("help"),
102                order: 3,
103                enabled: false,
104            },
105        ];
106
107        let sorted = sorted_focus_order(&items);
108        assert_eq!(sorted[0].id, "email");
109        assert_eq!(enabled_focus_items(&items).len(), 2);
110        assert!(is_focus_order_valid(&items));
111    }
112
113    #[test]
114    fn detects_duplicate_orders_and_empty_ids() {
115        let duplicates = [
116            FocusItem {
117                id: String::from("email"),
118                order: 1,
119                enabled: true,
120            },
121            FocusItem {
122                id: String::from("submit"),
123                order: 1,
124                enabled: true,
125            },
126        ];
127        let empty = [FocusItem {
128            id: String::from("  "),
129            order: 1,
130            enabled: true,
131        }];
132
133        assert!(has_duplicate_focus_order(&duplicates));
134        assert!(!is_focus_order_valid(&duplicates));
135        assert!(has_empty_focus_id(&empty));
136        assert!(!is_focus_order_valid(&empty));
137    }
138}