diff --git a/src/BaseSelect/index.tsx b/src/BaseSelect/index.tsx index 7e6f1ac82..44f27812c 100644 --- a/src/BaseSelect/index.tsx +++ b/src/BaseSelect/index.tsx @@ -184,7 +184,7 @@ export interface BaseSelectProps tokenSeparators?: string[] | ((input: string) => string[]); // >>> Icons - allowClear?: boolean | { clearIcon?: React.ReactNode }; + allowClear?: boolean | { clearIcon?: React.ReactNode; label?: string }; prefix?: React.ReactNode; /** @deprecated Please use `suffix` instead. */ suffixIcon?: RenderNode; @@ -720,7 +720,11 @@ const BaseSelect = React.forwardRef((props, ref) onInternalSearch('', false, false); }; - const { allowClear: mergedAllowClear, clearIcon: clearNode } = useAllowClear( + const { + allowClear: mergedAllowClear, + clearIcon: clearNode, + label: clearLabel, + } = useAllowClear( prefixCls, displayValues, allowClear, @@ -762,6 +766,7 @@ const BaseSelect = React.forwardRef((props, ref) prefix={prefix} suffix={mergedSuffixIcon} clearIcon={clearNode} + clearLabel={clearLabel} // Type or mode multiple={multiple} mode={mode} diff --git a/src/SelectInput/index.tsx b/src/SelectInput/index.tsx index 70f0d3490..e52d52239 100644 --- a/src/SelectInput/index.tsx +++ b/src/SelectInput/index.tsx @@ -20,6 +20,7 @@ export interface SelectInputProps extends Omit(function Selec prefix, suffix, clearIcon, + clearLabel, children, // Data @@ -283,17 +285,25 @@ export default React.forwardRef(function Selec {/* Clear Icon */} {clearIcon && ( - { - // Mark to tell not trigger open or focus + // Keep focus on the input and mark the native event so the root + // `onInternalMouseDown` handler does not open the dropdown. + // This must run on mousedown because the root handler fires + // before the button's onClick. + e.preventDefault(); (e.nativeEvent as any)._select_lazy = true; - onClearMouseDown?.(e); }} + // Clearing happens on click so it works for both pointer and + // keyboard (Enter/Space) activation. + onClick={onClearMouseDown} > {clearIcon} - + )} {children} diff --git a/src/hooks/useAllowClear.tsx b/src/hooks/useAllowClear.tsx index 2e8c46b4b..1ebf0f823 100644 --- a/src/hooks/useAllowClear.tsx +++ b/src/hooks/useAllowClear.tsx @@ -5,12 +5,13 @@ import { useMemo } from 'react'; export interface AllowClearConfig { allowClear: boolean; clearIcon: React.ReactNode; + label: string; } export const useAllowClear = ( prefixCls: string, displayValues: DisplayValueType[], - allowClear?: boolean | { clearIcon?: React.ReactNode }, + allowClear?: boolean | { clearIcon?: React.ReactNode; label?: string }, clearIcon?: React.ReactNode, disabled: boolean = false, mergedSearchValue?: string, @@ -37,6 +38,7 @@ export const useAllowClear = ( return { allowClear: mergedAllowClear, clearIcon: mergedAllowClear ? allowClearConfig.clearIcon || clearIcon || '×' : null, + label: mergedAllowClear ? allowClearConfig.label : '', }; }, [allowClearConfig, clearIcon, disabled, displayValues.length, mergedSearchValue, mode]); }; diff --git a/tests/Select.test.tsx b/tests/Select.test.tsx index 4e96d4606..5d2867baf 100644 --- a/tests/Select.test.tsx +++ b/tests/Select.test.tsx @@ -2508,7 +2508,7 @@ describe('Select.Basic', () => { it('should be focused when click clear button', () => { jest.useFakeTimers(); - const mouseDownPreventDefault = jest.fn(); + const clickPreventDefault = jest.fn(); const { container } = render( + + + , + ); + + const clear = container.querySelector('.rc-select-clear'); + expect(clear.tagName).toBe('BUTTON'); + expect(clear).toHaveAttribute('type', 'button'); + expect(clear).toHaveAttribute('aria-label', 'Clear all'); + }); + + it('clicking clear does not open the dropdown', () => { + const onPopupVisibleChange = jest.fn(); + const { container } = render( + , + ); + + // mousedown should be prevented (keeps focus on input) and must not open + const mouseDownEvent = createEvent.mouseDown(container.querySelector('.rc-select-clear')); + fireEvent(container.querySelector('.rc-select-clear'), mouseDownEvent); + expect(mouseDownEvent.defaultPrevented).toBe(true); + + fireEvent.click(container.querySelector('.rc-select-clear')); + + expectOpen(container, false); + expect(onPopupVisibleChange).not.toHaveBeenCalledWith(true); + }); + + it('clears value via keyboard activation', () => { + const onChange = jest.fn(); + const onClear = jest.fn(); + const { container } = render( + , + ); + + const clear = container.querySelector('.rc-select-clear'); + + clear.focus(); + expect(clear).toHaveFocus(); + // Enter/Space on a native button dispatch a click event + fireEvent.click(clear); + + expect(onChange).toHaveBeenCalledWith(undefined, undefined); + expect(onClear).toHaveBeenCalled(); + expect(container.querySelector('input').value).toBe(''); + }); + it('should support title', () => { const { container: container1 } = render(); expect(container.querySelector('.rc-select-clear')).toBeTruthy(); }); + + it('renders clear as a button', () => { + const { container } = render(); + const clear = container.querySelector('.rc-select-clear'); + const mouseDownEvent = createEvent.mouseDown(clear); + fireEvent(clear, mouseDownEvent); + expect(mouseDownEvent.defaultPrevented).toBe(true); + }); it('clears value', () => { const onClear = jest.fn(); const onChange = jest.fn(); @@ -37,7 +52,7 @@ export default function allowClearTest(mode: any, value: any) { // enabled rerender(renderDemo(false)); - fireEvent.mouseDown(container.querySelector('.rc-select-clear')); + fireEvent.click(container.querySelector('.rc-select-clear')); if (useArrayValue) { expect(onChange).toHaveBeenCalledWith([], []); } else {