mirror of
https://github.com/penpot/penpot.git
synced 2025-07-13 23:37:16 +02:00
269 lines
9.7 KiB
Clojure
269 lines
9.7 KiB
Clojure
;; 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.tooltip
|
|
(:require-macros
|
|
[app.common.data.macros :as dm]
|
|
[app.main.style :as stl])
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.util.dom :as dom]
|
|
[app.util.keyboard :as kbd]
|
|
[app.util.timers :as ts]
|
|
[rumext.v2 :as mf]))
|
|
|
|
(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 "block-size")
|
|
(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}
|
|
origin-brect
|
|
|
|
{tooltip-width :width
|
|
tooltip-height :height}
|
|
tooltip-brect
|
|
|
|
offset (d/nilv offset 2)]
|
|
|
|
(case placement
|
|
"bottom"
|
|
{:top (+ trigger-bottom offset)
|
|
:left (- (+ trigger-left (/ trigger-width 2)) (/ tooltip-width 2))
|
|
:right (+ trigger-left (/ trigger-width 2) (/ tooltip-width 2))
|
|
:bottom (+ (- trigger-bottom offset) tooltip-height)
|
|
:width tooltip-width
|
|
:height tooltip-height}
|
|
|
|
"left"
|
|
{:top (- (+ trigger-top (/ trigger-height 2) half-arrow-height) (/ tooltip-height 2))
|
|
:left (- trigger-left tooltip-width arrow-height)
|
|
:right (+ (- trigger-left tooltip-width) tooltip-width)
|
|
:bottom (+ (- (+ trigger-top (/ trigger-height 2) half-arrow-height) (/ tooltip-height 2)) tooltip-height)
|
|
:width tooltip-width
|
|
:height tooltip-height}
|
|
|
|
"right"
|
|
{:top (- (+ trigger-top (/ trigger-height 2) half-arrow-height) (/ tooltip-height 2))
|
|
:left (+ trigger-right offset)
|
|
:right (+ trigger-right offset tooltip-width)
|
|
:bottom (+ (- (+ trigger-top (/ trigger-height 2) half-arrow-height) (/ tooltip-height 2)) tooltip-height)
|
|
:width tooltip-width
|
|
:height tooltip-height}
|
|
|
|
"bottom-right"
|
|
{:top (+ trigger-bottom offset)
|
|
:left (- trigger-right overlay-offset)
|
|
:right (+ (- trigger-right overlay-offset) tooltip-width)
|
|
:bottom (+ (- trigger-bottom offset) tooltip-height)
|
|
:width tooltip-width
|
|
:height tooltip-height}
|
|
|
|
"bottom-left"
|
|
{:top (+ trigger-bottom offset)
|
|
:left (+ (- trigger-left tooltip-width) overlay-offset)
|
|
:right (+ trigger-left overlay-offset)
|
|
:bottom (+ (- trigger-bottom offset) tooltip-height)
|
|
:width tooltip-width
|
|
:height tooltip-height}
|
|
|
|
"top-right"
|
|
{:top (- trigger-top offset tooltip-height)
|
|
:left (- trigger-right overlay-offset)
|
|
:right (+ (- trigger-right overlay-offset) tooltip-width)
|
|
:bottom (- trigger-top offset)
|
|
:width tooltip-width
|
|
:height tooltip-height}
|
|
|
|
"top-left"
|
|
{:top (- trigger-top offset tooltip-height)
|
|
:left (+ (- trigger-left tooltip-width) overlay-offset)
|
|
:right (+ trigger-left overlay-offset)
|
|
:bottom (- trigger-top offset)
|
|
:width tooltip-width
|
|
:height tooltip-height}
|
|
|
|
{:top (- trigger-top offset tooltip-height)
|
|
:left (- (+ trigger-left (/ trigger-width 2)) (/ tooltip-width 2))
|
|
:right (+ (- (+ trigger-left (/ trigger-width 2)) (/ tooltip-width 2)) tooltip-width)
|
|
:bottom (- trigger-top offset)
|
|
:width tooltip-width
|
|
:height tooltip-height})))
|
|
|
|
(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"]
|
|
"left" ["left" "top" "right" "bottom" "top-left" "top-right" "bottom-right" "bottom-left"]
|
|
"right" ["right" "bottom" "left" "top" "bottom-left" "top-left" "top-right" "bottom-right"]
|
|
"top-right" ["top-right" "right" "bottom" "left" "top" "bottom-right" "bottom-left" "top-left"]
|
|
"bottom-right" ["bottom-right" "bottom" "left" "top" "right" "bottom-left" "top-left" "top-right"]
|
|
"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 content placement offset delay] :rest props}]
|
|
(let [internal-id
|
|
(mf/use-id)
|
|
|
|
id
|
|
(d/nilv id internal-id)
|
|
|
|
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 offset)
|
|
(fn [event]
|
|
(clear-schedule schedule-ref)
|
|
(when-let [tooltip (dom/get-element id)]
|
|
(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)]
|
|
(clear-schedule schedule-ref)
|
|
(hide-popover tooltip))))
|
|
|
|
handle-key-down
|
|
(mf/use-fn
|
|
(mf/deps on-hide)
|
|
(fn [event]
|
|
(when (kbd/esc? event)
|
|
(on-hide))))
|
|
|
|
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)]
|
|
|
|
[:> :div props
|
|
children
|
|
[:div {:class [class tooltip-class]
|
|
:id id
|
|
:popover "auto"
|
|
:role "tooltip"}
|
|
[:div {:class (stl/css :tooltip-content)} content]
|
|
[:div {:class (stl/css :tooltip-arrow)
|
|
:id "tooltip-arrow"}]]]))
|