diff --git a/components/auto-complete/__tests__/__snapshots__/index.test.tsx.snap b/components/auto-complete/__tests__/__snapshots__/index.test.tsx.snap index eaf1c54..b07b916 100644 --- a/components/auto-complete/__tests__/__snapshots__/index.test.tsx.snap +++ b/components/auto-complete/__tests__/__snapshots__/index.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AutoComplete should render correctly 1`] = ` - { expect(wrapper.prop('width')).toEqual('200px') }) + + it('should forward ref by default', () => { + const ref = React.createRef() + const wrapper = mount() + expect(ref.current).not.toBeNull() + expect(() => wrapper.unmount()).not.toThrow() + }) }) diff --git a/components/auto-complete/auto-complete.tsx b/components/auto-complete/auto-complete.tsx index 16611b4..0214169 100644 --- a/components/auto-complete/auto-complete.tsx +++ b/components/auto-complete/auto-complete.tsx @@ -1,4 +1,13 @@ -import React, { CSSProperties, useEffect, useMemo, useRef, useState } from 'react' +import React, { + CSSProperties, + PropsWithoutRef, + RefAttributes, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react' import Input from '../input' import AutoCompleteItem, { AutoCompleteItemProps } from './auto-complete-item' import AutoCompleteDropdown from './auto-complete-dropdown' @@ -71,160 +80,171 @@ const getSearchIcon = (searching?: boolean) => { return searching ? : } -const AutoComplete: React.FC> = ({ - options, - initialValue: customInitialValue, - onSelect, - onSearch, - onChange, - searching, - children, - size, - status, - value, - width, - clearable, - disabled, - dropdownClassName, - dropdownStyle, - disableMatchWidth, - disableFreeSolo, - ...props -}) => { - const ref = useRef(null) - const inputRef = useRef(null) - const resetTimer = useRef() - const [state, setState, stateRef] = useCurrentState(customInitialValue) - const [selectVal, setSelectVal] = useState(customInitialValue) - const [visible, setVisible] = useState(false) - - const [, searchChild] = pickChild(children, AutoCompleteSearching) - const [, emptyChild] = pickChild(children, AutoCompleteEmpty) - const autoCompleteItems = useMemo(() => { - const hasSearchChild = searchChild && React.Children.count(searchChild) > 0 - const hasEmptyChild = emptyChild && React.Children.count(emptyChild) > 0 - if (searching) { - return hasSearchChild ? ( - searchChild - ) : ( - Searching... - ) - } - if (options.length === 0) { - if (state === '') return null - return hasEmptyChild ? ( - emptyChild - ) : ( - No Options - ) - } - return childrenToOptionsNode(options as Array) - }, [searching, options]) - const showClearIcon = useMemo(() => clearable && searching === undefined, [ - clearable, - searching, - ]) - - const updateValue = (val: string) => { - if (disabled) return - setSelectVal(val) - onSelect && onSelect(val) - setState(val) - inputRef.current && inputRef.current.focus() - } - const updateVisible = (next: boolean) => setVisible(next) - const onInputChange = (event: React.ChangeEvent) => { - setVisible(true) - onSearch && onSearch(event.target.value) - setState(event.target.value) - } - const resetInputValue = () => { - if (!disableFreeSolo) return - if (!state || state === '') return - if (state !== selectVal) { - setState(selectVal) - } - } - - useEffect(() => { - onChange && onChange(state) - }, [state]) - useEffect(() => { - if (value === undefined) return - setState(value) - }, [value]) - - const initialValue = useMemo( - () => ({ - ref, +const AutoComplete = React.forwardRef< + HTMLInputElement, + React.PropsWithChildren +>( + ( + { + options, + initialValue: customInitialValue, + onSelect, + onSearch, + onChange, + searching, + children, size, - value: state, - updateValue, - visible, - updateVisible, - }), - [state, visible, size], - ) + status, + value, + width, + clearable, + disabled, + dropdownClassName, + dropdownStyle, + disableMatchWidth, + disableFreeSolo, + ...props + }, + userRef: React.Ref, + ) => { + const ref = useRef(null) + const inputRef = useRef(null) + const resetTimer = useRef() + const [state, setState, stateRef] = useCurrentState(customInitialValue) + const [selectVal, setSelectVal] = useState(customInitialValue) + const [visible, setVisible] = useState(false) + useImperativeHandle(userRef, () => inputRef.current) - const toggleFocusHandler = (next: boolean) => { - clearTimeout(resetTimer.current) - setVisible(next) - if (next) { - onSearch && onSearch(stateRef.current) - } else { - resetTimer.current = window.setTimeout(() => { - resetInputValue() - clearTimeout(resetTimer.current) - }, 100) + const [, searchChild] = pickChild(children, AutoCompleteSearching) + const [, emptyChild] = pickChild(children, AutoCompleteEmpty) + const autoCompleteItems = useMemo(() => { + const hasSearchChild = searchChild && React.Children.count(searchChild) > 0 + const hasEmptyChild = emptyChild && React.Children.count(emptyChild) > 0 + if (searching) { + return hasSearchChild ? ( + searchChild + ) : ( + Searching... + ) + } + if (options.length === 0) { + if (state === '') return null + return hasEmptyChild ? ( + emptyChild + ) : ( + No Options + ) + } + return childrenToOptionsNode(options as Array) + }, [searching, options]) + const showClearIcon = useMemo(() => clearable && searching === undefined, [ + clearable, + searching, + ]) + + const updateValue = (val: string) => { + if (disabled) return + setSelectVal(val) + onSelect && onSelect(val) + setState(val) + inputRef.current && inputRef.current.focus() + } + const updateVisible = (next: boolean) => setVisible(next) + const onInputChange = (event: React.ChangeEvent) => { + setVisible(true) + onSearch && onSearch(event.target.value) + setState(event.target.value) + } + const resetInputValue = () => { + if (!disableFreeSolo) return + if (!state || state === '') return + if (state !== selectVal) { + setState(selectVal) + } } - } - const inputProps = { - ...props, - width, - disabled, - value: state, - } + useEffect(() => { + onChange && onChange(state) + }, [state]) + useEffect(() => { + if (value === undefined) return + setState(value) + }, [value]) - return ( - -
- toggleFocusHandler(true)} - onBlur={() => toggleFocusHandler(false)} - clearable={showClearIcon} - iconRight={getSearchIcon(searching)} - {...inputProps} - /> - - {autoCompleteItems} - + const initialValue = useMemo( + () => ({ + ref, + size, + value: state, + updateValue, + visible, + updateVisible, + }), + [state, visible, size], + ) - -
-
- ) -} + const inputProps = { + ...props, + width, + disabled, + value: state, + } -type AutoCompleteComponent

= React.FC

& { + return ( + +

+ toggleFocusHandler(true)} + onBlur={() => toggleFocusHandler(false)} + clearable={showClearIcon} + iconRight={getSearchIcon(searching)} + {...inputProps} + /> + + {autoCompleteItems} + + + +
+ + ) + }, +) + +type AutoCompleteComponent = React.ForwardRefExoticComponent< + PropsWithoutRef

& RefAttributes +> & { Item: typeof AutoCompleteItem Option: typeof AutoCompleteItem Searching: typeof AutoCompleteSearching @@ -234,6 +254,6 @@ type AutoCompleteComponent

= React.FC

& { type ComponentProps = Partial & Omit & NativeAttrs -;(AutoComplete as AutoCompleteComponent).defaultProps = defaultProps +AutoComplete.defaultProps = defaultProps -export default AutoComplete as AutoCompleteComponent +export default AutoComplete as AutoCompleteComponent diff --git a/pages/en-us/components/auto-complete.mdx b/pages/en-us/components/auto-complete.mdx index 5819a3e..a971b24 100644 --- a/pages/en-us/components/auto-complete.mdx +++ b/pages/en-us/components/auto-complete.mdx @@ -1,5 +1,5 @@ import { Layout, Playground, Attributes } from 'lib/components' -import { AutoComplete, Spacer, Badge, Grid, Text } from 'components' +import { AutoComplete, Spacer, Badge, Grid, Text, Code } from 'components' import { useState, useRef, useEffect } from 'react' export const meta = { @@ -286,6 +286,7 @@ AutoComplete control of input field. | **dropdownStyle** | style of dropdown box | `object` | - | - | | **disableMatchWidth** | disable Option from follow parent width | `boolean` | - | `false` | | **disableFreeSolo** | only values can be changed through Select | `boolean` | - | `false` | +| **ref** | forwardRef | Ref | - | - | | ... | native props | `InputHTMLAttributes` | `'id', 'className', ...` | - | AutoComplete.Item diff --git a/pages/zh-cn/components/auto-complete.mdx b/pages/zh-cn/components/auto-complete.mdx index e0704a8..c48c30c 100644 --- a/pages/zh-cn/components/auto-complete.mdx +++ b/pages/zh-cn/components/auto-complete.mdx @@ -1,5 +1,5 @@ import { Layout, Playground, Attributes } from 'lib/components' -import { AutoComplete, Spacer, Badge, Text, Grid } from 'components' +import { AutoComplete, Spacer, Badge, Text, Grid, Code } from 'components' import { useState, useRef, useEffect } from 'react' export const meta = { @@ -287,6 +287,7 @@ export const meta = { | **dropdownStyle** | 自定义下拉框的样式 | `object` | - | - | | **disableMatchWidth** | 禁止 Option 跟随父元素的宽度 | `boolean` | - | `false` | | **disableFreeSolo** | 只允许通过 Select 事件更改值 (禁止 Input 输入随意值) | `boolean` | - | `false` | +| **ref** | 转发的原生输入框 Ref | Ref | - | - | | ... | 原生属性 | `InputHTMLAttributes` | `'id', 'className', ...` | - | AutoComplete.Item