mirror of
https://github.com/penpot/penpot.git
synced 2026-03-14 06:17:25 +00:00
♻️ Extract mouse navigation as hook
This commit is contained in:
@@ -24,33 +24,11 @@
|
||||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- focusable-option?
|
||||
[option]
|
||||
(and (:id option)
|
||||
(not= :group (:type option))
|
||||
(not= :separator (:type option))))
|
||||
|
||||
(defn- first-focusable-id
|
||||
[options]
|
||||
(some #(when (focusable-option? %) (:id %)) options))
|
||||
|
||||
(defn next-focus-id
|
||||
[options focused-id direction]
|
||||
(let [focusable (filter focusable-option? options)
|
||||
ids (map :id focusable)
|
||||
idx (.indexOf (clj->js ids) focused-id)
|
||||
next-idx (case direction
|
||||
:down (min (dec (count ids)) (inc (if (= idx -1) -1 idx)))
|
||||
:up (max 0 (dec (if (= idx -1) 0 idx))))]
|
||||
(nth ids next-idx nil)))
|
||||
|
||||
|
||||
(defn extract-partial-token
|
||||
[value cursor]
|
||||
(let [text-before (subs value 0 cursor)
|
||||
@@ -95,7 +73,6 @@
|
||||
(fn [options]
|
||||
(remove #(= (:id %) current-id) options)))))
|
||||
|
||||
|
||||
(defn- select-option-by-id
|
||||
[id options-ref input-node value]
|
||||
(let [cursor (.-selectionStart input-node)
|
||||
@@ -138,23 +115,24 @@
|
||||
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
|
||||
input-name name
|
||||
token-name (get-in @form [:data :name] nil)
|
||||
touched?
|
||||
(and (contains? (:data @form) name)
|
||||
(get-in @form [:touched name]))
|
||||
|
||||
error
|
||||
(get-in @form [:errors name])
|
||||
|
||||
value
|
||||
(get-in @form [:data name] "")
|
||||
|
||||
is-open* (mf/use-state false)
|
||||
is-open (deref is-open*)
|
||||
dropdown-pos* (mf/use-state nil)
|
||||
dropdown-pos (deref dropdown-pos*)
|
||||
dropdown-ready* (mf/use-state false)
|
||||
dropdown-ready (deref dropdown-ready*)
|
||||
|
||||
listbox-id (mf/use-id)
|
||||
filter-term* (mf/use-state "")
|
||||
filter-term (deref filter-term*)
|
||||
|
||||
focused-id* (mf/use-state nil)
|
||||
focused-id (deref focused-id*)
|
||||
|
||||
options-ref (mf/use-ref nil)
|
||||
dropdown-ref (mf/use-ref nil)
|
||||
internal-ref (mf/use-ref nil)
|
||||
@@ -163,16 +141,6 @@
|
||||
icon-button-ref (mf/use-ref nil)
|
||||
ref (or ref internal-ref)
|
||||
|
||||
touched?
|
||||
(and (contains? (:data @form) input-name)
|
||||
(get-in @form [:touched input-name]))
|
||||
|
||||
error
|
||||
(get-in @form [:errors input-name])
|
||||
|
||||
value
|
||||
(get-in @form [:data input-name] "")
|
||||
|
||||
raw-tokens-by-type (mf/use-ctx muc/active-tokens-by-type)
|
||||
|
||||
filtered-tokens-by-type
|
||||
@@ -213,15 +181,35 @@
|
||||
(rx/behavior-subject (:value token))
|
||||
(rx/subject)))
|
||||
|
||||
on-option-enter
|
||||
(mf/use-fn
|
||||
(mf/deps value resolve-stream name)
|
||||
(fn [id]
|
||||
(let [input-node (mf/ref-val ref)
|
||||
final-val (select-option-by-id id options-ref input-node value)]
|
||||
(fm/on-input-change form name final-val true)
|
||||
(rx/push! resolve-stream final-val)
|
||||
(reset! filter-term* "")
|
||||
(reset! is-open* false))))
|
||||
|
||||
{:keys [focused-id on-key-down]}
|
||||
(use-navigation
|
||||
{:is-open is-open
|
||||
:options-ref options-ref
|
||||
:nodes-ref nodes-ref
|
||||
:toggle-dropdown toggle-dropdown
|
||||
:is-open* is-open*
|
||||
:on-enter on-option-enter})
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
(mf/deps resolve-stream input-name form)
|
||||
(mf/deps resolve-stream name form)
|
||||
(fn [event]
|
||||
(let [node (dom/get-target event)
|
||||
value (dom/get-input-value node)
|
||||
token (active-token value node)]
|
||||
|
||||
(fm/on-input-change form input-name value)
|
||||
(fm/on-input-change form name value)
|
||||
(rx/push! resolve-stream value)
|
||||
|
||||
(if token
|
||||
@@ -234,14 +222,14 @@
|
||||
|
||||
on-option-click
|
||||
(mf/use-fn
|
||||
(mf/deps value resolve-stream ref)
|
||||
(mf/deps value resolve-stream ref name)
|
||||
(fn [event]
|
||||
(let [input-node (mf/ref-val ref)
|
||||
node (dom/get-current-target event)
|
||||
id (dom/get-data node "id")
|
||||
final-val (select-option-by-id id options-ref input-node value)]
|
||||
|
||||
(fm/on-input-change form input-name final-val true)
|
||||
(fm/on-input-change form name final-val true)
|
||||
(rx/push! resolve-stream final-val)
|
||||
|
||||
(reset! filter-term* "")
|
||||
@@ -252,68 +240,6 @@
|
||||
(set! (.-selectionStart input-node) new-cursor)
|
||||
(set! (.-selectionEnd input-node) new-cursor)))))
|
||||
|
||||
on-option-enter
|
||||
(mf/use-fn
|
||||
(mf/deps focused-id value resolve-stream)
|
||||
(fn [_]
|
||||
(let [input-node (mf/ref-val ref)
|
||||
final-val (select-option-by-id focused-id options-ref input-node value)]
|
||||
(fm/on-input-change form input-name final-val true)
|
||||
(rx/push! resolve-stream final-val)
|
||||
(reset! filter-term* "")
|
||||
(reset! is-open* false))))
|
||||
|
||||
on-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps is-open focused-id)
|
||||
(fn [event]
|
||||
(let [up? (kbd/up-arrow? event)
|
||||
down? (kbd/down-arrow? event)
|
||||
enter? (kbd/enter? event)
|
||||
esc? (kbd/esc? event)
|
||||
open-dropdown (kbd/is-key? event "{")
|
||||
close-dropdown (kbd/is-key? event "}")
|
||||
options (mf/ref-val options-ref)
|
||||
options (if (delay? options) @options options)]
|
||||
|
||||
(cond
|
||||
open-dropdown
|
||||
(reset! is-open* true)
|
||||
|
||||
close-dropdown
|
||||
(reset! is-open* false)
|
||||
|
||||
down?
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(if is-open
|
||||
(let [next-id (next-focus-id options focused-id :down)]
|
||||
(reset! focused-id* next-id))
|
||||
(when (some? @filtered-tokens-by-type)
|
||||
(do
|
||||
(toggle-dropdown event)
|
||||
(reset! focused-id* (first-focusable-id options))))))
|
||||
|
||||
up?
|
||||
(when is-open
|
||||
(dom/prevent-default event)
|
||||
(let [next-id (next-focus-id options focused-id :up)]
|
||||
(reset! focused-id* next-id)))
|
||||
|
||||
enter?
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(if is-open
|
||||
(on-option-enter event)
|
||||
(do
|
||||
(reset! focused-id* (first-focusable-id options))
|
||||
(toggle-dropdown event))))
|
||||
|
||||
esc?
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(reset! is-open* false))))))
|
||||
|
||||
hint*
|
||||
(mf/use-state {})
|
||||
|
||||
@@ -349,9 +275,12 @@
|
||||
(if (and error touched?)
|
||||
(mf/spread-props props {:hint-type "error"
|
||||
:hint-message (:message error)})
|
||||
props)]
|
||||
props)
|
||||
|
||||
(mf/with-effect [resolve-stream tokens token input-name token-name]
|
||||
|
||||
floating (use-floating-dropdown is-open wrapper-ref dropdown-ref)]
|
||||
|
||||
(mf/with-effect [resolve-stream tokens token name token-name]
|
||||
(let [subs (->> resolve-stream
|
||||
(rx/debounce 300)
|
||||
(rx/mapcat (partial resolve-value tokens token token-name))
|
||||
@@ -360,14 +289,14 @@
|
||||
(fn [error]
|
||||
((:error/fn error) (:error/value error))))))
|
||||
(rx/subs! (fn [{:keys [error value]}]
|
||||
(let [touched? (get-in @form [:touched input-name])]
|
||||
(let [touched? (get-in @form [:touched name])]
|
||||
(when touched?
|
||||
(if error
|
||||
(do
|
||||
(swap! form assoc-in [:extra-errors input-name] {:message error})
|
||||
(swap! form assoc-in [:extra-errors name] {:message error})
|
||||
(reset! hint* {:message error :type "error"}))
|
||||
(let [message (tr "workspace.tokens.resolved-value" value)]
|
||||
(swap! form update :extra-errors dissoc input-name)
|
||||
(swap! form update :extra-errors dissoc name)
|
||||
(reset! hint* {:message message :type "hint"}))))))))]
|
||||
(fn []
|
||||
(rx/dispose! subs))))
|
||||
@@ -392,24 +321,6 @@
|
||||
(.removeEventListener js/document "mousedown" handler)))))
|
||||
|
||||
|
||||
(mf/with-effect [is-open]
|
||||
(when is-open
|
||||
(let [options (mf/ref-val options-ref)
|
||||
options (if (delay? options) @options options)
|
||||
|
||||
first-id (first-focusable-id options)]
|
||||
|
||||
(when first-id
|
||||
(reset! focused-id* first-id)))))
|
||||
|
||||
(mf/with-effect [focused-id nodes-ref]
|
||||
(when focused-id
|
||||
(let [nodes (mf/ref-val nodes-ref)
|
||||
node (obj/get nodes focused-id)]
|
||||
(when node
|
||||
(dom/scroll-into-view-if-needed! node {:block "nearest"
|
||||
:inline "nearest"})))))
|
||||
|
||||
[:div {:ref wrapper-ref}
|
||||
[:> ds/input* props]
|
||||
(when ^boolean is-open
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
;; 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.workspace.tokens.management.forms.controls.navigation
|
||||
(:require
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- focusable-option?
|
||||
[option]
|
||||
(and (:id option)
|
||||
(not= :group (:type option))
|
||||
(not= :separator (:type option))))
|
||||
|
||||
(defn- first-focusable-id
|
||||
[options]
|
||||
(some #(when (focusable-option? %) (:id %)) options))
|
||||
|
||||
(defn next-focus-id
|
||||
[options focused-id direction]
|
||||
(let [focusable (filter focusable-option? options)
|
||||
ids (map :id focusable)
|
||||
idx (.indexOf (clj->js ids) focused-id)
|
||||
next-idx (case direction
|
||||
:down (min (dec (count ids)) (inc (if (= idx -1) -1 idx)))
|
||||
:up (max 0 (dec (if (= idx -1) 0 idx))))]
|
||||
(nth ids next-idx nil)))
|
||||
|
||||
(defn use-navigation
|
||||
[{:keys [is-open options-ref nodes-ref is-open* toggle-dropdown on-enter]}]
|
||||
|
||||
(let [focused-id* (mf/use-state nil)
|
||||
focused-id (deref focused-id*)
|
||||
|
||||
on-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps is-open focused-id)
|
||||
(fn [event]
|
||||
(let [up? (kbd/up-arrow? event)
|
||||
down? (kbd/down-arrow? event)
|
||||
enter? (kbd/enter? event)
|
||||
esc? (kbd/esc? event)
|
||||
;; TODO: this should be optional?
|
||||
open-dropdown (kbd/is-key? event "{")
|
||||
close-dropdown (kbd/is-key? event "}")
|
||||
options (mf/ref-val options-ref)
|
||||
options (if (delay? options) @options options)]
|
||||
|
||||
(cond
|
||||
|
||||
down?
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(if is-open
|
||||
(let [next-id (next-focus-id options focused-id :down)]
|
||||
(reset! focused-id* next-id))
|
||||
(do
|
||||
(toggle-dropdown event)
|
||||
(reset! focused-id* (first-focusable-id options)))))
|
||||
|
||||
up?
|
||||
(when is-open
|
||||
(dom/prevent-default event)
|
||||
(let [next-id (next-focus-id options focused-id :up)]
|
||||
(reset! focused-id* next-id)))
|
||||
|
||||
open-dropdown
|
||||
(reset! is-open* true)
|
||||
|
||||
close-dropdown
|
||||
(reset! is-open* false)
|
||||
|
||||
enter?
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(if is-open
|
||||
(on-enter focused-id)
|
||||
(do
|
||||
(reset! focused-id* (first-focusable-id options))
|
||||
(toggle-dropdown event))))
|
||||
esc?
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(reset! is-open* false))
|
||||
:else nil))))]
|
||||
|
||||
;; Initial focus on first option
|
||||
(mf/with-effect [is-open options-ref]
|
||||
(when is-open
|
||||
(let [options (mf/ref-val options-ref)
|
||||
options (if (delay? options) @options options)
|
||||
|
||||
first-id (first-focusable-id options)]
|
||||
|
||||
(when first-id
|
||||
(reset! focused-id* first-id)))))
|
||||
|
||||
;; auto scroll when key down
|
||||
(mf/with-effect [focused-id nodes-ref]
|
||||
(when focused-id
|
||||
(let [nodes (mf/ref-val nodes-ref)
|
||||
node (obj/get nodes focused-id)]
|
||||
(when node
|
||||
(dom/scroll-into-view-if-needed!
|
||||
node {:block "nearest"
|
||||
:inline "nearest"})))))
|
||||
|
||||
{:focused-id focused-id
|
||||
:on-key-down on-key-down}))
|
||||
Reference in New Issue
Block a user