#[allow(unused_imports)]
use leptos::prelude::Effect;
#[allow(unused_imports)]
use leptos::prelude::{
AriaAttributes, Callable, Callback, Children, ClassAttribute, CustomAttribute, ElementChild,
Get, GetUntracked, GlobalAttributes, IntoAny, IntoView, OnAttribute, Set, Signal,
StyleAttribute, Update, component, view,
};
#[allow(unused_imports)]
use std::cell::Cell;
use crate::util::TestAttr;
pub type NavbarMenuContext = leptos::prelude::RwSignal<bool>;
#[derive(Clone)]
pub struct NavbarMenuController {
pub open: Callback<()>,
pub close: Callback<()>,
pub toggle: Callback<()>,
pub set_open: Callback<bool>,
}
pub type NavbarMenuControllerContext = NavbarMenuController;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NavbarFixed {
Top,
Bottom,
}
impl NavbarFixed {
fn bulma(self) -> &'static str {
match self {
NavbarFixed::Top => "is-fixed-top",
NavbarFixed::Bottom => "is-fixed-bottom",
}
}
}
#[component]
pub fn Navbar(
#[prop(optional, into)]
classes: Signal<String>,
#[prop(optional)]
fixed: Option<NavbarFixed>,
#[prop(optional, into)]
transparent: Signal<bool>,
#[prop(optional, into)]
spaced: Signal<bool>,
#[prop(optional, into)]
padded: Signal<bool>,
#[prop(optional, into)]
navburger: Signal<bool>,
#[prop(optional)]
brand: Option<Children>,
#[prop(optional)]
start: Option<Children>,
#[prop(optional)]
end: Option<Children>,
#[prop(optional, into)]
test_attr: Option<TestAttr>,
) -> impl IntoView {
let class = {
let classes = classes.clone();
let transparent = transparent.clone();
let spaced = spaced.clone();
move || {
let mut parts = vec!["navbar".to_string()];
let extra = classes.get();
if !extra.trim().is_empty() {
parts.push(extra);
}
if let Some(fx) = fixed {
parts.push(fx.bulma().to_string());
}
if transparent.get() {
parts.push("is-transparent".to_string());
}
if spaced.get() {
parts.push("is-spaced".to_string());
}
parts.join(" ")
}
};
let is_menu_open = leptos::prelude::RwSignal::new(false);
leptos::prelude::provide_context::<NavbarMenuContext>(is_menu_open);
let open_cb = {
let is_menu_open = is_menu_open;
Callback::new(move |_| is_menu_open.set(true))
};
let close_cb = {
let is_menu_open = is_menu_open;
Callback::new(move |_| is_menu_open.set(false))
};
let toggle_cb = {
let is_menu_open = is_menu_open;
Callback::new(move |_| is_menu_open.update(|v| *v = !*v))
};
let set_open_cb = {
let is_menu_open = is_menu_open;
Callback::new(move |v: bool| is_menu_open.set(v))
};
leptos::prelude::provide_context::<NavbarMenuControllerContext>(NavbarMenuController {
open: open_cb,
close: close_cb,
toggle: toggle_cb,
set_open: set_open_cb,
});
let brand_view = brand.map(|children| children().into_any());
let start_view = start.map(|children| children().into_any());
let end_view = end.map(|children| children().into_any());
let padded_initial = padded.get_untracked();
let (data_testid, data_cy) = match &test_attr {
Some(attr) if attr.key == "data-testid" => (Some(attr.value.clone()), None),
Some(attr) if attr.key == "data-cy" => (None, Some(attr.value.clone())),
_ => (None, None),
};
let burger_node = {
let toggle_cb = toggle_cb.clone();
move || {
if !navburger.get() {
return view! { <></> }.into_any();
}
let burger_class = move || {
if is_menu_open.get() {
"navbar-burger is-active"
} else {
"navbar-burger"
}
};
view! {
<a
class=burger_class
role="button"
aria-label="menu"
aria-expanded=move || if is_menu_open.get() { "true" } else { "false" }
on:click=move |_| toggle_cb.run(())
href="#"
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
}
.into_any()
}
};
view! {
<nav
class=move || class()
role="navigation"
aria-label="main navigation"
attr:data-testid=move || data_testid.clone()
attr:data-cy=move || data_cy.clone()
>
{
if padded_initial {
view! {
<div class="container">
<div class="navbar-brand">
{brand_view.unwrap_or_else(|| view! { <></> }.into_any())}
{burger_node()}
</div>
<div class=move || if is_menu_open.get() { "navbar-menu is-active" } else { "navbar-menu" }>
<div class="navbar-start">
{start_view.unwrap_or_else(|| view! { <></> }.into_any())}
</div>
<div class="navbar-end">
{end_view.unwrap_or_else(|| view! { <></> }.into_any())}
</div>
</div>
</div>
}
.into_any()
} else {
view! {
<>
<div class="navbar-brand">
{brand_view.unwrap_or_else(|| view! { <></> }.into_any())}
{burger_node()}
</div>
<div class=move || if is_menu_open.get() { "navbar-menu is-active" } else { "navbar-menu" }>
<div class="navbar-start">
{start_view.unwrap_or_else(|| view! { <></> }.into_any())}
</div>
<div class="navbar-end">
{end_view.unwrap_or_else(|| view! { <></> }.into_any())}
</div>
</div>
</>
}
.into_any()
}
}
</nav>
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NavbarItemTag {
A,
Div,
}
#[component]
pub fn NavbarItem(
children: Children,
#[prop(optional, into)]
classes: Signal<String>,
#[prop(optional)]
tag: Option<NavbarItemTag>,
#[prop(optional)]
on_click: Option<Callback<()>>,
#[prop(optional, into)]
auto_close: Signal<bool>,
#[prop(optional, into)]
has_dropdown: Signal<bool>,
#[prop(optional, into)]
expanded: Signal<bool>,
#[prop(optional, into)]
tab: Signal<bool>,
#[prop(optional, into)]
active: Signal<bool>,
#[prop(optional, into)]
href: Signal<String>,
#[prop(optional, into)] rel: Signal<String>,
#[prop(optional, into)] target: Signal<String>,
#[prop(optional, into)]
test_attr: Option<TestAttr>,
) -> impl IntoView {
let class = {
let classes = classes.clone();
let has_dropdown = has_dropdown.clone();
let expanded = expanded.clone();
let tab = tab.clone();
let active = active.clone();
move || {
let mut parts = vec!["navbar-item".to_string()];
let extra = classes.get();
if !extra.trim().is_empty() {
parts.push(extra);
}
if has_dropdown.get() {
parts.push("has-dropdown".to_string());
}
if expanded.get() {
parts.push("is-expanded".to_string());
}
if tab.get() {
parts.push("is-tab".to_string());
}
if active.get() {
parts.push("is-active".to_string());
}
parts.join(" ")
}
};
let tag = tag.unwrap_or(NavbarItemTag::Div);
let controller = leptos::prelude::use_context::<NavbarMenuControllerContext>();
let handle_click = move |_| {
if let Some(cb) = &on_click {
cb.run(());
}
if auto_close.get() {
if let Some(ctrl) = &controller {
ctrl.close.run(());
}
}
};
let (data_testid, data_cy) = match &test_attr {
Some(attr) if attr.key == "data-testid" => (Some(attr.value.clone()), None),
Some(attr) if attr.key == "data-cy" => (None, Some(attr.value.clone())),
_ => (None, None),
};
match tag {
NavbarItemTag::A => view! {
<a
class=move || class()
href=href.get()
rel=rel.get()
target=target.get()
on:click=handle_click
attr:data-testid=move || data_testid.clone()
attr:data-cy=move || data_cy.clone()
>
{children()}
</a>
}
.into_any(),
NavbarItemTag::Div => view! {
<div
class=move || class()
on:click=handle_click
attr:data-testid=move || data_testid.clone()
attr:data-cy=move || data_cy.clone()
>
{children()}
</div>
}
.into_any(),
}
}
#[component]
pub fn NavbarDivider(
#[prop(optional, into)]
classes: Signal<String>,
#[prop(optional, into)]
test_attr: Option<TestAttr>,
) -> impl IntoView {
let class = {
let classes = classes.clone();
move || {
let extra = classes.get();
if extra.trim().is_empty() {
"navbar-divider".to_string()
} else {
format!("navbar-divider {}", extra)
}
}
};
let (data_testid, data_cy) = match &test_attr {
Some(attr) if attr.key == "data-testid" => (Some(attr.value.clone()), None),
Some(attr) if attr.key == "data-cy" => (None, Some(attr.value.clone())),
_ => (None, None),
};
view! {
<hr
class=move || class()
attr:data-testid=move || data_testid.clone()
attr:data-cy=move || data_cy.clone()
/>
}
}
#[component]
pub fn NavbarDropdown(
children: Children,
#[prop(optional, into)]
classes: Signal<String>,
navlink: Children,
#[prop(optional, into)]
hoverable: Signal<bool>,
#[prop(optional, into)]
dropup: Signal<bool>,
#[prop(optional, into)]
right: Signal<bool>,
#[prop(optional, into)]
arrowless: Signal<bool>,
#[prop(optional, into)]
boxed: Signal<bool>,
#[prop(optional, into)]
test_attr: Option<TestAttr>,
) -> impl IntoView {
let (is_active, set_is_active) = leptos::prelude::signal(false);
let container_class = {
let classes = classes.clone();
let hoverable = hoverable.clone();
let dropup = dropup.clone();
move || {
let mut parts = vec!["navbar-item".to_string(), "has-dropdown".to_string()];
let extra = classes.get();
if !extra.trim().is_empty() {
parts.push(extra);
}
if dropup.get() {
parts.push("has-dropdown-up".to_string());
}
if hoverable.get() {
parts.push("is-hoverable".to_string());
}
if is_active.get() && !hoverable.get() {
parts.push("is-active".to_string());
}
parts.join(" ")
}
};
let dropdown_class = {
let right = right.clone();
let boxed = boxed.clone();
move || {
let mut parts = vec!["navbar-dropdown".to_string()];
if right.get() {
parts.push("is-right".to_string());
}
if boxed.get() {
parts.push("is-boxed".to_string());
}
parts.join(" ")
}
};
let link_class = {
let arrowless = arrowless.clone();
move || {
let mut parts = vec!["navbar-link".to_string()];
if arrowless.get() {
parts.push("is-arrowless".to_string());
}
parts.join(" ")
}
};
let (data_testid, data_cy) = match &test_attr {
Some(attr) if attr.key == "data-testid" => (Some(attr.value.clone()), None),
Some(attr) if attr.key == "data-cy" => (None, Some(attr.value.clone())),
_ => (None, None),
};
view! {
<div
class=move || container_class()
attr:data-testid=move || data_testid.clone()
attr:data-cy=move || data_cy.clone()
>
{move || if is_active.get() && !hoverable.get() {
view! {
<div
style="z-index:10;background-color:rgba(0,0,0,0);position:fixed;top:0;bottom:0;left:0;right:0;"
></div>
}.into_any()
} else {
view! { <></> }.into_any()
}}
<a
class=move || link_class()
href="#"
>
{navlink()}
</a>
<div class=move || dropdown_class()>
{children()}
</div>
</div>
}
}