๐ฏ Composable Select component for React, with search, async-friendly option data, custom rendering, and virtual scrolling.
diff --git a/README.zh-CN.md b/README.zh-CN.md
index b84b8763..63759d62 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -1,6 +1,6 @@
@rc-component/select
-
Ant Design ็ๆ็ไธ้จๅใ
+
Ant Design ็ๆ็ไธ้จๅใ
๐ฏ React ้ๆฉๅจ็ปไปถ๏ผๆฏๆๅ้ใๅค้ใๆ็ดขใๆ ็ญพๅ่ชๅฎไนๆธฒๆใ
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 00000000..7fabcb28
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,100 @@
+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 legacyConfig = require('./.eslintrc.js');
+const recommendedTsRulesConfig = tsEslintPlugin.configs.recommended;
+const recommendedTsRulesObject = Array.isArray(recommendedTsRulesConfig)
+ ? recommendedTsRulesConfig.reduce((rules, config) => ({ ...rules, ...(config.rules || {}) }), {})
+ : recommendedTsRulesConfig?.rules || {};
+const recommendedTsRules = new Set(Object.keys(recommendedTsRulesObject));
+
+function hasCurrentTsRule(ruleName) {
+ const tsRuleName = ruleName.replace('@typescript-eslint/', '');
+ return Boolean(tsEslintPlugin.rules[tsRuleName]);
+}
+const localTsRules = new Set(
+ Object.keys(legacyConfig.rules || {}).filter(
+ (ruleName) => ruleName.startsWith('@typescript-eslint/') && hasCurrentTsRule(ruleName),
+ ),
+);
+
+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('@babel/')) {
+ return false;
+ }
+ if (!ruleName.startsWith('@typescript-eslint/')) {
+ return true;
+ }
+ return recommendedTsRules.has(ruleName) || localTsRules.has(ruleName);
+ }),
+ );
+ }
+
+ 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,
+ 'consistent-type-exports': noopRule,
+ },
+ },
+ },
+ },
+ ...compat.config(legacyConfig).map(normalizeConfig),
+ {
+ rules: {
+ '@typescript-eslint/no-empty-object-type': 'off',
+ '@typescript-eslint/no-unsafe-function-type': 'off',
+ '@typescript-eslint/no-unused-vars': 'off',
+ },
+ },
+];
diff --git a/global.d.ts b/global.d.ts
new file mode 100644
index 00000000..e0bd355c
--- /dev/null
+++ b/global.d.ts
@@ -0,0 +1,11 @@
+///
+///
+///
+///
+///
+
+declare module '*.css';
+declare module '*.less';
+declare module 'jsonp';
+
+declare module 'moment/locale/zh-cn';
diff --git a/package.json b/package.json
index 890f41d8..eec2ad88 100644
--- a/package.json
+++ b/package.json
@@ -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.1",
+ "@typescript-eslint/parser": "^8.62.1",
"@umijs/fabric": "^4.0.1",
- "babel-jest": "^29.7.0",
- "dumi": "^2.4.35",
- "eslint": "^8.57.1",
- "father": "^4.6.23",
+ "babel-jest": "^30.4.1",
+ "dumi": "^2.4.38",
+ "eslint": "^9.39.4",
+ "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-jest": "^29.15.4",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-unicorn": "^65.0.1",
+ "father": "^4.6.24",
"husky": "^9.1.7",
"jsonp": "^0.2.1",
"less": "^4.6.7",
- "lint-staged": "^16.4.0",
- "prettier": "^3.9.0",
+ "lint-staged": "^17.0.8",
+ "prettier": "^3.9.4",
"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"
diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx
index 44f27812..a4569e5e 100644
--- a/src/BaseSelect/index.tsx
+++ b/src/BaseSelect/index.tsx
@@ -171,9 +171,9 @@ export interface BaseSelectProps
// >>> Customize Input
/** @private Internal usage. Do not use in your production. */
- getInputElement?: () => JSX.Element;
+ getInputElement?: () => React.ReactElement;
/** @private Internal usage. Do not use in your production. */
- getRawInputElement?: () => JSX.Element;
+ getRawInputElement?: () => React.ReactElement;
// >>> Selector
maxTagTextLength?: number;
diff --git a/src/Select.tsx b/src/Select.tsx
index 41bd66cc..2489e2a3 100644
--- a/src/Select.tsx
+++ b/src/Select.tsx
@@ -539,7 +539,7 @@ const Select = React.forwardRef>();
+ const activeEventRef = React.useRef | undefined>(undefined);
const onActiveValue: OnActiveValue = React.useCallback(
(active, index, { source = 'keyboard' } = {}) => {
diff --git a/src/hooks/useRefFunc.ts b/src/hooks/useRefFunc.ts
index 720f972c..993e9021 100644
--- a/src/hooks/useRefFunc.ts
+++ b/src/hooks/useRefFunc.ts
@@ -5,7 +5,7 @@ import * as React from 'react';
* but redirect to real function.
*/
export default function useRefFunc any>(callback: T): T {
- const funcRef = React.useRef();
+ const funcRef = React.useRef(callback);
funcRef.current = callback;
const cacheFn = React.useCallback((...args: any[]) => {
diff --git a/src/utils/legacyUtil.ts b/src/utils/legacyUtil.ts
index 3e9a449a..644806d8 100644
--- a/src/utils/legacyUtil.ts
+++ b/src/utils/legacyUtil.ts
@@ -3,12 +3,12 @@ import { toArray } from '@rc-component/util';
import type { BaseOptionType, DefaultOptionType } from '../Select';
function convertNodeToOption(
- node: React.ReactElement,
+ node: React.ReactElement,
): OptionType {
const {
key,
props: { children, value, ...restProps },
- } = node as React.ReactElement;
+ } = node;
return { key, value: value !== undefined ? value : key, children, ...restProps };
}
@@ -18,7 +18,7 @@ export function convertChildrenToData {
+ .map((node: React.ReactElement, index: number): OptionType | null => {
if (!React.isValidElement(node) || !node.type) {
return null;
}
@@ -27,7 +27,7 @@ export function convertChildrenToData & { type: { isSelectOptGroup?: boolean } };
if (optionOnly || !isSelectOptGroup) {
return convertNodeToOption(node);
diff --git a/src/utils/warningPropsUtil.ts b/src/utils/warningPropsUtil.ts
index c5560894..e6f96ae3 100644
--- a/src/utils/warningPropsUtil.ts
+++ b/src/utils/warningPropsUtil.ts
@@ -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).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;
diff --git a/tests/__snapshots__/Combobox.test.tsx.snap b/tests/__snapshots__/Combobox.test.tsx.snap
index 4e4b3601..6eac14f0 100644
--- a/tests/__snapshots__/Combobox.test.tsx.snap
+++ b/tests/__snapshots__/Combobox.test.tsx.snap
@@ -1,4 +1,4 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Select.Combobox renders controlled correctly 1`] = `
diff --git a/tests/__snapshots__/Select.test.tsx.snap b/tests/__snapshots__/Select.test.tsx.snap
index 5ec0a630..b94d30ad 100644
--- a/tests/__snapshots__/Select.test.tsx.snap
+++ b/tests/__snapshots__/Select.test.tsx.snap
@@ -1,4 +1,4 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Select.Basic does not filter when filterOption value is false 1`] = `
diff --git a/tests/__snapshots__/ssr.test.tsx.snap b/tests/__snapshots__/ssr.test.tsx.snap
index de7ade80..1bdd9301 100644
--- a/tests/__snapshots__/ssr.test.tsx.snap
+++ b/tests/__snapshots__/ssr.test.tsx.snap
@@ -1,3 +1,3 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
-exports[`Select.SSR should work 1`] = `"
"`;
+exports[`Select.SSR should work 1`] = `"
"`;
diff --git a/tsconfig.json b/tsconfig.json
index eec0a112..cd160b40 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,18 +1,18 @@
{
"compilerOptions": {
"target": "esnext",
- "moduleResolution": "node",
- "baseUrl": "./",
+ "moduleResolution": "bundler",
"jsx": "preserve",
"declaration": true,
"skipLibCheck": true,
"esModuleInterop": true,
"paths": {
- "@/*": ["src/*"],
- "@@/*": [".dumi/tmp/*"],
- "@rc-component/select": ["src/index.ts"]
+ "@/*": ["./src/*"],
+ "@@/*": ["./.dumi/tmp/*"],
+ "@rc-component/select": ["./src/index.ts"]
},
- "ignoreDeprecations": "5.0"
+ "strict": false,
+ "module": "ESNext"
},
- "include": [".dumirc.ts", ".fatherrc.ts", "src", "tests", "docs"]
+ "include": ["global.d.ts", ".dumirc.ts", ".fatherrc.ts", "src", "tests", "docs"]
}