From 4c7867fb6250a85556b0828c08270ca4d98e6132 Mon Sep 17 00:00:00 2001 From: molimauro <50131258+molimauro@users.noreply.github.com> Date: Tue, 27 Dec 2022 20:33:27 +0100 Subject: [PATCH] feat(ui): outside click (#648) Co-authored-by: Darius Clark --- extensions/native/audio-factory/src/lib.rs | 17 ++--- extensions/native/emoji-selector/src/lib.rs | 85 ++++++++++----------- src/ui_kit/src/lib.rs | 1 + src/ui_kit/src/outside/mod.rs | 28 +++++++ src/ui_kit/src/outside/outside.js | 53 +++++++++++++ 5 files changed, 130 insertions(+), 54 deletions(-) create mode 100644 src/ui_kit/src/outside/mod.rs create mode 100644 src/ui_kit/src/outside/outside.js diff --git a/extensions/native/audio-factory/src/lib.rs b/extensions/native/audio-factory/src/lib.rs index 17d1a4b8..ded8d431 100644 --- a/extensions/native/audio-factory/src/lib.rs +++ b/extensions/native/audio-factory/src/lib.rs @@ -5,6 +5,7 @@ use sir::css; use ui_kit::{ button::{self, Button}, + outside::OutsideClick, select::*, switch::Switch, }; @@ -189,6 +190,7 @@ pub fn ExtAudioFactory(cx: Scope) -> Element { cx.render(rsx! { div { + id: "ext-audio-factory", class: "{styles}", div { class: "row", @@ -405,25 +407,18 @@ impl BasicExtension for AudioFactory { ); // TODO: Icon should be a record icon, it should turn red and become a ovular shape like a normal button which includes the duration of the recording and turns the icon red - let factory_visible = use_state(&cx, || false); - cx.render(rsx! { div { id: "audio-factory", class: "{styles}", - (factory_visible).then(|| rsx! { + OutsideClick { ExtAudioFactory { debug: false } - }), - Button { - icon: Shape::ViewfinderCircle, - state: if **factory_visible { - button::State::Primary - } else { - button::State::Secondary + Button { + icon: Shape::ViewfinderCircle, + on_pressed: move |_| {} } - on_pressed: move |_| factory_visible.set(!factory_visible) } } }) diff --git a/extensions/native/emoji-selector/src/lib.rs b/extensions/native/emoji-selector/src/lib.rs index 28ef89e8..4ae8ab48 100644 --- a/extensions/native/emoji-selector/src/lib.rs +++ b/extensions/native/emoji-selector/src/lib.rs @@ -2,7 +2,10 @@ use dioxus::prelude::*; use dioxus_heroicons::outline::Shape; use emojis::{Group, UnicodeVersion}; use sir::css; -use ui_kit::button::{self, Button}; +use ui_kit::{ + button::{self, Button}, + outside::OutsideClick, +}; use utils::extensions::{BasicExtension, ExtensionInfo, ExtensionType}; static MAX_UNICODE_VER: UnicodeVersion = UnicodeVersion::new(11, 0); @@ -72,7 +75,6 @@ impl BasicExtension for EmojiSelector { " ); - let is_opened = use_state(&cx, || false); let eval = use_eval(&cx); let insert = move |val: &str| { eval(format!( @@ -86,50 +88,47 @@ impl BasicExtension for EmojiSelector { let groups = Group::iter(); cx.render(rsx! { - div { - class: "ext-emoji-selector", - (is_opened).then(|| rsx! { - div { - onblur: |_| println!("blur"), - class: "{styles}", - groups.map(|group| { - let name = get_group_name(group); - rsx!( - div { - class: "category", + div { + class: "ext-emoji-selector", + rsx! { + OutsideClick { + div { + onblur: |_| println!("blur"), + class: "{styles}", + groups.map(|group| { + let name = get_group_name(group); + rsx!( div { - class: "name", - label { "{name}" } - }, - div { - class: "items", - group.emojis() - .filter(|v| v.unicode_version() <= MAX_UNICODE_VER) - .map(|v| { - let name = v.name(); - let emoji = v.as_str(); - rsx!(button { - onclick: move |_| insert(emoji), - class: "item", - title: "{name}", - "{v}" + class: "category", + div { + class: "name", + label { "{name}" } + }, + div { + class: "items", + group.emojis() + .filter(|v| v.unicode_version() <= MAX_UNICODE_VER) + .map(|v| { + let name = v.name(); + let emoji = v.as_str(); + rsx!(button { + onclick: move |_| insert(emoji), + class: "item", + title: "{name}", + "{v}" + }) }) - }) - }, - } - ) - }) - } - }) - Button { - icon: Shape::FaceSmile, - state: if **is_opened { - button::State::Primary - } else { - button::State::Secondary + }, + } + ) + }) + } + Button { + icon: Shape::FaceSmile, + on_pressed: move |_| {} + } } - on_pressed: move |_| is_opened.set(!is_opened) - } + } } }) } diff --git a/src/ui_kit/src/lib.rs b/src/ui_kit/src/lib.rs index fcc792a2..ce35510c 100644 --- a/src/ui_kit/src/lib.rs +++ b/src/ui_kit/src/lib.rs @@ -10,6 +10,7 @@ pub mod input_add_friend; pub mod loader; pub mod new_folder; pub mod numeric_indicator; +pub mod outside; pub mod photo_picker; pub mod pin; pub mod popup; diff --git a/src/ui_kit/src/outside/mod.rs b/src/ui_kit/src/outside/mod.rs new file mode 100644 index 00000000..e9420328 --- /dev/null +++ b/src/ui_kit/src/outside/mod.rs @@ -0,0 +1,28 @@ +use dioxus::prelude::*; +use warp::crypto::rand::{self, distributions::Alphanumeric, Rng}; + +#[derive(Props)] +pub struct Props<'a> { + children: Element<'a>, +} + +#[allow(non_snake_case)] +pub fn OutsideClick<'a>(cx: Scope<'a, Props<'a>>) -> Element<'a> { + let rand_s: &String = cx.use_hook(|_| { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(7) + .map(char::from) + .collect() + }); + + let script = include_str!("outside.js").replace("ID", rand_s); + + cx.render(rsx! { + span { + id: "outside-container-{rand_s}", + &cx.props.children, + script { "{script}" }, + } + }) +} diff --git a/src/ui_kit/src/outside/outside.js b/src/ui_kit/src/outside/outside.js new file mode 100644 index 00000000..74ff3418 --- /dev/null +++ b/src/ui_kit/src/outside/outside.js @@ -0,0 +1,53 @@ +function hideOnClickOutside(element, trigger) { + const outsideClickListener = (event) => { + if ( + !element.contains(event.target) && + !trigger.contains(event.target) && + isVisible(element) + ) { + element.style.display = "none" + trigger.style.backgroundColor = "var(--theme-secondary)" + removeClickListener() + } + } + + const removeClickListener = () => { + document.removeEventListener("click", outsideClickListener) + } + + document.addEventListener("click", outsideClickListener) +} + +function isVisible(elem) { + return ( + !!elem && + !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length) + ) +} + +function initClick() { + const container = document.getElementById("outside-container-ID") + if (container.children.length < 2) { + throw new Error( + "Outside click needs a container (the hiding container) as first child and a trigger button as second child", + ) + } + + const el = container.firstChild + el.style.display = "none" + + const trigger = container.children[1] + trigger.style.backgroundColor = "var(--theme-secondary)" + + trigger.addEventListener("click", () => { + el.style.display = el.style.display === "none" ? "" : "none" + trigger.style.backgroundColor = + el.style.display === "none" + ? "var(--theme-secondary)" + : "var(--theme-primary)" + + hideOnClickOutside(el, trigger) + }) +} + +initClick()