diff --git a/frontend/src/app/main/ui/ds/buttons/button.cljs b/frontend/src/app/main/ui/ds/buttons/button.cljs
index 37adcd41da..376d063fc7 100644
--- a/frontend/src/app/main/ui/ds/buttons/button.cljs
+++ b/frontend/src/app/main/ui/ds/buttons/button.cljs
@@ -36,4 +36,4 @@
(on-ref node)))})]
[:> "button" props
(when icon [:> icon* {:icon-id icon :size "m"}])
- [:span {:class (stl/css :label-wrapper)} children]]))
\ No newline at end of file
+ [:span {:class (stl/css :label-wrapper)} children]]))
diff --git a/frontend/src/app/main/ui/ds/buttons/icon_button.cljs b/frontend/src/app/main/ui/ds/buttons/icon_button.cljs
index 33ee369523..1d2995ea8e 100644
--- a/frontend/src/app/main/ui/ds/buttons/icon_button.cljs
+++ b/frontend/src/app/main/ui/ds/buttons/icon_button.cljs
@@ -6,11 +6,11 @@
(ns app.main.ui.ds.buttons.icon-button
(:require-macros
- [app.common.data.macros :as dm]
[app.main.style :as stl])
(:require
+ [app.common.data :as d]
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]]
- [app.main.ui.ds.tooltip.tooltip :refer [tooltip*]]
+ [app.main.ui.ds.tooltip :refer [tooltip*]]
[rumext.v2 :as mf]))
(def ^:private schema:icon-button
@@ -24,18 +24,30 @@
[:maybe [:enum "primary" "secondary" "ghost" "destructive" "action"]]]])
(mf/defc icon-button*
- {::mf/schema schema:icon-button}
+ {::mf/schema schema:icon-button
+ ::mf/memo true}
[{:keys [class icon icon-class variant aria-label children] :rest props}]
- (let [variant (or variant "primary")
- tooltip-id (mf/use-id)
- class (dm/str class " " (stl/css-case :icon-button true
- :icon-button-primary (= variant "primary")
- :icon-button-secondary (= variant "secondary")
- :icon-button-ghost (= variant "ghost")
- :icon-button-action (= variant "action")
- :icon-button-destructive (= variant "destructive")))
- props (mf/spread-props props {:class class
- :aria-labelledby tooltip-id})]
- [:> tooltip* {:tooltip-content aria-label
+ (let [variant
+ (d/nilv variant "primary")
+
+ tooltip-id
+ (mf/use-id)
+
+ button-class
+ (stl/css-case :icon-button true
+ :icon-button-primary (identical? variant "primary")
+ :icon-button-secondary (identical? variant "secondary")
+ :icon-button-ghost (identical? variant "ghost")
+ :icon-button-action (identical? variant "action")
+ :icon-button-destructive (identical? variant "destructive"))
+
+ props
+ (mf/spread-props props
+ {:class [class button-class]
+ :aria-labelledby tooltip-id})]
+
+ [:> tooltip* {:content aria-label
:id tooltip-id}
- [:> "button" props [:> icon* {:icon-id icon :aria-hidden true :class icon-class}] children]]))
\ No newline at end of file
+ [:> :button props
+ [:> icon* {:icon-id icon :aria-hidden true :class icon-class}]
+ children]]))
diff --git a/frontend/src/app/main/ui/ds/tooltip.cljs b/frontend/src/app/main/ui/ds/tooltip.cljs
new file mode 100644
index 0000000000..2cd68ec5d6
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/tooltip.cljs
@@ -0,0 +1,12 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) KALEIDOS INC
+
+(ns app.main.ui.ds.tooltip
+ (:require
+ [app.common.data.macros :as dm]
+ [app.main.ui.ds.tooltip.tooltip :as impl]))
+
+(dm/export impl/tooltip*)
diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs
index c69c2609e5..d43a9d20c8 100644
--- a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs
+++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs
@@ -15,21 +15,48 @@
[app.util.timers :as ts]
[rumext.v2 :as mf]))
-(defn- calculate-tooltip-rect [tooltip trigger-rect placement offset]
+(def ^:private ^:const arrow-height 12)
+(def ^:private ^:const half-arrow-height (/ arrow-height 2))
+(def ^:private ^:const overlay-offset 32)
+
+(defn- clear-schedule
+ [ref]
+ (when-let [schedule (mf/ref-val ref)]
+ (ts/dispose! schedule)
+ (mf/set-ref-val! ref nil)))
+
+(defn- add-schedule
+ [ref delay f]
+ (mf/set-ref-val! ref (ts/schedule delay f)))
+
+(defn- show-popover
+ [node]
+ (when (.-isConnected ^js node)
+ (.showPopover ^js node)))
+
+(defn- hide-popover
+ [node]
+ (dom/unset-css-property! node "display")
+ (.hidePopover ^js node))
+
+(defn- calculate-placement-bounding-rect
+ "Given a placement, calcultates the bounding rect for it taking in
+ account provided tooltip bounding rect and the origin bounding
+ rect."
+ [placement tooltip-brect origin-brect offset]
(let [{trigger-top :top
trigger-left :left
trigger-right :right
trigger-bottom :bottom
trigger-width :width
- trigger-height :height} trigger-rect
+ trigger-height :height}
+ origin-brect
{tooltip-width :width
- tooltip-height :height} (dom/get-bounding-rect tooltip)
+ tooltip-height :height}
+ tooltip-brect
- offset (d/nilv offset 2)
- arrow-height 12
- half-arrow-height (/ arrow-height 2)
- overlay-offset 32]
+ offset (d/nilv offset 2)]
(case placement
"bottom"
@@ -95,7 +122,10 @@
:width tooltip-width
:height tooltip-height})))
-(defn- get-fallback-order [placement]
+(defn- get-fallback-order
+ "Get a vector of placement followed with ordered fallback pacements
+ for the specified placement"
+ [placement]
(case placement
"top" ["top" "right" "bottom" "left" "top-right" "bottom-right" "bottom-left" "top-left"]
"bottom" ["bottom" "left" "top" "right" "bottom-right" "bottom-left" "top-left" "top-right"]
@@ -106,65 +136,93 @@
"bottom-left" ["bottom-left" "left" "top" "right" "bottom" "top-left" "top-right" "bottom-right"]
"top-left" ["top-left" "top" "right" "bottom" "left" "bottom-left" "top-right" "bottom-right"]))
+(defn- find-matching-placement
+ "Algorithm for find a correct placement and placement-brect for the
+ provided placement, if the current placement does not matches, it
+ uses the predefined fallbacks. Returns an array of matched placement
+ and its bounding rect."
+ [placement tooltip-brect origin-brect window-size offset]
+ (loop [placements (seq (get-fallback-order placement))]
+ (when-let [placement (first placements)]
+ (let [placement-brect (calculate-placement-bounding-rect placement tooltip-brect origin-brect offset)]
+ (if (dom/is-bounding-rect-outside? placement-brect window-size)
+ (recur (rest placements))
+ #js [placement placement-brect])))))
+
+(defn- update-tooltip-position
+ "Update the tooltip position having in account the current window
+ size, placement. It calculates the appropriate placement and updates
+ the dom with the result."
+ [tooltip placement origin-brect offset]
+ (show-popover tooltip)
+ (let [tooltip-brect (dom/get-bounding-rect tooltip)
+ window-size (dom/get-window-size)]
+ (when-let [[placement placement-rect] (find-matching-placement placement tooltip-brect origin-brect window-size offset)]
+
+ (let [height (if (or (= placement "right") (= placement "left"))
+ (- (:height placement-rect) arrow-height)
+ (:height placement-rect))]
+ (dom/set-css-property! tooltip "display" "grid")
+ (dom/set-css-property! tooltip "block-size" (dm/str height "px"))
+ (dom/set-css-property! tooltip "inset-block-start" (dm/str (:top placement-rect) "px"))
+ (dom/set-css-property! tooltip "inset-inline-start" (dm/str (:left placement-rect) "px")))
+ placement)))
+
(def ^:private schema:tooltip
[:map
[:class {:optional true} :string]
[:id {:optional true} :string]
[:offset {:optional true} :int]
[:delay {:optional true} :int]
+ [:content [:or fn? :string [:fn mf/element?]]]
[:placement {:optional true}
[:maybe [:enum "top" "bottom" "left" "right" "top-right" "bottom-right" "bottom-left" "top-left"]]]])
(mf/defc tooltip*
{::mf/schema schema:tooltip}
- [{:keys [class id children tooltip-content placement offset delay] :rest props}]
- (let [id (or id (mf/use-id))
- placement* (mf/use-state #(d/nilv placement "top"))
- placement (deref placement*)
- delay (d/nilv delay 300)
+ [{:keys [class id children content placement offset delay] :rest props}]
+ (let [internal-id
+ (mf/use-id)
- schedule-ref (mf/use-ref nil)
+ id
+ (d/nilv id internal-id)
- position-tooltip
- (fn [^js tooltip trigger-rect]
- (let [all-placements (get-fallback-order placement)]
- (when (.-isConnected tooltip)
- (.showPopover ^js tooltip))
- (loop [[current-placement & remaining-placements] all-placements]
- (when current-placement
- (reset! placement* current-placement)
- (let [tooltip-rect (calculate-tooltip-rect tooltip trigger-rect current-placement offset)]
- (if (dom/is-bounding-rect-outside? tooltip-rect)
- (recur remaining-placements)
- (do (dom/set-css-property! tooltip "display" "grid")
- (dom/set-css-property! tooltip "inset-block-start" (dm/str (:top tooltip-rect) "px"))
- (dom/set-css-property! tooltip "inset-inline-start" (dm/str (:left tooltip-rect) "px")))))))))
+ placement*
+ (mf/use-state #(d/nilv placement "top"))
+
+ placement
+ (deref placement*)
+
+ delay
+ (d/nilv delay 300)
+
+ schedule-ref
+ (mf/use-ref nil)
on-show
(mf/use-fn
- (mf/deps id placement)
+ (mf/deps id placement offset)
(fn [event]
- (when-let [schedule (mf/ref-val schedule-ref)]
- (ts/dispose! schedule)
- (mf/set-ref-val! schedule-ref nil))
+ (clear-schedule schedule-ref)
(when-let [tooltip (dom/get-element id)]
- (let [trigger-rect (->> (dom/get-target event)
- (dom/get-bounding-rect))]
- (mf/set-ref-val!
- schedule-ref
- (ts/schedule
- delay
- #(position-tooltip tooltip trigger-rect)))))))
+ (let [origin-brect
+ (->> (dom/get-target event)
+ (dom/get-bounding-rect))
+
+ update-position
+ (fn []
+ (let [placement (update-tooltip-position tooltip placement origin-brect offset)]
+ (reset! placement* placement)))]
+
+ (add-schedule schedule-ref delay update-position)))))
on-hide
(mf/use-fn
(mf/deps id)
- (fn [] (when-let [tooltip (dom/get-element id)]
- (when-let [schedule (mf/ref-val schedule-ref)]
- (ts/dispose! schedule)
- (mf/set-ref-val! schedule-ref nil))
- (dom/unset-css-property! tooltip "display")
- (.hidePopover ^js tooltip))))
+ (fn []
+ (when-let [tooltip (dom/get-element id)]
+ (clear-schedule schedule-ref)
+ (hide-popover tooltip))))
handle-key-down
(mf/use-fn
@@ -173,33 +231,38 @@
(when (kbd/esc? event)
(on-hide))))
- class (d/append-class class (stl/css-case
- :tooltip true
- :tooltip-top (= placement "top")
- :tooltip-bottom (= placement "bottom")
- :tooltip-left (= placement "left")
- :tooltip-right (= placement "right")
- :tooltip-top-right (= placement "top-right")
- :tooltip-bottom-right (= placement "bottom-right")
- :tooltip-bottom-left (= placement "bottom-left")
- :tooltip-top-left (= placement "top-left")))
+ tooltip-class
+ (stl/css-case
+ :tooltip true
+ :tooltip-top (identical? placement "top")
+ :tooltip-bottom (identical? placement "bottom")
+ :tooltip-left (identical? placement "left")
+ :tooltip-right (identical? placement "right")
+ :tooltip-top-right (identical? placement "top-right")
+ :tooltip-bottom-right (identical? placement "bottom-right")
+ :tooltip-bottom-left (identical? placement "bottom-left")
+ :tooltip-top-left (identical? placement "top-left"))
+
+ props
+ (mf/spread-props props
+ {:on-mouse-enter on-show
+ :on-mouse-leave on-hide
+ :on-focus on-show
+ :on-blur on-hide
+ :on-key-down handle-key-down
+ :class (stl/css :tooltip-trigger)
+ :aria-describedby id})
+ content
+ (if (fn? content)
+ (content)
+ content)]
- props (mf/spread-props props {:on-mouse-enter on-show
- :on-mouse-leave on-hide
- :on-focus on-show
- :on-blur on-hide
- :on-key-down handle-key-down
- :class (stl/css :tooltip-trigger)
- :aria-describedby id})]
[:> :div props
children
- [:div {:class class
+ [:div {:class [class tooltip-class]
:id id
:popover "auto"
:role "tooltip"}
- [:div {:class (stl/css :tooltip-content)}
- (if (fn? tooltip-content)
- (tooltip-content)
- tooltip-content)]
+ [:div {:class (stl/css :tooltip-content)} content]
[:div {:class (stl/css :tooltip-arrow)
- :id "tooltip-arrow"}]]]))
\ No newline at end of file
+ :id "tooltip-arrow"}]]]))
diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.mdx b/frontend/src/app/main/ui/ds/tooltip/tooltip.mdx
index 1dfb2945f0..dfae36dba7 100644
--- a/frontend/src/app/main/ui/ds/tooltip/tooltip.mdx
+++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.mdx
@@ -32,7 +32,7 @@ If `placement` is not provided, the tooltip will default to "top".
[:> tlp/tooltip* {:id "test-tooltip"
:placement "bottom"
- :tooltip-content "Tooltip content"}
+ :content "Tooltip content"}
[:div "Trigger component"]])
```
@@ -44,8 +44,7 @@ Tooltip content can include HTML elements:
[:> tlp/tooltip* {:id "test-tooltip"
:placement "bottom"
- :tooltip-content (mf/html
- [:span "Tooltip content"])}
+ :content (mf/html [:span "Tooltip content"])}
[:div "Trigger component"]])
```
diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.scss b/frontend/src/app/main/ui/ds/tooltip/tooltip.scss
index cb2ce09c2b..ae97c8b17b 100644
--- a/frontend/src/app/main/ui/ds/tooltip/tooltip.scss
+++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.scss
@@ -16,6 +16,7 @@ $arrow-side: 12px;
background-color: transparent;
overflow: hidden;
inline-size: fit-content;
+ block-size: fit-content;
}
.tooltip-arrow {
@@ -145,6 +146,7 @@ $arrow-side: 12px;
border: $b-1 solid var(--color-accent-primary-muted);
padding: var(--sp-s) var(--sp-m);
grid-area: content;
+ block-size: fit-content;
}
.tooltip-trigger {
diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.stories.jsx b/frontend/src/app/main/ui/ds/tooltip/tooltip.stories.jsx
index 985ad19895..0f2398edc0 100644
--- a/frontend/src/app/main/ui/ds/tooltip/tooltip.stories.jsx
+++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.stories.jsx
@@ -37,10 +37,10 @@ export default {
},
args: {
children: (
-
+
),
id: "popover-example",
- tooltipContent: "This is the tooltip content",
+ content: "This is the tooltip content",
delay: 300,
},
render: ({ children, ...args }) => (
@@ -74,18 +74,18 @@ export const Corners = {
>
-
+