0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-21 06:02:32 -05:00

Add abstract dropdown component

This commit is contained in:
Florian Schroedl 2024-09-10 15:16:52 +02:00
parent d54c5476d8
commit df16d0c222
4 changed files with 192 additions and 5 deletions

View file

@ -8,8 +8,19 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.data.shortcuts :as dsc]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.globals :as globals]
[app.util.keyboard :as kbd]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
[goog.events :as events]
[rumext.v2 :as mf])
(:import
goog.events.EventType))
;; Helpers ---------------------------------------------------------------------
@ -30,8 +41,88 @@
[(keyword (str/camel (name k))) v]
[k v])))))
(defn direction-select
"Returns next `n` in `direction` while wrapping around at the last item at the count of `coll`.
`direction` accepts `:up` or `:down`."
[direction n coll]
(let [last-n (dec (count coll))
next-n (case direction
:up (dec n)
:down (inc n))
wrap-around-n (cond
(neg? next-n) last-n
(> next-n last-n) 0
:else next-n)]
wrap-around-n))
(defn use-arrow-highlight [{:keys [shortcuts-key options on-select]}]
(let [highlighted* (mf/use-state nil)
highlighted (deref highlighted*)
on-dehighlight #(reset! highlighted* nil)
on-keyup (fn [event]
(cond
(and (kbd/enter? event) highlighted) (on-select (nth options highlighted))
(kbd/up-arrow? event) (do
(dom/prevent-default event)
(->> (direction-select :up (or highlighted 0) options)
(reset! highlighted*)))
(kbd/down-arrow? event) (do
(dom/prevent-default event)
(->> (direction-select :down (or highlighted -1) options)
(reset! highlighted*)))))]
(mf/with-effect [highlighted]
(let [shortcuts-key shortcuts-key
keys [(events/listen globals/document EventType.KEYUP on-keyup)
(events/listen globals/document EventType.KEYDOWN dom/prevent-default)]]
(st/emit! (dsc/push-shortcuts shortcuts-key {}))
(fn []
(doseq [key keys]
(events/unlistenByKey key))
(st/emit! (dsc/pop-shortcuts shortcuts-key)))))
{:highlighted highlighted
:on-dehighlight on-dehighlight}))
(defn use-dropdown-open-state []
(let [open? (mf/use-state false)
on-open (mf/use-fn #(reset! open? true))
on-close (mf/use-fn #(reset! open? false))
on-toggle (mf/use-fn #(swap! open? not))]
{:dropdown-open? @open?
:on-open-dropdown on-open
:on-close-dropdown on-close
:on-toggle-dropdown on-toggle}))
;; Components ------------------------------------------------------------------
(mf/defc dropdown-select
[{:keys [id _shortcuts-key options on-close element-ref on-select] :as props}]
(let [{:keys [highlighted on-dehighlight]} (use-arrow-highlight props)]
[:& dropdown {:show true
:on-close on-close}
[:> :div {:class (stl/css :dropdown)
:on-mouse-enter on-dehighlight
:ref element-ref}
[:ul {:class (stl/css :dropdown-list)}
(for [[index item] (d/enumerate options)]
(cond
(= :separator item)
[:li {:class (stl/css :separator)
:key (dm/str id "-" index)}]
:else
(let [{:keys [label selected? disabled?]} item
highlighted? (= highlighted index)]
[:li
{:key (str id "-" index)
:class (stl/css-case :dropdown-element true
:is-selected selected?
:is-highlighted highlighted?)
:data-label label
:disabled disabled?
:on-click #(on-select item)}
[:span {:class (stl/css :label)} label]
[:span {:class (stl/css :check-icon)} i/tick]])))]]]))
(mf/defc labeled-input
{::mf/wrap-props false}
[{:keys [label input-props auto-complete? error? icon render-right]}]

View file

@ -34,3 +34,82 @@
@extend .button-icon;
}
}
.dropdown {
@extend .dropdown-wrapper;
max-height: $s-320;
width: 100%;
margin-top: $s-4;
ul {
margin: 0;
}
.separator {
margin: 0;
height: $s-12;
}
.dropdown-element {
@extend .dropdown-element-base;
color: var(--menu-foreground-color-rest);
display: flex;
& > span {
display: flex;
justify-content: flex-start;
align-content: center;
}
.label,
.value {
width: fit-content;
}
.label {
text-transform: unset;
flex: 1;
}
.value {
text-align: right;
justify-content: flex-end;
flex: 0.6;
}
.check-icon {
@include flexCenter;
translate: -$s-4 0;
svg {
@extend .button-icon-small;
visibility: hidden;
stroke: var(--icon-foreground);
}
}
&.is-selected {
color: var(--menu-foreground-color);
.check-icon svg {
stroke: var(--menu-foreground-color);
visibility: visible;
}
}
&:hover {
background-color: var(--menu-background-color-hover);
color: var(--menu-foreground-color-hover);
.check-icon svg {
stroke: var(--menu-foreground-color-hover);
}
}
&.is-highlighted {
background-color: var(--button-primary-background-color-rest);
span {
color: var(--button-primary-foreground-color-rest);
}
.check-icon svg {
stroke: var(--button-primary-foreground-color-rest);
}
}
}
}

View file

@ -13,13 +13,14 @@
[app.main.store :as st]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.icons :as i]
[app.main.ui.workspace.tokens.common :refer [labeled-input]]
[app.main.ui.workspace.tokens.common :refer [labeled-input] :as wtco]
[app.main.ui.workspace.tokens.sets :as wts]
[app.main.ui.workspace.tokens.token-set :as wtts]
[app.util.dom :as dom]
[rumext.v2 :as mf]
[cuerdas.core :as str]
[app.main.ui.workspace.tokens.sets-context :as sets-context]))
[app.main.ui.workspace.tokens.sets-context :as sets-context]
[app.main.ui.shapes.group :as group]))
(def ^:private chevron-icon
(i/icon-xref :arrow (stl/css :chevron-icon)))
@ -109,7 +110,9 @@
(mf/defc edit-theme
[{:keys [token-sets theme theme-groups on-back on-submit]}]
(let [edit? (some? (:id theme))
(let [{:keys [dropdown-open? on-open-dropdown on-close-dropdown on-toggle-dropdown]} (wtco/use-dropdown-open-state)
edit? (some? (:id theme))
theme-state (mf/use-state {:token-sets token-sets
:theme theme})
disabled? (-> (get-in @theme-state [:theme :name])
@ -151,6 +154,15 @@
:on-click on-back}
chevron-icon "Back"]]
[:div {:class (stl/css :edit-theme-inputs-wrapper)}
(when dropdown-open?
[:& wtco/dropdown-select {:id ::groups-dropdown
:shortcuts-key ::groups-dropdown
:options (map (fn [group]
{:label group
:value group})
theme-groups)
:on-select #(on-update-group (:value %))
:on-close on-close-dropdown}])
[:& labeled-input {:label "Group"
:input-props {:default-value (:group theme)
:on-change on-update-group}
@ -158,7 +170,8 @@
[:button {:class (stl/css :group-drop-down-button)
:type "button"
:on-click (fn [e]
(dom/stop-propagation e))}
(dom/stop-propagation e)
(on-toggle-dropdown))}
i/arrow])}]
[:& labeled-input {:label "Theme"
:input-props {:default-value (:name theme)

View file

@ -212,6 +212,10 @@ hr {
}
}
.group-input-wrapper {
position: relative;
}
.group-drop-down-button {
@include buttonStyle;
width: $s-24;