close
Skip to content
Open
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
3 changes: 1 addition & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
module.exports = {
setupFiles: ['./tests/setup.js'],
snapshotSerializers: [require.resolve('enzyme-to-json/serializer')],
};
};
20 changes: 8 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,31 +43,27 @@
"*": "prettier --write --ignore-unknown"
},
"dependencies": {
"@rc-component/select": "~1.7.0",
"@rc-component/tree": "~1.3.0",
"@rc-component/util": "^1.4.0",
"@rc-component/select": "~1.7.1",
"@rc-component/tree": "~1.3.2",
"@rc-component/util": "^1.11.1",
"clsx": "^2.1.1"
},
"devDependencies": {
"@rc-component/dialog": "^1.2.0",
"@rc-component/father-plugin": "^2.0.2",
"@rc-component/father-plugin": "^2.2.0",
"@rc-component/form": "^1.4.0",
"@rc-component/np": "^1.0.3",
"@rc-component/trigger": "^3.0.0",
"@rc-component/virtual-list": "^1.0.1",
"@testing-library/react": "^12.1.5",
"@testing-library/react": "^16.0.0",
"@types/jest": "^29.5.13",
"@types/node": "^22.7.5",
"@types/react": "^18.3.11",
"@types/react-dom": "^19.0.1",
"@types/react-dom": "^18.0.0",
"@types/warning": "^3.0.3",
"@umijs/fabric": "^4.0.1",
"cheerio": "1.0.0-rc.12",
"cross-env": "^7.0.3",
"dumi": "^2.4.12",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.8",
"enzyme-to-json": "^3.6.2",
"eslint": "^8.57.1",
"eslint-plugin-jest": "^28.10.0",
"eslint-plugin-unicorn": "^56.0.0",
Expand All @@ -77,8 +73,8 @@
"lint-staged": "^15.2.10",
"prettier": "^3.3.3",
"rc-test": "^7.1.1",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"typescript": "^5.6.3"
},
"peerDependencies": {
Expand Down
6 changes: 3 additions & 3 deletions src/LegacyContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import type { DataEntity, IconType } from '@rc-component/tree/lib/interface';
import type { LegacyDataNode, SafeKey, Key } from './interface';
import type { LegacyDataNode, SafeKey, Key, IconType } from './interface';
import type { DataEntity } from './hooks/useDataEntities';

interface LegacyContextProps {
checkable: boolean | React.ReactNode;
Expand All @@ -20,7 +20,7 @@ interface LegacyContextProps {
loadData: (treeNode: LegacyDataNode) => Promise<unknown>;
onTreeLoad: (loadedKeys: Key[]) => void;

keyEntities: Record<SafeKey, DataEntity<any>>;
keyEntities: Record<SafeKey, DataEntity>;
}

const LegacySelectContext = React.createContext<LegacyContextProps>(null);
Expand Down
15 changes: 10 additions & 5 deletions src/OptionList.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { useBaseProps } from '@rc-component/select';
import type { RefOptionListProps } from '@rc-component/select/lib/OptionList';
import type { TreeProps } from '@rc-component/tree';
import Tree from '@rc-component/tree';
import { UnstableContext } from '@rc-component/tree';
import type { EventDataNode, ScrollTo } from '@rc-component/tree/lib/interface';
import KeyCode from '@rc-component/util/lib/KeyCode';
import useMemo from '@rc-component/util/lib/hooks/useMemo';
import type { EventDataNode } from '@rc-component/tree';
import { KeyCode, useEvent, useMemo } from '@rc-component/util';
import * as React from 'react';
import LegacyContext from './LegacyContext';
import TreeSelectContext from './TreeSelectContext';
import type { DataNode, Key, SafeKey } from './interface';
import { getAllKeys, isCheckDisabled } from './utils/valueUtil';
import { useEvent } from '@rc-component/util';

const HIDDEN_STYLE = {
width: 0,
Expand All @@ -30,6 +27,14 @@ interface TreeEventInfo {
checked?: boolean;
}

interface RefOptionListProps {
onKeyDown: React.KeyboardEventHandler;
onKeyUp: React.KeyboardEventHandler;
scrollTo?: (args: unknown) => void;
}

type ScrollTo = NonNullable<React.ComponentRef<typeof Tree>['scrollTo']>;

type ReviseRefOptionListProps = Omit<RefOptionListProps, 'scrollTo'> & { scrollTo: ScrollTo };
Comment on lines +30 to 38
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

目前的类型定义中,ReviseRefOptionListProps 通过交叉类型将 scrollTo 设为了必填项,且使用了 NonNullable。但在组件实现中(第 278 行),scrollTo 的值来自于 treeRef.current?.scrollTo,在树未渲染(如数据为空)时可能为 undefined。这会导致类型定义与实际行为不符。建议直接在接口中定义可选的 scrollTo 类型,简化定义并保持与实现一致。

Suggested change
interface RefOptionListProps {
onKeyDown: React.KeyboardEventHandler;
onKeyUp: React.KeyboardEventHandler;
scrollTo?: (args: unknown) => void;
}
type ScrollTo = NonNullable<React.ComponentRef<typeof Tree>['scrollTo']>;
type ReviseRefOptionListProps = Omit<RefOptionListProps, 'scrollTo'> & { scrollTo: ScrollTo };
interface RefOptionListProps {
onKeyDown: React.KeyboardEventHandler;
onKeyUp: React.KeyboardEventHandler;
scrollTo?: React.ComponentRef<typeof Tree>['scrollTo'];
}
type ReviseRefOptionListProps = RefOptionListProps;


const OptionList: React.ForwardRefRenderFunction<ReviseRefOptionListProps> = (_, ref) => {
Expand Down
18 changes: 9 additions & 9 deletions src/TreeSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import type { BaseSelectPropsWithoutPrivate, BaseSelectRef } from '@rc-component/select';
import type { BaseSelectSemanticName } from '@rc-component/select/lib/BaseSelect';
import { BaseSelect } from '@rc-component/select';
import useId from '@rc-component/util/lib/hooks/useId';
import type { IconType } from '@rc-component/tree/lib/interface';
import type { ExpandAction } from '@rc-component/tree/lib/Tree';
import { conductCheck } from '@rc-component/tree/lib/utils/conductUtil';
import useControlledState from '@rc-component/util/lib/hooks/useControlledState';
import { conductCheck } from '@rc-component/tree';
import { useControlledState, useId } from '@rc-component/util';
import * as React from 'react';
import useCache from './hooks/useCache';
import useCheckedKeys from './hooks/useCheckedKeys';
Expand All @@ -28,6 +24,8 @@ import type {
SafeKey,
Key,
DataNode,
ExpandAction,
IconType,
SimpleModeConfig,
ChangeEventExtra,
SelectSource,
Expand All @@ -37,7 +35,7 @@ import type {
} from './interface';
import useSearchConfig from './hooks/useSearchConfig';

export type SemanticName = BaseSelectSemanticName;
export type SemanticName = keyof NonNullable<BaseSelectPropsWithoutPrivate['classNames']>;
export type PopupSemantic = 'item' | 'itemTitle';
export interface SearchConfig {
searchValue?: string;
Expand All @@ -46,8 +44,10 @@ export interface SearchConfig {
filterTreeNode?: boolean | ((inputValue: string, treeNode: DataNode) => boolean);
treeNodeFilterProp?: string;
}
export interface TreeSelectProps<ValueType = any, OptionType extends DataNode = DataNode>
extends Omit<BaseSelectPropsWithoutPrivate, 'mode' | 'classNames' | 'styles' | 'showSearch'> {
export interface TreeSelectProps<
ValueType = any,
OptionType extends DataNode = DataNode,
> extends Omit<BaseSelectPropsWithoutPrivate, 'mode' | 'classNames' | 'styles' | 'showSearch'> {
prefixCls?: string;
id?: string;
children?: React.ReactNode;
Expand Down
5 changes: 2 additions & 3 deletions src/TreeSelectContext.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import * as React from 'react';
import type { ExpandAction } from '@rc-component/tree/lib/Tree';
import type { DataNode, FieldNames, Key } from './interface';
import type { DataNode, FieldNames, Key, ExpandAction } from './interface';
import type useDataEntities from './hooks/useDataEntities';
import { TreeSelectProps } from './TreeSelect';
import type { TreeSelectProps } from './TreeSelect';

export interface TreeSelectContextProps {
virtual?: boolean;
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useCheckedKeys.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import type { DataEntity } from '@rc-component/tree/lib/interface';
import { conductCheck } from '@rc-component/tree/lib/utils/conductUtil';
import { conductCheck } from '@rc-component/tree';
import type { LabeledValueType, SafeKey, Key } from '../interface';
import type { DataEntity } from './useDataEntities';

const useCheckedKeys = (
rawLabeledValues: LabeledValueType[],
Expand Down
7 changes: 4 additions & 3 deletions src/hooks/useDataEntities.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as React from 'react';
import { convertDataToEntities } from '@rc-component/tree/lib/utils/treeUtil';
import type { DataEntity } from '@rc-component/tree/lib/interface';
import { convertDataToEntities } from '@rc-component/tree';
import type { SafeKey, FieldNames } from '../interface';
import warning from '@rc-component/util/lib/warning';
import { warning } from '@rc-component/util';
import { isNil } from '../utils/valueUtil';

export type DataEntity = ReturnType<typeof convertDataToEntities>['keyEntities'][string];

export default (treeData: any, fieldNames: FieldNames) =>
React.useMemo<{
valueEntities: Map<SafeKey, DataEntity>;
Expand Down
10 changes: 8 additions & 2 deletions src/interface.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type * as React from 'react';
import type { SafeKey, Key, DataNode as TreeDataNode } from '@rc-component/tree/lib/interface';
import type { DataNode as TreeDataNode, TreeProps } from '@rc-component/tree';

export type { SafeKey, Key };
export type Key = React.Key;

export type SafeKey = Exclude<Key, bigint>;

export type ExpandAction = TreeProps['expandAction'];

export type IconType = TreeProps['icon'];

export interface DataNode extends Record<string, any>, Omit<TreeDataNode, 'key' | 'children'> {
key?: Key;
Expand Down
3 changes: 1 addition & 2 deletions src/utils/legacyUtil.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as React from 'react';
import toArray from '@rc-component/util/lib/Children/toArray';
import warning from '@rc-component/util/lib/warning';
import { toArray, warning } from '@rc-component/util';
import type {
DataNode,
ChangeEventExtra,
Expand Down
2 changes: 1 addition & 1 deletion src/utils/strategyUtil.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DataEntity } from '@rc-component/tree/lib/interface';
import type { SafeKey, FieldNames } from '../interface';
import type { DataEntity } from '../hooks/useDataEntities';
import { isCheckDisabled } from './valueUtil';

export const SHOW_ALL = 'SHOW_ALL';
Expand Down
2 changes: 1 addition & 1 deletion src/utils/warningPropsUtil.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import warning from '@rc-component/util/lib/warning';
import { warning } from '@rc-component/util';
import type { TreeSelectProps } from '../TreeSelect';
import { toArray } from './valueUtil';

Expand Down
79 changes: 35 additions & 44 deletions tests/Select.SearchInput.spec.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
/* eslint-disable no-undef */
import React, { useState } from 'react';
import { mount } from 'enzyme';
import { render, fireEvent } from '@testing-library/react';
import TreeSelect, { TreeNode } from '../src';
import KeyCode from '@rc-component/util/lib/KeyCode';
import { KeyCode } from '@rc-component/util';
import { getInput, search, selectNode } from './util';

describe('TreeSelect.SearchInput', () => {
it('select item will clean searchInput', () => {
const onSearch = jest.fn();

const wrapper = mount(
const { container } = render(
<TreeSelect onSearch={onSearch} open>
<TreeNode value="test" />
</TreeSelect>,
);

wrapper.search('test');
search(container, 'test');
expect(onSearch).toHaveBeenCalledWith('test');
onSearch.mockReset();

wrapper.selectNode();
selectNode();
expect(onSearch).not.toHaveBeenCalled();
expect(wrapper.find('input').first().props().value).toBeFalsy();
expect(getInput(container).value).toBeFalsy();
});

it('expandedKeys', () => {
const wrapper = mount(
const { container } = render(
<TreeSelect
open
showSearch
Expand All @@ -45,26 +45,19 @@ describe('TreeSelect.SearchInput', () => {
/>,
);

expect(wrapper.find('NodeList').prop('expandedKeys')).toEqual(['bamboo', 'light']);

function search(value) {
wrapper.find('input').first().simulate('change', { target: { value } });
wrapper.update();
}

function listProps() {
return wrapper.find('NodeList').props();
}
expect(container).toHaveTextContent('111');
expect(container).toHaveTextContent('222');

// Clean up
search('bambooA');
search(container, 'bambooA');

// Return back
search('bamboo');
search(container, 'bamboo');

// Back to default
search('');
expect(listProps().expandedKeys).toEqual(['bamboo', 'light']);
search(container, '');
expect(container).toHaveTextContent('111');
expect(container).toHaveTextContent('222');
});

it('not trigger loadData when clearing the search', () => {
Expand Down Expand Up @@ -123,36 +116,35 @@ describe('TreeSelect.SearchInput', () => {
</>
);
};
const wrapper = mount(<Demo />);
expect(wrapper.find('button').length).toBe(1);
const { container } = render(<Demo />);
expect(container.querySelector('button')).toBeTruthy();
expect(handleLoadData).not.toHaveBeenCalled();

function search(value) {
wrapper.find('input').first().simulate('change', { target: { value } });
wrapper.update();
function searchValue(value) {
search(container, value);
}
search('Tree Node');
searchValue('Tree Node');
expect(handleLoadData).not.toHaveBeenCalled();

search('');
searchValue('');
expect(handleLoadData).not.toHaveBeenCalled();

expect(wrapper.find('.rc-tree-select-empty').length).toBe(1);
expect(container.querySelector('.rc-tree-select-empty')).toBeTruthy();

wrapper.find('button').simulate('click');
expect(wrapper.find('.rc-tree-select-empty').length).toBe(0);
expect(wrapper.find('.rc-tree-select-tree').length).toBe(1);
fireEvent.click(container.querySelector('button'));
expect(container.querySelector('.rc-tree-select-empty')).toBeFalsy();
expect(container.querySelector('.rc-tree-select-tree')).toBeTruthy();

search('Tree Node');
searchValue('Tree Node');
expect(handleLoadData).not.toHaveBeenCalled();

search('');
searchValue('');
expect(handleLoadData).not.toHaveBeenCalled();
expect(called).toBe(0);

search('ex');
const nodes = wrapper.find(`[title="${'Expand to load'}"]`).hostNodes();
nodes.first().simulate('click');
searchValue('ex');
const node = document.querySelector('[title="Expand to load"]');
fireEvent.click(node);
expect(called).toBe(0); // should not trrigger all nodes to load data
});

Expand Down Expand Up @@ -188,16 +180,15 @@ describe('TreeSelect.SearchInput', () => {
/>
);
};
const wrapper = mount(<Demo />);
const { container } = render(<Demo />);

function search(value) {
wrapper.find('input').first().simulate('change', { target: { value } });
wrapper.update();
function searchValue(value) {
search(container, value);
}

search('ex');
const nodes = wrapper.find(`[title="${'Expand to load'}"]`).hostNodes();
nodes.first().simulate('click');
searchValue('ex');
const node = document.querySelector('[title="Expand to load"]');
fireEvent.click(node);
expect(called).toBe(1);
});

Expand Down
Loading
Loading