import cns from 'classnames' import React, { HTMLAttributes, ReactNode, useCallback, useMemo } from 'react' import Dropdown from '../Dropdown' import { concat, getText } from '../utils' import { useFocused, useRefs, useValue } from '../utils/hooks' import { CommonProps as InputCommonProps, useInputSize, useInputStyle } from '../Input/input' import { Value, OptionGroupStyle, useActive, FetchFn, useOptionItems, useSelectPrefix } from './common' import { SelectProvider, SelectCtxValue } from './context' import { StatusIcon, ClearIcon, OptionContent, OptionPlaceholder, useSearchInput } from './Selector' import Overlay, { OptionsOverlayProvider } from './Overlay' import { WithRef } from '../utils/type' export interface Props extends InputCommonProps { /** 是否支持搜索 */ searchable?: boolean /** 是否有清空功能 */ clearable?: boolean /** 自定义的数据获取逻辑 */ fetch?: FetchFn /** `OptionGroup` 的渲染样式 */ groupStyle?: OptionGroupStyle /** 内容(`Option` / `OptionGroup`) */ children?: ReactNode /** 根元素的 HTML 属性 */ rootHtmlProps?: WithRef, HTMLDivElement> } export default function Select({ value: valueFromProps, defaultValue = null, onChange, placeholder, searchable = false, clearable = false, fetch, groupStyle = 'default', children, disabled = false, size, validateStatus, className: classNameFromProps, rootHtmlProps, ...otherProps }: Props) { const { value, setValue } = useValue({ valueFromProps, defaultValue, onChange }) const { focused, focusedHandlers } = useFocused({}, disabled) const { active, setActive, selectorRef, overlayRef, toggleActive } = useActive(disabled) const mergedSelectorRef = useRefs(rootHtmlProps?.ref, selectorRef) // 是否处于搜索状态 const searchActive = searchable && active const { searchValue, setSearchValue, renderSearchInput, focusSearchInput } = useSearchInput(active) const { items, loading, getOption } = useOptionItems(active, searchValue, children, fetch) const selectClassName = useSelectPrefix() const className = cns( useInputStyle({ size, validateStatus, disabled, focused: focused || active }), selectClassName, active && concat(selectClassName, 'active'), searchActive && concat(selectClassName, 'search-active'), classNameFromProps, rootHtmlProps?.className ) const handleSelectorClick = useCallback((e: React.MouseEvent) => { if (disabled) return rootHtmlProps?.onClick?.(e) if (e.defaultPrevented) return if (searchActive) { focusSearchInput() return } toggleActive() }, [disabled, rootHtmlProps, toggleActive, searchActive, focusSearchInput]) const clearIconView = !disabled && clearable && ( ) const currentOption = value != null ? getOption(value) : undefined const currentOptionContent = ( value != null ? {currentOption != null ? currentOption.content : value} : {placeholder} ) const searchInput = searchable && renderSearchInput({ // 有选中项则使用选中项内容(`currentOption.content`)作为 placeholder, // 这里偷个懒直接使用 search input 的 attribute placeholder 来实现, // 所以将 `currentOption.content` 先转换为文本,对于复杂 option content 会效果欠佳; // 后边有时间可以将其改造为与上边 `` 类似的、单独 DOM 节点的方式, // 不过那需要额外逻辑去处理该“单独 DOM 节点”与 search input 的位置关系 placeholder: currentOption == null ? placeholder : getText(currentOption.content) }) const handleSelect = useCallback((v: T[]) => { const newValue = v.length > 0 ? v[0] : null setValue(newValue) if (newValue != null) setActive(false) }, [setValue, setActive]) const inputSize = useInputSize(size) const selectCtxValue: SelectCtxValue = useMemo(() => ({ selected: value != null ? [value] : [], onSelect: handleSelect, multiple: false, size: inputSize }), [value, handleSelect, inputSize]) const overlay = ( ) return ( {currentOptionContent} {searchInput} {clearIconView} ) }