Skip to content
Draft
Show file tree
Hide file tree
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
6 changes: 4 additions & 2 deletions app/components/form/fields/ComboboxField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ export function ComboboxField<
items,
transform,
validate,
popoverError,
...props
}: ComboboxFieldProps<TFieldValues, TName>) {
}: ComboboxFieldProps<TFieldValues, TName> & { popoverError?: boolean }) {
const { field, fieldState } = useController({
name,
control,
Expand Down Expand Up @@ -92,8 +93,9 @@ export function ComboboxField<
inputRef={field.ref}
transform={transform}
{...props}
popoverError={popoverError ? fieldState.error : undefined}
/>
<ErrorMessage error={fieldState.error} label={label} />
{!popoverError && <ErrorMessage error={fieldState.error} label={label} />}
</div>
)
}
11 changes: 10 additions & 1 deletion app/components/form/fields/DescriptionField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
*/
import type { FieldPath, FieldValues } from 'react-hook-form'

import { capitalize } from '~/util/str'

import { TextField, type TextFieldProps } from './TextField'

// TODO: Pull this from generated types
Expand All @@ -16,7 +18,14 @@ export function DescriptionField<
TFieldValues extends FieldValues,
TName extends FieldPath<TFieldValues>,
>(props: Omit<TextFieldProps<TFieldValues, TName>, 'validate'>) {
return <TextField as="textarea" validate={validateDescription} {...props} />
return (
<TextField
as="textarea"
label={props.label || capitalize(props.name)}
validate={validateDescription}
{...props}
/>
)
}

// TODO Update JSON schema to match this, add fuzz testing between this and name pattern
Expand Down
32 changes: 32 additions & 0 deletions app/components/form/fields/ErrorMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
*
* Copyright Oxide Computer Company
*/
import cn from 'classnames'
import type { FieldError } from 'react-hook-form'

import { Info12Icon } from '@oxide/design-system/icons/react'

import { TextInputError } from '~/ui/lib/TextInput'
import { Tooltip } from '~/ui/lib/Tooltip'

type ErrorMessageProps = {
error: FieldError | undefined
label: string
isSmall?: boolean
}

export function ErrorMessage({ error, label }: ErrorMessageProps) {
Expand All @@ -22,3 +27,30 @@ export function ErrorMessage({ error, label }: ErrorMessageProps) {

return <TextInputError>{message}</TextInputError>
}

export function PopoverErrorMessage({
error,
label,
className,
}: ErrorMessageProps & { className?: string }) {
if (!error) return null

const message = error.type === 'required' ? `${label} is required` : error.message
if (!message) return null

return (
<Tooltip content={message} placement="top" variant="error">
<button
type="button"
aria-label={`Error: ${message}`}
tabIndex={0}
className={cn(
className,
'-ml-1 flex h-6 w-6 flex-shrink-0 cursor-help items-center justify-center rounded-full bg-error-secondary'
)}
>
<Info12Icon className="text-error-secondary" aria-hidden="true" />
</button>
</Tooltip>
)
}
4 changes: 2 additions & 2 deletions app/components/form/fields/ListboxField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function ListboxField<
items,
name,
placeholder,
label = capitalize(name),
label,
disabled,
required,
description,
Expand Down Expand Up @@ -81,7 +81,7 @@ export function ListboxField<
buttonRef={field.ref}
hideOptionalTag={hideOptionalTag}
/>
<ErrorMessage error={fieldState.error} label={label} />
<ErrorMessage error={fieldState.error} label={label || capitalize(name)} />
</div>
)
}
33 changes: 19 additions & 14 deletions app/components/form/fields/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,26 +55,29 @@ export function TextField<
TName extends FieldPath<TFieldValues>,
>({
name,
label = capitalize(name),
label,
units,
description,
required,
...props
}: Omit<TextFieldProps<TFieldValues, TName>, 'id'> & UITextAreaProps) {
}: Omit<TextFieldProps<TFieldValues, TName>, 'id'> &
UITextAreaProps & { popoverError?: boolean }) {
// id is omitted from props because we generate it here
const id = useId()
return (
<div className="max-w-lg">
<div className="mb-2">
<FieldLabel htmlFor={id} id={`${id}-label`} optional={!required}>
{label} {units && <span className="ml-1 text-default">({units})</span>}
</FieldLabel>
{description && (
<TextInputHint id={`${id}-help-text`} className="mb-2">
{description}
</TextInputHint>
)}
</div>
{label && (
<div className="mb-2">
<FieldLabel htmlFor={id} id={`${id}-label`} optional={!required}>
{label} {units && <span className="ml-1 text-default">({units})</span>}
</FieldLabel>
{description && (
<TextInputHint id={`${id}-help-text`} className="mb-2">
{description}
</TextInputHint>
)}
</div>
)}
{/* passing the generated id is very important for a11y */}
<TextFieldInner name={name} {...props} id={id} />
</div>
Expand Down Expand Up @@ -102,8 +105,9 @@ export const TextFieldInner = <
required,
id: idProp,
transform,
popoverError,
...props
}: TextFieldProps<TFieldValues, TName> & UITextAreaProps) => {
}: TextFieldProps<TFieldValues, TName> & UITextAreaProps & { popoverError?: boolean }) => {
const generatedId = useId()
const id = idProp || generatedId
const {
Expand All @@ -119,10 +123,11 @@ export const TextFieldInner = <
error={!!error}
aria-labelledby={`${id}-label ${id}-help-text`}
onChange={(e) => onChange(transform ? transform(e.target.value) : e.target.value)}
popoverError={popoverError ? error : undefined}
{...fieldRest}
{...props}
/>
<ErrorMessage error={error} label={label} />
{!popoverError && <ErrorMessage error={error} label={label} />}
</>
)
}
Loading
Loading