๐ฏ 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..d85381ec
--- /dev/null
+++ b/eslint.config.mjs
@@ -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';
+ }),
+ );
+ }
+
+ 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',
+ },
+ },
+];
diff --git a/global.d.ts b/global.d.ts
new file mode 100644
index 00000000..85e4e4e7
--- /dev/null
+++ b/global.d.ts
@@ -0,0 +1,49 @@
+///
+///
+///
+///
+///
+
+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 = React.JSX.LibraryManagedAttributes;
+ interface IntrinsicAttributes extends React.JSX.IntrinsicAttributes {}
+ interface IntrinsicClassAttributes extends React.JSX.IntrinsicClassAttributes {}
+ interface IntrinsicElements extends React.JSX.IntrinsicElements {}
+}
+
+declare namespace jest {
+ interface Matchers {
+ lastCalledWith(...expected: unknown[]): R;
+ nthCalledWith(nthCall: number, ...expected: unknown[]): R;
+ toBeCalled(): R;
+ toBeCalledTimes(expected: number): R;
+ toBeCalledWith(...expected: unknown[]): R;
+ }
+}
+
+declare const vi: {
+ fn: any = (...args: any[]) => any>(
+ implementation?: T,
+ ) => jest.MockedFunction;
+ mock: (moduleName: string, factory?: (importOriginal: () => Promise) => unknown) => void;
+ spyOn: typeof jest.spyOn;
+ useFakeTimers: () => void;
+ useRealTimers: () => void;
+ advanceTimersByTime: (msToRun: number) => void;
+ clearAllTimers: () => void;
+ runAllTimers: () => void;
+ importActual: (moduleName: string) => Promise;
+ clearAllMocks: () => void;
+ resetAllMocks: () => void;
+ restoreAllMocks: () => void;
+};
+
+declare module 'moment/locale/zh-cn';
diff --git a/package.json b/package.json
index c0570d6d..4bf83498 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.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"
diff --git a/react-compat.d.ts b/react-compat.d.ts
new file mode 100644
index 00000000..ff05aa1b
--- /dev/null
+++ b/react-compat.d.ts
@@ -0,0 +1,16 @@
+import * as React from 'react';
+
+declare module 'react' {
+ type ReactText = string | number;
+ function useRef(): React.MutableRefObject;
+ function isValidElement(object: {} | null | undefined): object is React.ReactElement
;
+ function cloneElement
(
+ element: React.ReactElement
,
+ props?: (Partial
& React.Attributes) | null,
+ ...children: React.ReactNode[]
+ ): React.ReactElement
;
+}
+
+declare module 'react-dom' {
+ function hydrate(element: React.ReactNode, container: Element | DocumentFragment): void;
+}
diff --git a/src/utils/legacyUtil.ts b/src/utils/legacyUtil.ts
index 3e9a449a..cd069edb 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 as React.ReactElement;
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__/ssr.test.tsx.snap b/tests/__snapshots__/ssr.test.tsx.snap
index 9f1c6c0f..52f5328b 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
-exports[`Select.SSR should work 1`] = `""`;
+exports[`Select.SSR should work 1`] = `""`;
diff --git a/tsconfig.json b/tsconfig.json
index eec0a112..31eb4b16 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,18 +1,32 @@
{
"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"
+ "noImplicitAny": false,
+ "strictNullChecks": false,
+ "strictPropertyInitialization": false,
+ "strictFunctionTypes": false,
+ "strict": false,
+ "noImplicitThis": false,
+ "strictBindCallApply": false,
+ "module": "ESNext"
},
- "include": [".dumirc.ts", ".fatherrc.ts", "src", "tests", "docs"]
+ "include": [
+ "react-compat.d.ts",
+ "global.d.ts",
+ ".dumirc.ts",
+ ".fatherrc.ts",
+ "src",
+ "tests",
+ "docs"
+ ]
}