# Field label approaches
[toc]
## Desirable features
- Auto-associate label and input (the less people have to do manually to get good a11y, the better)
- Easy to have consistent layout (spacing, relative positioning)
- Correctly apply error message attributes
- (eventually) Consistent validation handling across field types
- Minimal amount of typing to achieve a "normal form layout"
- Must not require a parent form component
## Research
https://hackmd.io/OulpnA9eTEebdAc_0bVhMQ
## Field with label/description/error slots and input as child
```jsx=
<Field
className="rootClass"
label="hello"
description={<div>fancy hello</div>}
errorMessage="aka.ms/nohello"
required
>
<Input ... />
</Field>
```
maybe: checkbox, radio, option are special and MUST have a label built in?
## Field with slots for input/label/description
```jsx=
<Field
className="rootClass"
input={<Input />}
label="hello"
description={<div>fancy hello</div>}
errorMessage="aka.ms/nohello"
required
/>
```
(For greater layout customization, use the hooks to make a new component)
(Has anyone validated using the hooks to make a customized component?)
If you are looking for Zip Codes database then you can search any zip code on <a href="https://usazipcodes.net/">USA Zip Codes</a>.
Possible issues:
- how would someone change the layout? lots of positioning props? what if they wanted more customization?
## Field with children for input/label/description
Theoretically Field uses an internal context that our form label, error, description, and input/combobox/etc components use to set ids, error state, etc.
```jsx=
<Field id={requiredProp} required={true} validationSomething={something?}>
<FieldLabel>Email</FieldLabel>
<Input />
<FieldDescription>work email</FieldDescription>
<FieldError>please enter a valid email</FieldError>
</Field>
<Field id={requiredProp2}>
<FieldLabel>Country</FieldLabel>
<FieldDescription>the world's only three countries</FieldDescription>
<Select>
<Option>USA</Option>
<Option>UK</Option>
<Option>India</Option>
</Select>
</Field>
```
Possible issues:
- basically: how to know which child is which?
- how to handle layout? (which styles go on which child)
- how to handle setting error state on input based on presence of error message?
## Field hook
```typescript=
type SlotType = someActualTypeHereIForgetWhich
type FieldSlots = {
// maybe?
input: ObjectShorthandProps<React.InputHTMLAttributes<HTMLInputElement>>;
label: ObjectShorthandProps<React.LabelHTMLAttributes<HTMLLabelElement>>;
description: SlotType;
errorText: SlotType;
required: SlotType;
}
interface FieldProps extends ComponentProps<FieldSlots, 'input'> {
labelPosition?: 'start' | 'end' | 'top' | 'bottom';
// note: `size` is a native input prop name
fieldSize?: 'small' | 'medium' | 'large';
/** custom override for ID (generated ID used by default) */
id?: string;
// and some validation props maybe
}
interface FieldState {
// stuff
}
const useField = <
TProps extends FieldProps, TState extends FieldState
>(props: TProps): TState => {
// return field-related slots and state
}
const useFieldStyles = <TState extends FieldState>(state: TState) => {
// styles for positioning/spacing of label, field, description, etc
}
// const renderField = <TState extends FieldState>(state: TState) => {
// const { slots, slotProps } = getSlots<FieldSlots>(state, fieldShorthandProps);
// }
```
### Option 1: Input + InputField
Input stays as-is (basic component)
InputField
- what is the primary slot?
```typescript=
// some nuances TBD due to primary slot issues
type InputFieldSlots = FieldSlots & InputSlots;
interface InputProps extends InputProps, FieldProps { }
interface InputState extends InputState, FieldState { }
const useInputField = (props: InputProps, ref: React.Ref<HTMLInputElement>): InputState => {
}
```
#### Option 1a: InputField with DOM root as primary
```jsx=
<InputField
className="rootClass"
input={{
// Input props
className: 'inputClass'
}} />
```
#### Option 1b: InputField with input element as primary
```jsx=
<InputField
className="inputClass"
// how to give the root a class? same problem all over again :(
/>
```
### Option 2: Input includes Field features
downside: larger bundles
```typescript=
type InputSlots = FieldSlots & { /*more slots*/ };
interface InputProps extends FieldProps { /*more props*/ }
interface InputState extends FieldState { /*more state*/ }
const useInput = (props: InputProps, ref: React.Ref<HTMLInputElement>): InputState => {
}
```
## Field Group + Label
The issue: group labels for groups of checkboxes and radios are often treated as if they were the same as field labels for text inputs, comboboxes, sliders, etc.
- The field label is "Name (4 to 8 characters):" here, and should use a `<label>` element:
![screenshot of a blank text input with label](https://i.imgur.com/wN6Cmww.png)
- The group label is "Select a maintenance drone" here, and should use a `<legend>` element:
![screenshot of a set of three radio buttons and legend](https://i.imgur.com/9HEMejg.png)
### Group label option 1
We could make the `FormField` and `FieldLabel` components able to handle being a single field vs. a group of fields:
```jsx=
<FormField fieldType={FieldType.Group}>
<FieldLabel>Favorite pet:</FieldLabel>
<Radio label="Cat" />
<Radio label="Dog" />
<Radio label="Tribble" />
</FormField>
```
simplified output:
```htmlembedded=
<fieldset>
<legend>Favorite Pet:</legend>
<label for="radio1">Cat</label>
<input type="radio" id="radio1" name="group">
<label for="radio2">Dog</label>
<input type="radio" id="radio2" name="group">
<label for="radio3">Tribble</label>
<input type="radio" id="radio3" name="group">
</fieldset>
```
### Group label option 2
We could provide a more general `FieldGroup` component that could be used with checkboxes/radios, and could also be used to group other sets of components together (e.g. text fields in an address).
```jsx=
<FormGroup>
<FormGroupLabel>Address</FormGroupLabel>
<FormField>
<FieldLabel>Street Address</FieldLabel>
<InputField />
</FormField>
<FormField>
<FieldLabel>City</FieldLabel>
<InputField />
</FormField>
<FormField>
<FieldLabel>Zip code</FieldLabel>
<InputField />
</FormField>
</FormGroup>
```
simplified output:
```htmlembedded=
<fieldset>
<legend>Address</legend>
<div class="formfield">
<label for="id1">Street Address</label>
<input type="text" id="id1">
</div>
<div class="formfield">
<label for="id2">City</label>
<input type="text" id="id2">
</div>
<div class="formfield">
<label for="id3">Zip code</label>
<input type="text" id="id3">
</div>
</fieldset>
```
```tsx
<Field> //props.Children.only().type === Input props.children.only().props.onChange
<Input /> //onChange onBlur
</Field>
<Field errorMessage={(children) => string} missingRequired={(children) => string | boolean}>
<GeoffsComponent tags=[] /> //onChange onBlur
</Field>
<InputField />
```