BasicNumberInput now undos on Escape
This commit is contained in:
parent
4d7a4d7f25
commit
e479d32f3b
1 changed files with 40 additions and 38 deletions
|
@ -1,9 +1,10 @@
|
||||||
import {clamp, isInteger, round} from 'lodash-es'
|
import {clamp, isInteger, round} from 'lodash-es'
|
||||||
import type {MutableRefObject} from 'react'
|
import type {MutableRefObject} from 'react'
|
||||||
import React, {useMemo, useRef, useState} from 'react'
|
import React, {useMemo, useRef} from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import DraggableArea from '@theatre/studio/uiComponents/DraggableArea'
|
import DraggableArea from '@theatre/studio/uiComponents/DraggableArea'
|
||||||
import mergeRefs from 'react-merge-refs'
|
import mergeRefs from 'react-merge-refs'
|
||||||
|
import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -115,11 +116,11 @@ const BasicNumberInput: React.FC<{
|
||||||
onBlur?: () => void
|
onBlur?: () => void
|
||||||
nudge: BasicNumberInputNudgeFn
|
nudge: BasicNumberInputNudgeFn
|
||||||
}> = (propsA) => {
|
}> = (propsA) => {
|
||||||
const [stateA, setState] = useState<IState>({mode: 'noFocus'})
|
const [stateRef] = useRefAndState<IState>({mode: 'noFocus'})
|
||||||
const isValid = propsA.isValid ?? alwaysValid
|
const isValid = propsA.isValid ?? alwaysValid
|
||||||
|
|
||||||
const refs = useRef({state: stateA, props: propsA})
|
const propsRef = useRef(propsA)
|
||||||
refs.current = {state: stateA, props: propsA}
|
propsRef.current = propsA
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
|
@ -129,42 +130,43 @@ const BasicNumberInput: React.FC<{
|
||||||
const inputChange = (e: React.ChangeEvent) => {
|
const inputChange = (e: React.ChangeEvent) => {
|
||||||
const target = e.target as HTMLInputElement
|
const target = e.target as HTMLInputElement
|
||||||
const {value} = target
|
const {value} = target
|
||||||
const curState = refs.current.state as IState_EditingViaKeyboard
|
const curState = stateRef.current as IState_EditingViaKeyboard
|
||||||
|
|
||||||
setState({...curState, currentEditedValueInString: value})
|
stateRef.current = {...curState, currentEditedValueInString: value}
|
||||||
|
|
||||||
const valInFloat = parseFloat(value)
|
const valInFloat = parseFloat(value)
|
||||||
if (!isFinite(valInFloat) || !isValid(valInFloat)) return
|
if (!isFinite(valInFloat) || !isValid(valInFloat)) return
|
||||||
|
|
||||||
refs.current.props.temporarilySetValue(valInFloat)
|
propsRef.current.temporarilySetValue(valInFloat)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onBlur = () => {
|
const onBlur = () => {
|
||||||
if (refs.current.state.mode === 'editingViaKeyboard') {
|
if (stateRef.current.mode === 'editingViaKeyboard') {
|
||||||
commitKeyboardInput()
|
commitKeyboardInput()
|
||||||
setState({mode: 'noFocus'})
|
stateRef.current = {mode: 'noFocus'}
|
||||||
}
|
}
|
||||||
if (propsA.onBlur) propsA.onBlur()
|
if (propsA.onBlur) propsA.onBlur()
|
||||||
}
|
}
|
||||||
|
|
||||||
const commitKeyboardInput = () => {
|
const commitKeyboardInput = () => {
|
||||||
const curState = refs.current.state as IState_EditingViaKeyboard
|
const curState = stateRef.current as IState_EditingViaKeyboard
|
||||||
const value = parseFloat(curState.currentEditedValueInString)
|
const value = parseFloat(curState.currentEditedValueInString)
|
||||||
|
|
||||||
if (!isFinite(value) || !isValid(value)) {
|
if (!isFinite(value) || !isValid(value)) {
|
||||||
refs.current.props.discardTemporaryValue()
|
propsRef.current.discardTemporaryValue()
|
||||||
} else {
|
} else {
|
||||||
if (curState.valueBeforeEditing === value) {
|
if (curState.valueBeforeEditing === value) {
|
||||||
refs.current.props.discardTemporaryValue()
|
propsRef.current.discardTemporaryValue()
|
||||||
} else {
|
} else {
|
||||||
refs.current.props.permenantlySetValue(value)
|
propsRef.current.permenantlySetValue(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onInputKeyDown = (e: React.KeyboardEvent) => {
|
const onInputKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
refs.current.props.discardTemporaryValue()
|
propsRef.current.discardTemporaryValue()
|
||||||
|
stateRef.current = {mode: 'noFocus'}
|
||||||
inputRef.current!.blur()
|
inputRef.current!.blur()
|
||||||
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
||||||
commitKeyboardInput()
|
commitKeyboardInput()
|
||||||
|
@ -173,7 +175,7 @@ const BasicNumberInput: React.FC<{
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClick = (e: React.MouseEvent) => {
|
const onClick = (e: React.MouseEvent) => {
|
||||||
if (refs.current.state.mode === 'noFocus') {
|
if (stateRef.current.mode === 'noFocus') {
|
||||||
const c = inputRef.current!
|
const c = inputRef.current!
|
||||||
c.focus()
|
c.focus()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
@ -184,19 +186,19 @@ const BasicNumberInput: React.FC<{
|
||||||
}
|
}
|
||||||
|
|
||||||
const onFocus = () => {
|
const onFocus = () => {
|
||||||
if (refs.current.state.mode === 'noFocus') {
|
if (stateRef.current.mode === 'noFocus') {
|
||||||
transitionToEditingViaKeyboardMode()
|
transitionToEditingViaKeyboardMode()
|
||||||
} else if (refs.current.state.mode === 'editingViaKeyboard') {
|
} else if (stateRef.current.mode === 'editingViaKeyboard') {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const transitionToEditingViaKeyboardMode = () => {
|
const transitionToEditingViaKeyboardMode = () => {
|
||||||
const curValue = refs.current.props.value
|
const curValue = propsRef.current.value
|
||||||
setState({
|
stateRef.current = {
|
||||||
mode: 'editingViaKeyboard',
|
mode: 'editingViaKeyboard',
|
||||||
currentEditedValueInString: String(curValue),
|
currentEditedValueInString: String(curValue),
|
||||||
valueBeforeEditing: curValue,
|
valueBeforeEditing: curValue,
|
||||||
})
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
inputRef.current!.focus()
|
inputRef.current!.focus()
|
||||||
|
@ -207,39 +209,39 @@ const BasicNumberInput: React.FC<{
|
||||||
let inputWidth: number
|
let inputWidth: number
|
||||||
|
|
||||||
const transitionToDraggingMode = () => {
|
const transitionToDraggingMode = () => {
|
||||||
const curValue = refs.current.props.value
|
const curValue = propsRef.current.value
|
||||||
inputWidth = inputRef.current?.getBoundingClientRect().width!
|
inputWidth = inputRef.current?.getBoundingClientRect().width!
|
||||||
|
|
||||||
setState({
|
stateRef.current = {
|
||||||
mode: 'dragging',
|
mode: 'dragging',
|
||||||
valueBeforeDragging: curValue,
|
valueBeforeDragging: curValue,
|
||||||
currentDraggingValue: curValue,
|
currentDraggingValue: curValue,
|
||||||
})
|
}
|
||||||
|
|
||||||
bodyCursorBeforeDrag.current = document.body.style.cursor
|
bodyCursorBeforeDrag.current = document.body.style.cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDragEnd = (happened: boolean) => {
|
const onDragEnd = (happened: boolean) => {
|
||||||
if (!happened) {
|
if (!happened) {
|
||||||
refs.current.props.discardTemporaryValue()
|
propsRef.current.discardTemporaryValue()
|
||||||
setState({mode: 'noFocus'})
|
stateRef.current = {mode: 'noFocus'}
|
||||||
|
|
||||||
inputRef.current!.focus()
|
inputRef.current!.focus()
|
||||||
inputRef.current!.setSelectionRange(0, 100)
|
inputRef.current!.setSelectionRange(0, 100)
|
||||||
} else {
|
} else {
|
||||||
const curState = refs.current.state as IState_Dragging
|
const curState = stateRef.current as IState_Dragging
|
||||||
const value = curState.currentDraggingValue
|
const value = curState.currentDraggingValue
|
||||||
if (curState.valueBeforeDragging === value) {
|
if (curState.valueBeforeDragging === value) {
|
||||||
refs.current.props.discardTemporaryValue()
|
propsRef.current.discardTemporaryValue()
|
||||||
} else {
|
} else {
|
||||||
refs.current.props.permenantlySetValue(value)
|
propsRef.current.permenantlySetValue(value)
|
||||||
}
|
}
|
||||||
setState({mode: 'noFocus'})
|
stateRef.current = {mode: 'noFocus'}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDrag = (deltaX: number, _dy: number) => {
|
const onDrag = (deltaX: number, _dy: number) => {
|
||||||
const curState = refs.current.state as IState_Dragging
|
const curState = stateRef.current as IState_Dragging
|
||||||
|
|
||||||
let newValue =
|
let newValue =
|
||||||
curState.valueBeforeDragging +
|
curState.valueBeforeDragging +
|
||||||
|
@ -253,12 +255,12 @@ const BasicNumberInput: React.FC<{
|
||||||
newValue = clamp(newValue, propsA.range[0], propsA.range[1])
|
newValue = clamp(newValue, propsA.range[0], propsA.range[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
setState({
|
stateRef.current = {
|
||||||
...curState,
|
...curState,
|
||||||
currentDraggingValue: newValue,
|
currentDraggingValue: newValue,
|
||||||
})
|
}
|
||||||
|
|
||||||
refs.current.props.temporarilySetValue(newValue)
|
propsRef.current.temporarilySetValue(newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -271,12 +273,12 @@ const BasicNumberInput: React.FC<{
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
onDrag,
|
onDrag,
|
||||||
}
|
}
|
||||||
}, [refs, setState, inputRef])
|
}, [])
|
||||||
|
|
||||||
let value =
|
let value =
|
||||||
stateA.mode !== 'editingViaKeyboard'
|
stateRef.current.mode !== 'editingViaKeyboard'
|
||||||
? format(propsA.value)
|
? format(propsA.value)
|
||||||
: stateA.currentEditedValueInString
|
: stateRef.current.currentEditedValueInString
|
||||||
|
|
||||||
if (typeof value === 'number' && isNaN(value)) {
|
if (typeof value === 'number' && isNaN(value)) {
|
||||||
value = 'NaN'
|
value = 'NaN'
|
||||||
|
@ -319,13 +321,13 @@ const BasicNumberInput: React.FC<{
|
||||||
) : null
|
) : null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className={propsA.className + ' ' + refs.current.state.mode}>
|
<Container className={propsA.className + ' ' + stateRef.current.mode}>
|
||||||
<DraggableArea
|
<DraggableArea
|
||||||
key="draggableArea"
|
key="draggableArea"
|
||||||
onDragStart={callbacks.transitionToDraggingMode}
|
onDragStart={callbacks.transitionToDraggingMode}
|
||||||
onDragEnd={callbacks.onDragEnd}
|
onDragEnd={callbacks.onDragEnd}
|
||||||
onDrag={callbacks.onDrag}
|
onDrag={callbacks.onDrag}
|
||||||
enabled={refs.current.state.mode !== 'editingViaKeyboard'}
|
enabled={stateRef.current.mode !== 'editingViaKeyboard'}
|
||||||
lockCursorTo="ew-resize"
|
lockCursorTo="ew-resize"
|
||||||
>
|
>
|
||||||
{theInput}
|
{theInput}
|
||||||
|
|
Loading…
Reference in a new issue