fix: prevent category Cancel overflow and add Tab/Enter to create options
- TreeMetadataForm: add min-w-0 + shrink-0 to keep Cancel button in-panel - NodeFormDecision: Tab or Enter on the last non-empty option input adds a new option and auto-focuses it; empty last input lets Tab pass through normally Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { useRef, useEffect } from 'react'
|
||||||
import { Play } from 'lucide-react'
|
import { Play } from 'lucide-react'
|
||||||
import { DynamicArrayField } from './DynamicArrayField'
|
import { DynamicArrayField } from './DynamicArrayField'
|
||||||
import { useTreeEditorStore } from '@/store/treeEditorStore'
|
import { useTreeEditorStore } from '@/store/treeEditorStore'
|
||||||
@@ -18,6 +19,9 @@ const indexToLetter = (index: number): string => {
|
|||||||
export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
|
export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
|
||||||
const { validationErrors } = useTreeEditorStore()
|
const { validationErrors } = useTreeEditorStore()
|
||||||
const isRootNode = node.id === 'root'
|
const isRootNode = node.id === 'root'
|
||||||
|
// Track input elements by index so we can focus the newly added one
|
||||||
|
const inputRefs = useRef<Map<number, HTMLInputElement>>(new Map())
|
||||||
|
const shouldFocusLast = useRef(false)
|
||||||
|
|
||||||
const questionError = validationErrors.find(
|
const questionError = validationErrors.find(
|
||||||
e => e.nodeId === node.id && e.field === 'question'
|
e => e.nodeId === node.id && e.field === 'question'
|
||||||
@@ -27,6 +31,15 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
|
|||||||
e => e.nodeId === node.id && e.field === 'options'
|
e => e.nodeId === node.id && e.field === 'options'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// After options array grows (due to keyboard-triggered add), focus the last input
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldFocusLast.current) {
|
||||||
|
shouldFocusLast.current = false
|
||||||
|
const lastIndex = (node.options?.length ?? 1) - 1
|
||||||
|
inputRefs.current.get(lastIndex)?.focus()
|
||||||
|
}
|
||||||
|
}, [node.options?.length])
|
||||||
|
|
||||||
const handleAddOption = () => {
|
const handleAddOption = () => {
|
||||||
const newOption: TreeOption = {
|
const newOption: TreeOption = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
@@ -38,6 +51,12 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a new option and focus it (used by keyboard shortcut)
|
||||||
|
const handleAddOptionAndFocus = () => {
|
||||||
|
shouldFocusLast.current = true
|
||||||
|
handleAddOption()
|
||||||
|
}
|
||||||
|
|
||||||
const handleRemoveOption = (index: number) => {
|
const handleRemoveOption = (index: number) => {
|
||||||
const newOptions = [...(node.options || [])]
|
const newOptions = [...(node.options || [])]
|
||||||
newOptions.splice(index, 1)
|
newOptions.splice(index, 1)
|
||||||
@@ -147,6 +166,7 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
|
|||||||
e => e.nodeId === node.id && e.field === `options[${index}].label`
|
e => e.nodeId === node.id && e.field === `options[${index}].label`
|
||||||
)
|
)
|
||||||
const letter = indexToLetter(index)
|
const letter = indexToLetter(index)
|
||||||
|
const isLastOption = index === (node.options?.length ?? 1) - 1
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -158,12 +178,22 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
|
|||||||
</span>
|
</span>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<input
|
<input
|
||||||
|
ref={(el) => {
|
||||||
|
if (el) inputRefs.current.set(index, el)
|
||||||
|
else inputRefs.current.delete(index)
|
||||||
|
}}
|
||||||
type="text"
|
type="text"
|
||||||
value={option.label}
|
value={option.label}
|
||||||
onChange={(e) => handleUpdateOption(index, { label: e.target.value })}
|
onChange={(e) => handleUpdateOption(index, { label: e.target.value })}
|
||||||
placeholder={isRootNode
|
placeholder={isRootNode
|
||||||
? `Branch ${letter}: e.g., "Network Issues"...`
|
? `Branch ${letter}: e.g., "Network Issues"...`
|
||||||
: `Option ${letter} label`}
|
: `Option ${letter} label`}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if ((e.key === 'Tab' || e.key === 'Enter') && isLastOption && option.label.trim()) {
|
||||||
|
e.preventDefault()
|
||||||
|
handleAddOptionAndFocus()
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'block w-full rounded-md border px-3 py-2 text-sm',
|
'block w-full rounded-md border px-3 py-2 text-sm',
|
||||||
'bg-background text-foreground placeholder:text-muted-foreground',
|
'bg-background text-foreground placeholder:text-muted-foreground',
|
||||||
|
|||||||
@@ -132,19 +132,20 @@ export function TreeMetadataForm() {
|
|||||||
onChange={(e) => setCategory(e.target.value)}
|
onChange={(e) => setCategory(e.target.value)}
|
||||||
placeholder="Enter new category"
|
placeholder="Enter new category"
|
||||||
className={cn(
|
className={cn(
|
||||||
'block flex-1 rounded-md border border-border px-3 py-2 text-sm',
|
'block min-w-0 flex-1 rounded-md border border-border px-3 py-2 text-sm',
|
||||||
'bg-card text-foreground placeholder:text-muted-foreground',
|
'bg-card text-foreground placeholder:text-muted-foreground',
|
||||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||||
)}
|
)}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCustomCategory(false)
|
setCustomCategory(false)
|
||||||
setCategory('')
|
setCategory('')
|
||||||
setCategoryId(null)
|
setCategoryId(null)
|
||||||
}}
|
}}
|
||||||
className="rounded-md border border-border px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
className="shrink-0 rounded-md border border-border px-2.5 py-2 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user