mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-22 22:09:22 -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_utils = { version = "0.0.3", path = "./thaw_utils" }
|
||||
|
||||
leptos = { git = "https://github.com/leptos-rs/leptos", rev = "db02d3f5" }
|
||||
leptos_meta = { 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 = "f8c7a237" }
|
|
@ -9,7 +9,7 @@ edition = "2021"
|
|||
[dependencies]
|
||||
leptos = { 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" }
|
||||
demo_markdown = { path = "../demo_markdown" }
|
||||
icondata = "0.3.0"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
mod use_active_descendant;
|
||||
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>(
|
||||
match_option: MF,
|
||||
) -> (Arc<dyn Fn(Node)>, ActiveDescendantController)
|
||||
) -> (Arc<dyn Fn(Node) + Send + Sync>, ActiveDescendantController)
|
||||
where
|
||||
MF: Fn(HtmlElement) -> bool + 'static,
|
||||
MF: Fn(HtmlElement) -> bool + Send + Sync + 'static,
|
||||
{
|
||||
let (set_listbox, option_walker) = use_option_walker(match_option);
|
||||
//TODO
|
||||
let set_listbox = Arc::new(move |node| {
|
||||
set_listbox(&node);
|
||||
});
|
||||
|
|
|
@ -4,9 +4,9 @@ use std::sync::Arc;
|
|||
use wasm_bindgen::{closure::Closure, JsCast, UnwrapThrowExt};
|
||||
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
|
||||
MF: Fn(HtmlElement) -> bool + 'static,
|
||||
MF: Fn(HtmlElement) -> bool + Send + Sync + 'static,
|
||||
{
|
||||
let tree_walker = StoredValue::new(
|
||||
None::<(
|
||||
|
|
|
@ -103,30 +103,6 @@
|
|||
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 {
|
||||
column-gap: var(--spacingHorizontalXS);
|
||||
position: relative;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use super::utils::{get_dropdown_action_from_key, DropdownAction};
|
||||
use crate::{ConfigInjection, _aria::use_active_descendant};
|
||||
use super::listbox::{listbox_keyboard_event, Listbox};
|
||||
use crate::_aria::use_active_descendant;
|
||||
use leptos::{context::Provider, ev, html, prelude::*};
|
||||
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};
|
||||
|
||||
#[component]
|
||||
|
@ -14,7 +14,6 @@ pub fn Combobox(
|
|||
children: Children,
|
||||
) -> impl IntoView {
|
||||
mount_style("combobox", include_str!("./combobox.css"));
|
||||
let config_provider = ConfigInjection::use_();
|
||||
let trigger_ref = NodeRef::<html::Div>::new();
|
||||
let input_ref = NodeRef::<html::Input>::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 open = is_show_listbox.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 {
|
||||
is_show_listbox.set(true);
|
||||
}
|
||||
}
|
||||
DropdownAction::CloseSelect | DropdownAction::Close => {
|
||||
if let Some(option) = active_option {
|
||||
combobox_injection.options.with_value(|options| {
|
||||
if let Some((value, text)) = options.get(&option.id()) {
|
||||
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 => {}
|
||||
};
|
||||
listbox_keyboard_event(
|
||||
e,
|
||||
is_show_listbox,
|
||||
multiselect,
|
||||
&active_descendant_controller,
|
||||
move |option| {
|
||||
combobox_injection.options.with_value(|options| {
|
||||
if let Some((value, text)) = options.get(&option.id()) {
|
||||
combobox_injection.select_option(value, text);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
view! {
|
||||
|
@ -228,23 +196,9 @@ pub fn Combobox(
|
|||
width=FollowerWidth::MinTarget
|
||||
>
|
||||
<Provider value=combobox_injection>
|
||||
<CSSTransition
|
||||
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()}
|
||||
</div>
|
||||
</CSSTransition>
|
||||
<Listbox open=is_show_listbox.read_only() set_listbox listbox_ref class="thaw-combobox__listbox">
|
||||
{children()}
|
||||
</Listbox>
|
||||
</Provider>
|
||||
</Follower>
|
||||
</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_option;
|
||||
mod listbox;
|
||||
mod utils;
|
||||
|
||||
pub use combobox::*;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use leptos::ev;
|
||||
|
||||
pub fn get_dropdown_action_from_key(
|
||||
e: ev::KeyboardEvent,
|
||||
e: &ev::KeyboardEvent,
|
||||
open: bool,
|
||||
multiselect: bool,
|
||||
) -> DropdownAction {
|
||||
|
@ -29,24 +29,43 @@ pub fn get_dropdown_action_from_key(
|
|||
{
|
||||
DropdownAction::CloseSelect
|
||||
} else if multiselect && KeyboardKey::Space == code {
|
||||
DropdownAction::Select
|
||||
} else if KeyboardKey::Escape == code {
|
||||
DropdownAction::Close
|
||||
} else if KeyboardKey::ArrowDown == code {
|
||||
DropdownAction::Next
|
||||
} else if KeyboardKey::ArrowUp == code {
|
||||
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 {
|
||||
DropdownAction::None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum DropdownAction {
|
||||
None,
|
||||
Type,
|
||||
Open,
|
||||
CloseSelect,
|
||||
Select,
|
||||
Close,
|
||||
Next,
|
||||
Previous,
|
||||
First,
|
||||
Last,
|
||||
PageUp,
|
||||
PageDown,
|
||||
Tab,
|
||||
}
|
||||
|
||||
enum KeyboardKey {
|
||||
|
@ -54,6 +73,12 @@ enum KeyboardKey {
|
|||
ArrowUp,
|
||||
Enter,
|
||||
Space,
|
||||
Escape,
|
||||
Home,
|
||||
End,
|
||||
PageUp,
|
||||
PageDown,
|
||||
Tab,
|
||||
}
|
||||
|
||||
impl PartialEq<String> for KeyboardKey {
|
||||
|
@ -63,6 +88,12 @@ impl PartialEq<String> for KeyboardKey {
|
|||
Self::ArrowUp => other == "ArrowUp",
|
||||
Self::Enter => other == "Enter",
|
||||
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