diff --git a/common/src/app/common/files/variant.cljc b/common/src/app/common/files/variant.cljc index e901fd19ef..9729768e9d 100644 --- a/common/src/app/common/files/variant.cljc +++ b/common/src/app/common/files/variant.cljc @@ -8,8 +8,7 @@ [app.common.data.macros :as dm] [app.common.types.component :as ctc] [app.common.types.components-list :as ctcl] - [app.common.types.variant :as ctv] - [cuerdas.core :as str])) + [app.common.types.variant :as ctv])) (defn find-variant-components @@ -21,11 +20,6 @@ (map #(ctcl/get-component data % true)) reverse)) -(defn- dashes-to-end - [property-values] - (let [dashes (if (some #(= % "--") property-values) ["--"] [])] - (concat (remove #(= % "--") property-values) dashes))) - (defn extract-properties-names [shape data] @@ -42,10 +36,7 @@ (group-by :name) (map (fn [[k v]] {:name k - :value (->> v - (map #(if (str/empty? (:value %)) "--" (:value %))) - distinct - dashes-to-end)})))) + :value (->> v (map :value) distinct)})))) (defn get-variant-mains [component data] diff --git a/frontend/src/app/main/ui/ds/controls/combobox.cljs b/frontend/src/app/main/ui/ds/controls/combobox.cljs index a37325dc0a..1496a5a8f7 100644 --- a/frontend/src/app/main/ui/ds/controls/combobox.cljs +++ b/frontend/src/app/main/ui/ds/controls/combobox.cljs @@ -59,11 +59,12 @@ [:disabled {:optional true} :boolean] [:default-selected {:optional true} :string] [:on-change {:optional true} fn?] + [:empty-to-end {:optional true} :boolean] [:has-error {:optional true} :boolean]]) (mf/defc combobox* {::mf/schema schema:combobox} - [{:keys [id options class placeholder disabled has-error default-selected max-length on-change] :rest props}] + [{:keys [id options class placeholder disabled has-error default-selected max-length empty-to-end on-change] :rest props}] (let [is-open* (mf/use-state false) is-open (deref is-open*) @@ -187,7 +188,6 @@ (kbd/enter? event) (do - #_(handle-selection focused-value* selected-value* is-open*) (reset! selected-value* focused-value) (reset! is-open* false) (reset! focused-value* nil) @@ -286,4 +286,5 @@ :focused focused-value :set-ref set-option-ref :id listbox-id + :empty-to-end empty-to-end :data-testid "combobox-options"}])])) diff --git a/frontend/src/app/main/ui/ds/controls/select.cljs b/frontend/src/app/main/ui/ds/controls/select.cljs index 1bf64706e6..b11eaf8739 100644 --- a/frontend/src/app/main/ui/ds/controls/select.cljs +++ b/frontend/src/app/main/ui/ds/controls/select.cljs @@ -15,6 +15,7 @@ [app.util.dom :as dom] [app.util.keyboard :as kbd] [app.util.object :as obj] + [clojure.string :as str] [rumext.v2 :as mf])) (def listbox-id-index (atom 0)) @@ -66,13 +67,23 @@ [:class {:optional true} :string] [:disabled {:optional true} :boolean] [:default-selected {:optional true} :string] + [:empty-to-end {:optional true} :boolean] [:on-change {:optional true} fn?]]) (mf/defc select* {::mf/schema schema:select} - [{:keys [options class disabled default-selected on-change] :rest props}] - (let [open* (mf/use-state false) - open (deref open*) + [{:keys [options class disabled default-selected empty-to-end on-change] :rest props}] + (let [is-open* (mf/use-state false) + is-open (deref is-open*) + + selected-value* (mf/use-state #(get-selected-option-id options default-selected)) + selected-value (deref selected-value*) + + focused-value* (mf/use-state nil) + focused-value (deref focused-value*) + + has-focus* (mf/use-state false) + has-focus (deref has-focus*) listbox-id-ref (mf/use-ref (dm/str "select-listbox-" (swap! listbox-id-index inc))) options-nodes-refs (mf/use-ref nil) @@ -80,38 +91,11 @@ select-ref (mf/use-ref nil) listbox-id (mf/ref-val listbox-id-ref) - selected* (mf/use-state #(get-selected-option-id options default-selected)) - selected (deref selected*) + empty-selected-value? (str/blank? selected-value) - focused* (mf/use-state nil) - focused (deref focused*) - - has-focus* (mf/use-state false) - has-focus (deref has-focus*) - - on-click - (mf/use-fn - (mf/deps disabled) - (fn [event] - (dom/stop-propagation event) - (reset! has-focus* true) - (when-not disabled - (swap! open* not)))) - - on-option-click - (mf/use-fn - (mf/deps on-change) - (fn [event] - (let [node (dom/get-current-target event) - id (dom/get-data node "id")] - (reset! selected* id) - (reset! focused* nil) - (reset! open* false) - (when (fn? on-change) - (on-change id))))) - - set-ref + set-option-ref (mf/use-fn + (mf/deps options-nodes-refs) (fn [node id] (let [refs (or (mf/ref-val options-nodes-refs) #js {}) refs (if node @@ -119,47 +103,69 @@ (obj/unset! refs id))] (mf/set-ref-val! options-nodes-refs refs)))) - on-blur + on-option-click + (mf/use-fn + (mf/deps on-change) + (fn [event] + (let [node (dom/get-current-target event) + id (dom/get-data node "id")] + (reset! selected-value* id) + (reset! focused-value* nil) + (reset! is-open* false) + (when (fn? on-change) + (on-change id))))) + + on-component-click + (mf/use-fn + (mf/deps disabled) + (fn [event] + (dom/stop-propagation event) + (reset! has-focus* true) + (when-not disabled + (swap! is-open* not)))) + + on-component-blur (mf/use-fn (fn [event] (let [target (.-relatedTarget event) outside? (not (.contains (mf/ref-val select-ref) target))] (when outside? - (reset! focused* nil) - (reset! open* false) + (reset! focused-value* nil) + (reset! is-open* false) (reset! has-focus* false))))) - on-key-down + on-component-focus (mf/use-fn - (mf/deps focused disabled) + (fn [_] + (reset! has-focus* true))) + + on-button-key-down + (mf/use-fn + (mf/deps focused-value disabled) (fn [event] + (dom/stop-propagation event) (when-not disabled (let [options (mf/ref-val options-ref) len (alength options) - index (array/find-index #(= (deref focused*) (obj/get % "id")) options)] - (dom/stop-propagation event) + index (array/find-index #(= (deref focused-value*) (obj/get % "id")) options)] (cond (kbd/home? event) - (handle-focus-change options focused* 0 options-nodes-refs) + (handle-focus-change options focused-value* 0 options-nodes-refs) (kbd/up-arrow? event) - (handle-focus-change options focused* (mod (- index 1) len) options-nodes-refs) + (handle-focus-change options focused-value* (mod (- index 1) len) options-nodes-refs) (kbd/down-arrow? event) - (handle-focus-change options focused* (mod (+ index 1) len) options-nodes-refs) + (handle-focus-change options focused-value* (mod (+ index 1) len) options-nodes-refs) (or (kbd/space? event) (kbd/enter? event)) - (when (deref open*) + (when (deref is-open*) (dom/prevent-default event) - (handle-selection focused* selected* open*)) + (handle-selection focused-value* selected-value* is-open*)) (kbd/esc? event) - (do (reset! open* false) - (reset! focused* nil))))))) - - on-focus - (mf/use-fn - (fn [_] (reset! has-focus* true))) + (do (reset! is-open* false) + (reset! focused-value* nil))))))) class (dm/str class " " (stl/css-case :select true :focused has-focus)) @@ -168,13 +174,13 @@ :role "combobox" :aria-controls listbox-id :aria-haspopup "listbox" - :aria-activedescendant focused - :aria-expanded open - :on-key-down on-key-down + :aria-activedescendant focused-value + :aria-expanded is-open + :on-key-down on-button-key-down :disabled disabled - :on-click on-click}) + :on-click on-component-click}) - selected-option (get-option options selected) + selected-option (get-option options selected-value) label (obj/get selected-option "label") icon (obj/get selected-option "icon")] @@ -182,10 +188,11 @@ (mf/set-ref-val! options-ref options)) [:div {:class (stl/css :select-wrapper) - :on-click on-click - :on-focus on-focus + :on-click on-component-click + :on-focus on-component-focus :ref select-ref - :on-blur on-blur} + :on-blur on-component-blur} + [:> :button props [:span {:class (stl/css-case :select-header true :header-icon (some? icon))} @@ -193,16 +200,19 @@ [:> icon* {:icon-id icon :size "s" :aria-hidden true}]) - [:span {:class (stl/css :header-label)} - label]] + [:span {:class (stl/css-case :header-label true + :header-label-dimmed empty-selected-value?)} + (if empty-selected-value? "--" label)]] [:> icon* {:icon-id i/arrow :class (stl/css :arrow) :size "m" :aria-hidden true}]] - (when open + + (when is-open [:> options-dropdown* {:on-click on-option-click :id listbox-id :options options - :selected selected - :focused focused - :set-ref set-ref}])])) + :selected selected-value + :focused focused-value + :empty-to-end empty-to-end + :set-ref set-option-ref}])])) diff --git a/frontend/src/app/main/ui/ds/controls/select.scss b/frontend/src/app/main/ui/ds/controls/select.scss index db13d2c253..769a5fc3e1 100644 --- a/frontend/src/app/main/ui/ds/controls/select.scss +++ b/frontend/src/app/main/ui/ds/controls/select.scss @@ -11,6 +11,7 @@ .select-wrapper { --select-icon-fg-color: var(--color-foreground-secondary); --select-fg-color: var(--color-foreground-primary); + --select-fg-color-dimmed: var(--color-foreground-secondary); --select-bg-color: var(--color-background-tertiary); --select-outline-color: none; --select-border-color: none; @@ -80,6 +81,10 @@ color: var(--select-fg-color); } +.header-label-dimmed { + color: var(--select-fg-color-dimmed); +} + .header-icon { grid-template-columns: auto 1fr; color: var(--select-icon-fg-color); diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs index 83f50e29c1..d92926cbe2 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs +++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.cljs @@ -9,12 +9,14 @@ [app.main.style :as stl]) (:require [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] + [app.util.array :as array] [app.util.object :as obj] + [cuerdas.core :as str] [rumext.v2 :as mf])) (mf/defc option* {::mf/private true} - [{:keys [id label icon aria-label on-click selected set-ref focused] :rest props}] + [{:keys [id label icon aria-label on-click selected set-ref focused dimmed] :rest props}] [:> :li {:value id :class (stl/css-case :option true @@ -38,7 +40,8 @@ :aria-hidden (when label true) :aria-label (when (not label) aria-label)}]) - [:span {:class (stl/css :option-text)} label] + [:span {:class (stl/css-case :option-text true + :option-text-dimmed dimmed)} label] (when selected [:> icon* {:icon-id i/tick @@ -48,11 +51,18 @@ (mf/defc options-dropdown* {::mf/props :obj} - [{:keys [set-ref on-click options selected focused] :rest props}] + [{:keys [set-ref on-click options selected focused empty-to-end] :rest props}] (let [props (mf/spread-props props {:class (stl/css :option-list) :tab-index "-1" - :role "listbox"})] + :role "listbox"}) + + options-blank (when empty-to-end + (array/filter #(str/blank? (obj/get % "id")) options)) + options (if empty-to-end + (array/filter #((complement str/blank?) (obj/get % "id")) options) + options)] + [:> "ul" props (for [option ^js options] (let [id (obj/get option "id") @@ -67,4 +77,26 @@ :aria-label aria-label :set-ref set-ref :focused (= id focused) - :on-click on-click}]))])) + :dimmed false + :on-click on-click}])) + + (when (seq options-blank) + [:* + (when (seq options) + [:hr {:class (stl/css :option-separator)}]) + + (for [option ^js options-blank] + (let [id (obj/get option "id") + label (obj/get option "label") + aria-label (obj/get option "aria-label") + icon (obj/get option "icon")] + [:> option* {:selected (= id selected) + :key id + :id id + :label label + :icon icon + :aria-label aria-label + :set-ref set-ref + :focused (= id focused) + :dimmed true + :on-click on-click}]))])])) diff --git a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss index f86dc6dde6..ce9368f527 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss +++ b/frontend/src/app/main/ui/ds/controls/shared/options_dropdown.scss @@ -13,6 +13,7 @@ --options-dropdown-bg-color: var(--color-background-tertiary); --options-dropdown-outline-color: none; --options-dropdown-border-color: var(--color-background-quaternary); + --options-dropdown-empty: var(--color-canvas); position: absolute; right: 0; @@ -66,6 +67,10 @@ padding-inline-start: var(--sp-xxs); } +.option-text-dimmed { + color: var(--options-dropdown-empty); +} + .option-icon { color: var(--options-dropdown-icon-fg-color); } @@ -79,3 +84,9 @@ --options-dropdown-fg-color: var(--color-accent-primary); --options-dropdown-icon-fg-color: var(--color-accent-primary); } + +.option-separator { + border: $b-1 solid var(--options-dropdown-border-color); + margin-top: var(--sp-xs); + margin-bottom: var(--sp-xs); +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs index a1dcb8ddbd..2da11c1c7c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs @@ -31,6 +31,7 @@ [app.main.ui.context :as ctx] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.controls.combobox :refer [combobox*]] + [app.main.ui.ds.controls.select :refer [select*]] [app.main.ui.ds.foundations.assets.icon :refer [icon*]] [app.main.ui.ds.product.input-with-meta :refer [input-with-meta*]] [app.main.ui.hooks :as h] @@ -236,7 +237,9 @@ [:div {:class (stl/css :counter)} (str size "/300")])]]))) -(defn- get-variant-error-message [errors] +(defn- get-variant-error-message + "Generate error message depending on the selected variants" + [errors] (cond (and (= (count errors) 1) (some? (first errors))) (tr "workspace.options.component.variant.malformed.single.one") @@ -250,6 +253,16 @@ :else nil)) +(defn- get-variant-options + "Get variant options for a given property name" + [prop-name prop-vals] + (->> (filter #(= (:name %) prop-name) prop-vals) + first + :value + (map (fn [val] {:id val + :label (if (str/blank? val) (str "(" (tr "labels.empty") ")") val)})))) + + (mf/defc component-variant-main-instance* [{:keys [components shapes data]}] (let [component (first components) @@ -271,23 +284,17 @@ variant-errors (mapv :variant-error shapes) variant-error-msg (get-variant-error-message variant-errors) - empty-indicator "--" - get-options (mf/use-fn (mf/deps prop-vals) (fn [prop-name] - (->> (filter #(= (:name %) prop-name) prop-vals) - first - :value - (map (fn [val] {:label val :id val}))))) + (get-variant-options prop-name prop-vals))) update-property-value (mf/use-fn (mf/deps component-ids) (fn [pos value] - (let [value (str/trim value) - value (if (= value empty-indicator) "" value)] + (let [value (d/nilv (str/trim value) "")] (doseq [id component-ids] (st/emit! (dwv/update-property-value id pos value)) (st/emit! (dwv/update-error id nil)))))) @@ -315,9 +322,10 @@ (let [mixed-value? (= (:value prop) false)] [:> combobox* {:id (str "variant-prop-" variant-id "-" pos) - :placeholder (if mixed-value? (tr "settings.multiple") empty-indicator) + :placeholder (if mixed-value? (tr "settings.multiple") "--") :default-selected (if mixed-value? "" (:value prop)) :options (clj->js (get-options (:name prop))) + :empty-to-end true :on-change (partial update-property-value pos)}])]])] (when variant-error-msg @@ -329,6 +337,7 @@ [:div {:class (stl/css :variant-error-darken)} (tr "workspace.options.component.variant.malformed.structure.example")]])])) + (mf/defc component-variant* [{:keys [component shape data]}] (let [component-id (:id component) @@ -342,13 +351,11 @@ prop-vals (mf/with-memo [data objects variant-id] (cfv/extract-properties-values data objects variant-id)) - get-options-vals + get-options (mf/use-fn (mf/deps prop-vals) (fn [prop-name] - (->> (filter #(= (:name %) prop-name) prop-vals) - first - :value))) + (get-variant-options prop-name prop-vals))) switch-component (mf/use-fn @@ -360,7 +367,7 @@ valid-comps (->> variant-components (remove #(= (:id %) component-id)) (filter #(= (dm/get-in % [:variant-properties pos :value]) val))) - nearest-comp (apply min-key #(ctv/distance target-props (:variant-properties %)) valid-comps)] + nearest-comp (apply min-key #(ctv/distance target-props (:variant-properties %)) valid-comps)] (when nearest-comp (st/emit! (dwl/component-swap shape (:component-file shape) (:id nearest-comp) true)))))))] @@ -370,9 +377,11 @@ [:* [:span {:class (stl/css :variant-property-name)} (:name prop)] - [:& select {:default-value (if (str/empty? (:value prop)) "--" (:value prop)) - :options (clj->js (get-options-vals (:name prop))) - :on-change #(switch-component pos %)}]]])])) + [:> select* {:default-selected (:value prop) + :options (clj->js (get-options (:name prop))) + :empty-to-end true + :on-change (partial switch-component pos)}]]])])) + (mf/defc component-swap-item {::mf/props :obj} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 7e4700bf32..a68c5f9b8a 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -2051,6 +2051,10 @@ msgstr "Edit file" msgid "labels.editor" msgstr "Editor" +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:263 +msgid "labels.empty" +msgstr "Empty" + #: src/app/main/ui/dashboard/import.cljs:294 msgid "labels.error" msgstr "Error" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index b959980c77..98fc145a85 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -2088,6 +2088,10 @@ msgstr "Editar archivo" msgid "labels.editor" msgstr "Edición" +#: src/app/main/ui/workspace/sidebar/options/menus/component.cljs:263 +msgid "labels.empty" +msgstr "Vacío" + #: src/app/main/ui/dashboard/import.cljs:294 msgid "labels.error" msgstr "Error"