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
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ updates:
time: '21:00'
timezone: Asia/Shanghai
open-pull-requests-limit: 10
groups:
npm-dependencies:
patterns:
- '*'

- package-ecosystem: github-actions
directory: '/'
Expand All @@ -17,3 +21,7 @@ updates:
time: '21:00'
timezone: Asia/Shanghai
open-pull-requests-limit: 10
groups:
github-actions:
patterns:
- '*'
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div align="center">
<h1>@rc-component/select</h1>
<p><sub><img alt="Ant Design" height="14" src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg" style="vertical-align: -0.125em;" /> Part of the Ant Design ecosystem.</sub></p>
<p><sub><a href="https://ant.design"><img alt="Ant Design" height="14" src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg" style="vertical-align: -0.125em;" /></a> Part of the Ant Design ecosystem.</sub></p>
<p>🎯 Composable Select component for React, with search, async-friendly option data, custom rendering, and virtual scrolling.</p>

<p>
Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div align="center">
<h1>@rc-component/select</h1>
<p><sub><img alt="Ant Design" height="14" src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg" style="vertical-align: -0.125em;" /> Ant Design 生态的一部分。</sub></p>
<p><sub><a href="https://ant.design"><img alt="Ant Design" height="14" src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg" style="vertical-align: -0.125em;" /></a> Ant Design 生态的一部分。</sub></p>
<p>🎯 React 选择器组件,支持单选、多选、搜索、标签和自定义渲染。</p>

<p>
Expand Down
83 changes: 83 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { FlatCompat } from '@eslint/eslintrc';
import js from '@eslint/js';
import tsEslintPlugin from '@typescript-eslint/eslint-plugin';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);

const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});

const recommendedTsRules = new Set(Object.keys(tsEslintPlugin.configs.recommended.rules || {}));
const noopRule = {
meta: { type: 'problem', docs: {}, schema: [] },
create: () => ({}),
};

function normalizeConfig(config) {
const next = { ...config };

if (next.plugins?.['@typescript-eslint']) {
next.plugins = { ...next.plugins };
delete next.plugins['@typescript-eslint'];
}

if (next.rules) {
next.rules = Object.fromEntries(
Object.entries(next.rules).filter(([ruleName]) => {
if (!ruleName.startsWith('@typescript-eslint/')) {
return true;
}
return recommendedTsRules.has(ruleName) || ruleName === '@typescript-eslint/ban-types';
}),
);
}
Comment on lines +32 to +41

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

This filtering logic silently discards any custom @typescript-eslint rules configured in .eslintrc.js that are not part of the recommended set. This can lead to unexpected behavior where intended lint rules are silently disabled without warning.

Instead of silently filtering rules in a helper function, consider explicitly disabling or overriding specific rules in the flat config array, or updating .eslintrc.js to remove deprecated rules.


return next;
}

export default [
{
ignores: [
'node_modules/',
'coverage/',
'es/',
'lib/',
'dist/',
'docs-dist/',
'.dumi/',
'.doc/',
'.vercel/',
'.eslintrc.js',
'src/index.d.ts',
],
},
{
plugins: {
'@typescript-eslint': {
...tsEslintPlugin,
rules: {
...tsEslintPlugin.rules,
'ban-types': noopRule,
'consistent-type-exports': noopRule,
},
},
},
},
...compat.config(require('./.eslintrc.js')).map(normalizeConfig),
{
rules: {
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off',
'@typescript-eslint/no-unused-vars': 'off',
},
},
];
49 changes: 49 additions & 0 deletions global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/// <reference types="jest" />
/// <reference types="node" />
/// <reference types="react" />
/// <reference types="react-dom" />
/// <reference types="@testing-library/jest-dom" />

declare module '*.css';
declare module '*.less';
declare module 'jsonp';

declare namespace JSX {
type Element = React.JSX.Element;
interface ElementClass extends React.JSX.ElementClass {}
interface ElementAttributesProperty extends React.JSX.ElementAttributesProperty {}
interface ElementChildrenAttribute extends React.JSX.ElementChildrenAttribute {}
type LibraryManagedAttributes<C, P> = React.JSX.LibraryManagedAttributes<C, P>;
interface IntrinsicAttributes extends React.JSX.IntrinsicAttributes {}
interface IntrinsicClassAttributes<T> extends React.JSX.IntrinsicClassAttributes<T> {}
interface IntrinsicElements extends React.JSX.IntrinsicElements {}
}

declare namespace jest {
interface Matchers<R> {
lastCalledWith(...expected: unknown[]): R;
nthCalledWith(nthCall: number, ...expected: unknown[]): R;
toBeCalled(): R;
toBeCalledTimes(expected: number): R;
toBeCalledWith(...expected: unknown[]): R;
}
}

declare const vi: {
fn: <T extends (...args: any[]) => any = (...args: any[]) => any>(
implementation?: T,
) => jest.MockedFunction<T>;
mock: (moduleName: string, factory?: (importOriginal: <T>() => Promise<T>) => unknown) => void;
spyOn: typeof jest.spyOn;
useFakeTimers: () => void;
useRealTimers: () => void;
advanceTimersByTime: (msToRun: number) => void;
clearAllTimers: () => void;
runAllTimers: () => void;
importActual: <T>(moduleName: string) => Promise<T>;
clearAllMocks: () => void;
resetAllMocks: () => void;
restoreAllMocks: () => void;
};

declare module 'moment/locale/zh-cn';
30 changes: 21 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,30 +55,42 @@
"clsx": "^2.1.1"
},
"devDependencies": {
"@babel/eslint-parser": "^7.29.7",
"@babel/eslint-plugin": "^7.29.7",
"@eslint/eslintrc": "^3.3.5",
"@eslint/js": "^9.39.4",
"@rc-component/dialog": "^1.10.0",
"@rc-component/father-plugin": "^2.2.0",
"@rc-component/np": "^1.0.4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^15.0.7",
"@types/jest": "^29.5.14",
"@testing-library/react": "^16.3.2",
"@types/jest": "^30.0.0",
"@types/node": "^26.0.1",
"@types/react": "^18.3.31",
"@types/react-dom": "^18.3.7",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/parser": "^8.62.0",
"@umijs/fabric": "^4.0.1",
"babel-jest": "^29.7.0",
"dumi": "^2.4.35",
"eslint": "^8.57.1",
"eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jest": "^29.15.3",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-unicorn": "^65.0.1",
"father": "^4.6.23",
"husky": "^9.1.7",
"jsonp": "^0.2.1",
"less": "^4.6.7",
"lint-staged": "^16.4.0",
"lint-staged": "^17.0.8",
"prettier": "^3.9.0",
"querystring": "^0.2.1",
"rc-test": "^7.1.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript": "^5.9.3"
"react": "^19.2.7",
"react-dom": "^19.2.7",
"typescript": "^6.0.3"
},
"publishConfig": {
"access": "public"
Expand Down
16 changes: 16 additions & 0 deletions react-compat.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';

declare module 'react' {
type ReactText = string | number;
function useRef<T = undefined>(): React.MutableRefObject<T | undefined>;
function isValidElement<P = any>(object: {} | null | undefined): object is React.ReactElement<P>;
function cloneElement<P = any>(
element: React.ReactElement<P>,
props?: (Partial<P> & React.Attributes) | null,
...children: React.ReactNode[]
): React.ReactElement<P>;
}
Comment on lines +3 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Overriding standard React types globally via ambient module declaration (declare module 'react') is highly discouraged. It can cause subtle type conflicts for consumers of this library, especially when they use different React or TypeScript versions, or have strictNullChecks enabled.

Instead of global module augmentation, consider using local type assertions or utility types within the codebase where compatibility adjustments are needed.


declare module 'react-dom' {
function hydrate(element: React.ReactNode, container: Element | DocumentFragment): void;
}
8 changes: 4 additions & 4 deletions src/utils/legacyUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { toArray } from '@rc-component/util';
import type { BaseOptionType, DefaultOptionType } from '../Select';

function convertNodeToOption<OptionType extends BaseOptionType = DefaultOptionType>(
node: React.ReactElement,
node: React.ReactElement<any>,
): OptionType {
const {
key,
props: { children, value, ...restProps },
} = node as React.ReactElement;
} = node as React.ReactElement<any>;
Comment on lines 8 to +11

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The cast as React.ReactElement<any> is redundant here because the node parameter is already explicitly typed as React.ReactElement<any> in the function signature.

Suggested change
const {
key,
props: { children, value, ...restProps },
} = node as React.ReactElement;
} = node as React.ReactElement<any>;
const {
key,
props: { children, value, ...restProps },
} = node;


return { key, value: value !== undefined ? value : key, children, ...restProps };
}
Expand All @@ -18,7 +18,7 @@ export function convertChildrenToData<OptionType extends BaseOptionType = Defaul
optionOnly: boolean = false,
): OptionType[] {
return toArray(nodes)
.map((node: React.ReactElement, index: number): OptionType | null => {
.map((node: React.ReactElement<any>, index: number): OptionType | null => {
if (!React.isValidElement(node) || !node.type) {
return null;
}
Expand All @@ -27,7 +27,7 @@ export function convertChildrenToData<OptionType extends BaseOptionType = Defaul
type: { isSelectOptGroup },
key,
props: { children, ...restProps },
} = node as React.ReactElement & { type: { isSelectOptGroup?: boolean } };
} = node as React.ReactElement<any> & { type: { isSelectOptGroup?: boolean } };

if (optionOnly || !isSelectOptGroup) {
return convertNodeToOption(node);
Expand Down
26 changes: 13 additions & 13 deletions src/utils/warningPropsUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,19 +116,19 @@ function warningProps(props: SelectProps) {
return false;
}
if (type.isSelectOptGroup) {
const allChildrenValid = toNodeArray(node.props.children).every(
(subNode: React.ReactElement) => {
if (
!React.isValidElement(subNode) ||
!node.type ||
(subNode.type as { isSelectOption?: boolean }).isSelectOption
) {
return true;
}
invalidateChildType = subNode.type;
return false;
},
);
const allChildrenValid = toNodeArray(
(node as React.ReactElement<any>).props.children,
).every((subNode: React.ReactElement) => {
if (
!React.isValidElement(subNode) ||
!node.type ||
(subNode.type as { isSelectOption?: boolean }).isSelectOption
) {
return true;
}
invalidateChildType = subNode.type;
return false;
});

if (allChildrenValid) {
return false;
Expand Down
3 changes: 2 additions & 1 deletion tests/Accessibility.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { fireEvent } from '@testing-library/dom';
import * as React from 'react';
import { KeyCode } from '@rc-component/util';
import Select from '../src';
import { injectRunAllTimers, expectOpen, keyDown } from './utils/common';
import { act, fireEvent, render } from '@testing-library/react';
import { act, render } from '@testing-library/react';

describe('Select.Accessibility', () => {
injectRunAllTimers(jest);
Expand Down
3 changes: 2 additions & 1 deletion tests/BaseSelect.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { fireEvent } from '@testing-library/dom';
import type { OptionListProps, RefOptionListProps } from '../src/OptionList';
import { fireEvent, render } from '@testing-library/react';
import { render } from '@testing-library/react';
import { forwardRef, act } from 'react';
import BaseSelect from '../src/BaseSelect';
import { waitFakeTimer } from './utils/common';
Expand Down
3 changes: 2 additions & 1 deletion tests/Combobox.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { createEvent, fireEvent } from '@testing-library/dom';
/* eslint-disable max-classes-per-file */

import '@testing-library/jest-dom';
import { createEvent, fireEvent, render } from '@testing-library/react';
import { render } from '@testing-library/react';
import { KeyCode, resetWarned } from '@rc-component/util';
import React, { act } from 'react';
import type { SelectProps } from '../src';
Expand Down
3 changes: 2 additions & 1 deletion tests/Custom.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { fireEvent } from '@testing-library/dom';
import * as React from 'react';
import Select from '../src';
import { injectRunAllTimers, waitFakeTimer } from './utils/common';
import { fireEvent, render } from '@testing-library/react';
import { render } from '@testing-library/react';

describe('Select.Custom', () => {
injectRunAllTimers(jest);
Expand Down
3 changes: 2 additions & 1 deletion tests/Field.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { fireEvent } from '@testing-library/dom';
import React, { act } from 'react';
import Select from '../src';
import type { SelectProps } from '../src';
import { injectRunAllTimers } from './utils/common';
import { fireEvent, render } from '@testing-library/react';
import { render } from '@testing-library/react';

describe('Select.Field', () => {
injectRunAllTimers(jest);
Expand Down
3 changes: 2 additions & 1 deletion tests/Group.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { fireEvent } from '@testing-library/dom';
import * as React from 'react';
import Select, { OptGroup, Option } from '../src';
import { fireEvent, render } from '@testing-library/react';
import { render } from '@testing-library/react';

describe('Select.Group', () => {
it('group name support search', () => {
Expand Down
3 changes: 2 additions & 1 deletion tests/Multiple.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { fireEvent } from '@testing-library/dom';
import { KeyCode } from '@rc-component/util';
import React from 'react';
import Select, { Option, OptGroup } from '../src';
Expand All @@ -19,7 +20,7 @@ import {
keyUp,
} from './utils/common';
import allowClearTest from './shared/allowClearTest';
import { act, fireEvent, render, screen } from '@testing-library/react';
import { act, render, screen } from '@testing-library/react';

describe('Select.Multiple', () => {
injectRunAllTimers(jest);
Expand Down
3 changes: 2 additions & 1 deletion tests/OptionList.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createEvent, fireEvent } from '@testing-library/dom';
import { KeyCode, spyElementPrototypes } from '@rc-component/util';
import React, { act } from 'react';
import { BaseSelectContext } from '../src/hooks/useBaseProps';
Expand All @@ -6,7 +7,7 @@ import OptionList from '../src/OptionList';
import SelectContext from '../src/SelectContext';
import { fillFieldNames, flattenOptions } from '../src/utils/valueUtil';
import { injectRunAllTimers } from './utils/common';
import { createEvent, fireEvent, render, waitFor } from '@testing-library/react';
import { render, waitFor } from '@testing-library/react';
import Select from '../src';

jest.mock('../src/utils/platformUtil');
Expand Down
3 changes: 2 additions & 1 deletion tests/Popup.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fireEvent, render } from '@testing-library/react';
import { fireEvent } from '@testing-library/dom';
import { render } from '@testing-library/react';
import React from 'react';
import Select from '../src';
import { injectRunAllTimers } from './utils/common';
Expand Down
Loading
Loading