99 lines
3.4 KiB
Plaintext
99 lines
3.4 KiB
Plaintext
---
|
|
title: Compose a profile form
|
|
description: Combine Field, Input, Select, and Button into an accessible form while keeping state in the application.
|
|
order: 30
|
|
toc:
|
|
- id: complete-example
|
|
title: Complete example
|
|
- id: validation
|
|
title: Validation state
|
|
- id: controlled-select
|
|
title: Controlled Select
|
|
- id: styling
|
|
title: Style overrides
|
|
---
|
|
|
|
## Complete example {#complete-example}
|
|
|
|
```tsx
|
|
import * as React from "react"
|
|
import { Button } from "@workspace/ui/components/button"
|
|
import {
|
|
Field,
|
|
FieldDescription,
|
|
FieldError,
|
|
FieldGroup,
|
|
FieldLabel,
|
|
} from "@workspace/ui/components/field"
|
|
import { Input } from "@workspace/ui/components/input"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@workspace/ui/components/select"
|
|
|
|
export function ProfileForm() {
|
|
const [role, setRole] = React.useState("editor")
|
|
const [error, setError] = React.useState("")
|
|
|
|
function submit(event: React.FormEvent<HTMLFormElement>) {
|
|
event.preventDefault()
|
|
const data = new FormData(event.currentTarget)
|
|
setError(data.get("displayName") ? "" : "Enter a display name")
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={submit} className="max-w-md space-y-6">
|
|
<FieldGroup>
|
|
<Field data-invalid={Boolean(error)}>
|
|
<FieldLabel htmlFor="display-name">Display name</FieldLabel>
|
|
<Input
|
|
id="display-name"
|
|
name="displayName"
|
|
aria-invalid={Boolean(error)}
|
|
aria-describedby="display-name-error"
|
|
/>
|
|
<FieldDescription>
|
|
This name is visible to other members.
|
|
</FieldDescription>
|
|
<FieldError id="display-name-error">{error}</FieldError>
|
|
</Field>
|
|
|
|
<Field>
|
|
<FieldLabel htmlFor="role">Role</FieldLabel>
|
|
<Select
|
|
value={role}
|
|
onValueChange={(value) => setRole(value ?? "editor")}
|
|
>
|
|
<SelectTrigger id="role">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="viewer">Viewer</SelectItem>
|
|
<SelectItem value="editor">Editor</SelectItem>
|
|
<SelectItem value="admin">Administrator</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</Field>
|
|
</FieldGroup>
|
|
|
|
<Button type="submit">Save profile</Button>
|
|
</form>
|
|
)
|
|
}
|
|
```
|
|
|
|
## Validation state {#validation}
|
|
|
|
The UI package does not select a validation library. The application produces errors and connects the same state to `Field data-invalid`, the control's `aria-invalid`, and `FieldError`. Multiple schema errors can be passed through `FieldError errors={errors}`; the component deduplicates them before rendering a list.
|
|
|
|
## Controlled Select {#controlled-select}
|
|
|
|
The local Select is built on Base UI and uses `value` with `onValueChange`. `SelectTrigger` renders its own arrow, so it only needs `SelectValue`; do not add another chevron. The popup aligns to the trigger and adopts its width by default. Adjust `side`, `align`, `sideOffset`, and `alignItemWithTrigger` on `SelectContent` when required.
|
|
|
|
## Style overrides {#styling}
|
|
|
|
Use the existing `variant`, `size`, and structural parts first, then apply page layout through `className`. Components merge classes with `cn`, allowing consumer Tailwind classes to override default utilities. Promote overrides repeated across pages into a new variant or composite component instead of copying long class strings.
|