mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
🎉 Add drag-to-change for numeric inputs (#8536)
Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>
This commit is contained in:
parent
7adac6df40
commit
852f9ce07f
@ -35,6 +35,7 @@
|
||||
|
||||
- Access Tokens look & feel refinement [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114)
|
||||
- Enhance readability of applied tokens in plugins API [Taiga #13714](https://tree.taiga.io/project/penpot/issue/13714)
|
||||
- Add drag-to-change for numeric inputs in workspace sidebar [Github #2466](https://github.com/penpot/penpot/issues/2466)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
|
||||
@ -27,6 +27,13 @@ body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
&.cursor-drag-scrub {
|
||||
cursor: ew-resize !important;
|
||||
* {
|
||||
cursor: ew-resize !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#app {
|
||||
|
||||
@ -338,6 +338,12 @@
|
||||
background-color: var(--input-background-color);
|
||||
border: $s-1 solid var(--input-border-color);
|
||||
color: var(--input-foreground-color);
|
||||
&:not(:focus-within) {
|
||||
cursor: ew-resize;
|
||||
input {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
}
|
||||
span,
|
||||
label {
|
||||
@extend .input-label;
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
(ns app.main.ui.components.numeric-input
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.math :as mth]
|
||||
[app.common.schema :as sm]
|
||||
[app.main.ui.formats :as fmt]
|
||||
[app.main.ui.hooks :as h]
|
||||
@ -61,6 +62,11 @@
|
||||
;; Last value input by the user we need to store to save on unmount
|
||||
last-value* (mf/use-var value)
|
||||
|
||||
;; Drag scrubbing state
|
||||
drag-state* (mf/use-ref :idle)
|
||||
drag-start-x* (mf/use-ref 0)
|
||||
drag-start-val* (mf/use-ref 0)
|
||||
|
||||
parse-value
|
||||
(mf/use-fn
|
||||
(mf/deps min-value max-value value nillable? default)
|
||||
@ -213,16 +219,80 @@
|
||||
(mf/use-callback
|
||||
(mf/deps on-focus select-on-focus?)
|
||||
(fn [event]
|
||||
(reset! last-value* (parse-value))
|
||||
(let [target (dom/get-target event)]
|
||||
(when on-focus
|
||||
(mf/set-ref-val! dirty-ref true)
|
||||
(on-focus event))
|
||||
(when-not (= :dragging (mf/ref-val drag-state*))
|
||||
(reset! last-value* (parse-value))
|
||||
(let [target (dom/get-target event)]
|
||||
(when on-focus
|
||||
(mf/set-ref-val! dirty-ref true)
|
||||
(on-focus event))
|
||||
|
||||
(when select-on-focus?
|
||||
(dom/select-text! target)
|
||||
;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect
|
||||
(.addEventListener target "mouseup" dom/prevent-default #js {:once true})))))
|
||||
(when select-on-focus?
|
||||
(dom/select-text! target)
|
||||
;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect
|
||||
(.addEventListener target "mouseup" dom/prevent-default #js {:once true}))))))
|
||||
|
||||
on-scrub-pointer-down
|
||||
(mf/use-fn
|
||||
(mf/deps value value-str min-value max-value default)
|
||||
(fn [event]
|
||||
(let [disabled? (unchecked-get props "disabled")
|
||||
node (mf/ref-val ref)
|
||||
is-focused (and (some? node) (dom/active? node))]
|
||||
(when-not (or disabled? is-focused (= :multiple value-str))
|
||||
(let [client-x (.-clientX event)
|
||||
start-val (or value default 0)]
|
||||
(mf/set-ref-val! drag-state* :maybe-dragging)
|
||||
(mf/set-ref-val! drag-start-x* client-x)
|
||||
(mf/set-ref-val! drag-start-val* start-val)
|
||||
(dom/capture-pointer event))))))
|
||||
|
||||
on-scrub-pointer-move
|
||||
(mf/use-fn
|
||||
(mf/deps apply-value update-input step-value min-value max-value)
|
||||
(fn [event]
|
||||
(let [state (mf/ref-val drag-state*)]
|
||||
(when (or (= state :maybe-dragging) (= state :dragging))
|
||||
(let [client-x (.-clientX event)
|
||||
start-x (mf/ref-val drag-start-x*)
|
||||
delta-x (- client-x start-x)]
|
||||
(when (and (= state :maybe-dragging)
|
||||
(>= (js/Math.abs delta-x) 3))
|
||||
(mf/set-ref-val! drag-state* :dragging)
|
||||
(dom/add-class! (dom/get-body) "cursor-drag-scrub"))
|
||||
(when (= (mf/ref-val drag-state*) :dragging)
|
||||
(let [effective-step (cond
|
||||
(.-shiftKey event) (* step-value 10)
|
||||
(.-ctrlKey event) (* step-value 0.1)
|
||||
:else step-value)
|
||||
steps (js/Math.round (/ delta-x 1))
|
||||
new-val (+ (mf/ref-val drag-start-val*)
|
||||
(* steps effective-step))
|
||||
new-val (cond-> new-val
|
||||
(d/num? min-value) (mth/max min-value)
|
||||
(d/num? max-value) (mth/min max-value))]
|
||||
(update-input new-val)
|
||||
(apply-value event new-val))))))))
|
||||
|
||||
on-scrub-pointer-up
|
||||
(mf/use-fn
|
||||
(mf/deps ref)
|
||||
(fn [event]
|
||||
(let [state (mf/ref-val drag-state*)]
|
||||
(when (= state :maybe-dragging)
|
||||
(mf/set-ref-val! drag-state* :idle)
|
||||
(dom/release-pointer event)
|
||||
(when-let [node (mf/ref-val ref)]
|
||||
(dom/focus! node)))
|
||||
(when (= state :dragging)
|
||||
(mf/set-ref-val! drag-state* :idle)
|
||||
(dom/remove-class! (dom/get-body) "cursor-drag-scrub")
|
||||
(dom/release-pointer event)))))
|
||||
|
||||
on-scrub-lost-pointer-capture
|
||||
(mf/use-fn
|
||||
(fn [_event]
|
||||
(mf/set-ref-val! drag-state* :idle)
|
||||
(dom/remove-class! (dom/get-body) "cursor-drag-scrub")))
|
||||
|
||||
props (-> (obj/clone props)
|
||||
(obj/unset! "selectOnFocus")
|
||||
@ -236,7 +306,11 @@
|
||||
(obj/set! "title" title)
|
||||
(obj/set! "onKeyDown" handle-key-down)
|
||||
(obj/set! "onBlur" handle-blur)
|
||||
(obj/set! "onFocus" handle-focus))]
|
||||
(obj/set! "onFocus" handle-focus)
|
||||
(obj/set! "onPointerDown" on-scrub-pointer-down)
|
||||
(obj/set! "onPointerMove" on-scrub-pointer-move)
|
||||
(obj/set! "onPointerUp" on-scrub-pointer-up)
|
||||
(obj/set! "onLostPointerCapture" on-scrub-lost-pointer-capture))]
|
||||
|
||||
(mf/with-effect [value]
|
||||
(when-let [input-node (mf/ref-val ref)]
|
||||
|
||||
@ -136,6 +136,8 @@
|
||||
[:applied-token {:optional true} [:maybe [:or :string [:= :multiple]]]]
|
||||
[:empty-to-end {:optional true} :boolean]
|
||||
[:on-change {:optional true} fn?]
|
||||
[:on-change-start {:optional true} fn?]
|
||||
[:on-change-end {:optional true} fn?]
|
||||
[:on-blur {:optional true} fn?]
|
||||
[:on-focus {:optional true} fn?]
|
||||
[:on-detach {:optional true} fn?]
|
||||
@ -151,7 +153,8 @@
|
||||
min max max-length step
|
||||
is-selected-on-focus nillable
|
||||
tokens applied-token empty-to-end
|
||||
on-change on-blur on-focus on-detach
|
||||
on-change on-change-start on-change-end
|
||||
on-blur on-focus on-detach
|
||||
property align ref name
|
||||
tooltip-placement text-icon]
|
||||
:rest props}]
|
||||
@ -222,6 +225,11 @@
|
||||
open-dropdown-ref (mf/use-ref nil)
|
||||
token-detach-btn-ref (mf/use-ref nil)
|
||||
|
||||
;; Drag scrubbing state
|
||||
drag-state* (mf/use-ref :idle)
|
||||
drag-start-x* (mf/use-ref 0)
|
||||
drag-start-val* (mf/use-ref 0)
|
||||
|
||||
dropdown-options
|
||||
(mf/with-memo [tokens filter-id]
|
||||
(csu/get-token-dropdown-options tokens filter-id))
|
||||
@ -442,13 +450,14 @@
|
||||
(mf/use-fn
|
||||
(mf/deps on-focus select-on-focus)
|
||||
(fn [event]
|
||||
(when (fn? on-focus)
|
||||
(on-focus event))
|
||||
(let [target (dom/get-target event)]
|
||||
(when select-on-focus
|
||||
(dom/select-text! target)
|
||||
;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect
|
||||
(.addEventListener target "mouseup" dom/prevent-default #js {:once true})))))
|
||||
(when-not (= :dragging (mf/ref-val drag-state*))
|
||||
(when (fn? on-focus)
|
||||
(on-focus event))
|
||||
(let [target (dom/get-target event)]
|
||||
(when select-on-focus
|
||||
(dom/select-text! target)
|
||||
;; In webkit browsers the mouseup event will be called after the on-focus causing and unselect
|
||||
(.addEventListener target "mouseup" dom/prevent-default #js {:once true}))))))
|
||||
|
||||
on-mouse-wheel
|
||||
(mf/use-fn
|
||||
@ -468,6 +477,77 @@
|
||||
(dom/stop-propagation event)
|
||||
(apply-value (dm/str new-val)))))))
|
||||
|
||||
on-scrub-pointer-down
|
||||
(mf/use-fn
|
||||
(mf/deps disabled is-open is-multiple? ref min max nillable default)
|
||||
(fn [event]
|
||||
(when-not (or disabled is-open is-multiple?)
|
||||
(let [node (mf/ref-val ref)
|
||||
is-focused (and (some? node) (dom/active? node))
|
||||
has-token (some? (deref token-applied*))]
|
||||
(when-not (or is-focused has-token)
|
||||
(let [client-x (.-clientX event)
|
||||
parsed (parse-value (mf/ref-val raw-value*) (mf/ref-val last-value*) min max nillable)
|
||||
start-val (or parsed default 0)]
|
||||
(mf/set-ref-val! drag-state* :maybe-dragging)
|
||||
(mf/set-ref-val! drag-start-x* client-x)
|
||||
(mf/set-ref-val! drag-start-val* start-val)
|
||||
(dom/capture-pointer event)))))))
|
||||
|
||||
on-scrub-pointer-move
|
||||
(mf/use-fn
|
||||
(mf/deps apply-value update-input step min max on-change-start)
|
||||
(fn [event]
|
||||
(let [state (mf/ref-val drag-state*)]
|
||||
(when (or (= state :maybe-dragging) (= state :dragging))
|
||||
(let [client-x (.-clientX event)
|
||||
start-x (mf/ref-val drag-start-x*)
|
||||
delta-x (- client-x start-x)]
|
||||
(when (and (= state :maybe-dragging)
|
||||
(>= (js/Math.abs delta-x) 3))
|
||||
(mf/set-ref-val! drag-state* :dragging)
|
||||
(dom/add-class! (dom/get-body) "cursor-drag-scrub")
|
||||
(when (fn? on-change-start)
|
||||
(on-change-start)))
|
||||
(when (= (mf/ref-val drag-state*) :dragging)
|
||||
(let [effective-step (cond
|
||||
(.-shiftKey event) (* step 10)
|
||||
(.-ctrlKey event) (* step 0.1)
|
||||
:else step)
|
||||
steps (js/Math.round (/ delta-x 1))
|
||||
new-val (mth/clamp (+ (mf/ref-val drag-start-val*)
|
||||
(* steps effective-step))
|
||||
min max)]
|
||||
(update-input (fmt/format-number new-val))
|
||||
(apply-value (dm/str new-val)))))))))
|
||||
|
||||
on-scrub-pointer-up
|
||||
(mf/use-fn
|
||||
(mf/deps ref on-change-end)
|
||||
(fn [event]
|
||||
(let [state (mf/ref-val drag-state*)]
|
||||
(when (= state :maybe-dragging)
|
||||
(mf/set-ref-val! drag-state* :idle)
|
||||
(dom/release-pointer event)
|
||||
(when-let [node (mf/ref-val ref)]
|
||||
(dom/focus! node)))
|
||||
(when (= state :dragging)
|
||||
(mf/set-ref-val! drag-state* :idle)
|
||||
(dom/remove-class! (dom/get-body) "cursor-drag-scrub")
|
||||
(dom/release-pointer event)
|
||||
(when (fn? on-change-end)
|
||||
(on-change-end))))))
|
||||
|
||||
on-scrub-lost-pointer-capture
|
||||
(mf/use-fn
|
||||
(mf/deps on-change-end)
|
||||
(fn [_event]
|
||||
(let [was-dragging (= :dragging (mf/ref-val drag-state*))]
|
||||
(mf/set-ref-val! drag-state* :idle)
|
||||
(dom/remove-class! (dom/get-body) "cursor-drag-scrub")
|
||||
(when (and was-dragging (fn? on-change-end))
|
||||
(on-change-end)))))
|
||||
|
||||
open-dropdown
|
||||
(mf/use-fn
|
||||
(mf/deps disabled ref)
|
||||
@ -658,7 +738,11 @@
|
||||
(mf/set-ref-val! options-ref dropdown-options))
|
||||
|
||||
[:div {:class [class (stl/css :input-wrapper)]
|
||||
:ref wrapper-ref}
|
||||
:ref wrapper-ref
|
||||
:on-pointer-down on-scrub-pointer-down
|
||||
:on-pointer-move on-scrub-pointer-move
|
||||
:on-pointer-up on-scrub-pointer-up
|
||||
:on-lost-pointer-capture on-scrub-lost-pointer-capture}
|
||||
|
||||
(if (and (some? token-applied)
|
||||
(not= :multiple token-applied))
|
||||
|
||||
@ -20,6 +20,9 @@
|
||||
inline-size: 100%;
|
||||
position: relative;
|
||||
|
||||
&:not(:focus-within) {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
&:hover {
|
||||
--opacity-button: 1;
|
||||
}
|
||||
|
||||
@ -184,6 +184,9 @@
|
||||
@include t.use-typography("body-small");
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&:not(:focus-within) {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
block-size: $sz-32;
|
||||
inline-size: px2rem(60);
|
||||
padding-inline-start: var(--sp-xs);
|
||||
@ -240,6 +243,10 @@
|
||||
margin: var(--sp-xxs) 0;
|
||||
padding: 0 0 0 px2rem(6);
|
||||
color: var(--color-foreground-primary);
|
||||
cursor: ew-resize;
|
||||
&:focus {
|
||||
cursor: text;
|
||||
}
|
||||
&[disabled] {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user