mirror of
https://github.com/penpot/penpot.git
synced 2025-07-24 05:27:13 +02:00
✨ new combobox component to the ds with component testing
This commit is contained in:
parent
e675ff6db5
commit
2c8a44dfa1
12 changed files with 1191 additions and 315 deletions
252
frontend/src/app/main/ui/ds/controls/combobox.cljs
Normal file
252
frontend/src/app/main/ui/ds/controls/combobox.cljs
Normal file
|
@ -0,0 +1,252 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.ds.controls.combobox
|
||||
(: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.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)))
|
||||
|
||||
(defn- handle-selection
|
||||
[focused* selected* open*]
|
||||
(when-let [focused (deref focused*)]
|
||||
(reset! selected* focused))
|
||||
(reset! open* false)
|
||||
(reset! focused* nil))
|
||||
|
||||
(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
|
||||
[:map
|
||||
[:options [:vector {:min 1} schema:combobox-option]]
|
||||
[:class {:optional true} :string]
|
||||
[:disabled {:optional true} :boolean]
|
||||
[:default-selected {:optional true} :string]
|
||||
[:on-change {:optional true} fn?]])
|
||||
|
||||
(mf/defc combobox*
|
||||
{::mf/props :obj
|
||||
::mf/schema schema:combobox}
|
||||
[{:keys [options class disabled default-selected on-change] :rest props}]
|
||||
(let [open* (mf/use-state false)
|
||||
open (deref open*)
|
||||
|
||||
selected* (mf/use-state default-selected)
|
||||
selected (deref selected*)
|
||||
|
||||
focused* (mf/use-state nil)
|
||||
focused (deref focused*)
|
||||
|
||||
has-focus* (mf/use-state false)
|
||||
has-focus (deref has-focus*)
|
||||
|
||||
dropdown-options
|
||||
(mf/use-memo
|
||||
(mf/deps options selected)
|
||||
(fn []
|
||||
(->> options
|
||||
(array/filter (fn [option]
|
||||
(let [lower-option (.toLowerCase (obj/get option "id"))
|
||||
lower-filter (.toLowerCase selected)]
|
||||
(.includes lower-option lower-filter)))))))
|
||||
|
||||
on-click
|
||||
(mf/use-fn
|
||||
(mf/deps disabled)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(when-not disabled
|
||||
(reset! has-focus* true)
|
||||
(if (= "INPUT" (.-tagName (.-target event)))
|
||||
(reset! open* true)
|
||||
(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)))))
|
||||
|
||||
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)
|
||||
combobox-ref (mf/use-ref nil)
|
||||
|
||||
set-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))))
|
||||
|
||||
on-blur
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(let [target (.-relatedTarget event)
|
||||
outside? (not (.contains (mf/ref-val combobox-ref) target))]
|
||||
(when outside?
|
||||
(reset! focused* nil)
|
||||
(reset! open* false)
|
||||
(reset! has-focus* false)))))
|
||||
|
||||
on-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps open focused disabled dropdown-options)
|
||||
(fn [event]
|
||||
(when-not disabled
|
||||
(let [options dropdown-options
|
||||
focused (deref focused*)
|
||||
len (alength options)
|
||||
index (array/find-index #(= (deref focused*) (obj/get % "id")) options)]
|
||||
(dom/stop-propagation event)
|
||||
|
||||
(when (< len 0)
|
||||
(reset! index len))
|
||||
|
||||
(cond
|
||||
(and (not open) (kbd/down-arrow? event))
|
||||
(reset! open* true)
|
||||
|
||||
open
|
||||
(cond
|
||||
(kbd/home? event)
|
||||
(handle-focus-change options focused* 0 options-nodes-refs)
|
||||
|
||||
(kbd/up-arrow? event)
|
||||
(let [new-index (if (= index -1)
|
||||
(dec len)
|
||||
(mod (- index 1) len))]
|
||||
(handle-focus-change options focused* new-index options-nodes-refs))
|
||||
|
||||
|
||||
(kbd/down-arrow? event)
|
||||
(let [new-index (if (= index -1)
|
||||
0
|
||||
(mod (+ index 1) len))]
|
||||
(handle-focus-change options focused* new-index options-nodes-refs))
|
||||
|
||||
(or (kbd/space? event) (kbd/enter? event))
|
||||
(when (deref open*)
|
||||
(dom/prevent-default event)
|
||||
(handle-selection focused* selected* open*)
|
||||
(when (fn? on-change)
|
||||
(on-change focused)))
|
||||
|
||||
(kbd/esc? event)
|
||||
(do (reset! open* false)
|
||||
(reset! focused* nil))))))))
|
||||
|
||||
on-input-change
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(let [value (.-value (.-currentTarget event))]
|
||||
(reset! selected* value)
|
||||
(reset! focused* nil)
|
||||
(when (fn? on-change)
|
||||
(on-change value)))))
|
||||
on-focus
|
||||
(mf/use-fn
|
||||
(fn [_] (reset! has-focus* true)))
|
||||
|
||||
class (dm/str class " " (stl/css :combobox))
|
||||
|
||||
selected-option (get-option options selected)
|
||||
icon (obj/get selected-option "icon")]
|
||||
|
||||
(mf/with-effect [options]
|
||||
(mf/set-ref-val! options-ref options))
|
||||
|
||||
[:div {:ref combobox-ref
|
||||
:class (stl/css-case
|
||||
:combobox-wrapper true
|
||||
:focused has-focus)}
|
||||
|
||||
[:div {:class class
|
||||
:on-click on-click
|
||||
:on-focus on-focus
|
||||
:on-blur on-blur}
|
||||
[:span {:class (stl/css-case :combobox-header true
|
||||
:header-icon (some? icon))}
|
||||
(when icon
|
||||
[:> icon* {:id icon
|
||||
:size "s"
|
||||
:aria-hidden true}])
|
||||
[:input {:type "text"
|
||||
:role "combobox"
|
||||
:aria-autocomplete "both"
|
||||
:aria-expanded open
|
||||
:aria-controls listbox-id
|
||||
:aria-activedescendant focused
|
||||
:class (stl/css :input)
|
||||
:data-testid "combobox-input"
|
||||
:disabled disabled
|
||||
:value selected
|
||||
:on-change on-input-change
|
||||
:on-key-down on-key-down}]]
|
||||
|
||||
[:> :button {:tab-index "-1"
|
||||
:aria-expanded open
|
||||
:aria-controls listbox-id
|
||||
:class (stl/css :button-toggle-list)
|
||||
:on-click on-click}
|
||||
[:> icon* {:id i/arrow
|
||||
:class (stl/css :arrow)
|
||||
:size "s"
|
||||
:aria-hidden true
|
||||
:data-testid "combobox-open-button"}]]]
|
||||
|
||||
(when (and open (seq dropdown-options))
|
||||
[:> options-dropdown* {:on-click on-option-click
|
||||
:options dropdown-options
|
||||
:selected selected
|
||||
:focused focused
|
||||
:set-ref set-ref
|
||||
:id listbox-id
|
||||
:data-testid "combobox-options"}])]))
|
Loading…
Add table
Add a link
Reference in a new issue