mirror of
https://github.com/yewprint/yewprint
synced 2024-11-22 03:23:03 +00:00
Add components Portal and Overlay (#161)
This commit is contained in:
parent
b6a6004fcf
commit
ba9b1746f4
19 changed files with 591 additions and 257 deletions
202
Cargo.lock
generated
202
Cargo.lock
generated
|
@ -17,15 +17,6 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.66"
|
||||
|
@ -105,17 +96,6 @@ version = "2.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
|
||||
|
||||
[[package]]
|
||||
name = "build-data"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a94f9f7aab679acac7ce29ba5581c00d3971a861c3b501c5bb74c3ba0026d90"
|
||||
dependencies = [
|
||||
"chrono 0.4.23",
|
||||
"safe-lock",
|
||||
"safe-regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.11.1"
|
||||
|
@ -194,21 +174,6 @@ dependencies = [
|
|||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-integer",
|
||||
"num-traits 0.2.15",
|
||||
"time",
|
||||
"wasm-bindgen",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chunked_transfer"
|
||||
version = "1.4.0"
|
||||
|
@ -261,16 +226,6 @@ dependencies = [
|
|||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
|
||||
dependencies = [
|
||||
"termcolor",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
|
@ -281,12 +236,6 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.3.2"
|
||||
|
@ -339,50 +288,6 @@ dependencies = [
|
|||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxx"
|
||||
version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cxxbridge-flags",
|
||||
"cxxbridge-macro",
|
||||
"link-cplusplus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxx-build"
|
||||
version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"codespan-reporting",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"scratch",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-flags"
|
||||
version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12"
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-macro"
|
||||
version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.8.0"
|
||||
|
@ -784,30 +689,6 @@ version = "2.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
|
||||
dependencies = [
|
||||
"cxx",
|
||||
"cxx-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.2.1"
|
||||
|
@ -975,15 +856,6 @@ version = "0.2.138"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
|
||||
|
||||
[[package]]
|
||||
name = "link-cplusplus"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.1.3"
|
||||
|
@ -1226,7 +1098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "478cc5188290e4dc1fa99a3128705c325c569da6c5ca67621a47afb9bcaa00ea"
|
||||
dependencies = [
|
||||
"byteorder 0.5.3",
|
||||
"chrono 0.2.25",
|
||||
"chrono",
|
||||
"rustc-serialize",
|
||||
"serde 0.7.15",
|
||||
"xml-rs",
|
||||
|
@ -1436,59 +1308,6 @@ version = "1.0.11"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
|
||||
|
||||
[[package]]
|
||||
name = "safe-lock"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "077d73db7973cccf63eb4aff1e5a34dc2459baa867512088269ea5f2f4253c90"
|
||||
|
||||
[[package]]
|
||||
name = "safe-proc-macro2"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "814c536dcd27acf03296c618dab7ad62d28e70abd7ba41d3f34a2ce707a2c666"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "safe-quote"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77e530f7831f3feafcd5f1aae406ac205dd998436b4007c8e80f03eca78a88f7"
|
||||
dependencies = [
|
||||
"safe-proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "safe-regex"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a15289bf322e0673d52756a18194167f2378ec1a15fe884af6e2d2cb934822b0"
|
||||
dependencies = [
|
||||
"safe-regex-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "safe-regex-compiler"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fba76fae590a2aa665279deb1f57b5098cbace01a0c5e60e262fcf55f7c51542"
|
||||
dependencies = [
|
||||
"safe-proc-macro2",
|
||||
"safe-quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "safe-regex-macro"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96c2e96b5c03f158d1b16ba79af515137795f4ad4e8de3f790518aae91f1d127"
|
||||
dependencies = [
|
||||
"safe-proc-macro2",
|
||||
"safe-regex-compiler",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "safemem"
|
||||
version = "0.3.3"
|
||||
|
@ -1510,12 +1329,6 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "scratch"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.7.0"
|
||||
|
@ -1813,18 +1626,6 @@ version = "1.10.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.7.1"
|
||||
|
@ -2459,7 +2260,6 @@ dependencies = [
|
|||
name = "yewprint-doc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"build-data",
|
||||
"console_error_panic_hook",
|
||||
"gloo",
|
||||
"implicit-clone",
|
||||
|
|
|
@ -22,7 +22,7 @@ gloo = "0.8"
|
|||
id_tree = { version = "1.8", optional = true }
|
||||
implicit-clone = "0.3.5"
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = { version = "0.3", features = ["DomRect", "Element", "Event", "HtmlSelectElement"] }
|
||||
web-sys = { version = "0.3", features = ["DomRect", "Element", "Event", "HtmlSelectElement", "DomTokenList"] }
|
||||
yew = "0.20"
|
||||
|
||||
[workspace]
|
||||
|
|
|
@ -100,9 +100,9 @@ Roadmap
|
|||
- [x] [InputGroup](https://blueprintjs.com/docs/#core/components/text-inputs.input-group)
|
||||
- [x] [TextArea](https://blueprintjs.com/docs/#core/components/text-inputs.text-area)
|
||||
- [ ] [TagInput](https://blueprintjs.com/docs/#core/components/tag-input)
|
||||
- [ ] [Overlay](https://blueprintjs.com/docs/#core/components/overlay)
|
||||
- [x] [Overlay](https://blueprintjs.com/docs/#core/components/overlay)
|
||||
- depends on: Portal
|
||||
- [ ] [Portal](https://blueprintjs.com/docs/#core/components/portal)
|
||||
- [x] [Portal](https://blueprintjs.com/docs/#core/components/portal)
|
||||
- [ ] [Alert](https://blueprintjs.com/docs/#core/components/alert)
|
||||
- depends on: Button, Dialog
|
||||
- [ ] [Context menu](https://blueprintjs.com/docs/#core/components/context-menu)
|
||||
|
|
|
@ -23,6 +23,8 @@ pub struct ButtonProps {
|
|||
#[prop_or_default]
|
||||
pub icon: Option<Icon>,
|
||||
#[prop_or_default]
|
||||
pub right_icon: Option<Icon>,
|
||||
#[prop_or_default]
|
||||
pub intent: Option<Intent>,
|
||||
#[prop_or_default]
|
||||
pub title: Option<AttrValue>,
|
||||
|
@ -33,6 +35,8 @@ pub struct ButtonProps {
|
|||
#[prop_or_default]
|
||||
pub style: Option<AttrValue>,
|
||||
#[prop_or_default]
|
||||
pub button_ref: NodeRef,
|
||||
#[prop_or_default]
|
||||
pub children: Children,
|
||||
}
|
||||
|
||||
|
@ -48,11 +52,13 @@ pub fn button(props: &ButtonProps) -> Html {
|
|||
active,
|
||||
disabled,
|
||||
icon,
|
||||
right_icon,
|
||||
intent,
|
||||
title,
|
||||
onclick,
|
||||
class,
|
||||
style,
|
||||
button_ref,
|
||||
children,
|
||||
} = props;
|
||||
|
||||
|
@ -74,6 +80,7 @@ pub fn button(props: &ButtonProps) -> Html {
|
|||
{style}
|
||||
{title}
|
||||
onclick={(!disabled).then_some(onclick.clone())}
|
||||
ref={button_ref.clone()}
|
||||
>
|
||||
{
|
||||
loading
|
||||
|
@ -85,7 +92,6 @@ pub fn button(props: &ButtonProps) -> Html {
|
|||
})
|
||||
}
|
||||
<Icon {icon} />
|
||||
//{icon.map(|icon| html!(<Icon {icon} />))}
|
||||
{
|
||||
(!children.is_empty())
|
||||
.then(|| html! {
|
||||
|
@ -94,6 +100,7 @@ pub fn button(props: &ButtonProps) -> Html {
|
|||
</span>
|
||||
})
|
||||
}
|
||||
<Icon icon={right_icon} />
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ pub struct CardProps {
|
|||
#[prop_or(false)]
|
||||
pub interactive: bool,
|
||||
#[prop_or_default]
|
||||
pub style: AttrValue,
|
||||
pub style: Option<AttrValue>,
|
||||
pub children: Children,
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ impl IconSize {
|
|||
pub const LARGE: IconSize = IconSize(20.0);
|
||||
|
||||
pub fn as_f64(&self) -> f64 {
|
||||
self.0 as f64
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn as_f32(&self) -> f32 {
|
||||
|
|
|
@ -20,7 +20,9 @@ mod icon;
|
|||
mod input_group;
|
||||
mod menu;
|
||||
mod numeric_input;
|
||||
mod overlay;
|
||||
mod panel_stack;
|
||||
mod portal;
|
||||
mod progress_bar;
|
||||
mod radio;
|
||||
mod radio_group;
|
||||
|
@ -50,7 +52,9 @@ pub use id_tree;
|
|||
pub use input_group::*;
|
||||
pub use menu::*;
|
||||
pub use numeric_input::*;
|
||||
pub use overlay::*;
|
||||
pub use panel_stack::*;
|
||||
pub use portal::*;
|
||||
pub use progress_bar::*;
|
||||
pub use radio::*;
|
||||
pub use radio_group::*;
|
||||
|
|
275
src/overlay.rs
Normal file
275
src/overlay.rs
Normal file
|
@ -0,0 +1,275 @@
|
|||
use crate::Portal;
|
||||
use gloo::timers::callback::Timeout;
|
||||
use wasm_bindgen::closure::Closure;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::{Element, HtmlElement, NodeList};
|
||||
use yew::prelude::*;
|
||||
|
||||
// TODO:
|
||||
// - use without <Portal/>
|
||||
// - enforce focus inside the overlay
|
||||
// - CSS transitions
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Overlay {
|
||||
start_focus_trap: NodeRef,
|
||||
content_ref: NodeRef,
|
||||
callback_timeout: Option<Timeout>,
|
||||
initial_open: bool,
|
||||
#[allow(dead_code)]
|
||||
document_focus_closure: Closure<dyn FnMut(FocusEvent)>,
|
||||
last_active_element: Option<Element>,
|
||||
focus_first_element_closure: Closure<dyn FnMut()>,
|
||||
focus_last_element_closure: Closure<dyn FnMut()>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct OverlayProps {
|
||||
#[prop_or_default]
|
||||
pub class: Classes,
|
||||
#[prop_or_default]
|
||||
pub style: Option<AttrValue>,
|
||||
#[prop_or_default]
|
||||
pub open: bool,
|
||||
#[prop_or(true)]
|
||||
pub backdrop: bool,
|
||||
#[prop_or_default]
|
||||
pub onclose: Callback<()>,
|
||||
#[prop_or_default]
|
||||
pub children: Children,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
OnKeyDown(KeyboardEvent),
|
||||
OnClick(MouseEvent),
|
||||
FocusFirstElement,
|
||||
FocusLastElement,
|
||||
Close,
|
||||
}
|
||||
|
||||
impl Component for Overlay {
|
||||
type Properties = OverlayProps;
|
||||
type Message = Msg;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
let content_ref = NodeRef::default();
|
||||
|
||||
let document_focus_closure = {
|
||||
let callback = ctx.link().callback(|_| Msg::FocusFirstElement);
|
||||
let content_ref = content_ref.clone();
|
||||
Closure::new(Box::new(move |_event| {
|
||||
let active_element_in_content = content_ref
|
||||
.cast::<Element>()
|
||||
.map(|content| {
|
||||
content.contains(gloo::utils::document().active_element().as_deref())
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if !active_element_in_content {
|
||||
callback.emit(());
|
||||
}
|
||||
}) as Box<dyn FnMut(_)>)
|
||||
};
|
||||
gloo::utils::document().set_onfocus(Some(document_focus_closure.as_ref().unchecked_ref()));
|
||||
|
||||
let focus_first_element_closure = {
|
||||
let content_ref = content_ref.clone();
|
||||
Closure::new(Box::new(move || {
|
||||
if let Some(element) = get_focusable_elements(&content_ref)
|
||||
.and_then(|x| x.item(0))
|
||||
.and_then(|x| x.dyn_into::<HtmlElement>().ok())
|
||||
{
|
||||
element.focus().unwrap();
|
||||
}
|
||||
}) as Box<dyn FnMut()>)
|
||||
};
|
||||
|
||||
let focus_last_element_closure = {
|
||||
let content_ref = content_ref.clone();
|
||||
Closure::new(Box::new(move || {
|
||||
if let Some(element) = get_focusable_elements(&content_ref)
|
||||
.and_then(|x| x.item(x.length() - 1))
|
||||
.and_then(|x| x.dyn_into::<HtmlElement>().ok())
|
||||
{
|
||||
element.focus().unwrap();
|
||||
}
|
||||
}) as Box<dyn FnMut()>)
|
||||
};
|
||||
|
||||
Self {
|
||||
start_focus_trap: NodeRef::default(),
|
||||
content_ref,
|
||||
callback_timeout: None,
|
||||
initial_open: false,
|
||||
document_focus_closure,
|
||||
last_active_element: None,
|
||||
focus_first_element_closure,
|
||||
focus_last_element_closure,
|
||||
}
|
||||
}
|
||||
|
||||
fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
|
||||
let new_props = ctx.props();
|
||||
|
||||
if new_props.open != old_props.open {
|
||||
self.initial_open = false;
|
||||
}
|
||||
|
||||
if new_props.open {
|
||||
self.last_active_element = gloo::utils::document().active_element();
|
||||
gloo::utils::body()
|
||||
.class_list()
|
||||
.add_1("bp3-overlay-open")
|
||||
.unwrap();
|
||||
} else {
|
||||
gloo::utils::body()
|
||||
.class_list()
|
||||
.remove_1("bp3-overlay-open")
|
||||
.unwrap();
|
||||
if let Some(element) = self
|
||||
.last_active_element
|
||||
.take()
|
||||
.and_then(|x| x.dyn_into::<HtmlElement>().ok())
|
||||
{
|
||||
let _ = element.focus();
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::OnKeyDown(event) => {
|
||||
if event.key() == "Escape" {
|
||||
ctx.props().onclose.emit(());
|
||||
}
|
||||
false
|
||||
}
|
||||
Msg::OnClick(_event) => {
|
||||
if self.callback_timeout.is_none() {
|
||||
let callback = ctx.link().callback(|_| Msg::Close);
|
||||
self.callback_timeout
|
||||
.replace(Timeout::new(0, move || callback.emit(())));
|
||||
} else {
|
||||
self.callback_timeout.take();
|
||||
}
|
||||
false
|
||||
}
|
||||
Msg::FocusFirstElement => {
|
||||
// always delay focus manipulation to just before repaint to prevent scroll jumping
|
||||
gloo::utils::window()
|
||||
.request_animation_frame(
|
||||
self.focus_first_element_closure.as_ref().unchecked_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
false
|
||||
}
|
||||
Msg::FocusLastElement => {
|
||||
// always delay focus manipulation to just before repaint to prevent scroll jumping
|
||||
gloo::utils::window()
|
||||
.request_animation_frame(
|
||||
self.focus_last_element_closure.as_ref().unchecked_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
false
|
||||
}
|
||||
Msg::Close => {
|
||||
self.callback_timeout.take();
|
||||
ctx.props().onclose.emit(());
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let Self::Properties {
|
||||
class,
|
||||
style,
|
||||
open,
|
||||
backdrop,
|
||||
onclose: _,
|
||||
children,
|
||||
} = ctx.props();
|
||||
|
||||
let backdrop = backdrop.then(|| {
|
||||
html! {
|
||||
<div class="bp3-overlay-backdrop" />
|
||||
}
|
||||
});
|
||||
let inner = open.then(|| {
|
||||
html! {
|
||||
<>
|
||||
<div
|
||||
class="bp3-overlay-start-focus-trap"
|
||||
ref={self.start_focus_trap.clone()}
|
||||
tabindex=0
|
||||
// NOTE: I am not 100% sure this is correct. In Blueprint they capture the
|
||||
// Shift+Tab combination but it looks like it's more for historic
|
||||
// reason... well, it seems to work on current Chrome and Firefox so...
|
||||
onfocus={ctx.link().callback(|_| Msg::FocusLastElement)}
|
||||
/>
|
||||
{backdrop}
|
||||
<div
|
||||
class={classes!("bp3-overlay-content", class.clone())}
|
||||
{style}
|
||||
ref={self.content_ref.clone()}
|
||||
onclick={ctx.link().callback(Msg::OnClick)}
|
||||
>
|
||||
{for children.iter()}
|
||||
</div>
|
||||
<div
|
||||
class="bp3-overlay-end-focus-trap"
|
||||
ref={self.start_focus_trap.clone()}
|
||||
tabindex=0
|
||||
onfocus={ctx.link().callback(|_| Msg::FocusFirstElement)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
});
|
||||
|
||||
html! {
|
||||
<Portal>
|
||||
<div
|
||||
class={classes!(
|
||||
"bp3-overlay",
|
||||
"bp3-overlay-scroll-container",
|
||||
open.then_some("bp3-overlay-open"),
|
||||
)}
|
||||
aria-live="polite"
|
||||
onkeydown={ctx.link().callback(Msg::OnKeyDown)}
|
||||
onclick={ctx.link().callback(Msg::OnClick)}
|
||||
>
|
||||
{inner}
|
||||
</div>
|
||||
</Portal>
|
||||
}
|
||||
}
|
||||
|
||||
fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
|
||||
let Self::Properties { open, .. } = ctx.props();
|
||||
|
||||
if *open && !self.initial_open {
|
||||
self.initial_open = true;
|
||||
ctx.link().send_message(Msg::FocusFirstElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Overlay {
|
||||
fn drop(&mut self) {
|
||||
gloo::utils::document().set_onfocus(None);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_focusable_elements(node_ref: &NodeRef) -> Option<NodeList> {
|
||||
node_ref.cast::<Element>().and_then(|element| {
|
||||
element
|
||||
.query_selector_all(
|
||||
"a[href]:not([tabindex=\"-1\"]),button:not([disabled]):not([tabindex=\"-1\"]),\
|
||||
details:not([tabindex=\"-1\"]),input:not([disabled]):not([tabindex=\"-1\"]),\
|
||||
select:not([disabled]):not([tabindex=\"-1\"]),\
|
||||
textarea:not([disabled]):not([tabindex=\"-1\"]),[tabindex]:not([tabindex=\"-1\"])",
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
}
|
56
src/portal.rs
Normal file
56
src/portal.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use web_sys::Element;
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Portal {
|
||||
container_element: Option<Element>,
|
||||
node_ref: NodeRef,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct PortalProps {
|
||||
#[prop_or_default]
|
||||
pub children: Children,
|
||||
}
|
||||
|
||||
impl Component for Portal {
|
||||
type Properties = PortalProps;
|
||||
type Message = ();
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let contents = self
|
||||
.container_element
|
||||
.clone()
|
||||
.map(|container_element| {
|
||||
create_portal(
|
||||
html! {
|
||||
{for ctx.props().children.iter()}
|
||||
},
|
||||
container_element,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
html! {
|
||||
<div ref={self.node_ref.clone()}>
|
||||
{contents}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
|
||||
if first_render {
|
||||
let container_element = gloo::utils::document().create_element("div").unwrap();
|
||||
container_element.set_class_name("bp3-portal");
|
||||
gloo::utils::body()
|
||||
.append_child(&container_element)
|
||||
.unwrap();
|
||||
self.container_element.replace(container_element);
|
||||
ctx.link().send_message(());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,9 +26,7 @@ pub fn progress_bar(
|
|||
}: &ProgressBarProps,
|
||||
) -> Html {
|
||||
let style = if let Some(value) = value {
|
||||
// NOTE: nightly, issue #44095 for f32::clamp
|
||||
// let percent = ((1000. * value).ceil() / 10.).clamp(0.,100.);
|
||||
let percent = ((1000. * value).ceil() / 10.).max(0.).min(100.);
|
||||
let percent = value * 100.0;
|
||||
AttrValue::from(format!("width: {}%;", percent))
|
||||
} else {
|
||||
"".into()
|
||||
|
|
|
@ -69,7 +69,7 @@ pub(crate) fn generate_icons() -> Result<()> {
|
|||
src.push_str("];\n");
|
||||
src.push('}');
|
||||
|
||||
fs::write(&dest_path, src).context("could not write into file")?;
|
||||
fs::write(dest_path, src).context("could not write into file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ fn download_css(force: bool) -> Result<()> {
|
|||
"@blueprintjs/docs-theme",
|
||||
"3.11.1",
|
||||
Path::new("package/lib/css/docs-theme.css"),
|
||||
&static_path.join("docs-theme.css"),
|
||||
static_path.join("docs-theme.css"),
|
||||
)
|
||||
.context("while downloading CSS of @blueprintjs/docs-theme")?;
|
||||
log::info!("Docs Theme CSS updated to: {}", version);
|
||||
|
|
|
@ -30,7 +30,6 @@ yew-router = "0.17"
|
|||
yewprint = { path = ".." }
|
||||
|
||||
[build-dependencies]
|
||||
build-data = "0.1.3"
|
||||
syntect = "0.5.0"
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -10,8 +10,6 @@ fn main() {
|
|||
let theme_set = ThemeSet::load_defaults();
|
||||
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||
|
||||
build_data::set_GIT_BRANCH();
|
||||
|
||||
fn recursive<P: AsRef<Path>>(
|
||||
base_path: P,
|
||||
syntax_set: &SyntaxSet,
|
||||
|
|
|
@ -11,6 +11,7 @@ use crate::icon::*;
|
|||
use crate::input_group::*;
|
||||
use crate::menu::*;
|
||||
use crate::numeric_input::*;
|
||||
use crate::overlay::*;
|
||||
use crate::panel_stack::*;
|
||||
use crate::progress_bar::*;
|
||||
use crate::radio::*;
|
||||
|
@ -22,6 +23,7 @@ use crate::tag::*;
|
|||
use crate::text::*;
|
||||
use crate::text_area::*;
|
||||
use crate::tree::*;
|
||||
use crate::DARK;
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
use yewprint::{Icon, Menu, MenuItem};
|
||||
|
@ -35,9 +37,7 @@ pub fn app_root() -> Html {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
dark_theme: bool,
|
||||
}
|
||||
pub struct App;
|
||||
|
||||
pub enum Msg {
|
||||
ToggleLight,
|
||||
|
@ -49,17 +49,14 @@ impl Component for App {
|
|||
type Properties = ();
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
App {
|
||||
dark_theme: web_sys::window()
|
||||
.and_then(|x| x.match_media("(prefers-color-scheme: dark)").ok().flatten())
|
||||
.map(|x| x.matches())
|
||||
.unwrap_or(true),
|
||||
}
|
||||
Self
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::ToggleLight => self.dark_theme ^= true,
|
||||
Msg::ToggleLight => {
|
||||
DARK.with(|x| x.replace(!*x.borrow()));
|
||||
}
|
||||
Msg::GoToMenu(event, doc_menu) => {
|
||||
event.prevent_default();
|
||||
if let Some(navigator) = ctx.link().navigator() {
|
||||
|
@ -73,21 +70,15 @@ impl Component for App {
|
|||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let netlify_badge = if self.dark_theme {
|
||||
let dark = DARK.with(|x| *x.borrow());
|
||||
|
||||
let netlify_badge = if dark {
|
||||
"https://www.netlify.com/img/global/badges/netlify-color-accent.svg"
|
||||
} else {
|
||||
"https://www.netlify.com/img/global/badges/netlify-color-bg.svg"
|
||||
};
|
||||
let go_to_theme_label = if self.dark_theme {
|
||||
"Light theme"
|
||||
} else {
|
||||
"Dark theme"
|
||||
};
|
||||
let go_to_theme_icon = if self.dark_theme {
|
||||
Icon::Flash
|
||||
} else {
|
||||
Icon::Moon
|
||||
};
|
||||
let go_to_theme_label = if dark { "Light theme" } else { "Dark theme" };
|
||||
let go_to_theme_icon = if dark { Icon::Flash } else { Icon::Moon };
|
||||
|
||||
let menu = html! {
|
||||
<Menu>
|
||||
|
@ -175,6 +166,12 @@ impl Component for App {
|
|||
onclick={ctx.link()
|
||||
.callback(|e| Msg::GoToMenu(e, DocMenu::NumericInput))}
|
||||
/>
|
||||
<MenuItem
|
||||
text={html!("Overlay")}
|
||||
href="/overlay"
|
||||
onclick={ctx.link()
|
||||
.callback(|e| Msg::GoToMenu(e, DocMenu::Overlay))}
|
||||
/>
|
||||
<MenuItem
|
||||
text={html!("PanelStack")}
|
||||
href="/panel-stack"
|
||||
|
@ -279,7 +276,7 @@ impl Component for App {
|
|||
};
|
||||
|
||||
html! {
|
||||
<div class={classes!("docs-root", self.dark_theme.then_some("bp3-dark"))}>
|
||||
<div class={classes!("docs-root", dark.then_some("bp3-dark"))}>
|
||||
<div class={classes!("docs-app")}>
|
||||
{{ navigation }}
|
||||
<main class={classes!("docs-content-wrapper")} role="main">
|
||||
|
@ -308,6 +305,7 @@ fn switch(route: DocMenu) -> Html {
|
|||
DocMenu::InputGroup => html!(<InputGroupDoc />),
|
||||
DocMenu::Menu => html!(<MenuDoc />),
|
||||
DocMenu::NumericInput => html!(<NumericInputDoc />),
|
||||
DocMenu::Overlay => html!(<OverlayDoc />),
|
||||
DocMenu::PanelStack => html!(<PanelStackDoc />),
|
||||
DocMenu::ProgressBar => html!(<ProgressBarDoc />),
|
||||
DocMenu::Radio => html!(<RadioDoc />),
|
||||
|
@ -350,6 +348,8 @@ pub enum DocMenu {
|
|||
Menu,
|
||||
#[at("/numeric-input")]
|
||||
NumericInput,
|
||||
#[at("/overlay")]
|
||||
Overlay,
|
||||
#[at("/panel-stack")]
|
||||
PanelStack,
|
||||
#[at("/progress-bar")]
|
||||
|
|
|
@ -23,6 +23,7 @@ mod input_group;
|
|||
mod logo;
|
||||
mod menu;
|
||||
mod numeric_input;
|
||||
mod overlay;
|
||||
mod panel_stack;
|
||||
mod progress_bar;
|
||||
mod radio;
|
||||
|
@ -38,6 +39,16 @@ mod tree;
|
|||
pub use app::*;
|
||||
pub use example::*;
|
||||
pub use logo::*;
|
||||
use std::cell::RefCell;
|
||||
|
||||
thread_local! {
|
||||
pub static DARK: RefCell<bool> = {
|
||||
RefCell::new(web_sys::window()
|
||||
.and_then(|x| x.match_media("(prefers-color-scheme: dark)").ok().flatten())
|
||||
.map(|x| x.matches())
|
||||
.unwrap_or(true))
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! include_raw_html {
|
||||
|
@ -67,7 +78,7 @@ macro_rules! build_source_code_component {
|
|||
pub fn generate_url() -> String {
|
||||
use std::path::Path;
|
||||
|
||||
let component_name = Path::new(file!())
|
||||
let component = Path::new(file!())
|
||||
.parent()
|
||||
.unwrap()
|
||||
.file_name()
|
||||
|
@ -75,10 +86,13 @@ macro_rules! build_source_code_component {
|
|||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
format!(
|
||||
"https://github.com/yewprint/yewprint/blob/HEAD/src/{}.rs",
|
||||
component_name,
|
||||
)
|
||||
if let (Some(actor), Some(branch)) =
|
||||
(option_env!("GITHUB_ACTOR"), option_env!("GITHUB_HEAD_REF"))
|
||||
{
|
||||
format!("https://github.com/{actor}/yewprint/blob/{branch}/src/{component}.rs")
|
||||
} else {
|
||||
format!("https://github.com/yewprint/yewprint/blob/HEAD/src/{component}.rs")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
100
yewprint-doc/src/overlay/example.rs
Normal file
100
yewprint-doc/src/overlay/example.rs
Normal file
|
@ -0,0 +1,100 @@
|
|||
use crate::DARK;
|
||||
use yew::prelude::*;
|
||||
use yewprint::{Button, Card, Elevation, Icon, Intent, Overlay, H3};
|
||||
|
||||
pub struct Example {
|
||||
open: bool,
|
||||
tall: bool,
|
||||
show_button_ref: NodeRef,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Properties)]
|
||||
pub struct ExampleProps {
|
||||
pub backdrop: bool,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
Open,
|
||||
Close,
|
||||
ToggleTall,
|
||||
}
|
||||
|
||||
impl Component for Example {
|
||||
type Message = Msg;
|
||||
type Properties = ExampleProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Example {
|
||||
open: false,
|
||||
tall: false,
|
||||
show_button_ref: NodeRef::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::Open => {
|
||||
self.open = true;
|
||||
}
|
||||
Msg::Close => {
|
||||
self.open = false;
|
||||
}
|
||||
Msg::ToggleTall => {
|
||||
self.tall ^= true;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let Self::Properties { backdrop } = &ctx.props();
|
||||
|
||||
html! {
|
||||
<div>
|
||||
<div>
|
||||
<Button
|
||||
onclick={ctx.link().callback(|_| Msg::Open)}
|
||||
button_ref={self.show_button_ref.clone()}
|
||||
>
|
||||
{"Show overlay"}
|
||||
</Button>
|
||||
<Overlay
|
||||
open={self.open}
|
||||
onclose={ctx.link().callback(|_| Msg::Close)}
|
||||
{backdrop}
|
||||
class={classes!(
|
||||
DARK.with(|x| x.borrow().then_some("bp3-dark")),
|
||||
self.tall.then_some("docs-overlay-example-tall"),
|
||||
)}
|
||||
style="left: calc(50vw - 200px); margin: 10vh 0; top: 0; width: 400px;"
|
||||
>
|
||||
<Card elevation={Elevation::Level4} style="height: 100%">
|
||||
<H3>{"I'm an Overlay!"}</H3>
|
||||
<p>{"This is a simple container with some inline styles to position \
|
||||
it on the screen."}</p>
|
||||
<p>{"Click the \"Make me scroll\" button below to make this overlay's \
|
||||
content really tall, which will make the overlay's container \
|
||||
(but not the page) scrollable"}</p>
|
||||
<div class="bp3-dialog-footer-actions">
|
||||
<Button
|
||||
intent={Intent::Danger}
|
||||
onclick={ctx.link().callback(|_| Msg::Close)}
|
||||
>
|
||||
{"Close"}
|
||||
</Button>
|
||||
<Button
|
||||
active={self.tall}
|
||||
onclick={ctx.link().callback(|_| Msg::ToggleTall)}
|
||||
icon={Icon::DoubleChevronDown}
|
||||
right_icon={Icon::DoubleChevronDown}
|
||||
>
|
||||
{"Make me scroll"}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Overlay>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
79
yewprint-doc/src/overlay/mod.rs
Normal file
79
yewprint-doc/src/overlay/mod.rs
Normal file
|
@ -0,0 +1,79 @@
|
|||
mod example;
|
||||
|
||||
use crate::ExampleContainer;
|
||||
use example::*;
|
||||
use yew::prelude::*;
|
||||
use yewprint::{Switch, H1, H5};
|
||||
|
||||
pub struct OverlayDoc {
|
||||
callback: Callback<ExampleProps>,
|
||||
state: ExampleProps,
|
||||
}
|
||||
|
||||
impl Component for OverlayDoc {
|
||||
type Message = ExampleProps;
|
||||
type Properties = ();
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
OverlayDoc {
|
||||
callback: ctx.link().callback(|x| x),
|
||||
state: ExampleProps { backdrop: true },
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
self.state = msg;
|
||||
true
|
||||
}
|
||||
|
||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||
let example_props = self.state.clone();
|
||||
let source = crate::include_raw_html!(
|
||||
concat!(env!("OUT_DIR"), "/", file!(), ".html"),
|
||||
"bp3-code-block"
|
||||
);
|
||||
|
||||
let props_component = html! {
|
||||
<OverlayProps
|
||||
callback={self.callback.clone()}
|
||||
example_props={example_props.clone()}
|
||||
/>
|
||||
};
|
||||
|
||||
html! {
|
||||
<div>
|
||||
<H1 class={classes!("docs-title")}>{"Overlay"}</H1>
|
||||
<SourceCodeUrl />
|
||||
<div>
|
||||
<ExampleContainer
|
||||
source={source}
|
||||
props={Some(props_component)}
|
||||
>
|
||||
<Example ..example_props />
|
||||
</ExampleContainer>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
crate::build_example_prop_component! {
|
||||
OverlayProps for ExampleProps =>
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<div>
|
||||
<H5>{"Props"}</H5>
|
||||
<Switch
|
||||
onclick={self.update_props(ctx, |props, _| ExampleProps {
|
||||
backdrop: !props.backdrop,
|
||||
..props
|
||||
})}
|
||||
checked={ctx.props().example_props.backdrop}
|
||||
label={html!("Backdrop")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
crate::build_source_code_component!();
|
|
@ -120,23 +120,27 @@
|
|||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.docs-icon-search {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.docs-icon-search {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.docs-icon-list {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto auto;
|
||||
row-gap: 40px;
|
||||
column-gap: 20px;
|
||||
}
|
||||
.docs-icon-list {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto auto auto;
|
||||
row-gap: 40px;
|
||||
column-gap: 20px;
|
||||
}
|
||||
|
||||
.docs-icon-list-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
.docs-icon-list-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.docs-overlay-example-tall {
|
||||
height: 200%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
|
Loading…
Reference in a new issue