# 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 /> ```