Skip to main content

fc_dioxus/components/
search.rs

1//! A search landmark with a suggestions view, from search.css.
2use crate::{
3    core::{
4        assets,
5        attrs::{availability_attributes, component_attributes, open_attribute},
6    },
7    prelude::*,
8    theme::use_stylesheets,
9};
10const BEM: Bem = Bem::new("fc-search");
11// The input's and the view's modifiers hang off those elements, not the block.
12const INPUT: Bem = Bem::new("fc-search__input");
13const VIEW: Bem = Bem::new("fc-search__view");
14fc_modifier! { enum ClearButton for INPUT { Native => None, Own => "own-clear" } }
15fc_modifier! { enum ViewPosition for VIEW { Floating => None, Docked => "docked" } }
16
17/// A search bar in a `role="search"` landmark, with an optional [`SearchView`].
18///
19/// With `onsubmit` it is a `<form>`, so Enter submits; without, it is a plain landmark
20/// for filtering as you type.
21#[component]
22pub fn Search(
23    label: AccessibleName,
24    #[props(default)] value: Option<ReadSignal<String>>,
25    #[props(default)] default_value: String,
26    #[props(default)] oninput: Option<EventHandler<String>>,
27    #[props(default)] onsubmit: Option<EventHandler<String>>,
28    #[props(default)] placeholder: Option<String>,
29    /// Trailing actions, e.g. voice search or a named "Clear search" button.
30    #[props(default)]
31    trailing: Option<Element>,
32    /// The trailing slot holds its own clear button, so hide the browser's.
33    #[props(default)]
34    own_clear: bool,
35    #[props(default)] view: Option<Element>,
36    #[props(default)] invalid: bool,
37    #[props(default)] availability: Availability,
38    #[props(default)] class: Option<String>,
39    #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
40) -> Element {
41    let state = use_controllable(value, || default_value, oninput);
42    let attributes = component_attributes(
43        attributes,
44        ClassList::new(BEM.block()).extend(class.as_deref()),
45        &["role"],
46    );
47    let input_class = ClassList::new(INPUT.block())
48        .with_modifier(
49            INPUT,
50            if own_clear {
51                ClearButton::Own
52            } else {
53                ClearButton::Native
54            },
55        )
56        .to_string();
57    let input_state = availability_attributes(&availability);
58    let body = rsx! {
59        div { class: BEM.element("bar"),
60            span { class: BEM.element("icon"), aria_hidden: "true", "⚲" }
61            input { class: input_class, r#type: "search", aria_label: label.to_string(), placeholder,
62                aria_invalid: invalid.then_some("true"), value: state.get(),
63                oninput: move |event| state.set(event.value()), ..input_state }
64            if let Some(trailing) = trailing { span { class: BEM.element("trailing"), {trailing} } }
65        }
66        {view}
67    };
68    rsx! {
69        if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_SEARCH_CSS } }
70        if let Some(onsubmit) = onsubmit {
71            form { role: "search",
72                onsubmit: move |event| { event.prevent_default(); onsubmit.call(state.get()); },
73                ..attributes, {body}
74            }
75        } else {
76            div { role: "search", ..attributes, {body} }
77        }
78    }
79}
80
81/// Recent searches or suggestions under a [`Search`]. It shows while focus is in the
82/// search; `open` holds it open or closed regardless.
83#[component]
84pub fn SearchView(
85    label: AccessibleName,
86    heading: Element,
87    #[props(default)] docked: bool,
88    #[props(default)] open: Option<bool>,
89    /// Results are being fetched; the stale list stays readable.
90    #[props(default)]
91    busy: bool,
92    children: Element,
93) -> Element {
94    let class = ClassList::new(VIEW.block())
95        .with_modifier(
96            VIEW,
97            if docked {
98                ViewPosition::Docked
99            } else {
100                ViewPosition::Floating
101            },
102        )
103        .to_string();
104    let open = open_attribute(open);
105    rsx! {
106        div { class, role: "region", aria_label: label.to_string(), aria_busy: busy.then_some("true"), ..open,
107            p { class: BEM.element("view-label"), {heading} }
108            {children}
109        }
110    }
111}