mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-22 22:09:22 -05:00
feat: Combobox adds keyboard operations
This commit is contained in:
parent
c186c8b352
commit
900b949fa2
12 changed files with 393 additions and 14 deletions
|
@ -24,6 +24,8 @@ web-sys = { version = "0.3.69", features = [
|
||||||
"DataTransfer",
|
"DataTransfer",
|
||||||
"ScrollToOptions",
|
"ScrollToOptions",
|
||||||
"ScrollBehavior",
|
"ScrollBehavior",
|
||||||
|
"TreeWalker",
|
||||||
|
"NodeFilter",
|
||||||
] }
|
] }
|
||||||
wasm-bindgen = "0.2.92"
|
wasm-bindgen = "0.2.92"
|
||||||
icondata_core = "0.1.0"
|
icondata_core = "0.1.0"
|
||||||
|
|
4
thaw/src/_aria/active_descendant/mod.rs
Normal file
4
thaw/src/_aria/active_descendant/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
mod use_active_descendant;
|
||||||
|
mod use_option_walker;
|
||||||
|
|
||||||
|
pub use use_active_descendant::use_active_descendant;
|
97
thaw/src/_aria/active_descendant/use_active_descendant.rs
Normal file
97
thaw/src/_aria/active_descendant/use_active_descendant.rs
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
use super::use_option_walker::{use_option_walker, OptionWalker};
|
||||||
|
use send_wrapper::SendWrapper;
|
||||||
|
use std::{cell::RefCell, sync::Arc};
|
||||||
|
use web_sys::{HtmlElement, Node};
|
||||||
|
|
||||||
|
/// Applied to the element that is active descendant
|
||||||
|
const ACTIVEDESCENDANT_ATTRIBUTE: &str = "data-activedescendant";
|
||||||
|
|
||||||
|
/// Applied to the active descendant when the user is navigating with keyboard
|
||||||
|
const ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE: &str = "data-activedescendant-focusvisible";
|
||||||
|
|
||||||
|
pub fn use_active_descendant<MF>(
|
||||||
|
match_option: MF,
|
||||||
|
) -> (Arc<dyn Fn(Node)>, ActiveDescendantController)
|
||||||
|
where
|
||||||
|
MF: Fn(HtmlElement) -> bool + 'static,
|
||||||
|
{
|
||||||
|
let (set_listbox, option_walker) = use_option_walker(match_option);
|
||||||
|
let set_listbox = Arc::new(move |node| {
|
||||||
|
set_listbox(&node);
|
||||||
|
});
|
||||||
|
let controller = ActiveDescendantController {
|
||||||
|
option_walker,
|
||||||
|
active: Arc::new(SendWrapper::new(Default::default())),
|
||||||
|
// last_active: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
(set_listbox, controller)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ActiveDescendantController {
|
||||||
|
option_walker: OptionWalker,
|
||||||
|
active: Arc<SendWrapper<RefCell<Option<HtmlElement>>>>,
|
||||||
|
// last_active: RefCell<Option<HtmlElement>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveDescendantController {
|
||||||
|
fn blur_active_descendant(&self) {
|
||||||
|
let mut active = self.active.borrow_mut();
|
||||||
|
let Some(active_el) = active.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let _ = active_el.remove_attribute(ACTIVEDESCENDANT_ATTRIBUTE);
|
||||||
|
let _ = active_el.remove_attribute(ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE);
|
||||||
|
|
||||||
|
*active = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_active_descendant(&self, next_active: HtmlElement) {
|
||||||
|
self.blur_active_descendant();
|
||||||
|
|
||||||
|
let _ = next_active.set_attribute(ACTIVEDESCENDANT_ATTRIBUTE, "");
|
||||||
|
let _ = next_active.set_attribute(ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE, "");
|
||||||
|
|
||||||
|
*self.active.borrow_mut() = Some(next_active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveDescendantController {
|
||||||
|
pub fn first(&self) {
|
||||||
|
if let Some(first) = self.option_walker.first() {
|
||||||
|
self.focus_active_descendant(first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn last(&self) {
|
||||||
|
if let Some(last) = self.option_walker.last() {
|
||||||
|
self.focus_active_descendant(last);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&self) {
|
||||||
|
if let Some(next) = self.option_walker.next() {
|
||||||
|
self.focus_active_descendant(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev(&self) {
|
||||||
|
if let Some(prev) = self.option_walker.prev() {
|
||||||
|
self.focus_active_descendant(prev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blur(&self) {
|
||||||
|
self.blur_active_descendant();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn active(&self) -> Option<HtmlElement> {
|
||||||
|
let active = self.active.borrow();
|
||||||
|
if let Some(active) = active.as_ref() {
|
||||||
|
Some(active.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
90
thaw/src/_aria/active_descendant/use_option_walker.rs
Normal file
90
thaw/src/_aria/active_descendant/use_option_walker.rs
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
use leptos::{prelude::document, reactive_graph::owner::StoredValue};
|
||||||
|
use send_wrapper::SendWrapper;
|
||||||
|
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)
|
||||||
|
where
|
||||||
|
MF: Fn(HtmlElement) -> bool + 'static,
|
||||||
|
{
|
||||||
|
let tree_walker = StoredValue::new(
|
||||||
|
None::<(
|
||||||
|
SendWrapper<TreeWalker>,
|
||||||
|
SendWrapper<Closure<dyn Fn(Node) -> u32>>,
|
||||||
|
)>,
|
||||||
|
);
|
||||||
|
let option_walker = OptionWalker(tree_walker);
|
||||||
|
let match_option = Arc::new(match_option);
|
||||||
|
let set_listbox = move |el: &Node| {
|
||||||
|
let match_option = match_option.clone();
|
||||||
|
let cb: Closure<dyn Fn(Node) -> u32> = Closure::new(move |node: Node| {
|
||||||
|
if let Ok(html_element) = node.dyn_into() {
|
||||||
|
if match_option(html_element) {
|
||||||
|
return 1u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
3u32
|
||||||
|
});
|
||||||
|
let mut node_filter = NodeFilter::new();
|
||||||
|
node_filter.accept_node(cb.as_ref().unchecked_ref());
|
||||||
|
|
||||||
|
let tw = document()
|
||||||
|
.create_tree_walker_with_what_to_show_and_filter(el, 0x1, Some(&node_filter))
|
||||||
|
.unwrap_throw();
|
||||||
|
tree_walker.set_value(Some((SendWrapper::new(tw), SendWrapper::new(cb))));
|
||||||
|
};
|
||||||
|
|
||||||
|
(Box::new(set_listbox), option_walker)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct OptionWalker(
|
||||||
|
StoredValue<
|
||||||
|
Option<(
|
||||||
|
SendWrapper<TreeWalker>,
|
||||||
|
SendWrapper<Closure<dyn Fn(Node) -> u32>>,
|
||||||
|
)>,
|
||||||
|
>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl OptionWalker {
|
||||||
|
pub fn first(&self) -> Option<HtmlElement> {
|
||||||
|
self.0.with_value(|tree_walker| {
|
||||||
|
let Some((tree_walker, _)) = tree_walker.as_ref() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
tree_walker.set_current_node(&tree_walker.root());
|
||||||
|
tree_walker.first_child().unwrap_throw()?.dyn_into().ok()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn last(&self) -> Option<HtmlElement> {
|
||||||
|
self.0.with_value(|tree_walker| {
|
||||||
|
let Some((tree_walker, _)) = tree_walker.as_ref() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
tree_walker.set_current_node(&tree_walker.root());
|
||||||
|
tree_walker.last_child().unwrap_throw()?.dyn_into().ok()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&self) -> Option<HtmlElement> {
|
||||||
|
self.0.with_value(|tree_walker| {
|
||||||
|
let Some((tree_walker, _)) = tree_walker.as_ref() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
tree_walker.next_node().unwrap_throw()?.dyn_into().ok()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev(&self) -> Option<HtmlElement> {
|
||||||
|
self.0.with_value(|tree_walker| {
|
||||||
|
let Some((tree_walker, _)) = tree_walker.as_ref() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
tree_walker.previous_node().unwrap_throw()?.dyn_into().ok()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
3
thaw/src/_aria/mod.rs
Normal file
3
thaw/src/_aria/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod active_descendant;
|
||||||
|
|
||||||
|
pub use active_descendant::*;
|
|
@ -141,6 +141,19 @@
|
||||||
border-radius: var(--borderRadiusMedium);
|
border-radius: var(--borderRadiusMedium);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thaw-combobox-option[data-activedescendant-focusvisible]::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: -2px;
|
||||||
|
left: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
top: -2px;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: var(--borderRadiusMedium);
|
||||||
|
border: 2px solid var(--colorStrokeFocus2);
|
||||||
|
}
|
||||||
|
|
||||||
.thaw-combobox-option:hover {
|
.thaw-combobox-option:hover {
|
||||||
color: var(--colorNeutralForeground1Hover);
|
color: var(--colorNeutralForeground1Hover);
|
||||||
background-color: var(--colorNeutralBackground1Hover);
|
background-color: var(--colorNeutralBackground1Hover);
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use crate::ConfigInjection;
|
use super::utils::{get_dropdown_action_from_key, DropdownAction};
|
||||||
|
use crate::{ConfigInjection, _aria::use_active_descendant};
|
||||||
use leptos::{context::Provider, ev, html, prelude::*};
|
use leptos::{context::Provider, ev, html, prelude::*};
|
||||||
|
use std::collections::HashMap;
|
||||||
use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement, FollowerWidth};
|
use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement, FollowerWidth};
|
||||||
use thaw_utils::{add_event_listener, mount_style, Model};
|
use thaw_utils::{add_event_listener, mount_style, Model};
|
||||||
|
|
||||||
|
@ -14,8 +16,10 @@ pub fn Combobox(
|
||||||
mount_style("combobox", include_str!("./combobox.css"));
|
mount_style("combobox", include_str!("./combobox.css"));
|
||||||
let config_provider = ConfigInjection::use_();
|
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 listbox_ref = NodeRef::<html::Div>::new();
|
let listbox_ref = NodeRef::<html::Div>::new();
|
||||||
let is_show_listbox = RwSignal::new(false);
|
let is_show_listbox = RwSignal::new(false);
|
||||||
|
let options = StoredValue::new(HashMap::<String, (String, String)>::new());
|
||||||
|
|
||||||
let clear_icon_ref = NodeRef::<html::Span>::new();
|
let clear_icon_ref = NodeRef::<html::Span>::new();
|
||||||
let is_show_clear_icon = Memo::new(move |_| {
|
let is_show_clear_icon = Memo::new(move |_| {
|
||||||
|
@ -86,24 +90,83 @@ pub fn Combobox(
|
||||||
}
|
}
|
||||||
value.set(input_value);
|
value.set(input_value);
|
||||||
};
|
};
|
||||||
let on_blur = move |_| {
|
|
||||||
if multiselect {
|
let combobox_injection = ComboboxInjection {
|
||||||
value.set(String::new());
|
value,
|
||||||
} else {
|
multiselect,
|
||||||
if selected_options.with_untracked(|options| options.is_empty()) {
|
selected_options,
|
||||||
|
options,
|
||||||
|
is_show_listbox,
|
||||||
|
};
|
||||||
|
let (set_listbox, active_descendant_controller) =
|
||||||
|
use_active_descendant(move |el| el.class_list().contains("thaw-combobox-option"));
|
||||||
|
|
||||||
|
let on_blur = {
|
||||||
|
let active_descendant_controller = active_descendant_controller.clone();
|
||||||
|
move |_| {
|
||||||
|
if multiselect {
|
||||||
value.set(String::new());
|
value.set(String::new());
|
||||||
|
} else {
|
||||||
|
if selected_options.with_untracked(|options| options.is_empty()) {
|
||||||
|
value.set(String::new());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
active_descendant_controller.blur();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 => {}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Binder target_ref=trigger_ref>
|
<Binder target_ref=trigger_ref>
|
||||||
<div
|
<div
|
||||||
class="thaw-combobox"
|
class="thaw-combobox"
|
||||||
node_ref=trigger_ref
|
node_ref=trigger_ref
|
||||||
on:click=move |_| {
|
|
||||||
is_show_listbox.update(|show| *show = !*show);
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -113,8 +176,13 @@ pub fn Combobox(
|
||||||
prop:value=move || {
|
prop:value=move || {
|
||||||
value.get()
|
value.get()
|
||||||
}
|
}
|
||||||
|
node_ref=input_ref
|
||||||
on:input=on_input
|
on:input=on_input
|
||||||
on:blur=on_blur
|
on:blur=on_blur
|
||||||
|
on:keydown=on_keydown
|
||||||
|
on:click=move |_| {
|
||||||
|
is_show_listbox.update(|show| *show = !*show);
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
if clearable {
|
if clearable {
|
||||||
|
@ -140,6 +208,12 @@ pub fn Combobox(
|
||||||
aria-label="Open"
|
aria-label="Open"
|
||||||
class="thaw-combobox__expand-icon"
|
class="thaw-combobox__expand-icon"
|
||||||
style=move || is_show_clear_icon.get().then(|| "display: none").unwrap_or_default()
|
style=move || is_show_clear_icon.get().then(|| "display: none").unwrap_or_default()
|
||||||
|
on:click=move |_| {
|
||||||
|
is_show_listbox.update(|show| *show = !*show);
|
||||||
|
if let Some(el) = input_ref.get_untracked() {
|
||||||
|
let _ = el.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<svg fill="currentColor" aria-hidden="true" width="1em" height="1em" viewBox="0 0 20 20">
|
<svg fill="currentColor" aria-hidden="true" width="1em" height="1em" viewBox="0 0 20 20">
|
||||||
<path d="M15.85 7.65c.2.2.2.5 0 .7l-5.46 5.49a.55.55 0 0 1-.78 0L4.15 8.35a.5.5 0 1 1 .7-.7L10 12.8l5.15-5.16c.2-.2.5-.2.7 0Z" fill="currentColor">
|
<path d="M15.85 7.65c.2.2.2.5 0 .7l-5.46 5.49a.55.55 0 0 1-.78 0L4.15 8.35a.5.5 0 1 1 .7-.7L10 12.8l5.15-5.16c.2-.2.5-.2.7 0Z" fill="currentColor">
|
||||||
|
@ -153,7 +227,7 @@ pub fn Combobox(
|
||||||
placement=FollowerPlacement::BottomStart
|
placement=FollowerPlacement::BottomStart
|
||||||
width=FollowerWidth::MinTarget
|
width=FollowerWidth::MinTarget
|
||||||
>
|
>
|
||||||
<Provider value=ComboboxInjection{value, multiselect, selected_options, is_show_listbox}>
|
<Provider value=combobox_injection>
|
||||||
<CSSTransition
|
<CSSTransition
|
||||||
node_ref=listbox_ref
|
node_ref=listbox_ref
|
||||||
name="fade-in-scale-up-transition"
|
name="fade-in-scale-up-transition"
|
||||||
|
@ -181,21 +255,31 @@ pub fn Combobox(
|
||||||
pub(crate) struct ComboboxInjection {
|
pub(crate) struct ComboboxInjection {
|
||||||
value: Model<String>,
|
value: Model<String>,
|
||||||
selected_options: Model<Vec<String>>,
|
selected_options: Model<Vec<String>>,
|
||||||
|
options: StoredValue<HashMap<String, (String, String)>>,
|
||||||
is_show_listbox: RwSignal<bool>,
|
is_show_listbox: RwSignal<bool>,
|
||||||
pub multiselect: bool,
|
pub multiselect: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ComboboxInjection {
|
impl ComboboxInjection {
|
||||||
pub fn use_() -> Self {
|
pub fn expect_context() -> Self {
|
||||||
expect_context()
|
expect_context()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_option(&self, id: String, value: (String, String)) {
|
||||||
|
self.options
|
||||||
|
.update_value(|options| options.insert(id, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_option(&self, id: &String) {
|
||||||
|
self.options.update_value(|options| options.remove(id));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_selected(&self, value: &String) -> bool {
|
pub fn is_selected(&self, value: &String) -> bool {
|
||||||
self.selected_options
|
self.selected_options
|
||||||
.with(|options| options.contains(value))
|
.with(|options| options.contains(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_option_select(&self, value: &String, text: &String) {
|
pub fn select_option(&self, value: &String, text: &String) {
|
||||||
self.selected_options.update(|options| {
|
self.selected_options.update(|options| {
|
||||||
if self.multiselect {
|
if self.multiselect {
|
||||||
if let Some(index) = options.iter().position(|v| v == value) {
|
if let Some(index) = options.iter().position(|v| v == value) {
|
||||||
|
|
|
@ -9,21 +9,31 @@ pub fn ComboboxOption(
|
||||||
#[prop(into)] text: String,
|
#[prop(into)] text: String,
|
||||||
#[prop(optional)] children: Option<Children>,
|
#[prop(optional)] children: Option<Children>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let combobox = ComboboxInjection::use_();
|
let combobox = ComboboxInjection::expect_context();
|
||||||
let value = StoredValue::new(value.unwrap_or_else(|| text.clone()));
|
let value = StoredValue::new(value.unwrap_or_else(|| text.clone()));
|
||||||
let text = StoredValue::new(text);
|
let text = StoredValue::new(text);
|
||||||
|
let id = uuid::Uuid::new_v4().to_string();
|
||||||
let on_click = move |_| {
|
let on_click = move |_| {
|
||||||
text.with_value(|text| {
|
text.with_value(|text| {
|
||||||
value.with_value(|value| {
|
value.with_value(|value| {
|
||||||
combobox.on_option_select(value, text);
|
combobox.select_option(value, text);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
combobox.insert_option(id.clone(), (value.get_value(), text.get_value()));
|
||||||
|
let id = id.clone();
|
||||||
|
on_cleanup(move || {
|
||||||
|
combobox.remove_option(&id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div
|
<div
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected="true"
|
aria-selected="true"
|
||||||
|
id=id
|
||||||
class=class_list![
|
class=class_list![
|
||||||
"thaw-combobox-option",
|
"thaw-combobox-option",
|
||||||
("thaw-combobox-option--selected", move || value.with_value(|value| combobox.is_selected(&value)))
|
("thaw-combobox-option--selected", move || value.with_value(|value| combobox.is_selected(&value)))
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
mod combobox;
|
mod combobox;
|
||||||
mod combobox_option;
|
mod combobox_option;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
pub use combobox::*;
|
pub use combobox::*;
|
||||||
pub use combobox_option::*;
|
pub use combobox_option::*;
|
||||||
|
|
68
thaw/src/combobox/utils.rs
Normal file
68
thaw/src/combobox/utils.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use leptos::ev;
|
||||||
|
|
||||||
|
pub fn get_dropdown_action_from_key(
|
||||||
|
e: ev::KeyboardEvent,
|
||||||
|
open: bool,
|
||||||
|
multiselect: bool,
|
||||||
|
) -> DropdownAction {
|
||||||
|
let key = e.key();
|
||||||
|
let code = e.code();
|
||||||
|
let alt_key = e.alt_key();
|
||||||
|
let ctrl_key = e.ctrl_key();
|
||||||
|
let meta_key = e.meta_key();
|
||||||
|
|
||||||
|
if key.len() == 1 && KeyboardKey::Space != code && !alt_key && !ctrl_key && !meta_key {
|
||||||
|
DropdownAction::Type
|
||||||
|
} else if !open {
|
||||||
|
if KeyboardKey::ArrowDown == code
|
||||||
|
|| KeyboardKey::ArrowUp == code
|
||||||
|
|| KeyboardKey::Enter == code
|
||||||
|
|| KeyboardKey::Space == code
|
||||||
|
{
|
||||||
|
DropdownAction::Open
|
||||||
|
} else {
|
||||||
|
DropdownAction::None
|
||||||
|
}
|
||||||
|
} else if (KeyboardKey::ArrowUp == code && alt_key)
|
||||||
|
|| KeyboardKey::Enter == code
|
||||||
|
|| (!multiselect && KeyboardKey::Space == code)
|
||||||
|
{
|
||||||
|
DropdownAction::CloseSelect
|
||||||
|
} else if multiselect && KeyboardKey::Space == code {
|
||||||
|
DropdownAction::Close
|
||||||
|
} else if KeyboardKey::ArrowDown == code {
|
||||||
|
DropdownAction::Next
|
||||||
|
} else if KeyboardKey::ArrowUp == code {
|
||||||
|
DropdownAction::Previous
|
||||||
|
} else {
|
||||||
|
DropdownAction::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum DropdownAction {
|
||||||
|
None,
|
||||||
|
Type,
|
||||||
|
Open,
|
||||||
|
CloseSelect,
|
||||||
|
Close,
|
||||||
|
Next,
|
||||||
|
Previous,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum KeyboardKey {
|
||||||
|
ArrowDown,
|
||||||
|
ArrowUp,
|
||||||
|
Enter,
|
||||||
|
Space,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<String> for KeyboardKey {
|
||||||
|
fn eq(&self, other: &String) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::ArrowDown => other == "ArrowDown",
|
||||||
|
Self::ArrowUp => other == "ArrowUp",
|
||||||
|
Self::Enter => other == "Enter",
|
||||||
|
Self::Space => other == "Space",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod _aria;
|
||||||
mod accordion;
|
mod accordion;
|
||||||
mod anchor;
|
mod anchor;
|
||||||
mod auto_complete;
|
mod auto_complete;
|
||||||
|
|
|
@ -66,6 +66,8 @@ pub struct ColorTheme {
|
||||||
pub color_brand_stroke_2: String,
|
pub color_brand_stroke_2: String,
|
||||||
pub color_brand_stroke_2_contrast: String,
|
pub color_brand_stroke_2_contrast: String,
|
||||||
|
|
||||||
|
pub color_stroke_focus_2: String,
|
||||||
|
|
||||||
pub color_palette_red_background_1: String,
|
pub color_palette_red_background_1: String,
|
||||||
pub color_palette_red_background_3: String,
|
pub color_palette_red_background_3: String,
|
||||||
pub color_palette_red_foreground_1: String,
|
pub color_palette_red_foreground_1: String,
|
||||||
|
@ -181,6 +183,8 @@ impl ColorTheme {
|
||||||
color_brand_stroke_2: "#b4d6fa".into(),
|
color_brand_stroke_2: "#b4d6fa".into(),
|
||||||
color_brand_stroke_2_contrast: "#b4d6fa".into(),
|
color_brand_stroke_2_contrast: "#b4d6fa".into(),
|
||||||
|
|
||||||
|
color_stroke_focus_2: "#000000".into(),
|
||||||
|
|
||||||
color_palette_red_background_1: "#fdf6f6".into(),
|
color_palette_red_background_1: "#fdf6f6".into(),
|
||||||
color_palette_red_background_3: "#d13438".into(),
|
color_palette_red_background_3: "#d13438".into(),
|
||||||
color_palette_red_foreground_1: "#bc2f32".into(),
|
color_palette_red_foreground_1: "#bc2f32".into(),
|
||||||
|
@ -296,6 +300,8 @@ impl ColorTheme {
|
||||||
color_brand_stroke_2: "#0e4775".into(),
|
color_brand_stroke_2: "#0e4775".into(),
|
||||||
color_brand_stroke_2_contrast: "#0e4775".into(),
|
color_brand_stroke_2_contrast: "#0e4775".into(),
|
||||||
|
|
||||||
|
color_stroke_focus_2: "#ffffff".into(),
|
||||||
|
|
||||||
color_palette_red_background_1: "#3f1011".into(),
|
color_palette_red_background_1: "#3f1011".into(),
|
||||||
color_palette_red_background_3: "#d13438".into(),
|
color_palette_red_background_3: "#d13438".into(),
|
||||||
color_palette_red_foreground_1: "#e37d80".into(),
|
color_palette_red_foreground_1: "#e37d80".into(),
|
||||||
|
|
Loading…
Add table
Reference in a new issue