From 2d61d983aab32e33b0e085a7aaf570fed8b6d712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 9 Oct 2025 16:27:46 +0800 Subject: [PATCH 01/72] chore: init --- assets/index.less | 1 + assets/patch.less | 6 +++++ src/BaseSelect/index.tsx | 52 +++++++++++++++++++++++---------------- src/SelectInput/Affix.tsx | 17 +++++++++++++ src/SelectInput/index.tsx | 25 +++++++++++++++++++ 5 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 assets/patch.less create mode 100644 src/SelectInput/Affix.tsx create mode 100644 src/SelectInput/index.tsx diff --git a/assets/index.less b/assets/index.less index e8ba28bac..982871c93 100644 --- a/assets/index.less +++ b/assets/index.less @@ -1,4 +1,5 @@ @select-prefix: ~'rc-select'; +@import url('./patch.less'); * { box-sizing: border-box; diff --git a/assets/patch.less b/assets/patch.less new file mode 100644 index 000000000..35b50e330 --- /dev/null +++ b/assets/patch.less @@ -0,0 +1,6 @@ +// This is used for semantic refactoring +@import (reference) url('./index.less'); + +.@{select-prefix}.@{select-prefix} { + display: flex; +} diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 286f2a078..d4a37a52f 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -29,6 +29,7 @@ import SelectTrigger from '../SelectTrigger'; import TransBtn from '../TransBtn'; import { getSeparatedContent, isValidCount } from '../utils/valueUtil'; import Polite from './Polite'; +import SelectInput from '@/SelectInput'; export type BaseSelectSemanticName = 'prefix' | 'suffix' | 'input'; export type { @@ -850,27 +851,36 @@ const BaseSelect = React.forwardRef((props, ref) let renderNode: React.ReactNode; // Render raw - if (customizeRawInputElement) { - renderNode = selectorNode; - } else { - renderNode = ( -
- - {selectorNode} - {arrowNode} - {mergedAllowClear && clearNode} -
- ); - } + // if (customizeRawInputElement) { + // renderNode = selectorNode; + // } else { + // renderNode = ( + //
+ // + // {selectorNode} + // {arrowNode} + // {mergedAllowClear && clearNode} + //
+ // ); + // } + + renderNode = ( + + ); return ( {renderNode} diff --git a/src/SelectInput/Affix.tsx b/src/SelectInput/Affix.tsx new file mode 100644 index 000000000..8d91eb51d --- /dev/null +++ b/src/SelectInput/Affix.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; + +export interface AffixProps { + prefixCls: string; + type: 'prefix' | 'suffix'; + children?: React.ReactNode; +} + +export default function Affix(props: AffixProps) { + const { prefixCls, type, children } = props; + + if (!children) { + return null; + } + + return
123
; +} diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx new file mode 100644 index 000000000..cb5901244 --- /dev/null +++ b/src/SelectInput/index.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import Affix from './Affix'; + +export interface SelectInputProps { + prefixCls: string; + // Add other props that need to be passed through + className?: string; + style?: React.CSSProperties; + [key: string]: any; +} + +export default function SelectInput(props: SelectInputProps) { + const { prefixCls, className, style, ...restProps } = props; + + return ( +
+ {/* Prefix */} + + 2333 + {/* Suffix */} + +
+ ); +} From 597ecab0a64fa36365044e7e4d6f942a8f6f1fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 9 Oct 2025 18:06:40 +0800 Subject: [PATCH 02/72] of it --- assets/patch.less | 15 +++++- examples/InputExample.tsx | 49 +++++++++++++++++++ src/BaseSelect/index.tsx | 27 +++++++++-- src/SelectInput/Affix.tsx | 4 +- src/SelectInput/Content/MultipleContent.tsx | 18 +++++++ src/SelectInput/Content/SingleContent.tsx | 16 ++++++ src/SelectInput/Content/index.tsx | 25 ++++++++++ src/SelectInput/Input.tsx | 26 ++++++++++ src/SelectInput/index.tsx | 28 +++++++++-- src/hooks/useComponents.ts | 18 +++++++ tests/SelectInput/Input.test.tsx | 53 ++++++++++++++++++++ tests/hooks/useComponents.test.tsx | 54 +++++++++++++++++++++ 12 files changed, 322 insertions(+), 11 deletions(-) create mode 100644 examples/InputExample.tsx create mode 100644 src/SelectInput/Content/MultipleContent.tsx create mode 100644 src/SelectInput/Content/SingleContent.tsx create mode 100644 src/SelectInput/Content/index.tsx create mode 100644 src/SelectInput/Input.tsx create mode 100644 src/hooks/useComponents.ts create mode 100644 tests/SelectInput/Input.test.tsx create mode 100644 tests/hooks/useComponents.test.tsx diff --git a/assets/patch.less b/assets/patch.less index 35b50e330..d3abac578 100644 --- a/assets/patch.less +++ b/assets/patch.less @@ -2,5 +2,18 @@ @import (reference) url('./index.less'); .@{select-prefix}.@{select-prefix} { - display: flex; + display: inline-flex; + align-items: center; + + // Content 部分自动占据剩余宽度 + .@{select-prefix}-content { + flex: auto; + } + + // 其他部分禁止自动宽度,使用内容宽度 + .@{select-prefix}-prefix, + .@{select-prefix}-suffix, + .@{select-prefix}-clear { + flex: none; + } } diff --git a/examples/InputExample.tsx b/examples/InputExample.tsx new file mode 100644 index 000000000..b1bb6436c --- /dev/null +++ b/examples/InputExample.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import Input from '../src/SelectInput/Input'; + +const InputExample = () => { + const [value, setValue] = React.useState(''); + + return ( +
+

Input Component Examples

+ +
+

Basic Input

+ +
+ +
+

Controlled Input

+ setValue(e.target.value)} + placeholder="Controlled input..." + /> +

Current value: {value}

+
+ +
+

Disabled Input

+ +
+ +
+

Input with Custom Style

+ +
+
+ ); +}; + +export default InputExample; diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index d4a37a52f..1cae67171 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -13,6 +13,7 @@ import type { BaseSelectContextProps } from '../hooks/useBaseProps'; import useDelayReset from '../hooks/useDelayReset'; import useLock from '../hooks/useLock'; import useSelectTriggerControl from '../hooks/useSelectTriggerControl'; +import useComponents, { type ComponentsConfig } from '../hooks/useComponents'; import type { DisplayInfoType, DisplayValueType, @@ -32,6 +33,14 @@ import Polite from './Polite'; import SelectInput from '@/SelectInput'; export type BaseSelectSemanticName = 'prefix' | 'suffix' | 'input'; +/** + * ZombieJ: + * We are currently refactoring the semantic structure of the component. Changelog: + * - Remove `suffixIcon` and change to `suffix`. + * - Add `components.root` for replacing response element. + * - Remove `getInputElement` and `getRawInputElement` since we can use `components.input` instead. + */ + export type { DisplayInfoType, DisplayValueType, @@ -183,7 +192,7 @@ export interface BaseSelectProps extends BaseSelectPrivateProps, React.AriaAttri // >>> Icons allowClear?: boolean | { clearIcon?: RenderNode }; prefix?: React.ReactNode; - suffixIcon?: RenderNode; + suffix?: React.ReactNode; /** * Clear all icon * @deprecated Please use `allowClear` instead @@ -220,6 +229,9 @@ export interface BaseSelectProps extends BaseSelectPrivateProps, React.AriaAttri onMouseEnter?: React.MouseEventHandler; onMouseLeave?: React.MouseEventHandler; onClick?: React.MouseEventHandler; + + // >>> Components + components?: ComponentsConfig; } export const isMultiple = (mode: Mode) => mode === 'tags' || mode === 'multiple'; @@ -277,6 +289,7 @@ const BaseSelect = React.forwardRef((props, ref) allowClear, prefix, suffixIcon, + suffix, clearIcon, // Dropdown @@ -302,6 +315,9 @@ const BaseSelect = React.forwardRef((props, ref) onKeyDown, onMouseDown, + // Components + components, + // Rest Props ...restProps } = props; @@ -781,7 +797,7 @@ const BaseSelect = React.forwardRef((props, ref) [`${prefixCls}-focused`]: mockFocused, [`${prefixCls}-multiple`]: multiple, [`${prefixCls}-single`]: !multiple, - [`${prefixCls}-allow-clear`]: allowClear, + [`${prefixCls}-allow-clear`]: mergedAllowClear, [`${prefixCls}-show-arrow`]: showSuffixIcon, [`${prefixCls}-disabled`]: disabled, [`${prefixCls}-loading`]: loading, @@ -873,12 +889,17 @@ const BaseSelect = React.forwardRef((props, ref) // ); // } + const { root: RootComponent } = useComponents(components); renderNode = ( - ); diff --git a/src/SelectInput/Affix.tsx b/src/SelectInput/Affix.tsx index 8d91eb51d..501733179 100644 --- a/src/SelectInput/Affix.tsx +++ b/src/SelectInput/Affix.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; export interface AffixProps { prefixCls: string; - type: 'prefix' | 'suffix'; + type: 'prefix' | 'suffix' | 'clear'; children?: React.ReactNode; } @@ -13,5 +13,5 @@ export default function Affix(props: AffixProps) { return null; } - return
123
; + return
{children}
; } diff --git a/src/SelectInput/Content/MultipleContent.tsx b/src/SelectInput/Content/MultipleContent.tsx new file mode 100644 index 000000000..a5cf60d5f --- /dev/null +++ b/src/SelectInput/Content/MultipleContent.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import Input from '../Input'; + +// NOTO: do not modify this file since it's just a placeholder for future use + +export interface MultipleContentProps { + prefixCls: string; +} + +export default function MultipleContent(props: MultipleContentProps) { + const { prefixCls } = props; + + return ( +
+ +
+ ); +} diff --git a/src/SelectInput/Content/SingleContent.tsx b/src/SelectInput/Content/SingleContent.tsx new file mode 100644 index 000000000..b757492ad --- /dev/null +++ b/src/SelectInput/Content/SingleContent.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import Input from '../Input'; + +export interface SingleContentProps { + prefixCls: string; +} + +export default function SingleContent(props: SingleContentProps) { + const { prefixCls } = props; + + return ( +
+ +
+ ); +} diff --git a/src/SelectInput/Content/index.tsx b/src/SelectInput/Content/index.tsx new file mode 100644 index 000000000..855c31ab3 --- /dev/null +++ b/src/SelectInput/Content/index.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import SingleContent from './SingleContent'; +import MultipleContent from './MultipleContent'; +import type { DisplayValueType } from '../../interface'; + +export interface SelectContentProps { + prefixCls: string; + multiple?: boolean; +} + +export interface SharedContentProps { + prefixCls: string; + value: DisplayValueType[]; +} + +export default function SelectContent(props: SelectContentProps) { + const { prefixCls, multiple } = props; + const sharedProps: SharedContentProps = { prefixCls }; + + if (multiple) { + return ; + } + + return ; +} diff --git a/src/SelectInput/Input.tsx b/src/SelectInput/Input.tsx new file mode 100644 index 000000000..2a7df20b0 --- /dev/null +++ b/src/SelectInput/Input.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; + +export interface InputProps { + prefixCls: string; + disabled?: boolean; + readOnly?: boolean; + value?: string; + onChange?: React.ChangeEventHandler; + onKeyDown?: React.KeyboardEventHandler; + onFocus?: React.FocusEventHandler; + onBlur?: React.FocusEventHandler; + placeholder?: string; + className?: string; + style?: React.CSSProperties; + [key: string]: any; +} + +const Input = React.forwardRef((props, ref) => { + const { prefixCls, ...restProps } = props; + + const inputCls = `${prefixCls}-input`; + + return ; +}); + +export default Input; diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index cb5901244..6bfb1e339 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -1,9 +1,14 @@ import * as React from 'react'; import clsx from 'clsx'; import Affix from './Affix'; +import SelectContent from './Content'; export interface SelectInputProps { prefixCls: string; + prefix?: React.ReactNode; + suffix?: React.ReactNode; + clearIcon?: React.ReactNode; + multiple?: boolean; // Add other props that need to be passed through className?: string; style?: React.CSSProperties; @@ -11,15 +16,28 @@ export interface SelectInputProps { } export default function SelectInput(props: SelectInputProps) { - const { prefixCls, className, style, ...restProps } = props; + const { prefixCls, prefix, suffix, clearIcon, multiple, className, style, ...restProps } = props; return ( -
+
{/* Prefix */} - - 2333 + + {prefix} + + + {/* Content */} + + {/* Suffix */} - + + {suffix} + + {/* Clear Icon */} + {clearIcon && ( + + {clearIcon} + + )}
); } diff --git a/src/hooks/useComponents.ts b/src/hooks/useComponents.ts new file mode 100644 index 000000000..2a4f0c960 --- /dev/null +++ b/src/hooks/useComponents.ts @@ -0,0 +1,18 @@ +import * as React from 'react'; +import SelectInput, { type SelectInputProps } from '../SelectInput'; + +export interface ComponentsConfig { + root?: React.ComponentType; +} + +export interface ReturnType { + root: React.ComponentType; +} + +export default function useComponents(components?: ComponentsConfig): ReturnType { + return React.useMemo(() => { + const { root: RootComponent = SelectInput } = components || {}; + + return { root: RootComponent }; + }, [components]); +} diff --git a/tests/SelectInput/Input.test.tsx b/tests/SelectInput/Input.test.tsx new file mode 100644 index 000000000..1284ca30c --- /dev/null +++ b/tests/SelectInput/Input.test.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import Input from '../../src/SelectInput/Input'; + +describe('Input', () => { + it('should render correctly', () => { + const { container } = render(); + expect(container.querySelector('input')).toBeTruthy(); + }); + + it('should handle value changes', () => { + const handleChange = jest.fn(); + const { container } = render( + + ); + const input = container.querySelector('input'); + + fireEvent.change(input!, { target: { value: 'new value' } }); + expect(handleChange).toHaveBeenCalled(); + }); + + it('should apply className and style props', () => { + const { container } = render( + + ); + const input = container.querySelector('input'); + + expect(input!.classList.contains('custom-class')).toBeTruthy(); + expect(input!.style.color).toBe('red'); + }); + + it('should handle placeholder', () => { + const { container } = render( + + ); + const input = container.querySelector('input'); + + expect(input!.getAttribute('placeholder')).toBe('Enter text'); + }); + + it('should handle disabled state', () => { + const { container } = render( + + ); + const input = container.querySelector('input'); + + expect(input!.disabled).toBeTruthy(); + }); +}); diff --git a/tests/hooks/useComponents.test.tsx b/tests/hooks/useComponents.test.tsx new file mode 100644 index 000000000..48538d444 --- /dev/null +++ b/tests/hooks/useComponents.test.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import useComponents from '../../src/hooks/useComponents'; +import SelectInput from '../../src/SelectInput'; + +describe('useComponents', () => { + it('should return SelectInput as default component', () => { + const TestComponent = () => { + const { Component } = useComponents(); + return ; + }; + + const { container } = render(); + expect(container.querySelector('.test')).toBeTruthy(); + }); + + it('should return custom component when provided', () => { + const CustomComponent = () =>
Custom
; + + const TestComponent = () => { + const { Component } = useComponents({ root: CustomComponent }); + return ; + }; + + const { container } = render(); + expect(container.querySelector('.custom-component')).toBeTruthy(); + }); + + it('should memoize the component', () => { + let renderCount = 0; + + const CustomComponent = () => { + renderCount++; + return
Custom
; + }; + + const TestComponent = (props: { components?: any }) => { + const { Component } = useComponents(props.components); + return ; + }; + + const { rerender } = render(); + expect(renderCount).toBe(1); + + // Re-render with same component should not re-create + rerender(); + expect(renderCount).toBe(1); + + // Re-render with different component should re-create + const AnotherComponent = () =>
Another
; + rerender(); + expect(renderCount).toBe(1); // Still 1 because we're using a different component + }); +}); From f1e9ff94c7e42ee6fd743e0bc41e0127c098b165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 10 Oct 2025 11:14:23 +0800 Subject: [PATCH 03/72] chore: of it --- examples/SelectContentExample.tsx | 43 +++++++++++++ src/BaseSelect/index.tsx | 1 + src/SelectInput/Content/MultipleContent.tsx | 13 ++-- src/SelectInput/Content/Placeholder.tsx | 0 src/SelectInput/Content/SingleContent.tsx | 9 ++- src/SelectInput/Content/index.tsx | 5 +- src/SelectInput/context.ts | 17 +++++ src/SelectInput/index.tsx | 53 ++++++++++------ tests/SelectInput/Content.test.tsx | 70 +++++++++++++++++++++ 9 files changed, 185 insertions(+), 26 deletions(-) create mode 100644 examples/SelectContentExample.tsx create mode 100644 src/SelectInput/Content/Placeholder.tsx create mode 100644 src/SelectInput/context.ts create mode 100644 tests/SelectInput/Content.test.tsx diff --git a/examples/SelectContentExample.tsx b/examples/SelectContentExample.tsx new file mode 100644 index 000000000..9523f6d62 --- /dev/null +++ b/examples/SelectContentExample.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import SelectContent from '../src/SelectInput/Content'; +import type { DisplayValueType } from '../src/interface'; + +const SelectContentExample = () => { + const singleValue: DisplayValueType[] = [{ value: 'option1', label: 'Option 1' }]; + + const multipleValues: DisplayValueType[] = [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + { value: 'option3', label: 'Option 3' }, + ]; + + const emptyValues: DisplayValueType[] = []; + + return ( +
+

SelectContent Value Logic Examples

+ +
+

Single Mode with Value

+ +
+ +
+

Multiple Mode with Values

+ +
+ +
+

Single Mode with Empty Values

+ +
+ +
+

Multiple Mode with Empty Values

+ +
+
+ ); +}; + +export default SelectContentExample; diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 1cae67171..f1ccf153f 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -900,6 +900,7 @@ const BaseSelect = React.forwardRef((props, ref) suffix={suffix} clearIcon={clearNode} multiple={multiple} + displayValues={displayValues} /> ); diff --git a/src/SelectInput/Content/MultipleContent.tsx b/src/SelectInput/Content/MultipleContent.tsx index a5cf60d5f..24bab606e 100644 --- a/src/SelectInput/Content/MultipleContent.tsx +++ b/src/SelectInput/Content/MultipleContent.tsx @@ -1,18 +1,23 @@ import * as React from 'react'; import Input from '../Input'; - -// NOTO: do not modify this file since it's just a placeholder for future use +import type { DisplayValueType } from '../../../interface'; export interface MultipleContentProps { prefixCls: string; + value: DisplayValueType[]; } export default function MultipleContent(props: MultipleContentProps) { - const { prefixCls } = props; + const { prefixCls, value } = props; + + // For multiple mode, we show all values as a comma-separated string + const displayValue = value + .map((v) => v.label?.toString() || v.value?.toString() || '') + .join(', '); return (
- +
); } diff --git a/src/SelectInput/Content/Placeholder.tsx b/src/SelectInput/Content/Placeholder.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/src/SelectInput/Content/SingleContent.tsx b/src/SelectInput/Content/SingleContent.tsx index b757492ad..de694516a 100644 --- a/src/SelectInput/Content/SingleContent.tsx +++ b/src/SelectInput/Content/SingleContent.tsx @@ -1,16 +1,21 @@ import * as React from 'react'; import Input from '../Input'; +import type { DisplayValueType } from '../../interface'; export interface SingleContentProps { prefixCls: string; + value: DisplayValueType[]; } export default function SingleContent(props: SingleContentProps) { - const { prefixCls } = props; + const { prefixCls, value } = props; + + // For single mode, we only show the first value + const displayValue = value[0]?.label?.toString() || value[0]?.value?.toString() || ''; return (
- +
); } diff --git a/src/SelectInput/Content/index.tsx b/src/SelectInput/Content/index.tsx index 855c31ab3..92596fea7 100644 --- a/src/SelectInput/Content/index.tsx +++ b/src/SelectInput/Content/index.tsx @@ -6,6 +6,7 @@ import type { DisplayValueType } from '../../interface'; export interface SelectContentProps { prefixCls: string; multiple?: boolean; + value: DisplayValueType[]; } export interface SharedContentProps { @@ -14,8 +15,8 @@ export interface SharedContentProps { } export default function SelectContent(props: SelectContentProps) { - const { prefixCls, multiple } = props; - const sharedProps: SharedContentProps = { prefixCls }; + const { prefixCls, multiple, value } = props; + const sharedProps: SharedContentProps = { prefixCls, value }; if (multiple) { return ; diff --git a/src/SelectInput/context.ts b/src/SelectInput/context.ts new file mode 100644 index 000000000..249f1d562 --- /dev/null +++ b/src/SelectInput/context.ts @@ -0,0 +1,17 @@ +import type { DisplayValueType } from '../interface'; +import * as React from 'react'; + +// TODO: 把其他直接传导下列 props 的子组件全部都改成 useSelectInputContext 取值 +export interface ContentContextProps { + prefixCls: string; + multiple: boolean; + displayValues: DisplayValueType[]; +} + +const SelectInputContext = React.createContext(null!); + +export function useSelectInputContext() { + return React.useContext(SelectInputContext); +} + +export default SelectInputContext; diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index 6bfb1e339..32410a91d 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -2,6 +2,8 @@ import * as React from 'react'; import clsx from 'clsx'; import Affix from './Affix'; import SelectContent from './Content'; +import SelectInputContext from './context'; +import type { DisplayValueType } from '../interface'; export interface SelectInputProps { prefixCls: string; @@ -9,6 +11,7 @@ export interface SelectInputProps { suffix?: React.ReactNode; clearIcon?: React.ReactNode; multiple?: boolean; + displayValues: DisplayValueType[]; // Add other props that need to be passed through className?: string; style?: React.CSSProperties; @@ -16,28 +19,42 @@ export interface SelectInputProps { } export default function SelectInput(props: SelectInputProps) { - const { prefixCls, prefix, suffix, clearIcon, multiple, className, style, ...restProps } = props; + const { + prefixCls, + prefix, + suffix, + clearIcon, + multiple, + displayValues, + className, + style, + ...restProps + } = props; + + const cachedContext = React.useMemo(() => ({ prefixCls }), [prefixCls]); return ( -
- {/* Prefix */} - - {prefix} - + +
+ {/* Prefix */} + + {prefix} + - {/* Content */} - + {/* Content */} + - {/* Suffix */} - - {suffix} - - {/* Clear Icon */} - {clearIcon && ( - - {clearIcon} + {/* Suffix */} + + {suffix} - )} -
+ {/* Clear Icon */} + {clearIcon && ( + + {clearIcon} + + )} +
+ ); } diff --git a/tests/SelectInput/Content.test.tsx b/tests/SelectInput/Content.test.tsx new file mode 100644 index 000000000..5fa29dab5 --- /dev/null +++ b/tests/SelectInput/Content.test.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import SelectContent from '../../src/SelectInput/Content'; + +describe('SelectContent', () => { + it('should render SingleContent for single mode', () => { + const { container } = render( + , + ); + + expect(container.querySelector('.rc-select-content')).toBeTruthy(); + }); + + it('should render MultipleContent for multiple mode', () => { + const { container } = render( + , + ); + + expect(container.querySelector('.rc-select-content')).toBeTruthy(); + }); + + it('should pass value to SingleContent', () => { + const { container } = render( + , + ); + + const input = container.querySelector('input'); + expect(input?.value).toBe('Test Label'); + }); + + it('should pass value to MultipleContent', () => { + const { container } = render( + , + ); + + const input = container.querySelector('input'); + expect(input?.value).toBe('Test 1, Test 2'); + }); + + it('should handle empty values', () => { + const { container } = render( + , + ); + + const input = container.querySelector('input'); + expect(input?.value).toBe(''); + }); +}); From 4a6925f8601bbfe76d820354b17e39584ef0c8e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 10 Oct 2025 11:22:46 +0800 Subject: [PATCH 04/72] chore: fill content --- src/BaseSelect/index.tsx | 1 + src/SelectInput/Affix.tsx | 5 ++-- src/SelectInput/Content/MultipleContent.tsx | 13 +++------- src/SelectInput/Content/Placeholder.tsx | 8 ++++++ src/SelectInput/Content/SingleContent.tsx | 15 +++++------ src/SelectInput/Content/index.tsx | 22 ++++------------ src/SelectInput/Input.tsx | 5 ++-- src/SelectInput/context.ts | 2 +- src/SelectInput/index.tsx | 28 +++++++++++---------- 9 files changed, 46 insertions(+), 53 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index f1ccf153f..e2c50bb8f 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -901,6 +901,7 @@ const BaseSelect = React.forwardRef((props, ref) clearIcon={clearNode} multiple={multiple} displayValues={displayValues} + placeholder={placeholder} /> ); diff --git a/src/SelectInput/Affix.tsx b/src/SelectInput/Affix.tsx index 501733179..aaff3ecaa 100644 --- a/src/SelectInput/Affix.tsx +++ b/src/SelectInput/Affix.tsx @@ -1,13 +1,14 @@ import * as React from 'react'; +import { useSelectInputContext } from './context'; export interface AffixProps { - prefixCls: string; type: 'prefix' | 'suffix' | 'clear'; children?: React.ReactNode; } export default function Affix(props: AffixProps) { - const { prefixCls, type, children } = props; + const { type, children } = props; + const { prefixCls } = useSelectInputContext(); if (!children) { return null; diff --git a/src/SelectInput/Content/MultipleContent.tsx b/src/SelectInput/Content/MultipleContent.tsx index 24bab606e..a5dec40e6 100644 --- a/src/SelectInput/Content/MultipleContent.tsx +++ b/src/SelectInput/Content/MultipleContent.tsx @@ -1,14 +1,9 @@ import * as React from 'react'; import Input from '../Input'; -import type { DisplayValueType } from '../../../interface'; +import { useSelectInputContext } from '../context'; -export interface MultipleContentProps { - prefixCls: string; - value: DisplayValueType[]; -} - -export default function MultipleContent(props: MultipleContentProps) { - const { prefixCls, value } = props; +export default function MultipleContent() { + const { prefixCls, displayValues: value } = useSelectInputContext(); // For multiple mode, we show all values as a comma-separated string const displayValue = value @@ -17,7 +12,7 @@ export default function MultipleContent(props: MultipleContentProps) { return (
- +
); } diff --git a/src/SelectInput/Content/Placeholder.tsx b/src/SelectInput/Content/Placeholder.tsx index e69de29bb..b7f101da0 100644 --- a/src/SelectInput/Content/Placeholder.tsx +++ b/src/SelectInput/Content/Placeholder.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { useSelectInputContext } from '../context'; + +export default function Placeholder() { + const { prefixCls, placeholder } = useSelectInputContext(); + + return
{placeholder}
; +} diff --git a/src/SelectInput/Content/SingleContent.tsx b/src/SelectInput/Content/SingleContent.tsx index de694516a..9951855d3 100644 --- a/src/SelectInput/Content/SingleContent.tsx +++ b/src/SelectInput/Content/SingleContent.tsx @@ -1,21 +1,18 @@ import * as React from 'react'; import Input from '../Input'; -import type { DisplayValueType } from '../../interface'; +import { useSelectInputContext } from '../context'; +import Placeholder from './Placeholder'; -export interface SingleContentProps { - prefixCls: string; - value: DisplayValueType[]; -} - -export default function SingleContent(props: SingleContentProps) { - const { prefixCls, value } = props; +export default function SingleContent() { + const { prefixCls, displayValues: value } = useSelectInputContext(); // For single mode, we only show the first value const displayValue = value[0]?.label?.toString() || value[0]?.value?.toString() || ''; return (
- + +
); } diff --git a/src/SelectInput/Content/index.tsx b/src/SelectInput/Content/index.tsx index 92596fea7..044ddf725 100644 --- a/src/SelectInput/Content/index.tsx +++ b/src/SelectInput/Content/index.tsx @@ -1,26 +1,14 @@ import * as React from 'react'; import SingleContent from './SingleContent'; import MultipleContent from './MultipleContent'; -import type { DisplayValueType } from '../../interface'; +import { useSelectInputContext } from '../context'; -export interface SelectContentProps { - prefixCls: string; - multiple?: boolean; - value: DisplayValueType[]; -} - -export interface SharedContentProps { - prefixCls: string; - value: DisplayValueType[]; -} - -export default function SelectContent(props: SelectContentProps) { - const { prefixCls, multiple, value } = props; - const sharedProps: SharedContentProps = { prefixCls, value }; +export default function SelectContent() { + const { multiple } = useSelectInputContext(); if (multiple) { - return ; + return ; } - return ; + return ; } diff --git a/src/SelectInput/Input.tsx b/src/SelectInput/Input.tsx index 2a7df20b0..c2b9b9c3b 100644 --- a/src/SelectInput/Input.tsx +++ b/src/SelectInput/Input.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; +import { useSelectInputContext } from './context'; export interface InputProps { - prefixCls: string; disabled?: boolean; readOnly?: boolean; value?: string; @@ -16,7 +16,8 @@ export interface InputProps { } const Input = React.forwardRef((props, ref) => { - const { prefixCls, ...restProps } = props; + const { ...restProps } = props; + const { prefixCls } = useSelectInputContext(); const inputCls = `${prefixCls}-input`; diff --git a/src/SelectInput/context.ts b/src/SelectInput/context.ts index 249f1d562..052c8c84b 100644 --- a/src/SelectInput/context.ts +++ b/src/SelectInput/context.ts @@ -1,11 +1,11 @@ import type { DisplayValueType } from '../interface'; import * as React from 'react'; -// TODO: 把其他直接传导下列 props 的子组件全部都改成 useSelectInputContext 取值 export interface ContentContextProps { prefixCls: string; multiple: boolean; displayValues: DisplayValueType[]; + placeholder?: React.ReactNode; } const SelectInputContext = React.createContext(null!); diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index 32410a91d..9d8d9c239 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -12,6 +12,7 @@ export interface SelectInputProps { clearIcon?: React.ReactNode; multiple?: boolean; displayValues: DisplayValueType[]; + placeholder?: React.ReactNode; // Add other props that need to be passed through className?: string; style?: React.CSSProperties; @@ -26,34 +27,35 @@ export default function SelectInput(props: SelectInputProps) { clearIcon, multiple, displayValues, + placeholder, className, style, ...restProps } = props; - const cachedContext = React.useMemo(() => ({ prefixCls }), [prefixCls]); + const cachedContext = React.useMemo( + () => ({ + prefixCls, + multiple: !!multiple, + displayValues, + placeholder, + }), + [prefixCls, multiple, displayValues, placeholder], + ); return (
{/* Prefix */} - - {prefix} - + {prefix} {/* Content */} - + {/* Suffix */} - - {suffix} - + {suffix} {/* Clear Icon */} - {clearIcon && ( - - {clearIcon} - - )} + {clearIcon && {clearIcon}}
); From 2477dafc9d0f6cfbe44d75ec6cf9af767e0d9a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 10 Oct 2025 11:38:36 +0800 Subject: [PATCH 05/72] chore: style --- assets/patch.less | 16 ++++++++++++++++ src/BaseSelect/index.tsx | 1 + src/SelectInput/Content/Placeholder.tsx | 6 +++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/assets/patch.less b/assets/patch.less index d3abac578..77e98fe4b 100644 --- a/assets/patch.less +++ b/assets/patch.less @@ -8,6 +8,22 @@ // Content 部分自动占据剩余宽度 .@{select-prefix}-content { flex: auto; + display: flex; + align-items: center; + /* Prevent content from wrapping */ + min-width: 0; /* allow flex item to shrink */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + position: relative; + } + + .@{select-prefix}-input { + border: none; + position: absolute; + inset: 0; + inset: 0; + background: transparent; } // 其他部分禁止自动宽度,使用内容宽度 diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index e2c50bb8f..55cde9f30 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -256,6 +256,7 @@ const BaseSelect = React.forwardRef((props, ref) notFoundContent = 'Not Found', onClear, maxCount, + placeholder, // Mode mode, diff --git a/src/SelectInput/Content/Placeholder.tsx b/src/SelectInput/Content/Placeholder.tsx index b7f101da0..69137ac1d 100644 --- a/src/SelectInput/Content/Placeholder.tsx +++ b/src/SelectInput/Content/Placeholder.tsx @@ -2,7 +2,11 @@ import * as React from 'react'; import { useSelectInputContext } from '../context'; export default function Placeholder() { - const { prefixCls, placeholder } = useSelectInputContext(); + const { prefixCls, placeholder, displayValues } = useSelectInputContext(); + + if (displayValues.length) { + return null; + } return
{placeholder}
; } From 1c2573d4f1d12ae1107a39fdbf2305e077176742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 10 Oct 2025 11:48:39 +0800 Subject: [PATCH 06/72] chore: style --- src/BaseSelect/index.tsx | 4 ++++ src/SelectInput/Content/Placeholder.tsx | 4 +++- src/SelectInput/Content/SingleContent.tsx | 7 ++----- src/SelectInput/context.ts | 1 + src/SelectInput/index.tsx | 5 ++++- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 55cde9f30..100f15a82 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -903,6 +903,10 @@ const BaseSelect = React.forwardRef((props, ref) multiple={multiple} displayValues={displayValues} placeholder={placeholder} + searchValue={mergedSearchValue} + onSearch={onInternalSearch} + onSearchSubmit={onInternalSearchSubmit} + onInputBlur={onInputBlur} /> ); diff --git a/src/SelectInput/Content/Placeholder.tsx b/src/SelectInput/Content/Placeholder.tsx index 69137ac1d..a30e3f835 100644 --- a/src/SelectInput/Content/Placeholder.tsx +++ b/src/SelectInput/Content/Placeholder.tsx @@ -4,7 +4,9 @@ import { useSelectInputContext } from '../context'; export default function Placeholder() { const { prefixCls, placeholder, displayValues } = useSelectInputContext(); - if (displayValues.length) { + const { searchValue } = useSelectInputContext(); + + if (displayValues.length || searchValue) { return null; } diff --git a/src/SelectInput/Content/SingleContent.tsx b/src/SelectInput/Content/SingleContent.tsx index 9951855d3..af50f17d4 100644 --- a/src/SelectInput/Content/SingleContent.tsx +++ b/src/SelectInput/Content/SingleContent.tsx @@ -4,14 +4,11 @@ import { useSelectInputContext } from '../context'; import Placeholder from './Placeholder'; export default function SingleContent() { - const { prefixCls, displayValues: value } = useSelectInputContext(); - - // For single mode, we only show the first value - const displayValue = value[0]?.label?.toString() || value[0]?.value?.toString() || ''; + const { prefixCls, searchValue } = useSelectInputContext(); return (
- +
); diff --git a/src/SelectInput/context.ts b/src/SelectInput/context.ts index 052c8c84b..d777f4216 100644 --- a/src/SelectInput/context.ts +++ b/src/SelectInput/context.ts @@ -6,6 +6,7 @@ export interface ContentContextProps { multiple: boolean; displayValues: DisplayValueType[]; placeholder?: React.ReactNode; + searchValue?: string; } const SelectInputContext = React.createContext(null!); diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index 9d8d9c239..400d127a1 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -13,6 +13,7 @@ export interface SelectInputProps { multiple?: boolean; displayValues: DisplayValueType[]; placeholder?: React.ReactNode; + searchValue?: string; // Add other props that need to be passed through className?: string; style?: React.CSSProperties; @@ -28,6 +29,7 @@ export default function SelectInput(props: SelectInputProps) { multiple, displayValues, placeholder, + searchValue, className, style, ...restProps @@ -39,8 +41,9 @@ export default function SelectInput(props: SelectInputProps) { multiple: !!multiple, displayValues, placeholder, + searchValue, }), - [prefixCls, multiple, displayValues, placeholder], + [prefixCls, multiple, displayValues, placeholder, searchValue], ); return ( From e1c5e34c986cdc3ee491770545344702cacb8321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 10 Oct 2025 15:27:46 +0800 Subject: [PATCH 07/72] chore: basic content --- assets/patch.less | 19 ++++++ src/BaseSelect/index.tsx | 30 ++++++++ src/SelectInput/Content/MultipleContent.tsx | 2 + src/SelectInput/Content/Placeholder.tsx | 13 +++- src/SelectInput/Content/SingleContent.tsx | 6 +- src/SelectInput/Input.tsx | 76 ++++++++++++++++++++- src/SelectInput/context.ts | 7 +- src/SelectInput/index.tsx | 30 +++++++- 8 files changed, 173 insertions(+), 10 deletions(-) diff --git a/assets/patch.less b/assets/patch.less index 77e98fe4b..642f05a08 100644 --- a/assets/patch.less +++ b/assets/patch.less @@ -26,6 +26,25 @@ background: transparent; } + .@{select-prefix}-placeholder { + opacity: 0.5; + + &::after { + content: '\00a0'; // nbsp placeholder + width: 0; + overflow: hidden; + } + } + + .@{select-prefix}-input, + .@{select-prefix}-placeholder { + padding: 0; + margin: 0; + line-height: 1.5; + font-size: 14px; + font-weight: normal; + } + // 其他部分禁止自动宽度,使用内容宽度 .@{select-prefix}-prefix, .@{select-prefix}-suffix, diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 100f15a82..e2a4e17fc 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -904,12 +904,42 @@ const BaseSelect = React.forwardRef((props, ref) displayValues={displayValues} placeholder={placeholder} searchValue={mergedSearchValue} + mode={mode} + open={mergedOpen} onSearch={onInternalSearch} onSearchSubmit={onInternalSearchSubmit} onInputBlur={onInputBlur} + onFocus={onContainerFocus} + onBlur={onContainerBlur} /> ); + renderNode = ( + + {renderNode} + + ); + return ( {renderNode} ); diff --git a/src/SelectInput/Content/MultipleContent.tsx b/src/SelectInput/Content/MultipleContent.tsx index a5dec40e6..4841f69c8 100644 --- a/src/SelectInput/Content/MultipleContent.tsx +++ b/src/SelectInput/Content/MultipleContent.tsx @@ -2,6 +2,8 @@ import * as React from 'react'; import Input from '../Input'; import { useSelectInputContext } from '../context'; +// This is just a placeholder, do not code any logic here + export default function MultipleContent() { const { prefixCls, displayValues: value } = useSelectInputContext(); diff --git a/src/SelectInput/Content/Placeholder.tsx b/src/SelectInput/Content/Placeholder.tsx index a30e3f835..e10f1c1a4 100644 --- a/src/SelectInput/Content/Placeholder.tsx +++ b/src/SelectInput/Content/Placeholder.tsx @@ -6,9 +6,18 @@ export default function Placeholder() { const { searchValue } = useSelectInputContext(); - if (displayValues.length || searchValue) { + if (displayValues.length) { return null; } - return
{placeholder}
; + return ( +
+ {placeholder} +
+ ); } diff --git a/src/SelectInput/Content/SingleContent.tsx b/src/SelectInput/Content/SingleContent.tsx index af50f17d4..e7898d1e7 100644 --- a/src/SelectInput/Content/SingleContent.tsx +++ b/src/SelectInput/Content/SingleContent.tsx @@ -4,12 +4,14 @@ import { useSelectInputContext } from '../context'; import Placeholder from './Placeholder'; export default function SingleContent() { - const { prefixCls, searchValue } = useSelectInputContext(); + const { prefixCls, searchValue, displayValues } = useSelectInputContext(); + + const displayValue = displayValues[0]; return (
+ {displayValue ? displayValue.label : } -
); } diff --git a/src/SelectInput/Input.tsx b/src/SelectInput/Input.tsx index c2b9b9c3b..0874f1cd6 100644 --- a/src/SelectInput/Input.tsx +++ b/src/SelectInput/Input.tsx @@ -16,12 +16,82 @@ export interface InputProps { } const Input = React.forwardRef((props, ref) => { - const { ...restProps } = props; - const { prefixCls } = useSelectInputContext(); + const { onChange, onKeyDown, onBlur, ...restProps } = props; + const { prefixCls, mode, open, onSearch, onSearchSubmit, onInputBlur } = useSelectInputContext(); const inputCls = `${prefixCls}-input`; - return ; + // 用于处理输入法组合状态 + const compositionStatusRef = React.useRef(false); + + // 处理输入变化 + const handleChange: React.ChangeEventHandler = (event) => { + const { value } = event.target; + + // 调用 onSearch 回调 + if (onSearch) { + onSearch(value, true, compositionStatusRef.current); + } + + // 调用原始的 onChange 回调 + onChange?.(event); + }; + + // 处理键盘事件 + const handleKeyDown: React.KeyboardEventHandler = (event) => { + const { key } = event; + const { value } = event.currentTarget; + + // 处理 Enter 键提交 - 参考 Selector 的实现 + if ( + key === 'Enter' && + mode === 'tags' && + !compositionStatusRef.current && + !open && + onSearchSubmit + ) { + onSearchSubmit(value); + } + + // 调用原始的 onKeyDown 回调 + onKeyDown?.(event); + }; + + // 处理失焦事件 + const handleBlur: React.FocusEventHandler = (event) => { + // 调用 onInputBlur 回调 + onInputBlur?.(); + + // 调用原始的 onBlur 回调 + onBlur?.(event); + }; + + // 处理输入法组合开始 + const handleCompositionStart = () => { + compositionStatusRef.current = true; + }; + + // 处理输入法组合结束 + const handleCompositionEnd: React.CompositionEventHandler = (event) => { + compositionStatusRef.current = false; + + // 输入法组合结束时触发搜索 + const { value } = event.currentTarget; + onSearch?.(value, true, false); + }; + + return ( + + ); }); export default Input; diff --git a/src/SelectInput/context.ts b/src/SelectInput/context.ts index d777f4216..04c0a09da 100644 --- a/src/SelectInput/context.ts +++ b/src/SelectInput/context.ts @@ -1,4 +1,4 @@ -import type { DisplayValueType } from '../interface'; +import type { DisplayValueType, Mode } from '../interface'; import * as React from 'react'; export interface ContentContextProps { @@ -7,6 +7,11 @@ export interface ContentContextProps { displayValues: DisplayValueType[]; placeholder?: React.ReactNode; searchValue?: string; + mode?: Mode; + open?: boolean; + onSearch?: (searchText: string, fromTyping: boolean, isCompositing: boolean) => void; + onSearchSubmit?: (searchText: string) => void; + onInputBlur?: () => void; } const SelectInputContext = React.createContext(null!); diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index 400d127a1..222d45355 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -3,7 +3,7 @@ import clsx from 'clsx'; import Affix from './Affix'; import SelectContent from './Content'; import SelectInputContext from './context'; -import type { DisplayValueType } from '../interface'; +import type { DisplayValueType, Mode } from '../interface'; export interface SelectInputProps { prefixCls: string; @@ -14,6 +14,11 @@ export interface SelectInputProps { displayValues: DisplayValueType[]; placeholder?: React.ReactNode; searchValue?: string; + mode?: Mode; + open?: boolean; + onSearch?: (searchText: string, fromTyping: boolean, isCompositing: boolean) => void; + onSearchSubmit?: (searchText: string) => void; + onInputBlur?: () => void; // Add other props that need to be passed through className?: string; style?: React.CSSProperties; @@ -30,6 +35,11 @@ export default function SelectInput(props: SelectInputProps) { displayValues, placeholder, searchValue, + mode, + open, + onSearch, + onSearchSubmit, + onInputBlur, className, style, ...restProps @@ -42,8 +52,24 @@ export default function SelectInput(props: SelectInputProps) { displayValues, placeholder, searchValue, + mode, + open, + onSearch, + onSearchSubmit, + onInputBlur, }), - [prefixCls, multiple, displayValues, placeholder, searchValue], + [ + prefixCls, + multiple, + displayValues, + placeholder, + searchValue, + mode, + open, + onSearch, + onSearchSubmit, + onInputBlur, + ], ); return ( From d9544317c526710d8903a1c09f8eeda2e17e1e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 10 Oct 2025 17:38:26 +0800 Subject: [PATCH 08/72] chore: blur clear --- assets/patch.less | 1 + src/BaseSelect/index.tsx | 552 ++++++++++---------- src/SelectInput/Content/MultipleContent.tsx | 6 +- src/SelectInput/Content/SingleContent.tsx | 19 +- src/SelectInput/Content/index.tsx | 10 +- src/SelectInput/Input.tsx | 57 +- src/SelectInput/context.ts | 13 +- src/SelectInput/index.tsx | 92 +++- src/hooks/useOpen.ts | 58 ++ 9 files changed, 497 insertions(+), 311 deletions(-) create mode 100644 src/hooks/useOpen.ts diff --git a/assets/patch.less b/assets/patch.less index 642f05a08..4e356f5a6 100644 --- a/assets/patch.less +++ b/assets/patch.less @@ -4,6 +4,7 @@ .@{select-prefix}.@{select-prefix} { display: inline-flex; align-items: center; + user-select: none; // Content 部分自动占据剩余宽度 .@{select-prefix}-content { diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index e2a4e17fc..95c064aa0 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -30,7 +30,7 @@ import SelectTrigger from '../SelectTrigger'; import TransBtn from '../TransBtn'; import { getSeparatedContent, isValidCount } from '../utils/valueUtil'; import Polite from './Polite'; -import SelectInput from '@/SelectInput'; +import useOpen from '../hooks/useOpen'; export type BaseSelectSemanticName = 'prefix' | 'suffix' | 'input'; /** @@ -289,7 +289,6 @@ const BaseSelect = React.forwardRef((props, ref) // Icons allowClear, prefix, - suffixIcon, suffix, clearIcon, @@ -395,37 +394,42 @@ const BaseSelect = React.forwardRef((props, ref) ); // ============================== Open ============================== - // SSR not support Portal which means we need delay `open` for the first time render - const [rendered, setRendered] = React.useState(false); - useLayoutEffect(() => { - setRendered(true); - }, []); + // Not trigger `open` in `combobox` when `notFoundContent` is empty + const emptyListContent = !notFoundContent && emptyOptions; - const [innerOpen, setInnerOpen] = useControlledState(defaultOpen || false, open); + const [mergedOpen, triggerOpen] = useOpen(open, onPopupVisibleChange, (nextOpen) => + disabled || (emptyListContent && mergedOpen && mode === 'combobox') ? false : nextOpen, + ); - let mergedOpen = rendered ? innerOpen : false; + // // SSR not support Portal which means we need delay `open` for the first time render + // const [rendered, setRendered] = React.useState(false); + // useLayoutEffect(() => { + // setRendered(true); + // }, []); - // Not trigger `open` in `combobox` when `notFoundContent` is empty - const emptyListContent = !notFoundContent && emptyOptions; - if (disabled || (emptyListContent && mergedOpen && mode === 'combobox')) { - mergedOpen = false; - } - const triggerOpen = emptyListContent ? false : mergedOpen; + // const [innerOpen, setInnerOpen] = useControlledState(defaultOpen || false, open); - const onToggleOpen = React.useCallback( - (newOpen?: boolean) => { - const nextOpen = newOpen !== undefined ? newOpen : !mergedOpen; + // let mergedOpen = rendered ? innerOpen : false; - if (!disabled) { - setInnerOpen(nextOpen); + // if (disabled || (emptyListContent && mergedOpen && mode === 'combobox')) { + // mergedOpen = false; + // } + // const triggerOpen = emptyListContent ? false : mergedOpen; - if (mergedOpen !== nextOpen) { - onPopupVisibleChange?.(nextOpen); - } - } - }, - [disabled, mergedOpen, setInnerOpen, onPopupVisibleChange], - ); + // const onToggleOpen = React.useCallback( + // (newOpen?: boolean) => { + // const nextOpen = newOpen !== undefined ? newOpen : !mergedOpen; + + // if (!disabled) { + // setInnerOpen(nextOpen); + + // if (mergedOpen !== nextOpen) { + // onPopupVisibleChange?.(nextOpen); + // } + // } + // }, + // [disabled, mergedOpen, setInnerOpen, onPopupVisibleChange], + // ); // ============================= Search ============================= const tokenWithEnter = React.useMemo( @@ -457,7 +461,8 @@ const BaseSelect = React.forwardRef((props, ref) onSearchSplit?.(patchLabels); // Should close when paste finish - onToggleOpen(false); + // onToggleOpen(false); + triggerOpen(false); // Tell Selector that break next actions ret = false; @@ -492,16 +497,16 @@ const BaseSelect = React.forwardRef((props, ref) // ============================ Disabled ============================ // Close dropdown & remove focus state when disabled change - React.useEffect(() => { - if (innerOpen && disabled) { - setInnerOpen(false); - } + // React.useEffect(() => { + // if (innerOpen && disabled) { + // setInnerOpen(false); + // } - // After onBlur is triggered, the focused does not need to be reset - if (disabled && !blurRef.current) { - setMockFocused(false); - } - }, [disabled]); + // // After onBlur is triggered, the focused does not need to be reset + // if (disabled && !blurRef.current) { + // setMockFocused(false); + // } + // }, [disabled]); // ============================ Keyboard ============================ /** @@ -514,76 +519,76 @@ const BaseSelect = React.forwardRef((props, ref) const keyLockRef = React.useRef(false); // KeyDown - const onInternalKeyDown: React.KeyboardEventHandler = (event, ...rest) => { - const clearLock = getClearLock(); - const { key } = event; - - const isEnterKey = key === 'Enter'; - - if (isEnterKey) { - // Do not submit form when type in the input - if (mode !== 'combobox') { - event.preventDefault(); - } - - // We only manage open state here, close logic should handle by list component - if (!mergedOpen) { - onToggleOpen(true); - } - } - - setClearLock(!!mergedSearchValue); - - // Remove value by `backspace` - if ( - key === 'Backspace' && - !clearLock && - multiple && - !mergedSearchValue && - displayValues.length - ) { - const cloneDisplayValues = [...displayValues]; - let removedDisplayValue = null; - - for (let i = cloneDisplayValues.length - 1; i >= 0; i -= 1) { - const current = cloneDisplayValues[i]; - - if (!current.disabled) { - cloneDisplayValues.splice(i, 1); - removedDisplayValue = current; - break; - } - } - - if (removedDisplayValue) { - onDisplayValuesChange(cloneDisplayValues, { - type: 'remove', - values: [removedDisplayValue], - }); - } - } - - if (mergedOpen && (!isEnterKey || !keyLockRef.current)) { - // Lock the Enter key after it is pressed to avoid repeated triggering of the onChange event. - if (isEnterKey) { - keyLockRef.current = true; - } - listRef.current?.onKeyDown(event, ...rest); - } - - onKeyDown?.(event, ...rest); - }; - - // KeyUp - const onInternalKeyUp: React.KeyboardEventHandler = (event, ...rest) => { - if (mergedOpen) { - listRef.current?.onKeyUp(event, ...rest); - } - if (event.key === 'Enter') { - keyLockRef.current = false; - } - onKeyUp?.(event, ...rest); - }; + // const onInternalKeyDown: React.KeyboardEventHandler = (event, ...rest) => { + // const clearLock = getClearLock(); + // const { key } = event; + + // const isEnterKey = key === 'Enter'; + + // if (isEnterKey) { + // // Do not submit form when type in the input + // if (mode !== 'combobox') { + // event.preventDefault(); + // } + + // // We only manage open state here, close logic should handle by list component + // if (!mergedOpen) { + // onToggleOpen(true); + // } + // } + + // setClearLock(!!mergedSearchValue); + + // // Remove value by `backspace` + // if ( + // key === 'Backspace' && + // !clearLock && + // multiple && + // !mergedSearchValue && + // displayValues.length + // ) { + // const cloneDisplayValues = [...displayValues]; + // let removedDisplayValue = null; + + // for (let i = cloneDisplayValues.length - 1; i >= 0; i -= 1) { + // const current = cloneDisplayValues[i]; + + // if (!current.disabled) { + // cloneDisplayValues.splice(i, 1); + // removedDisplayValue = current; + // break; + // } + // } + + // if (removedDisplayValue) { + // onDisplayValuesChange(cloneDisplayValues, { + // type: 'remove', + // values: [removedDisplayValue], + // }); + // } + // } + + // if (mergedOpen && (!isEnterKey || !keyLockRef.current)) { + // // Lock the Enter key after it is pressed to avoid repeated triggering of the onChange event. + // if (isEnterKey) { + // keyLockRef.current = true; + // } + // listRef.current?.onKeyDown(event, ...rest); + // } + + // onKeyDown?.(event, ...rest); + // }; + + // // KeyUp + // const onInternalKeyUp: React.KeyboardEventHandler = (event, ...rest) => { + // if (mergedOpen) { + // listRef.current?.onKeyUp(event, ...rest); + // } + // if (event.key === 'Enter') { + // keyLockRef.current = false; + // } + // onKeyUp?.(event, ...rest); + // }; // ============================ Selector ============================ const onSelectorRemove = (val: DisplayValueType) => { @@ -602,38 +607,56 @@ const BaseSelect = React.forwardRef((props, ref) // ========================== Focus / Blur ========================== /** Record real focus status */ - const focusRef = React.useRef(false); - - const onContainerFocus: React.FocusEventHandler = (...args) => { - setMockFocused(true); - - if (!disabled) { - if (onFocus && !focusRef.current) { - onFocus(...args); - } - - // `showAction` should handle `focus` if set - if (showAction.includes('focus')) { - onToggleOpen(true); - } - } - - focusRef.current = true; - }; - - const onContainerBlur: React.FocusEventHandler = (...args) => { - blurRef.current = true; - - setMockFocused(false, () => { - focusRef.current = false; - blurRef.current = false; - onToggleOpen(false); - }); - - if (disabled) { - return; - } - + // const focusRef = React.useRef(false); + + // const onContainerFocus: React.FocusEventHandler = (...args) => { + // setMockFocused(true); + + // if (!disabled) { + // if (onFocus && !focusRef.current) { + // onFocus(...args); + // } + + // // `showAction` should handle `focus` if set + // if (showAction.includes('focus')) { + // triggerOpen(true); + // } + // } + + // focusRef.current = true; + // }; + + // const onContainerBlur: React.FocusEventHandler = (...args) => { + // blurRef.current = true; + + // setMockFocused(false, () => { + // focusRef.current = false; + // blurRef.current = false; + // triggerOpen(false); + // }); + + // if (disabled) { + // return; + // } + + // if (mergedSearchValue) { + // // `tags` mode should move `searchValue` into values + // if (mode === 'tags') { + // onSearch(mergedSearchValue, { source: 'submit' }); + // } else if (mode === 'multiple') { + // // `multiple` mode only clean the search value but not trigger event + // onSearch('', { + // source: 'blur', + // }); + // } + // } + + // if (onBlur) { + // onBlur(...args); + // } + // }; + + const onInternalBlur: React.FocusEventHandler = (event) => { if (mergedSearchValue) { // `tags` mode should move `searchValue` into values if (mode === 'tags') { @@ -646,9 +669,7 @@ const BaseSelect = React.forwardRef((props, ref) } } - if (onBlur) { - onBlur(...args); - } + onBlur?.(event); }; // Give focus back of Select @@ -662,26 +683,26 @@ const BaseSelect = React.forwardRef((props, ref) ); const onInternalMouseDown: React.MouseEventHandler = (event, ...restArgs) => { - const { target } = event; - const popupElement: HTMLDivElement = triggerRef.current?.getPopupElement(); - - // We should give focus back to selector if clicked item is not focusable - if (popupElement && popupElement.contains(target as HTMLElement)) { - const timeoutId = setTimeout(() => { - const index = activeTimeoutIds.indexOf(timeoutId); - if (index !== -1) { - activeTimeoutIds.splice(index, 1); - } - - cancelSetMockFocused(); - - if (!mobile && !popupElement.contains(document.activeElement)) { - selectorRef.current?.focus(); - } - }); + // const { target } = event; + // const popupElement: HTMLDivElement = triggerRef.current?.getPopupElement(); - activeTimeoutIds.push(timeoutId); - } + // // We should give focus back to selector if clicked item is not focusable + // if (popupElement?.contains(target as HTMLElement)) { + // const timeoutId = setTimeout(() => { + // const index = activeTimeoutIds.indexOf(timeoutId); + // if (index !== -1) { + // activeTimeoutIds.splice(index, 1); + // } + + // cancelSetMockFocused(); + + // if (!mobile && !popupElement.contains(document.activeElement)) { + // selectorRef.current?.focus(); + // } + // }); + + // activeTimeoutIds.push(timeoutId); + // } onMouseDown?.(event, ...restArgs); }; @@ -693,21 +714,21 @@ const BaseSelect = React.forwardRef((props, ref) forceUpdate({}); } - // Used for raw custom input trigger - let onTriggerVisibleChange: null | ((newOpen: boolean) => void); - if (customizeRawInputElement) { - onTriggerVisibleChange = (newOpen: boolean) => { - onToggleOpen(newOpen); - }; - } + // // Used for raw custom input trigger + // let onTriggerVisibleChange: null | ((newOpen: boolean) => void); + // if (customizeRawInputElement) { + // onTriggerVisibleChange = (newOpen: boolean) => { + // onToggleOpen(newOpen); + // }; + // } - // Close when click on non-select element - useSelectTriggerControl( - () => [containerRef.current, triggerRef.current?.getPopupElement()], - triggerOpen, - onToggleOpen, - !!customizeRawInputElement, - ); + // // Close when click on non-select element + // useSelectTriggerControl( + // () => [containerRef.current, triggerRef.current?.getPopupElement()], + // triggerOpen, + // onToggleOpen, + // !!customizeRawInputElement, + // ); // ============================ Context ============================= const baseSelectContext = React.useMemo( @@ -715,11 +736,11 @@ const BaseSelect = React.forwardRef((props, ref) ...props, notFoundContent, open: mergedOpen, - triggerOpen, + triggerOpen: mergedOpen, id, showSearch: mergedShowSearch, multiple, - toggleOpen: onToggleOpen, + toggleOpen: triggerOpen, showScrollBar, styles, classNames, @@ -728,11 +749,10 @@ const BaseSelect = React.forwardRef((props, ref) props, notFoundContent, triggerOpen, - mergedOpen, id, mergedShowSearch, multiple, - onToggleOpen, + mergedOpen, showScrollBar, styles, classNames, @@ -744,27 +764,27 @@ const BaseSelect = React.forwardRef((props, ref) // ================================================================== // ============================= Arrow ============================== - const showSuffixIcon = !!suffixIcon || loading; - let arrowNode: React.ReactNode; - - if (showSuffixIcon) { - arrowNode = ( - - ); - } + // const showSuffixIcon = !!suffixIcon || loading; + // let arrowNode: React.ReactNode; + + // if (showSuffixIcon) { + // arrowNode = ( + // + // ); + // } // ============================= Clear ============================== const onClearMouseDown: React.MouseEventHandler = () => { @@ -799,7 +819,7 @@ const BaseSelect = React.forwardRef((props, ref) [`${prefixCls}-multiple`]: multiple, [`${prefixCls}-single`]: !multiple, [`${prefixCls}-allow-clear`]: mergedAllowClear, - [`${prefixCls}-show-arrow`]: showSuffixIcon, + // [`${prefixCls}-show-arrow`]: showSuffixIcon, [`${prefixCls}-disabled`]: disabled, [`${prefixCls}-loading`]: loading, [`${prefixCls}-open`]: mergedOpen, @@ -808,61 +828,61 @@ const BaseSelect = React.forwardRef((props, ref) }); // >>> Selector - const selectorNode = ( - - {customizeRawInputElement ? ( - React.cloneElement(customizeRawInputElement, { - ref: customizeRawInputRef, - }) - ) : ( - - )} - - ); + // const selectorNode = ( + // + // {customizeRawInputElement ? ( + // React.cloneElement(customizeRawInputElement, { + // ref: customizeRawInputRef, + // }) + // ) : ( + // + // )} + // + // ); // >>> Render let renderNode: React.ReactNode; @@ -894,23 +914,29 @@ const BaseSelect = React.forwardRef((props, ref) renderNode = ( ); @@ -919,7 +945,7 @@ const BaseSelect = React.forwardRef((props, ref) ref={triggerRef} disabled={disabled} prefixCls={prefixCls} - visible={triggerOpen} + visible={mergedOpen} popupElement={optionList} animation={animation} transitionName={transitionName} @@ -933,7 +959,7 @@ const BaseSelect = React.forwardRef((props, ref) builtinPlacements={builtinPlacements} getPopupContainer={getPopupContainer} empty={emptyOptions} - onPopupVisibleChange={onTriggerVisibleChange} + // onPopupVisibleChange={onTriggerVisibleChange} onPopupMouseEnter={onPopupMouseEnter} > {renderNode} diff --git a/src/SelectInput/Content/MultipleContent.tsx b/src/SelectInput/Content/MultipleContent.tsx index 4841f69c8..6737c7296 100644 --- a/src/SelectInput/Content/MultipleContent.tsx +++ b/src/SelectInput/Content/MultipleContent.tsx @@ -4,7 +4,7 @@ import { useSelectInputContext } from '../context'; // This is just a placeholder, do not code any logic here -export default function MultipleContent() { +export default React.forwardRef(function MultipleContent(_, ref) { const { prefixCls, displayValues: value } = useSelectInputContext(); // For multiple mode, we show all values as a comma-separated string @@ -14,7 +14,7 @@ export default function MultipleContent() { return (
- +
); -} +}); diff --git a/src/SelectInput/Content/SingleContent.tsx b/src/SelectInput/Content/SingleContent.tsx index e7898d1e7..7d0916d36 100644 --- a/src/SelectInput/Content/SingleContent.tsx +++ b/src/SelectInput/Content/SingleContent.tsx @@ -3,15 +3,26 @@ import Input from '../Input'; import { useSelectInputContext } from '../context'; import Placeholder from './Placeholder'; -export default function SingleContent() { +export default React.forwardRef(function SingleContent(_, ref) { const { prefixCls, searchValue, displayValues } = useSelectInputContext(); const displayValue = displayValues[0]; return (
- {displayValue ? displayValue.label : } - + {displayValue ? ( +
+ {displayValue.label} +
+ ) : ( + + )} +
); -} +}); diff --git a/src/SelectInput/Content/index.tsx b/src/SelectInput/Content/index.tsx index 044ddf725..65c490261 100644 --- a/src/SelectInput/Content/index.tsx +++ b/src/SelectInput/Content/index.tsx @@ -3,12 +3,14 @@ import SingleContent from './SingleContent'; import MultipleContent from './MultipleContent'; import { useSelectInputContext } from '../context'; -export default function SelectContent() { +const SelectContent = React.forwardRef(function SelectContent(_, ref) { const { multiple } = useSelectInputContext(); if (multiple) { - return ; + return ; } - return ; -} + return ; +}); + +export default SelectContent; diff --git a/src/SelectInput/Input.tsx b/src/SelectInput/Input.tsx index 0874f1cd6..d255f2ae1 100644 --- a/src/SelectInput/Input.tsx +++ b/src/SelectInput/Input.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { useSelectInputContext } from './context'; +import useBaseProps from '../hooks/useBaseProps'; export interface InputProps { disabled?: boolean; @@ -17,71 +18,95 @@ export interface InputProps { const Input = React.forwardRef((props, ref) => { const { onChange, onKeyDown, onBlur, ...restProps } = props; - const { prefixCls, mode, open, onSearch, onSearchSubmit, onInputBlur } = useSelectInputContext(); + const { prefixCls, mode, onSearch, onSearchSubmit, onInputBlur } = useSelectInputContext(); + const { triggerOpen } = useBaseProps(); const inputCls = `${prefixCls}-input`; - // 用于处理输入法组合状态 + // Used to handle input method composition status const compositionStatusRef = React.useRef(false); - // 处理输入变化 + // Handle input changes const handleChange: React.ChangeEventHandler = (event) => { const { value } = event.target; - // 调用 onSearch 回调 + // Call onSearch callback if (onSearch) { onSearch(value, true, compositionStatusRef.current); } - // 调用原始的 onChange 回调 + // Call original onChange callback onChange?.(event); }; - // 处理键盘事件 + // Handle keyboard events const handleKeyDown: React.KeyboardEventHandler = (event) => { const { key } = event; const { value } = event.currentTarget; - // 处理 Enter 键提交 - 参考 Selector 的实现 + // Handle Enter key submission - referencing Selector implementation if ( key === 'Enter' && mode === 'tags' && !compositionStatusRef.current && - !open && + !triggerOpen && onSearchSubmit ) { onSearchSubmit(value); } - // 调用原始的 onKeyDown 回调 + // Call original onKeyDown callback onKeyDown?.(event); }; - // 处理失焦事件 + // Handle blur events const handleBlur: React.FocusEventHandler = (event) => { - // 调用 onInputBlur 回调 + // Call onInputBlur callback onInputBlur?.(); - // 调用原始的 onBlur 回调 + // Call original onBlur callback onBlur?.(event); }; - // 处理输入法组合开始 + // Handle input method composition start const handleCompositionStart = () => { compositionStatusRef.current = true; }; - // 处理输入法组合结束 + // Handle input method composition end const handleCompositionEnd: React.CompositionEventHandler = (event) => { compositionStatusRef.current = false; - // 输入法组合结束时触发搜索 + // Trigger search when input method composition ends const { value } = event.currentTarget; onSearch?.(value, true, false); }; + // ============================= Mouse ============================== + // const onMouseDown: React.MouseEventHandler = (event) => { + // // const inputMouseDown = getInputMouseDown(); + // // // when mode is combobox and it is disabled, don't prevent default behavior + // // // https://github.com/ant-design/ant-design/issues/37320 + // // // https://github.com/ant-design/ant-design/issues/48281 + // // if ( + // // event.target !== inputRef.current && + // // !inputMouseDown && + // // !(mode === 'combobox' && disabled) + // // ) { + // // event.preventDefault(); + // // } + // // if ((mode !== 'combobox' && (!showSearch || !inputMouseDown)) || !open) { + // // if (open && autoClearSearchValue !== false) { + // // onSearch('', true, false); + // // } + // // onToggleOpen(); + // // } + // }; + + // ============================= Render ============================= return ( ((props, ref) => { onBlur={handleBlur} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} - {...restProps} + // onMouseDown={onMouseDown} /> ); }); diff --git a/src/SelectInput/context.ts b/src/SelectInput/context.ts index 04c0a09da..cecd83303 100644 --- a/src/SelectInput/context.ts +++ b/src/SelectInput/context.ts @@ -5,13 +5,12 @@ export interface ContentContextProps { prefixCls: string; multiple: boolean; displayValues: DisplayValueType[]; - placeholder?: React.ReactNode; - searchValue?: string; - mode?: Mode; - open?: boolean; - onSearch?: (searchText: string, fromTyping: boolean, isCompositing: boolean) => void; - onSearchSubmit?: (searchText: string) => void; - onInputBlur?: () => void; + placeholder: React.ReactNode; + searchValue: string; + mode: Mode; + onSearch: (searchText: string, fromTyping: boolean, isCompositing: boolean) => void; + onSearchSubmit: (searchText: string) => void; + onInputBlur: () => void; } const SelectInputContext = React.createContext(null!); diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index 222d45355..9e47bcbad 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -2,10 +2,17 @@ import * as React from 'react'; import clsx from 'clsx'; import Affix from './Affix'; import SelectContent from './Content'; -import SelectInputContext from './context'; +import SelectInputContext, { type ContentContextProps } from './context'; import type { DisplayValueType, Mode } from '../interface'; +import useBaseProps from '../hooks/useBaseProps'; -export interface SelectInputProps { +export interface SelectInputRef { + focus: (options?: FocusOptions) => void; + blur: () => void; + nativeElement: HTMLDivElement; +} + +export interface SelectInputProps extends Omit, 'prefix'> { prefixCls: string; prefix?: React.ReactNode; suffix?: React.ReactNode; @@ -15,7 +22,6 @@ export interface SelectInputProps { placeholder?: React.ReactNode; searchValue?: string; mode?: Mode; - open?: boolean; onSearch?: (searchText: string, fromTyping: boolean, isCompositing: boolean) => void; onSearchSubmit?: (searchText: string) => void; onInputBlur?: () => void; @@ -25,27 +31,77 @@ export interface SelectInputProps { [key: string]: any; } -export default function SelectInput(props: SelectInputProps) { +export default React.forwardRef(function SelectInput( + props: SelectInputProps, + ref: React.ForwardedRef, +) { const { + // Style prefixCls, + className, + style, + + // UI prefix, suffix, clearIcon, + + // Data multiple, displayValues, placeholder, - searchValue, mode, - open, + + // Search + searchValue, onSearch, onSearchSubmit, onInputBlur, - className, - style, + + // Events + onMouseDown, + onBlur, + ...restProps } = props; - const cachedContext = React.useMemo( + const { triggerOpen, toggleOpen } = useBaseProps(); + + const rootRef = React.useRef(null); + const inputRef = React.useRef(null); + + // ====================== Refs ====================== + React.useImperativeHandle( + ref, + () => ({ + focus: (options?: FocusOptions) => { + // Focus the inner input if available, otherwise fall back to root div. + inputRef.current.focus?.(options); + }, + blur: () => { + inputRef.current.blur?.(); + }, + nativeElement: rootRef.current, + }), + [], + ); + + // ====================== Open ====================== + const onInternalMouseDown: SelectInputProps['onMouseDown'] = (event) => { + event.preventDefault(); + inputRef.current?.focus(); + + toggleOpen(); + onMouseDown?.(event); + }; + + const onInternalBlur: SelectInputProps['onBlur'] = (event) => { + toggleOpen(false); + onBlur?.(event); + }; + + // ==================== Context ===================== + const cachedContext = React.useMemo( () => ({ prefixCls, multiple: !!multiple, @@ -53,7 +109,6 @@ export default function SelectInput(props: SelectInputProps) { placeholder, searchValue, mode, - open, onSearch, onSearchSubmit, onInputBlur, @@ -65,21 +120,30 @@ export default function SelectInput(props: SelectInputProps) { placeholder, searchValue, mode, - open, onSearch, onSearchSubmit, onInputBlur, ], ); + // ===================== Render ===================== return ( -
+
{/* Prefix */} {prefix} {/* Content */} - + {/* Suffix */} {suffix} @@ -88,4 +152,4 @@ export default function SelectInput(props: SelectInputProps) {
); -} +}); diff --git a/src/hooks/useOpen.ts b/src/hooks/useOpen.ts new file mode 100644 index 000000000..226bd1d9b --- /dev/null +++ b/src/hooks/useOpen.ts @@ -0,0 +1,58 @@ +import { useControlledState, useEvent } from '@rc-component/util'; +import { useRef } from 'react'; + +const macroTask = (fn: VoidFunction) => { + const channel = new MessageChannel(); + channel.port1.onmessage = fn; + channel.port2.postMessage(null); +}; + +/** + * When `open` is controlled, follow the controlled value; + * Otherwise use uncontrolled logic. + * Setting `open` takes effect immediately, + * but setting it to `false` is delayed via MessageChannel. + */ +export default function useOpen( + propOpen: boolean, + onOpen: (nextOpen: boolean) => void, + postOpen: (nextOpen: boolean) => boolean, +) { + const [stateOpen, internalSetOpen] = useControlledState(false, propOpen); + const mergedOpen = postOpen(stateOpen); + + const taskIdRef = useRef(0); + + const triggerEvent = useEvent((nextOpen: boolean) => { + if (onOpen && mergedOpen !== nextOpen) { + onOpen(nextOpen); + } + internalSetOpen(nextOpen); + }); + + const toggleOpen = useEvent((nextOpen?: boolean) => { + taskIdRef.current += 1; + const id = taskIdRef.current; + + const nextOpenVal = typeof nextOpen === 'boolean' ? nextOpen : !mergedOpen; + + if (nextOpenVal === mergedOpen) { + return; + } + + console.error('toggleOpen', nextOpenVal); + + if (nextOpenVal) { + triggerEvent(true); + return; + } + + macroTask(() => { + if (id === taskIdRef.current) { + triggerEvent(false); + } + }); + }); + + return [mergedOpen, toggleOpen] as const; +} From b14de985cdd5d0e1b065c36dde88f5e09e1ad5b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Sat, 11 Oct 2025 09:59:23 +0800 Subject: [PATCH 09/72] chore: clear logic --- assets/patch.less | 9 +++++ src/BaseSelect/index.tsx | 2 +- src/SelectInput/Affix.tsx | 10 ++++-- src/SelectInput/index.tsx | 27 +++++++++++--- src/TransBtn.tsx | 10 ++++++ src/hooks/useAllowClear.tsx | 70 ++++++++++++++++++++++--------------- 6 files changed, 90 insertions(+), 38 deletions(-) diff --git a/assets/patch.less b/assets/patch.less index 4e356f5a6..2cae26909 100644 --- a/assets/patch.less +++ b/assets/patch.less @@ -5,6 +5,8 @@ display: inline-flex; align-items: center; user-select: none; + border: 1px solid blue; + position: relative; // Content 部分自动占据剩余宽度 .@{select-prefix}-content { @@ -37,6 +39,7 @@ } } + .@{select-prefix}-content, .@{select-prefix}-input, .@{select-prefix}-placeholder { padding: 0; @@ -52,4 +55,10 @@ .@{select-prefix}-clear { flex: none; } + + .@{select-prefix}-clear { + position: absolute; + top: 0; + right: 0; + } } diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 95c064aa0..9faa9ca63 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -190,7 +190,7 @@ export interface BaseSelectProps extends BaseSelectPrivateProps, React.AriaAttri tokenSeparators?: string[]; // >>> Icons - allowClear?: boolean | { clearIcon?: RenderNode }; + allowClear?: boolean | { clearIcon?: React.ReactNode }; prefix?: React.ReactNode; suffix?: React.ReactNode; /** diff --git a/src/SelectInput/Affix.tsx b/src/SelectInput/Affix.tsx index aaff3ecaa..afe4cdfaf 100644 --- a/src/SelectInput/Affix.tsx +++ b/src/SelectInput/Affix.tsx @@ -1,18 +1,22 @@ import * as React from 'react'; import { useSelectInputContext } from './context'; -export interface AffixProps { +export interface AffixProps extends React.HTMLAttributes { type: 'prefix' | 'suffix' | 'clear'; children?: React.ReactNode; } export default function Affix(props: AffixProps) { - const { type, children } = props; + const { type, children, ...restProps } = props; const { prefixCls } = useSelectInputContext(); if (!children) { return null; } - return
{children}
; + return ( +
+ {children} +
+ ); } diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index 9e47bcbad..50a0fce08 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -5,6 +5,7 @@ import SelectContent from './Content'; import SelectInputContext, { type ContentContextProps } from './context'; import type { DisplayValueType, Mode } from '../interface'; import useBaseProps from '../hooks/useBaseProps'; +import { useEvent } from '@rc-component/util'; export interface SelectInputRef { focus: (options?: FocusOptions) => void; @@ -87,13 +88,19 @@ export default React.forwardRef(function Selec ); // ====================== Open ====================== - const onInternalMouseDown: SelectInputProps['onMouseDown'] = (event) => { + const onInternalMouseDown: SelectInputProps['onMouseDown'] = useEvent((event) => { event.preventDefault(); - inputRef.current?.focus(); - toggleOpen(); + if (!(event.nativeEvent as any)._select_lazy) { + inputRef.current?.focus(); + toggleOpen(); + } else if (triggerOpen) { + // Lazy should also close when click clear icon + toggleOpen(false); + } + onMouseDown?.(event); - }; + }); const onInternalBlur: SelectInputProps['onBlur'] = (event) => { toggleOpen(false); @@ -148,7 +155,17 @@ export default React.forwardRef(function Selec {/* Suffix */} {suffix} {/* Clear Icon */} - {clearIcon && {clearIcon}} + {clearIcon && ( + { + // Mark to tell not trigger open or focus + (e.nativeEvent as any)._select_lazy = true; + }} + > + {clearIcon} + + )}
); diff --git a/src/TransBtn.tsx b/src/TransBtn.tsx index e9240c365..31fcaf7f0 100644 --- a/src/TransBtn.tsx +++ b/src/TransBtn.tsx @@ -12,6 +12,16 @@ export interface TransBtnProps { children?: React.ReactNode; } +/** + * Small wrapper for Select icons (clear/arrow/etc.). + * Prevents default mousedown to avoid blurring or caret moves, and + * renders a custom icon or a fallback icon span. + * + * DOM structure: + * + * { icon || {children} } + * + */ const TransBtn: React.FC = (props) => { const { className, style, customizeIcon, customizeIconProps, children, onMouseDown, onClick } = props; diff --git a/src/hooks/useAllowClear.tsx b/src/hooks/useAllowClear.tsx index 7631521dd..80b262254 100644 --- a/src/hooks/useAllowClear.tsx +++ b/src/hooks/useAllowClear.tsx @@ -1,6 +1,11 @@ import TransBtn from '../TransBtn'; import type { DisplayValueType, Mode, RenderNode } from '../interface'; -import React from 'react'; +import React, { useMemo } from 'react'; + +export interface AllowClearConfig { + allowClear: boolean; + clearIcon: React.ReactNode; +} export const useAllowClear = ( prefixCls: string, @@ -11,38 +16,45 @@ export const useAllowClear = ( disabled: boolean = false, mergedSearchValue?: string, mode?: Mode, -) => { - const mergedClearIcon = React.useMemo(() => { - if (typeof allowClear === 'object') { - return allowClear.clearIcon; +): AllowClearConfig => { + // Convert boolean to object first + const allowClearConfig = useMemo>(() => { + if (typeof allowClear === 'boolean') { + return { allowClear }; } - if (clearIcon) { - return clearIcon; + if (allowClear && typeof allowClear === 'object') { + return allowClear; } - }, [allowClear, clearIcon]); + return { allowClear: false }; + }, [allowClear]); - const mergedAllowClear = React.useMemo(() => { - if ( + return useMemo(() => { + const mergedAllowClear = !disabled && - !!allowClear && + allowClearConfig.allowClear !== false && (displayValues.length || mergedSearchValue) && - !(mode === 'combobox' && mergedSearchValue === '') - ) { - return true; - } - return false; - }, [allowClear, disabled, displayValues.length, mergedSearchValue, mode]); + !(mode === 'combobox' && mergedSearchValue === ''); - return { - allowClear: mergedAllowClear, - clearIcon: ( - - × - - ), - }; + return { + allowClear: mergedAllowClear, + clearIcon: mergedAllowClear ? ( + + × + + ) : null, + }; + }, [ + allowClearConfig, + clearIcon, + disabled, + displayValues.length, + mergedSearchValue, + mode, + onClearMouseDown, + prefixCls, + ]); }; From 992b22df8d5c309e83afee311a39577cbb4b2372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Sat, 11 Oct 2025 10:24:26 +0800 Subject: [PATCH 10/72] chore: fit logic --- src/SelectInput/Content/MultipleContent.tsx | 6 +++--- src/SelectInput/Content/SingleContent.tsx | 6 +++--- src/SelectInput/Input.tsx | 5 ++++- src/SelectInput/context.ts | 1 + src/SelectInput/index.tsx | 5 +++++ 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/SelectInput/Content/MultipleContent.tsx b/src/SelectInput/Content/MultipleContent.tsx index 6737c7296..b00bccaff 100644 --- a/src/SelectInput/Content/MultipleContent.tsx +++ b/src/SelectInput/Content/MultipleContent.tsx @@ -4,8 +4,8 @@ import { useSelectInputContext } from '../context'; // This is just a placeholder, do not code any logic here -export default React.forwardRef(function MultipleContent(_, ref) { - const { prefixCls, displayValues: value } = useSelectInputContext(); +export default React.forwardRef(function MultipleContent(_, ref) { + const { prefixCls, displayValues: value, maxLength } = useSelectInputContext(); // For multiple mode, we show all values as a comma-separated string const displayValue = value @@ -14,7 +14,7 @@ export default React.forwardRef(function MultipleCont return (
- +
); }); diff --git a/src/SelectInput/Content/SingleContent.tsx b/src/SelectInput/Content/SingleContent.tsx index 7d0916d36..66aae072e 100644 --- a/src/SelectInput/Content/SingleContent.tsx +++ b/src/SelectInput/Content/SingleContent.tsx @@ -3,8 +3,8 @@ import Input from '../Input'; import { useSelectInputContext } from '../context'; import Placeholder from './Placeholder'; -export default React.forwardRef(function SingleContent(_, ref) { - const { prefixCls, searchValue, displayValues } = useSelectInputContext(); +export default React.forwardRef(function SingleContent(_, ref) { + const { prefixCls, searchValue, displayValues, maxLength } = useSelectInputContext(); const displayValue = displayValues[0]; @@ -22,7 +22,7 @@ export default React.forwardRef(function SingleConten ) : ( )} - +
); }); diff --git a/src/SelectInput/Input.tsx b/src/SelectInput/Input.tsx index d255f2ae1..7a73d878d 100644 --- a/src/SelectInput/Input.tsx +++ b/src/SelectInput/Input.tsx @@ -13,12 +13,14 @@ export interface InputProps { placeholder?: string; className?: string; style?: React.CSSProperties; + maxLength?: number; [key: string]: any; } const Input = React.forwardRef((props, ref) => { const { onChange, onKeyDown, onBlur, ...restProps } = props; - const { prefixCls, mode, onSearch, onSearchSubmit, onInputBlur } = useSelectInputContext(); + const { prefixCls, mode, onSearch, onSearchSubmit, onInputBlur, maxLength } = + useSelectInputContext(); const { triggerOpen } = useBaseProps(); const inputCls = `${prefixCls}-input`; @@ -109,6 +111,7 @@ const Input = React.forwardRef((props, ref) => { {...restProps} ref={ref} className={inputCls} + maxLength={mode === 'combobox' ? maxLength : undefined} onChange={handleChange} onKeyDown={handleKeyDown} onBlur={handleBlur} diff --git a/src/SelectInput/context.ts b/src/SelectInput/context.ts index cecd83303..c284d03eb 100644 --- a/src/SelectInput/context.ts +++ b/src/SelectInput/context.ts @@ -7,6 +7,7 @@ export interface ContentContextProps { displayValues: DisplayValueType[]; placeholder: React.ReactNode; searchValue: string; + maxLength?: number; mode: Mode; onSearch: (searchText: string, fromTyping: boolean, isCompositing: boolean) => void; onSearchSubmit: (searchText: string) => void; diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index 50a0fce08..0c598a3ec 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -26,6 +26,7 @@ export interface SelectInputProps extends Omit void; onSearchSubmit?: (searchText: string) => void; onInputBlur?: () => void; + maxLength?: number; // Add other props that need to be passed through className?: string; style?: React.CSSProperties; @@ -108,6 +109,8 @@ export default React.forwardRef(function Selec }; // ==================== Context ===================== + const maxLength = props.maxLength; + const cachedContext = React.useMemo( () => ({ prefixCls, @@ -119,6 +122,7 @@ export default React.forwardRef(function Selec onSearch, onSearchSubmit, onInputBlur, + maxLength, }), [ prefixCls, @@ -130,6 +134,7 @@ export default React.forwardRef(function Selec onSearch, onSearchSubmit, onInputBlur, + maxLength, ], ); From 012e64fdec177fe8db481b4b3be46353aa242c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Sat, 11 Oct 2025 10:38:21 +0800 Subject: [PATCH 11/72] chore: connect logic --- src/BaseSelect/index.tsx | 6 +- src/SelectInput/index.tsx | 3 + src/hooks/useAllowClear.tsx | 32 ++------- tests/Combobox.test.tsx | 2 + tests/__snapshots__/Combobox.test.tsx.snap | 75 ---------------------- 5 files changed, 15 insertions(+), 103 deletions(-) delete mode 100644 tests/__snapshots__/Combobox.test.tsx.snap diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 9faa9ca63..6a3698da1 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -197,7 +197,7 @@ export interface BaseSelectProps extends BaseSelectPrivateProps, React.AriaAttri * Clear all icon * @deprecated Please use `allowClear` instead **/ - clearIcon?: RenderNode; + clearIcon?: React.ReactNode; /** Selector remove icon */ removeIcon?: RenderNode; @@ -790,7 +790,7 @@ const BaseSelect = React.forwardRef((props, ref) const onClearMouseDown: React.MouseEventHandler = () => { onClear?.(); - selectorRef.current?.focus(); + containerRef.current?.focus(); onDisplayValuesChange([], { type: 'clear', @@ -801,7 +801,6 @@ const BaseSelect = React.forwardRef((props, ref) const { allowClear: mergedAllowClear, clearIcon: clearNode } = useAllowClear( prefixCls, - onClearMouseDown, displayValues, allowClear, clearIcon, @@ -935,6 +934,7 @@ const BaseSelect = React.forwardRef((props, ref) onInputBlur={onInputBlur} // onFocus={onContainerFocus} onBlur={onInternalBlur} + onClearMouseDown={onClearMouseDown} // Open onMouseDown={onInternalMouseDown} /> diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index 0c598a3ec..125c223ec 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -26,6 +26,7 @@ export interface SelectInputProps extends Omit void; onSearchSubmit?: (searchText: string) => void; onInputBlur?: () => void; + onClearMouseDown?: React.MouseEventHandler; maxLength?: number; // Add other props that need to be passed through className?: string; @@ -63,6 +64,7 @@ export default React.forwardRef(function Selec // Events onMouseDown, onBlur, + onClearMouseDown, ...restProps } = props; @@ -166,6 +168,7 @@ export default React.forwardRef(function Selec onMouseDown={(e) => { // Mark to tell not trigger open or focus (e.nativeEvent as any)._select_lazy = true; + onClearMouseDown?.(e); }} > {clearIcon} diff --git a/src/hooks/useAllowClear.tsx b/src/hooks/useAllowClear.tsx index 80b262254..2e8c46b4b 100644 --- a/src/hooks/useAllowClear.tsx +++ b/src/hooks/useAllowClear.tsx @@ -1,6 +1,6 @@ -import TransBtn from '../TransBtn'; -import type { DisplayValueType, Mode, RenderNode } from '../interface'; -import React, { useMemo } from 'react'; +import type { DisplayValueType, Mode } from '../interface'; +import type React from 'react'; +import { useMemo } from 'react'; export interface AllowClearConfig { allowClear: boolean; @@ -9,10 +9,9 @@ export interface AllowClearConfig { export const useAllowClear = ( prefixCls: string, - onClearMouseDown: React.MouseEventHandler, displayValues: DisplayValueType[], - allowClear?: boolean | { clearIcon?: RenderNode }, - clearIcon?: RenderNode, + allowClear?: boolean | { clearIcon?: React.ReactNode }, + clearIcon?: React.ReactNode, disabled: boolean = false, mergedSearchValue?: string, mode?: Mode, @@ -37,24 +36,7 @@ export const useAllowClear = ( return { allowClear: mergedAllowClear, - clearIcon: mergedAllowClear ? ( - - × - - ) : null, + clearIcon: mergedAllowClear ? allowClearConfig.clearIcon || clearIcon || '×' : null, }; - }, [ - allowClearConfig, - clearIcon, - disabled, - displayValues.length, - mergedSearchValue, - mode, - onClearMouseDown, - prefixCls, - ]); + }, [allowClearConfig, clearIcon, disabled, displayValues.length, mergedSearchValue, mode]); }; diff --git a/tests/Combobox.test.tsx b/tests/Combobox.test.tsx index 17b0fc102..48c11b3fb 100644 --- a/tests/Combobox.test.tsx +++ b/tests/Combobox.test.tsx @@ -31,6 +31,8 @@ describe('Select.Combobox', () => { keyDownTest('combobox'); openControlledTest('combobox'); + return; + it('renders correctly', () => { const { container } = render( - - - Search - - - - -`; - -exports[`Select.Combobox renders correctly 1`] = ` - -`; From d78b95fab2861da18618bd77a459f8bd44d9c843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Sat, 11 Oct 2025 10:49:52 +0800 Subject: [PATCH 12/72] chore: fix logic --- src/BaseSelect/index.tsx | 12 ++++++------ src/SelectInput/Input.tsx | 3 ++- src/SelectInput/context.ts | 1 + src/SelectInput/index.tsx | 8 ++++++-- tests/shared/allowClearTest.tsx | 2 +- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 6a3698da1..90514edbe 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -56,7 +56,6 @@ const DEFAULT_OMIT_PROPS = [ 'onChange', 'removeIcon', 'placeholder', - 'autoFocus', 'maxTagCount', 'maxTagTextLength', 'maxTagPlaceholder', @@ -359,13 +358,14 @@ const BaseSelect = React.forwardRef((props, ref) // =========================== Imperative =========================== React.useImperativeHandle(ref, () => ({ - focus: selectorRef.current?.focus, - blur: selectorRef.current?.blur, + focus: containerRef.current?.focus, + blur: containerRef.current?.blur, scrollTo: (arg) => listRef.current?.scrollTo(arg), nativeElement: - containerRef.current || - selectorRef.current?.nativeElement || - (getDOM(customDomRef.current) as HTMLElement), + // containerRef.current || + // selectorRef.current?.nativeElement || + // (getDOM(customDomRef.current) as HTMLElement), + selectorRef.current?.nativeElement, })); // ========================== Search Value ========================== diff --git a/src/SelectInput/Input.tsx b/src/SelectInput/Input.tsx index 7a73d878d..b95d3d66e 100644 --- a/src/SelectInput/Input.tsx +++ b/src/SelectInput/Input.tsx @@ -19,7 +19,7 @@ export interface InputProps { const Input = React.forwardRef((props, ref) => { const { onChange, onKeyDown, onBlur, ...restProps } = props; - const { prefixCls, mode, onSearch, onSearchSubmit, onInputBlur, maxLength } = + const { prefixCls, mode, onSearch, onSearchSubmit, onInputBlur, maxLength, autoFocus } = useSelectInputContext(); const { triggerOpen } = useBaseProps(); @@ -110,6 +110,7 @@ const Input = React.forwardRef((props, ref) => { void; onSearchSubmit: (searchText: string) => void; diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index 125c223ec..738d7520f 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -61,6 +61,10 @@ export default React.forwardRef(function Selec onSearchSubmit, onInputBlur, + // Input + maxLength, + autoFocus, + // Events onMouseDown, onBlur, @@ -111,8 +115,6 @@ export default React.forwardRef(function Selec }; // ==================== Context ===================== - const maxLength = props.maxLength; - const cachedContext = React.useMemo( () => ({ prefixCls, @@ -125,6 +127,7 @@ export default React.forwardRef(function Selec onSearchSubmit, onInputBlur, maxLength, + autoFocus, }), [ prefixCls, @@ -137,6 +140,7 @@ export default React.forwardRef(function Selec onSearchSubmit, onInputBlur, maxLength, + autoFocus, ], ); diff --git a/tests/shared/allowClearTest.tsx b/tests/shared/allowClearTest.tsx index e4903ee73..30cd2ba73 100644 --- a/tests/shared/allowClearTest.tsx +++ b/tests/shared/allowClearTest.tsx @@ -6,7 +6,7 @@ export default function allowClearTest(mode: any, value: any) { describe('allowClear', () => { it('renders correctly', () => { const { container } = render( + ); }); diff --git a/src/SelectInput/Content/SingleContent.tsx b/src/SelectInput/Content/SingleContent.tsx index 66aae072e..a538248a9 100644 --- a/src/SelectInput/Content/SingleContent.tsx +++ b/src/SelectInput/Content/SingleContent.tsx @@ -2,9 +2,13 @@ import * as React from 'react'; import Input from '../Input'; import { useSelectInputContext } from '../context'; import Placeholder from './Placeholder'; +import type { SharedContentProps } from '.'; -export default React.forwardRef(function SingleContent(_, ref) { - const { prefixCls, searchValue, displayValues, maxLength } = useSelectInputContext(); +export default React.forwardRef(function SingleContent( + { inputProps }, + ref, +) { + const { prefixCls, searchValue, displayValues, maxLength, mode } = useSelectInputContext(); const displayValue = displayValues[0]; @@ -22,7 +26,12 @@ export default React.forwardRef(function SingleContent(_, ref) ) : ( )} - + ); }); diff --git a/src/SelectInput/Content/index.tsx b/src/SelectInput/Content/index.tsx index 65c490261..72f717f49 100644 --- a/src/SelectInput/Content/index.tsx +++ b/src/SelectInput/Content/index.tsx @@ -3,14 +3,22 @@ import SingleContent from './SingleContent'; import MultipleContent from './MultipleContent'; import { useSelectInputContext } from '../context'; +export interface SharedContentProps { + inputProps: React.HTMLAttributes; +} + const SelectContent = React.forwardRef(function SelectContent(_, ref) { - const { multiple } = useSelectInputContext(); + const { multiple, onInputKeyDown } = useSelectInputContext(); + + const sharedInputProps: SharedContentProps['inputProps'] = { + onKeyDown: onInputKeyDown, + }; if (multiple) { - return ; + return ; } - return ; + return ; }); export default SelectContent; diff --git a/src/SelectInput/Input.tsx b/src/SelectInput/Input.tsx index b95d3d66e..708f360a9 100644 --- a/src/SelectInput/Input.tsx +++ b/src/SelectInput/Input.tsx @@ -112,7 +112,6 @@ const Input = React.forwardRef((props, ref) => { ref={ref} autoFocus={autoFocus} className={inputCls} - maxLength={mode === 'combobox' ? maxLength : undefined} onChange={handleChange} onKeyDown={handleKeyDown} onBlur={handleBlur} diff --git a/src/SelectInput/context.ts b/src/SelectInput/context.ts index f708d2835..e439521f5 100644 --- a/src/SelectInput/context.ts +++ b/src/SelectInput/context.ts @@ -1,19 +1,7 @@ -import type { DisplayValueType, Mode } from '../interface'; import * as React from 'react'; +import type { SelectInputProps } from '.'; -export interface ContentContextProps { - prefixCls: string; - multiple: boolean; - displayValues: DisplayValueType[]; - placeholder: React.ReactNode; - searchValue: string; - maxLength?: number; - autoFocus?: boolean; - mode: Mode; - onSearch: (searchText: string, fromTyping: boolean, isCompositing: boolean) => void; - onSearchSubmit: (searchText: string) => void; - onInputBlur: () => void; -} +export type ContentContextProps = SelectInputProps; const SelectInputContext = React.createContext(null!); diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index 452bde179..72784242a 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -4,22 +4,6 @@ import Affix from './Affix'; import SelectContent from './Content'; import SelectInputContext, { type ContentContextProps } from './context'; import type { DisplayValueType, Mode } from '../interface'; -import useBaseProps from '../hooks/useBaseProps'; -import { omit, useEvent } from '@rc-component/util'; - -const DEFAULT_OMIT_PROPS = [ - 'value', - 'onChange', - 'removeIcon', - 'placeholder', - 'maxTagCount', - 'maxTagTextLength', - 'maxTagPlaceholder', - 'choiceTransitionName', - 'onInputKeyDown', - 'onPopupScroll', - 'tabIndex', -] as const; export interface SelectInputRef { focus: (options?: FocusOptions) => void; @@ -41,12 +25,30 @@ export interface SelectInputProps extends Omit void; onInputBlur?: () => void; onClearMouseDown?: React.MouseEventHandler; + onInputKeyDown?: React.KeyboardEventHandler; maxLength?: number; + autoFocus?: boolean; // Add other props that need to be passed through className?: string; style?: React.CSSProperties; [key: string]: any; } +import useBaseProps from '../hooks/useBaseProps'; +import { omit, useEvent } from '@rc-component/util'; + +const DEFAULT_OMIT_PROPS = [ + 'value', + 'onChange', + 'removeIcon', + 'placeholder', + 'maxTagCount', + 'maxTagTextLength', + 'maxTagPlaceholder', + 'choiceTransitionName', + 'onInputKeyDown', + 'onPopupScroll', + 'tabIndex', +] as const; export default React.forwardRef(function SelectInput( props: SelectInputProps, @@ -83,6 +85,7 @@ export default React.forwardRef(function Selec onMouseDown, onBlur, onClearMouseDown, + onInputKeyDown, ...restProps } = props; @@ -128,41 +131,11 @@ export default React.forwardRef(function Selec onBlur?.(event); }; - // ==================== Context ===================== - const cachedContext = React.useMemo( - () => ({ - prefixCls, - multiple: !!multiple, - displayValues, - placeholder, - searchValue, - mode, - onSearch, - onSearchSubmit, - onInputBlur, - maxLength, - autoFocus, - }), - [ - prefixCls, - multiple, - displayValues, - placeholder, - searchValue, - mode, - onSearch, - onSearchSubmit, - onInputBlur, - maxLength, - autoFocus, - ], - ); - // ===================== Render ===================== const domProps = omit(restProps, DEFAULT_OMIT_PROPS); return ( - +
Date: Sat, 11 Oct 2025 11:23:48 +0800 Subject: [PATCH 16/72] chore: adjust omit position --- tests/Combobox.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Combobox.test.tsx b/tests/Combobox.test.tsx index 48c11b3fb..23dc20ec0 100644 --- a/tests/Combobox.test.tsx +++ b/tests/Combobox.test.tsx @@ -31,8 +31,6 @@ describe('Select.Combobox', () => { keyDownTest('combobox'); openControlledTest('combobox'); - return; - it('renders correctly', () => { const { container } = render( diff --git a/tests/Multiple.test.tsx b/tests/Multiple.test.tsx index 8048fc56b..7c3a51312 100644 --- a/tests/Multiple.test.tsx +++ b/tests/Multiple.test.tsx @@ -422,9 +422,9 @@ describe('Select.Multiple', () => { const { container } = render( , ); const inputSpy = jest.spyOn(container1.querySelector('input'), 'focus'); - fireEvent.mouseDown(container1.querySelector('.rc-select-selection-placeholder')); - fireEvent.click(container1.querySelector('.rc-select-selection-placeholder')); + fireEvent.mouseDown(container1.querySelector('.rc-select-placeholder')); + fireEvent.click(container1.querySelector('.rc-select-placeholder')); expect(inputSpy).toHaveBeenCalled(); }); }); diff --git a/tests/__snapshots__/Combobox.test.tsx.snap b/tests/__snapshots__/Combobox.test.tsx.snap new file mode 100644 index 000000000..0fdcd8888 --- /dev/null +++ b/tests/__snapshots__/Combobox.test.tsx.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Select.Combobox renders controlled correctly 1`] = ` + +`; + +exports[`Select.Combobox renders correctly 1`] = ` + +`; diff --git a/tests/placeholder.test.tsx b/tests/placeholder.test.tsx index 9f6229d68..543ae637e 100644 --- a/tests/placeholder.test.tsx +++ b/tests/placeholder.test.tsx @@ -6,26 +6,26 @@ import { render } from '@testing-library/react'; describe('Select placeholder', () => { it('when searchValue is controlled', () => { const { container } = render(); - expect(container.querySelector('.rc-select-selection-placeholder')).toBeTruthy(); - expect(container.querySelector('.rc-select-selection-placeholder').textContent).toBe('bamboo'); + expect(container.querySelector('.rc-select-placeholder')).toBeTruthy(); + expect(container.querySelector('.rc-select-placeholder').textContent).toBe('bamboo'); }); it('not when value is null but it is an Option', () => { const { container } = render( , ); - expect(container.querySelector('.rc-select-selection-placeholder')).toHaveStyle({ + expect(container.querySelector('.rc-select-placeholder')).toHaveStyle({ visibility: 'hidden', }); - expect(container.querySelector('.rc-select-selection-placeholder').textContent).toBe( - 'placeholder', - ); + expect(container.querySelector('.rc-select-placeholder').textContent).toBe('placeholder'); }); }); From 7f38025baea392f4660a74a90b063e2fca5ec915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Sat, 11 Oct 2025 11:36:53 +0800 Subject: [PATCH 18/72] chore: test config --- jest.config.js | 3 +++ tests/Combobox.test.tsx | 6 +++--- tests/setup.ts | 1 + tests/utils/common.ts | 4 ++-- 4 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 jest.config.js create mode 100644 tests/setup.ts diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..5a1e56553 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + setupFilesAfterEnv: ['/tests/setup.ts'], +}; diff --git a/tests/Combobox.test.tsx b/tests/Combobox.test.tsx index 7868d9379..d207e0284 100644 --- a/tests/Combobox.test.tsx +++ b/tests/Combobox.test.tsx @@ -112,11 +112,9 @@ describe('Select.Combobox', () => { toggleOpen(container); selectItem(container); - expect(container.querySelector('input').value).toEqual('1'); + expect(container.querySelector('input')).toHaveValue('1'); }); - return; - describe('input value', () => { const createSelect = (props?: Partial) => ( , ); - expect(container.querySelector('.rc-select-clear-icon')).toBeTruthy(); + expect(container.querySelector('.rc-select-clear')).toBeTruthy(); }); it("should hide clear icon when inputValue is ''", () => { @@ -362,11 +358,13 @@ describe('Select.Combobox', () => { ); fireEvent.change(container.querySelector('input')!, { target: { value: '1' } }); - expect(container.querySelector('.rc-select-clear-icon')).toBeTruthy(); + expect(container.querySelector('.rc-select-clear')).toBeTruthy(); fireEvent.change(container.querySelector('input')!, { target: { value: '' } }); - expect(container.querySelector('.rc-select-clear-icon')).toBeFalsy(); + expect(container.querySelector('.rc-select-clear')).toBeFalsy(); }); + return; + it('autocomplete - option update when input change', () => { class App extends React.Component { public state = { From 9a4d6d3f8ed2c0e27d24b3c717d47b0728ff63b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Sat, 11 Oct 2025 14:45:41 +0800 Subject: [PATCH 21/72] chore: backfill --- src/BaseSelect/index.tsx | 6 +++++ src/SelectInput/Content/Placeholder.tsx | 10 ++++++-- src/SelectInput/Content/SingleContent.tsx | 28 +++++++++++++++++++---- src/SelectInput/Input.tsx | 9 +------- src/SelectInput/index.tsx | 1 + tests/Combobox.test.tsx | 2 +- 6 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index cefafdbd2..73a2968f2 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -474,6 +474,11 @@ const BaseSelect = React.forwardRef((props, ref) }); } + // Open if from typing + if (searchText && fromTyping) { + triggerOpen(true); + } + return ret; }; @@ -923,6 +928,7 @@ const BaseSelect = React.forwardRef((props, ref) displayValues={displayValues} placeholder={placeholder} searchValue={mergedSearchValue} + activeValue={activeValue} onSearch={onInternalSearch} onSearchSubmit={onInternalSearchSubmit} onInputBlur={onInputBlur} diff --git a/src/SelectInput/Content/Placeholder.tsx b/src/SelectInput/Content/Placeholder.tsx index e10f1c1a4..e774062af 100644 --- a/src/SelectInput/Content/Placeholder.tsx +++ b/src/SelectInput/Content/Placeholder.tsx @@ -1,11 +1,17 @@ import * as React from 'react'; import { useSelectInputContext } from '../context'; -export default function Placeholder() { +export interface PlaceholderProps { + hasSearchValue?: boolean; +} + +export default function Placeholder(props: PlaceholderProps) { const { prefixCls, placeholder, displayValues } = useSelectInputContext(); const { searchValue } = useSelectInputContext(); + const { hasSearchValue = !!searchValue } = props; + if (displayValues.length) { return null; } @@ -14,7 +20,7 @@ export default function Placeholder() {
{placeholder} diff --git a/src/SelectInput/Content/SingleContent.tsx b/src/SelectInput/Content/SingleContent.tsx index a538248a9..4f61e0411 100644 --- a/src/SelectInput/Content/SingleContent.tsx +++ b/src/SelectInput/Content/SingleContent.tsx @@ -8,29 +8,49 @@ export default React.forwardRef(function S { inputProps }, ref, ) { - const { prefixCls, searchValue, displayValues, maxLength, mode } = useSelectInputContext(); + const { prefixCls, searchValue, activeValue, displayValues, maxLength, mode, onSearch } = + useSelectInputContext(); + const [inputChanged, setInputChanged] = React.useState(false); + + const combobox = mode === 'combobox'; const displayValue = displayValues[0]; + // Implement the same logic as the old SingleSelector + let mergedSearchValue: string = searchValue || ''; + if (combobox && activeValue && !inputChanged) { + mergedSearchValue = activeValue; + } + + React.useEffect(() => { + if (combobox) { + setInputChanged(false); + } + }, [combobox, activeValue]); + return (
{displayValue ? (
{displayValue.label}
) : ( - + )} { + setInputChanged(true); + inputProps.onChange?.(e); + }} />
); diff --git a/src/SelectInput/Input.tsx b/src/SelectInput/Input.tsx index 2c287e23e..003858321 100644 --- a/src/SelectInput/Input.tsx +++ b/src/SelectInput/Input.tsx @@ -13,12 +13,11 @@ export interface InputProps { className?: string; style?: React.CSSProperties; maxLength?: number; - [key: string]: any; } const Input = React.forwardRef((props, ref) => { const { onChange, onKeyDown, onBlur, ...restProps } = props; - const { prefixCls, mode, onSearch, onSearchSubmit, onInputBlur, onInputKeyDown, autoFocus } = + const { prefixCls, mode, onSearch, onSearchSubmit, onInputBlur, autoFocus } = useSelectInputContext(); const inputCls = `${prefixCls}-input`; @@ -44,12 +43,6 @@ const Input = React.forwardRef((props, ref) => { const { key } = event; const { value } = event.currentTarget; - // Call the internal keyboard handler from context first - // This handles up/down arrow prevention and validate open keys - if (onInputKeyDown) { - onInputKeyDown(event); - } - // Handle Enter key submission - referencing Selector implementation if (key === 'Enter' && mode === 'tags' && !compositionStatusRef.current && onSearchSubmit) { onSearchSubmit(value); diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index 488c9b631..c38c38209 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -20,6 +20,7 @@ export interface SelectInputProps extends Omit void; onSearchSubmit?: (searchText: string) => void; diff --git a/tests/Combobox.test.tsx b/tests/Combobox.test.tsx index 818f1758e..0c4115e0a 100644 --- a/tests/Combobox.test.tsx +++ b/tests/Combobox.test.tsx @@ -319,7 +319,7 @@ describe('Select.Combobox', () => { onChange.mockReset(); keyDown(inputEle, KeyCode.DOWN); - expect(inputEle.value).toEqual('light@gmail.com'); + expect(inputEle).toHaveValue('light@gmail.com'); expect(onChange).not.toHaveBeenCalled(); keyDown(inputEle, KeyCode.ENTER); From 8101a4ced69a1a97ba1dc4c3d345db82b62a6046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Sat, 11 Oct 2025 14:50:22 +0800 Subject: [PATCH 22/72] chore: lots of logic --- src/SelectInput/Content/SingleContent.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/SelectInput/Content/SingleContent.tsx b/src/SelectInput/Content/SingleContent.tsx index 4f61e0411..1b606f4d7 100644 --- a/src/SelectInput/Content/SingleContent.tsx +++ b/src/SelectInput/Content/SingleContent.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import Input from '../Input'; import { useSelectInputContext } from '../context'; +import useBaseProps from '../../hooks/useBaseProps'; import Placeholder from './Placeholder'; import type { SharedContentProps } from '.'; @@ -8,8 +9,9 @@ export default React.forwardRef(function S { inputProps }, ref, ) { - const { prefixCls, searchValue, activeValue, displayValues, maxLength, mode, onSearch } = + const { prefixCls, searchValue, activeValue, displayValues, maxLength, mode } = useSelectInputContext(); + const { triggerOpen } = useBaseProps(); const [inputChanged, setInputChanged] = React.useState(false); @@ -18,7 +20,7 @@ export default React.forwardRef(function S // Implement the same logic as the old SingleSelector let mergedSearchValue: string = searchValue || ''; - if (combobox && activeValue && !inputChanged) { + if (combobox && activeValue && !inputChanged && triggerOpen) { mergedSearchValue = activeValue; } From fcc578d4640b83c7954c3674f690e207f50ebaa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Sat, 11 Oct 2025 14:59:24 +0800 Subject: [PATCH 23/72] chore: lots of logic --- src/SelectInput/index.tsx | 1 + tests/Combobox.test.tsx | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index c38c38209..dec53eda9 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -51,6 +51,7 @@ const DEFAULT_OMIT_PROPS = [ 'onInputKeyDown', 'onPopupScroll', 'tabIndex', + 'activeValue', ] as const; export default React.forwardRef(function SelectInput( diff --git a/tests/Combobox.test.tsx b/tests/Combobox.test.tsx index 0c4115e0a..597ba1994 100644 --- a/tests/Combobox.test.tsx +++ b/tests/Combobox.test.tsx @@ -363,8 +363,6 @@ describe('Select.Combobox', () => { expect(container.querySelector('.rc-select-clear')).toBeFalsy(); }); - return; - it('autocomplete - option update when input change', () => { class App extends React.Component { public state = { @@ -469,6 +467,7 @@ describe('Select.Combobox', () => { }); it('should reset value by control', () => { + jest.useFakeTimers(); const onChange = jest.fn(); const { container } = render( From d04df92c29f249fd3c384b0e472bf35f0adb2f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Sat, 11 Oct 2025 15:38:46 +0800 Subject: [PATCH 24/72] chore: connect combobox --- src/SelectInput/Content/index.tsx | 5 ++++- src/SelectInput/index.tsx | 30 +++++++++++++++++++----------- tests/Combobox.test.tsx | 6 ++---- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/SelectInput/Content/index.tsx b/src/SelectInput/Content/index.tsx index 72f717f49..0111272ff 100644 --- a/src/SelectInput/Content/index.tsx +++ b/src/SelectInput/Content/index.tsx @@ -2,16 +2,19 @@ import * as React from 'react'; import SingleContent from './SingleContent'; import MultipleContent from './MultipleContent'; import { useSelectInputContext } from '../context'; +import useBaseProps from '../../hooks/useBaseProps'; export interface SharedContentProps { - inputProps: React.HTMLAttributes; + inputProps: React.InputHTMLAttributes; } const SelectContent = React.forwardRef(function SelectContent(_, ref) { const { multiple, onInputKeyDown } = useSelectInputContext(); + const { showSearch } = useBaseProps(); const sharedInputProps: SharedContentProps['inputProps'] = { onKeyDown: onInputKeyDown, + readOnly: !showSearch, }; if (multiple) { diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index dec53eda9..0708e54de 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import clsx from 'clsx'; import Affix from './Affix'; import SelectContent from './Content'; import SelectInputContext from './context'; @@ -94,7 +93,7 @@ export default React.forwardRef(function Selec ...restProps } = props; - const { triggerOpen, toggleOpen } = useBaseProps(); + const { triggerOpen, toggleOpen, showSearch, disabled } = useBaseProps(); const rootRef = React.useRef(null); const inputRef = React.useRef(null); @@ -151,14 +150,23 @@ export default React.forwardRef(function Selec // ====================== Open ====================== const onInternalMouseDown: SelectInputProps['onMouseDown'] = useEvent((event) => { - event.preventDefault(); - - if (!(event.nativeEvent as any)._select_lazy) { - inputRef.current?.focus(); - toggleOpen(); - } else if (triggerOpen) { - // Lazy should also close when click clear icon - toggleOpen(false); + if (!disabled) { + event.preventDefault(); + + // Check if we should prevent closing when clicking on selector + // Don't close if: open && not multiple && (combobox mode || showSearch) + const shouldPreventClose = triggerOpen && !multiple && (mode === 'combobox' || showSearch); + + if (!(event.nativeEvent as any)._select_lazy) { + inputRef.current?.focus(); + // Only toggle open if we should not prevent close + if (!shouldPreventClose) { + toggleOpen(); + } + } else if (triggerOpen) { + // Lazy should also close when click clear icon + toggleOpen(false); + } } onMouseDown?.(event); @@ -184,7 +192,7 @@ export default React.forwardRef(function Selec {...domProps} // Style ref={rootRef} - className={clsx(className)} + className={className} style={style} // Open onMouseDown={onInternalMouseDown} diff --git a/tests/Combobox.test.tsx b/tests/Combobox.test.tsx index 597ba1994..dd579bc93 100644 --- a/tests/Combobox.test.tsx +++ b/tests/Combobox.test.tsx @@ -486,8 +486,6 @@ describe('Select.Combobox', () => { jest.useRealTimers(); }); - return; - it('should keep close after blur', async () => { const { container } = render( , ); - const selectorEle = container.querySelector('.rc-select-selector'); + const selectorEle = container.querySelector('.rc-select'); const mouseDownEvent = createEvent.mouseDown(selectorEle); mouseDownEvent.preventDefault = preventDefault; @@ -636,7 +634,7 @@ describe('Select.Combobox', () => { , ); - const selectorEle = container.querySelector('.rc-select-selector'); + const selectorEle = container.querySelector('.rc-select'); const mouseDownEvent = createEvent.mouseDown(selectorEle); mouseDownEvent.preventDefault = preventDefault; From 341a8349e04e427f03df0f3e4d8005fbe185052d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Sat, 11 Oct 2025 15:52:39 +0800 Subject: [PATCH 25/72] test: fix test --- src/SelectInput/Content/index.tsx | 7 ++++++- tests/Accessibility.test.tsx | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/SelectInput/Content/index.tsx b/src/SelectInput/Content/index.tsx index 0111272ff..cae4117d8 100644 --- a/src/SelectInput/Content/index.tsx +++ b/src/SelectInput/Content/index.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import pickAttrs from '@rc-component/util/lib/pickAttrs'; import SingleContent from './SingleContent'; import MultipleContent from './MultipleContent'; import { useSelectInputContext } from '../context'; @@ -10,9 +11,13 @@ export interface SharedContentProps { const SelectContent = React.forwardRef(function SelectContent(_, ref) { const { multiple, onInputKeyDown } = useSelectInputContext(); - const { showSearch } = useBaseProps(); + const baseProps = useBaseProps(); + const { showSearch } = baseProps; + + const ariaProps = pickAttrs(baseProps, { aria: true }); const sharedInputProps: SharedContentProps['inputProps'] = { + ...ariaProps, onKeyDown: onInputKeyDown, readOnly: !showSearch, }; diff --git a/tests/Accessibility.test.tsx b/tests/Accessibility.test.tsx index 1e4df9e90..8b48812ac 100644 --- a/tests/Accessibility.test.tsx +++ b/tests/Accessibility.test.tsx @@ -62,6 +62,10 @@ describe('Select.Accessibility', () => { expect(onActive).toHaveBeenCalledTimes(1); keyDown(container.querySelector('input')!, KeyCode.ENTER); + await act(async () => { + jest.runAllTimers(); + await Promise.resolve(); + }); expectOpen(container, false); // Next Match From 3f538ab6aed435f627437376f3fa545b75c3e8c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Sat, 11 Oct 2025 16:08:46 +0800 Subject: [PATCH 26/72] chore: add polit --- src/BaseSelect/index.tsx | 52 +++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 73a2968f2..d32b624f0 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -350,11 +350,11 @@ const BaseSelect = React.forwardRef((props, ref) const triggerRef = React.useRef(null); const selectorRef = React.useRef(null); const listRef = React.useRef(null); - const blurRef = React.useRef(false); + // const blurRef = React.useRef(false); const customDomRef = React.useRef(null); /** Used for component focused management */ - const [mockFocused, setMockFocused, cancelSetMockFocused] = useDelayReset(); + const [focused, setFocused] = React.useState(false); // =========================== Imperative =========================== React.useImperativeHandle(ref, () => ({ @@ -502,16 +502,16 @@ const BaseSelect = React.forwardRef((props, ref) // ============================ Disabled ============================ // Close dropdown & remove focus state when disabled change - // React.useEffect(() => { - // if (innerOpen && disabled) { - // setInnerOpen(false); - // } + React.useEffect(() => { + // if (mergedOpen && disabled) { + // triggerOpen(false); + // } - // // After onBlur is triggered, the focused does not need to be reset - // if (disabled && !blurRef.current) { - // setMockFocused(false); - // } - // }, [disabled]); + // After onBlur is triggered, the focused does not need to be reset + if (disabled) { + setFocused(false); + } + }, [disabled, mergedOpen]); // ============================ Keyboard ============================ /** @@ -615,17 +615,15 @@ const BaseSelect = React.forwardRef((props, ref) // const focusRef = React.useRef(false); const onInternalFocus: React.FocusEventHandler = (event) => { - // setMockFocused(true); - // if (!disabled) { - // if (onFocus && !focusRef.current) { - // onFocus(...args); - // } - // // `showAction` should handle `focus` if set - // if (showAction.includes('focus')) { - // triggerOpen(true); - // } - // } - // focusRef.current = true; + setFocused(true); + + if (!disabled) { + // `showAction` should handle `focus` if set + if (showAction.includes('focus')) { + triggerOpen(true); + } + } + onFocus?.(event); }; @@ -656,6 +654,8 @@ const BaseSelect = React.forwardRef((props, ref) // }; const onInternalBlur: React.FocusEventHandler = (event) => { + setFocused(false); + if (mergedSearchValue) { // `tags` mode should move `searchValue` into values if (mode === 'tags') { @@ -668,6 +668,7 @@ const BaseSelect = React.forwardRef((props, ref) } } + triggerOpen(false); onBlur?.(event); }; @@ -813,7 +814,7 @@ const BaseSelect = React.forwardRef((props, ref) // ============================= Select ============================= const mergedClassName = clsx(prefixCls, className, { - [`${prefixCls}-focused`]: mockFocused, + [`${prefixCls}-focused`]: focused, [`${prefixCls}-multiple`]: multiple, [`${prefixCls}-single`]: !multiple, [`${prefixCls}-allow-clear`]: mergedAllowClear, @@ -968,7 +969,10 @@ const BaseSelect = React.forwardRef((props, ref) ); return ( - {renderNode} + + + {renderNode} + ); }); From 484c5236a41f304e9966566dfe5620e630086808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Sat, 11 Oct 2025 16:49:20 +0800 Subject: [PATCH 27/72] chore: mv code --- assets/patch.less | 20 +- src/SelectInput/Content/MultipleContent.tsx | 208 +++++++++++++++++++- 2 files changed, 215 insertions(+), 13 deletions(-) diff --git a/assets/patch.less b/assets/patch.less index 2cae26909..6e5299223 100644 --- a/assets/patch.less +++ b/assets/patch.less @@ -23,9 +23,6 @@ .@{select-prefix}-input { border: none; - position: absolute; - inset: 0; - inset: 0; background: transparent; } @@ -61,4 +58,21 @@ top: 0; right: 0; } + + // ============================= Single ============================= + &-single { + .@{select-prefix}-input { + position: absolute; + inset: 0; + } + } + + // ============================ Multiple ============================ + &-multiple { + .@{select-prefix}-item { + background: rgba(0, 0, 0, 0.1); + border-radius: 8px; + margin-right: 4px; + } + } } diff --git a/src/SelectInput/Content/MultipleContent.tsx b/src/SelectInput/Content/MultipleContent.tsx index c66b7d31b..f52d05d2f 100644 --- a/src/SelectInput/Content/MultipleContent.tsx +++ b/src/SelectInput/Content/MultipleContent.tsx @@ -1,29 +1,217 @@ import * as React from 'react'; +import { useState } from 'react'; +import { clsx } from 'clsx'; +import Overflow from 'rc-overflow'; import Input from '../Input'; import { useSelectInputContext } from '../context'; import type { SharedContentProps } from '.'; +import type { DisplayValueType, RawValueType } from '../../interface'; +import type { RenderNode, CustomTagProps } from '../../BaseSelect'; +import TransBtn from '../../TransBtn'; +import { getTitle } from '../../utils/commonUtil'; +import useLayoutEffect from '../../hooks/useLayoutEffect'; +import useBaseProps from '../../hooks/useBaseProps'; -// This is just a placeholder, do not code any logic here +function itemKey(value: DisplayValueType) { + return value.key ?? value.value; +} + +const onPreventMouseDown = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); +}; export default React.forwardRef(function MultipleContent( { inputProps }, ref, ) { - const { prefixCls, displayValues: value, maxLength, mode } = useSelectInputContext(); + const { prefixCls, displayValues, searchValue } = useSelectInputContext(); + const baseProps = useBaseProps(); + const { disabled, showSearch, open, toggleOpen } = baseProps; - // For multiple mode, we show all values as a comma-separated string - const displayValue = value - .map((v) => v.label?.toString() || v.value?.toString() || '') - .join(', '); + const measureRef = React.useRef(null); + const [inputWidth, setInputWidth] = useState(0); + const [focused, setFocused] = useState(false); - return ( -
+ // ===================== Search ====================== + const inputValue = showSearch ? searchValue : ''; + const inputEditable: boolean = showSearch && (open || focused); + + // We measure width and set to the input immediately + useLayoutEffect(() => { + if (measureRef.current) { + setInputWidth(measureRef.current.scrollWidth); + } + }, [inputValue]); + + // These would typically come from parent props - using defaults for now + const removeIcon: RenderNode = '×'; + const maxTagTextLength: number | undefined = undefined; + const maxTagCount: number | 'responsive' | undefined = undefined; + const maxTagPlaceholder = (omittedValues: DisplayValueType[]) => `+ ${omittedValues.length} ...`; + const tagRender: ((props: CustomTagProps) => React.ReactElement) | undefined = undefined; + + const onToggleOpen = (newOpen?: boolean) => { + toggleOpen(newOpen); + }; + + const onRemove = (value: DisplayValueType) => { + // TODO: This should be connected to parent's remove logic + console.log('Remove:', value); + }; + + // ======================== Item ======================== + // >>> Render Selector Node. Includes Item & Rest + const defaultRenderSelector = ( + item: DisplayValueType, + content: React.ReactNode, + itemDisabled: boolean, + closable?: boolean, + onClose?: React.MouseEventHandler, + ) => ( + + {content} + {closable && ( + + × + + )} + + ); + + const customizeRenderSelector = ( + value: RawValueType, + content: React.ReactNode, + itemDisabled: boolean, + closable?: boolean, + onClose?: React.MouseEventHandler, + isMaxTag?: boolean, + info?: { index: number }, + ) => { + const onMouseDown = (e: React.MouseEvent) => { + onPreventMouseDown(e); + onToggleOpen(!open); + }; + return ( + + {tagRender({ + label: content, + value, + index: info?.index, + disabled: itemDisabled, + closable, + onClose, + isMaxTag: !!isMaxTag, + })} + + ); + }; + + // ======================= Input ======================== + // >>> Input Node + const inputNode = ( +
{ + setFocused(true); + }} + onBlur={() => { + setFocused(false); + }} + > + + {/* Measure Node */} + + {inputValue}  +
); + + // ====================== Overflow ====================== + const renderItem = (valueItem: DisplayValueType, info: { index: number }) => { + const { disabled: itemDisabled, label, value } = valueItem; + const closable = !disabled && !itemDisabled; + + let displayLabel: React.ReactNode = label; + + if (typeof maxTagTextLength === 'number') { + if (typeof label === 'string' || typeof label === 'number') { + const strLabel = String(displayLabel); + if (strLabel.length > maxTagTextLength) { + displayLabel = `${strLabel.slice(0, maxTagTextLength)}...`; + } + } + } + + const onClose = (event?: React.MouseEvent) => { + if (event) { + event.stopPropagation(); + } + onRemove(valueItem); + }; + + return typeof tagRender === 'function' + ? customizeRenderSelector( + value, + displayLabel, + itemDisabled, + closable, + onClose, + undefined, + info, + ) + : defaultRenderSelector(valueItem, displayLabel, itemDisabled, closable, onClose); + }; + + const renderRest = (omittedValues: DisplayValueType[]) => { + // https://github.com/ant-design/ant-design/issues/48930 + if (!displayValues.length) { + return null; + } + const content = + typeof maxTagPlaceholder === 'function' + ? maxTagPlaceholder(omittedValues) + : maxTagPlaceholder; + return typeof tagRender === 'function' + ? customizeRenderSelector(undefined, content, false, false, undefined, true) + : defaultRenderSelector({ title: content }, content, false); + }; + + // ======================= Render ======================= + // return ( + // //
+ // {/* */} + + // //
+ // ); + + return ( + + ); }); From 0a18a4fc714001d560e704ae73653f867a191d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Sat, 11 Oct 2025 17:02:43 +0800 Subject: [PATCH 28/72] chore: multiple --- docs/examples/multiple-with-maxCount.tsx | 1 + src/BaseSelect/index.tsx | 6 ++++-- src/SelectInput/Content/MultipleContent.tsx | 7 +++---- src/SelectInput/index.tsx | 3 +++ 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/examples/multiple-with-maxCount.tsx b/docs/examples/multiple-with-maxCount.tsx index a84ddd5ae..2e968c1b9 100644 --- a/docs/examples/multiple-with-maxCount.tsx +++ b/docs/examples/multiple-with-maxCount.tsx @@ -14,6 +14,7 @@ const Test: React.FC = () => { <>

Multiple with maxCount

+ } itemKey={itemKey} maxCount={maxTagCount} /> diff --git a/src/SelectInput/Input.tsx b/src/SelectInput/Input.tsx index 003858321..bc01bbe1d 100644 --- a/src/SelectInput/Input.tsx +++ b/src/SelectInput/Input.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { useSelectInputContext } from './context'; +import useLayoutEffect from '@rc-component/util/lib/hooks/useLayoutEffect'; export interface InputProps { disabled?: boolean; @@ -13,10 +14,12 @@ export interface InputProps { className?: string; style?: React.CSSProperties; maxLength?: number; + /** width always match content width */ + syncWidth?: boolean; } const Input = React.forwardRef((props, ref) => { - const { onChange, onKeyDown, onBlur, ...restProps } = props; + const { onChange, onKeyDown, onBlur, style, syncWidth, value, ...restProps } = props; const { prefixCls, mode, onSearch, onSearchSubmit, onInputBlur, autoFocus } = useSelectInputContext(); @@ -25,27 +28,34 @@ const Input = React.forwardRef((props, ref) => { // Used to handle input method composition status const compositionStatusRef = React.useRef(false); + // ============================== Refs ============================== + const inputRef = React.useRef(null); + + React.useImperativeHandle(ref, () => inputRef.current); + + // ============================== Data ============================== // Handle input changes const handleChange: React.ChangeEventHandler = (event) => { - const { value } = event.target; + const { value: nextVal } = event.target; // Call onSearch callback if (onSearch) { - onSearch(value, true, compositionStatusRef.current); + onSearch(nextVal, true, compositionStatusRef.current); } // Call original onChange callback onChange?.(event); }; + // ============================ Keyboard ============================ // Handle keyboard events const handleKeyDown: React.KeyboardEventHandler = (event) => { const { key } = event; - const { value } = event.currentTarget; + const { value: nextVal } = event.currentTarget; // Handle Enter key submission - referencing Selector implementation if (key === 'Enter' && mode === 'tags' && !compositionStatusRef.current && onSearchSubmit) { - onSearchSubmit(value); + onSearchSubmit(nextVal); } // Call original onKeyDown callback @@ -71,38 +81,41 @@ const Input = React.forwardRef((props, ref) => { compositionStatusRef.current = false; // Trigger search when input method composition ends - const { value } = event.currentTarget; - onSearch?.(value, true, false); + const { value: nextVal } = event.currentTarget; + onSearch?.(nextVal, true, false); }; - // ============================= Mouse ============================== - // const onMouseDown: React.MouseEventHandler = (event) => { - // // const inputMouseDown = getInputMouseDown(); - // // // when mode is combobox and it is disabled, don't prevent default behavior - // // // https://github.com/ant-design/ant-design/issues/37320 - // // // https://github.com/ant-design/ant-design/issues/48281 - // // if ( - // // event.target !== inputRef.current && - // // !inputMouseDown && - // // !(mode === 'combobox' && disabled) - // // ) { - // // event.preventDefault(); - // // } - // // if ((mode !== 'combobox' && (!showSearch || !inputMouseDown)) || !open) { - // // if (open && autoClearSearchValue !== false) { - // // onSearch('', true, false); - // // } - // // onToggleOpen(); - // // } - // }; + // ============================= Width ============================== + const [widthCssVar, setWidthCssVar] = React.useState(undefined); + + // When syncWidth is enabled, adjust input width based on content + useLayoutEffect(() => { + const input = inputRef.current; + + if (syncWidth && input) { + input.style.width = '0px'; + const scrollWidth = input.scrollWidth; + setWidthCssVar(scrollWidth); + + // Reset input style + input.style.width = ''; + } + }, [syncWidth, value]); // ============================= Render ============================= return ( Date: Mon, 13 Oct 2025 10:42:45 +0800 Subject: [PATCH 30/72] chore: clean up --- src/BaseSelect/index.tsx | 2 + src/SelectInput/Content/MultipleContent.tsx | 53 ++------------------- src/SelectInput/index.tsx | 1 + 3 files changed, 7 insertions(+), 49 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 5abe4a80c..1f58c4d5a 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -919,6 +919,8 @@ const BaseSelect = React.forwardRef((props, ref) // Style prefixCls={prefixCls} className={mergedClassName} + // Focus state + focused={focused} // UI prefix={prefix} suffix={suffix} diff --git a/src/SelectInput/Content/MultipleContent.tsx b/src/SelectInput/Content/MultipleContent.tsx index 35a37914f..f6df7fb90 100644 --- a/src/SelectInput/Content/MultipleContent.tsx +++ b/src/SelectInput/Content/MultipleContent.tsx @@ -35,23 +35,13 @@ export default React.forwardRef(function M maxTagTextLength, maxTagCount, tagRender: tagRenderFromContext, + focused, } = useSelectInputContext(); - const { disabled, showSearch, open, toggleOpen } = useBaseProps(); - - const measureRef = React.useRef(null); - const [inputWidth, setInputWidth] = useState(0); - const [focused, setFocused] = useState(false); + const { disabled, showSearch, triggerOpen, toggleOpen } = useBaseProps(); // ===================== Search ====================== const inputValue = showSearch ? searchValue : ''; - const inputEditable: boolean = showSearch && (open || focused); - - // We measure width and set to the input immediately - useLayoutEffect(() => { - if (measureRef.current) { - setInputWidth(measureRef.current.scrollWidth); - } - }, [inputValue]); + const inputEditable: boolean = showSearch && (triggerOpen || focused); // Props from context with safe defaults const removeIcon: RenderNode = removeIconFromContext ?? '×'; @@ -111,7 +101,7 @@ export default React.forwardRef(function M ) => { const onMouseDown = (e: React.MouseEvent) => { onPreventMouseDown(e); - onToggleOpen(!open); + onToggleOpen(!triggerOpen); }; return ( @@ -128,34 +118,6 @@ export default React.forwardRef(function M ); }; - // ======================= Input ======================== - // >>> Input Node - const inputNode = ( -
{ - setFocused(true); - }} - onBlur={() => { - setFocused(false); - }} - > - - - {/* Measure Node */} - - {inputValue}  - -
- ); - // ====================== Overflow ====================== const renderItem = (valueItem: DisplayValueType, info: { index: number }) => { const { disabled: itemDisabled, label, value } = valueItem; @@ -207,13 +169,6 @@ export default React.forwardRef(function M }; // ======================= Render ======================= - // return ( - // //
- // {/* */} - - // //
- // ); - return ( Date: Mon, 13 Oct 2025 11:11:00 +0800 Subject: [PATCH 31/72] chore: fix logic --- src/BaseSelect/index.tsx | 10 +++++----- src/Select.tsx | 2 +- src/hooks/useSearchConfig.ts | 6 ++++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 1f58c4d5a..242e542bc 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -324,8 +324,8 @@ const BaseSelect = React.forwardRef((props, ref) // ============================== MISC ============================== const multiple = isMultiple(mode); - const mergedShowSearch = - (showSearch !== undefined ? showSearch : multiple) || mode === 'combobox'; + // const mergedShowSearch = + // (showSearch !== undefined ? showSearch : multiple) || mode === 'combobox'; const domProps = { ...restProps, @@ -739,7 +739,7 @@ const BaseSelect = React.forwardRef((props, ref) open: mergedOpen, triggerOpen: mergedOpen, id, - showSearch: mergedShowSearch, + showSearch, multiple, toggleOpen: triggerOpen, showScrollBar, @@ -751,7 +751,7 @@ const BaseSelect = React.forwardRef((props, ref) notFoundContent, triggerOpen, id, - mergedShowSearch, + showSearch, multiple, mergedOpen, showScrollBar, @@ -824,7 +824,7 @@ const BaseSelect = React.forwardRef((props, ref) [`${prefixCls}-loading`]: loading, [`${prefixCls}-open`]: mergedOpen, [`${prefixCls}-customize-input`]: customizeInputElement, - [`${prefixCls}-show-search`]: mergedShowSearch, + [`${prefixCls}-show-search`]: showSearch, }); // >>> Selector diff --git a/src/Select.tsx b/src/Select.tsx index b9e2e557c..c9ecf6744 100644 --- a/src/Select.tsx +++ b/src/Select.tsx @@ -238,7 +238,7 @@ const Select = React.forwardRef | undefined, props: SearchConfig, + mode: SelectProps['mode'], ) { const { filterOption, @@ -26,8 +27,9 @@ export default function useSearchConfig( ...(isObject ? showSearch : {}), }; - return [isObject ? true : showSearch, searchConfig]; + return [isObject || (!showSearch && mode === 'tags') ? true : showSearch, searchConfig]; }, [ + mode, showSearch, filterOption, searchValue, From db75428566f7e5c450fc6dad886898ce36290bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 13 Oct 2025 11:53:44 +0800 Subject: [PATCH 32/72] chore: fix logic --- src/BaseSelect/index.tsx | 14 +++++++------- src/SelectInput/Affix.tsx | 5 +++-- src/SelectInput/Input.tsx | 8 ++++++-- src/SelectInput/index.tsx | 12 +++++++++--- src/hooks/useSelectTriggerControl.ts | 2 +- tests/Select.test.tsx | 28 ++++++++++++++++++---------- 6 files changed, 44 insertions(+), 25 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 242e542bc..5df2323d3 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -723,13 +723,13 @@ const BaseSelect = React.forwardRef((props, ref) // }; // } - // // Close when click on non-select element - // useSelectTriggerControl( - // () => [containerRef.current, triggerRef.current?.getPopupElement()], - // triggerOpen, - // onToggleOpen, - // !!customizeRawInputElement, - // ); + // Close when click on non-select element + useSelectTriggerControl( + () => [getDOM(containerRef.current), triggerRef.current?.getPopupElement()], + mergedOpen, + triggerOpen, + !!customizeRawInputElement, + ); // ============================ Context ============================= const baseSelectContext = React.useMemo( diff --git a/src/SelectInput/Affix.tsx b/src/SelectInput/Affix.tsx index afe4cdfaf..aa42697d0 100644 --- a/src/SelectInput/Affix.tsx +++ b/src/SelectInput/Affix.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import clsx from 'clsx'; import { useSelectInputContext } from './context'; export interface AffixProps extends React.HTMLAttributes { @@ -7,7 +8,7 @@ export interface AffixProps extends React.HTMLAttributes { } export default function Affix(props: AffixProps) { - const { type, children, ...restProps } = props; + const { type, children, className, ...restProps } = props; const { prefixCls } = useSelectInputContext(); if (!children) { @@ -15,7 +16,7 @@ export default function Affix(props: AffixProps) { } return ( -
+
{children}
); diff --git a/src/SelectInput/Input.tsx b/src/SelectInput/Input.tsx index bc01bbe1d..78f36486c 100644 --- a/src/SelectInput/Input.tsx +++ b/src/SelectInput/Input.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; +import clsx from 'clsx'; import { useSelectInputContext } from './context'; import useLayoutEffect from '@rc-component/util/lib/hooks/useLayoutEffect'; +import useBaseProps from '../hooks/useBaseProps'; export interface InputProps { disabled?: boolean; @@ -19,11 +21,12 @@ export interface InputProps { } const Input = React.forwardRef((props, ref) => { - const { onChange, onKeyDown, onBlur, style, syncWidth, value, ...restProps } = props; + const { onChange, onKeyDown, onBlur, style, syncWidth, value, className, ...restProps } = props; const { prefixCls, mode, onSearch, onSearchSubmit, onInputBlur, autoFocus } = useSelectInputContext(); + const { classNames, styles } = useBaseProps() || {}; - const inputCls = `${prefixCls}-input`; + const inputCls = clsx(`${prefixCls}-input`, classNames?.input, className); // Used to handle input method composition status const compositionStatusRef = React.useRef(false); @@ -109,6 +112,7 @@ const Input = React.forwardRef((props, ref) => { ref={inputRef} style={ { + ...styles?.input, ...style, '--select-input-width': widthCssVar, } as React.CSSProperties diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index 62289609b..85075731b 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -97,7 +97,7 @@ export default React.forwardRef(function Selec ...restProps } = props; - const { triggerOpen, toggleOpen, showSearch, disabled } = useBaseProps(); + const { triggerOpen, toggleOpen, showSearch, disabled, classNames, styles } = useBaseProps(); const rootRef = React.useRef(null); const inputRef = React.useRef(null); @@ -203,17 +203,23 @@ export default React.forwardRef(function Selec onBlur={onInternalBlur} > {/* Prefix */} - {prefix} + + {prefix} + {/* Content */} {/* Suffix */} - {suffix} + + {suffix} + {/* Clear Icon */} {clearIcon && ( { // Mark to tell not trigger open or focus (e.nativeEvent as any)._select_lazy = true; diff --git a/src/hooks/useSelectTriggerControl.ts b/src/hooks/useSelectTriggerControl.ts index 5ae29c606..4d3906b26 100644 --- a/src/hooks/useSelectTriggerControl.ts +++ b/src/hooks/useSelectTriggerControl.ts @@ -1,7 +1,7 @@ import * as React from 'react'; export default function useSelectTriggerControl( - elements: () => (HTMLElement | undefined)[], + elements: () => (HTMLElement | SVGElement | undefined)[], open: boolean, triggerOpen: (open: boolean) => void, customizedTrigger: boolean, diff --git a/tests/Select.test.tsx b/tests/Select.test.tsx index 09488275d..ad693d879 100644 --- a/tests/Select.test.tsx +++ b/tests/Select.test.tsx @@ -1558,7 +1558,7 @@ describe('Select.Basic', () => { , ); - expect(container.querySelector('.rc-select-arrow-loading')).toBeTruthy(); + expect(container.querySelector('.rc-select-suffix-loading')).toBeTruthy(); }); it('if loading and multiple which has not arrow, but have loading icon', () => { const renderDemo = (loading?: boolean) => ( @@ -1569,11 +1569,11 @@ describe('Select.Basic', () => { ); const { container, rerender } = render(renderDemo()); - expect(container.querySelector('.rc-select-arrow-icon')).toBeFalsy(); - expect(container.querySelector('.rc-select-arrow-loading')).toBeFalsy(); + expect(container.querySelector('.rc-select-suffix-icon')).toBeFalsy(); + expect(container.querySelector('.rc-select-suffix-loading')).toBeFalsy(); rerender(renderDemo(true)); - expect(container.querySelector('.rc-select-arrow-loading')).toBeTruthy(); + expect(container.querySelector('.rc-select-suffix-loading')).toBeTruthy(); }); it('should keep trigger onSelect by select', () => { @@ -1799,6 +1799,8 @@ describe('Select.Basic', () => { }); it('click outside to close select', () => { + jest.useFakeTimers(); + const { container } = render( , ); - expect(container.querySelector('.rc-select-selection-item').getAttribute('title')).toBe( - 'title', - ); + expect(container.querySelector('.rc-select-item').getAttribute('title')).toBe('title'); }); it('should not render title defaultly when label is ReactNode', () => { const { container } = render( , ); - expect(container.querySelector('.rc-select-selection-item-remove')).toBeFalsy(); + expect(container.querySelector('.rc-select-item-remove')).toBeFalsy(); }); it('do not crash if value not in options when removing option', () => { @@ -573,8 +571,8 @@ describe('Select.Multiple', () => { , ); - expect(wrapper1.container.querySelector('.rc-select-selection-item')).toBeFalsy(); - expect(wrapper2.container.querySelector('.rc-select-selection-item')).toBeFalsy(); + expect(wrapper1.container.querySelector('.rc-select-item')).toBeFalsy(); + expect(wrapper2.container.querySelector('.rc-select-item')).toBeFalsy(); }); describe('optionLabelProp', () => { diff --git a/tests/Select.test.tsx b/tests/Select.test.tsx index ad693d879..30413f1d6 100644 --- a/tests/Select.test.tsx +++ b/tests/Select.test.tsx @@ -1830,10 +1830,10 @@ describe('Select.Basic', () => { [undefined].forEach((value) => { it(`to ${value}`, () => { const { container, rerender } = render(); - expect(container.querySelector('.rc-select-selection-item')).toBeFalsy(); + expect(container.querySelector('.rc-select-item')).toBeFalsy(); }); }); }); @@ -1936,7 +1936,7 @@ describe('Select.Basic', () => { toggleOpen(container); selectItem(container, index); expect(onChange).toHaveBeenCalledWith(value, expect.anything()); - expect(container.querySelector('.rc-select-selection-item').textContent).toEqual(showValue); + expect(container.querySelector('.rc-select-item').textContent).toEqual(showValue); }); expect(errorSpy).toHaveBeenCalledWith(warningMessage); @@ -2204,8 +2204,8 @@ describe('Select.Basic', () => { ); expect(container1.querySelector('.rc-select').getAttribute('title')).toBeFalsy(); - expect(container1.querySelector('.rc-select-selection-item').getAttribute('title')).toBe( - 'lucy', - ); + expect(container1.querySelector('.rc-select-item').getAttribute('title')).toBe('lucy'); const { container: container2 } = render(, ); expect(container3.querySelector('.rc-select').getAttribute('title')).toBe('title'); - expect(container3.querySelector('.rc-select-selection-item').getAttribute('title')).toBe( - 'title', - ); + expect(container3.querySelector('.rc-select-item').getAttribute('title')).toBe('title'); }); it('scrollbar should be left position with rtl direction', () => { diff --git a/tests/placeholder.test.tsx b/tests/placeholder.test.tsx index 543ae637e..718e2f7b2 100644 --- a/tests/placeholder.test.tsx +++ b/tests/placeholder.test.tsx @@ -26,9 +26,9 @@ describe('Select placeholder', () => { , ); - expect(container.querySelectorAll('.rc-select-selection-item')).toHaveLength(3); + expect(container.querySelectorAll('.rc-select-item')).toHaveLength(3); }); it('truncates tags by maxTagCount while maxTagCount is 0', () => { @@ -38,7 +38,7 @@ export default function maxTagTextLengthTest(mode: any) { , ); - expect(container.querySelectorAll('.rc-select-selection-item')).toHaveLength(1); + expect(container.querySelectorAll('.rc-select-item')).toHaveLength(1); expect(findSelection(container).textContent).toEqual('+ 3 ...'); }); diff --git a/tests/shared/removeSelectedTest.tsx b/tests/shared/removeSelectedTest.tsx index aa997b699..fe4737e0f 100644 --- a/tests/shared/removeSelectedTest.tsx +++ b/tests/shared/removeSelectedTest.tsx @@ -42,7 +42,7 @@ export default function removeSelectedTest(mode: any) { , ); - expect(container.querySelector('.rc-select-selection-item-remove')).toBeFalsy(); + expect(container.querySelector('.rc-select-item-remove')).toBeFalsy(); }); it('wrap value when labelInValue', () => { diff --git a/tests/utils/common.ts b/tests/utils/common.ts index 9506fb69d..e40bad1ab 100644 --- a/tests/utils/common.ts +++ b/tests/utils/common.ts @@ -34,8 +34,8 @@ export function selectItem(wrapper: any, index: number = 0) { export function findSelection(wrapper: any, index: number = 0) { if (wrapper instanceof HTMLElement) { - const itemNode = wrapper.querySelectorAll('.rc-select-selection-item')[index]; - const contentNode = itemNode.querySelector('.rc-select-selection-item-content'); + const itemNode = wrapper.querySelectorAll('.rc-select-item')[index]; + const contentNode = itemNode.querySelector('.rc-select-item-content'); if (contentNode) { return contentNode; @@ -43,8 +43,8 @@ export function findSelection(wrapper: any, index: number = 0) { return itemNode; } else { - const itemNode = wrapper.find('.rc-select-selection-item').at(index); - const contentNode = itemNode.find('.rc-select-selection-item-content'); + const itemNode = wrapper.find('.rc-select-item').at(index); + const contentNode = itemNode.find('.rc-select-item-content'); if (contentNode.length) { return contentNode; @@ -58,17 +58,17 @@ export function removeSelection(wrapper: any, index: number = 0) { const preventDefault = jest.fn(); if (wrapper instanceof HTMLElement) { - const ele = wrapper.querySelectorAll('.rc-select-selection-item-remove')[index]; + const ele = wrapper.querySelectorAll('.rc-select-item-remove')[index]; const mouseDownEvent = createEvent.mouseDown(ele); mouseDownEvent.preventDefault = preventDefault; fireEvent(ele, mouseDownEvent); - fireEvent.click(wrapper.querySelectorAll('.rc-select-selection-item-remove')[index]); + fireEvent.click(wrapper.querySelectorAll('.rc-select-item-remove')[index]); } else { wrapper - .find('.rc-select-selection-item') + .find('.rc-select-item') .at(index) - .find('.rc-select-selection-item-remove') + .find('.rc-select-item-remove') .last() .simulate('mousedown', { preventDefault }) .simulate('click'); From ef3481335a6b4c39ae7080db6cc58373d1250e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 13 Oct 2025 14:46:08 +0800 Subject: [PATCH 34/72] test: batch update --- src/SelectInput/Content/SingleContent.tsx | 38 ++++++++++++++++++----- tests/Select.test.tsx | 3 +- tests/utils/common.ts | 4 ++- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/SelectInput/Content/SingleContent.tsx b/src/SelectInput/Content/SingleContent.tsx index 1b606f4d7..465a05a09 100644 --- a/src/SelectInput/Content/SingleContent.tsx +++ b/src/SelectInput/Content/SingleContent.tsx @@ -1,9 +1,11 @@ import * as React from 'react'; +import clsx from 'clsx'; import Input from '../Input'; import { useSelectInputContext } from '../context'; import useBaseProps from '../../hooks/useBaseProps'; import Placeholder from './Placeholder'; import type { SharedContentProps } from '.'; +import SelectContext from '../../SelectContext'; export default React.forwardRef(function SingleContent( { inputProps }, @@ -12,6 +14,7 @@ export default React.forwardRef(function S const { prefixCls, searchValue, activeValue, displayValues, maxLength, mode } = useSelectInputContext(); const { triggerOpen } = useBaseProps(); + const selectContext = React.useContext(SelectContext); const [inputChanged, setInputChanged] = React.useState(false); @@ -24,6 +27,32 @@ export default React.forwardRef(function S mergedSearchValue = activeValue; } + // Extract option props, excluding label and value, and handle className/style merging + const optionProps = React.useMemo(() => { + let restProps = { + className: `${prefixCls}-content-value`, + style: { + visibility: mergedSearchValue ? 'hidden' : 'visible', + }, + }; + + if (displayValue && selectContext?.flattenOptions) { + const option = selectContext.flattenOptions.find((opt) => opt.value === displayValue.value); + if (option?.data) { + const { label, value, className, style, ...rest } = option.data; + + restProps = { + ...restProps, + ...rest, + className: clsx(restProps.className, className), + style: { ...restProps.style, ...style }, + }; + } + } + + return restProps; + }, [displayValue, selectContext?.flattenOptions, prefixCls, mergedSearchValue]); + React.useEffect(() => { if (combobox) { setInputChanged(false); @@ -33,14 +62,7 @@ export default React.forwardRef(function S return (
{displayValue ? ( -
- {displayValue.label} -
+
{displayValue.label}
) : ( )} diff --git a/tests/Select.test.tsx b/tests/Select.test.tsx index 30413f1d6..39ab25045 100644 --- a/tests/Select.test.tsx +++ b/tests/Select.test.tsx @@ -2204,8 +2204,7 @@ describe('Select.Basic', () => { @@ -1873,9 +1875,25 @@ describe('Select.Basic', () => { const { container, rerender } = render(renderDemo()); toggleOpen(container); + act(() => { + jest.runAllTimers(); + }); + console.log('~~~~1'); rerender(renderDemo(true)); + act(() => { + jest.runAllTimers(); + }); + + console.log('~~~~2'); rerender(renderDemo(false)); + + act(() => { + jest.runAllTimers(); + }); + expectOpen(container, false); + + jest.useRealTimers(); }); }); @@ -2113,9 +2131,7 @@ describe('Select.Basic', () => { const { container } = render(); - fireEvent.click(container.querySelector('.rc-select-selector')); + fireEvent.click(container.querySelector('.rc-select')); expect(onClick).toHaveBeenCalled(); }); diff --git a/tests/focus.test.tsx b/tests/focus.test.tsx index c35c976d2..f1a7bca6e 100644 --- a/tests/focus.test.tsx +++ b/tests/focus.test.tsx @@ -78,7 +78,7 @@ describe('Select.Focus', () => { const focusFn = jest.spyOn(container.querySelector('input'), 'focus'); - fireEvent.click(container.querySelector('.rc-select-selector')); + fireEvent.click(container.querySelector('.rc-select')); jest.runAllTimers(); expect(focusFn).toHaveBeenCalled(); diff --git a/tests/shared/blurTest.tsx b/tests/shared/blurTest.tsx index 2ad391466..154cabacc 100644 --- a/tests/shared/blurTest.tsx +++ b/tests/shared/blurTest.tsx @@ -62,11 +62,9 @@ export default function blurTest(mode: any) { handleBlur.mockReset(); const preventDefault = jest.fn(); - const mouseDownEvent = createEvent.mouseDown( - wrapper.container.querySelector('.rc-select-selector'), - ); + const mouseDownEvent = createEvent.mouseDown(wrapper.container.querySelector('.rc-select')); mouseDownEvent.preventDefault = preventDefault; - fireEvent(wrapper.container.querySelector('.rc-select-selector'), mouseDownEvent); + fireEvent(wrapper.container.querySelector('.rc-select'), mouseDownEvent); expect(preventDefault).toHaveBeenCalled(); expect(handleBlur).not.toHaveBeenCalled(); From afdf9c53f04bd03f0d833a8efc22338fe39e5f6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 13 Oct 2025 15:48:44 +0800 Subject: [PATCH 36/72] fix: display logic --- src/BaseSelect/index.tsx | 4 ++-- src/SelectInput/Content/SingleContent.tsx | 13 ++++++++----- src/utils/keyUtil.ts | 5 +++++ tests/shared/inputFilterTest.tsx | 8 +++++++- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 02c365f69..e93d2b0a5 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -395,11 +395,11 @@ const BaseSelect = React.forwardRef((props, ref) ); // ============================== Open ============================== - // Not trigger `open` in `combobox` when `notFoundContent` is empty + // Not trigger `open` when `notFoundContent` is empty const emptyListContent = !notFoundContent && emptyOptions; const [mergedOpen, triggerOpen] = useOpen(open, onPopupVisibleChange, (nextOpen) => - disabled || (emptyListContent && mergedOpen && mode === 'combobox') ? false : nextOpen, + disabled || emptyListContent ? false : nextOpen, ); // // SSR not support Portal which means we need delay `open` for the first time render diff --git a/src/SelectInput/Content/SingleContent.tsx b/src/SelectInput/Content/SingleContent.tsx index 465a05a09..a6dce94c0 100644 --- a/src/SelectInput/Content/SingleContent.tsx +++ b/src/SelectInput/Content/SingleContent.tsx @@ -22,14 +22,17 @@ export default React.forwardRef(function S const displayValue = displayValues[0]; // Implement the same logic as the old SingleSelector - let mergedSearchValue: string = searchValue || ''; - if (combobox && activeValue && !inputChanged && triggerOpen) { - mergedSearchValue = activeValue; - } + const mergedSearchValue = React.useMemo(() => { + if (combobox && activeValue && !inputChanged && triggerOpen) { + return activeValue; + } + + return searchValue || ''; + }, [combobox, activeValue, inputChanged, triggerOpen, searchValue]); // Extract option props, excluding label and value, and handle className/style merging const optionProps = React.useMemo(() => { - let restProps = { + let restProps: React.HTMLAttributes = { className: `${prefixCls}-content-value`, style: { visibility: mergedSearchValue ? 'hidden' : 'visible', diff --git a/src/utils/keyUtil.ts b/src/utils/keyUtil.ts index bdfae6c2a..ece550ed7 100644 --- a/src/utils/keyUtil.ts +++ b/src/utils/keyUtil.ts @@ -22,6 +22,11 @@ export function isValidateOpenKey(currentKeyCode: number): boolean { KeyCode.EQUALS, KeyCode.CAPS_LOCK, KeyCode.CONTEXT_MENU, + // Arrow keys - should not trigger open when navigating in input + KeyCode.UP, + KeyCode.DOWN, + KeyCode.LEFT, + KeyCode.RIGHT, // F1-F12 KeyCode.F1, KeyCode.F2, diff --git a/tests/shared/inputFilterTest.tsx b/tests/shared/inputFilterTest.tsx index a64f8a53c..f189f6099 100644 --- a/tests/shared/inputFilterTest.tsx +++ b/tests/shared/inputFilterTest.tsx @@ -1,10 +1,12 @@ import * as React from 'react'; import Option from '../../src/Option'; import Select from '../../src/Select'; -import { fireEvent, render } from '@testing-library/react'; +import { act, fireEvent, render } from '@testing-library/react'; export default function inputFilterTest(mode: any) { it('should keep input filter after select when autoClearSearchValue is false', () => { + jest.useFakeTimers(); + const { container } = render( , ); - expect(container.querySelector('.rc-select-clear-icon')).toBeFalsy(); + expect(container.querySelector('.rc-select-clear')).toBeFalsy(); }); it("should show clear icon when inputValue is not ''", () => { diff --git a/tests/Select.test.tsx b/tests/Select.test.tsx index 5b4fbb3fa..143b9aeb6 100644 --- a/tests/Select.test.tsx +++ b/tests/Select.test.tsx @@ -262,7 +262,7 @@ describe('Select.Basic', () => { , ); - expect(container1.querySelector('.rc-select-clear-icon')).toBeTruthy(); + expect(container1.querySelector('.rc-select-clear')).toBeTruthy(); const { container: container2 } = render( , ); - expect(container2.querySelector('.rc-select-clear-icon')).toBeFalsy(); + expect(container2.querySelector('.rc-select-clear')).toBeFalsy(); const { container: container3 } = render( , ); - expect(container.querySelector('.rc-select-clear-icon')).toBeTruthy(); + expect(container.querySelector('.rc-select-clear')).toBeTruthy(); - const mouseDownEvent = createEvent.mouseDown(container.querySelector('.rc-select-clear-icon')); + const mouseDownEvent = createEvent.mouseDown(container.querySelector('.rc-select-clear')); mouseDownEvent.preventDefault = mouseDownPreventDefault; - fireEvent(container.querySelector('.rc-select-clear-icon'), mouseDownEvent); + fireEvent(container.querySelector('.rc-select-clear'), mouseDownEvent); jest.runAllTimers(); expect(container.querySelector('.rc-select').className).toContain('-focused'); diff --git a/tests/__snapshots__/Select.test.tsx.snap b/tests/__snapshots__/Select.test.tsx.snap index 37e74e55a..4f5673f3c 100644 --- a/tests/__snapshots__/Select.test.tsx.snap +++ b/tests/__snapshots__/Select.test.tsx.snap @@ -111,38 +111,19 @@ exports[`Select.Basic no search 1`] = ` class="rc-select rc-select-single" >
- - - - - - 1 - - + 1 +
+
`; From 0ed8ae60998c7077a5d6b8ad6d1f3512d3ba94dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 13 Oct 2025 16:08:56 +0800 Subject: [PATCH 38/72] chore: more logic --- src/SelectInput/Input.tsx | 1 + src/utils/keyUtil.ts | 2 +- tests/Select.test.tsx | 21 ++++++++++++++++++++- tests/utils/common.ts | 6 ++++++ 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/SelectInput/Input.tsx b/src/SelectInput/Input.tsx index 78f36486c..7170bd702 100644 --- a/src/SelectInput/Input.tsx +++ b/src/SelectInput/Input.tsx @@ -108,6 +108,7 @@ const Input = React.forwardRef((props, ref) => { // ============================= Render ============================= return ( { injectRunAllTimers(jest); + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.clearAllTimers(); + jest.useRealTimers(); + }); + allowClearTest(undefined, '903'); focusTest('single', {}); blurTest('single'); @@ -786,7 +795,7 @@ describe('Select.Basic', () => { }); [KeyCode.ENTER, KeyCode.DOWN].forEach((keyCode) => { - it('open on key press', () => { + it(`open on key press: ${keyCode}`, () => { const { container } = render( Date: Mon, 13 Oct 2025 16:42:08 +0800 Subject: [PATCH 40/72] chore: more and more --- src/BaseSelect/index.tsx | 23 ++-- src/SelectInput/Affix.tsx | 13 +-- src/SelectInput/Content/SingleContent.tsx | 129 +++++++++++----------- src/SelectInput/index.tsx | 22 +++- tests/Select.test.tsx | 4 +- 5 files changed, 98 insertions(+), 93 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index e93d2b0a5..fc3f06955 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -32,7 +32,7 @@ import { getSeparatedContent, isValidCount } from '../utils/valueUtil'; import Polite from './Polite'; import useOpen from '../hooks/useOpen'; import { useEvent } from '@rc-component/util'; -export type BaseSelectSemanticName = 'prefix' | 'suffix' | 'input'; +export type BaseSelectSemanticName = 'prefix' | 'suffix' | 'input' | 'clear'; /** * ZombieJ: @@ -582,16 +582,16 @@ const BaseSelect = React.forwardRef((props, ref) onKeyDown?.(event); }; - // // KeyUp - // const onInternalKeyUp: React.KeyboardEventHandler = (event, ...rest) => { - // if (mergedOpen) { - // listRef.current?.onKeyUp(event, ...rest); - // } - // if (event.key === 'Enter') { - // keyLockRef.current = false; - // } - // onKeyUp?.(event, ...rest); - // }; + // KeyUp + const onInternalKeyUp: React.KeyboardEventHandler = (event, ...rest) => { + if (mergedOpen) { + listRef.current?.onKeyUp(event, ...rest); + } + if (event.key === 'Enter') { + keyLockRef.current = false; + } + onKeyUp?.(event, ...rest); + }; // ============================ Selector ============================ const onSelectorRemove = useEvent((val: DisplayValueType) => { @@ -937,6 +937,7 @@ const BaseSelect = React.forwardRef((props, ref) onBlur={onInternalBlur} onClearMouseDown={onClearMouseDown} onKeyDown={onInternalKeyDown} + onKeyUp={onInternalKeyUp} onSelectorRemove={onSelectorRemove} // Open onMouseDown={onInternalMouseDown} diff --git a/src/SelectInput/Affix.tsx b/src/SelectInput/Affix.tsx index aa42697d0..4ebb878a6 100644 --- a/src/SelectInput/Affix.tsx +++ b/src/SelectInput/Affix.tsx @@ -1,23 +1,16 @@ import * as React from 'react'; -import clsx from 'clsx'; -import { useSelectInputContext } from './context'; export interface AffixProps extends React.HTMLAttributes { - type: 'prefix' | 'suffix' | 'clear'; children?: React.ReactNode; } +// Affix is a simple wrapper which should not read context or logical props export default function Affix(props: AffixProps) { - const { type, children, className, ...restProps } = props; - const { prefixCls } = useSelectInputContext(); + const { children, ...restProps } = props; if (!children) { return null; } - return ( -
- {children} -
- ); + return
{children}
; } diff --git a/src/SelectInput/Content/SingleContent.tsx b/src/SelectInput/Content/SingleContent.tsx index a6dce94c0..cc27cb0ef 100644 --- a/src/SelectInput/Content/SingleContent.tsx +++ b/src/SelectInput/Content/SingleContent.tsx @@ -7,78 +7,79 @@ import Placeholder from './Placeholder'; import type { SharedContentProps } from '.'; import SelectContext from '../../SelectContext'; -export default React.forwardRef(function SingleContent( - { inputProps }, - ref, -) { - const { prefixCls, searchValue, activeValue, displayValues, maxLength, mode } = - useSelectInputContext(); - const { triggerOpen } = useBaseProps(); - const selectContext = React.useContext(SelectContext); +const SingleContent = React.forwardRef( + ({ inputProps }, ref) => { + const { prefixCls, searchValue, activeValue, displayValues, maxLength, mode } = + useSelectInputContext(); + const { triggerOpen } = useBaseProps(); + const selectContext = React.useContext(SelectContext); - const [inputChanged, setInputChanged] = React.useState(false); + const [inputChanged, setInputChanged] = React.useState(false); - const combobox = mode === 'combobox'; - const displayValue = displayValues[0]; + const combobox = mode === 'combobox'; + const displayValue = displayValues[0]; - // Implement the same logic as the old SingleSelector - const mergedSearchValue = React.useMemo(() => { - if (combobox && activeValue && !inputChanged && triggerOpen) { - return activeValue; - } + // Implement the same logic as the old SingleSelector + const mergedSearchValue = React.useMemo(() => { + if (combobox && activeValue && !inputChanged && triggerOpen) { + return activeValue; + } - return searchValue || ''; - }, [combobox, activeValue, inputChanged, triggerOpen, searchValue]); + return searchValue || ''; + }, [combobox, activeValue, inputChanged, triggerOpen, searchValue]); - // Extract option props, excluding label and value, and handle className/style merging - const optionProps = React.useMemo(() => { - let restProps: React.HTMLAttributes = { - className: `${prefixCls}-content-value`, - style: { - visibility: mergedSearchValue ? 'hidden' : 'visible', - }, - }; + // Extract option props, excluding label and value, and handle className/style merging + const optionProps = React.useMemo(() => { + let restProps: React.HTMLAttributes = { + className: `${prefixCls}-content-value`, + style: { + visibility: mergedSearchValue ? 'hidden' : 'visible', + }, + }; - if (displayValue && selectContext?.flattenOptions) { - const option = selectContext.flattenOptions.find((opt) => opt.value === displayValue.value); - if (option?.data) { - const { label, value, className, style, ...rest } = option.data; + if (displayValue && selectContext?.flattenOptions) { + const option = selectContext.flattenOptions.find((opt) => opt.value === displayValue.value); + if (option?.data) { + const { label, value, className, style, key, ...rest } = option.data; - restProps = { - ...restProps, - ...rest, - className: clsx(restProps.className, className), - style: { ...restProps.style, ...style }, - }; + restProps = { + ...restProps, + ...rest, + className: clsx(restProps.className, className), + style: { ...restProps.style, ...style }, + }; + } } - } - return restProps; - }, [displayValue, selectContext?.flattenOptions, prefixCls, mergedSearchValue]); + return restProps; + }, [displayValue, selectContext?.flattenOptions, prefixCls, mergedSearchValue]); + + React.useEffect(() => { + if (combobox) { + setInputChanged(false); + } + }, [combobox, activeValue]); - React.useEffect(() => { - if (combobox) { - setInputChanged(false); - } - }, [combobox, activeValue]); + return ( +
+ {displayValue ? ( +
{displayValue.label}
+ ) : ( + + )} + { + setInputChanged(true); + inputProps.onChange?.(e); + }} + /> +
+ ); + }, +); - return ( -
- {displayValue ? ( -
{displayValue.label}
- ) : ( - - )} - { - setInputChanged(true); - inputProps.onChange?.(e); - }} - /> -
- ); -}); +export default SingleContent; diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index c8aef1511..dcc802266 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -39,6 +39,7 @@ import useBaseProps from '../hooks/useBaseProps'; import { omit, useEvent } from '@rc-component/util'; import KeyCode from '@rc-component/util/lib/KeyCode'; import { isValidateOpenKey } from '../utils/keyUtil'; +import clsx from 'clsx'; const DEFAULT_OMIT_PROPS = [ 'value', @@ -98,7 +99,8 @@ export default React.forwardRef(function Selec ...restProps } = props; - const { triggerOpen, toggleOpen, showSearch, disabled, classNames, styles } = useBaseProps(); + const { triggerOpen, toggleOpen, showSearch, disabled, loading, classNames, styles } = + useBaseProps(); const rootRef = React.useRef(null); const inputRef = React.useRef(null); @@ -204,7 +206,7 @@ export default React.forwardRef(function Selec onBlur={onInternalBlur} > {/* Prefix */} - + {prefix} @@ -212,15 +214,23 @@ export default React.forwardRef(function Selec {/* Suffix */} - + {suffix} {/* Clear Icon */} {clearIcon && ( { // Mark to tell not trigger open or focus (e.nativeEvent as any)._select_lazy = true; diff --git a/tests/Select.test.tsx b/tests/Select.test.tsx index d90182844..df33c9c8e 100644 --- a/tests/Select.test.tsx +++ b/tests/Select.test.tsx @@ -1572,7 +1572,7 @@ describe('Select.Basic', () => { it('if loading, arrow should show loading icon', () => { const { container } = render( - , @@ -1581,7 +1581,7 @@ describe('Select.Basic', () => { }); it('if loading and multiple which has not arrow, but have loading icon', () => { const renderDemo = (loading?: boolean) => ( - From c91c43b046b838b3f21ca75120b2bda7e78292c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 13 Oct 2025 17:00:37 +0800 Subject: [PATCH 41/72] test: update snapshot --- src/SelectInput/Content/SingleContent.tsx | 14 +++- tests/Select.test.tsx | 8 +- tests/__snapshots__/Select.test.tsx.snap | 96 +++++++---------------- tests/shared/blurTest.tsx | 7 +- 4 files changed, 52 insertions(+), 73 deletions(-) diff --git a/src/SelectInput/Content/SingleContent.tsx b/src/SelectInput/Content/SingleContent.tsx index cc27cb0ef..84ae1d0e9 100644 --- a/src/SelectInput/Content/SingleContent.tsx +++ b/src/SelectInput/Content/SingleContent.tsx @@ -6,12 +6,13 @@ import useBaseProps from '../../hooks/useBaseProps'; import Placeholder from './Placeholder'; import type { SharedContentProps } from '.'; import SelectContext from '../../SelectContext'; +import { getTitle } from '../../utils/commonUtil'; const SingleContent = React.forwardRef( ({ inputProps }, ref) => { const { prefixCls, searchValue, activeValue, displayValues, maxLength, mode } = useSelectInputContext(); - const { triggerOpen } = useBaseProps(); + const { triggerOpen, title: rootTitle } = useBaseProps(); const selectContext = React.useContext(SelectContext); const [inputChanged, setInputChanged] = React.useState(false); @@ -45,14 +46,23 @@ const SingleContent = React.forwardRef( restProps = { ...restProps, ...rest, + title: getTitle(option.data), className: clsx(restProps.className, className), style: { ...restProps.style, ...style }, }; } } + if (displayValue && !restProps.title) { + restProps.title = getTitle(displayValue); + } + + if (rootTitle !== undefined) { + restProps.title = rootTitle; + } + return restProps; - }, [displayValue, selectContext?.flattenOptions, prefixCls, mergedSearchValue]); + }, [displayValue, selectContext?.flattenOptions, prefixCls, mergedSearchValue, rootTitle]); React.useEffect(() => { if (combobox) { diff --git a/tests/Select.test.tsx b/tests/Select.test.tsx index df33c9c8e..94093e4e9 100644 --- a/tests/Select.test.tsx +++ b/tests/Select.test.tsx @@ -2295,15 +2295,17 @@ describe('Select.Basic', () => { it('should support title', () => { const { container: container1 } = render(); expect(container2.querySelector('.rc-select').getAttribute('title')).toBeFalsy(); - expect(container2.querySelector('.rc-select-item').getAttribute('title')).toBe(''); + expect(container2.querySelector('.rc-select-content-value').getAttribute('title')).toBe(''); const { container: container3 } = render( -
- - 2 - - + 2 +
+
- + × +
`; @@ -286,39 +265,24 @@ exports[`Select.Basic render renders data-attributes correctly 1`] = ` exports[`Select.Basic render renders disabled select correctly 1`] = ` `; diff --git a/tests/shared/blurTest.tsx b/tests/shared/blurTest.tsx index 154cabacc..61c5503df 100644 --- a/tests/shared/blurTest.tsx +++ b/tests/shared/blurTest.tsx @@ -2,7 +2,7 @@ import React from 'react'; import Option from '../../src/Option'; import Select from '../../src/Select'; import { injectRunAllTimers } from '../utils/common'; -import { type RenderResult, render, fireEvent, createEvent } from '@testing-library/react'; +import { type RenderResult, render, fireEvent, createEvent, act } from '@testing-library/react'; export default function blurTest(mode: any) { describe(`blur of ${mode}`, () => { @@ -34,8 +34,11 @@ export default function blurTest(mode: any) { it('clears inputValue', () => { fireEvent.change(wrapper.container.querySelector('input'), { target: { value: '1' } }); fireEvent.blur(wrapper.container.querySelector('input')); + act(() => { + jest.runAllTimers(); + }); - expect(wrapper.container.querySelector('input').value).toBe(''); + expect(wrapper.container.querySelector('input')).toHaveValue(''); }); it('blur()', () => { From 8b2da8033940ea384e61d17e57fa57b06b8b4018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 13 Oct 2025 17:07:26 +0800 Subject: [PATCH 42/72] test: update snapshot --- src/SelectInput/Input.tsx | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/SelectInput/Input.tsx b/src/SelectInput/Input.tsx index 10c229de4..62536d60b 100644 --- a/src/SelectInput/Input.tsx +++ b/src/SelectInput/Input.tsx @@ -19,13 +19,25 @@ export interface InputProps { maxLength?: number; /** width always match content width */ syncWidth?: boolean; + /** autoComplete for input */ + autoComplete?: string; } const Input = React.forwardRef((props, ref) => { - const { onChange, onKeyDown, onBlur, style, syncWidth, value, className, ...restProps } = props; + const { + onChange, + onKeyDown, + onBlur, + style, + syncWidth, + value, + className, + autoComplete, + ...restProps + } = props; const { prefixCls, mode, onSearch, onSearchSubmit, onInputBlur, autoFocus } = useSelectInputContext(); - const { id, classNames, styles } = useBaseProps() || {}; + const { id, classNames, styles, open, activeDescendantId } = useBaseProps() || {}; const inputCls = clsx(`${prefixCls}-input`, classNames?.input, className); @@ -121,6 +133,7 @@ const Input = React.forwardRef((props, ref) => { } as React.CSSProperties } autoFocus={autoFocus} + autoComplete={autoComplete || 'off'} className={inputCls} value={value || ''} onChange={handleChange} @@ -128,6 +141,14 @@ const Input = React.forwardRef((props, ref) => { onBlur={handleBlur} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} + // Accessibility attributes + role="combobox" + aria-expanded={open || false} + aria-haspopup="listbox" + aria-owns={`${id}_list`} + aria-autocomplete="list" + aria-controls={`${id}_list`} + aria-activedescendant={open ? activeDescendantId : undefined} // onMouseDown={onMouseDown} /> ); From 27e7b2c9213237be8b1bbb0ca92218fa8f56338a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 13 Oct 2025 17:27:04 +0800 Subject: [PATCH 43/72] test: base test --- src/BaseSelect/index.tsx | 10 +- src/SelectInput/Input.tsx | 6 +- src/hooks/useBaseProps.ts | 1 + tests/Select.test.tsx | 10 +- tests/__snapshots__/Select.test.tsx.snap | 184 +++++++++-------------- 5 files changed, 85 insertions(+), 126 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index fc3f06955..5c9116991 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -620,9 +620,9 @@ const BaseSelect = React.forwardRef((props, ref) if (showAction.includes('focus')) { triggerOpen(true); } - } - onFocus?.(event); + onFocus?.(event); + } }; // const onContainerBlur: React.FocusEventHandler = (...args) => { @@ -666,8 +666,10 @@ const BaseSelect = React.forwardRef((props, ref) } } - triggerOpen(false); - onBlur?.(event); + if (!disabled) { + triggerOpen(false); + onBlur?.(event); + } }; // Give focus back of Select diff --git a/src/SelectInput/Input.tsx b/src/SelectInput/Input.tsx index 62536d60b..af77de17c 100644 --- a/src/SelectInput/Input.tsx +++ b/src/SelectInput/Input.tsx @@ -6,7 +6,6 @@ import useBaseProps from '../hooks/useBaseProps'; export interface InputProps { id?: string; - disabled?: boolean; readOnly?: boolean; value?: string; onChange?: React.ChangeEventHandler; @@ -37,7 +36,7 @@ const Input = React.forwardRef((props, ref) => { } = props; const { prefixCls, mode, onSearch, onSearchSubmit, onInputBlur, autoFocus } = useSelectInputContext(); - const { id, classNames, styles, open, activeDescendantId } = useBaseProps() || {}; + const { id, classNames, styles, open, activeDescendantId, role, disabled } = useBaseProps() || {}; const inputCls = clsx(`${prefixCls}-input`, classNames?.input, className); @@ -135,6 +134,7 @@ const Input = React.forwardRef((props, ref) => { autoFocus={autoFocus} autoComplete={autoComplete || 'off'} className={inputCls} + disabled={disabled} value={value || ''} onChange={handleChange} onKeyDown={handleKeyDown} @@ -142,7 +142,7 @@ const Input = React.forwardRef((props, ref) => { onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} // Accessibility attributes - role="combobox" + role={role || 'combobox'} aria-expanded={open || false} aria-haspopup="listbox" aria-owns={`${id}_list`} diff --git a/src/hooks/useBaseProps.ts b/src/hooks/useBaseProps.ts index 827157440..70bbe6097 100644 --- a/src/hooks/useBaseProps.ts +++ b/src/hooks/useBaseProps.ts @@ -10,6 +10,7 @@ export interface BaseSelectContextProps extends BaseSelectProps { triggerOpen: boolean; multiple: boolean; toggleOpen: (open?: boolean) => void; + role?: React.AriaRole; } export const BaseSelectContext = React.createContext(null); diff --git a/tests/Select.test.tsx b/tests/Select.test.tsx index 94093e4e9..4ace0076b 100644 --- a/tests/Select.test.tsx +++ b/tests/Select.test.tsx @@ -685,8 +685,6 @@ describe('Select.Basic', () => { fireEvent.click(container.querySelector('.rc-select')); expect(focusSpy).toHaveBeenCalled(); - // We should mock trigger focus event since it not work in jsdom - fireEvent.focus(container.querySelector('input')); jest.runAllTimers(); }); @@ -696,7 +694,7 @@ describe('Select.Basic', () => { it('fires focus event', () => { expect(handleFocus).toHaveBeenCalled(); - expect(handleFocus.mock.calls.length).toBe(1); + expect(handleFocus.mock.calls).toHaveLength(1); }); it('set className', () => { @@ -1849,10 +1847,10 @@ describe('Select.Basic', () => { [undefined].forEach((value) => { it(`to ${value}`, () => { const { container, rerender } = render(); - expect(container.querySelector('.rc-select-item')).toBeFalsy(); + expect(container.querySelector('.rc-select-content-value')).toBeFalsy(); }); }); }); @@ -1973,7 +1971,7 @@ describe('Select.Basic', () => { toggleOpen(container); selectItem(container, index); expect(onChange).toHaveBeenCalledWith(value, expect.anything()); - expect(container.querySelector('.rc-select-item').textContent).toEqual(showValue); + expect(container.querySelector('.rc-select-content-value').textContent).toEqual(showValue); }); expect(errorSpy).toHaveBeenCalledWith(warningMessage); diff --git a/tests/__snapshots__/Select.test.tsx.snap b/tests/__snapshots__/Select.test.tsx.snap index ba963e258..367ad15d0 100644 --- a/tests/__snapshots__/Select.test.tsx.snap +++ b/tests/__snapshots__/Select.test.tsx.snap @@ -135,50 +135,36 @@ exports[`Select.Basic render renders aria-attributes correctly 1`] = ` class="antd select-test antd-single antd-allow-clear antd-show-search" >
- - - - - - 2 - - + 2 +
+ - + × + `; @@ -218,48 +204,34 @@ exports[`Select.Basic render renders data-attributes correctly 1`] = ` data-test="test-id" >
- - - - - - 2 - - + 2 +
+ - + × + `; @@ -295,48 +267,34 @@ exports[`Select.Basic render renders role prop correctly 1`] = ` role="button" >
- - - - - - 2 - - + 2 +
+ - + × + `; From 04db27558cdf3cdb4a6e7b48737411451c043c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 13 Oct 2025 17:31:42 +0800 Subject: [PATCH 44/72] chore: update --- src/hooks/useComponents.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/hooks/useComponents.ts b/src/hooks/useComponents.ts index 2a4f0c960..f4394df2e 100644 --- a/src/hooks/useComponents.ts +++ b/src/hooks/useComponents.ts @@ -3,16 +3,18 @@ import SelectInput, { type SelectInputProps } from '../SelectInput'; export interface ComponentsConfig { root?: React.ComponentType; + input?: React.ComponentType; } export interface ReturnType { - root: React.ComponentType; + root: React.ComponentType | string; + input: React.ComponentType | string; } export default function useComponents(components?: ComponentsConfig): ReturnType { return React.useMemo(() => { - const { root: RootComponent = SelectInput } = components || {}; + const { root: RootComponent = SelectInput, input: InputComponent = 'input' } = components || {}; - return { root: RootComponent }; + return { root: RootComponent, input: InputComponent }; }, [components]); } From de31413e2d5ea731ac6e28a94017f97ba50a65af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 13 Oct 2025 17:58:44 +0800 Subject: [PATCH 45/72] chore: update --- src/BaseSelect/index.tsx | 5 +- src/SelectInput/Input.tsx | 36 +- src/SelectInput/index.tsx | 5 + tests/Tags.test.tsx | 10 +- tests/__snapshots__/Tags.test.tsx.snap | 901 +++++++++++-------------- tests/utils/common.ts | 3 + 6 files changed, 434 insertions(+), 526 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 5c9116991..88dbc7f9d 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -462,7 +462,6 @@ const BaseSelect = React.forwardRef((props, ref) onSearchSplit?.(patchLabels); // Should close when paste finish - // onToggleOpen(false); triggerOpen(false); // Tell Selector that break next actions @@ -476,7 +475,7 @@ const BaseSelect = React.forwardRef((props, ref) } // Open if from typing - if (searchText && fromTyping) { + if (searchText && fromTyping && ret) { triggerOpen(true); } @@ -941,6 +940,8 @@ const BaseSelect = React.forwardRef((props, ref) onKeyDown={onInternalKeyDown} onKeyUp={onInternalKeyUp} onSelectorRemove={onSelectorRemove} + // Token handling + tokenWithEnter={tokenWithEnter} // Open onMouseDown={onInternalMouseDown} /> diff --git a/src/SelectInput/Input.tsx b/src/SelectInput/Input.tsx index af77de17c..72e98a72b 100644 --- a/src/SelectInput/Input.tsx +++ b/src/SelectInput/Input.tsx @@ -34,7 +34,7 @@ const Input = React.forwardRef((props, ref) => { autoComplete, ...restProps } = props; - const { prefixCls, mode, onSearch, onSearchSubmit, onInputBlur, autoFocus } = + const { prefixCls, mode, onSearch, onSearchSubmit, onInputBlur, autoFocus, tokenWithEnter } = useSelectInputContext(); const { id, classNames, styles, open, activeDescendantId, role, disabled } = useBaseProps() || {}; @@ -43,6 +43,9 @@ const Input = React.forwardRef((props, ref) => { // Used to handle input method composition status const compositionStatusRef = React.useRef(false); + // Used to handle paste content, similar to original Selector implementation + const pastedTextRef = React.useRef(null); + // ============================== Refs ============================== const inputRef = React.useRef(null); @@ -51,7 +54,20 @@ const Input = React.forwardRef((props, ref) => { // ============================== Data ============================== // Handle input changes const handleChange: React.ChangeEventHandler = (event) => { - const { value: nextVal } = event.target; + let { value: nextVal } = event.target; + + // Handle pasted text with tokenWithEnter, similar to original Selector implementation + if (tokenWithEnter && pastedTextRef.current && /[\r\n]/.test(pastedTextRef.current)) { + // CRLF will be treated as a single space for input element + const replacedText = pastedTextRef.current + .replace(/[\r\n]+$/, '') + .replace(/\r\n/g, ' ') + .replace(/[\r\n]/g, ' '); + nextVal = nextVal.replace(replacedText, pastedTextRef.current); + } + + // Reset pasted text reference + pastedTextRef.current = null; // Call onSearch callback if (onSearch) { @@ -95,9 +111,18 @@ const Input = React.forwardRef((props, ref) => { const handleCompositionEnd: React.CompositionEventHandler = (event) => { compositionStatusRef.current = false; - // Trigger search when input method composition ends - const { value: nextVal } = event.currentTarget; - onSearch?.(nextVal, true, false); + // Trigger search when input method composition ends, similar to original Selector + if (mode !== 'combobox') { + const { value: nextVal } = event.currentTarget; + onSearch?.(nextVal, true, false); + } + }; + + // Handle paste events to track pasted content + const handlePaste: React.ClipboardEventHandler = (event) => { + const { clipboardData } = event; + const pastedValue = clipboardData?.getData('text'); + pastedTextRef.current = pastedValue || ''; }; // ============================= Width ============================== @@ -139,6 +164,7 @@ const Input = React.forwardRef((props, ref) => { onChange={handleChange} onKeyDown={handleKeyDown} onBlur={handleBlur} + onPaste={handlePaste} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} // Accessibility attributes diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index dcc802266..23406c8a2 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -29,6 +29,8 @@ export interface SelectInputProps extends Omit void; maxLength?: number; autoFocus?: boolean; + /** Check if `tokenSeparators` contains `\n` or `\r\n` */ + tokenWithEnter?: boolean; // Add other props that need to be passed through className?: string; style?: React.CSSProperties; @@ -96,6 +98,9 @@ export default React.forwardRef(function Selec onInputKeyDown, onSelectorRemove, + // Token handling + tokenWithEnter, + ...restProps } = props; diff --git a/tests/Tags.test.tsx b/tests/Tags.test.tsx index d045146e5..72283d9f5 100644 --- a/tests/Tags.test.tsx +++ b/tests/Tags.test.tsx @@ -1,4 +1,4 @@ -import { createEvent, fireEvent, render } from '@testing-library/react'; +import { act, createEvent, fireEvent, render } from '@testing-library/react'; import KeyCode from '@rc-component/util/lib/KeyCode'; import { clsx } from 'clsx'; import * as React from 'react'; @@ -62,11 +62,10 @@ describe('Select.Tags', () => { it('tokenize input', () => { const handleChange = jest.fn(); const handleSelect = jest.fn(); - const option2 = ; const { container } = render( , ); @@ -79,6 +78,7 @@ describe('Select.Tags', () => { expect(findSelection(container, 1).textContent).toEqual('3'); expect(findSelection(container, 2).textContent).toEqual('4'); expect(container.querySelector('input').value).toBe(''); + expectOpen(container, false); }); @@ -513,9 +513,7 @@ describe('Select.Tags', () => { const { container } = render( - - - - - + foo + + + + +
+ +
-
-
+
+
+
+
+ `; @@ -522,99 +423,73 @@ exports[`Select.Tags max tag render truncates values by maxTagTextLength 1`] = ` class="rc-select rc-select-multiple rc-select-show-search" >
- -
-
- - - On... - - - -
-
+
-
+ +
+
+ + - -
-
-
+ Tw... + + + +
+
+ +
`; diff --git a/tests/utils/common.ts b/tests/utils/common.ts index d9e488e6e..43c738e99 100644 --- a/tests/utils/common.ts +++ b/tests/utils/common.ts @@ -3,6 +3,9 @@ import { createEvent, fireEvent } from '@testing-library/react'; export function expectOpen(wrapper: any, open: boolean = true) { if (wrapper instanceof HTMLElement) { + act(() => { + jest.runAllTimers(); + }); expect(!!wrapper.querySelector('.rc-select-open')).toBe(open); return; } From 648f73d07eb597e4140b59d86b5ff60cf18e00a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 13 Oct 2025 18:09:34 +0800 Subject: [PATCH 46/72] test: fix test --- src/SelectInput/Content/MultipleContent.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/SelectInput/Content/MultipleContent.tsx b/src/SelectInput/Content/MultipleContent.tsx index f6df7fb90..d64239a14 100644 --- a/src/SelectInput/Content/MultipleContent.tsx +++ b/src/SelectInput/Content/MultipleContent.tsx @@ -31,13 +31,19 @@ export default React.forwardRef(function M searchValue, onSelectorRemove, removeIcon: removeIconFromContext, + + focused, + } = useSelectInputContext(); + const { + disabled, + showSearch, + triggerOpen, + toggleOpen, + tagRender: tagRenderFromContext, maxTagPlaceholder: maxTagPlaceholderFromContext, maxTagTextLength, maxTagCount, - tagRender: tagRenderFromContext, - focused, - } = useSelectInputContext(); - const { disabled, showSearch, triggerOpen, toggleOpen } = useBaseProps(); + } = useBaseProps(); // ===================== Search ====================== const inputValue = showSearch ? searchValue : ''; From 3f98735ec486d5750c7d513a1b2613f3e3401562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Mon, 13 Oct 2025 21:52:40 +0800 Subject: [PATCH 47/72] chore: adjust cls --- src/BaseSelect/index.tsx | 60 ++++++++++----------- src/SelectInput/Content/MultipleContent.tsx | 10 ++-- tests/Tags.test.tsx | 2 +- tests/__snapshots__/Tags.test.tsx.snap | 56 +++++++++---------- tests/shared/maxTagRenderTest.tsx | 4 +- tests/utils/common.ts | 12 ++--- 6 files changed, 73 insertions(+), 71 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 88dbc7f9d..807965712 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -522,7 +522,7 @@ const BaseSelect = React.forwardRef((props, ref) // KeyDown const onInternalKeyDown: React.KeyboardEventHandler = (event) => { - // const clearLock = getClearLock(); + const clearLock = getClearLock(); const { key } = event; const isEnterKey = key === 'Enter'; @@ -539,36 +539,36 @@ const BaseSelect = React.forwardRef((props, ref) } } - // setClearLock(!!mergedSearchValue); - - // // Remove value by `backspace` - // if ( - // key === 'Backspace' && - // !clearLock && - // multiple && - // !mergedSearchValue && - // displayValues.length - // ) { - // const cloneDisplayValues = [...displayValues]; - // let removedDisplayValue = null; - - // for (let i = cloneDisplayValues.length - 1; i >= 0; i -= 1) { - // const current = cloneDisplayValues[i]; - - // if (!current.disabled) { - // cloneDisplayValues.splice(i, 1); - // removedDisplayValue = current; - // break; - // } - // } + setClearLock(!!mergedSearchValue); + + // Remove value by `backspace` + if ( + key === 'Backspace' && + !clearLock && + multiple && + !mergedSearchValue && + displayValues.length + ) { + const cloneDisplayValues = [...displayValues]; + let removedDisplayValue = null; + + for (let i = cloneDisplayValues.length - 1; i >= 0; i -= 1) { + const current = cloneDisplayValues[i]; + + if (!current.disabled) { + cloneDisplayValues.splice(i, 1); + removedDisplayValue = current; + break; + } + } - // if (removedDisplayValue) { - // onDisplayValuesChange(cloneDisplayValues, { - // type: 'remove', - // values: [removedDisplayValue], - // }); - // } - // } + if (removedDisplayValue) { + onDisplayValuesChange(cloneDisplayValues, { + type: 'remove', + values: [removedDisplayValue], + }); + } + } if (mergedOpen && (!isEnterKey || !keyLockRef.current)) { // Lock the Enter key after it is pressed to avoid repeated triggering of the onChange event. diff --git a/src/SelectInput/Content/MultipleContent.tsx b/src/SelectInput/Content/MultipleContent.tsx index d64239a14..53b03ae10 100644 --- a/src/SelectInput/Content/MultipleContent.tsx +++ b/src/SelectInput/Content/MultipleContent.tsx @@ -45,6 +45,8 @@ export default React.forwardRef(function M maxTagCount, } = useBaseProps(); + const selectionItemPrefixCls = `${prefixCls}-selection-item`; + // ===================== Search ====================== const inputValue = showSearch ? searchValue : ''; const inputEditable: boolean = showSearch && (triggerOpen || focused); @@ -78,14 +80,14 @@ export default React.forwardRef(function M ) => ( - {content} + {content} {closable && ( Jack
`; exports[`Select.Multiple max tag render truncates values by maxTagTextLength 1`] = ` - +[ + "On...", + "Tw...", +] `; diff --git a/tests/shared/maxTagRenderTest.tsx b/tests/shared/maxTagRenderTest.tsx index 18b2cde1f..8d93c049e 100644 --- a/tests/shared/maxTagRenderTest.tsx +++ b/tests/shared/maxTagRenderTest.tsx @@ -14,7 +14,11 @@ export default function maxTagTextLengthTest(mode: any) { , ); - expect(container.firstChild).toMatchSnapshot(); + expect( + Array.from(container.querySelectorAll('.rc-select-selection-item-content')).map( + (ele) => ele.textContent, + ), + ).toMatchSnapshot(); }); it('truncates tags by maxTagCount', () => { @@ -44,14 +48,15 @@ export default function maxTagTextLengthTest(mode: any) { it('not display maxTagPlaceholder if maxTagCount not reach', () => { const { container } = render( - , ); - expect(container.firstChild).toMatchSnapshot(); + expect(container.querySelectorAll('.rc-select-content-item')).toHaveLength(2); + expect(container.querySelector('.rc-select-content-item-rest')).toBeFalsy(); }); it('truncates tags by maxTagCount and show maxTagPlaceholder', () => { @@ -68,7 +73,8 @@ export default function maxTagTextLengthTest(mode: any) { , ); - expect(container.firstChild).toMatchSnapshot(); + expect(container.querySelectorAll('.rc-select-content-item')).toHaveLength(4); + expect(container.querySelector('.rc-select-content-item-rest')).toHaveTextContent('Omitted'); }); it('truncates tags by maxTagCount and show maxTagPlaceholder function', () => { @@ -88,7 +94,10 @@ export default function maxTagTextLengthTest(mode: any) { , ); - expect(container.firstChild).toMatchSnapshot(); + expect(container.querySelectorAll('.rc-select-content-item')).toHaveLength(4); + expect(container.querySelector('.rc-select-content-item-rest')).toHaveTextContent( + '1 values omitted', + ); }); it('tagRender should work on maxTagPlaceholder', () => { From 46492410e71380786bd0672cbba5fb461487062a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 14 Oct 2025 11:29:02 +0800 Subject: [PATCH 51/72] test: all of multiple --- src/SelectInput/Content/MultipleContent.tsx | 12 ++- src/SelectInput/index.tsx | 21 ++++- tests/__snapshots__/Multiple.test.tsx.snap | 93 --------------------- 3 files changed, 28 insertions(+), 98 deletions(-) diff --git a/src/SelectInput/Content/MultipleContent.tsx b/src/SelectInput/Content/MultipleContent.tsx index 55f7e98c4..1ef40a770 100644 --- a/src/SelectInput/Content/MultipleContent.tsx +++ b/src/SelectInput/Content/MultipleContent.tsx @@ -30,16 +30,16 @@ export default React.forwardRef(function M prefixCls, displayValues, searchValue, + mode, onSelectorRemove, removeIcon: removeIconFromContext, - - focused, } = useSelectInputContext(); const { disabled, showSearch, triggerOpen, toggleOpen, + autoClearSearchValue, tagRender: tagRenderFromContext, maxTagPlaceholder: maxTagPlaceholderFromContext, maxTagTextLength, @@ -49,7 +49,13 @@ export default React.forwardRef(function M const selectionItemPrefixCls = `${prefixCls}-selection-item`; // ===================== Search ====================== - const inputValue = showSearch ? searchValue : ''; + // Apply autoClearSearchValue logic: when dropdown is closed and autoClearSearchValue is not false (default true), clear search value + let computedSearchValue = searchValue; + if (!triggerOpen && mode === 'multiple' && autoClearSearchValue !== false) { + computedSearchValue = ''; + } + + const inputValue = showSearch ? computedSearchValue || '' : ''; const inputEditable: boolean = showSearch && !disabled; // Props from context with safe defaults diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index 23406c8a2..f0aa1d717 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -21,6 +21,7 @@ export interface SelectInputProps extends Omit void; onSearchSubmit?: (searchText: string) => void; onInputBlur?: () => void; @@ -104,8 +105,16 @@ export default React.forwardRef(function Selec ...restProps } = props; - const { triggerOpen, toggleOpen, showSearch, disabled, loading, classNames, styles } = - useBaseProps(); + const { + triggerOpen, + toggleOpen, + showSearch, + disabled, + loading, + classNames, + styles, + autoClearSearchValue, + } = useBaseProps(); const rootRef = React.useRef(null); const inputRef = React.useRef(null); @@ -171,6 +180,14 @@ export default React.forwardRef(function Selec if (!(event.nativeEvent as any)._select_lazy) { inputRef.current?.focus(); + + // Clear search value if autoClearSearchValue is not false when closing + if ((mode !== 'combobox' && (!showSearch || shouldPreventClose)) || !triggerOpen) { + if (triggerOpen && autoClearSearchValue !== false) { + onSearch?.('', true, false); + } + } + // Only toggle open if we should not prevent close if (!shouldPreventClose) { toggleOpen(); diff --git a/tests/__snapshots__/Multiple.test.tsx.snap b/tests/__snapshots__/Multiple.test.tsx.snap index 3c9c338d0..c266dcd51 100644 --- a/tests/__snapshots__/Multiple.test.tsx.snap +++ b/tests/__snapshots__/Multiple.test.tsx.snap @@ -1,98 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Select.Multiple max tag render truncates tags by maxTagCount and show maxTagPlaceholder function 1`] = ` - -`; - exports[`Select.Multiple max tag render truncates values by maxTagTextLength 1`] = ` [ "On...", From 36220d3a55d3d9523e0fd580e22c65de17162018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 14 Oct 2025 11:30:08 +0800 Subject: [PATCH 52/72] chore: clean up --- src/SelectInput/Content/MultipleContent.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/SelectInput/Content/MultipleContent.tsx b/src/SelectInput/Content/MultipleContent.tsx index 1ef40a770..1cba3c25b 100644 --- a/src/SelectInput/Content/MultipleContent.tsx +++ b/src/SelectInput/Content/MultipleContent.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { useState } from 'react'; import { clsx } from 'clsx'; import Overflow from 'rc-overflow'; import Input from '../Input'; @@ -9,7 +8,6 @@ import type { DisplayValueType, RawValueType } from '../../interface'; import type { RenderNode, CustomTagProps } from '../../BaseSelect'; import TransBtn from '../../TransBtn'; import { getTitle } from '../../utils/commonUtil'; -import useLayoutEffect from '@rc-component/util/lib/hooks/useLayoutEffect'; import useBaseProps from '../../hooks/useBaseProps'; import Placeholder from './Placeholder'; From e81f2f5a7a3c844f831f399cd4b576db6b9c7a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 14 Oct 2025 12:00:33 +0800 Subject: [PATCH 53/72] chore: adjust logic --- src/BaseSelect/index.tsx | 6 +++++- src/hooks/useComponents.ts | 21 ++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 807965712..03319c91b 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -908,7 +908,9 @@ const BaseSelect = React.forwardRef((props, ref) // ); // } - const { root: RootComponent } = useComponents(components); + const mergedComponents = useComponents(components); + const { root: RootComponent } = mergedComponents; + renderNode = ( ((props, ref) tokenWithEnter={tokenWithEnter} // Open onMouseDown={onInternalMouseDown} + // Components + components={mergedComponents} /> ); diff --git a/src/hooks/useComponents.ts b/src/hooks/useComponents.ts index f4394df2e..a9f7d090d 100644 --- a/src/hooks/useComponents.ts +++ b/src/hooks/useComponents.ts @@ -2,19 +2,26 @@ import * as React from 'react'; import SelectInput, { type SelectInputProps } from '../SelectInput'; export interface ComponentsConfig { - root?: React.ComponentType; - input?: React.ComponentType; + root?: React.ComponentType | string; + input?: + | React.ComponentType< + | React.TextareaHTMLAttributes + | React.InputHTMLAttributes + > + | string; } -export interface ReturnType { - root: React.ComponentType | string; - input: React.ComponentType | string; +export interface FilledComponentsConfig { + root: React.ComponentType; + input: React.ComponentType< + React.TextareaHTMLAttributes | React.InputHTMLAttributes + >; } -export default function useComponents(components?: ComponentsConfig): ReturnType { +export default function useComponents(components?: ComponentsConfig) { return React.useMemo(() => { const { root: RootComponent = SelectInput, input: InputComponent = 'input' } = components || {}; - return { root: RootComponent, input: InputComponent }; + return { root: RootComponent, input: InputComponent } as FilledComponentsConfig; }, [components]); } From de3f3a69f7a85399196157af218a8da8285725eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 14 Oct 2025 15:42:16 +0800 Subject: [PATCH 54/72] chore: replace components --- src/BaseSelect/index.tsx | 8 ++------ src/SelectInput/Input.tsx | 14 +++++++++++--- src/SelectInput/index.tsx | 25 +++++++++++-------------- src/hooks/useComponents.ts | 17 +++++++---------- tests/Multiple.test.tsx | 8 ++++---- 5 files changed, 35 insertions(+), 37 deletions(-) diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 03319c91b..0a878ff5b 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -1,7 +1,5 @@ import type { AlignType, BuildInPlacements } from '@rc-component/trigger/lib/interface'; import { clsx } from 'clsx'; -import useLayoutEffect from '@rc-component/util/lib/hooks/useLayoutEffect'; -import useControlledState from '@rc-component/util/lib/hooks/useControlledState'; import isMobile from '@rc-component/util/lib/isMobile'; import { useComposeRef } from '@rc-component/util/lib/ref'; import { getDOM } from '@rc-component/util/lib/Dom/findDOMNode'; @@ -10,7 +8,6 @@ import * as React from 'react'; import { useAllowClear } from '../hooks/useAllowClear'; import { BaseSelectContext } from '../hooks/useBaseProps'; import type { BaseSelectContextProps } from '../hooks/useBaseProps'; -import useDelayReset from '../hooks/useDelayReset'; import useLock from '../hooks/useLock'; import useSelectTriggerControl from '../hooks/useSelectTriggerControl'; import useComponents, { type ComponentsConfig } from '../hooks/useComponents'; @@ -24,14 +21,13 @@ import type { RenderNode, } from '../interface'; import type { RefSelectorProps } from '../Selector'; -import Selector from '../Selector'; import type { RefTriggerProps } from '../SelectTrigger'; import SelectTrigger from '../SelectTrigger'; -import TransBtn from '../TransBtn'; import { getSeparatedContent, isValidCount } from '../utils/valueUtil'; import Polite from './Polite'; import useOpen from '../hooks/useOpen'; import { useEvent } from '@rc-component/util'; +import type { SelectInputRef } from '../SelectInput'; export type BaseSelectSemanticName = 'prefix' | 'suffix' | 'input' | 'clear'; /** @@ -347,7 +343,7 @@ const BaseSelect = React.forwardRef((props, ref) }, []); // ============================== Refs ============================== - const containerRef = React.useRef(null); + const containerRef = React.useRef(null); const triggerRef = React.useRef(null); const selectorRef = React.useRef(null); const listRef = React.useRef(null); diff --git a/src/SelectInput/Input.tsx b/src/SelectInput/Input.tsx index 72e98a72b..d6237c405 100644 --- a/src/SelectInput/Input.tsx +++ b/src/SelectInput/Input.tsx @@ -34,8 +34,16 @@ const Input = React.forwardRef((props, ref) => { autoComplete, ...restProps } = props; - const { prefixCls, mode, onSearch, onSearchSubmit, onInputBlur, autoFocus, tokenWithEnter } = - useSelectInputContext(); + const { + prefixCls, + mode, + onSearch, + onSearchSubmit, + onInputBlur, + autoFocus, + tokenWithEnter, + components: { input: InputComponent }, + } = useSelectInputContext(); const { id, classNames, styles, open, activeDescendantId, role, disabled } = useBaseProps() || {}; const inputCls = clsx(`${prefixCls}-input`, classNames?.input, className); @@ -144,7 +152,7 @@ const Input = React.forwardRef((props, ref) => { // ============================= Render ============================= return ( - void; @@ -36,13 +43,8 @@ export interface SelectInputProps extends Omit(function Selec // ====================== Open ====================== const onInternalMouseDown: SelectInputProps['onMouseDown'] = useEvent((event) => { if (!disabled) { - event.preventDefault(); + if (event.target !== getDOM(inputRef.current)) { + event.preventDefault(); + } // Check if we should prevent closing when clicking on selector // Don't close if: open && not multiple && (combobox mode || showSearch) @@ -181,13 +185,6 @@ export default React.forwardRef(function Selec if (!(event.nativeEvent as any)._select_lazy) { inputRef.current?.focus(); - // Clear search value if autoClearSearchValue is not false when closing - if ((mode !== 'combobox' && (!showSearch || shouldPreventClose)) || !triggerOpen) { - if (triggerOpen && autoClearSearchValue !== false) { - onSearch?.('', true, false); - } - } - // Only toggle open if we should not prevent close if (!shouldPreventClose) { toggleOpen(); diff --git a/src/hooks/useComponents.ts b/src/hooks/useComponents.ts index a9f7d090d..1e30863af 100644 --- a/src/hooks/useComponents.ts +++ b/src/hooks/useComponents.ts @@ -1,20 +1,17 @@ import * as React from 'react'; -import SelectInput, { type SelectInputProps } from '../SelectInput'; +import SelectInput, { type SelectInputRef, type SelectInputProps } from '../SelectInput'; export interface ComponentsConfig { root?: React.ComponentType | string; - input?: - | React.ComponentType< - | React.TextareaHTMLAttributes - | React.InputHTMLAttributes - > - | string; + input?: React.ComponentType | string; } export interface FilledComponentsConfig { - root: React.ComponentType; - input: React.ComponentType< - React.TextareaHTMLAttributes | React.InputHTMLAttributes + root: React.ForwardRefExoticComponent>; + input: React.ForwardRefExoticComponent< + | React.TextareaHTMLAttributes + | (React.InputHTMLAttributes & + React.RefAttributes) >; } diff --git a/tests/Multiple.test.tsx b/tests/Multiple.test.tsx index 0fe95dd79..4ef8a5556 100644 --- a/tests/Multiple.test.tsx +++ b/tests/Multiple.test.tsx @@ -678,7 +678,7 @@ describe('Select.Multiple', () => { const { container } = render( , ); - expect(container.firstChild).toMatchSnapshot(); + expect(container.querySelector('input')).toHaveAttribute('readonly'); }); it('should contain falsy children', () => { @@ -2256,15 +2256,15 @@ describe('Select.Basic', () => { />, ); - expect(container.querySelectorAll('span.rc-select-item')[0].getAttribute('title')).toEqual( - 'TitleBamboo', - ); - expect(container.querySelectorAll('span.rc-select-item')[1].getAttribute('title')).toEqual( - 'little', - ); - expect(container.querySelectorAll('span.rc-select-item')[2].getAttribute('title')).toEqual( - '+ 1 ...', - ); + expect( + container.querySelectorAll('span.rc-select-selection-item')[0].getAttribute('title'), + ).toEqual('TitleBamboo'); + expect( + container.querySelectorAll('span.rc-select-selection-item')[1].getAttribute('title'), + ).toEqual('little'); + expect( + container.querySelectorAll('span.rc-select-selection-item')[2].getAttribute('title'), + ).toEqual('+ 1 ...'); }); }); @@ -2424,7 +2424,6 @@ describe('Select.Basic', () => { const ref = React.useRef(null); const onSelect = () => { ref.current!.blur(); - fireEvent.blur(ref.current); }; const getInputElement = () => { return ; diff --git a/tests/__snapshots__/Select.test.tsx.snap b/tests/__snapshots__/Select.test.tsx.snap index 367ad15d0..76ed88b0a 100644 --- a/tests/__snapshots__/Select.test.tsx.snap +++ b/tests/__snapshots__/Select.test.tsx.snap @@ -106,28 +106,6 @@ exports[`Select.Basic filterOption could be true as described in default value 1
`; -exports[`Select.Basic no search 1`] = ` -
-
-
- 1 -
- -
-
-`; - exports[`Select.Basic render renders aria-attributes correctly 1`] = `
@@ -250,8 +235,16 @@ exports[`Select.Basic render renders disabled select correctly 1`] = ` 2
From 2cb9ad2e5e455c321d7cbf86c78cbfe2e7fa1dfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Tue, 14 Oct 2025 17:38:57 +0800 Subject: [PATCH 59/72] test: fix test --- tests/__snapshots__/Combobox.test.tsx.snap | 24 +- tests/__snapshots__/Tags.test.tsx.snap | 292 +-------------------- tests/__snapshots__/ssr.test.tsx.snap | 2 +- 3 files changed, 27 insertions(+), 291 deletions(-) diff --git a/tests/__snapshots__/Combobox.test.tsx.snap b/tests/__snapshots__/Combobox.test.tsx.snap index 0fdcd8888..520d48736 100644 --- a/tests/__snapshots__/Combobox.test.tsx.snap +++ b/tests/__snapshots__/Combobox.test.tsx.snap @@ -2,7 +2,7 @@ exports[`Select.Combobox renders controlled correctly 1`] = `