diff --git a/frontend/deps.edn b/frontend/deps.edn index 2fc983a630..86ced3d563 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -20,8 +20,8 @@ :git/url "https://github.com/funcool/beicon.git"} funcool/rumext - {:git/tag "v2.22" - :git/sha "92879b6" + {:git/tag "v2.24" + :git/sha "17a0c94" :git/url "https://github.com/funcool/rumext.git"} instaparse/instaparse {:mvn/version "1.5.0"} @@ -42,7 +42,7 @@ :dev {:extra-paths ["dev"] :extra-deps - {thheller/shadow-cljs {:mvn/version "3.1.5"} + {thheller/shadow-cljs {:mvn/version "3.1.7"} com.bhauman/rebel-readline {:mvn/version "RELEASE"} org.clojure/tools.namespace {:mvn/version "RELEASE"} criterium/criterium {:mvn/version "RELEASE"} diff --git a/frontend/src/app/main/ui/ds/controls/combobox.cljs b/frontend/src/app/main/ui/ds/controls/combobox.cljs index 48674f468b..98805119c6 100644 --- a/frontend/src/app/main/ui/ds/controls/combobox.cljs +++ b/frontend/src/app/main/ui/ds/controls/combobox.cljs @@ -6,102 +6,85 @@ (ns app.main.ui.ds.controls.combobox (:require-macros - [app.common.data.macros :as dm] [app.main.style :as stl]) (:require [app.common.data :as d] [app.main.constants :refer [max-input-length]] - [app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown*]] - [app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list] :as i] - [app.util.array :as array] + [app.main.ui.ds.controls.select :refer [get-option handle-focus-change]] + [app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown* schema:option]] + [app.main.ui.ds.foundations.assets.icon :as i] [app.util.dom :as dom] [app.util.keyboard :as kbd] [app.util.object :as obj] - [rumext.v2 :as mf])) - -(def listbox-id-index (atom 0)) - -(defn- get-option - [options id] - (array/find #(= id (obj/get % "id")) options)) - -(defn- handle-focus-change - [options focused* new-index options-nodes-refs] - (let [option (aget options new-index) - id (obj/get option "id") - nodes (mf/ref-val options-nodes-refs) - node (obj/get nodes id)] - (reset! focused* id) - (dom/scroll-into-view-if-needed! node))) - -(def ^:private schema:combobox-option - [:and - [:map {:title "option"} - [:id :string] - [:icon {:optional true} - [:and :string [:fn #(contains? icon-list %)]]] - [:label {:optional true} :string] - [:aria-label {:optional true} :string]] - [:fn {:error/message "invalid data: missing required props"} - (fn [option] - (or (and (contains? option :icon) - (or (contains? option :label) - (contains? option :aria-label))) - (contains? option :label)))]]) + [cuerdas.core :as str] + [rumext.v2 :as mf] + [rumext.v2.util :as mfu])) (def ^:private schema:combobox [:map [:id {:optional true} :string] - [:options [:vector schema:combobox-option]] + [:options [:vector schema:option]] [:class {:optional true} :string] [:max-length {:optional true} :int] [:placeholder {:optional true} :string] [:disabled {:optional true} :boolean] [:default-selected {:optional true} :string] [:on-change {:optional true} fn?] - [:empty-to-end {:optional true} :boolean] + [:empty-to-end {:optional true} [:maybe :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 empty-to-end on-change] :rest props}] - (let [is-open* (mf/use-state false) - is-open (deref is-open*) + (let [;; NOTE: we use mfu/bean here for transparently handle + ;; options provide as clojure data structures or javascript + ;; plain objects and lists. + options (if (array? options) + (mfu/bean options) + options) + empty-to-end (d/nilv empty-to-end false) - selected-value* (mf/use-state default-selected) - selected-value (deref selected-value*) + is-open* (mf/use-state false) + is-open (deref is-open*) - filter-value* (mf/use-state "") - filter-value (deref filter-value*) + selected-id* (mf/use-state default-selected) + selected-id (deref selected-id*) - focused-value* (mf/use-state nil) - focused-value (deref focused-value*) + filter-id* (mf/use-state "") + filter-id (deref filter-id*) - combobox-ref (mf/use-ref nil) - input-ref (mf/use-ref nil) - options-nodes-refs (mf/use-ref nil) - options-ref (mf/use-ref nil) - listbox-id-ref (mf/use-ref (dm/str "listbox-" (swap! listbox-id-index inc))) - listbox-id (mf/ref-val listbox-id-ref) + focused-id* (mf/use-state nil) + focused-id (deref focused-id*) + + combobox-ref (mf/use-ref nil) + input-ref (mf/use-ref nil) + nodes-ref (mf/use-ref nil) + options-ref (mf/use-ref nil) + listbox-id (mf/use-id) dropdown-options - (mf/use-memo - (mf/deps options filter-value) - (fn [] - (->> options - (array/filter (fn [option] - (let [lower-option (.toLowerCase (obj/get option "id")) - lower-filter (.toLowerCase filter-value)] - (.includes lower-option lower-filter))))))) + (mf/with-memo [options filter-id] + (->> options + (filterv (fn [option] + (let [option (str/lower (get option :id)) + filter (str/lower filter-id)] + (str/includes? option filter)))) + (not-empty))) set-option-ref (mf/use-fn - (fn [node id] - (let [refs (or (mf/ref-val options-nodes-refs) #js {}) - refs (if node - (obj/set! refs id node) - (obj/unset! refs id))] - (mf/set-ref-val! options-nodes-refs refs)))) + (fn [node] + (let [state (mf/ref-val nodes-ref) + state (d/nilv state #js {}) + id (dom/get-data node "id") + state (obj/set! state id node)] + (mf/set-ref-val! nodes-ref state) + (fn [] + (let [state (mf/ref-val nodes-ref) + state (d/nilv state #js {}) + id (dom/get-data node "id") + state (obj/unset! state id)] + (mf/set-ref-val! nodes-ref state)))))) on-option-click (mf/use-fn @@ -110,34 +93,36 @@ (dom/stop-propagation event) (let [node (dom/get-current-target event) id (dom/get-data node "id")] - (reset! selected-value* id) + (reset! selected-id* id) (reset! is-open* false) - (reset! focused-value* nil) + (reset! focused-id* nil) (when (fn? on-change) (on-change id))))) - on-component-click + on-click (mf/use-fn (mf/deps disabled) (fn [event] (dom/stop-propagation event) (when-not disabled (when-not (deref is-open*) - (reset! filter-value* "")) + (reset! filter-id* "")) (swap! is-open* not)))) - on-component-blur + + on-blur (mf/use-fn (mf/deps on-change) (fn [event] (dom/stop-propagation event) - (let [target (.-relatedTarget event) - outside? (not (.contains (mf/ref-val combobox-ref) target))] - (when outside? + (let [target (dom/get-related-target event) + self-node (mf/ref-val combobox-ref)] + (when-not (dom/is-child? self-node target) (reset! is-open* false) - (reset! focused-value* nil) + (reset! focused-id* nil) (when (fn? on-change) - (on-change (dom/get-input-value (mf/ref-val input-ref)))))))) + (when-let [input-node (mf/ref-val input-ref)] + (on-change (dom/get-input-value input-node)))))))) on-input-click (mf/use-fn @@ -146,7 +131,7 @@ (dom/stop-propagation event) (when-not disabled (when-not (deref is-open*) - (reset! filter-value* "")) + (reset! filter-id* "")) (reset! is-open* true)))) on-input-focus @@ -158,47 +143,47 @@ on-input-key-down (mf/use-fn - (mf/deps is-open focused-value disabled dropdown-options) + (mf/deps is-open focused-id disabled) (fn [event] (dom/stop-propagation event) (when-not disabled - (let [len (alength dropdown-options) - index (array/find-index #(= (deref focused-value*) (obj/get % "id")) dropdown-options)] - - (when (< len 0) - (reset! index len)) + (let [options (mf/ref-val options-ref) + len (count options) + index (d/index-of-pred options #(= focused-id (get % :id))) + index (d/nilv index -1) + nodes (mf/ref-val nodes-ref)] (if is-open (cond (kbd/home? event) - (handle-focus-change dropdown-options focused-value* 0 options-nodes-refs) + (handle-focus-change options focused-id* 0 nodes) (kbd/up-arrow? event) (let [new-index (if (= index -1) (dec len) (mod (- index 1) len))] - (handle-focus-change dropdown-options focused-value* new-index options-nodes-refs)) + (handle-focus-change options focused-id* new-index nodes)) (kbd/down-arrow? event) (let [new-index (if (= index -1) 0 (mod (+ index 1) len))] - (handle-focus-change dropdown-options focused-value* new-index options-nodes-refs)) + (handle-focus-change options focused-id* new-index nodes)) (kbd/enter? event) (do - (reset! selected-value* focused-value) + (reset! selected-id* focused-id) (reset! is-open* false) - (reset! focused-value* nil) + (reset! focused-id* nil) (dom/blur! (mf/ref-val input-ref)) (when (and (fn? on-change) - (some? focused-value)) - (on-change focused-value))) + (some? focused-id)) + (on-change focused-id))) (kbd/esc? event) (do (reset! is-open* false) - (reset! focused-value* nil) + (reset! focused-id* nil) (dom/blur! (mf/ref-val input-ref)))) (cond @@ -215,20 +200,24 @@ (let [value (-> event dom/get-target dom/get-value)] - (reset! selected-value* value) - (reset! filter-value* value) - (reset! focused-value* nil)))) + (reset! selected-id* value) + (reset! filter-id* value) + (reset! focused-id* nil)))) - selected-option (get-option options selected-value) - icon (obj/get selected-option "icon")] + selected-option + (mf/with-memo [options selected-id] + (get-option options selected-id)) - (mf/with-effect [options] - (mf/set-ref-val! options-ref options)) + icon + (get selected-option :icon)] + + (mf/with-effect [dropdown-options] + (mf/set-ref-val! options-ref dropdown-options)) (mf/use-effect (mf/deps default-selected) (fn [] - (reset! selected-value* default-selected))) + (reset! selected-id* default-selected))) [:div {:ref combobox-ref :class (stl/css-case @@ -236,16 +225,16 @@ :has-error has-error :disabled disabled)} - [:div {:class (dm/str class " " (stl/css :combobox)) - :on-blur on-component-blur - :on-click on-component-click} + [:div {:class [class (stl/css :combobox)] + :on-blur on-blur + :on-click on-click} [:span {:class (stl/css-case :header true :header-icon (some? icon))} (when icon - [:> icon* {:icon-id icon - :size "s" - :aria-hidden true}]) + [:> i/icon* {:icon-id icon + :size "s" + :aria-hidden true}]) [:input {:id id :ref input-ref :type "text" @@ -255,11 +244,11 @@ :aria-autocomplete "both" :aria-expanded is-open :aria-controls listbox-id - :aria-activedescendant focused-value + :aria-activedescendant focused-id :data-testid "combobox-input" :max-length (d/nilv max-length max-input-length) :disabled disabled - :value (d/nilv selected-value "") + :value (d/nilv selected-id "") :placeholder placeholder :on-change on-input-change :on-click on-input-click @@ -267,24 +256,25 @@ :on-key-down on-input-key-down}]] (when (d/not-empty? options) - [:> :button {:type "button" - :tab-index "-1" - :aria-expanded is-open - :aria-controls listbox-id - :class (stl/css :button-toggle-list) - :on-click on-component-click} - [:> icon* {:icon-id i/arrow - :class (stl/css :arrow) - :size "s" - :aria-hidden true - :data-testid "combobox-open-button"}]])] + [:button {:type "button" + :tab-index "-1" + :aria-expanded is-open + :aria-controls listbox-id + :class (stl/css :button-toggle-list) + :on-click on-click} + [:> i/icon* {:icon-id i/arrow + :class (stl/css :arrow) + :size "s" + :aria-hidden true + :data-testid "combobox-open-button"}]])] - (when (and is-open (seq dropdown-options)) + (when (and ^boolean is-open + ^boolean dropdown-options) [:> options-dropdown* {:on-click on-option-click :options dropdown-options - :selected selected-value - :focused focused-value - :set-ref set-option-ref + :selected selected-id + :focused focused-id + :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 b11eaf8739..d64d807bc2 100644 --- a/frontend/src/app/main/ui/ds/controls/select.cljs +++ b/frontend/src/app/main/ui/ds/controls/select.cljs @@ -6,50 +6,34 @@ (ns app.main.ui.ds.controls.select (:require-macros - [app.common.data.macros :as dm] [app.main.style :as stl]) (:require - [app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown*]] - [app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list] :as i] - [app.util.array :as array] + [app.common.data :as d] + [app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown* schema:option]] + [app.main.ui.ds.foundations.assets.icon :as i] [app.util.dom :as dom] [app.util.keyboard :as kbd] [app.util.object :as obj] [clojure.string :as str] - [rumext.v2 :as mf])) + [rumext.v2 :as mf] + [rumext.v2.util :as mfu])) -(def listbox-id-index (atom 0)) - -(def ^:private schema:select-option - [:and - [:map {:title "option"} - [:id :string] - [:icon {:optional true} - [:and :string [:fn #(contains? icon-list %)]]] - [:label {:optional true} :string] - [:aria-label {:optional true} :string]] - [:fn {:error/message "invalid data: missing required props"} - (fn [option] - (or (and (contains? option :icon) - (or (contains? option :label) - (contains? option :aria-label))) - (contains? option :label)))]]) - -(defn- get-option +(defn get-option [options id] - (or (array/find #(= id (obj/get % "id")) options) - (aget options 0))) + (or (d/seek #(= id (get % :id)) options) + (nth options 0))) (defn- get-selected-option-id [options default] (let [option (get-option options default)] - (obj/get option "id"))) + (get option :id))) -(defn- handle-focus-change - [options focused* new-index options-nodes-refs] - (let [option (aget options new-index) - id (obj/get option "id") - nodes (mf/ref-val options-nodes-refs) + +;; Also used in combobox +(defn handle-focus-change + [options focused* new-index nodes] + (let [option (get options new-index) + id (get option :id) node (obj/get nodes id)] (reset! focused* id) (dom/scroll-into-view-if-needed! node))) @@ -63,45 +47,59 @@ (def ^:private schema:select [:map - [:options [:vector {:min 1} schema:select-option]] + [:options [:vector {:min 1} schema:option]] [:class {:optional true} :string] [:disabled {:optional true} :boolean] [:default-selected {:optional true} :string] - [:empty-to-end {:optional true} :boolean] + [:empty-to-end {:optional true} [:maybe :boolean]] [:on-change {:optional true} fn?]]) (mf/defc select* {::mf/schema schema:select} [{: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*) + (let [;; NOTE: we use mfu/bean here for transparently handle + ;; options provide as clojure data structures or javascript + ;; plain objects and lists. + options (if (array? options) + (mfu/bean options) + options) - selected-value* (mf/use-state #(get-selected-option-id options default-selected)) - selected-value (deref selected-value*) + empty-to-end (d/nilv empty-to-end false) + is-open* (mf/use-state false) + is-open (deref is-open*) - focused-value* (mf/use-state nil) - focused-value (deref focused-value*) + selected-id* (mf/use-state #(get-selected-option-id options default-selected)) + selected-id (deref selected-id*) - has-focus* (mf/use-state false) - has-focus (deref has-focus*) + focused-id* (mf/use-state selected-id) + focused-id (deref focused-id*) - listbox-id-ref (mf/use-ref (dm/str "select-listbox-" (swap! listbox-id-index inc))) - options-nodes-refs (mf/use-ref nil) - options-ref (mf/use-ref nil) - select-ref (mf/use-ref nil) - listbox-id (mf/ref-val listbox-id-ref) + has-focus* (mf/use-state false) + has-focus (deref has-focus*) - empty-selected-value? (str/blank? selected-value) + listbox-id (mf/use-id) + + nodes-ref (mf/use-ref nil) + options-ref (mf/use-ref nil) + select-ref (mf/use-ref nil) + + empty-selected-id? + (str/blank? selected-id) 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 - (obj/set! refs id node) - (obj/unset! refs id))] - (mf/set-ref-val! options-nodes-refs refs)))) + (fn [node] + (let [state (mf/ref-val nodes-ref) + state (d/nilv state #js {}) + id (dom/get-data node "id") + state (obj/set! state id node)] + (mf/set-ref-val! nodes-ref state) + (fn [] + (let [state (mf/ref-val nodes-ref) + state (d/nilv state #js {}) + id (dom/get-data node "id") + state (obj/unset! state id)] + (mf/set-ref-val! nodes-ref state)))))) on-option-click (mf/use-fn @@ -109,13 +107,13 @@ (fn [event] (let [node (dom/get-current-target event) id (dom/get-data node "id")] - (reset! selected-value* id) - (reset! focused-value* nil) + (reset! selected-id* id) + (reset! focused-id* nil) (reset! is-open* false) (when (fn? on-change) (on-change id))))) - on-component-click + on-click (mf/use-fn (mf/deps disabled) (fn [event] @@ -124,95 +122,108 @@ (when-not disabled (swap! is-open* not)))) - on-component-blur + on-blur (mf/use-fn (fn [event] - (let [target (.-relatedTarget event) - outside? (not (.contains (mf/ref-val select-ref) target))] - (when outside? - (reset! focused-value* nil) + (let [target (dom/get-related-target event) + select-node (mf/ref-val select-ref)] + (when-not (dom/is-child? select-node target) + (reset! focused-id* nil) (reset! is-open* false) (reset! has-focus* false))))) - on-component-focus + on-focus (mf/use-fn - (fn [_] - (reset! has-focus* true))) + #(reset! has-focus* true)) on-button-key-down (mf/use-fn - (mf/deps focused-value disabled) + (mf/deps focused-id 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-value*) (obj/get % "id")) options)] + len (count options) + index (d/index-of-pred options #(= focused-id (get % :id))) + nodes (mf/ref-val nodes-ref)] (cond (kbd/home? event) - (handle-focus-change options focused-value* 0 options-nodes-refs) + (handle-focus-change options focused-id* 0 nodes) (kbd/up-arrow? event) - (handle-focus-change options focused-value* (mod (- index 1) len) options-nodes-refs) + (handle-focus-change options focused-id* (mod (- index 1) len) nodes) (kbd/down-arrow? event) - (handle-focus-change options focused-value* (mod (+ index 1) len) options-nodes-refs) + (handle-focus-change options focused-id* (mod (+ index 1) len) nodes) - (or (kbd/space? event) (kbd/enter? event)) + (or (kbd/space? event) + (kbd/enter? event)) (when (deref is-open*) (dom/prevent-default event) - (handle-selection focused-value* selected-value* is-open*)) + (handle-selection focused-id* selected-id* is-open*)) (kbd/esc? event) (do (reset! is-open* false) - (reset! focused-value* nil))))))) + (reset! focused-id* nil))))))) - class (dm/str class " " (stl/css-case :select true - :focused has-focus)) + select-class + (stl/css-case :select true + :focused has-focus) - props (mf/spread-props props {:class class - :role "combobox" - :aria-controls listbox-id - :aria-haspopup "listbox" - :aria-activedescendant focused-value - :aria-expanded is-open - :on-key-down on-button-key-down - :disabled disabled - :on-click on-component-click}) + props + (mf/spread-props props {:class [class select-class] + :role "combobox" + :aria-controls listbox-id + :aria-haspopup "listbox" + :aria-activedescendant focused-id + :aria-expanded is-open + :on-key-down on-button-key-down + :disabled disabled + :on-click on-click}) - selected-option (get-option options selected-value) - label (obj/get selected-option "label") - icon (obj/get selected-option "icon")] + selected-option + (mf/with-memo [options selected-id] + (get-option options selected-id)) + + label + (get selected-option :label) + + icon + (get selected-option :icon) + + has-icon? + (some? icon)] (mf/with-effect [options] (mf/set-ref-val! options-ref options)) [:div {:class (stl/css :select-wrapper) - :on-click on-component-click - :on-focus on-component-focus + :on-click on-click + :on-focus on-focus :ref select-ref - :on-blur on-component-blur} + :on-blur on-blur} [:> :button props [:span {:class (stl/css-case :select-header true - :header-icon (some? icon))} - (when icon - [:> icon* {:icon-id icon - :size "s" - :aria-hidden true}]) + :header-icon has-icon?)} + (when ^boolean has-icon? + [:> i/icon* {:icon-id icon + :size "s" + :aria-hidden true}]) [: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}]] + :header-label-dimmed empty-selected-id?)} + (if ^boolean empty-selected-id? "--" label)]] - (when is-open + [:> i/icon* {:icon-id i/arrow + :class (stl/css :arrow) + :size "m" + :aria-hidden true}]] + + (when ^boolean is-open [:> options-dropdown* {:on-click on-option-click :id listbox-id :options options - :selected selected-value - :focused focused-value + :selected selected-id + :focused focused-id :empty-to-end empty-to-end - :set-ref set-option-ref}])])) + :ref set-option-ref}])])) diff --git a/frontend/src/app/main/ui/ds/controls/select.stories.jsx b/frontend/src/app/main/ui/ds/controls/select.stories.jsx index 03f488e8e0..68774e833e 100644 --- a/frontend/src/app/main/ui/ds/controls/select.stories.jsx +++ b/frontend/src/app/main/ui/ds/controls/select.stories.jsx @@ -28,7 +28,7 @@ export default { }, { label: "Menu", - id: "opeion-menu", + id: "option-menu", }, ], defaultSelected: "option-code", 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 d92926cbe2..437c05b360 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 @@ -8,74 +8,114 @@ (:require-macros [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] + [app.main.ui.ds.foundations.assets.icon :as i] [cuerdas.core :as str] [rumext.v2 :as mf])) +(def ^:private schema:icon-list + [:and :string + [:fn {:error/message "invalid data: invalid icon"} #(contains? i/icon-list %)]]) + +(def schema:option + [:and + [:map {:title "option"} + [:id :string] + [:icon {:optional true} schema:icon-list] + [:label {:optional true} :string] + [:aria-label {:optional true} :string]] + [:fn {:error/message "invalid data: missing required props"} + (fn [option] + (or (and (contains? option :icon) + (or (contains? option :label) + (contains? option :aria-label))) + (contains? option :label)))]]) + +(def ^:private schema:options-dropdown + [:map + [:ref {:optional true} fn?] + [:on-click fn?] + [:options [:vector schema:option]] + [:selected :any] + [:focused :any] + [:empty-to-end {:optional true} :boolean]]) + +(def ^:private + xf:filter-blank-id + (filter #(str/blank? (get % :id)))) + +(def ^:private + xf:filter-non-blank-id + (remove #(str/blank? (get % :id)))) + (mf/defc option* {::mf/private true} - [{:keys [id label icon aria-label on-click selected set-ref focused dimmed] :rest props}] + [{:keys [id ref label icon aria-label on-click selected focused dimmed] :rest props}] + (let [class (stl/css-case :option true + :option-with-icon (some? icon) + :option-selected selected + :option-current focused)] + [:li {:value id + :class class + :aria-selected selected + :ref ref + :role "option" + :id id + :on-click on-click + :data-id id + :data-testid "dropdown-option"} - [:> :li {:value id - :class (stl/css-case :option true - :option-with-icon (some? icon) - :option-selected selected - :option-current focused) - :aria-selected selected - :ref (fn [node] - (set-ref node id)) - :role "option" - :id id - :on-click on-click - :data-id id - :data-testid "dropdown-option"} + (when (some? icon) + [:> i/icon* + {:icon-id icon + :size "s" + :class (stl/css :option-icon) + :aria-hidden (when label true) + :aria-label (when (not label) aria-label)}]) - (when (some? icon) - [:> icon* - {:icon-id icon - :size "s" - :class (stl/css :option-icon) - :aria-hidden (when label true) - :aria-label (when (not label) aria-label)}]) + [:span {:class (stl/css-case :option-text true + :option-text-dimmed dimmed)} + label] - [:span {:class (stl/css-case :option-text true - :option-text-dimmed dimmed)} label] - (when selected - [:> icon* - {:icon-id i/tick - :size "s" - :class (stl/css :option-check) - :aria-hidden (when label true)}])]) + (when selected + [:> i/icon* + {:icon-id i/tick + :size "s" + :class (stl/css :option-check) + :aria-hidden (when label true)}])])) (mf/defc options-dropdown* - {::mf/props :obj} - [{: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"}) + {::mf/schema schema:options-dropdown} + [{:keys [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"}) - 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)] + options-blank + (mf/with-memo [empty-to-end options] + (when ^boolean empty-to-end + (into [] xf:filter-blank-id options))) - [:> "ul" props - (for [option ^js options] - (let [id (obj/get option "id") - label (obj/get option "label") - aria-label (obj/get option "aria-label") - icon (obj/get option "icon")] + options + (mf/with-memo [empty-to-end options] + (if ^boolean empty-to-end + (into [] xf:filter-non-blank-id options) + options))] + + [:> :ul props + (for [option options] + (let [id (get option :id) + label (get option :label) + aria-label (get option :aria-label) + icon (get option :icon)] [:> option* {:selected (= id selected) :key id :id id :label label :icon icon :aria-label aria-label - :set-ref set-ref + :ref ref :focused (= id focused) :dimmed false :on-click on-click}])) @@ -85,18 +125,18 @@ (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")] + (for [option options-blank] + (let [id (get option :id) + label (get option :label) + aria-label (get option :aria-label) + icon (get option :icon)] [:> option* {:selected (= id selected) :key id :id id :label label :icon icon :aria-label aria-label - :set-ref set-ref + :ref ref :focused (= id focused) :dimmed true :on-click on-click}]))])])) diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index e76f8d71ae..2458fbed67 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -220,6 +220,7 @@ (when on-dispose (on-dispose)))))))) ;; https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state +;; FIXME: replace with rumext (defn use-previous "Returns the value from previous render cycle." [value] @@ -238,6 +239,7 @@ (reset! ptr value)) ptr)) +;; FIXME: replace with rumext (defn use-update-ref [value] (let [ref (mf/use-ref value)] @@ -245,6 +247,7 @@ (mf/set-ref-val! ref value)) ref)) +;; FIXME: replace with rumext (defn use-ref-callback "Returns a stable callback pointer what calls the interned callback. The interned callback will be automatically updated on @@ -260,6 +263,7 @@ (when ^boolean obj (apply (.-f obj) args))))))) +;; FIXME: replace with rumext (defn use-ref-value "Returns a ref that will be automatically updated when the value is changed" [v] @@ -268,6 +272,7 @@ (mf/set-ref-val! ref v)) ref)) +;; FIXME: replace with rumext (defn use-equal-memo [val] (let [ref (mf/use-ref nil)] @@ -285,6 +290,7 @@ (mf/with-memo [focus objects] (cpf/focus-objects objects focus)))) +;; FIXME: replace with rumext (defn use-debounce [ms value] (let [[state update-state-fn] (mf/useState value) 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 3d5b02a686..e6e3adadc4 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 @@ -365,7 +365,7 @@ [:> combobox* {:id (str "variant-prop-" variant-id "-" pos) :placeholder (if mixed-value? (tr "settings.multiple") "--") :default-selected (if mixed-value? "" (:value prop)) - :options (clj->js (get-options (:name prop))) + :options (get-options (:name prop)) :empty-to-end true :max-length ctv/property-max-length :on-change (partial update-property-value pos)}])]])] @@ -438,7 +438,7 @@ [:span {:class (stl/css :variant-property-name)} (:name prop)] [:> select* {:default-selected (:value prop) - :options (clj->js (get-options (:name prop))) + :options (get-options (:name prop)) :empty-to-end true :on-change (partial switch-component pos)}]]])]