dwui/components/widgets/
tab_list.rs1use crate::theme::prelude::*;
2use crate::utils::component_id;
3use dominator::{clone, events, html, Dom, EventOptions};
4use dwind::prelude::*;
5use futures_signals::signal::SignalExt;
6use futures_signals::signal_vec::SignalVecExt;
7use futures_signals_component_macro::component;
8use std::rc::Rc;
9use web_sys::wasm_bindgen::JsCast;
10
11#[component(render_fn = tab_list)]
22struct TabList {
23 #[signal_vec]
24 #[default(vec![])]
25 tabs: (String, String),
26
27 #[signal]
28 #[default("".to_string())]
29 selected: String,
30
31 #[default(Box::new(|_|{}))]
32 on_select: dyn Fn(String) + 'static,
33
34 #[signal]
36 #[default("Tabs".to_string())]
37 aria_label: String,
38}
39
40pub fn tab_list(props: TabListProps) -> Dom {
41 let TabListProps {
42 tabs,
43 selected,
44 on_select,
45 aria_label,
46 apply,
47 } = props;
48
49 let on_select = Rc::new(on_select);
50 let selected = selected.broadcast();
51 let tabs = tabs.to_signal_cloned().broadcast();
52
53 let tablist_id = component_id("tablist");
54
55 let tab_dom_id = {
56 let tablist_id = tablist_id.clone();
57 move |key: &str| format!("{}-tab-{}", tablist_id, key)
58 };
59
60 html!("div", {
61 .attr("role", "tablist")
62 .attr("id", &tablist_id)
63 .attr_signal("aria-label", aria_label)
64 .dwclass!("flex flex-row gap-1 border-b")
65 .dwclass!("dwui-border-void-700 is(.light *):dwui-border-void-300")
66 .children_signal_vec(tabs.signal_cloned().map(clone!(selected, tab_dom_id => move |tabs_vec| {
67 tabs_vec
68 .iter()
69 .enumerate()
70 .map(|(index, (key, label))| {
71 let key = key.clone();
72 let is_selected = selected
73 .signal_cloned()
74 .map(clone!(key => move |s| s == key))
75 .broadcast();
76
77 html!("button", {
78 .attr("type", "button")
79 .attr("role", "tab")
80 .attr("id", &tab_dom_id(&key))
81 .attr_signal("aria-selected", is_selected.signal().map(|v| if v { "true" } else { "false" }))
82 .attr_signal("tabindex", is_selected.signal().map(|v| if v { "0" } else { "-1" }))
83 .dwclass!("h-10 p-l-4 p-r-4 cursor-pointer font-medium text-base transition-colors bg-transparent")
84 .dwclass!("rounded-t-md border-b-2")
85 .dwclass!("dwui-focusable")
86 .dwclass_signal!("dwui-text-on-primary-50 is(.light *):dwui-text-on-primary-950", is_selected.signal())
87 .dwclass_signal!("dwui-border-primary-400 is(.light *):dwui-border-primary-600", is_selected.signal())
88 .dwclass_signal!(
89 "dwui-text-on-primary-400 hover:dwui-text-on-primary-200 is(.light *):dwui-text-on-primary-600 is(.light *):hover:dwui-text-on-primary-800",
90 is_selected.signal().map(|v| !v)
91 )
92 .dwclass_signal!("border-transparent", is_selected.signal().map(|v| !v))
93 .text(label)
94 .event(clone!(on_select, key => move |_: events::Click| {
95 (on_select)(key.clone());
96 }))
97 .event_with_options(&EventOptions::preventable(), clone!(on_select, tabs_vec, tab_dom_id => move |e: events::KeyDown| {
98 let count = tabs_vec.len();
99
100 if count == 0 {
101 return;
102 }
103
104 let target_index = match e.key().as_str() {
105 "ArrowRight" => (index + 1) % count,
106 "ArrowLeft" => (index + count - 1) % count,
107 "Home" => 0,
108 "End" => count - 1,
109 _ => return,
110 };
111
112 e.prevent_default();
113
114 let target_key = tabs_vec[target_index].0.clone();
115
116 if let Some(element) = web_sys::window()
118 .and_then(|w| w.document())
119 .and_then(|d| d.get_element_by_id(&tab_dom_id(&target_key)))
120 {
121 if let Ok(element) = element.dyn_into::<web_sys::HtmlElement>() {
122 let _ = element.focus();
123 }
124 }
125
126 (on_select)(target_key);
127 }))
128 })
129 })
130 .collect::<Vec<_>>()
131 })).to_signal_vec())
132 .apply_if(apply.is_some(), |b| b.apply(apply.unwrap()))
133 })
134}