diff --git a/theatre/studio/src/UIRoot/useKeyboardShortcuts.ts b/theatre/studio/src/UIRoot/useKeyboardShortcuts.ts index 2100ecc..a8c641e 100644 --- a/theatre/studio/src/UIRoot/useKeyboardShortcuts.ts +++ b/theatre/studio/src/UIRoot/useKeyboardShortcuts.ts @@ -8,7 +8,8 @@ export default function useKeyboardShortcuts() { const studio = getStudio() useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - const target: null | HTMLElement = e.target as unknown as $IntentionalAny + const target: null | HTMLElement = + e.composedPath()[0] as unknown as $IntentionalAny if ( target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') diff --git a/theatre/studio/src/panels/DetailPanel/propEditors/StringPropEditor.tsx b/theatre/studio/src/panels/DetailPanel/propEditors/StringPropEditor.tsx index 3320516..5ee8020 100644 --- a/theatre/studio/src/panels/DetailPanel/propEditors/StringPropEditor.tsx +++ b/theatre/studio/src/panels/DetailPanel/propEditors/StringPropEditor.tsx @@ -1,6 +1,6 @@ import type {PropTypeConfig_String} from '@theatre/core/propTypes' import type SheetObject from '@theatre/core/sheetObjects/SheetObject' -import React, {useCallback} from 'react' +import React from 'react' import {useEditingToolsForPrimitiveProp} from './utils/useEditingToolsForPrimitiveProp' import {SingleRowPropEditor} from './utils/SingleRowPropEditor' import BasicStringInput from '@theatre/studio/uiComponents/form/BasicStringInput' @@ -16,16 +16,14 @@ const StringPropEditor: React.FC<{ propConfig, ) - const onChange = useCallback( - (el: React.ChangeEvent) => { - stuff.permenantlySetValue(String(el.target.value)) - }, - [propConfig, pointerToProp, obj], - ) - return ( - + ) } diff --git a/theatre/studio/src/uiComponents/form/BasicNumberInput.tsx b/theatre/studio/src/uiComponents/form/BasicNumberInput.tsx index 626247a..db906e0 100644 --- a/theatre/studio/src/uiComponents/form/BasicNumberInput.tsx +++ b/theatre/studio/src/uiComponents/form/BasicNumberInput.tsx @@ -5,8 +5,6 @@ import styled from 'styled-components' import DraggableArea from '@theatre/studio/uiComponents/DraggableArea' import mergeRefs from 'react-merge-refs' -type IMode = IState['mode'] - const Container = styled.div` height: 100%; width: 100%; diff --git a/theatre/studio/src/uiComponents/form/BasicStringInput.tsx b/theatre/studio/src/uiComponents/form/BasicStringInput.tsx index 163f2ee..e00c7cf 100644 --- a/theatre/studio/src/uiComponents/form/BasicStringInput.tsx +++ b/theatre/studio/src/uiComponents/form/BasicStringInput.tsx @@ -1,6 +1,9 @@ import styled from 'styled-components' +import type {MutableRefObject} from 'react' +import React, {useMemo, useRef, useState} from 'react' +import mergeRefs from 'react-merge-refs' -const BasicStringInput = styled.input.attrs({type: 'text'})` +const Input = styled.input.attrs({type: 'text'})` background: transparent; border: 1px solid transparent; color: rgba(255, 255, 255, 0.9); @@ -28,4 +31,162 @@ const BasicStringInput = styled.input.attrs({type: 'text'})` } ` +type IState_NoFocus = { + mode: 'noFocus' +} + +type IState_EditingViaKeyboard = { + mode: 'editingViaKeyboard' + currentEditedValueInString: string + valueBeforeEditing: string +} + +type IState = IState_NoFocus | IState_EditingViaKeyboard + +const alwaysValid = (v: string) => true + +const BasicStringInput: React.FC<{ + value: string + temporarilySetValue: (v: string) => void + discardTemporaryValue: () => void + permenantlySetValue: (v: string) => void + className?: string + isValid?: (v: string) => boolean + inputRef?: MutableRefObject + /** + * Called when the user hits Enter. One of the *SetValue() callbacks will be called + * before this, so use this for UI purposes such as closing a popover. + */ + onBlur?: () => void +}> = (propsA) => { + const [stateA, setState] = useState({mode: 'noFocus'}) + const isValid = propsA.isValid ?? alwaysValid + + const refs = useRef({state: stateA, props: propsA}) + refs.current = {state: stateA, props: propsA} + + const inputRef = useRef(null) + + const callbacks = useMemo(() => { + const inputChange = (e: React.ChangeEvent) => { + const target = e.target as HTMLInputElement + const {value} = target + const curState = refs.current.state as IState_EditingViaKeyboard + + setState({...curState, currentEditedValueInString: value}) + + if (!isValid(value)) return + + refs.current.props.temporarilySetValue(value) + } + + const onBlur = () => { + if (refs.current.state.mode === 'editingViaKeyboard') { + commitKeyboardInput() + setState({mode: 'noFocus'}) + } + if (propsA.onBlur) propsA.onBlur() + } + + const commitKeyboardInput = () => { + const curState = refs.current.state as IState_EditingViaKeyboard + const value = curState.currentEditedValueInString + + if (!isValid(value)) { + refs.current.props.discardTemporaryValue() + } else { + if (curState.valueBeforeEditing === value) { + refs.current.props.discardTemporaryValue() + } else { + refs.current.props.permenantlySetValue(value) + } + } + } + + const onInputKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + refs.current.props.discardTemporaryValue() + inputRef.current!.blur() + } else if (e.key === 'Enter' || e.key === 'Tab') { + commitKeyboardInput() + inputRef.current!.blur() + } + } + + const onClick = (e: React.MouseEvent) => { + if (refs.current.state.mode === 'noFocus') { + const c = inputRef.current! + c.focus() + e.preventDefault() + e.stopPropagation() + } else { + e.stopPropagation() + } + } + + const onFocus = () => { + if (refs.current.state.mode === 'noFocus') { + transitionToEditingViaKeyboardMode() + } else if (refs.current.state.mode === 'editingViaKeyboard') { + } + } + + const transitionToEditingViaKeyboardMode = () => { + const curValue = refs.current.props.value + setState({ + mode: 'editingViaKeyboard', + currentEditedValueInString: String(curValue), + valueBeforeEditing: curValue, + }) + + setTimeout(() => { + inputRef.current!.focus() + }) + } + + return { + inputChange, + onBlur, + onInputKeyDown, + onClick, + onFocus, + } + }, [refs, setState, inputRef]) + + let value = + stateA.mode !== 'editingViaKeyboard' + ? format(propsA.value) + : stateA.currentEditedValueInString + + const _refs = [inputRef] + if (propsA.inputRef) _refs.push(propsA.inputRef) + + const theInput = ( + { + e.stopPropagation() + }} + onDoubleClick={(e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + }} + /> + ) + + return theInput +} + +function format(v: string): string { + return v +} + export default BasicStringInput