Skip to content
340 changes: 308 additions & 32 deletions packages/react-components/react-field/Spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,338 @@

## Background

_Description and use cases of this component_
Fields add add a label, validation text, and helper text to form input components. The existing input components (such as `Input` and `Combobox`) are wrapped to create field versions of them (such as `InputField` and `ComboboxField`).

Epic issue tracking implementation: https://github.com/microsoft/fluentui/issues/19627

## Prior Art

_Include background research done for this component_
Existing libraries tend to take one of the following approaches to field.

1. Include support for label, error text, etc. in the base input component. Libraries using this approach include:
- **FluentUI v8** - [`TextField`](https://developer.microsoft.com/en-us/fluentui#/controls/web/textfield), [`Dropdown`](https://developer.microsoft.com/en-us/fluentui#/controls/web/dropdown), [`ChoiceGroup`](https://developer.microsoft.com/en-us/fluentui#/controls/web/choicegroup), etc.
- **Spectrum** - [`TextField`](https://react-spectrum.adobe.com/react-spectrum/TextField.html), [`Slider`](https://react-spectrum.adobe.com/react-spectrum/Slider.html), [`RadioGroup`](https://react-spectrum.adobe.com/react-spectrum/RadioGroup.html), etc.
2. Provide a set of components that are manually constructed into a field. This requires manually hooking up the components using props like `htmlFor` and `aria-describedby`. Libraries using this approach include:
- **FluentUI v0** - [`FormField`](https://fluentsite.z22.web.core.windows.net/0.64.0/components/form/props#form-field), [`FormLabel`](https://fluentsite.z22.web.core.windows.net/0.64.0/components/form/props#form-label), [`FormMessage`](https://fluentsite.z22.web.core.windows.net/0.64.0/components/form/props#form-message)
- **Ant** - [`Form.Item`](https://ant.design/components/form/#Form.Item) (uses context to do some of the hooking up between the item and the field component).
3. Provide base components without a label or descriptive text, and then Field versions of those controls. Libraries using this approach include:
- **FluentUI v0** - [`Input`](https://fluentsite.z22.web.core.windows.net/0.64.0/components/input/props) and [`FormInput`](https://fluentsite.z22.web.core.windows.net/0.64.0/components/form/props#form-input), for example.
- **Evergreen UI** - [`TextInput`](https://evergreen.segment.com/components/text-input) and [`TextInputField`](https://evergreen.segment.com/components/text-input#textinputfield), for example.

The Field implementation in this spec follows pattern (3). There are Field versions of all components that can be used as form inputs. There are several reasons, including:

- _Link to Open UI research_
- _Link to comparison of v7 and v0_
- _Link to GitHub epic issue for the converged component_
- **Accessibility**: By combining a base component with the field props into a single component, all of the accessibility props like `htmlFor` and `aria-describedby` are set correctly for "free".
- **Simplicity**: All props related to the component (such as `label`, `id`, `status="error"`, etc.) are on the same component, rather than split between multiple components (like separate `Field` and `Input` components).
- **Consistency**: All of the Field components share a common set of props for the label, status, helperText, etc.
- **Bundle size**: When the label and other field functionality is not needed, it is still possible to use the base components without pulling in unnecessary dependencies (like `Label` and the field styling).

## Sample Code

_Provide some representative example code that uses the proposed API for the component_
Each input component has a field version (such as `InputField`, `ComboboxField`, etc.) that includes the features of Field added to that component.

```jsx
<>
<InputField
// Field-specific props
label="This is the field label"
fieldOrientation="horizontal"
status="error"
statusText="This is error text"
// All props and slots of the underlying Input component are supported
required
size="small"
contentBefore="$"
contentAfter=".00"
/>
<RadioGroupField label="Radio group field">
<Radio value="one" label="Option one" />
<Radio value="two" label="Option two" />
<Radio value="three" label="Option three" />
</RadioGroupField>
<ComboboxField label="Combobox field" status="success" statusText="Success text">
<Option value="one">Option one</Option>
<Option value="two">Option two</Option>
<Option value="three">Option three</Option>
</ComboboxField>
<SliderField label="Slider field" status="warning" statusText="Warning text" />
<SpinButtonField label="Spin button field" helperText="Help text" />
</>
```

These field versions of the components use a common set of Field hooks, and can be defined using very little code.

```ts
export type InputFieldProps = FieldProps<typeof Input>;

export const InputField: ForwardRefComponent<InputFieldProps> = React.forwardRef((props, ref) => {
const state = useField_unstable(props, ref, Input);
useFieldStyles_unstable(state);
return renderField_unstable(state);
});

InputField.displayName = 'InputField';
```

## Components

The following field components will be defined. If more form components are added in the future, they should also include a Field version.

- `CheckboxField`
- This will not use the Field's `label`, and instead forward the `label` prop to the underlying `Checkbox`.
- `ComboboxField`
- `DropdownField`
- `InputField`
- `RadioGroupField`
- `SelectField`
- `SliderField`
- `SpinnerField`
- `SwitchField`
- _Open question:_ how should this handle the `label`? Should it forward to the underlying `Switch`, or keep the label in the same place as the field's label? Might need a prop to control this behavior.
- `TextareaField`

## Variants

_Describe visual or functional variants of this control, if applicable. For example, a slider could have a 2D variant._
- **Orientation**: The `fieldOrientation` prop affects the layout of the label and field component:
- `'vertical'` (default) - label is above the field component
- `'horizontal'` - label is to the left of the field component, and is 33% the width of the field (this allows multiple stacked fields to all align their labels)
- **Status**: The `status` prop affects the icon and color used by the `statusText`:
- `'error'` - Red x icon, red text color
- `'warning'` - Yellow exclamation icon, neutral color text
- `'success'` - Green check icon, neutral color text
- `undefined` (default): No status icon, neutral color text
- **Error**: Some control types (like `Input` and `Combobox`) have a prop that makes the border red. This prop will be set `status="error"`.

Field also forwards some props from the wrapped component to the label as well:

- **Size**: If the wrapped component supports a `size` prop, it will also be applied to the field's label.
- **Required**: If set, the Label will get a required asterisk: `*`

## API

_List the **Props** and **Slots** proposed for the component. Ideally this would just be a link to the component's `.types.ts` file_
### FieldComponent

The `FieldComponent` type defines the minimum set of props that the wrapped component must support. This is used for the generic types as the requirement for the type parameter: `FieldProps<T extends FieldComponent>`

```ts
/**
* The minimum requirement for a component used by Field.
*
* Note: the use of VoidFunctionComponent means that component is not *required* to have a children prop,
* but it is still allowed to have a children prop.
*/
export type FieldComponent = React.VoidFunctionComponent<
Pick<
React.HTMLAttributes<HTMLElement>,
'id' | 'className' | 'style' | 'aria-labelledby' | 'aria-describedby' | 'aria-invalid' | 'aria-errormessage'
>
>;
```

### Slots

_Note: TypeScript crashes if the `Slot` type is used with a template type parameter. The `SlotComponent` type is a simplified version of that type, which only supports `React.ComponentType`/`React.VoidFunctionComponent`._

```ts
export type FieldSlots<T extends FieldComponent> = {
root: NonNullable<Slot<'div'>>;

/**
* The underlying component wrapped by this field.
*
* This is the PRIMARY slot: all intrinsic HTML properties will be applied to this slot,
* except `className` and `style`, which remain on the root slot.
*/
fieldComponent: SlotComponent<T>;

/**
* The label associated with the field.
*/
label?: SlotComponent<typeof Label>;

/**
* A status or validation message. The appearance of the statusText depends on the value of the `status` prop.
*/
statusText?: Slot<'span'>;

/**
* The icon associated with the status. If the `status` prop is set, this will default to a corresponding icon.
*
* This will only be displayed if `statusText` is set.
*/
statusIcon?: Slot<'span'>;

/**
* Additional text below the field.
*/
helperText?: Slot<'span'>;
};
```

### Props

```ts
export type FieldProps<T extends FieldComponent> = ComponentProps<Partial<FieldSlots<T>>, 'fieldComponent'> & {
/**
* The orientation of the label relative to the field component.
* This only affects the label, and not the statusText or helperText (which always appear below the field component).
*
* @default vertical
*/
fieldOrientation?: 'vertical' | 'horizontal';

/**
* The status affects the color of the statusText, the statusIcon, and for some field components, an error status
* causes the border to become red.
*
* @default undefined
*/
status?: 'error' | 'warning' | 'success';
};
```

Field also reads some props from the underlying component. These are not part of `FieldProps` because they are not added to the components that don't support them. However, they are accepted by `useField`:

```ts
/**
* Props that are supported by Field, but not required to be supported by the component that implements field.
*/
export type OptionalFieldComponentProps = {
/**
* Whether the field label should be marked as required.
*/
required?: boolean;

/**
* Size of the field label.
*
* Number sizes will be ignored, but are allowed because the HTML <input> element has a `size` prop of type `number`.
*/
size?: 'small' | 'medium' | 'large' | number;
};
```

### State

```ts
export type FieldState<T extends FieldComponent> = ComponentState<Required<FieldSlots<T>>> &
Pick<FieldProps<T>, 'fieldOrientation' | 'status'>;
```

## Structure

- _**Public**_
- _**Internal**_
- _**DOM** - how the component will be rendered as HTML elements_
### Public API

```jsx
<InputField
label="This is the field label"
fieldOrientation="horizontal"
status="error"
statusText="This is status text"
helperText="This is helper text"
/>
```

(similar API for other Field components)

### Slot structure

```jsx
<slots.root>
<slots.label {...slotProps.label} />
<slots.fieldComponent {...slotProps.fieldComponent} />
<slots.statusText {...slotProps.statusText}>
<slots.statusIcon {...slotProps.statusIcon} />
{slotProps.statusText.children}
</slots.statusText>
<slots.helperText {...slotProps.helperText} />
</slots.root>
```

### DOM structure

```html
<div className="fui-Field">
<label className="fui-Field__label fui-Label">This is the field label</label>
<!-- wrapped field component goes here -->
<span className="fui-Field__statusText">
<span className="fui-Field__statusIcon"><svg>...</svg></span>
This is status text
</span>
<span className="fui-Field__helperText">This is helper text</span>
</div>
```

## Migration

_Describe what will need to be done to upgrade from the existing implementations:_
### Migration from v8

Migration from v8 will require picking between the normal and `Field` version of an input control, depending on whether the field-specific features are required: (`label`, `status="error"`, `statusText`, `helperText`)

See individual input components for more detailed migration guides.

| v8 Control | v9 Base control | v9 Field control | Notes |
| ------------- | --------------------- | ------------------------------- | -------------------------------------------------------------------------------------------- |
| `Checkbox` | `Checkbox` | `CheckboxField` | Only use `CheckboxField` if an error message is needed, or if required for layout in a form. |
| `ChoiceGroup` | `RadioGroup` | `RadioGroupField` | |
| `ComboBox` | `Combobox` | `ComboboxField` | `errorMessage="..."` is replaced by `status="error" statusText="..."` |
| `Dropdown` | `Dropdown` | `DropdownField` | `errorMessage="..."` is replaced by `status="error" statusText="..."` |
| `Slider` | `Slider` | `SliderField` | |
| `SpinButton` | `SpinButton` | `SpinButtonField` | |
| `TextField` | `Input` OR `Textarea` | `InputField` OR `TextareaField` | `errorMessage="..."` is replaced by `status="error" statusText="..."` |
| `Toggle` | `Switch` | `SwitchField` | |

### Migration from v0

Many components in v0 have `Form___` versions (such as `FormInput`). Those are replaced by the `___Field` equivalent. See the underlying component's migration guides for more detailed migration information.

- _Migration from v8_
- _Migration from v0_
Component mapping:

- `FormButton` => Not supported
- `FormCheckbox` => `CheckboxField` OR `SwitchField`
- `FormDatepicker` => _(Not yet implemented)_
- `FormDropdown` => `DropdownField`
- `FormField` => Not supported
- `FormFieldCustom` => Not supported
- `FormLabel` => The `label` prop of the field component
- `FormMessage` => Either the `statusText` or `helperText` prop of the field component
- `FormRadioGroup` => `RadioGroupField`
- `FormSlider` => `SliderField`
- `FormTextArea` => `TextareaField`

The following props are common to each of the `Form___` components:

- `label` => `label`
- `message` => either `statusText` or `helperText`
- `errorMessage` => `statusText` with `status="error"`

## Behaviors

_Explain how the component will behave in use, including:_
### Form validation

Field has no logic to perform input validation. It is expected that the validation will be done externally (possibly using a third party form validation library like Formik).

### Interaction

- _Component States_
- _Interaction_
- _Keyboard_
- _Cursor_
- _Touch_
- _Screen readers_
The Field itself is not interactive. The wrapped component has the same interactions as it does outside of a field.

## Accessibility

Base accessibility information is included in the design document. After the spec is filled and review, outcomes from it need to be communicated to design and incorporated in the design document.

- Decide whether to use **native element** or folow **ARIA** and provide reasons
- Identify the **[ARIA](https://www.w3.org/TR/wai-aria-practices-1.2/) pattern** and, if the component is listed there, follow its specification as possible.
- Identify accessibility **variants**, the `role` ([ARIA roles](https://www.w3.org/TR/wai-aria-1.1/#role_definitions)) of the component, its `slots` and `aria-*` props.
- Describe the **keyboard navigation**: Tab Oder and Arrow Key Navigation. Describe any other keyboard **shortcuts** used
- Specify texts for **state change announcements** - [ARIA live regions
](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) (number of available items in dropdown, error messages, confirmations, ...)
- Identify UI parts that appear on **hover or focus** and specify keyboard and screen reader interaction with them
- List cases when **focus** needs to be **trapped** in sections of the UI (for dialogs and popups or for hierarchical navigation)
- List cases when **focus** needs to be **moved programatically** (if parts of the UI are appearing/disappearing or other cases)
- **ARIA pattern**
- Field itself does not implement a defined ARIA pattern. It has no role applied to the root element.
- **Attributes**
- The following are applied on the wrapped component:
- `aria-labelledby={label.id}`, if the label is present.
- `aria-describedby` is set to one of:
- `aria-describedby={statusText.id}`, if statusText is present, and _only if_ `status !== 'error'`
- `aria-describedby={helperText.id}`, if helperText is present
- `aria-describedby={statusText.id + ' ' + helperText.id}`, if both conditions above apply
- `aria-errormessage={statusText.id}`, if statusText is present, and _only if_ `status === 'error'`
- `aria-invalid={true}`, _only if_ `status === 'error'`
- On the `label` slot:
- `htmlFor={fieldComponent.id}` - the wrapped component's `id` (an ID is generated if not supplied via props).
- **Live regions** (state change announcements)
- TBD: Need to determine if the status text should be an aria live region.
- **UI parts appearing on hover or focus**
- None.
- **Focus behavior**
- No special focus behavior: no focus trapping or programmatic focus moving.