mirror of
https://github.com/adoyle0/thaw.git
synced 2025-01-22 22:09:22 -05:00
feat: AutoComplete adds keyboard operations
This commit is contained in:
parent
0a8e98576a
commit
87a4ef115c
13 changed files with 167 additions and 216 deletions
|
@ -150,7 +150,7 @@ pub fn SiteHeader() -> impl IntoView {
|
|||
comp_ref=auto_complete_ref
|
||||
>
|
||||
<For each=move || search_options.get() key=|option| option.label.clone() let:option>
|
||||
<AutoCompleteOption key=option.value>{option.label}</AutoCompleteOption>
|
||||
<AutoCompleteOption value=option.value>{option.label}</AutoCompleteOption>
|
||||
</For>
|
||||
<AutoCompletePrefix slot>
|
||||
<Icon
|
||||
|
|
|
@ -20,7 +20,7 @@ view! {
|
|||
key=|option| option.0.clone()
|
||||
let:option
|
||||
>
|
||||
<AutoCompleteOption key=option.0>
|
||||
<AutoCompleteOption value=option.0>
|
||||
{option.1}
|
||||
</AutoCompleteOption>
|
||||
</For>
|
||||
|
@ -36,14 +36,6 @@ view! {
|
|||
}
|
||||
```
|
||||
|
||||
### Invalid
|
||||
|
||||
```rust demo
|
||||
view! {
|
||||
<AutoComplete placeholder="Email" invalid=true/>
|
||||
}
|
||||
```
|
||||
|
||||
### Prefix & Suffix
|
||||
|
||||
```rust demo
|
||||
|
|
|
@ -54,16 +54,6 @@ view! {
|
|||
}
|
||||
```
|
||||
|
||||
### Invalid
|
||||
|
||||
```rust demo
|
||||
let value = RwSignal::new(String::from("o"));
|
||||
|
||||
view! {
|
||||
<Input value invalid=true/>
|
||||
}
|
||||
```
|
||||
|
||||
### Imperative handle
|
||||
|
||||
```rust demo
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use super::use_option_walker::{use_option_walker, OptionWalker};
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::{cell::RefCell, sync::Arc};
|
||||
use thaw_utils::scroll_into_view;
|
||||
use web_sys::{HtmlElement, Node};
|
||||
|
||||
/// Applied to the element that is active descendant
|
||||
|
@ -50,7 +51,7 @@ impl ActiveDescendantController {
|
|||
|
||||
fn focus_active_descendant(&self, next_active: HtmlElement) {
|
||||
self.blur_active_descendant();
|
||||
|
||||
scroll_into_view(&next_active);
|
||||
let _ = next_active.set_attribute(ACTIVEDESCENDANT_ATTRIBUTE, "");
|
||||
let _ = next_active.set_attribute(ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE, "");
|
||||
|
||||
|
|
|
@ -33,34 +33,6 @@ div.thaw-auto-complete__listbox {
|
|||
background-color: var(--thaw-background-color-hover);
|
||||
}
|
||||
|
||||
.thaw-auto-complete__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),
|
||||
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.thaw-auto-complete__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),
|
||||
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.thaw-auto-complete__listbox.fade-in-scale-up-transition-enter-from,
|
||||
.thaw-auto-complete__listbox.fade-in-scale-up-transition-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.thaw-auto-complete__listbox.fade-in-scale-up-transition-leave-from,
|
||||
.thaw-auto-complete__listbox.fade-in-scale-up-transition-enter-to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.thaw-auto-complete-option {
|
||||
column-gap: var(--spacingHorizontalXS);
|
||||
position: relative;
|
||||
|
@ -75,6 +47,19 @@ div.thaw-auto-complete__listbox {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.thaw-auto-complete-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-auto-complete-option:hover {
|
||||
color: var(--colorNeutralForeground1Hover);
|
||||
background-color: var(--colorNeutralBackground1Hover);
|
||||
|
|
|
@ -2,17 +2,28 @@ use super::AutoCompleteInjection;
|
|||
use leptos::prelude::*;
|
||||
|
||||
#[component]
|
||||
pub fn AutoCompleteOption(key: String, children: Children) -> impl IntoView {
|
||||
let value = key.clone();
|
||||
let auto_complete = AutoCompleteInjection::use_();
|
||||
let is_selected = Memo::new(move |_| auto_complete.is_selected(&key));
|
||||
pub fn AutoCompleteOption(value: String, children: Children) -> impl IntoView {
|
||||
let auto_complete = AutoCompleteInjection::expect_context();
|
||||
let is_selected = Memo::new({
|
||||
let value = value.clone();
|
||||
move |_| auto_complete.is_selected(&value)
|
||||
});
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
auto_complete.insert_option(id.clone(), value.clone());
|
||||
{
|
||||
let id = id.clone();
|
||||
on_cleanup(move || {
|
||||
auto_complete.remove_option(&id);
|
||||
});
|
||||
}
|
||||
|
||||
view! {
|
||||
<div
|
||||
class="thaw-auto-complete-option"
|
||||
role="option"
|
||||
id=id
|
||||
aria-selected=move || if is_selected.get() { "true" } else { "false" }
|
||||
on:click=move |_| auto_complete.select_value(value.clone())
|
||||
on:click=move |_| auto_complete.select_option(value.clone())
|
||||
>
|
||||
{children()}
|
||||
</div>
|
||||
|
|
|
@ -2,9 +2,14 @@ mod auto_complete_option;
|
|||
|
||||
pub use auto_complete_option::AutoCompleteOption;
|
||||
|
||||
use crate::{ComponentRef, ConfigInjection, Input, InputPrefix, InputRef, InputSuffix};
|
||||
use crate::{
|
||||
combobox::listbox::{listbox_keyboard_event, Listbox},
|
||||
ComponentRef, Input, InputPrefix, InputRef, InputSuffix,
|
||||
_aria::use_active_descendant,
|
||||
};
|
||||
use leptos::{context::Provider, either::Either, html, prelude::*};
|
||||
use thaw_components::{Binder, CSSTransition, Follower, FollowerPlacement, FollowerWidth};
|
||||
use std::collections::HashMap;
|
||||
use thaw_components::{Binder, Follower, FollowerPlacement, FollowerWidth};
|
||||
use thaw_utils::{class_list, mount_style, BoxOneCallback, Model, OptionalProp};
|
||||
|
||||
#[slot]
|
||||
|
@ -25,8 +30,6 @@ pub fn AutoComplete(
|
|||
#[prop(optional, into)] blur_after_select: MaybeSignal<bool>,
|
||||
#[prop(optional, into)] on_select: Option<BoxOneCallback<String>>,
|
||||
#[prop(optional, into)] disabled: MaybeSignal<bool>,
|
||||
#[prop(optional, into)] allow_free_input: bool,
|
||||
#[prop(optional, into)] invalid: MaybeSignal<bool>,
|
||||
#[prop(optional, into)] class: OptionalProp<MaybeSignal<String>>,
|
||||
#[prop(optional)] auto_complete_prefix: Option<AutoCompletePrefix>,
|
||||
#[prop(optional)] auto_complete_suffix: Option<AutoCompleteSuffix>,
|
||||
|
@ -34,27 +37,20 @@ pub fn AutoComplete(
|
|||
#[prop(optional)] children: Option<Children>,
|
||||
) -> impl IntoView {
|
||||
mount_style("auto-complete", include_str!("./auto-complete.css"));
|
||||
let config_provider = ConfigInjection::use_();
|
||||
let input_ref = ComponentRef::<InputRef>::new();
|
||||
|
||||
let default_index = if allow_free_input { None } else { Some(0) };
|
||||
|
||||
let select_option_index = RwSignal::<Option<usize>>::new(default_index);
|
||||
let menu_ref = NodeRef::<html::Div>::new();
|
||||
let is_show_menu = RwSignal::new(false);
|
||||
let listbox_ref = NodeRef::<html::Div>::new();
|
||||
let auto_complete_ref = NodeRef::<html::Div>::new();
|
||||
let open_menu = move || {
|
||||
select_option_index.set(default_index);
|
||||
is_show_menu.set(true);
|
||||
};
|
||||
let open_listbox = RwSignal::new(false);
|
||||
let options = StoredValue::new(HashMap::<String, String>::new());
|
||||
|
||||
let allow_value = move |_| {
|
||||
if !is_show_menu.get_untracked() {
|
||||
open_menu();
|
||||
if !open_listbox.get_untracked() {
|
||||
open_listbox.set(true);
|
||||
}
|
||||
true
|
||||
};
|
||||
|
||||
let select_value = Callback::new(move |option_value: String| {
|
||||
let select_option = Callback::new(move |option_value: String| {
|
||||
if clear_after_select.get_untracked() {
|
||||
value.set(String::new());
|
||||
} else {
|
||||
|
@ -63,10 +59,8 @@ pub fn AutoComplete(
|
|||
if let Some(on_select) = on_select.as_ref() {
|
||||
on_select(option_value);
|
||||
}
|
||||
if allow_free_input {
|
||||
select_option_index.set(None);
|
||||
}
|
||||
is_show_menu.set(false);
|
||||
|
||||
open_listbox.set(false);
|
||||
if blur_after_select.get_untracked() {
|
||||
if let Some(input_ref) = input_ref.get_untracked() {
|
||||
input_ref.blur();
|
||||
|
@ -74,153 +68,118 @@ pub fn AutoComplete(
|
|||
}
|
||||
});
|
||||
|
||||
// we unset selection index whenever options get changed
|
||||
// otherwise e.g. selection could move from one item to
|
||||
// another staying on the same index
|
||||
//create_effect(move |_| {
|
||||
//options.track();
|
||||
//select_option_index.set(default_index);
|
||||
//});
|
||||
let (set_listbox, active_descendant_controller) =
|
||||
use_active_descendant(move |el| el.class_list().contains("thaw-auto-complete-option"));
|
||||
let on_blur = {
|
||||
let active_descendant_controller = active_descendant_controller.clone();
|
||||
move |_| {
|
||||
active_descendant_controller.blur();
|
||||
open_listbox.set(false);
|
||||
}
|
||||
};
|
||||
let on_keydown = move |e| {
|
||||
listbox_keyboard_event(
|
||||
e,
|
||||
open_listbox,
|
||||
false,
|
||||
&active_descendant_controller,
|
||||
move |option| {
|
||||
options.with_value(|options| {
|
||||
if let Some(value) = options.get(&option.id()) {
|
||||
select_option.call(value.clone());
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
//let on_keydown = move |event: ev::KeyboardEvent| {
|
||||
//if !is_show_menu.get_untracked() {
|
||||
//return;
|
||||
//}
|
||||
//let key = event.key();
|
||||
//if key == *"ArrowDown" {
|
||||
//select_option_index.update(|index| {
|
||||
//if *index == Some(options.with_untracked(|options| options.len()) - 1) {
|
||||
//*index = default_index
|
||||
//} else {
|
||||
//*index = Some(index.map_or(0, |index| index + 1))
|
||||
//}
|
||||
//});
|
||||
//} else if key == *"ArrowUp" {
|
||||
//select_option_index.update(|index| {
|
||||
//match *index {
|
||||
//None => *index = Some(options.with_untracked(|options| options.len()) - 1),
|
||||
//Some(0) => {
|
||||
//if allow_free_input {
|
||||
//*index = None
|
||||
//} else {
|
||||
//*index = Some(options.with_untracked(|options| options.len()) - 1)
|
||||
//}
|
||||
//}
|
||||
//Some(prev_index) => *index = Some(prev_index - 1),
|
||||
//};
|
||||
//});
|
||||
//} else if key == *"Enter" {
|
||||
//event.prevent_default();
|
||||
//let option_value = options.with_untracked(|options| {
|
||||
//let index = select_option_index.get_untracked();
|
||||
//match index {
|
||||
//None if allow_free_input => {
|
||||
//let value = value.get_untracked();
|
||||
//(!value.is_empty()).then_some(value)
|
||||
//}
|
||||
//Some(index) if options.len() > index => {
|
||||
//let option = &options[index];
|
||||
//Some(option.value.clone())
|
||||
//}
|
||||
//_ => None,
|
||||
//}
|
||||
//});
|
||||
//if let Some(option_value) = option_value {
|
||||
//select_value(option_value);
|
||||
//}
|
||||
//}
|
||||
//};
|
||||
comp_ref.load(AutoCompleteRef { input_ref });
|
||||
// input_ref.on_load(move |_| {
|
||||
// });
|
||||
|
||||
view! {
|
||||
<Binder target_ref=auto_complete_ref>
|
||||
<div
|
||||
class=class_list!["thaw-auto-complete", class.map(| c | move || c.get())]
|
||||
node_ref=auto_complete_ref
|
||||
// on:keydown=on_keydown
|
||||
<Binder target_ref=auto_complete_ref>
|
||||
<div
|
||||
class=class_list!["thaw-auto-complete", class.map(| c | move || c.get())]
|
||||
node_ref=auto_complete_ref
|
||||
on:keydown=on_keydown
|
||||
>
|
||||
<Input
|
||||
value
|
||||
placeholder
|
||||
disabled
|
||||
on_focus=move |_| open_listbox.set(true)
|
||||
on_blur=on_blur
|
||||
allow_value
|
||||
comp_ref=input_ref
|
||||
>
|
||||
<Input
|
||||
value
|
||||
placeholder
|
||||
disabled
|
||||
invalid
|
||||
on_focus=move |_| open_menu()
|
||||
on_blur=move |_| is_show_menu.set(false)
|
||||
allow_value
|
||||
comp_ref=input_ref
|
||||
>
|
||||
<InputPrefix if_=auto_complete_prefix.is_some() slot>
|
||||
<InputPrefix if_=auto_complete_prefix.is_some() slot>
|
||||
|
||||
{if let Some(auto_complete_prefix) = auto_complete_prefix {
|
||||
Some((auto_complete_prefix.children)())
|
||||
{if let Some(auto_complete_prefix) = auto_complete_prefix {
|
||||
Some((auto_complete_prefix.children)())
|
||||
} else {
|
||||
None
|
||||
}}
|
||||
|
||||
</InputPrefix>
|
||||
<InputSuffix if_=auto_complete_suffix.is_some() slot>
|
||||
|
||||
{if let Some(auto_complete_suffix) = auto_complete_suffix {
|
||||
Some((auto_complete_suffix.children)())
|
||||
} else {
|
||||
None
|
||||
}}
|
||||
|
||||
</InputSuffix>
|
||||
</Input>
|
||||
</div>
|
||||
<Follower
|
||||
slot
|
||||
show=open_listbox
|
||||
placement=FollowerPlacement::BottomStart
|
||||
width=FollowerWidth::Target
|
||||
>
|
||||
<Provider value=AutoCompleteInjection{value, select_option, options}>
|
||||
<Listbox open=open_listbox.read_only() set_listbox listbox_ref class="thaw-auto-complete__listbox">
|
||||
{
|
||||
if let Some(children) = children {
|
||||
Either::Left(children())
|
||||
} else {
|
||||
None
|
||||
}}
|
||||
|
||||
</InputPrefix>
|
||||
<InputSuffix if_=auto_complete_suffix.is_some() slot>
|
||||
|
||||
{if let Some(auto_complete_suffix) = auto_complete_suffix {
|
||||
Some((auto_complete_suffix.children)())
|
||||
} else {
|
||||
None
|
||||
}}
|
||||
|
||||
</InputSuffix>
|
||||
</Input>
|
||||
</div>
|
||||
<Follower
|
||||
slot
|
||||
show=is_show_menu
|
||||
placement=FollowerPlacement::BottomStart
|
||||
width=FollowerWidth::Target
|
||||
>
|
||||
<Provider value=AutoCompleteInjection(value, select_value)>
|
||||
<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-config-provider thaw-auto-complete__listbox"
|
||||
style=move || display.get().unwrap_or_default()
|
||||
data-thaw-id=config_provider.id().clone()
|
||||
node_ref=menu_ref
|
||||
role="listbox"
|
||||
>
|
||||
{
|
||||
if let Some(children) = children {
|
||||
Either::Left(children())
|
||||
} else {
|
||||
Either::Right(())
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</CSSTransition>
|
||||
</Provider>
|
||||
</Follower>
|
||||
</Binder>
|
||||
}
|
||||
Either::Right(())
|
||||
}
|
||||
}
|
||||
</Listbox>
|
||||
</Provider>
|
||||
</Follower>
|
||||
</Binder>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct AutoCompleteInjection(pub Model<String>, pub Callback<String>);
|
||||
pub(crate) struct AutoCompleteInjection {
|
||||
value: Model<String>,
|
||||
select_option: Callback<String>,
|
||||
options: StoredValue<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
impl AutoCompleteInjection {
|
||||
pub fn use_() -> Self {
|
||||
pub fn expect_context() -> Self {
|
||||
expect_context()
|
||||
}
|
||||
|
||||
pub fn is_selected(&self, key: &String) -> bool {
|
||||
self.0.with(|value| value == key)
|
||||
self.value.with(|value| value == key)
|
||||
}
|
||||
|
||||
pub fn select_value(&self, key: String) {
|
||||
self.1.call(key.clone());
|
||||
pub fn select_option(&self, value: String) {
|
||||
self.select_option.call(value);
|
||||
}
|
||||
|
||||
pub fn insert_option(&self, id: String, value: 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,9 @@ pub fn ComboboxOption(
|
|||
let combobox = ComboboxInjection::expect_context();
|
||||
let value = StoredValue::new(value.unwrap_or_else(|| text.clone()));
|
||||
let text = StoredValue::new(text);
|
||||
let is_selected = Memo::new(move |_| value.with_value(|value| combobox.is_selected(&value)));
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let on_click = move |_| {
|
||||
text.with_value(|text| {
|
||||
value.with_value(|value| {
|
||||
|
@ -32,11 +34,11 @@ pub fn ComboboxOption(
|
|||
view! {
|
||||
<div
|
||||
role="option"
|
||||
aria-selected="true"
|
||||
aria-selected=move || if is_selected.get() { "true" } else { "false" }
|
||||
id=id
|
||||
class=class_list![
|
||||
"thaw-combobox-option",
|
||||
("thaw-combobox-option--selected", move || value.with_value(|value| combobox.is_selected(&value)))
|
||||
("thaw-combobox-option--selected", move || is_selected.get())
|
||||
]
|
||||
on:click=on_click
|
||||
>
|
||||
|
@ -44,7 +46,7 @@ pub fn ComboboxOption(
|
|||
if combobox.multiselect {
|
||||
view! {
|
||||
<span aria-hidden="true" class="thaw-combobox-option__check-icon--multiselect">
|
||||
<If cond=Signal::derive(move || value.with_value(|value| combobox.is_selected(&value)))>
|
||||
<If cond=is_selected>
|
||||
<Then slot>
|
||||
<svg fill="currentColor" aria-hidden="true" width="12" height="12" viewBox="0 0 12 12">
|
||||
<path d="M9.76 3.2c.3.29.32.76.04 1.06l-4.25 4.5a.75.75 0 0 1-1.08.02L2.22 6.53a.75.75 0 0 1 1.06-1.06l1.7 1.7L8.7 3.24a.75.75 0 0 1 1.06-.04Z" fill="currentColor"></path>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
mod combobox;
|
||||
mod combobox_option;
|
||||
mod listbox;
|
||||
pub(crate) mod listbox;
|
||||
mod utils;
|
||||
|
||||
pub use combobox::*;
|
||||
|
|
|
@ -122,7 +122,3 @@
|
|||
.thaw-input--disabled > .thaw-input__input::placeholder {
|
||||
color: var(--colorNeutralForegroundDisabled);
|
||||
}
|
||||
|
||||
.thaw-input--invalid {
|
||||
border-color: var(--thaw-border-color-error);
|
||||
}
|
||||
|
|
|
@ -40,7 +40,6 @@ pub fn Input(
|
|||
#[prop(optional, into)] on_focus: Option<BoxOneCallback<ev::FocusEvent>>,
|
||||
#[prop(optional, into)] on_blur: Option<BoxOneCallback<ev::FocusEvent>>,
|
||||
#[prop(optional, into)] disabled: MaybeSignal<bool>,
|
||||
#[prop(optional, into)] invalid: MaybeSignal<bool>,
|
||||
#[prop(optional)] input_prefix: Option<InputPrefix>,
|
||||
#[prop(optional)] input_suffix: Option<InputSuffix>,
|
||||
#[prop(optional)] comp_ref: ComponentRef<InputRef>,
|
||||
|
@ -125,8 +124,8 @@ pub fn Input(
|
|||
"thaw-input",
|
||||
("thaw-input--prefix", prefix_if_),
|
||||
("thaw-input--suffix", suffix_if_),
|
||||
("thaw-input--disabled", move || disabled.get()), ("thaw-input--invalid", move ||
|
||||
invalid.get()), class.map(| c | move || c.get())
|
||||
("thaw-input--disabled", move || disabled.get()),
|
||||
class.map(| c | move || c.get())
|
||||
]
|
||||
|
||||
on:mousedown=on_mousedown
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
mod get_scroll_parent;
|
||||
mod mount_style;
|
||||
mod scroll_into_view;
|
||||
|
||||
pub use get_scroll_parent::get_scroll_parent;
|
||||
pub use mount_style::{mount_dynamic_style, mount_style};
|
||||
pub use scroll_into_view::scroll_into_view;
|
||||
|
|
14
thaw_utils/src/dom/scroll_into_view.rs
Normal file
14
thaw_utils/src/dom/scroll_into_view.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
use super::get_scroll_parent;
|
||||
use web_sys::HtmlElement;
|
||||
|
||||
pub fn scroll_into_view(el: &HtmlElement) {
|
||||
if let Some(parent) = get_scroll_parent(el) {
|
||||
let parent_rect = parent.get_bounding_client_rect();
|
||||
let el_rect = el.get_bounding_client_rect();
|
||||
if el_rect.y() < parent_rect.y() {
|
||||
el.scroll_into_view_with_bool(true);
|
||||
} else if el_rect.y() + el_rect.height() > parent_rect.y() + parent_rect.height() {
|
||||
el.scroll_into_view_with_bool(false);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue