import React, { CSSProperties, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react' import { NormalTypes } from '../utils/prop-types' import useTheme from '../use-theme' import useCurrentState from '../utils/use-current-state' import { pickChildByProps } from '../utils/collections' import SelectIcon from './select-icon' import SelectDropdown from './select-dropdown' import SelectMultipleValue from './select-multiple-value' import Grid from '../grid' import { SelectContext, SelectConfig } from './select-context' import { getColors } from './styles' import Ellipsis from '../shared/ellipsis' import SelectInput from './select-input' import useScaleable, { withScaleable } from '../use-scaleable' export type SelectRef = { focus: () => void blur: () => void scrollTo?: (options?: ScrollToOptions) => void } export type SelectTypes = NormalTypes interface Props { disabled?: boolean type?: SelectTypes value?: string | string[] initialValue?: string | string[] placeholder?: React.ReactNode | string icon?: React.ComponentType onChange?: (value: string | string[]) => void pure?: boolean multiple?: boolean clearable?: boolean className?: string dropdownClassName?: string dropdownStyle?: CSSProperties disableMatchWidth?: boolean onDropdownVisibleChange?: (visible: boolean) => void getPopupContainer?: () => HTMLElement | null } const defaultProps = { disabled: false, type: 'default' as SelectTypes, icon: SelectIcon as React.ComponentType, pure: false, multiple: false, clearable: true, className: '', disableMatchWidth: false, onDropdownVisibleChange: () => {}, } type NativeAttrs = Omit, keyof Props> export type SelectProps = Props & NativeAttrs const SelectComponent = React.forwardRef>( ( { children, type, disabled, initialValue: init, value: customValue, icon: Icon, onChange, pure, multiple, clearable, placeholder, className, dropdownClassName, dropdownStyle, disableMatchWidth, getPopupContainer, onDropdownVisibleChange, ...props }: React.PropsWithChildren & typeof defaultProps, selectRef, ) => { const theme = useTheme() const { SCALES } = useScaleable() const ref = useRef(null) const inputRef = useRef(null) const dropdownRef = useRef(null) const [visible, setVisible] = useState(false) const [selectFocus, setSelectFocus] = useState(false) const [value, setValue, valueRef] = useCurrentState( () => { if (!multiple) return init if (Array.isArray(init)) return init return typeof init === 'undefined' ? [] : [init] }, ) const isEmpty = useMemo(() => { if (!Array.isArray(value)) return !value return value.length === 0 }, [value]) const { border, borderActive, iconBorder, placeholderColor } = useMemo( () => getColors(theme.palette, type), [theme.palette, type], ) const updateVisible = (next: boolean) => { onDropdownVisibleChange(next) setVisible(next) } const updateValue = (next: string) => { setValue(last => { if (!Array.isArray(last)) return next if (!last.includes(next)) return [...last, next] return last.filter(item => item !== next) }) onChange && onChange(valueRef.current as string | string[]) if (!multiple) { updateVisible(false) } } const initialValue: SelectConfig = useMemo( () => ({ value, visible, updateValue, updateVisible, ref, disableAll: disabled, }), [visible, disabled, ref, value, multiple], ) const clickHandler = (event: React.MouseEvent) => { event.stopPropagation() event.nativeEvent.stopImmediatePropagation() event.preventDefault() if (disabled) return updateVisible(!visible) event.preventDefault() } const mouseDownHandler = (event: React.MouseEvent) => { /* istanbul ignore next */ if (visible) { event.preventDefault() } } useEffect(() => { if (customValue === undefined) return setValue(customValue) }, [customValue]) useImperativeHandle( selectRef, () => ({ focus: () => inputRef.current?.focus(), blur: () => inputRef.current?.blur(), scrollTo: options => dropdownRef.current?.scrollTo(options), }), [inputRef, dropdownRef], ) const selectedChild = useMemo(() => { const [, optionChildren] = pickChildByProps(children, 'value', value) return React.Children.map(optionChildren, child => { if (!React.isValidElement(child)) return null const el = React.cloneElement(child, { preventAllEvents: true }) if (!multiple) return el return ( updateValue(child.props.value) : null}> {el} ) }) }, [value, children, multiple]) const onInputBlur = () => { updateVisible(false) setSelectFocus(false) } return (
setSelectFocus(true)} /> {isEmpty && ( {placeholder} )} {value && !multiple && {selectedChild}} {value && multiple && ( {selectedChild} )} {children} {!pure && (
)}
) }, ) SelectComponent.defaultProps = defaultProps SelectComponent.displayName = 'GeistSelect' const Select = withScaleable(SelectComponent) export default Select