radix_leptos_primitives/components/
label.rs

1use crate::utils::{merge_classes, generate_id};
2use leptos::callback::Callback;
3use leptos::children::Children;
4use leptos::prelude::*;
5
6/// Label component - Form labels with accessibility features
7#[component]
8pub fn Label(
9    #[prop(optional)] class: Option<String>,
10    #[prop(optional)] style: Option<String>,
11    #[prop(optional)] children: Option<Children>,
12    #[prop(optional)] for_id: Option<String>,
13    #[prop(optional)] required: Option<bool>,
14    #[prop(optional)] disabled: Option<bool>,
15    #[prop(optional)] size: Option<LabelSize>,
16    #[prop(optional)] variant: Option<LabelVariant>,
17    #[prop(optional)] on_click: Option<Callback<()>>,
18) -> impl IntoView {
19    let for_id = for_id.unwrap_or_default();
20    let required = required.unwrap_or(false);
21    let disabled = disabled.unwrap_or(false);
22    let size = size.unwrap_or_default();
23    let variant = variant.unwrap_or_default();
24
25    let class = merge_classes(vec![
26        "label",
27        &size.to_class(),
28        &variant.to_class(),
29        class.as_deref().unwrap_or(""),
30    ]);
31
32    let handle_click = move |_| {
33        if !disabled {
34            if let Some(callback) = on_click {
35                callback.run(());
36            }
37        }
38    };
39
40    view! {
41        <label
42            class=class
43            style=style
44            for=for_id
45            aria-required=required
46            aria-disabled=disabled
47            on:click=handle_click
48        >
49            {children.map(|c| c())}
50        </label>
51    }
52}
53
54/// Label Text component
55#[component]
56pub fn LabelText(
57    #[prop(optional)] class: Option<String>,
58    #[prop(optional)] style: Option<String>,
59    #[prop(optional)] children: Option<Children>,
60    #[prop(optional)] text: Option<String>,
61    #[prop(optional)] required: Option<bool>,
62) -> impl IntoView {
63    let text = text.unwrap_or_default();
64    let required = required.unwrap_or(false);
65
66    let class = merge_classes(vec!["label-text"]);
67
68    view! {
69        <span class=class style=style>
70            {text}
71            {if required { "*" } else { "" }}
72            {children.map(|c| c())}
73        </span>
74    }
75}
76
77/// Label Description component
78#[component]
79pub fn LabelDescription(
80    #[prop(optional)] class: Option<String>,
81    #[prop(optional)] style: Option<String>,
82    #[prop(optional)] children: Option<Children>,
83    #[prop(optional)] description: Option<String>,
84) -> impl IntoView {
85    let description = description.unwrap_or_default();
86
87    let class = merge_classes(vec!["label-description", class.as_deref().unwrap_or("")]);
88
89    view! {
90        <div
91            class=class
92            style=style
93            role="text"
94            aria-label="Label description"
95        >
96            {children.map(|c| c())}
97        </div>
98    }
99}
100
101/// Label Error component
102#[component]
103pub fn LabelError(
104    #[prop(optional)] class: Option<String>,
105    #[prop(optional)] style: Option<String>,
106    #[prop(optional)] children: Option<Children>,
107    #[prop(optional)] error: Option<String>,
108    #[prop(optional)] visible: Option<bool>,
109) -> impl IntoView {
110    let error = error.unwrap_or_default();
111    let visible = visible.unwrap_or(false);
112
113    let class = merge_classes(vec!["label-error"]);
114
115    view! {
116        <div class=class style=style aria-live="polite">
117            {if visible { error } else { "".to_string() }}
118            {children.map(|c| c())}
119        </div>
120    }
121}
122
123/// Label Group component
124#[component]
125pub fn LabelGroup(
126    #[prop(optional)] class: Option<String>,
127    #[prop(optional)] style: Option<String>,
128    #[prop(optional)] children: Option<Children>,
129    #[prop(optional)] orientation: Option<LabelOrientation>,
130    #[prop(optional)] spacing: Option<LabelSpacing>,
131) -> impl IntoView {
132    let orientation = orientation.unwrap_or_default();
133    let spacing = spacing.unwrap_or_default();
134
135    let class = merge_classes(vec![
136        "label-group",
137        &orientation.to_class(),
138        &spacing.to_class(),
139        class.as_deref().unwrap_or(""),
140    ]);
141
142    view! {
143        <div
144            class=class
145            style=style
146            role="group"
147            aria-label="Label group"
148            data-orientation=orientation.to_string()
149            data-spacing=spacing.to_string()
150        >
151            {children.map(|c| c())}
152        </div>
153    }
154}
155
156/// Label Size enum
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
158pub enum LabelSize {
159    #[default]
160    Small,
161    Medium,
162    Large,
163}
164
165impl LabelSize {
166    pub fn to_class(&self) -> &'static str {
167        match self {
168            LabelSize::Small => "size-small",
169            LabelSize::Medium => "size-medium",
170            LabelSize::Large => "size-large",
171        }
172    }
173
174    pub fn to_string(&self) -> &'static str {
175        match self {
176            LabelSize::Small => "small",
177            LabelSize::Medium => "medium",
178            LabelSize::Large => "large",
179        }
180    }
181}
182
183/// Label Variant enum
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
185pub enum LabelVariant {
186    #[default]
187    Default,
188    Primary,
189    Secondary,
190    Success,
191    Warning,
192    Error,
193}
194
195impl LabelVariant {
196    pub fn to_class(&self) -> &'static str {
197        match self {
198            LabelVariant::Default => "variant-default",
199            LabelVariant::Primary => "variant-primary",
200            LabelVariant::Secondary => "variant-secondary",
201            LabelVariant::Success => "variant-success",
202            LabelVariant::Warning => "variant-warning",
203            LabelVariant::Error => "variant-error",
204        }
205    }
206
207    pub fn to_string(&self) -> &'static str {
208        match self {
209            LabelVariant::Default => "default",
210            LabelVariant::Primary => "primary",
211            LabelVariant::Secondary => "secondary",
212            LabelVariant::Success => "success",
213            LabelVariant::Warning => "warning",
214            LabelVariant::Error => "error",
215        }
216    }
217}
218
219/// Label Orientation enum
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
221pub enum LabelOrientation {
222    #[default]
223    Horizontal,
224    Vertical,
225}
226
227impl LabelOrientation {
228    pub fn to_class(&self) -> &'static str {
229        match self {
230            LabelOrientation::Horizontal => "orientation-horizontal",
231            LabelOrientation::Vertical => "orientation-vertical",
232        }
233    }
234
235    pub fn to_string(&self) -> &'static str {
236        match self {
237            LabelOrientation::Horizontal => "horizontal",
238            LabelOrientation::Vertical => "vertical",
239        }
240    }
241}
242
243/// Label Spacing enum
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
245pub enum LabelSpacing {
246    #[default]
247    Tight,
248    Normal,
249    Loose,
250}
251
252impl LabelSpacing {
253    pub fn to_class(&self) -> &'static str {
254        match self {
255            LabelSpacing::Tight => "spacing-tight",
256            LabelSpacing::Normal => "spacing-normal",
257            LabelSpacing::Loose => "spacing-loose",
258        }
259    }
260
261    pub fn to_string(&self) -> &'static str {
262        match self {
263            LabelSpacing::Tight => "tight",
264            LabelSpacing::Normal => "normal",
265            LabelSpacing::Loose => "loose",
266        }
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use proptest::prelude::*;
273    use wasm_bindgen_test::*;
274
275    wasm_bindgen_test_configure!(run_in_browser);
276
277    // Unit Tests
278    #[test]
279    fn test_label_creation() {}
280    #[test]
281    fn test_label_with_class() {}
282    #[test]
283    fn test_label_with_style() {}
284    #[test]
285    fn test_label_for_id() {}
286    #[test]
287    fn test_labelrequired() {}
288    #[test]
289    fn test_labeldisabled() {}
290    #[test]
291    fn test_label_size() {}
292    #[test]
293    fn test_label_variant() {}
294    #[test]
295    fn test_label_on_click() {}
296
297    // Label Text tests
298    #[test]
299    fn test_label_text_creation() {}
300    #[test]
301    fn test_label_text_with_class() {}
302    #[test]
303    fn test_label_text_text() {}
304    #[test]
305    fn test_label_textrequired() {}
306
307    // Label Description tests
308    #[test]
309    fn test_label_description_creation() {}
310    #[test]
311    fn test_label_description_with_class() {}
312    #[test]
313    fn test_label_description_description() {}
314
315    // Label Error tests
316    #[test]
317    fn test_label_error_creation() {}
318    #[test]
319    fn test_label_error_with_class() {}
320    #[test]
321    fn test_label_error_error() {}
322    #[test]
323    fn test_label_errorvisible() {}
324
325    // Label Group tests
326    #[test]
327    fn test_label_group_creation() {}
328    #[test]
329    fn test_label_group_with_class() {}
330    #[test]
331    fn test_label_group_orientation() {}
332    #[test]
333    fn test_label_group_spacing() {}
334
335    // Label Size tests
336    #[test]
337    fn test_label_size_default() {}
338    #[test]
339    fn test_label_size_small() {}
340    #[test]
341    fn test_label_size_medium() {}
342    #[test]
343    fn test_label_size_large() {}
344
345    // Label Variant tests
346    #[test]
347    fn test_label_variant_default() {}
348    #[test]
349    fn test_label_variant_primary() {}
350    #[test]
351    fn test_label_variant_secondary() {}
352    #[test]
353    fn test_label_variant_success() {}
354    #[test]
355    fn test_label_variant_warning() {}
356    #[test]
357    fn test_label_variant_error() {}
358
359    // Label Orientation tests
360    #[test]
361    fn test_label_orientation_default() {}
362    #[test]
363    fn test_label_orientation_horizontal() {}
364    #[test]
365    fn test_label_orientation_vertical() {}
366
367    // Label Spacing tests
368    #[test]
369    fn test_label_spacing_default() {}
370    #[test]
371    fn test_label_spacing_tight() {}
372    #[test]
373    fn test_label_spacing_normal() {}
374    #[test]
375    fn test_label_spacing_loose() {}
376
377    // Helper function tests
378    #[test]
379    fn test_merge_classes_empty() {}
380    #[test]
381    fn test_merge_classes_single() {}
382    #[test]
383    fn test_merge_classes_multiple() {}
384    #[test]
385    fn test_merge_classes_with_empty() {}
386
387    // Property-based Tests
388    #[test]
389    fn test_label_property_based() {
390        proptest!(|(____class in ".*", __style in ".*")| {
391
392        });
393    }
394
395    #[test]
396    fn test_label_accessibility_validation() {
397        proptest!(|(__for_id in ".*", _required: bool, _disabled: bool)| {
398
399        });
400    }
401
402    #[test]
403    fn test_label_variant_validation() {
404        proptest!(|(____variant in ".*")| {
405
406        });
407    }
408
409    // Integration Tests
410    #[test]
411    fn test_label_accessibility() {}
412    #[test]
413    fn test_label_form_integration() {}
414    #[test]
415    fn test_label_validation_workflow() {}
416    #[test]
417    fn test_label_error_display() {}
418    #[test]
419    fn test_label_responsive_behavior() {}
420
421    // Performance Tests
422    #[test]
423    fn test_label_large_forms() {}
424    #[test]
425    fn test_label_render_performance() {}
426    #[test]
427    fn test_label_memory_usage() {}
428    #[test]
429    fn test_label_validation_performance() {}
430}