mirror of
https://github.com/penpot/penpot.git
synced 2026-03-29 07:40:35 +02:00
🐛 Fix review comments (#8708)
* 🐛 Fix focus option only on arrowdown not at open * 🐛 Fix focus on input when visible focus should be on options * ♻️ Improve nativation, adding tab control and moving throught options is now cyclic * ✨ Add selected option when inside cursor is inside option * 🐛 Dropdown is positioned nex to the input alwais
This commit is contained in:
@@ -33,17 +33,7 @@
|
||||
[:label {:optional true} :string]
|
||||
[:aria-label {:optional true} :string]])
|
||||
|
||||
(def ^:private schema:options-dropdown
|
||||
[:map
|
||||
[:ref {:optional true} fn?]
|
||||
[:class {:optional true} :string]
|
||||
[:wrapper-ref {:optional true} :any]
|
||||
[:on-click fn?]
|
||||
[:options [:vector schema:option]]
|
||||
[:selected {:optional true} :any]
|
||||
[:focused {:optional true} :any]
|
||||
[:empty-to-end {:optional true} [:maybe :boolean]]
|
||||
[:align {:optional true} [:maybe [:enum :left :right]]]])
|
||||
|
||||
|
||||
(def ^:private
|
||||
xf:filter-blank-id
|
||||
@@ -104,6 +94,17 @@
|
||||
:dimmed (true? (:dimmed option))
|
||||
:on-click on-click}]))))
|
||||
|
||||
(def ^:private schema:options-dropdown
|
||||
[:map
|
||||
[:ref {:optional true} fn?]
|
||||
[:class {:optional true} :string]
|
||||
[:wrapper-ref {:optional true} :any]
|
||||
[:on-click fn?]
|
||||
[:options [:vector schema:option]]
|
||||
[:selected {:optional true} :any]
|
||||
[:focused {:optional true} :any]
|
||||
[:empty-to-end {:optional true} [:maybe :boolean]]
|
||||
[:align {:optional true} [:maybe [:enum :left :right]]]])
|
||||
|
||||
(mf/defc options-dropdown*
|
||||
{::mf/schema schema:options-dropdown}
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
has-hint hint-type
|
||||
max-length variant
|
||||
slot-start slot-end
|
||||
data-option-focused
|
||||
input-wrapper-ref
|
||||
aria-label] :rest props} ref]
|
||||
(let [input-ref (mf/use-ref)
|
||||
type (d/nilv type "text")
|
||||
@@ -74,7 +76,9 @@
|
||||
(dom/select-node input-node)
|
||||
(dom/focus! input-node))))]
|
||||
|
||||
[:div {:class [inside-class class]}
|
||||
[:div {:class [inside-class class]
|
||||
:ref input-wrapper-ref
|
||||
:data-option-focused data-option-focused}
|
||||
(when (some? slot-start)
|
||||
slot-start)
|
||||
(when (some? icon)
|
||||
|
||||
@@ -41,6 +41,11 @@
|
||||
--input-bg-color: var(--color-background-primary);
|
||||
--input-outline-color: var(--color-background-quaternary);
|
||||
}
|
||||
|
||||
&[data-option-focused="true"]:has(*:focus-visible) {
|
||||
--input-bg-color: var(--color-background-tertiary);
|
||||
--input-outline-color: none;
|
||||
}
|
||||
}
|
||||
|
||||
.variant-dense,
|
||||
|
||||
@@ -84,11 +84,15 @@
|
||||
filter-term* (mf/use-state "")
|
||||
filter-term (deref filter-term*)
|
||||
|
||||
selected-id* (mf/use-state nil)
|
||||
selected-id (deref selected-id*)
|
||||
|
||||
options-ref (mf/use-ref nil)
|
||||
dropdown-ref (mf/use-ref nil)
|
||||
internal-ref (mf/use-ref nil)
|
||||
nodes-ref (mf/use-ref nil)
|
||||
wrapper-ref (mf/use-ref nil)
|
||||
input-wrapper-ref (mf/use-ref nil)
|
||||
icon-button-ref (mf/use-ref nil)
|
||||
ref (or ref internal-ref)
|
||||
|
||||
@@ -117,12 +121,28 @@
|
||||
state (obj/set! state id node)]
|
||||
(mf/set-ref-val! nodes-ref state))))
|
||||
|
||||
get-selected-id
|
||||
(mf/use-fn
|
||||
(mf/deps dropdown-options)
|
||||
(fn []
|
||||
(let [input-node (mf/ref-val ref)
|
||||
value (dom/get-input-value input-node)
|
||||
cursor (dom/selection-start input-node)
|
||||
token-name (tp/token-at-cursor value cursor)
|
||||
options (if (delay? dropdown-options) @dropdown-options dropdown-options)]
|
||||
(when token-name
|
||||
(->> options
|
||||
(filter #(= (:name %) token-name))
|
||||
first
|
||||
:id)))))
|
||||
|
||||
toggle-dropdown
|
||||
(mf/use-fn
|
||||
(mf/deps is-open)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(swap! is-open* not)
|
||||
(reset! selected-id* (get-selected-id))
|
||||
(let [input-node (mf/ref-val ref)]
|
||||
(dom/focus! input-node))))
|
||||
|
||||
@@ -157,7 +177,8 @@
|
||||
:options dropdown-options
|
||||
:toggle-dropdown toggle-dropdown
|
||||
:is-open* is-open*
|
||||
:on-enter on-option-enter})
|
||||
:on-enter on-option-enter
|
||||
:get-selected-id get-selected-id})
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
@@ -216,11 +237,13 @@
|
||||
:hint-message (:message hint)
|
||||
:on-key-down on-key-down
|
||||
:hint-type (:type hint)
|
||||
:input-wrapper-ref input-wrapper-ref
|
||||
:ref ref
|
||||
:role "combobox"
|
||||
:aria-activedescendant focused-id
|
||||
:aria-controls listbox-id
|
||||
:aria-expanded is-open
|
||||
:data-option-focused (boolean focused-id)
|
||||
:slot-end
|
||||
(when (some? @filtered-tokens-by-type)
|
||||
(mf/html
|
||||
@@ -241,7 +264,7 @@
|
||||
props)
|
||||
|
||||
|
||||
{:keys [style ready?]} (use-floating-dropdown is-open wrapper-ref dropdown-ref)]
|
||||
{:keys [style ready?]} (use-floating-dropdown is-open input-wrapper-ref wrapper-ref dropdown-ref)]
|
||||
|
||||
(mf/with-effect [resolve-stream tokens token name token-name]
|
||||
(let [subs (->> resolve-stream
|
||||
@@ -300,7 +323,7 @@
|
||||
:id listbox-id
|
||||
:options options
|
||||
:focused focused-id
|
||||
:selected nil
|
||||
:selected selected-id
|
||||
:align :right
|
||||
:empty-to-end empty-to-end
|
||||
:wrapper-ref dropdown-ref
|
||||
|
||||
@@ -26,14 +26,13 @@
|
||||
[focusables focused-id direction]
|
||||
(let [ids (vec (map :id focusables))
|
||||
idx (.indexOf (clj->js ids) focused-id)
|
||||
idx (if (= idx -1) -1 idx)
|
||||
next-idx (case direction
|
||||
:down (min (dec (count ids)) (inc idx))
|
||||
:up (max 0 (dec (if (= idx -1) 0 idx))))]
|
||||
(nth ids next-idx nil)))
|
||||
count (count ids)]
|
||||
(case direction
|
||||
:down (nth ids (mod (inc idx) count) nil)
|
||||
:up (nth ids (mod (if (= idx -1) 0 (dec idx)) count) nil))))
|
||||
|
||||
(defn use-navigation
|
||||
[{:keys [is-open options nodes-ref is-open* toggle-dropdown on-enter]}]
|
||||
[{:keys [is-open options nodes-ref is-open* toggle-dropdown on-enter get-selected-id]}]
|
||||
|
||||
(let [focused-id* (mf/use-state nil)
|
||||
focused-id (deref focused-id*)
|
||||
@@ -46,6 +45,7 @@
|
||||
down? (kbd/down-arrow? event)
|
||||
enter? (kbd/enter? event)
|
||||
esc? (kbd/esc? event)
|
||||
tab? (kbd/tab? event)
|
||||
open-dropdown (kbd/is-key? event "{")
|
||||
close-dropdown (kbd/is-key? event "}")
|
||||
options (if (delay? options) @options options)]
|
||||
@@ -56,18 +56,21 @@
|
||||
(dom/prevent-default event)
|
||||
(let [focusables (focusable-options options)]
|
||||
(cond
|
||||
;; Dropdown open: move focus to next option
|
||||
is-open
|
||||
(when (seq focusables)
|
||||
(let [next-id (next-focus-id focusables focused-id :down)]
|
||||
(reset! focused-id* next-id)))
|
||||
|
||||
;; Dropdown closed with options: open and focus first
|
||||
(seq focusables)
|
||||
(do
|
||||
(toggle-dropdown event)
|
||||
(when get-selected-id
|
||||
(get-selected-id))
|
||||
(reset! focused-id* (first-focusable-id focusables)))
|
||||
|
||||
:else
|
||||
nil)))
|
||||
:else nil)))
|
||||
|
||||
up?
|
||||
(when is-open
|
||||
@@ -77,7 +80,9 @@
|
||||
(reset! focused-id* next-id)))
|
||||
|
||||
open-dropdown
|
||||
(reset! is-open* true)
|
||||
(do
|
||||
(reset! is-open* true)
|
||||
(reset! focused-id* nil))
|
||||
|
||||
close-dropdown
|
||||
(reset! is-open* false)
|
||||
@@ -89,21 +94,23 @@
|
||||
(dom/prevent-default event)
|
||||
(when (some #(= (:id %) focused-id) focusables)
|
||||
(on-enter focused-id)))))
|
||||
|
||||
esc?
|
||||
(do
|
||||
(when is-open
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(reset! is-open* false))
|
||||
|
||||
tab?
|
||||
(when is-open
|
||||
(reset! is-open* false)
|
||||
(reset! focused-id* nil))
|
||||
|
||||
:else nil))))]
|
||||
|
||||
;; Initial focus on first option
|
||||
(mf/with-effect [is-open options]
|
||||
(when is-open
|
||||
(let [opts (if (delay? options) @options options)
|
||||
focusables (focusable-options opts)
|
||||
ids (set (map :id focusables))]
|
||||
(when (and (seq focusables)
|
||||
(not (contains? ids focused-id)))
|
||||
(reset! focused-id* (:id (first focusables)))))))
|
||||
(mf/with-effect [is-open]
|
||||
(when (not is-open)
|
||||
(reset! focused-id* nil)))
|
||||
|
||||
;; auto scroll when key down
|
||||
(mf/with-effect [focused-id nodes-ref]
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
[app.util.dom :as dom]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn use-floating-dropdown [is-open wrapper-ref dropdown-ref]
|
||||
(defn use-floating-dropdown [is-open input-wrapper-ref outer-wrapper-ref dropdown-ref]
|
||||
(let [position* (mf/use-state nil)
|
||||
position (deref position*)
|
||||
ready* (mf/use-state false)
|
||||
@@ -32,7 +32,7 @@
|
||||
(> dropdown-height space-below))
|
||||
|
||||
position (if open-up?
|
||||
{:bottom (str (- windows-height (:top combobox-rect) 12) "px")
|
||||
{:bottom (str (- windows-height (:top combobox-rect) -8) "px")
|
||||
:left (str (:left combobox-rect) "px")
|
||||
:width (str (:width combobox-rect) "px")
|
||||
:placement :top}
|
||||
@@ -44,27 +44,41 @@
|
||||
(reset! ready* true)
|
||||
(reset! position* position)))]
|
||||
|
||||
(mf/with-effect [is-open dropdown-ref wrapper-ref]
|
||||
(mf/with-effect [is-open dropdown-ref input-wrapper-ref outer-wrapper-ref]
|
||||
(when is-open
|
||||
(let [handler (fn [event]
|
||||
(let [dropdown-node (mf/ref-val dropdown-ref)
|
||||
target (dom/get-target event)]
|
||||
(when (or (nil? dropdown-node)
|
||||
(not (instance? js/Node target))
|
||||
(not (.contains dropdown-node target)))
|
||||
(js/requestAnimationFrame
|
||||
(fn []
|
||||
(let [wrapper-node (mf/ref-val wrapper-ref)]
|
||||
(reset! ready* true)
|
||||
(calculate-position wrapper-node)))))))]
|
||||
(let [recalculate
|
||||
(fn []
|
||||
(js/requestAnimationFrame
|
||||
(fn []
|
||||
(let [input-node (mf/ref-val input-wrapper-ref)]
|
||||
(calculate-position input-node)))))
|
||||
|
||||
handler
|
||||
(fn [event]
|
||||
(let [dropdown-node (mf/ref-val dropdown-ref)
|
||||
target (dom/get-target event)]
|
||||
(when (or (nil? dropdown-node)
|
||||
(not (instance? js/Node target))
|
||||
(not (.contains dropdown-node target)))
|
||||
(recalculate))))
|
||||
|
||||
resize-observer (js/ResizeObserver. (fn [_] (recalculate)))
|
||||
outer-node (mf/ref-val outer-wrapper-ref)
|
||||
dropdown-node (mf/ref-val dropdown-ref)]
|
||||
|
||||
(handler nil)
|
||||
|
||||
(.addEventListener js/window "resize" handler)
|
||||
(.addEventListener js/window "scroll" handler true)
|
||||
(when outer-node
|
||||
(.observe resize-observer outer-node))
|
||||
(when dropdown-node
|
||||
(.observe resize-observer dropdown-node))
|
||||
|
||||
(fn []
|
||||
(.removeEventListener js/window "resize" handler)
|
||||
(.removeEventListener js/window "scroll" handler true)))))
|
||||
(.removeEventListener js/window "scroll" handler true)
|
||||
(.disconnect resize-observer)))))
|
||||
|
||||
{:style position
|
||||
:ready? ready
|
||||
|
||||
@@ -22,6 +22,18 @@
|
||||
:end (or (str/index-of value "}" last-open) cursor)
|
||||
:partial (subs text-before (inc last-open))})))
|
||||
|
||||
(defn token-at-cursor
|
||||
"Returns the full token name at the cursor position if cursor is
|
||||
inside a complete {token-name} reference, nil otherwise."
|
||||
[value cursor]
|
||||
(let [last-open (str/last-index-of (subs value 0 cursor) "{")
|
||||
last-close (str/index-of value "}" (or last-open 0))]
|
||||
(when (and last-open last-close (> last-close last-open))
|
||||
(let [token-name (subs value (inc last-open) last-close)]
|
||||
(when (and (seq token-name)
|
||||
(not (str/includes? token-name " ")))
|
||||
token-name)))))
|
||||
|
||||
|
||||
(defn active-token [value input-node]
|
||||
(let [cursor (dom/selection-start input-node)]
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
[token]
|
||||
{:id (str (get token :id))
|
||||
:type :token
|
||||
:resolved-value (get token :resolved-value)
|
||||
:resolved-value (get token :value)
|
||||
:name (get token :name)})
|
||||
|
||||
(defn- generate-dropdown-options
|
||||
|
||||
Reference in New Issue
Block a user