1- import { useInputControl } from '@conform-to/react'
1+ import { FieldMetadata , useInputControl } from '@conform-to/react'
22import { REGEXP_ONLY_DIGITS_AND_CHARS , type OTPInputProps } from 'input-otp'
3- import React , { useId } from 'react'
4- import { Checkbox , type CheckboxProps } from './ui/checkbox.tsx '
3+ import React , { useId , useState } from 'react'
4+ import { Checkbox , type CheckboxProps } from './ui/checkbox'
55import {
66 InputOTP ,
77 InputOTPGroup ,
88 InputOTPSeparator ,
99 InputOTPSlot ,
10- } from './ui/input-otp.tsx'
11- import { Input } from './ui/input.tsx'
12- import { Label } from './ui/label.tsx'
13- import { Textarea } from './ui/textarea.tsx'
10+ } from './ui/input-otp'
11+ import { Input } from './ui/input'
12+ import { Label } from './ui/label'
13+ import { Button } from './ui/button'
14+ import {
15+ Select ,
16+ SelectContent ,
17+ SelectItem ,
18+ SelectTrigger ,
19+ SelectValue ,
20+ } from './ui/select'
21+ import { Textarea } from './ui/textarea'
22+ import {
23+ Command ,
24+ CommandEmpty ,
25+ CommandGroup ,
26+ CommandInput ,
27+ CommandItem ,
28+ CommandList ,
29+ CommandDialog ,
30+ } from '#app/components/ui/command'
31+ import {
32+ Popover ,
33+ PopoverContent ,
34+ PopoverTrigger ,
35+ } from '#app/components/ui/popover'
36+ import { Icon } from '#app/components/ui/icon'
37+ import { cn } from '#app/utils/misc'
1438
1539export type ListOfErrors = Array < string | null | undefined > | null | undefined
1640
@@ -48,6 +72,7 @@ export function Field({
4872 const fallbackId = useId ( )
4973 const id = inputProps . id ?? fallbackId
5074 const errorId = errors ?. length ? `${ id } -error` : undefined
75+
5176 return (
5277 < div className = { className } >
5378 < Label htmlFor = { id } { ...labelProps } />
@@ -64,6 +89,58 @@ export function Field({
6489 )
6590}
6691
92+ export function SelectField ( {
93+ labelProps,
94+ selectProps,
95+ selectOptions,
96+ errors,
97+ className,
98+ } : {
99+ labelProps : React . LabelHTMLAttributes < HTMLLabelElement >
100+ selectProps : {
101+ name ?: string
102+ placeholder ?: string
103+ disabled ?: boolean
104+ onValueChange ?: ( value : string ) => void
105+ }
106+ selectOptions ?: { value : string ; label : string } [ ]
107+ errors ?: ListOfErrors
108+ className ?: string
109+ } ) {
110+ const fallbackId = useId ( )
111+ const id = selectProps . name ?? fallbackId
112+ const errorId = errors ?. length ? `${ id } -error` : undefined
113+ const placeholder = selectProps ?. placeholder || 'Select'
114+
115+ return (
116+ < div className = { className } >
117+ < Label htmlFor = { id } { ...labelProps } />
118+ < Select { ...selectProps } >
119+ < SelectTrigger
120+ aria-invalid = { errorId ? true : undefined }
121+ aria-describedby = { errorId }
122+ >
123+ < SelectValue placeholder = { placeholder } />
124+ </ SelectTrigger >
125+ < SelectContent >
126+ { selectOptions ?. map ( ( option ) => (
127+ < SelectItem
128+ key = { `${ id } -${ option . value } ` }
129+ value = { option . value }
130+ className = "hover:bg-primary/10"
131+ >
132+ { option . label }
133+ </ SelectItem >
134+ ) ) }
135+ </ SelectContent >
136+ </ Select >
137+ < div className = "min-h-[32px] px-4 pb-3 pt-1" >
138+ { errorId ? < ErrorList id = { errorId } errors = { errors } /> : null }
139+ </ div >
140+ </ div >
141+ )
142+ }
143+
67144export function OTPField ( {
68145 labelProps,
69146 inputProps,
@@ -122,6 +199,7 @@ export function TextareaField({
122199 const fallbackId = useId ( )
123200 const id = textareaProps . id ?? textareaProps . name ?? fallbackId
124201 const errorId = errors ?. length ? `${ id } -error` : undefined
202+
125203 return (
126204 < div className = { className } >
127205 < Label htmlFor = { id } { ...labelProps } />
@@ -200,3 +278,167 @@ export function CheckboxField({
200278 </ div >
201279 )
202280}
281+
282+ // Add an input for the type="time" input. Should model the Field component.
283+ export function TimeField ( {
284+ labelProps,
285+ inputProps,
286+ errors,
287+ className,
288+ } : {
289+ labelProps : React . LabelHTMLAttributes < HTMLLabelElement >
290+ inputProps : React . InputHTMLAttributes < HTMLInputElement >
291+ errors ?: ListOfErrors
292+ className ?: string
293+ } ) {
294+ const fallbackId = useId ( )
295+ const id = inputProps . id ?? fallbackId
296+ const errorId = errors ?. length ? `${ id } -error` : undefined
297+
298+ return (
299+ < div className = { className } >
300+ < Label htmlFor = { id } { ...labelProps } />
301+ < Input
302+ id = { id }
303+ type = "time"
304+ aria-invalid = { errorId ? true : undefined }
305+ aria-describedby = { errorId }
306+ { ...inputProps }
307+ />
308+ < div className = "min-h-[32px] px-4 pb-3 pt-1" >
309+ { errorId ? < ErrorList id = { errorId } errors = { errors } /> : null }
310+ </ div >
311+ </ div >
312+ )
313+ }
314+
315+ export function NumberField ( {
316+ labelProps,
317+ inputProps,
318+ errors,
319+ className,
320+ } : {
321+ labelProps : React . LabelHTMLAttributes < HTMLLabelElement >
322+ inputProps : React . InputHTMLAttributes < HTMLInputElement >
323+ errors ?: ListOfErrors
324+ className ?: string
325+ } ) {
326+ const fallbackId = useId ( )
327+ const id = inputProps . id ?? fallbackId
328+ const errorId = errors ?. length ? `${ id } -error` : undefined
329+
330+ return (
331+ < div className = { className } >
332+ < Label htmlFor = { id } { ...labelProps } />
333+ < Input
334+ id = { id }
335+ type = "number"
336+ aria-invalid = { errorId ? true : undefined }
337+ aria-describedby = { errorId }
338+ { ...inputProps }
339+ />
340+ < div className = "min-h-[32px] px-4 pb-3 pt-1" >
341+ { errorId ? < ErrorList id = { errorId } errors = { errors } /> : null }
342+ </ div >
343+ </ div >
344+ )
345+ }
346+
347+ export function ComboboxField ( {
348+ labelProps,
349+ buttonProps,
350+ comboboxProps,
351+ comboboxOptions,
352+ errors,
353+ className,
354+ field,
355+ } : {
356+ labelProps : React . LabelHTMLAttributes < HTMLLabelElement >
357+ buttonProps ?: React . ButtonHTMLAttributes < HTMLButtonElement >
358+ comboboxProps : {
359+ name : string
360+ placeholder ?: string
361+ emptyText ?: string | JSX . Element
362+ }
363+ comboboxOptions : { value : string ; label : string } [ ]
364+ errors ?: ListOfErrors
365+ className ?: string
366+ field : FieldMetadata < string >
367+ } ) {
368+ const [ open , setOpen ] = useState ( false )
369+ const control = useInputControl ( field )
370+ const fallbackId = useId ( )
371+ const id = comboboxProps . name ?? fallbackId
372+ const errorId = errors ?. length ? `${ id } -error` : undefined
373+
374+ return (
375+ < div className = { className } >
376+ < Label htmlFor = { id } { ...labelProps } />
377+ < Popover open = { open } onOpenChange = { setOpen } >
378+ < PopoverTrigger asChild >
379+ < Button
380+ { ...buttonProps }
381+ variant = "outline"
382+ role = "combobox"
383+ aria-expanded = { open }
384+ aria-invalid = { errorId ? true : undefined }
385+ aria-describedby = { errorId }
386+ className = "w-full justify-between"
387+ >
388+ { control . value
389+ ? comboboxOptions . find ( ( option ) => option . value === control . value )
390+ ?. label
391+ : comboboxProps . placeholder || 'Select...' }
392+ < Icon name = "caret-sort" className = "ml-2 shrink-0" />
393+ </ Button >
394+ </ PopoverTrigger >
395+
396+ < PopoverContent className = "w-[--radix-popover-trigger-width] p-0" >
397+ < Command
398+ filter = { ( value , search ) => {
399+ const item = comboboxOptions . find ( ( item ) => item . value === value )
400+ if ( ! item ) return 0
401+ if ( item . label . toLowerCase ( ) . includes ( search . toLowerCase ( ) ) )
402+ return 1
403+
404+ return 0
405+ } }
406+ >
407+ < CommandInput placeholder = "Search..." />
408+ < CommandList >
409+ < CommandEmpty >
410+ { comboboxProps . emptyText || 'No options found.' }
411+ </ CommandEmpty >
412+ < CommandGroup >
413+ { comboboxOptions . map ( ( option ) => (
414+ < CommandItem
415+ key = { option . value }
416+ value = { option . value }
417+ onSelect = { ( currentValue ) => {
418+ control . change ( currentValue )
419+ setOpen ( false )
420+ } }
421+ >
422+ < Icon
423+ name = "check"
424+ className = { cn (
425+ 'mr-2 h-4 w-4' ,
426+ control . value === option . value
427+ ? 'opacity-100'
428+ : 'opacity-0' ,
429+ ) }
430+ />
431+ { option . label }
432+ </ CommandItem >
433+ ) ) }
434+ </ CommandGroup >
435+ </ CommandList >
436+ </ Command >
437+ </ PopoverContent >
438+ </ Popover >
439+ < div className = "min-h-[32px] px-4 pb-3 pt-1" >
440+ { errorId ? < ErrorList id = { errorId } errors = { errors } /> : null }
441+ </ div >
442+ </ div >
443+ )
444+ }
0 commit comments