Skip to main content

fc_dioxus/components/
segmented.rs

1//! A row of mutually exclusive or independent options drawn as one control, from
2//! segmented.css.
3use crate::{
4    core::{
5        assets,
6        attrs::{availability_attributes, component_attributes},
7    },
8    prelude::*,
9    theme::use_stylesheets,
10};
11const BEM: Bem = Bem::new("fc-segmented");
12const HIDDEN: Bem = Bem::new("fc-u-visually-hidden");
13
14/// Whether a segmented control picks one option or any number.
15#[derive(Debug, Clone, PartialEq)]
16pub enum Segments {
17    /// Radios: exactly one value.
18    Single {
19        /// Shared input name.
20        name: Id,
21        /// The chosen value.
22        value: Option<String>,
23        /// Called with the newly chosen value.
24        onchange: Option<EventHandler<String>>,
25    },
26    /// Checkboxes: any set of values.
27    Multiple {
28        /// The chosen values.
29        values: Vec<String>,
30        /// Called with the new set, in item order of toggling.
31        onchange: Option<EventHandler<Vec<String>>>,
32    },
33}
34
35#[derive(Debug, Clone, PartialEq)]
36struct SegmentContext {
37    segments: Segments,
38    invalid: bool,
39    required: bool,
40}
41
42/// A fieldset of [`Segment`]s. The legend is visually hidden; the options say enough to
43/// sighted users, but a screen reader needs the question.
44#[component]
45pub fn Segmented(
46    legend: AccessibleName,
47    segments: Segments,
48    #[props(default)] invalid: bool,
49    #[props(default)] required: bool,
50    #[props(default)] class: Option<String>,
51    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
52    children: Element,
53) -> Element {
54    let context = SegmentContext {
55        segments,
56        invalid,
57        required,
58    };
59    let mut shared = use_context_provider(|| Signal::new(context.clone()));
60    if *shared.peek() != context {
61        shared.set(context);
62    }
63    let attributes = component_attributes(
64        attributes,
65        ClassList::new(BEM.block()).extend(class.as_deref()),
66        &[],
67    );
68    rsx! {
69        if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_SEGMENTED_CSS } }
70        fieldset { ..attributes,
71            legend { class: HIDDEN.block(), "{legend}" }
72            {children}
73        }
74    }
75}
76
77/// One option of a [`Segmented`] control.
78#[component]
79pub fn Segment(
80    value: String,
81    #[props(default)] availability: Availability,
82    children: Element,
83) -> Element {
84    let Some(context) = try_consume_context::<Signal<SegmentContext>>().map(|c| c.read().clone())
85    else {
86        return rsx! {};
87    };
88    let blocked = availability != Availability::Enabled;
89    let state = availability_attributes(&availability);
90    let (kind, name, checked) = match &context.segments {
91        Segments::Single {
92            name,
93            value: chosen,
94            ..
95        } => (
96            "radio",
97            Some(name.to_string()),
98            chosen.as_deref() == Some(value.as_str()),
99        ),
100        Segments::Multiple { values, .. } => ("checkbox", None, values.contains(&value)),
101    };
102    let segments = context.segments.clone();
103    let own = value.clone();
104    rsx! {
105        label { class: BEM.element("item"),
106            input { class: BEM.element("input"), r#type: kind, name, value: value.clone(), checked,
107                aria_invalid: context.invalid.then_some("true"), required: context.required,
108                onclick: move |event| if blocked { event.prevent_default(); },
109                onchange: move |event| match &segments {
110                    Segments::Single { onchange: Some(onchange), .. } => onchange.call(own.clone()),
111                    Segments::Multiple { values, onchange: Some(onchange) } => {
112                        let mut next: Vec<String> = values.iter().filter(|v| **v != own).cloned().collect();
113                        if event.checked() { next.push(own.clone()); }
114                        onchange.call(next);
115                    }
116                    _ => {}
117                },
118                ..state,
119            }
120            span { class: BEM.element("check"), aria_hidden: "true" }
121            {children}
122        }
123    }
124}