mirror of
https://github.com/penpot/penpot.git
synced 2026-08-01 10:56:20 +00:00
✨ Add portal option to select
This commit is contained in:
parent
54468de5c5
commit
1c21b1b1bd
@ -26,10 +26,10 @@
|
||||
[app.main.ui.ds.foundations.typography :as t]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||
[app.main.ui.ds.product.loader :refer [loader*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.notifications.context-notification :refer [context-notification]]
|
||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||
[app.main.ui.ds.product.loader :refer [loader*]]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.worker :as mw]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
@ -224,13 +224,13 @@
|
||||
;; FIXME: rename to format
|
||||
format (:type entry)
|
||||
|
||||
loading? false #_(or (= :analyze status)
|
||||
loading? (or (= :analyze status)
|
||||
(= :import-progress status)
|
||||
(and is-progress (= :import-ready status)))
|
||||
analyze-error? true #_(= :analyze-error status)
|
||||
import-success? false #_(= :import-success status)
|
||||
import-error? false #_(= :import-error status)
|
||||
import-ready? false #_(= :import-ready status)
|
||||
analyze-error? (= :analyze-error status)
|
||||
import-success? (= :import-success status)
|
||||
import-error? (= :import-error status)
|
||||
import-ready? (= :import-ready status)
|
||||
|
||||
level (cond
|
||||
import-success? :success
|
||||
@ -319,19 +319,20 @@
|
||||
(when ^boolean is-shared?
|
||||
[:> icon* {:icon-id i/library :class (stl/css :icon)}])]])])
|
||||
|
||||
[:div {:class (stl/css :edit-entry-buttons)}
|
||||
(when ^boolean editable?
|
||||
[:> icon-button* {:on-click on-edit'
|
||||
:variant "ghost"
|
||||
:icon-size "s"
|
||||
:aria-label (tr "labels.edit")
|
||||
:icon i/curve}])
|
||||
(when ^boolean can-be-deleted
|
||||
[:> icon-button* {:on-click on-delete'
|
||||
:variant "ghost"
|
||||
:icon-size "s"
|
||||
:aria-label (tr "labels.delete")
|
||||
:icon i/delete}])]]
|
||||
(when ^boolean (or editable? can-be-deleted)
|
||||
[:div {:class (stl/css :edit-entry-buttons)}
|
||||
(when ^boolean editable?
|
||||
[:> icon-button* {:on-click on-edit'
|
||||
:variant "ghost"
|
||||
:icon-size "s"
|
||||
:aria-label (tr "labels.edit")
|
||||
:icon i/curve}])
|
||||
(when ^boolean can-be-deleted
|
||||
[:> icon-button* {:on-click on-delete'
|
||||
:variant "ghost"
|
||||
:icon-size "s"
|
||||
:aria-label (tr "labels.delete")
|
||||
:icon i/delete}])])]
|
||||
(cond
|
||||
analyze-error?
|
||||
[:> text* {:class (stl/css :error-message)
|
||||
@ -401,8 +402,10 @@
|
||||
(on-select id (:id first-c))))))
|
||||
|
||||
[:div {:class (stl/css :library-resolution)}
|
||||
[:p {:class (stl/css :library-resolution-message)}
|
||||
(tr "dashboard.import.resolve-libraries")]
|
||||
[:> text* {:class (stl/css :library-resolution-message)
|
||||
:as "p"
|
||||
:typography t/body-large}
|
||||
"Some libraries couldn't be linked automatically. Select the correct library for each:"]
|
||||
|
||||
(for [{:keys [id name candidates]} candidates]
|
||||
(let [options (mapv (fn [c]
|
||||
@ -415,7 +418,9 @@
|
||||
[:div {:class (stl/css :library-resolution-item-name)}
|
||||
name]
|
||||
[:> select* {:options options
|
||||
:class (stl/css :library-resolution-select)
|
||||
:default-selected (or (some-> selected str) "")
|
||||
:has-portal true
|
||||
:on-change (partial on-select id)}]]))]))
|
||||
|
||||
(mf/defc library-resolution-summary-file*
|
||||
@ -735,26 +740,26 @@
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
(cond
|
||||
(and (= :analyze status) errors?)
|
||||
[:& context-notification
|
||||
[:> context-notification*
|
||||
{:level :warning
|
||||
:class (stl/css :context-notification-error)
|
||||
:content (tr "dashboard.import.import-warning")}]
|
||||
:class (stl/css :context-notification-error)}
|
||||
(tr "dashboard.import.import-warning")]
|
||||
|
||||
(= :import-success status)
|
||||
[:*
|
||||
[:& context-notification
|
||||
{:level (if (zero? import-success-total) :warning :success)
|
||||
:content (tr "dashboard.import.import-message" (i18n/c import-success-total))}]
|
||||
[:> context-notification*
|
||||
{:level (if (zero? import-success-total) :warning :success)}
|
||||
(tr "dashboard.import.import-message" (i18n/c import-success-total))]
|
||||
(when (pos? auto-linked-count)
|
||||
[:& context-notification
|
||||
{:level :success
|
||||
:content (tr "dashboard.import.auto-linked-libraries" (i18n/c auto-linked-count))}])]
|
||||
[:> context-notification*
|
||||
{:level :success}
|
||||
(tr "dashboard.import.auto-linked-libraries" (i18n/c auto-linked-count))])]
|
||||
|
||||
(= :import-error status)
|
||||
[:& context-notification
|
||||
[:> context-notification*
|
||||
{:level :error
|
||||
:class (stl/css :context-notification-error)
|
||||
:content (tr "dashboard.import.import-error.disclaimer")}]
|
||||
:class (stl/css :context-notification-error)}
|
||||
(tr "dashboard.import.import-error.disclaimer")]
|
||||
|
||||
;; :resolution — wizard step (current derived file)
|
||||
(= :library-resolution status)
|
||||
|
||||
@ -426,11 +426,6 @@
|
||||
}
|
||||
|
||||
.library-resolution-message {
|
||||
// TODO: Typography does not match any existing DS typography token.
|
||||
font-family: "worksans", "vazirmatn", sans-serif;
|
||||
font-size: px2rem(12);
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
color: var(--color-foreground-secondary);
|
||||
margin-block-end: var(--sp-s);
|
||||
}
|
||||
|
||||
@ -13,9 +13,11 @@
|
||||
[app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown* schema:option]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.tooltip.tooltip :refer [tooltip*]]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as timers]
|
||||
[clojure.string :as str]
|
||||
[rumext.v2 :as mf]
|
||||
[rumext.v2.util :as mfu]))
|
||||
@ -58,11 +60,12 @@
|
||||
[:empty-to-end {:optional true} [:maybe :boolean]]
|
||||
[:on-change {:optional true} fn?]
|
||||
[:dropdown-alignment {:optional true} [:maybe [:enum :left :right]]]
|
||||
[:variant {:optional true} [:maybe [:enum "default" "ghost" "icon-only"]]]])
|
||||
[:variant {:optional true} [:maybe [:enum "default" "ghost" "icon-only"]]]
|
||||
[:has-portal {:optional true} :boolean]])
|
||||
|
||||
(mf/defc select*
|
||||
{::mf/schema schema:select}
|
||||
[{:keys [options class disabled default-selected empty-to-end on-change variant wrapper-class dropdown-alignment] :rest props}]
|
||||
[{:keys [options class disabled default-selected empty-to-end on-change variant wrapper-class dropdown-alignment has-portal] :rest props}]
|
||||
(let [;; NOTE: we use mfu/bean here for transparently handle
|
||||
;; options provide as clojure data structures or javascript
|
||||
;; plain objects and lists.
|
||||
@ -88,6 +91,9 @@
|
||||
options-ref (mf/use-ref nil)
|
||||
select-ref (mf/use-ref nil)
|
||||
|
||||
container (hooks/use-portal-container :popup)
|
||||
dropdown-wrapper-ref (mf/use-ref nil)
|
||||
|
||||
empty-selected-id?
|
||||
(str/blank? selected-id)
|
||||
|
||||
@ -208,10 +214,63 @@
|
||||
(reset! selected-id*
|
||||
(get-selected-option-id options default-selected)))
|
||||
|
||||
;; Portal mode: click-outside + floating positioning
|
||||
(mf/with-effect [is-open has-portal]
|
||||
(when (and is-open has-portal)
|
||||
(let [handler
|
||||
(fn [event]
|
||||
(let [wrapper-node (mf/ref-val select-ref)
|
||||
dropdown-node (mf/ref-val dropdown-wrapper-ref)
|
||||
target (dom/get-target event)]
|
||||
(when (and wrapper-node dropdown-node
|
||||
(not (dom/child? target wrapper-node))
|
||||
(not (dom/child? target dropdown-node)))
|
||||
(reset! is-open* false)
|
||||
(reset! focused-id* nil))))
|
||||
|
||||
calculate
|
||||
(fn []
|
||||
(timers/raf
|
||||
(fn []
|
||||
(when-let [select-node (mf/ref-val select-ref)]
|
||||
(when-let [dropdown-node (mf/ref-val dropdown-wrapper-ref)]
|
||||
(let [select-rect (dom/get-bounding-rect select-node)
|
||||
dropdown-rect (dom/get-bounding-rect dropdown-node)
|
||||
window-height (.-innerHeight js/window)
|
||||
space-below (- window-height (:bottom select-rect))
|
||||
open-up? (> (:height dropdown-rect) space-below)]
|
||||
(if open-up?
|
||||
(let [bottom (+ (- window-height (:top select-rect)) 4)]
|
||||
(dom/set-css-property! dropdown-node "top" "unset")
|
||||
(dom/set-css-property! dropdown-node "bottom" (str bottom "px")))
|
||||
(let [top (+ (:bottom select-rect) 4)]
|
||||
(dom/set-css-property! dropdown-node "bottom" "unset")
|
||||
(dom/set-css-property! dropdown-node "top" (str top "px"))))
|
||||
(dom/set-css-property! dropdown-node "left" (str (:left select-rect) "px"))
|
||||
(dom/set-css-property! dropdown-node "width" (str (:width select-rect) "px"))
|
||||
(dom/set-css-property! dropdown-node "position" "fixed")))))))]
|
||||
|
||||
(.addEventListener js/document "mousedown" handler)
|
||||
|
||||
(let [ro (js/ResizeObserver. (fn [_] (calculate)))]
|
||||
(when-let [node (mf/ref-val select-ref)]
|
||||
(.observe ro node))
|
||||
|
||||
(.addEventListener js/window "resize" calculate)
|
||||
(.addEventListener js/window "scroll" calculate true)
|
||||
|
||||
(calculate)
|
||||
|
||||
(fn []
|
||||
(.removeEventListener js/document "mousedown" handler)
|
||||
(.disconnect ro)
|
||||
(.removeEventListener js/window "resize" calculate)
|
||||
(.removeEventListener js/window "scroll" calculate true))))))
|
||||
|
||||
[:div {:class [wrapper-class (stl/css :select-wrapper)]
|
||||
:on-click on-click
|
||||
:ref select-ref
|
||||
:on-blur on-blur}
|
||||
:on-blur (when-not has-portal on-blur)}
|
||||
|
||||
[:> :button props
|
||||
[:span {:class (stl/css-case :select-header true
|
||||
@ -241,11 +300,24 @@
|
||||
:aria-hidden true}]]
|
||||
|
||||
(when ^boolean is-open
|
||||
[:> options-dropdown* {:on-click on-option-click
|
||||
:id listbox-id
|
||||
:options options
|
||||
:selected selected-id
|
||||
:focused focused-id
|
||||
:align dropdown-alignment
|
||||
:empty-to-end empty-to-end
|
||||
:ref set-option-ref}])]))
|
||||
(if has-portal
|
||||
(mf/portal
|
||||
(mf/html
|
||||
[:> options-dropdown* {:on-click on-option-click
|
||||
:id listbox-id
|
||||
:options options
|
||||
:selected selected-id
|
||||
:focused focused-id
|
||||
:align dropdown-alignment
|
||||
:empty-to-end empty-to-end
|
||||
:ref set-option-ref
|
||||
:wrapper-ref dropdown-wrapper-ref}])
|
||||
container)
|
||||
[:> options-dropdown* {:on-click on-option-click
|
||||
:id listbox-id
|
||||
:options options
|
||||
:selected selected-id
|
||||
:focused focused-id
|
||||
:align dropdown-alignment
|
||||
:empty-to-end empty-to-end
|
||||
:ref set-option-ref}]))]))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user