Files
payloadcms/packages/ui/src/elements/ReactSelect/index.tsx
2024-05-29 14:01:13 -04:00

223 lines
6.0 KiB
TypeScript

'use client'
import type { KeyboardEventHandler } from 'react'
import { arrayMove } from '@dnd-kit/sortable'
import { getTranslation } from '@payloadcms/translations'
import React, { useEffect, useId } from 'react'
import Select from 'react-select'
import CreatableSelect from 'react-select/creatable'
import type { Option } from './types.js'
import type { Props as ReactSelectAdapterProps } from './types.js'
export type { Option } from './types.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { DraggableSortable } from '../DraggableSortable/index.js'
import { ShimmerEffect } from '../ShimmerEffect/index.js'
import { ClearIndicator } from './ClearIndicator/index.js'
import { Control } from './Control/index.js'
import { DropdownIndicator } from './DropdownIndicator/index.js'
import { MultiValue, generateMultiValueDraggableID } from './MultiValue/index.js'
import { MultiValueLabel } from './MultiValueLabel/index.js'
import { MultiValueRemove } from './MultiValueRemove/index.js'
import { SingleValue } from './SingleValue/index.js'
import { ValueContainer } from './ValueContainer/index.js'
import './index.scss'
const createOption = (label: string) => ({
label,
value: label,
})
const SelectAdapter: React.FC<ReactSelectAdapterProps> = (props) => {
const { i18n, t } = useTranslation()
const [inputValue, setInputValue] = React.useState('') // for creatable select
const uuid = useId()
const [hasMounted, setHasMounted] = React.useState(false)
useEffect(() => {
setHasMounted(true)
}, [])
const {
className,
components,
customProps,
disabled = false,
filterOption = undefined,
getOptionValue,
isClearable = true,
isCreatable,
isLoading,
isSearchable = true,
noOptionsMessage = () => t('general:noOptions'),
numberOnly = false,
onChange,
onMenuClose,
onMenuOpen,
options,
placeholder = t('general:selectValue'),
showError,
value,
} = props
const loadingMessage = () => t('general:loading') + '...'
const classes = [className, 'react-select', showError && 'react-select--error']
.filter(Boolean)
.join(' ')
if (!hasMounted) {
return <ShimmerEffect height="calc(var(--base) * 2 + 2px)" />
}
if (!isCreatable) {
return (
<Select
captureMenuScroll
customProps={customProps}
isLoading={isLoading}
placeholder={getTranslation(placeholder, i18n)}
{...props}
className={classes}
classNamePrefix="rs"
components={{
ClearIndicator,
Control,
DropdownIndicator,
MultiValue,
MultiValueLabel,
MultiValueRemove,
SingleValue,
ValueContainer,
...components,
}}
filterOption={filterOption}
getOptionValue={getOptionValue}
instanceId={uuid}
isClearable={isClearable}
isDisabled={disabled}
isSearchable={isSearchable}
loadingMessage={loadingMessage}
menuPlacement="auto"
noOptionsMessage={noOptionsMessage}
onChange={onChange}
onMenuClose={onMenuClose}
onMenuOpen={onMenuOpen}
options={options}
value={value}
/>
)
}
const handleKeyDown: KeyboardEventHandler = (event) => {
// eslint-disable-next-line no-restricted-globals
if (numberOnly === true) {
const acceptableKeys = [
'Tab',
'Escape',
'Backspace',
'Enter',
'ArrowRight',
'ArrowLeft',
'ArrowUp',
'ArrowDown',
]
const isNumber = !/\D/.test(event.key)
const isActionKey = acceptableKeys.includes(event.key)
if (!isNumber && !isActionKey) {
event.preventDefault()
return
}
}
if (!value || !inputValue || inputValue.trim() === '') return
if (filterOption && !filterOption(null, inputValue)) {
return
}
switch (event.key) {
case 'Enter':
case 'Tab':
onChange([...(value as Option[]), createOption(inputValue)])
setInputValue('')
event.preventDefault()
break
default:
break
}
}
return (
<CreatableSelect
captureMenuScroll
isLoading={isLoading}
placeholder={getTranslation(placeholder, i18n)}
{...props}
className={classes}
classNamePrefix="rs"
components={{
ClearIndicator,
Control,
DropdownIndicator,
MultiValue,
MultiValueLabel,
MultiValueRemove,
SingleValue,
ValueContainer,
...components,
}}
filterOption={filterOption}
inputValue={inputValue}
instanceId={uuid}
isClearable={isClearable}
isDisabled={disabled}
isSearchable={isSearchable}
loadingMessage={loadingMessage}
menuPlacement="auto"
noOptionsMessage={noOptionsMessage}
onChange={onChange}
onInputChange={(newValue) => setInputValue(newValue)}
onKeyDown={handleKeyDown}
onMenuClose={onMenuClose}
onMenuOpen={onMenuOpen}
options={options}
value={value}
/>
)
}
const SortableSelect: React.FC<ReactSelectAdapterProps> = (props) => {
const { getOptionValue, onChange, value } = props
let draggableIDs: string[] = []
if (value) {
draggableIDs = (Array.isArray(value) ? value : [value]).map((optionValue) => {
return generateMultiValueDraggableID(optionValue, getOptionValue)
})
}
return (
<DraggableSortable
className="react-select-container"
ids={draggableIDs}
onDragEnd={({ moveFromIndex, moveToIndex }) => {
let sorted = value
if (value && Array.isArray(value)) {
sorted = arrayMove(value, moveFromIndex, moveToIndex)
}
onChange(sorted)
}}
>
<SelectAdapter {...props} />
</DraggableSortable>
)
}
export const ReactSelect: React.FC<ReactSelectAdapterProps> = (props) => {
const { isMulti, isSortable } = props
if (isMulti && isSortable) {
return <SortableSelect {...props} />
}
return <SelectAdapter {...props} />
}