packages/react-components/react-field/library/docs/Spec.md
Field adds a label, validation text, and hint text to form input components. It can be added around any form components from this library, such as <Input> or <Combobox>. Its child can also be a render function, which allows it to be used with intrinsic <input> elements, or custom form controls.
Epic issue tracking implementation: https://github.com/microsoft/fluentui/issues/19627
Existing libraries take one of several approaches to Field. The basic problem that all are trying to solve is to (a) render a label and some descriptive text around a control, and (b) connect that text to the control via for/aria-labelledby/aria-describedby.
TextField, Dropdown, ChoiceGroup, etc.TextField, Slider, RadioGroup, etc.htmlFor and aria-describedby. Libraries using this approach include:
FormField, FormLabel, FormMessageInput and FormInput, for example.TextInput and TextInputField, for example.The Field implementation in this spec follows pattern (4). Field uses context for its child child to connect the field's label and message text. There are several reasons:
aria-labelledby and aria-describedby can be set correctly on the child via context.Input without pulling in unnecessary dependencies (like Label and the field styling).<>
<Field
label="This is the field label"
validationMessage="This is error text"
size="small"
orientation="horizontal"
required
>
<Input contentBefore="$" contentAfter=".00" />
</Field>
<Field label="Radio group field">
<RadioGroup>
<Radio value="one" label="Option one" />
<Radio value="two" label="Option two" />
<Radio value="three" label="Option three" />
</RadioGroup>
</Field>
<Field label="Combobox field" validationState="success" validationMessage="Success text">
<Combobox>
<Option value="one">Option one</Option>
<Option value="two">Option two</Option>
<Option value="three">Option three</Option>
</Combobox>
</Field>
<Field label="Slider field" validationState="warning" validationMessage="Warning text">
<Slider defaultValue={25} />
</Field>
<Field label="Spin button field" hint="Hint text">
<SpinButton />
</Field>
</>
orientation 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)validationState prop affects the icon and color used by the validationMessage:
'error' - (default when there is a validationMessage): Red x icon, red text color
aria-invalid on the child element. Some controls such as Input and Combobox draw a red border when aria-invalid is set.'warning' - Yellow exclamation icon, neutral color text'success' - Green check icon, neutral color text'none' - No validation message icon, neutral color textField also forwards some props to its Label:
*, and the component will set either required (if supported), or aria-required.export type FieldSlots = {
root: NonNullable<Slot<'div'>>;
/**
* The label associated with the field.
*/
label?: Slot<typeof Label>;
/**
* A message about the validation state. By default, this is an error message, but it can be a success, warning,
* or custom message by setting `validationState`.
*/
validationMessage?: Slot<'div'>;
/**
* The icon associated with the `validationMessage`. This will only be displayed if `validationMessage` is set.
*
* The default depends on `validationState`:
* * `error` - `<ErrorCircle12Filled />`
* * `warning` - `<Warning12Filled />`
* * `success` - `<CheckmarkCircle12Filled />`
* * `none` - `null`
*/
validationMessageIcon?: Slot<'span'>;
/**
* Additional hint text below the field.
*/
hint?: Slot<'div'>;
};
export type FieldProps = Omit<ComponentProps<FieldSlots>, 'children'> & {
/**
* The Field's child can be a single form control, or a render function that takes the props that should be spread on
* a form control.
*
* All form controls in this library can be used directly as children (such as `<Input>` or `<RadioGroup>`), as well
* as intrinsic form controls like `<input>` or `<textarea>`. Custom controls can also be used as long as they
* accept FieldControlProps and spread them on the appropriate element.
*
* For more complex scenarios, a render function can be used to pass the FieldControlProps to the appropriate control.
*/
children?: React.ReactNode | ((props: FieldControlProps) => React.ReactNode);
/**
* The orientation of the label relative to the field component.
* This only affects the label, and not the validationMessage or hint (which always appear below the field component).
*
* @default vertical
*/
orientation?: 'vertical' | 'horizontal';
/**
* The `validationState` affects the display of the `validationMessage` and `validationMessageIcon`.
*
* * `error` - (default) The validation message has a red error icon and red text, with `role="alert"` so it is
* announced by screen readers. Additionally, the control inside the field has `aria-invalid` set, which adds a
* red border to some field components (such as `Input`).
* * `success` - The validation message has a green checkmark icon and gray text.
* * `warning` - The validation message has a yellow exclamation icon and gray text.
* * `none` - The validation message has no icon and gray text.
*
* @default error when `validationMessage` is set; none otherwise.
*/
validationState?: 'error' | 'warning' | 'success' | 'none';
/**
* Marks the Field as required. If `true`, an asterisk will be appended to the label, and `aria-required` will be set
* on the Field's child.
*/
required?: boolean;
/**
* The size of the Field's label.
*
* @default medium
*/
size?: 'small' | 'medium' | 'large';
};
export type FieldState = ComponentState<Required<FieldSlots>> &
Required<Pick<FieldProps, 'orientation' | 'required' | 'size' | 'validationState'>> &
Pick<FieldProps, 'children'> & {
/**
* The ID generated for the control inside the field, and the default value of label.htmlFor prop.
*/
generatedControlId: string;
};
The FieldContext provides some of the props passed to the Field, as well the IDs that were generated for the control, field, validationMessage, and hint. This can be used by a control inside the Field to set its accessibility properties, or use the useFieldControlProps hook (below) that handles the prop merging.
export type FieldContextValue = Readonly<
Pick<FieldState, 'generatedControlId' | 'orientation' | 'required' | 'size' | 'validationState'> & {
/** The label's for prop. Undefined if there is no label. */
labelFor?: string;
/** The label's id prop. Undefined if there is no label. */
labelId?: string;
/** The validationMessage's id prop. Undefined if there is no validationMessage. */
validationMessageId?: string;
/** The hint's id prop. Undefined if there is no hint. */
hintId?: string;
}
>;
The FieldControlProps type defines the props that may be set by useFieldControlProps, or passed to the child render function.
/**
* The props added to the control inside the Field.
*/
export type FieldControlProps = Pick<
React.HTMLAttributes<HTMLElement>,
'id' | 'aria-labelledby' | 'aria-describedby' | 'aria-invalid' | 'aria-required'
>;
The useFieldControlProps hook reads the FieldContext, and merges the control's props with props from the Field.
This is the mechanism that all form components in this library, including <Input>, <RadioGroup>, etc. get props from the Field. It is also one of two ways a third party component could be used inside a Field (the other being a render function as the child of Field).
/**
* Gets the control props from the field context, if this inside a `<Field>`.
*
* If `props` is provided: copies and merges the FieldControlProps with the given props, if this inside a `<Field>`.
* Otherwise, returns the FieldControlProps that should be applied to the control.
*
* It is preferred to pass a `props` object if available, to improve the resulting merged props.
*
* @param props - The existing props for the control. These will be merged with the control props from the field context.
* @param options - Option to include the size prop.
* @returns Merged props if inside a `<Field>`, otherwise the original props, or undefined if no props given.
*/
export function useFieldControlProps_unstable<Props extends FieldControlProps>(
props?: Props,
options?: FieldControlPropsOptions,
): Props | undefined;
/**
* Options for `useFieldControlProps_unstable`.
*/
export type FieldControlPropsOptions = {
/**
* Skips setting `aria-labelledby` on the control if the `label.htmlFor` refers to the control.
*
* This should be used with controls that can be the target of a label's `for` prop:
* `<button>`, `<input>`, `<progress>`, `<select>`, `<textarea>`.
*/
supportsLabelFor?: boolean;
/**
* Sets `required` instead of `aria-required` when the Field is marked required.
*
* This should be used with controls that support the `required` prop:
* `<input>` (except `range` or `color`), `<select>`, `<textarea>`.
*/
supportsRequired?: boolean;
/**
* Sets the size prop on the control to match the Field's size: `'small' | 'medium' | 'large'`.
*
* This should be used with controls that have a custom size prop that matches the Field's size prop.
*/
supportsSize?: boolean;
};
With a child element:
<Field
label="This is the field label"
orientation="horizontal"
validationState="error"
validationMessage="This is a validation message"
hint="This is a hint message"
>
<Input />
</Field>
With a child render function:
<Field
label="This is the field label"
orientation="horizontal"
validationState="error"
validationMessage="This is a validation message"
hint="This is a hint message"
>
{fieldProps => (
<div>
<input {...fieldProps} />
</div>
)}
</Field>
<slots.root>
<slots.label {...slotProps.label} />
{slotProps.root.children}
<slots.validationMessage {...slotProps.validationMessage}>
<slots.validationMessageIcon {...slotProps.validationMessageIcon} />
{slotProps.validationMessage.children}
</slots.validationMessage>
<slots.hint {...slotProps.hint} />
</slots.root>
<div className="fui-Field">
<label className="fui-Field__label fui-Label">This is the field label</label>
<!-- child field component goes here -->
<span className="fui-Field__validationMessage">
<span className="fui-Field__validationMessageIcon"><svg>...</svg></span>
This is a validation message
</span>
<span className="fui-Field__hint">This is a hint message</span>
</div>
See Migration.md.
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).
The Field itself is not interactive. The wrapped component has the same interactions as it does outside of a field.
useFieldControlProps or passed to the child render function:
id={generatedChildID} - if the label is present, and the child doesn't have an id already.aria-labelledby={label.id} - if the label is present. ONLY added if the child is NOT a control that supports being the target of label.htmlFor.aria-describedby={validationMessage.id + ' ' + hint.id} - if the validationMessage and/or hint are present.error, sets ONE of:
invalid={true} - if the control supports the invalid proparia-invalid={true} - if the control does NOT support the invalid prop (or a render function is used).label slot:
htmlFor={generatedChildID}validationMessage slot:
role="alert" - unless validationState set to something other than error.role="alert" on the validationMessage when it is an error causes the message to be announced by screen readers when it appears.