From df16d0c222d6a90f2f73af6e9a53d2632283d852 Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Tue, 10 Sep 2024 15:16:52 +0200 Subject: [PATCH] Add abstract dropdown component --- .../app/main/ui/workspace/tokens/common.cljs | 93 ++++++++++++++++++- .../app/main/ui/workspace/tokens/common.scss | 79 ++++++++++++++++ .../ui/workspace/tokens/modals/themes.cljs | 21 ++++- .../ui/workspace/tokens/modals/themes.scss | 4 + 4 files changed, 192 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/tokens/common.cljs b/frontend/src/app/main/ui/workspace/tokens/common.cljs index e1f258053..7f1c78779 100644 --- a/frontend/src/app/main/ui/workspace/tokens/common.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/common.cljs @@ -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]}] diff --git a/frontend/src/app/main/ui/workspace/tokens/common.scss b/frontend/src/app/main/ui/workspace/tokens/common.scss index 9398a2bb2..bb067e683 100644 --- a/frontend/src/app/main/ui/workspace/tokens/common.scss +++ b/frontend/src/app/main/ui/workspace/tokens/common.scss @@ -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); + } + } + } +} diff --git a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs index 2d6465c07..520255da9 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs @@ -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) diff --git a/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss b/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss index 83ca1ae21..ca41bd82e 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss +++ b/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss @@ -212,6 +212,10 @@ hr { } } +.group-input-wrapper { + position: relative; +} + .group-drop-down-button { @include buttonStyle; width: $s-24;