mirror of
https://github.com/penpot/penpot.git
synced 2025-08-07 14:38:33 +02:00
♻️ Add minor refactor to options dropdown options handling and validation (#6739)
* ♻️ Refactor options-dropdown* and related components * 🐛 Fix props error * 🐛 Fix test * 📎 Update rumext --------- Co-authored-by: Eva Marco <evamarcod@gmail.com>
This commit is contained in:
parent
6bd3253e5e
commit
403d92838a
7 changed files with 336 additions and 289 deletions
|
@ -20,8 +20,8 @@
|
||||||
:git/url "https://github.com/funcool/beicon.git"}
|
:git/url "https://github.com/funcool/beicon.git"}
|
||||||
|
|
||||||
funcool/rumext
|
funcool/rumext
|
||||||
{:git/tag "v2.22"
|
{:git/tag "v2.24"
|
||||||
:git/sha "92879b6"
|
:git/sha "17a0c94"
|
||||||
:git/url "https://github.com/funcool/rumext.git"}
|
:git/url "https://github.com/funcool/rumext.git"}
|
||||||
|
|
||||||
instaparse/instaparse {:mvn/version "1.5.0"}
|
instaparse/instaparse {:mvn/version "1.5.0"}
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
:dev
|
:dev
|
||||||
{:extra-paths ["dev"]
|
{:extra-paths ["dev"]
|
||||||
:extra-deps
|
: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"}
|
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
|
||||||
org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
org.clojure/tools.namespace {:mvn/version "RELEASE"}
|
||||||
criterium/criterium {:mvn/version "RELEASE"}
|
criterium/criterium {:mvn/version "RELEASE"}
|
||||||
|
|
|
@ -6,102 +6,85 @@
|
||||||
|
|
||||||
(ns app.main.ui.ds.controls.combobox
|
(ns app.main.ui.ds.controls.combobox
|
||||||
(:require-macros
|
(:require-macros
|
||||||
[app.common.data.macros :as dm]
|
|
||||||
[app.main.style :as stl])
|
[app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.main.constants :refer [max-input-length]]
|
[app.main.constants :refer [max-input-length]]
|
||||||
[app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown*]]
|
[app.main.ui.ds.controls.select :refer [get-option handle-focus-change]]
|
||||||
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list] :as i]
|
[app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown* schema:option]]
|
||||||
[app.util.array :as array]
|
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.keyboard :as kbd]
|
[app.util.keyboard :as kbd]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[rumext.v2 :as mf]))
|
[cuerdas.core :as str]
|
||||||
|
[rumext.v2 :as mf]
|
||||||
(def listbox-id-index (atom 0))
|
[rumext.v2.util :as mfu]))
|
||||||
|
|
||||||
(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)))]])
|
|
||||||
|
|
||||||
(def ^:private schema:combobox
|
(def ^:private schema:combobox
|
||||||
[:map
|
[:map
|
||||||
[:id {:optional true} :string]
|
[:id {:optional true} :string]
|
||||||
[:options [:vector schema:combobox-option]]
|
[:options [:vector schema:option]]
|
||||||
[:class {:optional true} :string]
|
[:class {:optional true} :string]
|
||||||
[:max-length {:optional true} :int]
|
[:max-length {:optional true} :int]
|
||||||
[:placeholder {:optional true} :string]
|
[:placeholder {:optional true} :string]
|
||||||
[:disabled {:optional true} :boolean]
|
[:disabled {:optional true} :boolean]
|
||||||
[:default-selected {:optional true} :string]
|
[:default-selected {:optional true} :string]
|
||||||
[:on-change {:optional true} fn?]
|
[:on-change {:optional true} fn?]
|
||||||
[:empty-to-end {:optional true} :boolean]
|
[:empty-to-end {:optional true} [:maybe :boolean]]
|
||||||
[:has-error {:optional true} :boolean]])
|
[:has-error {:optional true} :boolean]])
|
||||||
|
|
||||||
(mf/defc combobox*
|
(mf/defc combobox*
|
||||||
{::mf/schema schema:combobox}
|
{::mf/schema schema:combobox}
|
||||||
[{:keys [id options class placeholder disabled has-error default-selected max-length empty-to-end 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)
|
(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)
|
||||||
|
|
||||||
|
is-open* (mf/use-state false)
|
||||||
is-open (deref is-open*)
|
is-open (deref is-open*)
|
||||||
|
|
||||||
selected-value* (mf/use-state default-selected)
|
selected-id* (mf/use-state default-selected)
|
||||||
selected-value (deref selected-value*)
|
selected-id (deref selected-id*)
|
||||||
|
|
||||||
filter-value* (mf/use-state "")
|
filter-id* (mf/use-state "")
|
||||||
filter-value (deref filter-value*)
|
filter-id (deref filter-id*)
|
||||||
|
|
||||||
focused-value* (mf/use-state nil)
|
focused-id* (mf/use-state nil)
|
||||||
focused-value (deref focused-value*)
|
focused-id (deref focused-id*)
|
||||||
|
|
||||||
combobox-ref (mf/use-ref nil)
|
combobox-ref (mf/use-ref nil)
|
||||||
input-ref (mf/use-ref nil)
|
input-ref (mf/use-ref nil)
|
||||||
options-nodes-refs (mf/use-ref nil)
|
nodes-ref (mf/use-ref nil)
|
||||||
options-ref (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/use-id)
|
||||||
listbox-id (mf/ref-val listbox-id-ref)
|
|
||||||
|
|
||||||
dropdown-options
|
dropdown-options
|
||||||
(mf/use-memo
|
(mf/with-memo [options filter-id]
|
||||||
(mf/deps options filter-value)
|
|
||||||
(fn []
|
|
||||||
(->> options
|
(->> options
|
||||||
(array/filter (fn [option]
|
(filterv (fn [option]
|
||||||
(let [lower-option (.toLowerCase (obj/get option "id"))
|
(let [option (str/lower (get option :id))
|
||||||
lower-filter (.toLowerCase filter-value)]
|
filter (str/lower filter-id)]
|
||||||
(.includes lower-option lower-filter)))))))
|
(str/includes? option filter))))
|
||||||
|
(not-empty)))
|
||||||
|
|
||||||
set-option-ref
|
set-option-ref
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [node id]
|
(fn [node]
|
||||||
(let [refs (or (mf/ref-val options-nodes-refs) #js {})
|
(let [state (mf/ref-val nodes-ref)
|
||||||
refs (if node
|
state (d/nilv state #js {})
|
||||||
(obj/set! refs id node)
|
id (dom/get-data node "id")
|
||||||
(obj/unset! refs id))]
|
state (obj/set! state id node)]
|
||||||
(mf/set-ref-val! options-nodes-refs refs))))
|
(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
|
on-option-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
@ -110,34 +93,36 @@
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
(let [node (dom/get-current-target event)
|
(let [node (dom/get-current-target event)
|
||||||
id (dom/get-data node "id")]
|
id (dom/get-data node "id")]
|
||||||
(reset! selected-value* id)
|
(reset! selected-id* id)
|
||||||
(reset! is-open* false)
|
(reset! is-open* false)
|
||||||
(reset! focused-value* nil)
|
(reset! focused-id* nil)
|
||||||
(when (fn? on-change)
|
(when (fn? on-change)
|
||||||
(on-change id)))))
|
(on-change id)))))
|
||||||
|
|
||||||
on-component-click
|
on-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps disabled)
|
(mf/deps disabled)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
(when-not disabled
|
(when-not disabled
|
||||||
(when-not (deref is-open*)
|
(when-not (deref is-open*)
|
||||||
(reset! filter-value* ""))
|
(reset! filter-id* ""))
|
||||||
(swap! is-open* not))))
|
(swap! is-open* not))))
|
||||||
|
|
||||||
on-component-blur
|
|
||||||
|
on-blur
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-change)
|
(mf/deps on-change)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
(let [target (.-relatedTarget event)
|
(let [target (dom/get-related-target event)
|
||||||
outside? (not (.contains (mf/ref-val combobox-ref) target))]
|
self-node (mf/ref-val combobox-ref)]
|
||||||
(when outside?
|
(when-not (dom/is-child? self-node target)
|
||||||
(reset! is-open* false)
|
(reset! is-open* false)
|
||||||
(reset! focused-value* nil)
|
(reset! focused-id* nil)
|
||||||
(when (fn? on-change)
|
(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
|
on-input-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
@ -146,7 +131,7 @@
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
(when-not disabled
|
(when-not disabled
|
||||||
(when-not (deref is-open*)
|
(when-not (deref is-open*)
|
||||||
(reset! filter-value* ""))
|
(reset! filter-id* ""))
|
||||||
(reset! is-open* true))))
|
(reset! is-open* true))))
|
||||||
|
|
||||||
on-input-focus
|
on-input-focus
|
||||||
|
@ -158,47 +143,47 @@
|
||||||
|
|
||||||
on-input-key-down
|
on-input-key-down
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps is-open focused-value disabled dropdown-options)
|
(mf/deps is-open focused-id disabled)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
(when-not disabled
|
(when-not disabled
|
||||||
(let [len (alength dropdown-options)
|
(let [options (mf/ref-val options-ref)
|
||||||
index (array/find-index #(= (deref focused-value*) (obj/get % "id")) dropdown-options)]
|
len (count options)
|
||||||
|
index (d/index-of-pred options #(= focused-id (get % :id)))
|
||||||
(when (< len 0)
|
index (d/nilv index -1)
|
||||||
(reset! index len))
|
nodes (mf/ref-val nodes-ref)]
|
||||||
|
|
||||||
(if is-open
|
(if is-open
|
||||||
(cond
|
(cond
|
||||||
(kbd/home? event)
|
(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)
|
(kbd/up-arrow? event)
|
||||||
(let [new-index (if (= index -1)
|
(let [new-index (if (= index -1)
|
||||||
(dec len)
|
(dec len)
|
||||||
(mod (- index 1) 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)
|
(kbd/down-arrow? event)
|
||||||
(let [new-index (if (= index -1)
|
(let [new-index (if (= index -1)
|
||||||
0
|
0
|
||||||
(mod (+ index 1) 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/enter? event)
|
(kbd/enter? event)
|
||||||
(do
|
(do
|
||||||
(reset! selected-value* focused-value)
|
(reset! selected-id* focused-id)
|
||||||
(reset! is-open* false)
|
(reset! is-open* false)
|
||||||
(reset! focused-value* nil)
|
(reset! focused-id* nil)
|
||||||
(dom/blur! (mf/ref-val input-ref))
|
(dom/blur! (mf/ref-val input-ref))
|
||||||
(when (and (fn? on-change)
|
(when (and (fn? on-change)
|
||||||
(some? focused-value))
|
(some? focused-id))
|
||||||
(on-change focused-value)))
|
(on-change focused-id)))
|
||||||
|
|
||||||
(kbd/esc? event)
|
(kbd/esc? event)
|
||||||
(do (reset! is-open* false)
|
(do (reset! is-open* false)
|
||||||
(reset! focused-value* nil)
|
(reset! focused-id* nil)
|
||||||
(dom/blur! (mf/ref-val input-ref))))
|
(dom/blur! (mf/ref-val input-ref))))
|
||||||
|
|
||||||
(cond
|
(cond
|
||||||
|
@ -215,20 +200,24 @@
|
||||||
(let [value (-> event
|
(let [value (-> event
|
||||||
dom/get-target
|
dom/get-target
|
||||||
dom/get-value)]
|
dom/get-value)]
|
||||||
(reset! selected-value* value)
|
(reset! selected-id* value)
|
||||||
(reset! filter-value* value)
|
(reset! filter-id* value)
|
||||||
(reset! focused-value* nil))))
|
(reset! focused-id* nil))))
|
||||||
|
|
||||||
selected-option (get-option options selected-value)
|
selected-option
|
||||||
icon (obj/get selected-option "icon")]
|
(mf/with-memo [options selected-id]
|
||||||
|
(get-option options selected-id))
|
||||||
|
|
||||||
(mf/with-effect [options]
|
icon
|
||||||
(mf/set-ref-val! options-ref options))
|
(get selected-option :icon)]
|
||||||
|
|
||||||
|
(mf/with-effect [dropdown-options]
|
||||||
|
(mf/set-ref-val! options-ref dropdown-options))
|
||||||
|
|
||||||
(mf/use-effect
|
(mf/use-effect
|
||||||
(mf/deps default-selected)
|
(mf/deps default-selected)
|
||||||
(fn []
|
(fn []
|
||||||
(reset! selected-value* default-selected)))
|
(reset! selected-id* default-selected)))
|
||||||
|
|
||||||
[:div {:ref combobox-ref
|
[:div {:ref combobox-ref
|
||||||
:class (stl/css-case
|
:class (stl/css-case
|
||||||
|
@ -236,14 +225,14 @@
|
||||||
:has-error has-error
|
:has-error has-error
|
||||||
:disabled disabled)}
|
:disabled disabled)}
|
||||||
|
|
||||||
[:div {:class (dm/str class " " (stl/css :combobox))
|
[:div {:class [class (stl/css :combobox)]
|
||||||
:on-blur on-component-blur
|
:on-blur on-blur
|
||||||
:on-click on-component-click}
|
:on-click on-click}
|
||||||
|
|
||||||
[:span {:class (stl/css-case :header true
|
[:span {:class (stl/css-case :header true
|
||||||
:header-icon (some? icon))}
|
:header-icon (some? icon))}
|
||||||
(when icon
|
(when icon
|
||||||
[:> icon* {:icon-id icon
|
[:> i/icon* {:icon-id icon
|
||||||
:size "s"
|
:size "s"
|
||||||
:aria-hidden true}])
|
:aria-hidden true}])
|
||||||
[:input {:id id
|
[:input {:id id
|
||||||
|
@ -255,11 +244,11 @@
|
||||||
:aria-autocomplete "both"
|
:aria-autocomplete "both"
|
||||||
:aria-expanded is-open
|
:aria-expanded is-open
|
||||||
:aria-controls listbox-id
|
:aria-controls listbox-id
|
||||||
:aria-activedescendant focused-value
|
:aria-activedescendant focused-id
|
||||||
:data-testid "combobox-input"
|
:data-testid "combobox-input"
|
||||||
:max-length (d/nilv max-length max-input-length)
|
:max-length (d/nilv max-length max-input-length)
|
||||||
:disabled disabled
|
:disabled disabled
|
||||||
:value (d/nilv selected-value "")
|
:value (d/nilv selected-id "")
|
||||||
:placeholder placeholder
|
:placeholder placeholder
|
||||||
:on-change on-input-change
|
:on-change on-input-change
|
||||||
:on-click on-input-click
|
:on-click on-input-click
|
||||||
|
@ -267,24 +256,25 @@
|
||||||
:on-key-down on-input-key-down}]]
|
:on-key-down on-input-key-down}]]
|
||||||
|
|
||||||
(when (d/not-empty? options)
|
(when (d/not-empty? options)
|
||||||
[:> :button {:type "button"
|
[:button {:type "button"
|
||||||
:tab-index "-1"
|
:tab-index "-1"
|
||||||
:aria-expanded is-open
|
:aria-expanded is-open
|
||||||
:aria-controls listbox-id
|
:aria-controls listbox-id
|
||||||
:class (stl/css :button-toggle-list)
|
:class (stl/css :button-toggle-list)
|
||||||
:on-click on-component-click}
|
:on-click on-click}
|
||||||
[:> icon* {:icon-id i/arrow
|
[:> i/icon* {:icon-id i/arrow
|
||||||
:class (stl/css :arrow)
|
:class (stl/css :arrow)
|
||||||
:size "s"
|
:size "s"
|
||||||
:aria-hidden true
|
:aria-hidden true
|
||||||
:data-testid "combobox-open-button"}]])]
|
: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* {:on-click on-option-click
|
||||||
:options dropdown-options
|
:options dropdown-options
|
||||||
:selected selected-value
|
:selected selected-id
|
||||||
:focused focused-value
|
:focused focused-id
|
||||||
:set-ref set-option-ref
|
:ref set-option-ref
|
||||||
:id listbox-id
|
:id listbox-id
|
||||||
:empty-to-end empty-to-end
|
:empty-to-end empty-to-end
|
||||||
:data-testid "combobox-options"}])]))
|
:data-testid "combobox-options"}])]))
|
||||||
|
|
|
@ -6,50 +6,34 @@
|
||||||
|
|
||||||
(ns app.main.ui.ds.controls.select
|
(ns app.main.ui.ds.controls.select
|
||||||
(:require-macros
|
(:require-macros
|
||||||
[app.common.data.macros :as dm]
|
|
||||||
[app.main.style :as stl])
|
[app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
[app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown*]]
|
[app.common.data :as d]
|
||||||
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list] :as i]
|
[app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown* schema:option]]
|
||||||
[app.util.array :as array]
|
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.keyboard :as kbd]
|
[app.util.keyboard :as kbd]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]
|
||||||
|
[rumext.v2.util :as mfu]))
|
||||||
|
|
||||||
(def listbox-id-index (atom 0))
|
(defn get-option
|
||||||
|
|
||||||
(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
|
|
||||||
[options id]
|
[options id]
|
||||||
(or (array/find #(= id (obj/get % "id")) options)
|
(or (d/seek #(= id (get % :id)) options)
|
||||||
(aget options 0)))
|
(nth options 0)))
|
||||||
|
|
||||||
(defn- get-selected-option-id
|
(defn- get-selected-option-id
|
||||||
[options default]
|
[options default]
|
||||||
(let [option (get-option 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]
|
;; Also used in combobox
|
||||||
(let [option (aget options new-index)
|
(defn handle-focus-change
|
||||||
id (obj/get option "id")
|
[options focused* new-index nodes]
|
||||||
nodes (mf/ref-val options-nodes-refs)
|
(let [option (get options new-index)
|
||||||
|
id (get option :id)
|
||||||
node (obj/get nodes id)]
|
node (obj/get nodes id)]
|
||||||
(reset! focused* id)
|
(reset! focused* id)
|
||||||
(dom/scroll-into-view-if-needed! node)))
|
(dom/scroll-into-view-if-needed! node)))
|
||||||
|
@ -63,45 +47,59 @@
|
||||||
|
|
||||||
(def ^:private schema:select
|
(def ^:private schema:select
|
||||||
[:map
|
[:map
|
||||||
[:options [:vector {:min 1} schema:select-option]]
|
[:options [:vector {:min 1} schema:option]]
|
||||||
[:class {:optional true} :string]
|
[:class {:optional true} :string]
|
||||||
[:disabled {:optional true} :boolean]
|
[:disabled {:optional true} :boolean]
|
||||||
[:default-selected {:optional true} :string]
|
[:default-selected {:optional true} :string]
|
||||||
[:empty-to-end {:optional true} :boolean]
|
[:empty-to-end {:optional true} [:maybe :boolean]]
|
||||||
[:on-change {:optional true} fn?]])
|
[:on-change {:optional true} fn?]])
|
||||||
|
|
||||||
(mf/defc select*
|
(mf/defc select*
|
||||||
{::mf/schema schema:select}
|
{::mf/schema schema:select}
|
||||||
[{:keys [options class disabled default-selected empty-to-end on-change] :rest props}]
|
[{:keys [options class disabled default-selected empty-to-end on-change] :rest props}]
|
||||||
(let [is-open* (mf/use-state false)
|
(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)
|
||||||
|
is-open* (mf/use-state false)
|
||||||
is-open (deref is-open*)
|
is-open (deref is-open*)
|
||||||
|
|
||||||
selected-value* (mf/use-state #(get-selected-option-id options default-selected))
|
selected-id* (mf/use-state #(get-selected-option-id options default-selected))
|
||||||
selected-value (deref selected-value*)
|
selected-id (deref selected-id*)
|
||||||
|
|
||||||
focused-value* (mf/use-state nil)
|
focused-id* (mf/use-state selected-id)
|
||||||
focused-value (deref focused-value*)
|
focused-id (deref focused-id*)
|
||||||
|
|
||||||
has-focus* (mf/use-state false)
|
has-focus* (mf/use-state false)
|
||||||
has-focus (deref has-focus*)
|
has-focus (deref has-focus*)
|
||||||
|
|
||||||
listbox-id-ref (mf/use-ref (dm/str "select-listbox-" (swap! listbox-id-index inc)))
|
listbox-id (mf/use-id)
|
||||||
options-nodes-refs (mf/use-ref nil)
|
|
||||||
|
nodes-ref (mf/use-ref nil)
|
||||||
options-ref (mf/use-ref nil)
|
options-ref (mf/use-ref nil)
|
||||||
select-ref (mf/use-ref nil)
|
select-ref (mf/use-ref nil)
|
||||||
listbox-id (mf/ref-val listbox-id-ref)
|
|
||||||
|
|
||||||
empty-selected-value? (str/blank? selected-value)
|
empty-selected-id?
|
||||||
|
(str/blank? selected-id)
|
||||||
|
|
||||||
set-option-ref
|
set-option-ref
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps options-nodes-refs)
|
(fn [node]
|
||||||
(fn [node id]
|
(let [state (mf/ref-val nodes-ref)
|
||||||
(let [refs (or (mf/ref-val options-nodes-refs) #js {})
|
state (d/nilv state #js {})
|
||||||
refs (if node
|
id (dom/get-data node "id")
|
||||||
(obj/set! refs id node)
|
state (obj/set! state id node)]
|
||||||
(obj/unset! refs id))]
|
(mf/set-ref-val! nodes-ref state)
|
||||||
(mf/set-ref-val! options-nodes-refs refs))))
|
(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
|
on-option-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
@ -109,13 +107,13 @@
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(let [node (dom/get-current-target event)
|
(let [node (dom/get-current-target event)
|
||||||
id (dom/get-data node "id")]
|
id (dom/get-data node "id")]
|
||||||
(reset! selected-value* id)
|
(reset! selected-id* id)
|
||||||
(reset! focused-value* nil)
|
(reset! focused-id* nil)
|
||||||
(reset! is-open* false)
|
(reset! is-open* false)
|
||||||
(when (fn? on-change)
|
(when (fn? on-change)
|
||||||
(on-change id)))))
|
(on-change id)))))
|
||||||
|
|
||||||
on-component-click
|
on-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps disabled)
|
(mf/deps disabled)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
|
@ -124,95 +122,108 @@
|
||||||
(when-not disabled
|
(when-not disabled
|
||||||
(swap! is-open* not))))
|
(swap! is-open* not))))
|
||||||
|
|
||||||
on-component-blur
|
on-blur
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(let [target (.-relatedTarget event)
|
(let [target (dom/get-related-target event)
|
||||||
outside? (not (.contains (mf/ref-val select-ref) target))]
|
select-node (mf/ref-val select-ref)]
|
||||||
(when outside?
|
(when-not (dom/is-child? select-node target)
|
||||||
(reset! focused-value* nil)
|
(reset! focused-id* nil)
|
||||||
(reset! is-open* false)
|
(reset! is-open* false)
|
||||||
(reset! has-focus* false)))))
|
(reset! has-focus* false)))))
|
||||||
|
|
||||||
on-component-focus
|
on-focus
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [_]
|
#(reset! has-focus* true))
|
||||||
(reset! has-focus* true)))
|
|
||||||
|
|
||||||
on-button-key-down
|
on-button-key-down
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps focused-value disabled)
|
(mf/deps focused-id disabled)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
(when-not disabled
|
(when-not disabled
|
||||||
(let [options (mf/ref-val options-ref)
|
(let [options (mf/ref-val options-ref)
|
||||||
len (alength options)
|
len (count options)
|
||||||
index (array/find-index #(= (deref focused-value*) (obj/get % "id")) options)]
|
index (d/index-of-pred options #(= focused-id (get % :id)))
|
||||||
|
nodes (mf/ref-val nodes-ref)]
|
||||||
(cond
|
(cond
|
||||||
(kbd/home? event)
|
(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)
|
(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)
|
(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*)
|
(when (deref is-open*)
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
(handle-selection focused-value* selected-value* is-open*))
|
(handle-selection focused-id* selected-id* is-open*))
|
||||||
|
|
||||||
(kbd/esc? event)
|
(kbd/esc? event)
|
||||||
(do (reset! is-open* false)
|
(do (reset! is-open* false)
|
||||||
(reset! focused-value* nil)))))))
|
(reset! focused-id* nil)))))))
|
||||||
|
|
||||||
class (dm/str class " " (stl/css-case :select true
|
select-class
|
||||||
:focused has-focus))
|
(stl/css-case :select true
|
||||||
|
:focused has-focus)
|
||||||
|
|
||||||
props (mf/spread-props props {:class class
|
props
|
||||||
|
(mf/spread-props props {:class [class select-class]
|
||||||
:role "combobox"
|
:role "combobox"
|
||||||
:aria-controls listbox-id
|
:aria-controls listbox-id
|
||||||
:aria-haspopup "listbox"
|
:aria-haspopup "listbox"
|
||||||
:aria-activedescendant focused-value
|
:aria-activedescendant focused-id
|
||||||
:aria-expanded is-open
|
:aria-expanded is-open
|
||||||
:on-key-down on-button-key-down
|
:on-key-down on-button-key-down
|
||||||
:disabled disabled
|
:disabled disabled
|
||||||
:on-click on-component-click})
|
:on-click on-click})
|
||||||
|
|
||||||
selected-option (get-option options selected-value)
|
selected-option
|
||||||
label (obj/get selected-option "label")
|
(mf/with-memo [options selected-id]
|
||||||
icon (obj/get selected-option "icon")]
|
(get-option options selected-id))
|
||||||
|
|
||||||
|
label
|
||||||
|
(get selected-option :label)
|
||||||
|
|
||||||
|
icon
|
||||||
|
(get selected-option :icon)
|
||||||
|
|
||||||
|
has-icon?
|
||||||
|
(some? icon)]
|
||||||
|
|
||||||
(mf/with-effect [options]
|
(mf/with-effect [options]
|
||||||
(mf/set-ref-val! options-ref options))
|
(mf/set-ref-val! options-ref options))
|
||||||
|
|
||||||
[:div {:class (stl/css :select-wrapper)
|
[:div {:class (stl/css :select-wrapper)
|
||||||
:on-click on-component-click
|
:on-click on-click
|
||||||
:on-focus on-component-focus
|
:on-focus on-focus
|
||||||
:ref select-ref
|
:ref select-ref
|
||||||
:on-blur on-component-blur}
|
:on-blur on-blur}
|
||||||
|
|
||||||
[:> :button props
|
[:> :button props
|
||||||
[:span {:class (stl/css-case :select-header true
|
[:span {:class (stl/css-case :select-header true
|
||||||
:header-icon (some? icon))}
|
:header-icon has-icon?)}
|
||||||
(when icon
|
(when ^boolean has-icon?
|
||||||
[:> icon* {:icon-id icon
|
[:> i/icon* {:icon-id icon
|
||||||
:size "s"
|
:size "s"
|
||||||
:aria-hidden true}])
|
:aria-hidden true}])
|
||||||
[:span {:class (stl/css-case :header-label true
|
[:span {:class (stl/css-case :header-label true
|
||||||
:header-label-dimmed empty-selected-value?)}
|
:header-label-dimmed empty-selected-id?)}
|
||||||
(if empty-selected-value? "--" label)]]
|
(if ^boolean empty-selected-id? "--" label)]]
|
||||||
[:> icon* {:icon-id i/arrow
|
|
||||||
|
[:> i/icon* {:icon-id i/arrow
|
||||||
:class (stl/css :arrow)
|
:class (stl/css :arrow)
|
||||||
:size "m"
|
:size "m"
|
||||||
:aria-hidden true}]]
|
:aria-hidden true}]]
|
||||||
|
|
||||||
(when is-open
|
(when ^boolean is-open
|
||||||
[:> options-dropdown* {:on-click on-option-click
|
[:> options-dropdown* {:on-click on-option-click
|
||||||
:id listbox-id
|
:id listbox-id
|
||||||
:options options
|
:options options
|
||||||
:selected selected-value
|
:selected selected-id
|
||||||
:focused focused-value
|
:focused focused-id
|
||||||
:empty-to-end empty-to-end
|
:empty-to-end empty-to-end
|
||||||
:set-ref set-option-ref}])]))
|
:ref set-option-ref}])]))
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Menu",
|
label: "Menu",
|
||||||
id: "opeion-menu",
|
id: "option-menu",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
defaultSelected: "option-code",
|
defaultSelected: "option-code",
|
||||||
|
|
|
@ -8,24 +8,56 @@
|
||||||
(:require-macros
|
(:require-macros
|
||||||
[app.main.style :as stl])
|
[app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||||
[app.util.array :as array]
|
|
||||||
[app.util.object :as obj]
|
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[rumext.v2 :as mf]))
|
[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/defc option*
|
||||||
{::mf/private true}
|
{::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
|
||||||
[:> :li {:value id
|
|
||||||
:class (stl/css-case :option true
|
|
||||||
:option-with-icon (some? icon)
|
:option-with-icon (some? icon)
|
||||||
:option-selected selected
|
:option-selected selected
|
||||||
:option-current focused)
|
:option-current focused)]
|
||||||
|
[:li {:value id
|
||||||
|
:class class
|
||||||
:aria-selected selected
|
:aria-selected selected
|
||||||
:ref (fn [node]
|
:ref ref
|
||||||
(set-ref node id))
|
|
||||||
:role "option"
|
:role "option"
|
||||||
:id id
|
:id id
|
||||||
:on-click on-click
|
:on-click on-click
|
||||||
|
@ -33,7 +65,7 @@
|
||||||
:data-testid "dropdown-option"}
|
:data-testid "dropdown-option"}
|
||||||
|
|
||||||
(when (some? icon)
|
(when (some? icon)
|
||||||
[:> icon*
|
[:> i/icon*
|
||||||
{:icon-id icon
|
{:icon-id icon
|
||||||
:size "s"
|
:size "s"
|
||||||
:class (stl/css :option-icon)
|
:class (stl/css :option-icon)
|
||||||
|
@ -41,41 +73,49 @@
|
||||||
:aria-label (when (not label) aria-label)}])
|
:aria-label (when (not label) aria-label)}])
|
||||||
|
|
||||||
[:span {:class (stl/css-case :option-text true
|
[:span {:class (stl/css-case :option-text true
|
||||||
:option-text-dimmed dimmed)} label]
|
:option-text-dimmed dimmed)}
|
||||||
|
label]
|
||||||
|
|
||||||
(when selected
|
(when selected
|
||||||
[:> icon*
|
[:> i/icon*
|
||||||
{:icon-id i/tick
|
{:icon-id i/tick
|
||||||
:size "s"
|
:size "s"
|
||||||
:class (stl/css :option-check)
|
:class (stl/css :option-check)
|
||||||
:aria-hidden (when label true)}])])
|
:aria-hidden (when label true)}])]))
|
||||||
|
|
||||||
(mf/defc options-dropdown*
|
(mf/defc options-dropdown*
|
||||||
{::mf/props :obj}
|
{::mf/schema schema:options-dropdown}
|
||||||
[{:keys [set-ref on-click options selected focused empty-to-end] :rest props}]
|
[{:keys [ref on-click options selected focused empty-to-end] :rest props}]
|
||||||
(let [props (mf/spread-props props
|
(let [props
|
||||||
|
(mf/spread-props props
|
||||||
{:class (stl/css :option-list)
|
{:class (stl/css :option-list)
|
||||||
:tab-index "-1"
|
:tab-index "-1"
|
||||||
:role "listbox"})
|
:role "listbox"})
|
||||||
|
|
||||||
options-blank (when empty-to-end
|
options-blank
|
||||||
(array/filter #(str/blank? (obj/get % "id")) options))
|
(mf/with-memo [empty-to-end options]
|
||||||
options (if empty-to-end
|
(when ^boolean empty-to-end
|
||||||
(array/filter #((complement str/blank?) (obj/get % "id")) options)
|
(into [] xf:filter-blank-id options)))
|
||||||
options)]
|
|
||||||
|
|
||||||
[:> "ul" props
|
options
|
||||||
(for [option ^js options]
|
(mf/with-memo [empty-to-end options]
|
||||||
(let [id (obj/get option "id")
|
(if ^boolean empty-to-end
|
||||||
label (obj/get option "label")
|
(into [] xf:filter-non-blank-id options)
|
||||||
aria-label (obj/get option "aria-label")
|
options))]
|
||||||
icon (obj/get option "icon")]
|
|
||||||
|
[:> :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)
|
[:> option* {:selected (= id selected)
|
||||||
:key id
|
:key id
|
||||||
:id id
|
:id id
|
||||||
:label label
|
:label label
|
||||||
:icon icon
|
:icon icon
|
||||||
:aria-label aria-label
|
:aria-label aria-label
|
||||||
:set-ref set-ref
|
:ref ref
|
||||||
:focused (= id focused)
|
:focused (= id focused)
|
||||||
:dimmed false
|
:dimmed false
|
||||||
:on-click on-click}]))
|
:on-click on-click}]))
|
||||||
|
@ -85,18 +125,18 @@
|
||||||
(when (seq options)
|
(when (seq options)
|
||||||
[:hr {:class (stl/css :option-separator)}])
|
[:hr {:class (stl/css :option-separator)}])
|
||||||
|
|
||||||
(for [option ^js options-blank]
|
(for [option options-blank]
|
||||||
(let [id (obj/get option "id")
|
(let [id (get option :id)
|
||||||
label (obj/get option "label")
|
label (get option :label)
|
||||||
aria-label (obj/get option "aria-label")
|
aria-label (get option :aria-label)
|
||||||
icon (obj/get option "icon")]
|
icon (get option :icon)]
|
||||||
[:> option* {:selected (= id selected)
|
[:> option* {:selected (= id selected)
|
||||||
:key id
|
:key id
|
||||||
:id id
|
:id id
|
||||||
:label label
|
:label label
|
||||||
:icon icon
|
:icon icon
|
||||||
:aria-label aria-label
|
:aria-label aria-label
|
||||||
:set-ref set-ref
|
:ref ref
|
||||||
:focused (= id focused)
|
:focused (= id focused)
|
||||||
:dimmed true
|
:dimmed true
|
||||||
:on-click on-click}]))])]))
|
:on-click on-click}]))])]))
|
||||||
|
|
|
@ -220,6 +220,7 @@
|
||||||
(when on-dispose (on-dispose))))))))
|
(when on-dispose (on-dispose))))))))
|
||||||
|
|
||||||
;; https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
|
;; https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
|
||||||
|
;; FIXME: replace with rumext
|
||||||
(defn use-previous
|
(defn use-previous
|
||||||
"Returns the value from previous render cycle."
|
"Returns the value from previous render cycle."
|
||||||
[value]
|
[value]
|
||||||
|
@ -238,6 +239,7 @@
|
||||||
(reset! ptr value))
|
(reset! ptr value))
|
||||||
ptr))
|
ptr))
|
||||||
|
|
||||||
|
;; FIXME: replace with rumext
|
||||||
(defn use-update-ref
|
(defn use-update-ref
|
||||||
[value]
|
[value]
|
||||||
(let [ref (mf/use-ref value)]
|
(let [ref (mf/use-ref value)]
|
||||||
|
@ -245,6 +247,7 @@
|
||||||
(mf/set-ref-val! ref value))
|
(mf/set-ref-val! ref value))
|
||||||
ref))
|
ref))
|
||||||
|
|
||||||
|
;; FIXME: replace with rumext
|
||||||
(defn use-ref-callback
|
(defn use-ref-callback
|
||||||
"Returns a stable callback pointer what calls the interned
|
"Returns a stable callback pointer what calls the interned
|
||||||
callback. The interned callback will be automatically updated on
|
callback. The interned callback will be automatically updated on
|
||||||
|
@ -260,6 +263,7 @@
|
||||||
(when ^boolean obj
|
(when ^boolean obj
|
||||||
(apply (.-f obj) args)))))))
|
(apply (.-f obj) args)))))))
|
||||||
|
|
||||||
|
;; FIXME: replace with rumext
|
||||||
(defn use-ref-value
|
(defn use-ref-value
|
||||||
"Returns a ref that will be automatically updated when the value is changed"
|
"Returns a ref that will be automatically updated when the value is changed"
|
||||||
[v]
|
[v]
|
||||||
|
@ -268,6 +272,7 @@
|
||||||
(mf/set-ref-val! ref v))
|
(mf/set-ref-val! ref v))
|
||||||
ref))
|
ref))
|
||||||
|
|
||||||
|
;; FIXME: replace with rumext
|
||||||
(defn use-equal-memo
|
(defn use-equal-memo
|
||||||
[val]
|
[val]
|
||||||
(let [ref (mf/use-ref nil)]
|
(let [ref (mf/use-ref nil)]
|
||||||
|
@ -285,6 +290,7 @@
|
||||||
(mf/with-memo [focus objects]
|
(mf/with-memo [focus objects]
|
||||||
(cpf/focus-objects objects focus))))
|
(cpf/focus-objects objects focus))))
|
||||||
|
|
||||||
|
;; FIXME: replace with rumext
|
||||||
(defn use-debounce
|
(defn use-debounce
|
||||||
[ms value]
|
[ms value]
|
||||||
(let [[state update-state-fn] (mf/useState value)
|
(let [[state update-state-fn] (mf/useState value)
|
||||||
|
|
|
@ -365,7 +365,7 @@
|
||||||
[:> combobox* {:id (str "variant-prop-" variant-id "-" pos)
|
[:> combobox* {:id (str "variant-prop-" variant-id "-" pos)
|
||||||
:placeholder (if mixed-value? (tr "settings.multiple") "--")
|
:placeholder (if mixed-value? (tr "settings.multiple") "--")
|
||||||
:default-selected (if mixed-value? "" (:value prop))
|
:default-selected (if mixed-value? "" (:value prop))
|
||||||
:options (clj->js (get-options (:name prop)))
|
:options (get-options (:name prop))
|
||||||
:empty-to-end true
|
:empty-to-end true
|
||||||
:max-length ctv/property-max-length
|
:max-length ctv/property-max-length
|
||||||
:on-change (partial update-property-value pos)}])]])]
|
:on-change (partial update-property-value pos)}])]])]
|
||||||
|
@ -438,7 +438,7 @@
|
||||||
[:span {:class (stl/css :variant-property-name)}
|
[:span {:class (stl/css :variant-property-name)}
|
||||||
(:name prop)]
|
(:name prop)]
|
||||||
[:> select* {:default-selected (:value prop)
|
[:> select* {:default-selected (:value prop)
|
||||||
:options (clj->js (get-options (:name prop)))
|
:options (get-options (:name prop))
|
||||||
:empty-to-end true
|
:empty-to-end true
|
||||||
:on-change (partial switch-component pos)}]]])]
|
:on-change (partial switch-component pos)}]]])]
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue