Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New form fields #855

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 249 additions & 7 deletions app/components/forms.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
import { useInputControl } from '@conform-to/react'
import { FieldMetadata, useInputControl } from '@conform-to/react'

Check warning on line 1 in app/components/forms.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

Imports "FieldMetadata" are only used as type
import { REGEXP_ONLY_DIGITS_AND_CHARS, type OTPInputProps } from 'input-otp'
import React, { useId } from 'react'
import { Checkbox, type CheckboxProps } from './ui/checkbox.tsx'
import React, { useId, useState } from 'react'
import { Checkbox, type CheckboxProps } from './ui/checkbox'
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from './ui/input-otp.tsx'
import { Input } from './ui/input.tsx'
import { Label } from './ui/label.tsx'
import { Textarea } from './ui/textarea.tsx'
} from './ui/input-otp'
import { Input } from './ui/input'

Check warning on line 11 in app/components/forms.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

`./ui/input` import should occur before import of `./ui/input-otp`
import { Label } from './ui/label'
import { Button } from './ui/button'

Check warning on line 13 in app/components/forms.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

`./ui/button` import should occur before import of `./ui/checkbox`
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from './ui/select'

Check failure on line 20 in app/components/forms.tsx

View workflow job for this annotation

GitHub Actions / ʦ TypeScript

Cannot find module './ui/select' or its corresponding type declarations.
import { Textarea } from './ui/textarea'
import {

Check warning on line 22 in app/components/forms.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

`#app/components/ui/command` import should occur before import of `./ui/checkbox`
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandDialog,

Check warning on line 29 in app/components/forms.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'CommandDialog' is defined but never used. Allowed unused vars must match /^ignored/u
} from '#app/components/ui/command'

Check failure on line 30 in app/components/forms.tsx

View workflow job for this annotation

GitHub Actions / ʦ TypeScript

Cannot find module '#app/components/ui/command' or its corresponding type declarations.
import {

Check warning on line 31 in app/components/forms.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

`#app/components/ui/popover` import should occur before import of `./ui/checkbox`
Popover,
PopoverContent,
PopoverTrigger,
} from '#app/components/ui/popover'

Check failure on line 35 in app/components/forms.tsx

View workflow job for this annotation

GitHub Actions / ʦ TypeScript

Cannot find module '#app/components/ui/popover' or its corresponding type declarations.
import { Icon } from '#app/components/ui/icon'

Check warning on line 36 in app/components/forms.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

`#app/components/ui/icon` import should occur before import of `./ui/checkbox`
import { cn } from '#app/utils/misc'

Check warning on line 37 in app/components/forms.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

`#app/utils/misc` import should occur before import of `./ui/checkbox`

export type ListOfErrors = Array<string | null | undefined> | null | undefined

Expand Down Expand Up @@ -48,6 +72,7 @@
const fallbackId = useId()
const id = inputProps.id ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined

return (
<div className={className}>
<Label htmlFor={id} {...labelProps} />
Expand All @@ -64,6 +89,58 @@
)
}

export function SelectField({
labelProps,
selectProps,
selectOptions,
errors,
className,
}: {
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
selectProps: {
name?: string
placeholder?: string
disabled?: boolean
onValueChange?: (value: string) => void
}
selectOptions?: { value: string; label: string }[]
errors?: ListOfErrors
className?: string
}) {
const fallbackId = useId()
const id = selectProps.name ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined
const placeholder = selectProps?.placeholder || 'Select'

return (
<div className={className}>
<Label htmlFor={id} {...labelProps} />
<Select {...selectProps}>
<SelectTrigger
aria-invalid={errorId ? true : undefined}
aria-describedby={errorId}
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{selectOptions?.map((option) => (
<SelectItem
key={`${id}-${option.value}`}
value={option.value}
className="hover:bg-primary/10"
>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="min-h-[32px] px-4 pb-3 pt-1">
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
</div>
</div>
)
}

export function OTPField({
labelProps,
inputProps,
Expand Down Expand Up @@ -122,6 +199,7 @@
const fallbackId = useId()
const id = textareaProps.id ?? textareaProps.name ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined

return (
<div className={className}>
<Label htmlFor={id} {...labelProps} />
Expand Down Expand Up @@ -200,3 +278,167 @@
</div>
)
}

// Add an input for the type="time" input. Should model the Field component.
export function TimeField({
labelProps,
inputProps,
errors,
className,
}: {
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
inputProps: React.InputHTMLAttributes<HTMLInputElement>
errors?: ListOfErrors
className?: string
}) {
const fallbackId = useId()
const id = inputProps.id ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined

return (
<div className={className}>
<Label htmlFor={id} {...labelProps} />
<Input
id={id}
type="time"
aria-invalid={errorId ? true : undefined}
aria-describedby={errorId}
{...inputProps}
/>
<div className="min-h-[32px] px-4 pb-3 pt-1">
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
</div>
</div>
)
}

export function NumberField({
labelProps,
inputProps,
errors,
className,
}: {
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
inputProps: React.InputHTMLAttributes<HTMLInputElement>
errors?: ListOfErrors
className?: string
}) {
const fallbackId = useId()
const id = inputProps.id ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined

return (
<div className={className}>
<Label htmlFor={id} {...labelProps} />
<Input
id={id}
type="number"
aria-invalid={errorId ? true : undefined}
aria-describedby={errorId}
{...inputProps}
/>
<div className="min-h-[32px] px-4 pb-3 pt-1">
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
</div>
</div>
)
}

export function ComboboxField({
labelProps,
buttonProps,
comboboxProps,
comboboxOptions,
errors,
className,
field,
}: {
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
buttonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>
comboboxProps: {
name: string
placeholder?: string
emptyText?: string | JSX.Element
}
comboboxOptions: { value: string; label: string }[]
errors?: ListOfErrors
className?: string
field: FieldMetadata<string>
}) {
const [open, setOpen] = useState(false)
const control = useInputControl(field)
const fallbackId = useId()
const id = comboboxProps.name ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined

return (
<div className={className}>
<Label htmlFor={id} {...labelProps} />
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
{...buttonProps}
variant="outline"
role="combobox"
aria-expanded={open}
aria-invalid={errorId ? true : undefined}
aria-describedby={errorId}
className="w-full justify-between"
>
{control.value
? comboboxOptions.find((option) => option.value === control.value)
?.label
: comboboxProps.placeholder || 'Select...'}
<Icon name="caret-sort" className="ml-2 shrink-0" />

Check failure on line 392 in app/components/forms.tsx

View workflow job for this annotation

GitHub Actions / ʦ TypeScript

Type '"caret-sort"' is not assignable to type '"reset" | "arrow-left" | "arrow-right" | "avatar" | "camera" | "check" | "clock" | "cross-1" | "dots-horizontal" | "download" | "envelope-closed" | "exit" | "file-text" | "github-logo" | ... 12 more ... | "update"'.
</Button>
</PopoverTrigger>

<PopoverContent className="w-[--radix-popover-trigger-width] p-0">
<Command
filter={(value, search) => {

Check failure on line 398 in app/components/forms.tsx

View workflow job for this annotation

GitHub Actions / ʦ TypeScript

Parameter 'value' implicitly has an 'any' type.

Check failure on line 398 in app/components/forms.tsx

View workflow job for this annotation

GitHub Actions / ʦ TypeScript

Parameter 'search' implicitly has an 'any' type.
const item = comboboxOptions.find((item) => item.value === value)
if (!item) return 0
if (item.label.toLowerCase().includes(search.toLowerCase()))
return 1

return 0
}}
>
<CommandInput placeholder="Search..." />
<CommandList>
<CommandEmpty>
{comboboxProps.emptyText || 'No options found.'}
</CommandEmpty>
<CommandGroup>
{comboboxOptions.map((option) => (
<CommandItem
key={option.value}
value={option.value}
onSelect={(currentValue) => {

Check failure on line 417 in app/components/forms.tsx

View workflow job for this annotation

GitHub Actions / ʦ TypeScript

Parameter 'currentValue' implicitly has an 'any' type.
control.change(currentValue)
setOpen(false)
}}
>
<Icon
name="check"
className={cn(
'mr-2 h-4 w-4',
control.value === option.value
? 'opacity-100'
: 'opacity-0',
)}
/>
{option.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<div className="min-h-[32px] px-4 pb-3 pt-1">
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
</div>
</div>
)
}
Loading