Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions examples/custom-icon.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 <span style={{ position: 'absolute', right: 0 }}>{p.isSelected ? '🌹' : '☑️'}</span>;
};
Expand All @@ -33,7 +33,7 @@ const singleItemIcon = (
</span>
);

const getSvg = path => (
const getSvg = (path) => (
<i>
<svg
viewBox="0 0 1024 1024"
Expand All @@ -60,7 +60,7 @@ class CustomIconComponent extends React.Component {
});
};

onKeyDown = e => {
onKeyDown = (e) => {
const { value } = this.state;
if (e.keyCode === 13) {
console.log('onEnter', value);
Expand Down Expand Up @@ -158,7 +158,7 @@ class Test extends React.Component {
console.log(args);
};

useAnim = e => {
useAnim = (e) => {
this.setState({
useAnim: e.target.checked,
});
Expand Down
22 changes: 13 additions & 9 deletions src/Selector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -129,7 +129,7 @@ const Selector: React.RefForwardingComponent<RefSelectorProps, SelectorProps> =
// ====================== Input ======================
const [getInputMouseDown, setInputMouseDown] = useLock(0);

const onInternalInputKeyDown: React.KeyboardEventHandler<HTMLInputElement> = event => {
const onInternalInputKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
const { which } = event;

if (which === KeyCode.UP || which === KeyCode.DOWN) {
Expand Down Expand Up @@ -172,12 +172,16 @@ const Selector: React.RefForwardingComponent<RefSelectorProps, SelectorProps> =
compositionStatusRef.current = true;
};

const onInputCompositionEnd: React.CompositionEventHandler<HTMLInputElement> = e => {
const onInputCompositionEnd: React.CompositionEventHandler<HTMLInputElement> = (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<HTMLInputElement> = event => {
const onInputChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
let {
target: { value },
} = event;
Expand All @@ -197,7 +201,7 @@ const Selector: React.RefForwardingComponent<RefSelectorProps, SelectorProps> =
triggerOnSearch(value);
};

const onInputPaste: React.ClipboardEventHandler = e => {
const onInputPaste: React.ClipboardEventHandler = (e) => {
const { clipboardData } = e;
const value = clipboardData.getData('text');

Expand All @@ -218,7 +222,7 @@ const Selector: React.RefForwardingComponent<RefSelectorProps, SelectorProps> =
}
};

const onMouseDown: React.MouseEventHandler<HTMLElement> = event => {
const onMouseDown: React.MouseEventHandler<HTMLElement> = (event) => {
const inputMouseDown = getInputMouseDown();
if (event.target !== inputRef.current && !inputMouseDown) {
event.preventDefault();
Expand Down
44 changes: 25 additions & 19 deletions src/generate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
Expand All @@ -59,7 +62,7 @@ const DEFAULT_OMIT_PROPS = [
export interface RefSelectProps {
focus: () => void;
blur: () => void;
scrollTo?: ScrollTo,
scrollTo?: ScrollTo;
}

export interface SelectProps<OptionsType extends object[], ValueType> extends React.AriaAttributes {
Expand Down Expand Up @@ -194,7 +197,7 @@ export interface GenerateConfig<OptionsType extends object[]> {
getLabeledValue: GetLabeledValue<FlattenOptionsType<OptionsType>>;
filterOptions: FilterOptions<OptionsType>;
findValueOption: // Need still support legacy ts api
| ((values: RawValueType[], options: FlattenOptionsType<OptionsType>) => OptionsType)
| ((values: RawValueType[], options: FlattenOptionsType<OptionsType>) => OptionsType)
// New API add prevValueOptions support
| ((
values: RawValueType[],
Expand Down Expand Up @@ -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];
});

Expand All @@ -337,7 +340,8 @@ export default function generateSelector<
const listRef = useRef<RefOptionListProps>(null);

const tokenWithEnter = useMemo(
() => (tokenSeparators || []).some(tokenSeparator => ['\n', '\r\n'].includes(tokenSeparator)),
() =>
(tokenSeparators || []).some((tokenSeparator) => ['\n', '\r\n'].includes(tokenSeparator)),
[tokenSeparators],
);

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
);
Expand All @@ -695,7 +699,7 @@ export default function generateSelector<
new Set<RawValueType>([...mergedRawValue, ...patchRawValues]),
);
triggerChange(newRawValues);
newRawValues.forEach(newRawValue => {
newRawValues.forEach((newRawValue) => {
triggerSelect(newRawValue, true, 'input');
});

Expand All @@ -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<RawValueType>([...mergedRawValue, searchText]));
const newRawValues = Array.from(
new Set<RawValueType>([...mergedRawValue, searchText]),
);
triggerChange(newRawValues);
newRawValues.forEach(newRawValue => {
newRawValues.forEach((newRawValue) => {
triggerSelect(newRawValue, true, 'input');
});
setInnerSearchValue('');
Expand Down Expand Up @@ -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);
},
[],
Expand Down
52 changes: 39 additions & 13 deletions tests/Combobox.test.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
});
}
Expand Down Expand Up @@ -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) => (
<Option value={item.key}>{item.label}</Option>
));
return (
Expand Down Expand Up @@ -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) => (
<Option value={item.key}>{item.label}</Option>
));
return (
Expand Down Expand Up @@ -243,10 +255,7 @@ describe('Select.Combobox', () => {
</Select>,
);

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();
});
Expand Down Expand Up @@ -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,
Expand All @@ -351,7 +360,7 @@ describe('Select.Combobox', () => {
public render() {
return (
<Select mode="combobox" onChange={this.updateOptions}>
{this.state.options.map(opt => (
{this.state.options.map((opt) => (
<Option value={opt}>{opt}</Option>
))}
</Select>
Expand Down Expand Up @@ -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(<Select mode="combobox" onChange={onChange} />);

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);
});
});
16 changes: 16 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}
}
}