From fc655224afb72fc0c4e222c7da07e8185488e4b5 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 9 Jun 2025 16:17:10 +0200 Subject: [PATCH 1/3] :zap: Add memorization to icon-button* ds component --- .../src/app/main/ui/ds/buttons/button.cljs | 2 +- .../app/main/ui/ds/buttons/icon_button.cljs | 38 +++++++++++++------ 2 files changed, 27 insertions(+), 13 deletions(-) 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..43d2c9ed09 100644 --- a/frontend/src/app/main/ui/ds/buttons/icon_button.cljs +++ b/frontend/src/app/main/ui/ds/buttons/icon_button.cljs @@ -9,6 +9,7 @@ [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*]] [rumext.v2 :as mf])) @@ -24,18 +25,31 @@ [: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})] + (let [variant + (d/nilv 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 :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]])) From f7e94accc3dea3007534e9c299b12238e51c4898 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 9 Jun 2025 16:36:07 +0200 Subject: [PATCH 2/3] :sparkles: Add internal performance oriented changes to tooltip* --- .../app/main/ui/ds/buttons/icon_button.cljs | 24 +-- frontend/src/app/main/ui/ds/tooltip.cljs | 12 ++ .../src/app/main/ui/ds/tooltip/tooltip.cljs | 194 ++++++++++++------ .../src/app/main/ui/ds/tooltip/tooltip.mdx | 5 +- .../main/ui/ds/tooltip/tooltip.stories.jsx | 40 ++-- frontend/src/app/util/dom.cljs | 15 +- frontend/src/app/util/extends.cljs | 3 +- 7 files changed, 180 insertions(+), 113 deletions(-) create mode 100644 frontend/src/app/main/ui/ds/tooltip.cljs 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 43d2c9ed09..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,12 +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 @@ -34,22 +33,21 @@ 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"))) + 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 + {:class [class button-class] :aria-labelledby tooltip-id})] - [:> tooltip* {:tooltip-content aria-label + [:> tooltip* {:content aria-label :id tooltip-id} - [:> "button" props + [:> :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..9177fffd97 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,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"}]]])) \ 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.stories.jsx b/frontend/src/app/main/ui/ds/tooltip/tooltip.stories.jsx index 985ad19895..cb43587f7a 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 = { > - + + - + - + - + - + - + - + ), diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index b629631de1..c674698a83 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -421,17 +421,16 @@ :height (.-height ^js rect)})) (defn is-bounding-rect-outside? - [rect] - (let [{:keys [left top right bottom]} rect - {:keys [width height]} (get-window-size)] - (or (< left 0) - (< top 0) - (> right width) - (> bottom height)))) + [{:keys [left top right bottom]} {:keys [width height]}] + (or (< left 0) + (< top 0) + (> right width) + (> bottom height))) (defn is-element-outside? [element] - (is-bounding-rect-outside? (get-bounding-rect element))) + (is-bounding-rect-outside? (get-bounding-rect element) + (get-window-size))) (defn bounding-rect->rect [rect] diff --git a/frontend/src/app/util/extends.cljs b/frontend/src/app/util/extends.cljs index 4cec0733f3..aff5220d6b 100644 --- a/frontend/src/app/util/extends.cljs +++ b/frontend/src/app/util/extends.cljs @@ -9,6 +9,7 @@ extensions" (:require [promesa.impl :as pi]) - (:import goog.async.Deferred)) + (:import + goog.async.Deferred)) (pi/extend-promise! Deferred) From 691a67b595e9e05ccf017bcaba8d944f7f38d90d Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Mon, 16 Jun 2025 14:39:59 +0200 Subject: [PATCH 3/3] :bug: Fix tooltip height on safari --- .../src/app/main/ui/ds/tooltip/tooltip.cljs | 11 ++++++++--- .../src/app/main/ui/ds/tooltip/tooltip.scss | 2 ++ .../app/main/ui/ds/tooltip/tooltip.stories.jsx | 18 +++++++++--------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs index 9177fffd97..d43a9d20c8 100644 --- a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs +++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs @@ -158,9 +158,14 @@ (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")) + + (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 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 cb43587f7a..0f2398edc0 100644 --- a/frontend/src/app/main/ui/ds/tooltip/tooltip.stories.jsx +++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.stories.jsx @@ -74,7 +74,7 @@ export const Corners = { >