Field
The Field suite of components provide the basic building blocks for creating various form controls. These components are provided with the expectation they can be used to create consistent Design System forms.
- Install
npm install @pluralsight/ps-design-system-field
- Import
import Field from '@pluralsight/ps-design-system-field'
Examples
Text input field
An example of creating a basic input field component through composition.
import React, { ComponentProps } from 'react'import Field from '@pluralsight/ps-design-system-field'interface Props extends ComponentProps<typeof Field> {}const TextInputField: React.FC<Props> = (props) => {const { disabled, placeholder, type = 'text', ...rest } = propsconst labelId = 'this-needs-to-be-a-unique-label-id-1'const inputId = 'this-needs-to-be-a-unique-input-id-1'return (<Fielddisabled={disabled}label={<Field.Label htmlFor={inputId} id={labelId}>Text input label area</Field.Label>}subLabel={<Field.SubLabel>Area for additional information</Field.SubLabel>}{...rest}><Field.Inputdisabled={disabled}id={inputId}placeholder={placeholder}type={type}/></Field>)}const Example = () => <TextInputField />export default Example
Fields are designed to display inline but can be changed to block display. Doing so will allow the contents to stretch fully to the parent's width.
You can apply additional styles, like
display
, to the outermost container using the proprenderContainer
.
import React, { ComponentProps, forwardRef } from 'react'import Field from '@pluralsight/ps-design-system-field'const BlockRenderContainer = forwardRef((props, ref) => (<div ref={ref} {...props} style={{ display: 'block'}} />))const TextInputField: React.FC<ComponentProps<typeof Field>> = (props) => {const { disabled, placeholder, type = 'text', ...rest } = propsconst labelId = 'this-needs-to-be-a-unique-label-id-1'const inputId = 'this-needs-to-be-a-unique-input-id-1'return (<FieldrenderContainer={BlockRenderContainer}disabled={disabled}label={<Field.Label htmlFor={inputId} id={labelId}>My content stretchs fully to my parent's width</Field.Label>}subLabel={<Field.SubLabel>Area for additional information</Field.SubLabel>}{...rest}><Field.Inputdisabled={disabled}id={inputId}placeholder={placeholder}type={type}/></Field>)}const Example = () => <TextInputField />export default Example
Text area field
An example of creating a basic textarea field component that grows with the content.
import React, { ComponentProps, RefObject } from 'react'import Field from '@pluralsight/ps-design-system-field'interface Props extends ComponentProps<typeof Field> {}const TextAreaField: React.FC<Props> = (props) => {const { disabled, placeholder, ...rest } = propsconst [value, setValue] = useState<string>('')const labelId = 'this-needs-to-be-a-unique-label-id-2'const areaId = 'this-needs-to-be-a-unique-area-id-2'const areaRef = useRef<HTMLTextAreaElement>(null)useAutoGrow(areaRef, value)return (<Fielddisabled={disabled}label={<Field.Label htmlFor={areaId} id={labelId}>Text area label area</Field.Label>}subLabel={<Field.SubLabel>Area for additional information</Field.SubLabel>}{...rest}><Field.TextArearef={areaRef}disabled={disabled}id={areaId}onChange={evt => {setValue(evt.target.value)}}placeholder={placeholder}value={value}/></Field>)}function useAutoGrow(ref: RefObject<HTMLTextAreaElement | undefined>,value: string) {useEffect(() => {if (!ref.current) returnconst { current: el } = refel.style.height = 'inherit'const computed = window.getComputedStyle(el)const height =parseInt(computed.getPropertyValue('border-top-width'), 10) +parseInt(computed.getPropertyValue('padding-top'), 10) +el.scrollHeight +parseInt(computed.getPropertyValue('padding-bottom'), 10) +parseInt(computed.getPropertyValue('border-bottom-width'), 10)el.style.height = String(height) + 'px'}, [ref, value])}const Example = () => <TextAreaField />export default Example
Advanced examples integrating downshift
Here are some examples of building more complicated fields using the downshift headless library.
Tags Field
import React, { useMemo } from 'react'import { layout } from '@pluralsight/ps-design-system-core'import { CloseIcon } from '@pluralsight/ps-design-system-icon'import Tag from '@pluralsight/ps-design-system-tag'import Field from '@pluralsight/ps-design-system-field'import { useMultipleSelection } from 'downshift'const GUTTER_SIZE = 2interface Props extends ComponentProps<typeof Field> {}const TagField: React.FC<Props> = (props) => {const { disabled, placeholder, ...rest } = propsconst options = useMemo(() => [{ label: 'Hydrogen', value: 'H' },{ label: 'Helium', value: 'He' },{ label: 'Lithium', value: 'Li' },{ label: 'Beryllium', value: 'Be' },{ label: 'Boron', value: 'B' },{ label: 'Carbon', value: 'C' },{ label: 'Nitrogren', value: 'N' },{ label: 'Oxygen', value: 'O' },{ label: 'Fluorine', value: 'F' },], [])const labelId = 'this-needs-to-be-a-unique-label-id-3'const inputId = 'this-needs-to-be-a-unique-input-id-3'const [filterTerm, setFilterTerm] = useState('')const handleFilterTermChange = evt => {setFilterTerm(evt.target.value)}const initialSelectedItems = useMemo(() => [options[1].value], [options])const {addSelectedItem,getDropdownProps,getSelectedItemProps,removeSelectedItem,selectedItems} = useMultipleSelection({ initialSelectedItems })const handleAddSelected = (evt, item) => {evt.stopPropagation()setFilterTerm('')addSelectedItem(item)}const handleRemoveSelected = (evt, item) => {evt.stopPropagation()removeSelectedItem(item)}const unselectedOptions = useMemo(() => {return options.filter(option => !selectedItems.includes(option.value))}, [options, selectedItems])const filterResults = useMemo(() => {if (!filterTerm) return unselectedOptionsreturn unselectedOptions.filter(option =>option.label.toLowerCase().includes(filterTerm))}, [filterTerm, unselectedOptions])return (<><Fieldlabel={<Field.Label htmlFor={inputId} id={labelId}>Some label text</Field.Label>}renderTag={RenderTagNoPadding}size={Field.sizes.small}{...rest}><Pills>{selectedItems.map((selectedItem, index) => {const option = options.find(o => o.value === selectedItem)if (!option) return nullreturn (<Pillkey={`selected-item-${index}`}onRequestRemove={e => handleRemoveSelected(e, selectedItem)}{...getSelectedItemProps({ selectedItem, index })}>{option.label}</Pill>)})}<CustomInputdisabled={disabled}placeholder={placeholder}id={inputId}onChange={handleFilterTermChange}value={filterTerm}{...getDropdownProps()}/></Pills></Field><divstyle={{border: '2px dashed pink',margin: '20px 0',maxHeight: 200,overflow: 'scroll',padding: 20}}><p>Filtered Options</p><ul>{filterResults.map((option, index) => (<li key={`filter-result-${index}`}><span>{option.label} </span><button onClick={e => handleAddSelected(e, option.value)}>add</button></li>))}</ul></div></>)}const RenderTagNoPadding: React.FC = p => (<div {...p} style={{ padding: 0 }} />)const Pills = forwardRef((props, ref) => {const { children, ...rest } = propsconst styles = {alignItems: 'center',display: 'flex',flex: 1,flexWrap: 'wrap',maxHeight: 75,overflowY: 'scroll',padding: `${layout.spacingXSmall}`,width: '100%'}return (<div ref={ref} {...rest} style={styles}>{children}</div>)})interface PillProps extends ComponentProps<typeof Tag> {onRequestRemove: React.MouseEventHandler}const Pill = forwardRef<HTMLDivElement, PillProps>((props, ref) => {const { children, onRequestRemove, ...rest } = propsconst styles = { margin: `calc(${GUTTER_SIZE}px / 2)` }return (<div ref={ref} {...rest} style={styles}><Tagicon={<CloseIcon onClick={onRequestRemove} />}isPressedsize={Tag.sizes.small}>{children}</Tag></div>)})const CustomInput = forwardRef<HTMLInputElement, React.ComponentProps<typeof Field.Input>>((props, ref) => {const Container = useMemo(() =>forwardRef((p, r) => (<divref={r}{...p}style={{ margin: `calc(${GUTTER_SIZE}px / 2)` }}/>)),[])return (<Field.Inputref={ref}renderContainer={Container}type="text"{...props}style={{ minWidth: 50 }}/>)})const Example = () => <TagField />export default Example
Accessibility
WCAG 2.1 AA Compliance
100% axe-core testsManual audit
Props
Field
Name | Type | Description | Default |
---|---|---|---|
children | ReactNode |
| |
disabled | boolean | disabled state | false |
error | boolean | error state | false |
label | ReactNode |
| |
prefix | ReactNode |
| |
renderContainer | (props, ref) => ReactNode | render prop used to replace container with custom element | (props, ref) => <div ref={ref} {...props} /> |
renderTag | (props) => ReactNode | render prop used to replace field tag with custom element | (props) => <div {...props} /> |
size |
| size (from Field.sizes) |
|
subLabel | ReactNode |
| |
suffix | ReactNode |
|
Field.Label
Name | Type | Description | Default |
---|---|---|---|
children | ReactNode |
|
Field.SubLabel
Name | Type | Description | Default |
---|---|---|---|
children | ReactNode |
|
Field.Input
An unstyled input
element that can be used inside the Field.
Name | Type | Description | Default |
---|---|---|---|
renderContainer | (props, ref) => ReactNode | render prop used to replace container with custom element | (props, ref) => <div ref={ref} {...props} /> |
renderTag | (props, ref) => ReactNode | render prop used to replace the default input | (props, ref) => <input ref={ref} {...props} /> |
Field.TextArea
An unstyled textarea
element that can be used inside the Field.
Name | Type | Description | Default |
---|---|---|---|
renderContainer | (props, ref) => ReactNode | render prop used to replace container with custom element | (props, ref) => <div ref={ref} {...props} /> |