From 1c23ebe3f99426b820b82c14ab742ba6623071a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E8=89=B3=E5=85=B5?= Date: Tue, 13 Oct 2026 15:41:24 +0800 Subject: [PATCH] feat: unify allowClear configuration --- README.md | 101 +++++++++------ package.json | 2 +- src/BaseInput.tsx | 10 +- src/TextArea.tsx | 6 +- src/hooks/useAllowClear.ts | 12 -- src/interface.ts | 5 +- src/utils/commonUtils.ts | 4 +- .../TextArea.allowClear.test.tsx.snap | 6 + tests/__snapshots__/TextArea.test.tsx.snap | 1 + tests/__snapshots__/index.test.tsx.snap | 7 + tests/allowClear.test.tsx | 122 ++++++++++-------- tests/index.test.tsx | 3 +- 12 files changed, 161 insertions(+), 118 deletions(-) delete mode 100644 src/hooks/useAllowClear.ts diff --git a/README.md b/README.md index cf13115..43b37f5 100644 --- a/README.md +++ b/README.md @@ -58,50 +58,50 @@ Then open `http://localhost:8000`. ### Input -| Property | Type | Default | Description | -| ------------ | ----------------------------------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------- | -| addonAfter | `ReactNode` | - | Element displayed after the input. | -| addonBefore | `ReactNode` | - | Element displayed before the input. | -| allowClear | `boolean \| { disabled?: boolean; clearIcon?: ReactNode; tabIndex?: number }` | `false` | Show a clear button for the current value. Set `tabIndex: -1` to skip it during Tab navigation. | -| className | `string` | - | Class name for the input element. | -| classNames | `InputProps['classNames']` | - | Semantic class names for input slots. | -| count | `CountConfig` | - | Custom count strategy, limit, visibility, and exceed formatter. | -| defaultValue | `string \| number \| readonly string[] \| bigint` | - | Initial input value. | -| disabled | `boolean` | `false` | Disable the input. | -| htmlSize | `number` | - | Native input `size` attribute. | -| maxLength | `number` | - | Native input `maxLength` attribute. | -| prefix | `ReactNode` | - | Prefix content inside the input wrapper. | -| prefixCls | `string` | `rc-input` | Class name prefix. | -| showCount | `boolean \| { formatter: ShowCountFormatter }` | `false` | Show character count. Prefer `count.show` for new code. | -| styles | `InputProps['styles']` | - | Semantic styles for input slots. | -| suffix | `ReactNode` | - | Suffix content inside the input wrapper. | -| type | `InputProps['type']` | `text` | Native input type. Use `TextArea` for textarea behavior. | -| value | `string \| number \| readonly string[] \| bigint` | - | Controlled input value. | -| onChange | `React.ChangeEventHandler` | - | Triggered when the value changes. | -| onClear | `() => void` | - | Triggered when the clear button is clicked. | -| onPressEnter | `React.KeyboardEventHandler` | - | Triggered when Enter is pressed. | +| Property | Type | Default | Description | +| ------------ | ------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------- | +| addonAfter | `ReactNode` | - | Element displayed after the input. | +| addonBefore | `ReactNode` | - | Element displayed before the input. | +| allowClear | `boolean \| AllowClearConfig` | `false` | Show a clear button for the current value. Set `tabIndex: -1` to skip it during Tab navigation. | +| className | `string` | - | Class name for the input element. | +| classNames | `InputProps['classNames']` | - | Semantic class names for input slots. | +| count | `CountConfig` | - | Custom count strategy, limit, visibility, and exceed formatter. | +| defaultValue | `string \| number \| readonly string[] \| bigint` | - | Initial input value. | +| disabled | `boolean` | `false` | Disable the input. | +| htmlSize | `number` | - | Native input `size` attribute. | +| maxLength | `number` | - | Native input `maxLength` attribute. | +| prefix | `ReactNode` | - | Prefix content inside the input wrapper. | +| prefixCls | `string` | `rc-input` | Class name prefix. | +| showCount | `boolean \| { formatter: ShowCountFormatter }` | `false` | Show character count. Prefer `count.show` for new code. | +| styles | `InputProps['styles']` | - | Semantic styles for input slots. | +| suffix | `ReactNode` | - | Suffix content inside the input wrapper. | +| type | `InputProps['type']` | `text` | Native input type. Use `TextArea` for textarea behavior. | +| value | `string \| number \| readonly string[] \| bigint` | - | Controlled input value. | +| onChange | `React.ChangeEventHandler` | - | Triggered when the value changes. | +| onClear | `() => void` | - | Triggered when the clear button is clicked. | +| onPressEnter | `React.KeyboardEventHandler` | - | Triggered when Enter is pressed. | ### TextArea -| Property | Type | Default | Description | -| ------------ | ----------------------------------------------------------------------------- | ------------- | ----------------------------------------------------------------------------------------------- | -| allowClear | `boolean \| { disabled?: boolean; clearIcon?: ReactNode; tabIndex?: number }` | `false` | Show a clear button for the current value. Set `tabIndex: -1` to skip it during Tab navigation. | -| autoSize | `boolean \| { minRows?: number; maxRows?: number }` | `false` | Auto resize height by content. | -| className | `string` | - | Class name for the textarea. | -| classNames | `TextAreaProps['classNames']` | - | Semantic class names for textarea slots. | -| count | `CountConfig` | - | Custom count strategy, limit, visibility, and exceed formatter. | -| defaultValue | `string \| number \| readonly string[] \| bigint` | - | Initial textarea value. | -| maxLength | `number` | - | Native textarea `maxLength` attribute. | -| prefixCls | `string` | `rc-textarea` | Class name prefix. | -| showCount | `boolean \| { formatter: ShowCountFormatter }` | `false` | Show character count. Prefer `count.show` for new code. | -| style | `React.CSSProperties` | - | Inline styles for the textarea. | -| styles | `TextAreaProps['styles']` | - | Semantic styles for textarea slots. | -| suffix | `ReactNode` | - | Suffix content inside the textarea wrapper. | -| value | `string \| number \| readonly string[] \| bigint` | - | Controlled textarea value. | -| onChange | `React.ChangeEventHandler` | - | Triggered when the value changes. | -| onClear | `() => void` | - | Triggered when the clear button is clicked. | -| onPressEnter | `React.KeyboardEventHandler` | - | Triggered when Enter is pressed. | -| onResize | `(size: { width: number; height: number }) => void` | - | Triggered when textarea size changes. | +| Property | Type | Default | Description | +| ------------ | --------------------------------------------------- | ------------- | ----------------------------------------------------------------------------------------------- | +| allowClear | `boolean \| AllowClearConfig` | `false` | Show a clear button for the current value. Set `tabIndex: -1` to skip it during Tab navigation. | +| autoSize | `boolean \| { minRows?: number; maxRows?: number }` | `false` | Auto resize height by content. | +| className | `string` | - | Class name for the textarea. | +| classNames | `TextAreaProps['classNames']` | - | Semantic class names for textarea slots. | +| count | `CountConfig` | - | Custom count strategy, limit, visibility, and exceed formatter. | +| defaultValue | `string \| number \| readonly string[] \| bigint` | - | Initial textarea value. | +| maxLength | `number` | - | Native textarea `maxLength` attribute. | +| prefixCls | `string` | `rc-textarea` | Class name prefix. | +| showCount | `boolean \| { formatter: ShowCountFormatter }` | `false` | Show character count. Prefer `count.show` for new code. | +| style | `React.CSSProperties` | - | Inline styles for the textarea. | +| styles | `TextAreaProps['styles']` | - | Semantic styles for textarea slots. | +| suffix | `ReactNode` | - | Suffix content inside the textarea wrapper. | +| value | `string \| number \| readonly string[] \| bigint` | - | Controlled textarea value. | +| onChange | `React.ChangeEventHandler` | - | Triggered when the value changes. | +| onClear | `() => void` | - | Triggered when the clear button is clicked. | +| onPressEnter | `React.KeyboardEventHandler` | - | Triggered when Enter is pressed. | +| onResize | `(size: { width: number; height: number }) => void` | - | Triggered when textarea size changes. | ### Refs @@ -146,3 +146,22 @@ The release flow is handled by `@rc-component/np` through the `rc-np` command af ## License @rc-component/input is released under the [MIT](./LICENSE) license. + +### Clear configuration + +`allowClear` accepts a boolean or an `AllowClearConfig` object from `@rc-component/util`: + +```ts +interface AllowClearConfig { + clearIcon?: React.ReactNode; + disabled?: boolean; + tabIndex?: number; + label?: string; +} +``` + +All fields are optional. `disabled: true` behaves like `allowClear={false}`: the clear button and space reserved solely for it are removed; the input remains editable. Existing suffixes and other content are preserved. This changes the previous hidden-button spacing behavior where applicable. + +`tabIndex` affects only the clear button; `-1` removes it from Tab navigation while retaining mouse activation. `label` supplies its accessible name through `aria-label`. Defaults, clear values, callbacks, and component state restrictions are unchanged. + +When no other affix keeps the wrapper mounted, dynamically disabling clearing can recreate the input DOM node and lose focus or selection, just like switching to `allowClear={false}`. The value is preserved. With an existing suffix, the wrapper, focus, and selection remain stable. diff --git a/package.json b/package.json index 4e98640..8d0230e 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/BaseInput.tsx b/src/BaseInput.tsx index eebfa73..88a0b1f 100644 --- a/src/BaseInput.tsx +++ b/src/BaseInput.tsx @@ -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'; @@ -38,7 +37,8 @@ const BaseInput = React.forwardRef((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'; @@ -77,8 +77,7 @@ const BaseInput = React.forwardRef((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 @@ -88,6 +87,7 @@ const BaseInput = React.forwardRef((props, ref) => {