Add internal performance oriented changes to tooltip*

This commit is contained in:
Andrey Antukh 2025-06-09 16:36:07 +02:00
parent fc655224af
commit f7e94accc3
7 changed files with 180 additions and 113 deletions

View file

@ -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,88 @@
"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)]
(dom/set-css-property! tooltip "display" "grid")
(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 +226,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"}]]]))
:id "tooltip-arrow"}]]]))