Add components Portal and Overlay (#161)

This commit is contained in:
Cecile Tonglet 2022-12-19 16:49:06 +01:00 committed by GitHub
parent b6a6004fcf
commit ba9b1746f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 591 additions and 257 deletions

202
Cargo.lock generated
View file

@ -17,15 +17,6 @@ dependencies = [
"memchr", "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]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.66" version = "1.0.66"
@ -105,17 +96,6 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9" 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]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.11.1" version = "3.11.1"
@ -194,21 +174,6 @@ dependencies = [
"time", "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]] [[package]]
name = "chunked_transfer" name = "chunked_transfer"
version = "1.4.0" version = "1.4.0"
@ -261,16 +226,6 @@ dependencies = [
"cc", "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]] [[package]]
name = "console_error_panic_hook" name = "console_error_panic_hook"
version = "0.1.7" version = "0.1.7"
@ -281,12 +236,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.3.2" version = "1.3.2"
@ -339,50 +288,6 @@ dependencies = [
"cfg-if 1.0.0", "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]] [[package]]
name = "either" name = "either"
version = "1.8.0" version = "1.8.0"
@ -784,30 +689,6 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 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]] [[package]]
name = "id-arena" name = "id-arena"
version = "2.2.1" version = "2.2.1"
@ -975,15 +856,6 @@ version = "0.2.138"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" 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]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.1.3" version = "0.1.3"
@ -1226,7 +1098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478cc5188290e4dc1fa99a3128705c325c569da6c5ca67621a47afb9bcaa00ea" checksum = "478cc5188290e4dc1fa99a3128705c325c569da6c5ca67621a47afb9bcaa00ea"
dependencies = [ dependencies = [
"byteorder 0.5.3", "byteorder 0.5.3",
"chrono 0.2.25", "chrono",
"rustc-serialize", "rustc-serialize",
"serde 0.7.15", "serde 0.7.15",
"xml-rs", "xml-rs",
@ -1436,59 +1308,6 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" 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]] [[package]]
name = "safemem" name = "safemem"
version = "0.3.3" version = "0.3.3"
@ -1510,12 +1329,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "scratch"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
[[package]] [[package]]
name = "sct" name = "sct"
version = "0.7.0" version = "0.7.0"
@ -1813,18 +1626,6 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" 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]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.7.1" version = "0.7.1"
@ -2459,7 +2260,6 @@ dependencies = [
name = "yewprint-doc" name = "yewprint-doc"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"build-data",
"console_error_panic_hook", "console_error_panic_hook",
"gloo", "gloo",
"implicit-clone", "implicit-clone",

View file

@ -22,7 +22,7 @@ gloo = "0.8"
id_tree = { version = "1.8", optional = true } id_tree = { version = "1.8", optional = true }
implicit-clone = "0.3.5" implicit-clone = "0.3.5"
wasm-bindgen = "0.2" 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" yew = "0.20"
[workspace] [workspace]

View file

@ -100,9 +100,9 @@ Roadmap
- [x] [InputGroup](https://blueprintjs.com/docs/#core/components/text-inputs.input-group) - [x] [InputGroup](https://blueprintjs.com/docs/#core/components/text-inputs.input-group)
- [x] [TextArea](https://blueprintjs.com/docs/#core/components/text-inputs.text-area) - [x] [TextArea](https://blueprintjs.com/docs/#core/components/text-inputs.text-area)
- [ ] [TagInput](https://blueprintjs.com/docs/#core/components/tag-input) - [ ] [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 - 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) - [ ] [Alert](https://blueprintjs.com/docs/#core/components/alert)
- depends on: Button, Dialog - depends on: Button, Dialog
- [ ] [Context menu](https://blueprintjs.com/docs/#core/components/context-menu) - [ ] [Context menu](https://blueprintjs.com/docs/#core/components/context-menu)

View file

@ -23,6 +23,8 @@ pub struct ButtonProps {
#[prop_or_default] #[prop_or_default]
pub icon: Option<Icon>, pub icon: Option<Icon>,
#[prop_or_default] #[prop_or_default]
pub right_icon: Option<Icon>,
#[prop_or_default]
pub intent: Option<Intent>, pub intent: Option<Intent>,
#[prop_or_default] #[prop_or_default]
pub title: Option<AttrValue>, pub title: Option<AttrValue>,
@ -33,6 +35,8 @@ pub struct ButtonProps {
#[prop_or_default] #[prop_or_default]
pub style: Option<AttrValue>, pub style: Option<AttrValue>,
#[prop_or_default] #[prop_or_default]
pub button_ref: NodeRef,
#[prop_or_default]
pub children: Children, pub children: Children,
} }
@ -48,11 +52,13 @@ pub fn button(props: &ButtonProps) -> Html {
active, active,
disabled, disabled,
icon, icon,
right_icon,
intent, intent,
title, title,
onclick, onclick,
class, class,
style, style,
button_ref,
children, children,
} = props; } = props;
@ -74,6 +80,7 @@ pub fn button(props: &ButtonProps) -> Html {
{style} {style}
{title} {title}
onclick={(!disabled).then_some(onclick.clone())} onclick={(!disabled).then_some(onclick.clone())}
ref={button_ref.clone()}
> >
{ {
loading loading
@ -85,7 +92,6 @@ pub fn button(props: &ButtonProps) -> Html {
}) })
} }
<Icon {icon} /> <Icon {icon} />
//{icon.map(|icon| html!(<Icon {icon} />))}
{ {
(!children.is_empty()) (!children.is_empty())
.then(|| html! { .then(|| html! {
@ -94,6 +100,7 @@ pub fn button(props: &ButtonProps) -> Html {
</span> </span>
}) })
} }
<Icon icon={right_icon} />
</button> </button>
} }
} }

View file

@ -12,7 +12,7 @@ pub struct CardProps {
#[prop_or(false)] #[prop_or(false)]
pub interactive: bool, pub interactive: bool,
#[prop_or_default] #[prop_or_default]
pub style: AttrValue, pub style: Option<AttrValue>,
pub children: Children, pub children: Children,
} }

View file

@ -14,7 +14,7 @@ impl IconSize {
pub const LARGE: IconSize = IconSize(20.0); pub const LARGE: IconSize = IconSize(20.0);
pub fn as_f64(&self) -> f64 { pub fn as_f64(&self) -> f64 {
self.0 as f64 self.0
} }
pub fn as_f32(&self) -> f32 { pub fn as_f32(&self) -> f32 {

View file

@ -20,7 +20,9 @@ mod icon;
mod input_group; mod input_group;
mod menu; mod menu;
mod numeric_input; mod numeric_input;
mod overlay;
mod panel_stack; mod panel_stack;
mod portal;
mod progress_bar; mod progress_bar;
mod radio; mod radio;
mod radio_group; mod radio_group;
@ -50,7 +52,9 @@ pub use id_tree;
pub use input_group::*; pub use input_group::*;
pub use menu::*; pub use menu::*;
pub use numeric_input::*; pub use numeric_input::*;
pub use overlay::*;
pub use panel_stack::*; pub use panel_stack::*;
pub use portal::*;
pub use progress_bar::*; pub use progress_bar::*;
pub use radio::*; pub use radio::*;
pub use radio_group::*; pub use radio_group::*;

275
src/overlay.rs Normal file
View 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
View 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(());
}
}
}

View file

@ -26,9 +26,7 @@ pub fn progress_bar(
}: &ProgressBarProps, }: &ProgressBarProps,
) -> Html { ) -> Html {
let style = if let Some(value) = value { let style = if let Some(value) = value {
// NOTE: nightly, issue #44095 for f32::clamp let percent = value * 100.0;
// let percent = ((1000. * value).ceil() / 10.).clamp(0.,100.);
let percent = ((1000. * value).ceil() / 10.).max(0.).min(100.);
AttrValue::from(format!("width: {}%;", percent)) AttrValue::from(format!("width: {}%;", percent))
} else { } else {
"".into() "".into()

View file

@ -69,7 +69,7 @@ pub(crate) fn generate_icons() -> Result<()> {
src.push_str("];\n"); src.push_str("];\n");
src.push('}'); 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(()) Ok(())
} }

View file

@ -69,7 +69,7 @@ fn download_css(force: bool) -> Result<()> {
"@blueprintjs/docs-theme", "@blueprintjs/docs-theme",
"3.11.1", "3.11.1",
Path::new("package/lib/css/docs-theme.css"), 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")?; .context("while downloading CSS of @blueprintjs/docs-theme")?;
log::info!("Docs Theme CSS updated to: {}", version); log::info!("Docs Theme CSS updated to: {}", version);

View file

@ -30,7 +30,6 @@ yew-router = "0.17"
yewprint = { path = ".." } yewprint = { path = ".." }
[build-dependencies] [build-dependencies]
build-data = "0.1.3"
syntect = "0.5.0" syntect = "0.5.0"
[dev-dependencies] [dev-dependencies]

View file

@ -10,8 +10,6 @@ fn main() {
let theme_set = ThemeSet::load_defaults(); let theme_set = ThemeSet::load_defaults();
let out_dir = env::var_os("OUT_DIR").unwrap(); let out_dir = env::var_os("OUT_DIR").unwrap();
build_data::set_GIT_BRANCH();
fn recursive<P: AsRef<Path>>( fn recursive<P: AsRef<Path>>(
base_path: P, base_path: P,
syntax_set: &SyntaxSet, syntax_set: &SyntaxSet,

View file

@ -11,6 +11,7 @@ use crate::icon::*;
use crate::input_group::*; use crate::input_group::*;
use crate::menu::*; use crate::menu::*;
use crate::numeric_input::*; use crate::numeric_input::*;
use crate::overlay::*;
use crate::panel_stack::*; use crate::panel_stack::*;
use crate::progress_bar::*; use crate::progress_bar::*;
use crate::radio::*; use crate::radio::*;
@ -22,6 +23,7 @@ use crate::tag::*;
use crate::text::*; use crate::text::*;
use crate::text_area::*; use crate::text_area::*;
use crate::tree::*; use crate::tree::*;
use crate::DARK;
use yew::prelude::*; use yew::prelude::*;
use yew_router::prelude::*; use yew_router::prelude::*;
use yewprint::{Icon, Menu, MenuItem}; use yewprint::{Icon, Menu, MenuItem};
@ -35,9 +37,7 @@ pub fn app_root() -> Html {
} }
} }
pub struct App { pub struct App;
dark_theme: bool,
}
pub enum Msg { pub enum Msg {
ToggleLight, ToggleLight,
@ -49,17 +49,14 @@ impl Component for App {
type Properties = (); type Properties = ();
fn create(_ctx: &Context<Self>) -> Self { fn create(_ctx: &Context<Self>) -> Self {
App { Self
dark_theme: web_sys::window()
.and_then(|x| x.match_media("(prefers-color-scheme: dark)").ok().flatten())
.map(|x| x.matches())
.unwrap_or(true),
}
} }
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg { match msg {
Msg::ToggleLight => self.dark_theme ^= true, Msg::ToggleLight => {
DARK.with(|x| x.replace(!*x.borrow()));
}
Msg::GoToMenu(event, doc_menu) => { Msg::GoToMenu(event, doc_menu) => {
event.prevent_default(); event.prevent_default();
if let Some(navigator) = ctx.link().navigator() { if let Some(navigator) = ctx.link().navigator() {
@ -73,21 +70,15 @@ impl Component for App {
} }
fn view(&self, ctx: &Context<Self>) -> Html { 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" "https://www.netlify.com/img/global/badges/netlify-color-accent.svg"
} else { } else {
"https://www.netlify.com/img/global/badges/netlify-color-bg.svg" "https://www.netlify.com/img/global/badges/netlify-color-bg.svg"
}; };
let go_to_theme_label = if self.dark_theme { let go_to_theme_label = if dark { "Light theme" } else { "Dark theme" };
"Light theme" let go_to_theme_icon = if dark { Icon::Flash } else { Icon::Moon };
} else {
"Dark theme"
};
let go_to_theme_icon = if self.dark_theme {
Icon::Flash
} else {
Icon::Moon
};
let menu = html! { let menu = html! {
<Menu> <Menu>
@ -175,6 +166,12 @@ impl Component for App {
onclick={ctx.link() onclick={ctx.link()
.callback(|e| Msg::GoToMenu(e, DocMenu::NumericInput))} .callback(|e| Msg::GoToMenu(e, DocMenu::NumericInput))}
/> />
<MenuItem
text={html!("Overlay")}
href="/overlay"
onclick={ctx.link()
.callback(|e| Msg::GoToMenu(e, DocMenu::Overlay))}
/>
<MenuItem <MenuItem
text={html!("PanelStack")} text={html!("PanelStack")}
href="/panel-stack" href="/panel-stack"
@ -279,7 +276,7 @@ impl Component for App {
}; };
html! { 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")}> <div class={classes!("docs-app")}>
{{ navigation }} {{ navigation }}
<main class={classes!("docs-content-wrapper")} role="main"> <main class={classes!("docs-content-wrapper")} role="main">
@ -308,6 +305,7 @@ fn switch(route: DocMenu) -> Html {
DocMenu::InputGroup => html!(<InputGroupDoc />), DocMenu::InputGroup => html!(<InputGroupDoc />),
DocMenu::Menu => html!(<MenuDoc />), DocMenu::Menu => html!(<MenuDoc />),
DocMenu::NumericInput => html!(<NumericInputDoc />), DocMenu::NumericInput => html!(<NumericInputDoc />),
DocMenu::Overlay => html!(<OverlayDoc />),
DocMenu::PanelStack => html!(<PanelStackDoc />), DocMenu::PanelStack => html!(<PanelStackDoc />),
DocMenu::ProgressBar => html!(<ProgressBarDoc />), DocMenu::ProgressBar => html!(<ProgressBarDoc />),
DocMenu::Radio => html!(<RadioDoc />), DocMenu::Radio => html!(<RadioDoc />),
@ -350,6 +348,8 @@ pub enum DocMenu {
Menu, Menu,
#[at("/numeric-input")] #[at("/numeric-input")]
NumericInput, NumericInput,
#[at("/overlay")]
Overlay,
#[at("/panel-stack")] #[at("/panel-stack")]
PanelStack, PanelStack,
#[at("/progress-bar")] #[at("/progress-bar")]

View file

@ -23,6 +23,7 @@ mod input_group;
mod logo; mod logo;
mod menu; mod menu;
mod numeric_input; mod numeric_input;
mod overlay;
mod panel_stack; mod panel_stack;
mod progress_bar; mod progress_bar;
mod radio; mod radio;
@ -38,6 +39,16 @@ mod tree;
pub use app::*; pub use app::*;
pub use example::*; pub use example::*;
pub use logo::*; 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_export]
macro_rules! include_raw_html { macro_rules! include_raw_html {
@ -67,7 +78,7 @@ macro_rules! build_source_code_component {
pub fn generate_url() -> String { pub fn generate_url() -> String {
use std::path::Path; use std::path::Path;
let component_name = Path::new(file!()) let component = Path::new(file!())
.parent() .parent()
.unwrap() .unwrap()
.file_name() .file_name()
@ -75,10 +86,13 @@ macro_rules! build_source_code_component {
.to_str() .to_str()
.unwrap(); .unwrap();
format!( if let (Some(actor), Some(branch)) =
"https://github.com/yewprint/yewprint/blob/HEAD/src/{}.rs", (option_env!("GITHUB_ACTOR"), option_env!("GITHUB_HEAD_REF"))
component_name, {
) format!("https://github.com/{actor}/yewprint/blob/{branch}/src/{component}.rs")
} else {
format!("https://github.com/yewprint/yewprint/blob/HEAD/src/{component}.rs")
}
} }
} }

View 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>
}
}
}

View 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!();

View file

@ -120,23 +120,27 @@
padding-top: 2px; padding-top: 2px;
} }
.docs-icon-search { .docs-icon-search {
padding-bottom: 20px; padding-bottom: 20px;
} }
.docs-icon-list { .docs-icon-list {
display: grid; display: grid;
grid-template-columns: auto auto auto auto; grid-template-columns: auto auto auto auto;
row-gap: 40px; row-gap: 40px;
column-gap: 20px; column-gap: 20px;
} }
.docs-icon-list-item { .docs-icon-list-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 3px; gap: 3px;
} }
.docs-overlay-example-tall {
height: 200%;
}
</style> </style>
</head> </head>