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 } = props
const labelId = 'this-needs-to-be-a-unique-label-id-1'
const inputId = 'this-needs-to-be-a-unique-input-id-1'
return (
<Field
disabled={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.Input
disabled={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 prop renderContainer.

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 } = props
const labelId = 'this-needs-to-be-a-unique-label-id-1'
const inputId = 'this-needs-to-be-a-unique-input-id-1'
return (
<Field
renderContainer={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.Input
disabled={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 } = props
const [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 (
<Field
disabled={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.TextArea
ref={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) return
const { current: el } = ref
el.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 = 2
interface Props extends ComponentProps<typeof Field> {}
const TagField: React.FC<Props> = (props) => {
const { disabled, placeholder, ...rest } = props
const 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 unselectedOptions
return unselectedOptions.filter(option =>
option.label.toLowerCase().includes(filterTerm)
)
}, [filterTerm, unselectedOptions])
return (
<>
<Field
label={
<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 null
return (
<Pill
key={`selected-item-${index}`}
onRequestRemove={e => handleRemoveSelected(e, selectedItem)}
{...getSelectedItemProps({ selectedItem, index })}
>
{option.label}
</Pill>
)
})}
<CustomInput
disabled={disabled}
placeholder={placeholder}
id={inputId}
onChange={handleFilterTermChange}
value={filterTerm}
{...getDropdownProps()}
/>
</Pills>
</Field>
<div
style={{
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 } = props
const 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 } = props
const styles = { margin: `calc(${GUTTER_SIZE}px / 2)` }
return (
<div ref={ref} {...rest} style={styles}>
<Tag
icon={<CloseIcon onClick={onRequestRemove} />}
isPressed
size={Tag.sizes.small}
>
{children}
</Tag>
</div>
)
})
const CustomInput = forwardRef<HTMLInputElement, React.ComponentProps<typeof Field.Input>>((props, ref) => {
const Container = useMemo(
() =>
forwardRef((p, r) => (
<div
ref={r}
{...p}
style={{ margin: `calc(${GUTTER_SIZE}px / 2)` }}
/>
)),
[]
)
return (
<Field.Input
ref={ref}
renderContainer={Container}
type="text"
{...props}
style={{ minWidth: 50 }}
/>
)
})
const Example = () => <TagField />
export default Example

Accessibility

WCAG 2.1 AA Compliance

100% axe-core tests
Manual audit

Props

Field

Name
Type
Description
Default
childrenReactNode
disabledbooleandisabled statefalse
errorbooleanerror statefalse
labelReactNode
prefixReactNode
renderContainer(props, ref) => ReactNoderender prop used to replace container with custom element(props, ref) => <div ref={ref} {...props} />
renderTag(props) => ReactNoderender prop used to replace field tag with custom element(props) => <div {...props} />
size
medium | small
size (from Field.sizes)
subLabelReactNode
suffixReactNode

Field.Label

Name
Type
Description
Default
childrenReactNode

Field.SubLabel

Name
Type
Description
Default
childrenReactNode

Field.Input

An unstyled input element that can be used inside the Field.

Name
Type
Description
Default
renderContainer(props, ref) => ReactNoderender prop used to replace container with custom element(props, ref) => <div ref={ref} {...props} />
renderTag(props, ref) => ReactNoderender 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) => ReactNoderender prop used to replace container with custom element(props, ref) => <div ref={ref} {...props} />