├── .husky └── pre-commit ├── vercel.json ├── docs ├── examples │ ├── single.less │ ├── option-render.tsx │ ├── custom-selector.tsx │ ├── auto-tokenization.tsx │ ├── optionLabelProp.tsx │ ├── common │ │ └── tbFetchSuggest.tsx │ ├── singleFieldNames.tsx │ ├── scroll-loading.tsx │ ├── filterSort.tsx │ ├── multiple-with-maxCount.tsx │ ├── optionFilterProp.tsx │ ├── focus.tsx │ ├── single-animation.tsx │ ├── email.tsx │ ├── multiple-readonly.tsx │ ├── optgroup.tsx │ ├── mul-suggest.tsx │ ├── mul-tag-suggest.tsx │ ├── update-option.tsx │ ├── loading.tsx │ ├── custom-label.tsx │ ├── force-suggest.tsx │ ├── suggest.tsx │ ├── getPopupContainer.tsx │ ├── dropdownRender.tsx │ ├── controlled.tsx │ ├── auto-adjust-dropdown.tsx │ ├── custom-tags.tsx │ ├── tags.tsx │ ├── single.tsx │ ├── multiple.tsx │ ├── combobox.tsx │ └── custom-icon.tsx ├── demo │ ├── email.md │ ├── focus.md │ ├── tags.md │ ├── loading.md │ ├── single.md │ ├── suggest.md │ ├── combobox.md │ ├── multiple.md │ ├── optgroup.md │ ├── controlled.md │ ├── filterSort.md │ ├── custom-icon.md │ ├── custom-label.md │ ├── custom-tags.md │ ├── mul-suggest.md │ ├── force-suggest.md │ ├── option-render.md │ ├── update-option.md │ ├── custom-selector.md │ ├── dropdownRender.md │ ├── mul-tag-suggest.md │ ├── optionLabelProp.md │ ├── scroll-loading.md │ ├── auto-tokenization.md │ ├── getPopupContainer.md │ ├── multiple-readonly.md │ ├── optionFilterProp.md │ ├── single-animation.md │ ├── singleFieldNames.md │ ├── auto-adjust-dropdown.md │ └── multiple-with-maxCount.md └── index.md ├── HISTORY.md ├── src ├── utils │ ├── __mocks__ │ │ └── platformUtil.ts │ ├── platformUtil.ts │ ├── commonUtil.ts │ ├── keyUtil.ts │ ├── legacyUtil.ts │ ├── valueUtil.ts │ └── warningPropsUtil.ts ├── SelectInput │ ├── context.ts │ ├── Affix.tsx │ └── Content │ │ ├── Placeholder.tsx │ │ ├── index.tsx │ │ └── SingleContent.tsx ├── hooks │ ├── useRefFunc.ts │ ├── useBaseProps.ts │ ├── useLock.ts │ ├── useSearchConfig.ts │ ├── useComponents.ts │ ├── useAllowClear.tsx │ ├── useSelectTriggerControl.ts │ ├── useCache.ts │ ├── useOptions.ts │ ├── useFilterOptions.ts │ └── useOpen.ts ├── index.ts ├── OptGroup.tsx ├── Option.tsx ├── interface.ts ├── BaseSelect │ └── Polite.tsx ├── SelectContext.ts ├── TransBtn.tsx └── SelectTrigger.tsx ├── tests ├── __mocks__ │ └── @rc-component │ │ ├── virtual-list.tsx │ │ └── trigger.tsx ├── __snapshots__ │ ├── Multiple.test.tsx.snap │ ├── ssr.test.tsx.snap │ ├── Combobox.test.tsx.snap │ └── OptionList.test.tsx.snap ├── SelectTrigger.spec.tsx ├── shared │ ├── keyDownTest.tsx │ ├── throwOptionValue.tsx │ ├── hoverTest.tsx │ ├── openControlledTest.tsx │ ├── allowClearTest.tsx │ ├── inputFilterTest.tsx │ ├── focusTest.tsx │ ├── blurTest.tsx │ ├── removeSelectedTest.tsx │ ├── maxTagRenderTest.tsx │ └── dynamicChildrenTest.tsx ├── setup.ts ├── Custom.test.tsx ├── Hooks.test.tsx ├── Popup.test.tsx ├── type.test.tsx ├── ssr.test.tsx ├── React.test.tsx ├── components.test.tsx ├── placeholder.test.tsx ├── utils.test.jsx ├── Field.test.tsx ├── Group.test.tsx ├── focus.test.tsx ├── utils │ └── common.ts ├── semantic.test.tsx └── BaseSelect.test.tsx ├── .fatherrc.js ├── jest.config.js ├── .prettierignore ├── .github ├── workflows │ ├── test.yml │ └── codeql.yml ├── dependabot.yml └── FUNDING.yml ├── .prettierrc ├── .editorconfig ├── typings └── index.d.ts ├── .dumirc.ts ├── .eslintrc.js ├── tsconfig.json ├── .gitignore ├── LICENSE.md ├── assets └── patch.less └── package.json /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "umijs" 3 | } 4 | -------------------------------------------------------------------------------- /docs/examples/single.less: -------------------------------------------------------------------------------- 1 | .test-option { 2 | font-weight: bolder; 3 | } 4 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | See [rc-select releases](https://github.com/react-component/select/releases) 2 | -------------------------------------------------------------------------------- /src/utils/__mocks__/platformUtil.ts: -------------------------------------------------------------------------------- 1 | export function isPlatformMac() { 2 | return true; 3 | } 4 | -------------------------------------------------------------------------------- /tests/__mocks__/@rc-component/virtual-list.tsx: -------------------------------------------------------------------------------- 1 | import List from '@rc-component/virtual-list/lib/mock'; 2 | 3 | export default List; 4 | -------------------------------------------------------------------------------- /docs/demo/email.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: email 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/focus.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: focus 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/tags.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: tags 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hero: 3 | title: rc-select 4 | description: React Select Component 5 | --- 6 | 7 | 8 | -------------------------------------------------------------------------------- /.fatherrc.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | plugins: ['@rc-component/father-plugin'], 5 | }); 6 | -------------------------------------------------------------------------------- /docs/demo/loading.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: loading 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/single.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: single 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/suggest.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: suggest 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/combobox.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: combobox 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/multiple.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: multiple 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/optgroup.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: optgroup 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupFilesAfterEnv: ['/tests/setup.ts'], 3 | collectCoverageFrom: ['src/**/*.{ts,tsx,js,jsx}'], 4 | }; 5 | -------------------------------------------------------------------------------- /docs/demo/controlled.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: controlled 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/filterSort.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: filterSort 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .doc 2 | .storybook 3 | es 4 | lib 5 | **/*.svg 6 | **/*.ejs 7 | **/*.html 8 | package.json 9 | .umi 10 | .umi-production 11 | .umi-test 12 | -------------------------------------------------------------------------------- /docs/demo/custom-icon.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: custom-icon 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/custom-label.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: custom-label 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/custom-tags.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: custom-tags 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/mul-suggest.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: mul-suggest 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/force-suggest.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: force-suggest 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/option-render.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: optionRender 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/update-option.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: update-option 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/utils/platformUtil.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | export function isPlatformMac(): boolean { 3 | return /(mac\sos|macintosh)/i.test(navigator.appVersion); 4 | } 5 | -------------------------------------------------------------------------------- /docs/demo/custom-selector.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: custom-selector 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/dropdownRender.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: dropdownRender 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/mul-tag-suggest.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: mul-tag-suggest 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/optionLabelProp.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: optionLabelProp 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/scroll-loading.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: scroll loading 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/auto-tokenization.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: auto-tokenization 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/getPopupContainer.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: getPopupContainer 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/multiple-readonly.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: multiple-readonly 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/optionFilterProp.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: optionFilterProp 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/single-animation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: single-animation 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/singleFieldNames.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: singleFieldNames 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: ✅ test 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | uses: react-component/rc-test/.github/workflows/test.yml@main 6 | secrets: inherit 7 | -------------------------------------------------------------------------------- /docs/demo/auto-adjust-dropdown.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: auto-adjust-dropdown 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/demo/multiple-with-maxCount.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: multiple-with-maxCount 3 | nav: 4 | title: Demo 5 | path: /demo 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "proseWrap": "never", 8 | "printWidth": 100 9 | } 10 | -------------------------------------------------------------------------------- /tests/__snapshots__/Multiple.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Select.Multiple max tag render truncates values by maxTagTextLength 1`] = ` 4 | [ 5 | "On...", 6 | "Tw...", 7 | ] 8 | `; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*.{js,css}] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-lifecycles-compat'; 2 | 3 | declare module 'component-classes'; 4 | 5 | declare module 'rc-menu'; 6 | 7 | declare module '@rc-component/util/lib/Children/toArray'; 8 | 9 | declare module 'dom-scroll-into-view'; 10 | -------------------------------------------------------------------------------- /.dumirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi'; 2 | 3 | export default defineConfig({ 4 | favicons: ['https://avatars0.githubusercontent.com/u/9441414?s=200&v=4'], 5 | themeConfig: { 6 | name: 'Select', 7 | logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4', 8 | }, 9 | outputPath: '.doc', 10 | }); 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const base = require('@umijs/fabric/dist/eslint'); 2 | 3 | module.exports = { 4 | ...base, 5 | rules: { 6 | ...base.rules, 7 | 'default-case': 0, 8 | 'react/sort-comp': 0, 9 | 'jsx-a11y/interactive-supports-focus': 0, 10 | 'jsx-a11y/no-autofocus': 0, 11 | 'react/no-unknown-property': 0, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /tests/__mocks__/@rc-component/trigger.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Trigger from '@rc-component/trigger/lib/mock'; 3 | import type { TriggerProps, TriggerRef } from '@rc-component/trigger'; 4 | 5 | export default React.forwardRef((props, ref) => { 6 | global.triggerProps = props; 7 | return ; 8 | }); 9 | -------------------------------------------------------------------------------- /src/SelectInput/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import type { SelectInputProps } from '.'; 3 | 4 | export type ContentContextProps = SelectInputProps; 5 | 6 | const SelectInputContext = React.createContext(null!); 7 | 8 | export function useSelectInputContext() { 9 | return React.useContext(SelectInputContext); 10 | } 11 | 12 | export default SelectInputContext; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "baseUrl": "./", 6 | "jsx": "preserve", 7 | "declaration": true, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "paths": { 11 | "@/*": ["src/*"], 12 | "@@/*": [".dumi/tmp/*"], 13 | "@rc-component/select": ["src/index.ts"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/__snapshots__/ssr.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Select.SSR should work 1`] = `"
"`; 4 | -------------------------------------------------------------------------------- /docs/examples/option-render.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import Select from '@rc-component/select'; 3 | import '../../assets/index.less'; 4 | 5 | export default () => { 6 | return ( 7 | Content} 10 | mode="multiple" 11 | options={[{ value: 'light' }, { value: 'bamboo' }]} 12 | allowClear 13 | placeholder="2333" 14 | /> 15 | ); 16 | }; 17 | /* eslint-enable */ 18 | -------------------------------------------------------------------------------- /src/SelectInput/Affix.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export interface AffixProps extends React.HTMLAttributes { 4 | children?: React.ReactNode; 5 | } 6 | 7 | // Affix is a simple wrapper which should not read context or logical props 8 | export default function Affix(props: AffixProps) { 9 | const { children, ...restProps } = props; 10 | 11 | if (!children) { 12 | return null; 13 | } 14 | 15 | return
{children}
; 16 | } 17 | -------------------------------------------------------------------------------- /docs/examples/auto-tokenization.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Select from '@rc-component/select'; 3 | import '../../assets/index.less'; 4 | 5 | const Demo: React.FC = () => ( 6 | <> 7 |

自动分词

8 | 11 | 12 | 13 | , 14 | ); 15 | 16 | fireEvent.keyDown(container.querySelector('input')); 17 | expect(onInputKeyDown).toHaveBeenCalled(); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/Option.tsx: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import type * as React from 'react'; 3 | import type { DefaultOptionType } from './Select'; 4 | 5 | export interface OptionProps extends Omit { 6 | children: React.ReactNode; 7 | 8 | /** Save for customize data */ 9 | [prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any 10 | } 11 | 12 | export interface OptionFC extends React.FC { 13 | /** Legacy for check if is a Option Group */ 14 | isSelectOption: boolean; 15 | } 16 | 17 | /** This is a placeholder, not real render in dom */ 18 | const Option: OptionFC = () => null; 19 | Option.isSelectOption = true; 20 | 21 | export default Option; 22 | -------------------------------------------------------------------------------- /docs/examples/optionLabelProp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Select from '@rc-component/select'; 3 | import '../../assets/index.less'; 4 | 5 | const data: { value: number; label: string; displayLabel: string }[] = []; 6 | for (let i = 0; i < 10; i += 1) { 7 | data.push({ 8 | value: i, 9 | label: `label ${i}`, 10 | displayLabel: `display ${i}`, 11 | }); 12 | } 13 | 14 | function Test() { 15 | return ( 16 |
17 |

Select optionLabelProp

18 | 14 | 15 | , 16 | ); 17 | 18 | expect(errorSpy).toHaveBeenCalledWith( 19 | 'Warning: `value` of Option should not use number type when `mode` is `tags` or `combobox`.', 20 | ); 21 | 22 | errorSpy.mockRestore(); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /tests/shared/hoverTest.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Select, { Option } from '../../src'; 3 | import { fireEvent, render } from '@testing-library/react'; 4 | 5 | export default function hoverTest(mode: any) { 6 | it('triggers mouseEnter and mouseLeave', () => { 7 | const onMouseEnter = jest.fn(); 8 | const onMouseLeave = jest.fn(); 9 | const { container } = render( 10 | , 14 | ); 15 | 16 | fireEvent.mouseEnter(container.querySelector('.rc-select')); 17 | expect(onMouseEnter).toBeCalled(); 18 | fireEvent.mouseLeave(container.querySelector('.rc-select')); 19 | expect(onMouseLeave).toBeCalled(); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /tests/shared/openControlledTest.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Option from '../../src/Option'; 3 | import Select from '../../src/Select'; 4 | import { expectOpen } from '../utils/common'; 5 | import { render } from '@testing-library/react'; 6 | 7 | export default function openControlledTest(mode: any) { 8 | it('selectTriggerRef.props.visible should be equal to props.open', () => { 9 | const renderDemo = (open = true) => ( 10 | 15 | ); 16 | 17 | const { container, rerender } = render(renderDemo()); 18 | expectOpen(container); 19 | 20 | rerender(renderDemo(false)); 21 | expectOpen(container, false); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /docs/examples/common/tbFetchSuggest.tsx: -------------------------------------------------------------------------------- 1 | import jsonp from 'jsonp'; 2 | import querystring from 'querystring'; 3 | 4 | let timeout; 5 | let currentValue; 6 | 7 | export function fetch(value, callback) { 8 | if (timeout) { 9 | clearTimeout(timeout); 10 | timeout = null; 11 | } 12 | currentValue = value; 13 | 14 | function fake() { 15 | const str = querystring.encode({ 16 | code: 'utf-8', 17 | q: value, 18 | }); 19 | jsonp(`http://suggest.taobao.com/sug?${str}`, (err, d) => { 20 | if (currentValue === value) { 21 | const { result } = d; 22 | const data = []; 23 | result.forEach((r) => { 24 | data.push({ 25 | value: r[0], 26 | text: r[0], 27 | }); 28 | }); 29 | callback(data); 30 | } 31 | }); 32 | } 33 | 34 | timeout = setTimeout(fake, 300); 35 | } 36 | -------------------------------------------------------------------------------- /src/SelectInput/Content/Placeholder.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { clsx } from 'clsx'; 3 | import { useSelectInputContext } from '../context'; 4 | import useBaseProps from '../../hooks/useBaseProps'; 5 | 6 | export interface PlaceholderProps { 7 | show?: boolean; 8 | } 9 | 10 | export default function Placeholder(props: PlaceholderProps) { 11 | const { prefixCls, placeholder, displayValues } = useSelectInputContext(); 12 | const { classNames, styles } = useBaseProps(); 13 | const { show = true } = props; 14 | 15 | if (displayValues.length) { 16 | return null; 17 | } 18 | 19 | return ( 20 |
27 | {placeholder} 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/interface.ts: -------------------------------------------------------------------------------- 1 | import type * as React from 'react'; 2 | 3 | export type RawValueType = string | number; 4 | export interface FlattenOptionData { 5 | label?: React.ReactNode; 6 | data: OptionType; 7 | key: React.Key; 8 | value?: RawValueType; 9 | groupOption?: boolean; 10 | group?: boolean; 11 | } 12 | 13 | export interface DisplayValueType { 14 | key?: React.Key; 15 | value?: RawValueType; 16 | label?: React.ReactNode; 17 | title?: React.ReactNode; 18 | disabled?: boolean; 19 | index?: number; 20 | } 21 | 22 | export type RenderNode = React.ReactNode | ((props: any) => React.ReactNode); 23 | 24 | export type RenderDOMFunc = (props: any) => HTMLElement; 25 | 26 | export type Mode = 'multiple' | 'tags' | 'combobox'; 27 | 28 | export type Placement = 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight'; 29 | 30 | export type DisplayInfoType = 'add' | 'remove' | 'clear'; 31 | -------------------------------------------------------------------------------- /tests/Custom.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Select from '../src'; 3 | import { injectRunAllTimers, waitFakeTimer } from './utils/common'; 4 | import { fireEvent, render } from '@testing-library/react'; 5 | 6 | describe('Select.Custom', () => { 7 | injectRunAllTimers(jest); 8 | 9 | beforeEach(() => { 10 | jest.useFakeTimers(); 11 | }); 12 | 13 | afterEach(() => { 14 | jest.useRealTimers(); 15 | }); 16 | 17 | it('getRawInputElement', async () => { 18 | const onPopupVisibleChange = jest.fn(); 19 | const { container } = render( 20 | 35 | ); 36 | }; 37 | /* eslint-enable */ 38 | -------------------------------------------------------------------------------- /src/BaseSelect/Polite.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import type { DisplayValueType } from '.'; 3 | 4 | export interface PoliteProps { 5 | visible: boolean; 6 | values: DisplayValueType[]; 7 | } 8 | 9 | export default function Polite(props: PoliteProps) { 10 | const { visible, values } = props; 11 | 12 | if (!visible) { 13 | return null; 14 | } 15 | 16 | // Only cut part of values since it's a screen reader 17 | const MAX_COUNT = 50; 18 | 19 | return ( 20 | 24 | {/* Merge into one string to make screen reader work as expect */} 25 | {`${values 26 | .slice(0, MAX_COUNT) 27 | .map(({ label, value }) => (['number', 'string'].includes(typeof label) ? label : value)) 28 | .join(', ')}`} 29 | {values.length > MAX_COUNT ? ', ...' : null} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ant-design # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: ant-design # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /docs/examples/scroll-loading.tsx: -------------------------------------------------------------------------------- 1 | import Select from '@rc-component/select'; 2 | import React from 'react'; 3 | import '../../assets/index.less'; 4 | 5 | function genData(len: number) { 6 | return new Array(len).fill(0).map((_, index) => ({ 7 | label: `label ${index}`, 8 | value: index, 9 | })); 10 | } 11 | 12 | const Loading = ({ onLoad }) => { 13 | React.useEffect(() => { 14 | setTimeout(onLoad, 1000); 15 | }, []); 16 | 17 | return
Loading...
; 18 | }; 19 | 20 | export default () => { 21 | const [options, setOptions] = React.useState(() => genData(10)); 22 | 23 | return ( 24 | document.body} 23 | />, 24 | ); 25 | 26 | fireEvent.mouseDown(document.querySelector('.rc-select-dropdown')); 27 | expect(onPopupVisibleChange).not.toHaveBeenCalled(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: 'CodeQL' 2 | 3 | on: 4 | push: 5 | branches: ['master'] 6 | pull_request: 7 | branches: ['master'] 8 | schedule: 9 | - cron: '36 13 * * 3' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [javascript] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: '/language:${{ matrix.language }}' 42 | -------------------------------------------------------------------------------- /docs/examples/filterSort.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Select from '@rc-component/select'; 3 | import '../../assets/index.less'; 4 | 5 | const incidencesStateResource = [ 6 | { value: 4, label: 'Not Identified' }, 7 | { value: 3, label: 'Closed' }, 8 | { value: 2, label: 'Communicated' }, 9 | { value: 6, label: 'Identified' }, 10 | { value: 1, label: 'Resolved' }, 11 | { value: 5, label: 'Cancelled' }, 12 | ]; 13 | 14 | const sorterByLabel = (optionA, optionB) => optionA.label.localeCompare(optionB.label); 15 | 16 | const Test = () => ( 17 |
18 |

with filter sort

19 | 33 |
34 | ); 35 | 36 | export default Test; 37 | -------------------------------------------------------------------------------- /src/hooks/useLock.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | /** 4 | * Locker return cached mark. 5 | * If set to `true`, will return `true` in a short time even if set `false`. 6 | * If set to `false` and then set to `true`, will change to `true`. 7 | * And after time duration, it will back to `null` automatically. 8 | */ 9 | export default function useLock(duration: number = 250): [() => boolean, (lock: boolean) => void] { 10 | const lockRef = React.useRef(null); 11 | const timeoutRef = React.useRef(null); 12 | 13 | // Clean up 14 | React.useEffect( 15 | () => () => { 16 | window.clearTimeout(timeoutRef.current); 17 | }, 18 | [], 19 | ); 20 | 21 | function doLock(locked: boolean) { 22 | if (locked || lockRef.current === null) { 23 | lockRef.current = locked; 24 | } 25 | 26 | window.clearTimeout(timeoutRef.current); 27 | timeoutRef.current = window.setTimeout(() => { 28 | lockRef.current = null; 29 | }, duration); 30 | } 31 | 32 | return [() => lockRef.current, doLock]; 33 | } 34 | -------------------------------------------------------------------------------- /docs/examples/multiple-with-maxCount.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import React from 'react'; 3 | import Select from '@rc-component/select'; 4 | import '../../assets/index.less'; 5 | 6 | const Test: React.FC = () => { 7 | const [value, setValue] = React.useState(['1']); 8 | 9 | const onChange = (v: any) => { 10 | setValue(v); 11 | }; 12 | 13 | return ( 14 | <> 15 |

Multiple with maxCount

16 | { 17 | setValue(val); 18 | }} 19 | value={value} 20 | > 21 | 24 | 27 | 30 | 33 | 34 |
35 | {value} 36 | 37 | ); 38 | }; 39 | 40 | export default Test; 41 | -------------------------------------------------------------------------------- /docs/examples/focus.tsx: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useRef, useState } from 'react'; 2 | import type { BaseSelectRef } from '@rc-component/select'; 3 | import Select, { Option } from '@rc-component/select'; 4 | import '../../assets/index.less'; 5 | 6 | const MySelect = () => { 7 | const ref = useRef(null); 8 | useLayoutEffect(() => { 9 | if (ref.current) { 10 | ref.current.focus({ preventScroll: true }); 11 | } 12 | }, []); 13 | return ( 14 |
15 | 23 |
24 | ); 25 | }; 26 | 27 | const Demo = () => { 28 | const [open, setOpen] = useState(false); 29 | return ( 30 |
31 |
32 | setOpen(!open)}>{`${open}`} 33 |
34 | {open && } 35 |
36 |
37 | ); 38 | }; 39 | 40 | export default Demo; 41 | -------------------------------------------------------------------------------- /src/utils/commonUtil.ts: -------------------------------------------------------------------------------- 1 | import type { DisplayValueType } from '../BaseSelect'; 2 | 3 | export function toArray(value: T | T[]): T[] { 4 | if (Array.isArray(value)) { 5 | return value; 6 | } 7 | return value !== undefined ? [value] : []; 8 | } 9 | 10 | export const isClient = 11 | typeof window !== 'undefined' && window.document && window.document.documentElement; 12 | 13 | /** Is client side and not jsdom */ 14 | export const isBrowserClient = process.env.NODE_ENV !== 'test' && isClient; 15 | 16 | export function hasValue(value) { 17 | return value !== undefined && value !== null; 18 | } 19 | 20 | /** combo mode no value judgment function */ 21 | export function isComboNoValue(value) { 22 | return !value && value !== 0; 23 | } 24 | 25 | function isTitleType(title: any) { 26 | return ['string', 'number'].includes(typeof title); 27 | } 28 | 29 | export function getTitle(item: DisplayValueType): string { 30 | let title: string = undefined; 31 | if (item) { 32 | if (isTitleType(item.title)) { 33 | title = item.title.toString(); 34 | } else if (isTitleType(item.label)) { 35 | title = item.label.toString(); 36 | } 37 | } 38 | 39 | return title; 40 | } 41 | -------------------------------------------------------------------------------- /src/SelectInput/Content/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import pickAttrs from '@rc-component/util/lib/pickAttrs'; 3 | import SingleContent from './SingleContent'; 4 | import MultipleContent from './MultipleContent'; 5 | import { useSelectInputContext } from '../context'; 6 | import useBaseProps from '../../hooks/useBaseProps'; 7 | 8 | export interface SharedContentProps { 9 | inputProps: React.InputHTMLAttributes; 10 | } 11 | 12 | const SelectContent = React.forwardRef(function SelectContent(_, ref) { 13 | const { multiple, onInputKeyDown, tabIndex } = useSelectInputContext(); 14 | const baseProps = useBaseProps(); 15 | const { showSearch } = baseProps; 16 | 17 | const ariaProps = pickAttrs(baseProps, { aria: true }); 18 | 19 | const sharedInputProps: SharedContentProps['inputProps'] = { 20 | ...ariaProps, 21 | onKeyDown: onInputKeyDown, 22 | readOnly: !showSearch, 23 | tabIndex, 24 | }; 25 | 26 | if (multiple) { 27 | return ; 28 | } 29 | 30 | return ; 31 | }); 32 | 33 | export default SelectContent; 34 | -------------------------------------------------------------------------------- /tests/type.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Select from '../src'; 3 | 4 | describe('Select.typescript', () => { 5 | it('Select.items', () => { 6 | const select = ( 7 | 14 | ); 15 | 16 | expect(select).toBeTruthy(); 17 | }); 18 | 19 | it('Select.items Customizable ValueType', () => { 20 | const select = ( 21 | 22 | defaultValue="TEAM_1" 23 | showSearch 24 | style={{ width: 200 }} 25 | optionFilterProp="children" 26 | onSelect={(_, option) => { 27 | console.log(option); 28 | }} 29 | filterOption={(input, option) => 30 | (option && option.title.toLowerCase().indexOf(input.toLowerCase()) >= 0) ?? false 31 | } 32 | > 33 | 34 | Team 131 35 | 36 | )) 37 | 38 | ); 39 | 40 | expect(select).toBeTruthy(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/utils/keyUtil.ts: -------------------------------------------------------------------------------- 1 | import KeyCode from '@rc-component/util/lib/KeyCode'; 2 | 3 | /** keyCode Judgment function */ 4 | export function isValidateOpenKey(currentKeyCode: number): boolean { 5 | return ( 6 | // Undefined for Edge bug: 7 | // https://github.com/ant-design/ant-design/issues/51292 8 | currentKeyCode && 9 | // Other keys 10 | ![ 11 | // System function button 12 | KeyCode.ESC, 13 | KeyCode.SHIFT, 14 | KeyCode.BACKSPACE, 15 | KeyCode.TAB, 16 | KeyCode.WIN_KEY, 17 | KeyCode.ALT, 18 | KeyCode.META, 19 | KeyCode.WIN_KEY_RIGHT, 20 | KeyCode.CTRL, 21 | KeyCode.SEMICOLON, 22 | KeyCode.EQUALS, 23 | KeyCode.CAPS_LOCK, 24 | KeyCode.CONTEXT_MENU, 25 | // Arrow keys - should not trigger open when navigating in input 26 | KeyCode.UP, 27 | // KeyCode.DOWN, 28 | KeyCode.LEFT, 29 | KeyCode.RIGHT, 30 | // F1-F12 31 | KeyCode.F1, 32 | KeyCode.F2, 33 | KeyCode.F3, 34 | KeyCode.F4, 35 | KeyCode.F5, 36 | KeyCode.F6, 37 | KeyCode.F7, 38 | KeyCode.F8, 39 | KeyCode.F9, 40 | KeyCode.F10, 41 | KeyCode.F11, 42 | KeyCode.F12, 43 | ].includes(currentKeyCode) 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /docs/examples/single-animation.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import React from 'react'; 3 | import Select, { Option } from '@rc-component/select'; 4 | import '../../assets/index.less'; 5 | 6 | function onChange(value) { 7 | console.log(`selected ${value}`); 8 | } 9 | 10 | const Test = () => ( 11 |
12 |
13 |

Single Select

14 | 15 |
16 | 43 |
44 |
45 | ); 46 | 47 | export default Test; 48 | /* eslint-enable */ 49 | -------------------------------------------------------------------------------- /tests/ssr.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import { renderToString } from 'react-dom/server'; 3 | import Select from '../src'; 4 | import { injectRunAllTimers } from './utils/common'; 5 | 6 | // Mock @rc-component/util `canUseDom` 7 | jest.mock('@rc-component/util/lib/Dom/canUseDom', () => { 8 | return () => global.canUseDom; 9 | }); 10 | 11 | jest.mock('@rc-component/trigger', () => { 12 | return jest.requireActual('@rc-component/trigger'); 13 | }); 14 | 15 | describe('Select.SSR', () => { 16 | injectRunAllTimers(jest); 17 | 18 | beforeEach(() => { 19 | global.canUseDom = true; 20 | }); 21 | 22 | it('should work', () => { 23 | const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); 24 | 25 | global.canUseDom = false; 26 | 27 | const Demo = () => 44 | {options} 45 | 46 | ); 47 | } 48 | } 49 | 50 | export default Test; 51 | /* eslint-enable */ 52 | -------------------------------------------------------------------------------- /docs/examples/multiple-readonly.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import React from 'react'; 3 | import Select, { Option } from '@rc-component/select'; 4 | import '../../assets/index.less'; 5 | 6 | const children: React.ReactNode[] = []; 7 | 8 | for (let i = 10; i < 36; i += 1) { 9 | // 11 => readonly selected item 10 | children.push( 11 | , 14 | ); 15 | } 16 | 17 | const Test: React.FC = () => { 18 | const [value, setValue] = React.useState(['b11']); 19 | 20 | const onChange = (v: any) => { 21 | console.log('onChange', v); 22 | setValue(v); 23 | }; 24 | 25 | return ( 26 |
27 |

multiple readonly default selected item

28 |
29 | 42 |
43 |
44 | ); 45 | }; 46 | 47 | export default Test; 48 | /* eslint-enable */ 49 | -------------------------------------------------------------------------------- /tests/__snapshots__/Combobox.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Select.Combobox renders controlled correctly 1`] = ` 4 | 29 | `; 30 | 31 | exports[`Select.Combobox renders correctly 1`] = ` 32 | 57 | `; 58 | -------------------------------------------------------------------------------- /src/hooks/useSearchConfig.ts: -------------------------------------------------------------------------------- 1 | import type { SearchConfig, DefaultOptionType, SelectProps } from '../Select'; 2 | import * as React from 'react'; 3 | 4 | // Convert `showSearch` to unique config 5 | export default function useSearchConfig( 6 | showSearch: boolean | SearchConfig | undefined, 7 | props: SearchConfig, 8 | mode: SelectProps['mode'], 9 | ) { 10 | const { 11 | filterOption, 12 | searchValue, 13 | optionFilterProp, 14 | filterSort, 15 | onSearch, 16 | autoClearSearchValue, 17 | } = props; 18 | return React.useMemo<[boolean | undefined, SearchConfig]>(() => { 19 | const isObject = typeof showSearch === 'object'; 20 | const searchConfig = { 21 | filterOption, 22 | searchValue, 23 | optionFilterProp, 24 | filterSort, 25 | onSearch, 26 | autoClearSearchValue, 27 | ...(isObject ? showSearch : {}), 28 | }; 29 | 30 | return [ 31 | isObject || 32 | mode === 'combobox' || 33 | mode === 'tags' || 34 | (mode === 'multiple' && showSearch === undefined) 35 | ? true 36 | : showSearch, 37 | searchConfig, 38 | ]; 39 | }, [ 40 | mode, 41 | showSearch, 42 | filterOption, 43 | searchValue, 44 | optionFilterProp, 45 | filterSort, 46 | onSearch, 47 | autoClearSearchValue, 48 | ]); 49 | } 50 | -------------------------------------------------------------------------------- /tests/React.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Select, { type BaseSelectRef } from '../src'; 3 | import { injectRunAllTimers } from './utils/common'; 4 | import { render } from '@testing-library/react'; 5 | 6 | describe('React', () => { 7 | injectRunAllTimers(jest); 8 | 9 | beforeEach(() => { 10 | jest.useFakeTimers(); 11 | }); 12 | 13 | afterEach(() => { 14 | jest.useRealTimers(); 15 | }); 16 | 17 | it('not warning findDOMNode', () => { 18 | const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); 19 | 20 | render(); 30 | 31 | expect(selectRef.current?.nativeElement).toBe(container.querySelector('.rc-select')); 32 | }); 33 | 34 | it('getRawInputElement', () => { 35 | const selectRef = React.createRef(); 36 | const { container } = render( 37 | 48 |
49 |
50 | ); 51 | 52 | export default Test; 53 | /* eslint-enable */ 54 | -------------------------------------------------------------------------------- /src/SelectContext.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import type { RawValueType, RenderNode } from './BaseSelect'; 3 | import type { 4 | BaseOptionType, 5 | FieldNames, 6 | OnActiveValue, 7 | OnInternalSelect, 8 | SelectProps, 9 | SemanticName, 10 | PopupSemantic, 11 | } from './Select'; 12 | import type { FlattenOptionData } from './interface'; 13 | 14 | // Use any here since we do not get the type during compilation 15 | /** 16 | * SelectContext is only used for Select. BaseSelect should not consume this context. 17 | */ 18 | export interface SelectContextProps { 19 | classNames?: Partial> & { 20 | popup?: Partial>; 21 | }; 22 | styles?: Partial> & { 23 | popup?: Partial>; 24 | }; 25 | options: BaseOptionType[]; 26 | optionRender?: SelectProps['optionRender']; 27 | flattenOptions: FlattenOptionData[]; 28 | onActiveValue: OnActiveValue; 29 | defaultActiveFirstOption?: boolean; 30 | onSelect: OnInternalSelect; 31 | menuItemSelectedIcon?: RenderNode; 32 | rawValues: Set; 33 | fieldNames?: FieldNames; 34 | virtual?: boolean; 35 | direction?: 'ltr' | 'rtl'; 36 | listHeight?: number; 37 | listItemHeight?: number; 38 | childrenAsData?: boolean; 39 | maxCount?: number; 40 | } 41 | 42 | const SelectContext = React.createContext(null); 43 | 44 | export default SelectContext; 45 | -------------------------------------------------------------------------------- /src/utils/legacyUtil.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import toArray from '@rc-component/util/lib/Children/toArray'; 3 | import type { BaseOptionType, DefaultOptionType } from '../Select'; 4 | 5 | function convertNodeToOption( 6 | node: React.ReactElement, 7 | ): OptionType { 8 | const { 9 | key, 10 | props: { children, value, ...restProps }, 11 | } = node as React.ReactElement; 12 | 13 | return { key, value: value !== undefined ? value : key, children, ...restProps }; 14 | } 15 | 16 | export function convertChildrenToData( 17 | nodes: React.ReactNode, 18 | optionOnly: boolean = false, 19 | ): OptionType[] { 20 | return toArray(nodes) 21 | .map((node: React.ReactElement, index: number): OptionType | null => { 22 | if (!React.isValidElement(node) || !node.type) { 23 | return null; 24 | } 25 | 26 | const { 27 | type: { isSelectOptGroup }, 28 | key, 29 | props: { children, ...restProps }, 30 | } = node as React.ReactElement & { type: { isSelectOptGroup?: boolean } }; 31 | 32 | if (optionOnly || !isSelectOptGroup) { 33 | return convertNodeToOption(node); 34 | } 35 | 36 | return { 37 | key: `__RC_SELECT_GRP__${key === null ? index : key}__`, 38 | label: key, 39 | ...restProps, 40 | options: convertChildrenToData(children), 41 | }; 42 | }) 43 | .filter((data) => data); 44 | } 45 | -------------------------------------------------------------------------------- /tests/components.test.tsx: -------------------------------------------------------------------------------- 1 | import { createEvent, fireEvent, render } from '@testing-library/react'; 2 | import React from 'react'; 3 | import Select from '../src'; 4 | import { injectRunAllTimers } from './utils/common'; 5 | 6 | describe('Select.Components', () => { 7 | injectRunAllTimers(jest); 8 | 9 | beforeEach(() => { 10 | jest.useFakeTimers(); 11 | }); 12 | 13 | afterEach(() => { 14 | jest.clearAllTimers(); 15 | jest.useRealTimers(); 16 | }); 17 | 18 | it('should pass placeholder to custom input', () => { 19 | const { container } = render( 20 |