feat: input.otp add separator api (#52668)
Some checks failed
Publish Any Commit / build (push) Has been cancelled
🔀 Sync mirror to Gitee / mirror (push) Has been cancelled
✅ test / lint (push) Has been cancelled
✅ test / test-react-legacy (16, 1/2) (push) Has been cancelled
✅ test / test-react-legacy (16, 2/2) (push) Has been cancelled
✅ test / test-react-legacy (17, 1/2) (push) Has been cancelled
✅ test / test-react-legacy (17, 2/2) (push) Has been cancelled
✅ test / test-node (push) Has been cancelled
✅ test / test-react-latest (dom, 1/2) (push) Has been cancelled
✅ test / test-react-latest (dom, 2/2) (push) Has been cancelled
✅ test / build (push) Has been cancelled
✅ test / test lib/es module (es, 1/2) (push) Has been cancelled
✅ test / test lib/es module (es, 2/2) (push) Has been cancelled
✅ test / test lib/es module (lib, 1/2) (push) Has been cancelled
✅ test / test lib/es module (lib, 2/2) (push) Has been cancelled
👁️ Visual Regression Persist Start / test image (push) Has been cancelled
✅ test / test-react-latest-dist (dist, 1/2) (push) Has been cancelled
✅ test / test-react-latest-dist (dist, 2/2) (push) Has been cancelled
✅ test / test-react-latest-dist (dist-min, 1/2) (push) Has been cancelled
✅ test / test-react-latest-dist (dist-min, 2/2) (push) Has been cancelled
✅ test / test-coverage (push) Has been cancelled

* feature(component): Input.OPT added separator api

* test: add tests for OTP component separator rendering cases

* refactor: apply code review suggestions
This commit is contained in:
Haceral 2025-02-08 15:54:26 +08:00 committed by GitHub
parent c9c05c53c9
commit a8856dfb5b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 279 additions and 15 deletions

View File

@ -17,6 +17,7 @@ import type { InputRef } from '../Input';
import useStyle from '../style/otp';
import OTPInput from './OTPInput';
import type { OTPInputProps } from './OTPInput';
import type { ReactNode } from 'react';
export interface OTPRef {
focus: VoidFunction;
@ -41,6 +42,7 @@ export interface OTPProps
value?: string;
onChange?: (value: string) => void;
formatter?: (value: string) => string;
separator?: ((index: number) => ReactNode) | ReactNode;
// Status
disabled?: boolean;
@ -66,6 +68,7 @@ const OTP = React.forwardRef<OTPRef, OTPProps>((props, ref) => {
value,
onChange,
formatter,
separator,
variant,
disabled,
status: customStatus,
@ -229,6 +232,11 @@ const OTP = React.forwardRef<OTPRef, OTPProps>((props, ref) => {
inputMode,
};
const renderSeparator = (index: number) => {
const result = typeof separator === 'function' ? separator(index) : separator;
return result ? <span className={`${prefixCls}-separator`}>{result}</span> : null;
};
return wrapCSSVar(
<div
{...domAttrs}
@ -249,21 +257,23 @@ const OTP = React.forwardRef<OTPRef, OTPProps>((props, ref) => {
const key = `otp-${index}`;
const singleValue = valueCells[index] || '';
return (
<OTPInput
ref={(inputEle) => {
refs.current[index] = inputEle;
}}
key={key}
index={index}
size={mergedSize}
htmlSize={1}
className={`${prefixCls}-input`}
onChange={onInputChange}
value={singleValue}
onActiveChange={onInputActiveChange}
autoFocus={index === 0 && autoFocus}
{...inputSharedProps}
/>
<React.Fragment key={key}>
<OTPInput
ref={(inputEle) => {
refs.current[index] = inputEle;
}}
index={index}
size={mergedSize}
htmlSize={1}
className={`${prefixCls}-input`}
onChange={onInputChange}
value={singleValue}
onActiveChange={onInputActiveChange}
autoFocus={index === 0 && autoFocus}
{...inputSharedProps}
/>
{separator && index < length - 1 && renderSeparator(index)}
</React.Fragment>
);
})}
</FormItemInputContext.Provider>

View File

@ -10519,6 +10519,110 @@ exports[`renders components/input/demo/otp.tsx extend context correctly 1`] = `
value=""
/>
</div>
<h5
class="ant-typography"
>
With custom string separator
</h5>
<div
class="ant-otp"
>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<span
class="ant-otp-separator"
>
-
</span>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
</div>
<h5
class="ant-typography"
>
With custom JSX separator
</h5>
<div
class="ant-otp"
>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<span
class="ant-otp-separator"
>
<span
style="color: red;"
>
</span>
</span>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
</div>
</div>
`;

View File

@ -3869,6 +3869,110 @@ exports[`renders components/input/demo/otp.tsx correctly 1`] = `
value=""
/>
</div>
<h5
class="ant-typography"
>
With custom string separator
</h5>
<div
class="ant-otp"
>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<span
class="ant-otp-separator"
>
-
</span>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
</div>
<h5
class="ant-typography"
>
With custom JSX separator
</h5>
<div
class="ant-otp"
>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<span
class="ant-otp-separator"
>
<span
style="color:red"
>
</span>
</span>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
<input
class="ant-input ant-input-outlined ant-otp-input"
size="1"
type="text"
value=""
/>
</div>
</div>
`;

View File

@ -200,4 +200,41 @@ describe('Input.OTP', () => {
expect(event.defaultPrevented).toBeTruthy();
});
it('renders separator between input fields', () => {
const { container } = render(
<OTP
length={4}
separator={(index) => (
<span key={index} className="custom-separator">
|
</span>
)}
/>,
);
const separators = container.querySelectorAll('.custom-separator');
expect(separators.length).toBe(3);
separators.forEach((separator) => {
expect(separator.textContent).toBe('|');
});
});
it('renders separator when separator is a string', () => {
const { container } = render(<OTP length={4} separator="-" />);
const separators = container.querySelectorAll(`.ant-otp-separator`);
expect(separators.length).toBe(3);
separators.forEach((separator) => {
expect(separator.textContent).toBe('-');
});
});
it('renders separator when separator is a element', () => {
const customSeparator = <div data-testid="custom-separator">X</div>;
const { getAllByTestId } = render(<OTP length={4} separator={customSeparator} />);
const separators = getAllByTestId('custom-separator');
expect(separators.length).toBe(3);
separators.forEach((separator) => {
expect(separator.textContent).toBe('X');
});
});
});

View File

@ -32,6 +32,13 @@ const App: React.FC = () => {
<Input.OTP variant="filled" {...sharedProps} />
<Title level={5}>With custom display character</Title>
<Input.OTP mask="🔒" {...sharedProps} />
<Title level={5}>With custom string separator</Title>
<Input.OTP separator={(index) => (index === 2 ? '-' : undefined)} {...sharedProps} />
<Title level={5}>With custom JSX separator</Title>
<Input.OTP
separator={(index) => index === 1 && <span style={{ color: 'red' }}></span>}
{...sharedProps}
/>
</Flex>
);
};

View File

@ -135,6 +135,7 @@ Added in `5.16.0`.
| defaultValue | Default value | string | - | |
| disabled | Whether the input is disabled | boolean | false | |
| formatter | Format display, blank fields will be filled with ` ` | (value: string) => string | - | |
| separator | render the separator after the input box of the specified index | ((index: number) => ReactNode) \| ReactNode | - | |
| mask | Custom display, the original value will not be modified | boolean \| string | `false` | `5.17.0` |
| length | The number of input elements | number | 6 | |
| status | Set validation status | 'error' \| 'warning' | - | |

View File

@ -136,6 +136,7 @@ interface CountConfig {
| defaultValue | 默认值 | string | - | |
| disabled | 是否禁用 | boolean | false | |
| formatter | 格式化展示,留空字段会被 ` ` 填充 | (value: string) => string | - | |
| separator | 分隔符,在指定索引的输入框后渲染分隔符 | ((index: number) => ReactNode) \| ReactNode | - | |
| mask | 自定义展示,和 `formatter` 的区别是不会修改原始值 | boolean \| string | `false` | `5.17.0` |
| length | 输入元素数量 | number | 6 | |
| status | 设置校验状态 | 'error' \| 'warning' | - | |