diff --git a/examples/custom-icon.tsx b/examples/custom-icon.tsx index 92d0ea850..e2389f6bd 100644 --- a/examples/custom-icon.tsx +++ b/examples/custom-icon.tsx @@ -1,4 +1,4 @@ -/* eslint-disable no-console */ +/* eslint-disable no-console, max-classes-per-file */ import React from 'react'; import Select, { Option } from '../src'; import '../assets/index.less'; @@ -22,7 +22,7 @@ const clearPath = ' 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h618c35.3 0 64-' + '28.7 64-64V306c0-35.3-28.7-64-64-64z'; -const menuItemSelectedIcon = props => { +const menuItemSelectedIcon = (props) => { const { ...p } = props; return {p.isSelected ? '🌹' : '☑️'}; }; @@ -33,7 +33,7 @@ const singleItemIcon = ( ); -const getSvg = path => ( +const getSvg = (path) => ( { + onKeyDown = (e) => { const { value } = this.state; if (e.keyCode === 13) { console.log('onEnter', value); @@ -158,7 +158,7 @@ class Test extends React.Component { console.log(args); }; - useAnim = e => { + useAnim = (e) => { this.setState({ useAnim: e.target.checked, }); diff --git a/src/Selector/index.tsx b/src/Selector/index.tsx index 9078b5555..d1fb1e2a3 100644 --- a/src/Selector/index.tsx +++ b/src/Selector/index.tsx @@ -11,11 +11,11 @@ import * as React from 'react'; import { useRef } from 'react'; import KeyCode from 'rc-util/lib/KeyCode'; -import { ScrollTo } from 'rc-virtual-list/lib/List'; +import type { ScrollTo } from 'rc-virtual-list/lib/List'; import MultipleSelector from './MultipleSelector'; import SingleSelector from './SingleSelector'; -import { LabelValueType, RawValueType, CustomTagProps } from '../interface/generator'; -import { RenderNode, Mode } from '../interface'; +import type { LabelValueType, RawValueType, CustomTagProps } from '../interface/generator'; +import type { RenderNode, Mode } from '../interface'; import useLock from '../hooks/useLock'; export interface InnerSelectorProps { @@ -129,7 +129,7 @@ const Selector: React.RefForwardingComponent = // ====================== Input ====================== const [getInputMouseDown, setInputMouseDown] = useLock(0); - const onInternalInputKeyDown: React.KeyboardEventHandler = event => { + const onInternalInputKeyDown: React.KeyboardEventHandler = (event) => { const { which } = event; if (which === KeyCode.UP || which === KeyCode.DOWN) { @@ -172,12 +172,16 @@ const Selector: React.RefForwardingComponent = compositionStatusRef.current = true; }; - const onInputCompositionEnd: React.CompositionEventHandler = e => { + const onInputCompositionEnd: React.CompositionEventHandler = (e) => { compositionStatusRef.current = false; - triggerOnSearch((e.target as HTMLInputElement).value); + + // Trigger search again to support `tokenSeparators` with typewriting + if (mode !== 'combobox') { + triggerOnSearch((e.target as HTMLInputElement).value); + } }; - const onInputChange: React.ChangeEventHandler = event => { + const onInputChange: React.ChangeEventHandler = (event) => { let { target: { value }, } = event; @@ -197,7 +201,7 @@ const Selector: React.RefForwardingComponent = triggerOnSearch(value); }; - const onInputPaste: React.ClipboardEventHandler = e => { + const onInputPaste: React.ClipboardEventHandler = (e) => { const { clipboardData } = e; const value = clipboardData.getData('text'); @@ -218,7 +222,7 @@ const Selector: React.RefForwardingComponent = } }; - const onMouseDown: React.MouseEventHandler = event => { + const onMouseDown: React.MouseEventHandler = (event) => { const inputMouseDown = getInputMouseDown(); if (event.target !== inputRef.current && !inputMouseDown) { event.preventDefault(); diff --git a/src/generate.tsx b/src/generate.tsx index b14e25cd2..22ee43ee2 100644 --- a/src/generate.tsx +++ b/src/generate.tsx @@ -12,11 +12,13 @@ import { useState, useRef, useEffect, useMemo } from 'react'; import KeyCode from 'rc-util/lib/KeyCode'; import classNames from 'classnames'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; -import { ScrollTo } from 'rc-virtual-list/lib/List'; -import Selector, { RefSelectorProps } from './Selector'; -import SelectTrigger, { RefTriggerProps } from './SelectTrigger'; -import { RenderNode, Mode, RenderDOMFunc, OnActiveValue } from './interface'; -import { +import type { ScrollTo } from 'rc-virtual-list/lib/List'; +import type { RefSelectorProps } from './Selector'; +import Selector from './Selector'; +import type { RefTriggerProps } from './SelectTrigger'; +import SelectTrigger from './SelectTrigger'; +import type { RenderNode, Mode, RenderDOMFunc, OnActiveValue } from './interface'; +import type { GetLabeledValue, FilterOptions, FilterFunc, @@ -29,11 +31,12 @@ import { FlattenOptionsType, SingleType, OnClear, - INTERNAL_PROPS_MARK, SelectSource, - CustomTagProps, + CustomTagProps} from './interface/generator'; +import { + INTERNAL_PROPS_MARK } from './interface/generator'; -import { OptionListProps, RefOptionListProps } from './OptionList'; +import type { OptionListProps, RefOptionListProps } from './OptionList'; import { toInnerValue, toOuterValues, removeLastEnabledValue, getUUID } from './utils/commonUtil'; import TransBtn from './TransBtn'; import useLock from './hooks/useLock'; @@ -59,7 +62,7 @@ const DEFAULT_OMIT_PROPS = [ export interface RefSelectProps { focus: () => void; blur: () => void; - scrollTo?: ScrollTo, + scrollTo?: ScrollTo; } export interface SelectProps extends React.AriaAttributes { @@ -194,7 +197,7 @@ export interface GenerateConfig { getLabeledValue: GetLabeledValue>; filterOptions: FilterOptions; findValueOption: // Need still support legacy ts api - | ((values: RawValueType[], options: FlattenOptionsType) => OptionsType) + | ((values: RawValueType[], options: FlattenOptionsType) => OptionsType) // New API add prevValueOptions support | (( values: RawValueType[], @@ -327,7 +330,7 @@ export default function generateSelector< const useInternalProps = internalProps.mark === INTERNAL_PROPS_MARK; const domProps = omitDOMProps ? omitDOMProps(restProps) : restProps; - DEFAULT_OMIT_PROPS.forEach(prop => { + DEFAULT_OMIT_PROPS.forEach((prop) => { delete domProps[prop]; }); @@ -337,7 +340,8 @@ export default function generateSelector< const listRef = useRef(null); const tokenWithEnter = useMemo( - () => (tokenSeparators || []).some(tokenSeparator => ['\n', '\r\n'].includes(tokenSeparator)), + () => + (tokenSeparators || []).some((tokenSeparator) => ['\n', '\r\n'].includes(tokenSeparator)), [tokenSeparators], ); @@ -446,7 +450,7 @@ export default function generateSelector< }); if ( mode === 'tags' && - filteredOptions.every(opt => opt[optionFilterProp] !== mergedSearchValue) + filteredOptions.every((opt) => opt[optionFilterProp] !== mergedSearchValue) ) { filteredOptions.unshift({ value: mergedSearchValue, @@ -682,7 +686,7 @@ export default function generateSelector< if (mode !== 'tags') { patchRawValues = patchLabels - .map(label => { + .map((label) => { const item = mergedFlattenOptions.find( ({ data }) => data[mergedOptionLabelProp] === label, ); @@ -695,7 +699,7 @@ export default function generateSelector< new Set([...mergedRawValue, ...patchRawValues]), ); triggerChange(newRawValues); - newRawValues.forEach(newRawValue => { + newRawValues.forEach((newRawValue) => { triggerSelect(newRawValue, true, 'input'); }); @@ -719,9 +723,11 @@ export default function generateSelector< // If menu is open, OptionList will take charge // If mode isn't tags, press enter is not meaningful when you can't see any option const onSearchSubmit = (searchText: string) => { - const newRawValues = Array.from(new Set([...mergedRawValue, searchText])); + const newRawValues = Array.from( + new Set([...mergedRawValue, searchText]), + ); triggerChange(newRawValues); - newRawValues.forEach(newRawValue => { + newRawValues.forEach((newRawValue) => { triggerSelect(newRawValue, true, 'input'); }); setInnerSearchValue(''); @@ -845,10 +851,10 @@ export default function generateSelector< } }; - const activeTimeoutIds: number[] = []; + const activeTimeoutIds: any[] = []; useEffect( () => () => { - activeTimeoutIds.forEach(timeoutId => clearTimeout(timeoutId)); + activeTimeoutIds.forEach((timeoutId) => clearTimeout(timeoutId)); activeTimeoutIds.splice(0, activeTimeoutIds.length); }, [], diff --git a/tests/Combobox.test.tsx b/tests/Combobox.test.tsx index cd688b5e5..58ff9f1cf 100644 --- a/tests/Combobox.test.tsx +++ b/tests/Combobox.test.tsx @@ -1,8 +1,11 @@ +/* eslint-disable max-classes-per-file */ + import { mount } from 'enzyme'; import KeyCode from 'rc-util/lib/KeyCode'; import React from 'react'; import { resetWarned } from 'rc-util/lib/warning'; -import Select, { Option, SelectProps } from '../src'; +import type { SelectProps } from '../src'; +import Select, { Option } from '../src'; import focusTest from './shared/focusTest'; import keyDownTest from './shared/keyDownTest'; import openControlledTest from './shared/openControlledTest'; @@ -11,7 +14,7 @@ import allowClearTest from './shared/allowClearTest'; import throwOptionValue from './shared/throwOptionValue'; async function delay(timeout = 0) { - return new Promise(resolve => { + return new Promise((resolve) => { setTimeout(resolve, timeout); }); } @@ -139,13 +142,16 @@ describe('Select.Combobox', () => { public handleChange = () => { setTimeout(() => { this.setState({ - data: [{ key: '1', label: '1' }, { key: '2', label: '2' }], + data: [ + { key: '1', label: '1' }, + { key: '2', label: '2' }, + ], }); }, 500); }; public render() { - const options = this.state.data.map(item => ( + const options = this.state.data.map((item) => ( )); return ( @@ -174,19 +180,25 @@ describe('Select.Combobox', () => { jest.useFakeTimers(); class AsyncCombobox extends React.Component { public state = { - data: [{ key: '1', label: '1' }, { key: '2', label: '2' }], + data: [ + { key: '1', label: '1' }, + { key: '2', label: '2' }, + ], }; public onSelect = () => { setTimeout(() => { this.setState({ - data: [{ key: '3', label: '3' }, { key: '4', label: '4' }], + data: [ + { key: '3', label: '3' }, + { key: '4', label: '4' }, + ], }); }, 500); }; public render() { - const options = this.state.data.map(item => ( + const options = this.state.data.map((item) => ( )); return ( @@ -243,10 +255,7 @@ describe('Select.Combobox', () => { , ); - wrapper - .find('.rc-select-item-option') - .first() - .simulate('mouseMove'); + wrapper.find('.rc-select-item-option').first().simulate('mouseMove'); expect(wrapper.find('input').props().value).toBeFalsy(); }); @@ -341,7 +350,7 @@ describe('Select.Combobox', () => { options: [], }; - public updateOptions = value => { + public updateOptions = (value) => { const options = [value, value + value, value + value + value]; this.setState({ options, @@ -351,7 +360,7 @@ describe('Select.Combobox', () => { public render() { return ( @@ -459,4 +468,21 @@ describe('Select.Combobox', () => { expect(wrapper.find('input').props().maxLength).toBe(6); }); }); + + it('typewriting should not trigger onChange multiple times', () => { + const onChange = jest.fn(); + const wrapper = mount(); + + wrapper.find('input').simulate('compositionStart', { target: { value: '' } }); + wrapper.find('input').simulate('change', { target: { value: 'a' } }); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenLastCalledWith('a', expect.anything()); + + wrapper.find('input').simulate('change', { target: { value: '啊' } }); + expect(onChange).toHaveBeenCalledTimes(2); + expect(onChange).toHaveBeenLastCalledWith('啊', expect.anything()); + + wrapper.find('input').simulate('compositionEnd', { target: { value: '啊' } }); + expect(onChange).toHaveBeenCalledTimes(2); + }); }); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..499a039e8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "esnext", + "moduleResolution": "node", + "baseUrl": "./", + "jsx": "preserve", + "declaration": true, + "skipLibCheck": true, + "esModuleInterop": true, + "paths": { + "@/*": ["src/*"], + "@@/*": ["src/.umi/*"], + "rc-table": ["src/index.ts"] + } + } +} \ No newline at end of file