mirror of
https://github.com/adoyle0/thaw.git
synced 2025-02-08 19:03:09 -05:00
feat: extract the Listbox component
This commit is contained in:
parent
900b949fa2
commit
0a8e98576a
11 changed files with 188 additions and 98 deletions
|
@ -16,5 +16,5 @@ thaw_components = { version = "0.1.1", path = "./thaw_components" }
|
||||||
thaw_macro = { version = "0.1.0", path = "./thaw_macro" }
|
thaw_macro = { version = "0.1.0", path = "./thaw_macro" }
|
||||||
thaw_utils = { version = "0.0.3", path = "./thaw_utils" }
|
thaw_utils = { version = "0.0.3", path = "./thaw_utils" }
|
||||||
|
|
||||||
leptos = { git = "https://github.com/leptos-rs/leptos", rev = "db02d3f5" }
|
leptos = { git = "https://github.com/leptos-rs/leptos", rev = "f8c7a237" }
|
||||||
leptos_meta = { git = "https://github.com/leptos-rs/leptos", rev = "db02d3f5" }
|
leptos_meta = { git = "https://github.com/leptos-rs/leptos", rev = "f8c7a237" }
|
|
@ -9,7 +9,7 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
leptos = { workspace = true }
|
leptos = { workspace = true }
|
||||||
leptos_meta = { workspace = true }
|
leptos_meta = { workspace = true }
|
||||||
leptos_router = { git = "https://github.com/leptos-rs/leptos", rev = "db02d3f5" }
|
leptos_router = { git = "https://github.com/leptos-rs/leptos", rev = "f8c7a237" }
|
||||||
thaw = { path = "../thaw" }
|
thaw = { path = "../thaw" }
|
||||||
demo_markdown = { path = "../demo_markdown" }
|
demo_markdown = { path = "../demo_markdown" }
|
||||||
icondata = "0.3.0"
|
icondata = "0.3.0"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
mod use_active_descendant;
|
mod use_active_descendant;
|
||||||
mod use_option_walker;
|
mod use_option_walker;
|
||||||
|
|
||||||
pub use use_active_descendant::use_active_descendant;
|
pub use use_active_descendant::{use_active_descendant, ActiveDescendantController};
|
||||||
|
|
|
@ -11,11 +11,12 @@ const ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE: &str = "data-activedescendant-foc
|
||||||
|
|
||||||
pub fn use_active_descendant<MF>(
|
pub fn use_active_descendant<MF>(
|
||||||
match_option: MF,
|
match_option: MF,
|
||||||
) -> (Arc<dyn Fn(Node)>, ActiveDescendantController)
|
) -> (Arc<dyn Fn(Node) + Send + Sync>, ActiveDescendantController)
|
||||||
where
|
where
|
||||||
MF: Fn(HtmlElement) -> bool + 'static,
|
MF: Fn(HtmlElement) -> bool + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
let (set_listbox, option_walker) = use_option_walker(match_option);
|
let (set_listbox, option_walker) = use_option_walker(match_option);
|
||||||
|
//TODO
|
||||||
let set_listbox = Arc::new(move |node| {
|
let set_listbox = Arc::new(move |node| {
|
||||||
set_listbox(&node);
|
set_listbox(&node);
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,9 +4,9 @@ use std::sync::Arc;
|
||||||
use wasm_bindgen::{closure::Closure, JsCast, UnwrapThrowExt};
|
use wasm_bindgen::{closure::Closure, JsCast, UnwrapThrowExt};
|
||||||
use web_sys::{HtmlElement, Node, NodeFilter, TreeWalker};
|
use web_sys::{HtmlElement, Node, NodeFilter, TreeWalker};
|
||||||
|
|
||||||
pub fn use_option_walker<MF>(match_option: MF) -> (Box<dyn Fn(&Node)>, OptionWalker)
|
pub fn use_option_walker<MF>(match_option: MF) -> (Box<dyn Fn(&Node) + Send + Sync>, OptionWalker)
|
||||||
where
|
where
|
||||||
MF: Fn(HtmlElement) -> bool + 'static,
|
MF: Fn(HtmlElement) -> bool + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
let tree_walker = StoredValue::new(
|
let tree_walker = StoredValue::new(
|
||||||
None::<(
|
None::<(
|
||||||
|
|
|
@ -103,30 +103,6 @@
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thaw-combobox__listbox.fade-in-scale-up-transition-leave-active {
|
|
||||||
transform-origin: inherit;
|
|
||||||
transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1),
|
|
||||||
transform 0.2s cubic-bezier(0.4, 0, 1, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.thaw-combobox__listbox.fade-in-scale-up-transition-enter-active {
|
|
||||||
transform-origin: inherit;
|
|
||||||
transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1),
|
|
||||||
transform 0.2s cubic-bezier(0, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.thaw-combobox__listbox.fade-in-scale-up-transition-enter-from,
|
|
||||||
.thaw-combobox__listbox.fade-in-scale-up-transition-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.thaw-combobox__listbox.fade-in-scale-up-transition-leave-from,
|
|
||||||
.thaw-combobox__listbox.fade-in-scale-up-transition-enter-to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.thaw-combobox-option {
|
.thaw-combobox-option {
|
||||||
column-gap: var(--spacingHorizontalXS);
|
column-gap: var(--spacingHorizontalXS);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use super::utils::{get_dropdown_action_from_key, DropdownAction};
|
use super::listbox::{listbox_keyboard_event, Listbox};
|
||||||
use crate::{ConfigInjection, _aria::use_active_descendant};
|
use crate::_aria::use_active_descendant;
|
||||||
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, CSSTransition, Follower, FollowerPlacement, FollowerWidth};
|
use thaw_components::{Binder, Follower, FollowerPlacement, FollowerWidth};
|
||||||
use thaw_utils::{add_event_listener, mount_style, Model};
|
use thaw_utils::{add_event_listener, mount_style, Model};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
|
@ -14,7 +14,6 @@ pub fn Combobox(
|
||||||
children: Children,
|
children: Children,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
mount_style("combobox", include_str!("./combobox.css"));
|
mount_style("combobox", include_str!("./combobox.css"));
|
||||||
let config_provider = ConfigInjection::use_();
|
|
||||||
let trigger_ref = NodeRef::<html::Div>::new();
|
let trigger_ref = NodeRef::<html::Div>::new();
|
||||||
let input_ref = NodeRef::<html::Input>::new();
|
let input_ref = NodeRef::<html::Input>::new();
|
||||||
let listbox_ref = NodeRef::<html::Div>::new();
|
let listbox_ref = NodeRef::<html::Div>::new();
|
||||||
|
@ -115,51 +114,20 @@ pub fn Combobox(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let effect = RenderEffect::new(move |_| {
|
|
||||||
if let Some(listbox_el) = listbox_ref.get() {
|
|
||||||
set_listbox(listbox_el.into());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
on_cleanup(move || {
|
|
||||||
drop(effect);
|
|
||||||
});
|
|
||||||
|
|
||||||
let on_keydown = move |e| {
|
let on_keydown = move |e| {
|
||||||
let open = is_show_listbox.get_untracked();
|
listbox_keyboard_event(
|
||||||
let action = get_dropdown_action_from_key(e, open, multiselect);
|
e,
|
||||||
let active_option = active_descendant_controller.active();
|
is_show_listbox,
|
||||||
|
multiselect,
|
||||||
match action {
|
&active_descendant_controller,
|
||||||
DropdownAction::Type | DropdownAction::Open => {
|
move |option| {
|
||||||
if !open {
|
|
||||||
is_show_listbox.set(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DropdownAction::CloseSelect | DropdownAction::Close => {
|
|
||||||
if let Some(option) = active_option {
|
|
||||||
combobox_injection.options.with_value(|options| {
|
combobox_injection.options.with_value(|options| {
|
||||||
if let Some((value, text)) = options.get(&option.id()) {
|
if let Some((value, text)) = options.get(&option.id()) {
|
||||||
combobox_injection.select_option(value, text);
|
combobox_injection.select_option(value, text);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
);
|
||||||
DropdownAction::Next => {
|
|
||||||
if active_option.is_some() {
|
|
||||||
active_descendant_controller.next();
|
|
||||||
} else {
|
|
||||||
active_descendant_controller.first();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DropdownAction::Previous => {
|
|
||||||
if active_option.is_some() {
|
|
||||||
active_descendant_controller.prev();
|
|
||||||
} else {
|
|
||||||
active_descendant_controller.first();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DropdownAction::None => {}
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
|
@ -228,23 +196,9 @@ pub fn Combobox(
|
||||||
width=FollowerWidth::MinTarget
|
width=FollowerWidth::MinTarget
|
||||||
>
|
>
|
||||||
<Provider value=combobox_injection>
|
<Provider value=combobox_injection>
|
||||||
<CSSTransition
|
<Listbox open=is_show_listbox.read_only() set_listbox listbox_ref class="thaw-combobox__listbox">
|
||||||
node_ref=listbox_ref
|
|
||||||
name="fade-in-scale-up-transition"
|
|
||||||
appear=is_show_listbox.get_untracked()
|
|
||||||
show=is_show_listbox
|
|
||||||
let:display
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="thaw-config-provider thaw-combobox__listbox"
|
|
||||||
style=move || display.get().unwrap_or_default()
|
|
||||||
data-thaw-id=config_provider.id().clone()
|
|
||||||
node_ref=listbox_ref
|
|
||||||
role="listbox"
|
|
||||||
>
|
|
||||||
{children()}
|
{children()}
|
||||||
</div>
|
</Listbox>
|
||||||
</CSSTransition>
|
|
||||||
</Provider>
|
</Provider>
|
||||||
</Follower>
|
</Follower>
|
||||||
</Binder>
|
</Binder>
|
||||||
|
|
23
thaw/src/combobox/listbox.css
Normal file
23
thaw/src/combobox/listbox.css
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
.thaw-listbox.fade-in-scale-up-transition-leave-active {
|
||||||
|
transform-origin: inherit;
|
||||||
|
transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1),
|
||||||
|
transform 0.2s cubic-bezier(0.4, 0, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-listbox.fade-in-scale-up-transition-enter-active {
|
||||||
|
transform-origin: inherit;
|
||||||
|
transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1),
|
||||||
|
transform 0.2s cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-listbox.fade-in-scale-up-transition-enter-from,
|
||||||
|
.thaw-listbox.fade-in-scale-up-transition-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thaw-listbox.fade-in-scale-up-transition-leave-from,
|
||||||
|
.thaw-listbox.fade-in-scale-up-transition-enter-to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
104
thaw/src/combobox/listbox.rs
Normal file
104
thaw/src/combobox/listbox.rs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use super::utils::{get_dropdown_action_from_key, DropdownAction};
|
||||||
|
use crate::{ConfigInjection, _aria::ActiveDescendantController};
|
||||||
|
use leptos::{ev, html, prelude::*};
|
||||||
|
use thaw_components::CSSTransition;
|
||||||
|
use thaw_utils::mount_style;
|
||||||
|
use web_sys::{HtmlElement, Node};
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Listbox(
|
||||||
|
open: ReadSignal<bool>,
|
||||||
|
class: &'static str,
|
||||||
|
set_listbox: Arc<dyn Fn(Node) + Send + Sync>,
|
||||||
|
listbox_ref: NodeRef<html::Div>,
|
||||||
|
children: Children,
|
||||||
|
) -> impl IntoView {
|
||||||
|
mount_style("listbox", include_str!("./listbox.css"));
|
||||||
|
|
||||||
|
let config_provider = ConfigInjection::expect_context();
|
||||||
|
let effect = RenderEffect::new(move |_| {
|
||||||
|
if let Some(listbox_el) = listbox_ref.get() {
|
||||||
|
set_listbox(listbox_el.into());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
on_cleanup(move || {
|
||||||
|
drop(effect);
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<CSSTransition
|
||||||
|
node_ref=listbox_ref
|
||||||
|
name="fade-in-scale-up-transition"
|
||||||
|
appear=open.get_untracked()
|
||||||
|
show=open
|
||||||
|
let:display
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class=format!("thaw-config-provider thaw-listbox {class}")
|
||||||
|
style=move || display.get().unwrap_or_default()
|
||||||
|
data-thaw-id=config_provider.id().clone()
|
||||||
|
node_ref=listbox_ref
|
||||||
|
role="listbox"
|
||||||
|
>
|
||||||
|
{children()}
|
||||||
|
</div>
|
||||||
|
</CSSTransition>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn listbox_keyboard_event(
|
||||||
|
e: ev::KeyboardEvent,
|
||||||
|
open: RwSignal<bool>,
|
||||||
|
multiselect: bool,
|
||||||
|
active_descendant_controller: &ActiveDescendantController,
|
||||||
|
select_option: impl Fn(HtmlElement),
|
||||||
|
) {
|
||||||
|
let (open, set_open) = open.split();
|
||||||
|
let open = open.get_untracked();
|
||||||
|
let action = get_dropdown_action_from_key(&e, open, multiselect);
|
||||||
|
let active_option = active_descendant_controller.active();
|
||||||
|
|
||||||
|
match action {
|
||||||
|
DropdownAction::Type | DropdownAction::Open => {
|
||||||
|
if !open {
|
||||||
|
set_open.set(true);
|
||||||
|
}
|
||||||
|
if action == DropdownAction::Open {
|
||||||
|
e.prevent_default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DropdownAction::CloseSelect | DropdownAction::Select => {
|
||||||
|
e.prevent_default();
|
||||||
|
if let Some(option) = active_option {
|
||||||
|
select_option(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DropdownAction::Next => {
|
||||||
|
e.prevent_default();
|
||||||
|
if active_option.is_some() {
|
||||||
|
active_descendant_controller.next();
|
||||||
|
} else {
|
||||||
|
active_descendant_controller.first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DropdownAction::Previous => {
|
||||||
|
e.prevent_default();
|
||||||
|
if active_option.is_some() {
|
||||||
|
active_descendant_controller.prev();
|
||||||
|
} else {
|
||||||
|
active_descendant_controller.first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DropdownAction::First | DropdownAction::PageUp => {
|
||||||
|
e.prevent_default();
|
||||||
|
active_descendant_controller.first();
|
||||||
|
}
|
||||||
|
DropdownAction::Last | DropdownAction::PageDown => {
|
||||||
|
e.prevent_default();
|
||||||
|
active_descendant_controller.last();
|
||||||
|
}
|
||||||
|
DropdownAction::Tab | DropdownAction::Close | DropdownAction::None => {}
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
mod combobox;
|
mod combobox;
|
||||||
mod combobox_option;
|
mod combobox_option;
|
||||||
|
mod listbox;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub use combobox::*;
|
pub use combobox::*;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use leptos::ev;
|
use leptos::ev;
|
||||||
|
|
||||||
pub fn get_dropdown_action_from_key(
|
pub fn get_dropdown_action_from_key(
|
||||||
e: ev::KeyboardEvent,
|
e: &ev::KeyboardEvent,
|
||||||
open: bool,
|
open: bool,
|
||||||
multiselect: bool,
|
multiselect: bool,
|
||||||
) -> DropdownAction {
|
) -> DropdownAction {
|
||||||
|
@ -29,24 +29,43 @@ pub fn get_dropdown_action_from_key(
|
||||||
{
|
{
|
||||||
DropdownAction::CloseSelect
|
DropdownAction::CloseSelect
|
||||||
} else if multiselect && KeyboardKey::Space == code {
|
} else if multiselect && KeyboardKey::Space == code {
|
||||||
|
DropdownAction::Select
|
||||||
|
} else if KeyboardKey::Escape == code {
|
||||||
DropdownAction::Close
|
DropdownAction::Close
|
||||||
} else if KeyboardKey::ArrowDown == code {
|
} else if KeyboardKey::ArrowDown == code {
|
||||||
DropdownAction::Next
|
DropdownAction::Next
|
||||||
} else if KeyboardKey::ArrowUp == code {
|
} else if KeyboardKey::ArrowUp == code {
|
||||||
DropdownAction::Previous
|
DropdownAction::Previous
|
||||||
|
} else if KeyboardKey::Home == code {
|
||||||
|
DropdownAction::First
|
||||||
|
} else if KeyboardKey::End == code {
|
||||||
|
DropdownAction::Last
|
||||||
|
} else if KeyboardKey::PageUp == code {
|
||||||
|
DropdownAction::PageUp
|
||||||
|
} else if KeyboardKey::PageDown == code {
|
||||||
|
DropdownAction::PageDown
|
||||||
|
} else if KeyboardKey::Tab == code {
|
||||||
|
DropdownAction::Tab
|
||||||
} else {
|
} else {
|
||||||
DropdownAction::None
|
DropdownAction::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
pub enum DropdownAction {
|
pub enum DropdownAction {
|
||||||
None,
|
None,
|
||||||
Type,
|
Type,
|
||||||
Open,
|
Open,
|
||||||
CloseSelect,
|
CloseSelect,
|
||||||
|
Select,
|
||||||
Close,
|
Close,
|
||||||
Next,
|
Next,
|
||||||
Previous,
|
Previous,
|
||||||
|
First,
|
||||||
|
Last,
|
||||||
|
PageUp,
|
||||||
|
PageDown,
|
||||||
|
Tab,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum KeyboardKey {
|
enum KeyboardKey {
|
||||||
|
@ -54,6 +73,12 @@ enum KeyboardKey {
|
||||||
ArrowUp,
|
ArrowUp,
|
||||||
Enter,
|
Enter,
|
||||||
Space,
|
Space,
|
||||||
|
Escape,
|
||||||
|
Home,
|
||||||
|
End,
|
||||||
|
PageUp,
|
||||||
|
PageDown,
|
||||||
|
Tab,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq<String> for KeyboardKey {
|
impl PartialEq<String> for KeyboardKey {
|
||||||
|
@ -63,6 +88,12 @@ impl PartialEq<String> for KeyboardKey {
|
||||||
Self::ArrowUp => other == "ArrowUp",
|
Self::ArrowUp => other == "ArrowUp",
|
||||||
Self::Enter => other == "Enter",
|
Self::Enter => other == "Enter",
|
||||||
Self::Space => other == "Space",
|
Self::Space => other == "Space",
|
||||||
|
Self::Escape => other == "Escape",
|
||||||
|
Self::Home => other == "Home",
|
||||||
|
Self::End => other == "End",
|
||||||
|
Self::PageUp => other == "PageUp",
|
||||||
|
Self::PageDown => other == "PageDown",
|
||||||
|
Self::Tab => other == "Tab",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue