mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-22 22:09:22 -05:00
Feat/tag picker (#260)
* refactor: Listbox and OptionGroup * feat: adds TagPickerOptionGroup component * fix: call_on_click_outside_with_list
This commit is contained in:
parent
09ef9f8630
commit
092581d46d
15 changed files with 172 additions and 53 deletions
|
@ -115,21 +115,6 @@
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thaw-combobox__listbox {
|
|
||||||
row-gap: var(--spacingHorizontalXXS);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-width: 160px;
|
|
||||||
max-height: 80vh;
|
|
||||||
background-color: var(--colorNeutralBackground1);
|
|
||||||
padding: var(--spacingHorizontalXS);
|
|
||||||
outline: 1px solid var(--colorTransparentStroke);
|
|
||||||
border-radius: var(--borderRadiusMedium);
|
|
||||||
box-shadow: var(--shadow16);
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thaw-combobox-option {
|
.thaw-combobox-option {
|
||||||
column-gap: var(--spacingHorizontalXS);
|
column-gap: var(--spacingHorizontalXS);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
use super::option_group::OptionGroup;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use thaw_utils::{class_list, mount_style};
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ComboboxOptionGroup(
|
pub fn ComboboxOptionGroup(
|
||||||
|
@ -9,17 +9,5 @@ pub fn ComboboxOptionGroup(
|
||||||
label: String,
|
label: String,
|
||||||
children: Children,
|
children: Children,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
mount_style(
|
view! { <OptionGroup class_prefix="thaw-combobox-option-group" class label children /> }
|
||||||
"combobox-option-group",
|
|
||||||
include_str!("./combobox-option-group.css"),
|
|
||||||
);
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<div role="group" class=class_list!["thaw-combobox-option-group", class]>
|
|
||||||
<span role="presentation" class="thaw-combobox-option-group__label">
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
{children()}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,18 @@
|
||||||
|
.thaw-listbox {
|
||||||
|
row-gap: var(--spacingHorizontalXXS);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 160px;
|
||||||
|
max-height: 80vh;
|
||||||
|
background-color: var(--colorNeutralBackground1);
|
||||||
|
padding: var(--spacingHorizontalXS);
|
||||||
|
outline: 1px solid var(--colorTransparentStroke);
|
||||||
|
border-radius: var(--borderRadiusMedium);
|
||||||
|
box-shadow: var(--shadow16);
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.thaw-listbox.fade-in-scale-up-transition-leave-active {
|
.thaw-listbox.fade-in-scale-up-transition-leave-active {
|
||||||
transform-origin: inherit;
|
transform-origin: inherit;
|
||||||
transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1),
|
transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1),
|
||||||
|
@ -20,4 +35,4 @@
|
||||||
.thaw-listbox.fade-in-scale-up-transition-enter-to {
|
.thaw-listbox.fade-in-scale-up-transition-enter-to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
3
thaw/src/combobox/common/mod.rs
Normal file
3
thaw/src/combobox/common/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod listbox;
|
||||||
|
pub mod option_group;
|
||||||
|
mod utils;
|
|
@ -1,10 +1,10 @@
|
||||||
.thaw-combobox-option-group {
|
.thaw-option-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
row-gap: var(--spacingHorizontalXXS);
|
row-gap: var(--spacingHorizontalXXS);
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thaw-combobox-option-group__label {
|
.thaw-option-group__label {
|
||||||
display: block;
|
display: block;
|
||||||
color: var(--colorNeutralForeground3);
|
color: var(--colorNeutralForeground3);
|
||||||
font-weight: var(--fontWeightSemibold);
|
font-weight: var(--fontWeightSemibold);
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
border-radius: var(--borderRadiusMedium);
|
border-radius: var(--borderRadiusMedium);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thaw-combobox-option-group:not(:last-child)::after {
|
.thaw-option-group:not(:last-child)::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 calc(var(--spacingHorizontalXS) * -1) var(--spacingVerticalXS);
|
margin: 0 calc(var(--spacingHorizontalXS) * -1) var(--spacingVerticalXS);
|
25
thaw/src/combobox/common/option_group.rs
Normal file
25
thaw/src/combobox/common/option_group.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use thaw_utils::{class_list, mount_style};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn OptionGroup(
|
||||||
|
class_prefix: &'static str,
|
||||||
|
class: MaybeProp<String>,
|
||||||
|
/// Label of the group.
|
||||||
|
label: String,
|
||||||
|
children: Children,
|
||||||
|
) -> impl IntoView {
|
||||||
|
mount_style("option-group", include_str!("./option-group.css"));
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div role="group" class=class_list!["thaw-option-group", class_prefix, class]>
|
||||||
|
<span
|
||||||
|
role="presentation"
|
||||||
|
class=format!("thaw-option-group__label {class_prefix}__label")
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
{children()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
mod combobox;
|
mod combobox;
|
||||||
mod combobox_option;
|
mod combobox_option;
|
||||||
mod combobox_option_group;
|
mod combobox_option_group;
|
||||||
pub(crate) mod listbox;
|
mod common;
|
||||||
mod utils;
|
|
||||||
|
|
||||||
pub use combobox::*;
|
pub use combobox::*;
|
||||||
pub use combobox_option::*;
|
pub use combobox_option::*;
|
||||||
pub use combobox_option_group::*;
|
pub use combobox_option_group::*;
|
||||||
|
pub(crate) use common::*;
|
||||||
|
|
|
@ -37,6 +37,76 @@ view! {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Grouped
|
||||||
|
|
||||||
|
```rust demo
|
||||||
|
use leptos::either::Either;
|
||||||
|
let selected_options = RwSignal::new(vec![]);
|
||||||
|
let land = vec!["Cat", "Dog", "Ferret", "Hamster"];
|
||||||
|
let water = vec!["Fish", "Jellyfish", "Octopus", "Seal"];
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<TagPicker selected_options>
|
||||||
|
<TagPickerControl slot>
|
||||||
|
<TagPickerGroup>
|
||||||
|
{move || {
|
||||||
|
selected_options.get().into_iter().map(|option| view!{
|
||||||
|
<Tag value=option.clone()>
|
||||||
|
{option}
|
||||||
|
</Tag>
|
||||||
|
}).collect_view()
|
||||||
|
}}
|
||||||
|
</TagPickerGroup>
|
||||||
|
<TagPickerInput />
|
||||||
|
</TagPickerControl>
|
||||||
|
{move || {
|
||||||
|
selected_options.with(|selected_options| {
|
||||||
|
let land_view = land.iter().filter_map(|option| {
|
||||||
|
if selected_options.iter().any(|o| o == option) {
|
||||||
|
return None
|
||||||
|
} else {
|
||||||
|
Some(view! {
|
||||||
|
<TagPickerOption value=option.clone() text=option.clone() />
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).collect_view();
|
||||||
|
if land_view.is_empty() {
|
||||||
|
Either::Left(())
|
||||||
|
} else {
|
||||||
|
Either::Right(view! {
|
||||||
|
<TagPickerOptionGroup label="Land">
|
||||||
|
{land_view}
|
||||||
|
</TagPickerOptionGroup>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
{move || {
|
||||||
|
selected_options.with(|selected_options| {
|
||||||
|
let water_view = water.iter().filter_map(|option| {
|
||||||
|
if selected_options.iter().any(|o| o == option) {
|
||||||
|
return None
|
||||||
|
} else {
|
||||||
|
Some(view! {
|
||||||
|
<TagPickerOption value=option.clone() text=option.clone() />
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).collect_view();
|
||||||
|
if water_view.is_empty() {
|
||||||
|
Either::Left(())
|
||||||
|
} else {
|
||||||
|
Either::Right(view! {
|
||||||
|
<TagPickerOptionGroup label="Sea">
|
||||||
|
{water_view}
|
||||||
|
</TagPickerOptionGroup>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</TagPicker>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### TagPicker Props
|
### TagPicker Props
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|
@ -68,3 +138,11 @@ view! {
|
||||||
| value | `String` | | Defines a unique identifier for the option. |
|
| value | `String` | | Defines a unique identifier for the option. |
|
||||||
| text | `String` | | An optional override the string value of the Option's display text, defaulting to the Option's child content. |
|
| text | `String` | | An optional override the string value of the Option's display text, defaulting to the Option's child content. |
|
||||||
| children | `Option<Children>` | `None` | |
|
| children | `Option<Children>` | `None` | |
|
||||||
|
|
||||||
|
### TagPickerOptionGroup Props
|
||||||
|
|
||||||
|
| Name | Type | Default | Desciption |
|
||||||
|
| -------- | ------------------- | -------------------- | ------------------- |
|
||||||
|
| class | `MaybeProp<String>` | `Default::default()` | |
|
||||||
|
| label | `String` | | Label of the group. |
|
||||||
|
| children | `Children` | | |
|
||||||
|
|
|
@ -2,8 +2,10 @@ mod tag_picker;
|
||||||
mod tag_picker_group;
|
mod tag_picker_group;
|
||||||
mod tag_picker_input;
|
mod tag_picker_input;
|
||||||
mod tag_picker_option;
|
mod tag_picker_option;
|
||||||
|
mod tag_picker_option_group;
|
||||||
|
|
||||||
pub use tag_picker::*;
|
pub use tag_picker::*;
|
||||||
pub use tag_picker_group::*;
|
pub use tag_picker_group::*;
|
||||||
pub use tag_picker_input::*;
|
pub use tag_picker_input::*;
|
||||||
pub use tag_picker_option::*;
|
pub use tag_picker_option::*;
|
||||||
|
pub use tag_picker_option_group::*;
|
||||||
|
|
|
@ -82,21 +82,6 @@
|
||||||
outline-style: none;
|
outline-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thaw-tag-picker__listbox {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
row-gap: var(--spacingHorizontalXXS);
|
|
||||||
overflow-y: auto;
|
|
||||||
min-width: 160px;
|
|
||||||
max-height: 80vh;
|
|
||||||
background-color: var(--colorNeutralBackground1);
|
|
||||||
padding: var(--spacingHorizontalXS);
|
|
||||||
outline: 1px solid var(--colorTransparentStroke);
|
|
||||||
border-radius: var(--borderRadiusMedium);
|
|
||||||
box-shadow: var(--shadow16);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thaw-tag-picker-option {
|
.thaw-tag-picker-option {
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
column-gap: var(--spacingHorizontalXS);
|
column-gap: var(--spacingHorizontalXS);
|
||||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
||||||
use leptos::{context::Provider, ev, html, prelude::*};
|
use leptos::{context::Provider, ev, html, prelude::*};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use thaw_components::{Binder, Follower, FollowerPlacement, FollowerWidth};
|
use thaw_components::{Binder, Follower, FollowerPlacement, FollowerWidth};
|
||||||
use thaw_utils::{call_on_click_outside, class_list, mount_style, BoxCallback, Model};
|
use thaw_utils::{call_on_click_outside_with_list, class_list, mount_style, BoxCallback, Model};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn TagPicker(
|
pub fn TagPicker(
|
||||||
|
@ -55,8 +55,8 @@ pub fn TagPicker(
|
||||||
}
|
}
|
||||||
is_show_listbox.update(|show| *show = !*show);
|
is_show_listbox.update(|show| *show = !*show);
|
||||||
};
|
};
|
||||||
call_on_click_outside(
|
call_on_click_outside_with_list(
|
||||||
trigger_ref,
|
vec![trigger_ref, listbox_ref],
|
||||||
{
|
{
|
||||||
move || {
|
move || {
|
||||||
is_show_listbox.set(false);
|
is_show_listbox.set(false);
|
||||||
|
|
13
thaw/src/tag_picker/tag_picker_option_group.rs
Normal file
13
thaw/src/tag_picker/tag_picker_option_group.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
use crate::option_group::OptionGroup;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn TagPickerOptionGroup(
|
||||||
|
#[prop(optional, into)] class: MaybeProp<String>,
|
||||||
|
/// Label of the group.
|
||||||
|
#[prop(into)]
|
||||||
|
label: String,
|
||||||
|
children: Children,
|
||||||
|
) -> impl IntoView {
|
||||||
|
view! { <OptionGroup class_prefix="thaw-tag-picker-option-group" class label children /> }
|
||||||
|
}
|
|
@ -21,3 +21,28 @@ pub fn call_on_click_outside(element: NodeRef<Div>, on_click: BoxCallback) {
|
||||||
let _ = on_click;
|
let _ = on_click;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn call_on_click_outside_with_list(refs: Vec<NodeRef<Div>>, on_click: BoxCallback) {
|
||||||
|
#[cfg(any(feature = "csr", feature = "hydrate"))]
|
||||||
|
{
|
||||||
|
let handle = window_event_listener(::leptos::ev::click, move |ev| {
|
||||||
|
let composed_path = ev.composed_path();
|
||||||
|
if refs.iter().any(|r| {
|
||||||
|
if let Some(el) = r.get_untracked() {
|
||||||
|
composed_path.includes(&el, 0)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
on_click();
|
||||||
|
});
|
||||||
|
on_cleanup(move || handle.remove());
|
||||||
|
}
|
||||||
|
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
|
||||||
|
{
|
||||||
|
let _ = refs;
|
||||||
|
let _ = on_click;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue