Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 60 additions & 41 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
},
"dependencies": {
"@rc-component/resize-observer": "^1.1.1",
"@rc-component/util": "^1.13.0",
"@rc-component/util": "^1.14.0",
"clsx": "^2.1.1"
},
"devDependencies": {
Expand Down
10 changes: 5 additions & 5 deletions src/BaseInput.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { clsx } from 'clsx';
import { isReactRenderable } from '@rc-component/util';
import { isReactRenderable, useAllowClear } from '@rc-component/util';
import type { ReactElement, ReactNode } from 'react';
import React, { cloneElement, useRef } from 'react';
import useAllowClear from './hooks/useAllowClear';
import type { BaseInputProps } from './interface';
import { hasAddon, hasPrefixSuffix } from './utils/commonUtils';

Expand Down Expand Up @@ -38,7 +37,8 @@ const BaseInput = React.forwardRef<HolderRef, BaseInputProps>((props, ref) => {
onClear,
} = props;

const [showAllowClear, allowClearConfig] = useAllowClear(allowClear);
const allowClearConfig = useAllowClear(allowClear);
const showAllowClear = !allowClearConfig.disabled;
const inputElement = children ?? inputEl;

const AffixWrapperComponent = components?.affixWrapper || 'span';
Expand Down Expand Up @@ -77,8 +77,7 @@ const BaseInput = React.forwardRef<HolderRef, BaseInputProps>((props, ref) => {
// ================== Clear Icon ================== //
let clearIcon: ReactNode = null;
if (showAllowClear) {
const needClear =
!disabled && !readOnly && value && !allowClearConfig.disabled;
const needClear = !disabled && !readOnly && value;
const clearIconCls = `${prefixCls}-clear-icon`;
const iconNode = isReactRenderable(allowClearConfig.clearIcon)
? allowClearConfig.clearIcon
Expand All @@ -88,6 +87,7 @@ const BaseInput = React.forwardRef<HolderRef, BaseInputProps>((props, ref) => {
<button
type="button"
tabIndex={allowClearConfig.tabIndex}
aria-label={allowClearConfig.label ?? 'Clear'}
onClick={(event) => {
handleReset?.(event);
onClear?.();
Expand Down
6 changes: 4 additions & 2 deletions src/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { clsx } from 'clsx';
import { useAllowClear } from '@rc-component/util';
import React, { useEffect, useImperativeHandle, useRef } from 'react';
import type { HolderRef } from './BaseInput';
import BaseInput from './BaseInput';
Expand Down Expand Up @@ -171,7 +172,8 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
}
};

const isPureTextArea = !autoSize && !showCount && !allowClear;
const clearConfig = useAllowClear(allowClear);
const isPureTextArea = !autoSize && !showCount && clearConfig.disabled;

return (
<BaseInput
Expand All @@ -185,7 +187,7 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
...classNames,
affixWrapper: clsx(classNames?.affixWrapper, {
[`${prefixCls}-show-count`]: showCount,
[`${prefixCls}-textarea-allow-clear`]: allowClear,
[`${prefixCls}-textarea-allow-clear`]: !clearConfig.disabled,
}),
}}
disabled={disabled}
Expand Down
12 changes: 0 additions & 12 deletions src/hooks/useAllowClear.ts

This file was deleted.

5 changes: 2 additions & 3 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
ReactNode,
TextareaHTMLAttributes,
} from 'react';
import type { InputFocusOptions } from '@rc-component/util';
import type { AllowClearConfig, InputFocusOptions } from '@rc-component/util';
import type { LiteralUnion } from './utils/types';

export interface CommonInputProps {
Expand Down Expand Up @@ -36,8 +36,7 @@ export interface CommonInputProps {
suffix?: CSSProperties;
clear?: CSSProperties;
};
allowClear?:
boolean | { disabled?: boolean; clearIcon?: ReactNode; tabIndex?: number };
allowClear?: boolean | AllowClearConfig;
}

type DataAttr = Record<`data-${string}`, string>;
Expand Down
4 changes: 3 additions & 1 deletion src/utils/commonUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export function hasPrefixSuffix(props: BaseInputProps | InputProps) {
return (
isReactRenderable(props.prefix) ||
isReactRenderable(props.suffix) ||
Boolean(props.allowClear)
(typeof props.allowClear === 'object'
? !!props.allowClear && !props.allowClear.disabled
: !!props.allowClear)
);
}

Expand Down
6 changes: 6 additions & 0 deletions tests/__snapshots__/TextArea.allowClear.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ exports[`should support allowClear should not show icon if defaultValue is undef
class="rc-textarea-suffix"
>
<button
aria-label="Clear"
class="rc-textarea-clear-icon rc-textarea-clear-icon-hidden"
type="button"
>
Expand All @@ -31,6 +32,7 @@ exports[`should support allowClear should not show icon if defaultValue is undef
class="rc-textarea-suffix"
>
<button
aria-label="Clear"
class="rc-textarea-clear-icon rc-textarea-clear-icon-hidden"
type="button"
>
Expand All @@ -51,6 +53,7 @@ exports[`should support allowClear should not show icon if defaultValue is undef
class="rc-textarea-suffix"
>
<button
aria-label="Clear"
class="rc-textarea-clear-icon rc-textarea-clear-icon-hidden"
type="button"
>
Expand All @@ -71,6 +74,7 @@ exports[`should support allowClear should not show icon if value is undefined, n
class="rc-textarea-suffix"
>
<button
aria-label="Clear"
class="rc-textarea-clear-icon rc-textarea-clear-icon-hidden"
type="button"
>
Expand All @@ -91,6 +95,7 @@ exports[`should support allowClear should not show icon if value is undefined, n
class="rc-textarea-suffix"
>
<button
aria-label="Clear"
class="rc-textarea-clear-icon rc-textarea-clear-icon-hidden"
type="button"
>
Expand All @@ -111,6 +116,7 @@ exports[`should support allowClear should not show icon if value is undefined, n
class="rc-textarea-suffix"
>
<button
aria-label="Clear"
class="rc-textarea-clear-icon rc-textarea-clear-icon-hidden"
type="button"
>
Expand Down
1 change: 1 addition & 0 deletions tests/__snapshots__/TextArea.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ exports[`TextArea classNames and styles should work 1`] = `
style="color: yellow;"
>
<button
aria-label="Clear"
class="rc-textarea-clear-icon rc-textarea-clear-icon-has-suffix custom-clear"
style="color: orange;"
type="button"
Expand Down
7 changes: 7 additions & 0 deletions tests/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ exports[`Input allowClear classNames and styles should work 1`] = `
style="color: yellow;"
>
<button
aria-label="Clear"
class="rc-input-clear-icon rc-input-clear-icon-has-suffix custom-clear"
style="color: orange;"
type="button"
Expand Down Expand Up @@ -123,6 +124,7 @@ exports[`Input allowClear should change type when click 1`] = `
class="rc-input-suffix"
>
<button
aria-label="Clear"
class="rc-input-clear-icon"
type="button"
>
Expand All @@ -147,6 +149,7 @@ exports[`Input allowClear should change type when click 2`] = `
class="rc-input-suffix"
>
<button
aria-label="Clear"
class="rc-input-clear-icon rc-input-clear-icon-hidden"
type="button"
>
Expand All @@ -171,6 +174,7 @@ exports[`Input allowClear should not show icon if defaultValue is undefined or e
class="rc-input-suffix"
>
<button
aria-label="Clear"
class="rc-input-clear-icon rc-input-clear-icon-hidden"
type="button"
>
Expand All @@ -195,6 +199,7 @@ exports[`Input allowClear should not show icon if defaultValue is undefined or e
class="rc-input-suffix"
>
<button
aria-label="Clear"
class="rc-input-clear-icon rc-input-clear-icon-hidden"
type="button"
>
Expand All @@ -219,6 +224,7 @@ exports[`Input allowClear should not show icon if value is undefined or empty st
class="rc-input-suffix"
>
<button
aria-label="Clear"
class="rc-input-clear-icon rc-input-clear-icon-hidden"
type="button"
>
Expand All @@ -243,6 +249,7 @@ exports[`Input allowClear should not show icon if value is undefined or empty st
class="rc-input-suffix"
>
<button
aria-label="Clear"
class="rc-input-clear-icon rc-input-clear-icon-hidden"
type="button"
>
Expand Down
122 changes: 70 additions & 52 deletions tests/allowClear.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,66 +7,84 @@ describe.each([
['Input', Input],
['TextArea', TextArea],
] as const)('%s allowClear', (_, Component) => {
it.each([false, undefined])(
'distinguishes allowClear=%s from a disabled clear button',
(allowClear) => {
it.each([undefined, 'suffix'])(
'removes the clear entry without changing other suffixes: %s',
async (suffix) => {
const user = userEvent.setup();
const onChange = jest.fn();
const onClear = jest.fn();
const config = Object.freeze({
clearIcon: 'custom',
tabIndex: -1,
label: 'Erase',
});
const props = {
prefixCls: 'rc-input',
defaultValue: 'test',
suffix,
onChange,
onClear,
};
const { container, getByRole, queryByRole, rerender } = render(
<Component
prefixCls="rc-input"
defaultValue="test"
suffix="suffix"
allowClear={allowClear}
/>,
);
expect(queryByRole('button')).toBeNull();
expect(container.firstChild).not.toHaveClass(
'rc-input-affix-wrapper-input-with-clear-btn',
<Component {...props} allowClear={false} />,
);

const disabledMarkup = container.innerHTML;
rerender(
<Component
prefixCls="rc-input"
defaultValue="test"
suffix="suffix"
allowClear={{ disabled: true, clearIcon: 'clear', tabIndex: -1 }}
/>,
);
const clearButton = getByRole('button');
expect(clearButton).toHaveClass('rc-input-clear-icon-hidden');
expect(clearButton).toHaveTextContent('clear');
expect(clearButton.tabIndex).toBe(-1);
expect(container.firstChild).toHaveClass(
'rc-input-affix-wrapper-input-with-clear-btn',
);

rerender(
<Component
prefixCls="rc-input"
defaultValue="test"
suffix="suffix"
allowClear
/>,
);
expect(getByRole('button')).toBe(clearButton);
expect(clearButton).not.toHaveClass('rc-input-clear-icon-hidden');
expect(clearButton).toHaveTextContent('✖');
expect(clearButton.tabIndex).toBe(0);

rerender(
<Component
prefixCls="rc-input"
defaultValue="test"
suffix="suffix"
allowClear={allowClear}
/>,
<Component {...props} allowClear={{ ...config, disabled: true }} />,
);
expect(container.innerHTML).toBe(disabledMarkup);
expect(queryByRole('button')).toBeNull();
expect(container.firstChild).not.toHaveClass(
'rc-input-affix-wrapper-input-with-clear-btn',
);
expect(getByRole('textbox')).toHaveValue('test');
expect(onChange).not.toHaveBeenCalled();
expect(onClear).not.toHaveBeenCalled();

rerender(<Component {...props} allowClear={config} />);
const clearButton = getByRole('button', { name: 'Erase' });
expect(clearButton).toHaveTextContent('custom');
expect(clearButton.tabIndex).toBe(-1);
await user.click(clearButton);
expect(getByRole('textbox')).toHaveValue('');
expect(onChange).toHaveBeenCalledTimes(1);
expect(onClear).toHaveBeenCalledTimes(1);
expect(config).toEqual({
clearIcon: 'custom',
tabIndex: -1,
label: 'Erase',
});
},
);

it('preserves focus and selection when a suffix keeps the wrapper mounted', async () => {
const user = userEvent.setup();
const { getByRole, rerender } = render(
<Component suffix="suffix" defaultValue="test" allowClear />,
);
const input = getByRole('textbox') as HTMLInputElement;
await user.click(input);
input.setSelectionRange(1, 3);
rerender(
<Component
suffix="suffix"
defaultValue="test"
allowClear={{ disabled: true }}
/>,
);
expect(getByRole('textbox')).toBe(input);
expect(input).toHaveFocus();
expect(input.selectionStart).toBe(1);
expect(input.selectionEnd).toBe(3);
});

it('uses the default accessible name and supports Space activation', async () => {
const user = userEvent.setup();
const { getByRole } = render(<Component defaultValue="test" allowClear />);
await user.tab();
await user.tab();
expect(getByRole('button', { name: 'Clear' })).toHaveFocus();
await user.keyboard('[Space]');
expect(getByRole('textbox')).toHaveValue('');
});

it.each([true, {}, { tabIndex: 0 }])(
'keeps the clear button keyboard accessible with %j',
async (allowClear) => {
Expand Down
3 changes: 2 additions & 1 deletion tests/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,8 @@ describe('Input allowClear', () => {
defaultValue="111"
/>,
);
expect(container.querySelector('.rc-input-clear-icon-hidden')).toBeTruthy();
expect(container.querySelector('.rc-input-clear-icon')).toBeNull();
expect(container.querySelector('.rc-input-affix-wrapper')).toBeNull();
});

it('classNames and styles should work', () => {
Expand Down
Loading