fix: ForkModal accessibility and UX (escape, click-outside, labels, maxLength)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { GitBranch, X } from 'lucide-react'
|
import { GitBranch, X } from 'lucide-react'
|
||||||
import { treesApi } from '@/api/trees'
|
import { treesApi } from '@/api/trees'
|
||||||
import { toast } from '@/lib/toast'
|
import { toast } from '@/lib/toast'
|
||||||
@@ -18,6 +18,14 @@ export function ForkModal({ treeId, treeName, onClose }: ForkModalProps) {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') onClose()
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}, [onClose])
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!name.trim()) return
|
if (!name.trim()) return
|
||||||
@@ -40,8 +48,14 @@ export function ForkModal({ treeId, treeName, onClose }: ForkModalProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
|
<div
|
||||||
<div className="w-full max-w-md rounded-xl border border-border bg-card shadow-xl">
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full max-w-md rounded-xl border border-border bg-card shadow-xl"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between border-b border-border px-5 py-4">
|
<div className="flex items-center justify-between border-b border-border px-5 py-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -50,6 +64,7 @@ export function ForkModal({ treeId, treeName, onClose }: ForkModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
aria-label="Close"
|
||||||
className="rounded-md p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
className="rounded-md p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
@@ -59,15 +74,17 @@ export function ForkModal({ treeId, treeName, onClose }: ForkModalProps) {
|
|||||||
{/* Body */}
|
{/* Body */}
|
||||||
<form onSubmit={handleSubmit} className="space-y-4 px-5 py-4">
|
<form onSubmit={handleSubmit} className="space-y-4 px-5 py-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-xs font-medium text-muted-foreground">
|
<label htmlFor="fork-name" className="mb-1.5 block text-xs font-medium text-muted-foreground">
|
||||||
Name <span className="text-red-400">*</span>
|
Name <span className="text-red-400">*</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
id="fork-name"
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
required
|
required
|
||||||
autoFocus
|
autoFocus
|
||||||
|
maxLength={255}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground',
|
'w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground',
|
||||||
'placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
'placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
|
||||||
@@ -76,11 +93,12 @@ export function ForkModal({ treeId, treeName, onClose }: ForkModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-xs font-medium text-muted-foreground">
|
<label htmlFor="fork-reason" className="mb-1.5 block text-xs font-medium text-muted-foreground">
|
||||||
Reason for Forking{' '}
|
Reason for Forking{' '}
|
||||||
<span className="text-muted-foreground/60">(optional)</span>
|
<span className="text-muted-foreground/60">(optional)</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
id="fork-reason"
|
||||||
value={forkReason}
|
value={forkReason}
|
||||||
onChange={(e) => setForkReason(e.target.value)}
|
onChange={(e) => setForkReason(e.target.value)}
|
||||||
rows={3}
|
rows={3}
|
||||||
@@ -108,7 +126,7 @@ export function ForkModal({ treeId, treeName, onClose }: ForkModalProps) {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmitting || !name.trim()}
|
disabled={isSubmitting || !name.trim()}
|
||||||
className="bg-gradient-brand flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20 hover:opacity-90 disabled:opacity-40"
|
className="bg-gradient-brand flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20 hover:opacity-90 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{isSubmitting ? 'Forking…' : 'Fork Flow'}
|
{isSubmitting ? 'Forking…' : 'Fork Flow'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user