Fix form validation: red outline only on submit, add required asterisks

- Remove instant invalid:ring/border from Input component (was showing
  red outline on empty required fields before any interaction)
- Add CSS rule: form[data-submitted] input:invalid shows red border
- Add global submit listener in main.tsx that sets data-submitted on forms
- Add required prop to Labels missing asterisks: PersonForm (First Name),
  LocationForm (Location Name), CalendarForm (Name), LockScreen
  (Username, Password, Confirm Password)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-02-25 17:53:15 +08:00
parent 4207a62ad8
commit f5265a589e
7 changed files with 23 additions and 7 deletions

View File

@ -204,7 +204,7 @@ export default function LockScreen() {
<form onSubmit={handleCredentialSubmit} className="space-y-4"> <form onSubmit={handleCredentialSubmit} className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="username">Username</Label> <Label htmlFor="username" required>Username</Label>
<Input <Input
id="username" id="username"
type="text" type="text"
@ -218,7 +218,7 @@ export default function LockScreen() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="password">Password</Label> <Label htmlFor="password" required>Password</Label>
<Input <Input
id="password" id="password"
type="password" type="password"
@ -232,7 +232,7 @@ export default function LockScreen() {
{isSetup && ( {isSetup && (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="confirm-password">Confirm Password</Label> <Label htmlFor="confirm-password" required>Confirm Password</Label>
<Input <Input
id="confirm-password" id="confirm-password"
type="password" type="password"

View File

@ -89,7 +89,7 @@ export default function CalendarForm({ calendar, onClose }: CalendarFormProps) {
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="cal-name">Name</Label> <Label htmlFor="cal-name" required>Name</Label>
<Input <Input
id="cal-name" id="cal-name"
value={name} value={name}

View File

@ -110,7 +110,7 @@ export default function LocationForm({ location, categories, onClose }: Location
<div className="px-6 py-5 space-y-4 flex-1"> <div className="px-6 py-5 space-y-4 flex-1">
{/* Location Name */} {/* Location Name */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="loc-name">Location Name</Label> <Label htmlFor="loc-name" required>Location Name</Label>
<Input <Input
id="loc-name" id="loc-name"
value={formData.name} value={formData.name}

View File

@ -132,7 +132,7 @@ export default function PersonForm({ person, categories, onClose }: PersonFormPr
{/* Row 2: First + Last name */} {/* Row 2: First + Last name */}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="first_name">First Name</Label> <Label htmlFor="first_name" required>First Name</Label>
<Input <Input
id="first_name" id="first_name"
value={formData.first_name} value={formData.first_name}

View File

@ -9,7 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input <input
type={type} type={type}
className={cn( className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 invalid:ring-red-500 invalid:border-red-500', 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className className
)} )}
ref={ref} ref={ref}

View File

@ -193,6 +193,14 @@
font-weight: 600; font-weight: 600;
} }
/* ── Form validation — red outline only after submit attempt ── */
form[data-submitted] input:invalid,
form[data-submitted] select:invalid,
form[data-submitted] textarea:invalid {
border-color: hsl(0 62.8% 50%);
box-shadow: 0 0 0 2px hsl(0 62.8% 50% / 0.25);
}
/* ── Ambient background animations ── */ /* ── Ambient background animations ── */
@keyframes drift-1 { @keyframes drift-1 {

View File

@ -6,6 +6,14 @@ import { Toaster } from 'sonner';
import App from './App'; import App from './App';
import './index.css'; import './index.css';
// Mark forms as submitted so CSS validation outlines only appear after a submit attempt.
// The attribute is cleared naturally when Sheet/Dialog forms unmount and remount.
document.addEventListener('submit', (e) => {
if (e.target instanceof HTMLFormElement) {
e.target.setAttribute('data-submitted', '');
}
}, true);
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {