Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for multiple options in Select #166

Merged
merged 23 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3c007c7
feat!: Add support for multiple options in Select
tqwewe Apr 15, 2024
d0e96eb
feat: Sync Select menu when value is updated
tqwewe Apr 15, 2024
5b09c9d
feat: Decrease line height of multi select items
tqwewe Apr 15, 2024
7bfd44b
fix: Invalid callback on multiselect tag close
tqwewe Apr 15, 2024
1e595b5
feat: Sync Select menu only for multiple values
tqwewe Apr 15, 2024
d1b0aff
feat: separate `MultiSelect` from `Select` component
tqwewe Apr 19, 2024
cb77f35
fix: SelectLabel slot implementation
tqwewe Apr 19, 2024
751eb72
feat: rename `MultiSelect` to `SelectMulti`
tqwewe Apr 19, 2024
56576ba
feat: rename and move `MultiSelect`
tqwewe Apr 20, 2024
c47bcdf
feat: add arrow to select component
tqwewe Apr 20, 2024
d91d0d5
feat: lower opacity of select dropdown arrow icon
tqwewe Apr 20, 2024
152ffab
fix: inconsistent select font size
tqwewe Apr 20, 2024
d6abfc9
fix: select menu font size
tqwewe Apr 20, 2024
a4a87c6
feat: add clear button to multi select
tqwewe Apr 20, 2024
f64d086
fix: Multi select icon on click attribute
tqwewe Apr 20, 2024
e2c0b2c
feat: use inline-block for select component
tqwewe Apr 20, 2024
95f2335
feat: detect select min width based on options
tqwewe Apr 20, 2024
183a5a0
feat: add `allow_clear` prop to `MultiSelect`
tqwewe Apr 20, 2024
3314ff7
feat: remove select min width detection
tqwewe Apr 21, 2024
d78e671
feat: use `Children` for `SelectLabel`
tqwewe Apr 22, 2024
eafaeda
feat: rename `allow_clear` to `clearable`
tqwewe Apr 22, 2024
c341f37
fix: follower min width
tqwewe Apr 22, 2024
68928ad
feat: remove inline-block from `Select`
tqwewe Apr 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 3 additions & 12 deletions demo/src/components/switch_version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,9 @@ use thaw::*;
#[component]
pub fn SwitchVersion() -> impl IntoView {
let options = vec![
SelectOption {
label: "main".into(),
value: "https://thawui.vercel.app".into(),
},
SelectOption {
label: "0.2.6".into(),
value: "https://thaw-mzh1656cm-thaw.vercel.app".into(),
},
SelectOption {
label: "0.2.5".into(),
value: "https://thaw-8og1kv8zs-thaw.vercel.app".into(),
},
SelectOption::new("main", "https://thawui.vercel.app".into()),
SelectOption::new("0.2.6", "https://thaw-mzh1656cm-thaw.vercel.app".into()),
SelectOption::new("0.2.5", "https://thaw-8og1kv8zs-thaw.vercel.app".into()),
];

cfg_if::cfg_if! {
Expand Down
52 changes: 43 additions & 9 deletions demo_markdown/docs/select/mod.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,37 @@
let value = create_rw_signal(None::<String>);

let options = vec![
SelectOption {
label: String::from("RwSignal"),
value: String::from("rw_signal"),
},
SelectOption {
label: String::from("Memo"),
value: String::from("memo"),
},
SelectOption::new("RwSignal", String::from("rw_signal")),
SelectOption::new("Memo", String::from("memo")),
];

view! {
<Select value options/>
<Select value options />
}
```

# Multiple Select

```rust demo
let value = create_rw_signal(vec![
"rust".to_string(),
"javascript".to_string(),
"zig".to_string(),
"python".to_string(),
"cpp".to_string(),
]);

let options = vec![
MultiSelectOption::new("Rust", String::from("rust")).with_variant(TagVariant::Success),
MultiSelectOption::new("JavaScript", String::from("javascript")),
MultiSelectOption::new("Python", String::from("python")).with_variant(TagVariant::Warning),
MultiSelectOption::new("C++", String::from("cpp")).with_variant(TagVariant::Error),
MultiSelectOption::new("Lua", String::from("lua")),
MultiSelectOption::new("Zig", String::from("zig")),
];

view! {
<MultiSelect value options />
}
```

Expand All @@ -26,3 +45,18 @@ view! {
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the select element. |
| value | `Model<Option<T>>` | `None` | Checked value. |
| options | `MaybeSignal<Vec<SelectOption<T>>>` | `vec![]` | Options that can be selected. |

### Multiple Select Props

| Name | Type | Default | Description |
| ----------- | ----------------------------------- | -------------------- | ----------------------------------------- |
| class | `OptionalProp<MaybeSignal<String>>` | `Default::default()` | Addtional classes for the select element. |
| value | `Model<Vec<T>>` | `vec![]` | Checked values. |
| options | `MaybeSignal<Vec<SelectOption<T>>>` | `vec![]` | Options that can be selected. |
| allow_clear | `MaybeSignal<bool>` | `false` | Allow the options to be cleared. |
tqwewe marked this conversation as resolved.
Show resolved Hide resolved

### Select Slots

| Name | Default | Description |
| ----------- | ------- | ------------- |
| SelectLabel | `None` | Select label. |
12 changes: 6 additions & 6 deletions demo_markdown/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ use syn::ItemFn;
macro_rules! file_path {
($($key:expr => $value:expr),*) => {
{
let mut pairs = Vec::new();
$(
pairs.push(($key, include_str!($value)));
)*
pairs
vec![
$(
($key, include_str!($value)),
)*
]
}
}
}
Expand Down Expand Up @@ -95,7 +95,7 @@ pub fn include_md(_token_stream: proc_macro::TokenStream) -> proc_macro::TokenSt
})
.map(|demo| {
syn::parse_str::<ItemFn>(&demo)
.expect(&format!("Cannot be resolved as a function: \n {demo}"))
.unwrap_or_else(|_| panic!("Cannot be resolved as a function: \n {demo}"))
})
.collect();

Expand Down
9 changes: 9 additions & 0 deletions thaw/src/icon/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub fn Icon(
/// HTML style attribute.
#[prop(into, optional)]
style: Option<MaybeSignal<String>>,
/// Callback when clicking on the icon.
#[prop(optional, into)]
on_click: Option<Callback<ev::MouseEvent>>,
) -> impl IntoView {
let icon_style = RwSignal::new(None);
let icon_x = RwSignal::new(None);
Expand All @@ -33,6 +36,11 @@ pub fn Icon(
let icon_stroke = RwSignal::new(None);
let icon_fill = RwSignal::new(None);
let icon_data = RwSignal::new(None);
let on_click = move |ev| {
if let Some(click) = on_click.as_ref() {
click.call(ev);
}
};

create_isomorphic_effect(move |_| {
let icon = icon.get();
Expand Down Expand Up @@ -84,6 +92,7 @@ pub fn Icon(
stroke=move || take(icon_stroke)
fill=move || take(icon_fill)
inner_html=move || take(icon_data)
on:click=on_click
></svg>
}
}
Expand Down
216 changes: 67 additions & 149 deletions thaw/src/select/mod.rs
Original file line number Diff line number Diff line change
@@ -1,177 +1,95 @@
mod multi;
mod raw;
mod theme;

pub use multi::*;
pub use theme::SelectTheme;

use crate::{theme::use_theme, Theme};
use leptos::*;
use std::hash::Hash;
use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement, FollowerWidth};
use thaw_utils::{class_list, mount_style, Model, OptionalProp};
use std::{hash::Hash, rc::Rc};
use thaw_utils::{Model, OptionalProp};

#[derive(Clone, PartialEq, Eq, Hash)]
use crate::{
select::raw::{RawSelect, SelectIcon},
Icon,
};

#[slot]
pub struct SelectLabel {
children: ChildrenFn,
}

#[derive(Clone, Default, PartialEq, Eq, Hash)]
pub struct SelectOption<T> {
pub label: String,
pub value: T,
}

impl<T> SelectOption<T> {
pub fn new(label: impl Into<String>, value: T) -> SelectOption<T> {
SelectOption {
label: label.into(),
value,
}
}
}

#[component]
pub fn Select<T>(
#[prop(optional, into)] value: Model<Option<T>>,
#[prop(optional, into)] options: MaybeSignal<Vec<SelectOption<T>>>,
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
#[prop(optional)] select_label: Option<SelectLabel>,
) -> impl IntoView
where
T: Eq + Hash + Clone + 'static,
{
mount_style("select", include_str!("./select.css"));

let theme = use_theme(Theme::light);
let css_vars = create_memo(move |_| {
let mut css_vars = String::new();
theme.with(|theme| {
let border_color_hover = theme.common.color_primary.clone();
css_vars.push_str(&format!("--thaw-border-color-hover: {border_color_hover};"));
css_vars.push_str(&format!(
"--thaw-background-color: {};",
theme.select.background_color
));
css_vars.push_str(&format!("--thaw-font-color: {};", theme.select.font_color));
css_vars.push_str(&format!(
"--thaw-border-color: {};",
theme.select.border_color
));
});

css_vars
});

let menu_css_vars = create_memo(move |_| {
let mut css_vars = String::new();
theme.with(|theme| {
css_vars.push_str(&format!(
"--thaw-background-color: {};",
theme.select.menu_background_color
));
css_vars.push_str(&format!(
"--thaw-background-color-hover: {};",
theme.select.menu_background_color_hover
));
css_vars.push_str(&format!("--thaw-font-color: {};", theme.select.font_color));
css_vars.push_str(&format!(
"--thaw-font-color-selected: {};",
theme.common.color_primary
));
let is_menu_visible = create_rw_signal(false);
let show_menu = move |_| is_menu_visible.set(true);
let hide_menu = move |_| is_menu_visible.set(false);
let is_selected = move |v: &T| with!(|value| value.as_ref() == Some(v));
let on_select: Callback<(ev::MouseEvent, SelectOption<T>)> =
Callback::new(move |(_, option): (ev::MouseEvent, SelectOption<T>)| {
let item_value = option.value;
value.set(Some(item_value));
hide_menu(());
});
css_vars
});

let is_show_menu = create_rw_signal(false);
let trigger_ref = create_node_ref::<html::Div>();
let menu_ref = create_node_ref::<html::Div>();
let show_menu = move |_| {
is_show_menu.set(true);
};

#[cfg(any(feature = "csr", feature = "hydrate"))]
{
use leptos::wasm_bindgen::__rt::IntoJsResult;
let timer = window_event_listener(ev::click, move |ev| {
let el = ev.target();
let mut el: Option<web_sys::Element> =
el.into_js_result().map_or(None, |el| Some(el.into()));
let body = document().body().unwrap();
while let Some(current_el) = el {
if current_el == *body {
break;
};
if current_el == ***menu_ref.get().unwrap()
|| current_el == ***trigger_ref.get().unwrap()
{
return;
let select_label = select_label.unwrap_or_else(|| {
let options = options.clone();
let value_label = Signal::derive(move || {
with!(|value, options| {
match value {
Some(value) => options
.iter()
.find(|opt| &opt.value == value)
.map_or(String::new(), |v| v.label.clone()),
None => String::new(),
}
el = current_el.parent_element();
}
is_show_menu.set(false);
})
});
on_cleanup(move || timer.remove());
}

let temp_options = options.clone();
let select_option_label = create_memo(move |_| match value.get() {
Some(value) => temp_options
.get()
.iter()
.find(move |v| v.value == value)
.map_or(String::new(), |v| v.label.clone()),
None => String::new(),
SelectLabel {
children: Rc::new(move || Fragment::new(vec![value_label.into_view()])),
}
});
let select_icon = SelectIcon {
children: Rc::new(move || {
Fragment::new(vec![
view! { <Icon class="thaw-select-dropdown-icon" icon=icondata_ai::AiDownOutlined/> }.into_view()
])
}),
};

view! {
<Binder target_ref=trigger_ref>
<div
class=class_list!["thaw-select", class.map(| c | move || c.get())]
ref=trigger_ref
on:click=show_menu
style=move || css_vars.get()
>

{move || select_option_label.get()}

</div>
<Follower
slot
show=is_show_menu
placement=FollowerPlacement::BottomStart
width=FollowerWidth::Target
>
<CSSTransition
node_ref=menu_ref
name="fade-in-scale-up-transition"
appear=is_show_menu.get_untracked()
show=is_show_menu
let:display
>
<div
class="thaw-select-menu"
style=move || {
display
.get()
.map(|d| d.to_string())
.unwrap_or_else(|| menu_css_vars.get())
}

ref=menu_ref
>
<For
each=move || options.get()
key=move |item| item.value.clone()
children=move |item| {
let item = store_value(item);
let onclick = move |_| {
let SelectOption { value: item_value, label: _ } = item
.get_value();
value.set(Some(item_value));
is_show_menu.set(false);
};
view! {
<div
class="thaw-select-menu__item"
class=(
"thaw-select-menu__item-selected",
move || value.get() == Some(item.get_value().value),
)

on:click=onclick
>
{item.get_value().label}
</div>
}
}
/>

</div>
</CSSTransition>
</Follower>
</Binder>
<RawSelect
options
class
select_label
select_icon
is_menu_visible
on_select=on_select
show_menu
hide_menu
is_selected=is_selected
/>
}
}
Loading