mirror of
https://github.com/ant-design/ant-design.git
synced 2026-02-09 02:49:18 +08:00
feat: Input.OTP component support (#48076)
* chore: basic control * chore: input instad * docs: update demo * chore: adjust operation interactive * chore: lock selection * chore: fix patch logic * chore: merge logic * chore: patch autoFocus * test: update snapshot * test: add test case * test: coverage * chore: update size limit * docs: update docs * test: fix test case * chore: update comment * refactor: change to length * chore: blur all * chore: size limit
This commit is contained in:
@@ -28500,6 +28500,216 @@ exports[`renders components/form/demo/validate-static.tsx extend context correct
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item ant-form-item-has-feedback ant-form-item-has-success"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label ant-col-xs-24 ant-col-sm-6"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
title="Success"
|
||||
>
|
||||
Success
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-sm-14"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item ant-form-item-has-feedback ant-form-item-has-warning"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label ant-col-xs-24 ant-col-sm-6"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
title="Warning"
|
||||
>
|
||||
Warning
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-sm-14"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item ant-form-item-has-feedback ant-form-item-has-error"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label ant-col-xs-24 ant-col-sm-6"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
title="Error"
|
||||
>
|
||||
Error
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-sm-14"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item ant-form-item-has-feedback ant-form-item-has-error"
|
||||
>
|
||||
|
||||
@@ -11508,6 +11508,216 @@ exports[`renders components/form/demo/validate-static.tsx correctly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item ant-form-item-has-feedback ant-form-item-has-success"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label ant-col-xs-24 ant-col-sm-6"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
title="Success"
|
||||
>
|
||||
Success
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-sm-14"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item ant-form-item-has-feedback ant-form-item-has-warning"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label ant-col-xs-24 ant-col-sm-6"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
title="Warning"
|
||||
>
|
||||
Warning
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-sm-14"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item ant-form-item-has-feedback ant-form-item-has-error"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label ant-col-xs-24 ant-col-sm-6"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
title="Error"
|
||||
>
|
||||
Error
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-sm-14"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item ant-form-item-has-feedback ant-form-item-has-error"
|
||||
>
|
||||
|
||||
@@ -136,6 +136,17 @@ const App: React.FC = () => (
|
||||
<Input.Password allowClear placeholder="with input password and allowClear" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Success" hasFeedback validateStatus="success">
|
||||
<Input.OTP />
|
||||
</Form.Item>
|
||||
<Form.Item label="Warning" hasFeedback validateStatus="warning">
|
||||
<Input.OTP />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Error" hasFeedback validateStatus="error">
|
||||
<Input.OTP />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Fail" validateStatus="error" hasFeedback>
|
||||
<Mentions />
|
||||
</Form.Item>
|
||||
|
||||
@@ -264,4 +264,8 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
|
||||
);
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Input.displayName = 'Input';
|
||||
}
|
||||
|
||||
export default Input;
|
||||
|
||||
69
components/input/OTP/OTPInput.tsx
Normal file
69
components/input/OTP/OTPInput.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import * as React from 'react';
|
||||
import raf from 'rc-util/lib/raf';
|
||||
|
||||
import Input, { type InputProps, type InputRef } from '../Input';
|
||||
|
||||
export interface OTPInputProps extends Omit<InputProps, 'onChange'> {
|
||||
index: number;
|
||||
onChange: (index: number, value: string) => void;
|
||||
/** Tell parent to do active offset */
|
||||
onActiveChange: (nextIndex: number) => void;
|
||||
}
|
||||
|
||||
const OTPInput = React.forwardRef<InputRef, OTPInputProps>((props, ref) => {
|
||||
const { value, onChange, onActiveChange, index, ...restProps } = props;
|
||||
|
||||
const onInternalChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
onChange(index, e.target.value);
|
||||
};
|
||||
|
||||
// ========================== Ref ===========================
|
||||
const inputRef = React.useRef<InputRef>(null);
|
||||
React.useImperativeHandle(ref, () => inputRef.current!);
|
||||
|
||||
// ========================= Focus ==========================
|
||||
const syncSelection = () => {
|
||||
raf(() => {
|
||||
const inputEle = inputRef.current?.input;
|
||||
if (document.activeElement === inputEle && inputEle) {
|
||||
inputEle.select();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ======================== Keyboard ========================
|
||||
const onInternalKeyDown: React.KeyboardEventHandler<HTMLInputElement> = ({ key }) => {
|
||||
if (key === 'ArrowLeft') {
|
||||
onActiveChange(index - 1);
|
||||
} else if (key === 'ArrowRight') {
|
||||
onActiveChange(index + 1);
|
||||
}
|
||||
|
||||
syncSelection();
|
||||
};
|
||||
|
||||
const onInternalKeyUp: React.KeyboardEventHandler<HTMLInputElement> = (e) => {
|
||||
if (e.key === 'Backspace' && !value) {
|
||||
onActiveChange(index - 1);
|
||||
}
|
||||
|
||||
syncSelection();
|
||||
};
|
||||
|
||||
// ========================= Render =========================
|
||||
return (
|
||||
<Input
|
||||
{...restProps}
|
||||
ref={inputRef}
|
||||
value={value}
|
||||
onInput={onInternalChange}
|
||||
onFocus={syncSelection}
|
||||
onKeyDown={onInternalKeyDown}
|
||||
onKeyUp={onInternalKeyUp}
|
||||
onMouseDown={syncSelection}
|
||||
onMouseUp={syncSelection}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default OTPInput;
|
||||
244
components/input/OTP/index.tsx
Normal file
244
components/input/OTP/index.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useEvent } from 'rc-util';
|
||||
import pickAttrs from 'rc-util/lib/pickAttrs';
|
||||
|
||||
import { getMergedStatus, type InputStatus } from '../../_util/statusUtils';
|
||||
import { ConfigContext } from '../../config-provider';
|
||||
import useCSSVarCls from '../../config-provider/hooks/useCSSVarCls';
|
||||
import useSize from '../../config-provider/hooks/useSize';
|
||||
import { type SizeType } from '../../config-provider/SizeContext';
|
||||
import { FormItemInputContext } from '../../form/context';
|
||||
import type { Variant } from '../../form/hooks/useVariants';
|
||||
import { type InputRef } from '../Input';
|
||||
import useStyle from '../style/otp';
|
||||
import OTPInput, { type OTPInputProps } from './OTPInput';
|
||||
|
||||
export interface OTPRef {
|
||||
focus: VoidFunction;
|
||||
blur: VoidFunction;
|
||||
nativeElement: HTMLDivElement;
|
||||
}
|
||||
|
||||
export interface OTPProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
||||
prefixCls?: string;
|
||||
length?: number;
|
||||
|
||||
// Style
|
||||
variant?: Variant;
|
||||
rootClassName?: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
size?: SizeType;
|
||||
|
||||
// Values
|
||||
defaultValue?: string;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
formatter?: (value: string) => string;
|
||||
|
||||
// Status
|
||||
disabled?: boolean;
|
||||
status?: InputStatus;
|
||||
}
|
||||
|
||||
function strToArr(str: string) {
|
||||
return str.split('');
|
||||
}
|
||||
|
||||
const OTP = React.forwardRef<OTPRef, OTPProps>((props, ref) => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
length = 6,
|
||||
size: customSize,
|
||||
defaultValue,
|
||||
value,
|
||||
onChange,
|
||||
formatter,
|
||||
variant,
|
||||
disabled,
|
||||
status: customStatus,
|
||||
autoFocus,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const { getPrefixCls, direction } = React.useContext(ConfigContext);
|
||||
const prefixCls = getPrefixCls('otp', customizePrefixCls);
|
||||
|
||||
const domAttrs = pickAttrs(restProps, {
|
||||
aria: true,
|
||||
data: true,
|
||||
attr: true,
|
||||
});
|
||||
|
||||
// ========================= Root =========================
|
||||
// Style
|
||||
const rootCls = useCSSVarCls(prefixCls);
|
||||
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
|
||||
|
||||
// ========================= Size =========================
|
||||
const mergedSize = useSize((ctx) => customSize ?? ctx);
|
||||
|
||||
// ======================== Status ========================
|
||||
const formContext = React.useContext(FormItemInputContext);
|
||||
const mergedStatus = getMergedStatus(formContext.status, customStatus);
|
||||
|
||||
const proxyFormContext = React.useMemo(
|
||||
() => ({
|
||||
...formContext,
|
||||
status: mergedStatus,
|
||||
hasFeedback: false,
|
||||
feedbackIcon: null,
|
||||
}),
|
||||
[formContext, mergedStatus],
|
||||
);
|
||||
|
||||
// ========================= Refs =========================
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const refs = React.useRef<Record<number, InputRef | null>>({});
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
refs.current[0]?.focus();
|
||||
},
|
||||
blur: () => {
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
refs.current[i]?.blur();
|
||||
}
|
||||
},
|
||||
nativeElement: containerRef.current!,
|
||||
}));
|
||||
|
||||
// ======================= Formatter ======================
|
||||
const internalFormatter = (txt: string) => (formatter ? formatter(txt) : txt);
|
||||
|
||||
// ======================== Values ========================
|
||||
const [valueCells, setValueCells] = React.useState<string[]>(
|
||||
strToArr(internalFormatter(defaultValue || '')),
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (value) {
|
||||
setValueCells(strToArr(value));
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const triggerValueCellsChange = useEvent((nextValueCells: string[]) => {
|
||||
setValueCells(nextValueCells);
|
||||
|
||||
// Trigger if all cells are filled
|
||||
if (
|
||||
onChange &&
|
||||
nextValueCells.length === length &&
|
||||
nextValueCells.every((c) => c) &&
|
||||
nextValueCells.some((c, index) => valueCells[index] !== c)
|
||||
) {
|
||||
onChange(nextValueCells.join(''));
|
||||
}
|
||||
});
|
||||
|
||||
const patchValue = useEvent((index: number, txt: string) => {
|
||||
let nextCells = [...valueCells];
|
||||
|
||||
// Fill cells till index
|
||||
for (let i = 0; i < index; i += 1) {
|
||||
if (!nextCells[i]) {
|
||||
nextCells[i] = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (txt.length <= 1) {
|
||||
nextCells[index] = txt;
|
||||
} else {
|
||||
nextCells = nextCells.slice(0, index).concat(strToArr(txt));
|
||||
}
|
||||
nextCells = nextCells.slice(0, length);
|
||||
|
||||
// Clean the last empty cell
|
||||
for (let i = nextCells.length - 1; i >= 0; i -= 1) {
|
||||
if (nextCells[i]) {
|
||||
break;
|
||||
}
|
||||
nextCells.pop();
|
||||
}
|
||||
|
||||
// Format if needed
|
||||
const formattedValue = internalFormatter(nextCells.map((c) => c || ' ').join(''));
|
||||
nextCells = strToArr(formattedValue).map((c, i) => {
|
||||
if (c === ' ' && !nextCells[i]) {
|
||||
return nextCells[i];
|
||||
}
|
||||
return c;
|
||||
});
|
||||
|
||||
return nextCells;
|
||||
});
|
||||
|
||||
// ======================== Change ========================
|
||||
const onInputChange: OTPInputProps['onChange'] = (index, txt) => {
|
||||
const nextCells = patchValue(index, txt);
|
||||
|
||||
const nextIndex = Math.min(index + txt.length, length - 1);
|
||||
if (nextIndex !== index) {
|
||||
refs.current[nextIndex]?.focus();
|
||||
}
|
||||
|
||||
triggerValueCellsChange(nextCells);
|
||||
};
|
||||
|
||||
const onInputActiveChange: OTPInputProps['onActiveChange'] = (nextIndex) => {
|
||||
refs.current[nextIndex]?.focus();
|
||||
};
|
||||
|
||||
// ======================== Render ========================
|
||||
const inputSharedProps = {
|
||||
variant,
|
||||
disabled,
|
||||
status: mergedStatus as InputStatus,
|
||||
};
|
||||
|
||||
return wrapCSSVar(
|
||||
<div
|
||||
{...domAttrs}
|
||||
ref={containerRef}
|
||||
className={classNames(
|
||||
prefixCls,
|
||||
{
|
||||
[`${prefixCls}-sm`]: mergedSize === 'small',
|
||||
[`${prefixCls}-lg`]: mergedSize === 'large',
|
||||
[`${prefixCls}-rtl`]: direction === 'rtl',
|
||||
},
|
||||
cssVarCls,
|
||||
hashId,
|
||||
)}
|
||||
>
|
||||
<FormItemInputContext.Provider value={proxyFormContext}>
|
||||
{new Array(length).fill(0).map((_, index) => {
|
||||
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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</FormItemInputContext.Provider>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
|
||||
export default OTP;
|
||||
@@ -10114,6 +10114,245 @@ exports[`renders components/input/demo/group.tsx extend context correctly 2`] =
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders components/input/demo/otp.tsx extend context correctly 1`] = `
|
||||
<div
|
||||
class="ant-space ant-space-vertical ant-space-gap-row-small ant-space-gap-col-small"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
With formatter (Upcase)
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<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=""
|
||||
/>
|
||||
<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>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
With Disabled
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
With Length (8)
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<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=""
|
||||
/>
|
||||
<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=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
With variant
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders components/input/demo/otp.tsx extend context correctly 2`] = `[]`;
|
||||
|
||||
exports[`renders components/input/demo/password-input.tsx extend context correctly 1`] = `
|
||||
<div
|
||||
class="ant-space ant-space-vertical ant-space-gap-row-small ant-space-gap-col-small"
|
||||
|
||||
@@ -3511,6 +3511,243 @@ exports[`renders components/input/demo/group.tsx correctly 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders components/input/demo/otp.tsx correctly 1`] = `
|
||||
<div
|
||||
class="ant-space ant-space-vertical ant-space-gap-row-small ant-space-gap-col-small"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
With formatter (Upcase)
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<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=""
|
||||
/>
|
||||
<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>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
With Disabled
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
With Length (8)
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<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=""
|
||||
/>
|
||||
<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=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
With variant
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders components/input/demo/password-input.tsx correctly 1`] = `
|
||||
<div
|
||||
class="ant-space ant-space-vertical ant-space-gap-row-small ant-space-gap-col-small"
|
||||
|
||||
44
components/input/__tests__/__snapshots__/otp.test.tsx.snap
Normal file
44
components/input/__tests__/__snapshots__/otp.test.tsx.snap
Normal file
@@ -0,0 +1,44 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Input.OTP rtl render component should be rendered correctly in RTL direction 1`] = `
|
||||
<div
|
||||
class="ant-otp ant-otp-rtl"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-rtl ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-rtl ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-rtl ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-rtl ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-rtl ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-rtl ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
131
components/input/__tests__/otp.test.tsx
Normal file
131
components/input/__tests__/otp.test.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React from 'react';
|
||||
|
||||
import Input from '..';
|
||||
import focusTest from '../../../tests/shared/focusTest';
|
||||
import mountTest from '../../../tests/shared/mountTest';
|
||||
import rtlTest from '../../../tests/shared/rtlTest';
|
||||
import { fireEvent, render, waitFakeTimer } from '../../../tests/utils';
|
||||
|
||||
const { OTP } = Input;
|
||||
|
||||
describe('Input.OTP', () => {
|
||||
focusTest(Input.OTP, { refFocus: true });
|
||||
mountTest(Input.OTP);
|
||||
rtlTest(Input.OTP);
|
||||
|
||||
function getText(container: HTMLElement) {
|
||||
const inputList = container.querySelectorAll('input');
|
||||
return Array.from(inputList)
|
||||
.map((input) => input.value || ' ')
|
||||
.join('')
|
||||
.replace(/\s*$/, '');
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('paste to fill all', async () => {
|
||||
const onChange = jest.fn();
|
||||
const { container } = render(<OTP onChange={onChange} />);
|
||||
|
||||
fireEvent.input(container.querySelector('input')!, { target: { value: '123456' } });
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onChange).toHaveBeenCalledWith('123456');
|
||||
});
|
||||
|
||||
it('fill step by step', () => {
|
||||
const CODE = 'BAMBOO';
|
||||
const onChange = jest.fn();
|
||||
render(<OTP onChange={onChange} autoFocus />);
|
||||
|
||||
for (let i = 0; i < CODE.length; i += 1) {
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
fireEvent.input(document.activeElement!, { target: { value: CODE[i] } });
|
||||
}
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onChange).toHaveBeenCalledWith(CODE);
|
||||
});
|
||||
|
||||
it('backspace to delete', async () => {
|
||||
const CODE = 'LITTLE';
|
||||
|
||||
const onChange = jest.fn();
|
||||
const { container } = render(<OTP defaultValue={CODE} onChange={onChange} />);
|
||||
expect(getText(container)).toBe(CODE);
|
||||
|
||||
// Focus on the last cell
|
||||
const inputList = container.querySelectorAll('input');
|
||||
inputList[inputList.length - 1].focus();
|
||||
|
||||
for (let i = 0; i < CODE.length; i += 1) {
|
||||
fireEvent.keyDown(document.activeElement!, { key: 'Backspace' });
|
||||
fireEvent.input(document.activeElement!, { target: { value: '' } });
|
||||
fireEvent.keyUp(document.activeElement!, { key: 'Backspace' });
|
||||
}
|
||||
|
||||
expect(getText(container)).toBe('');
|
||||
|
||||
// We do not trigger change if empty. It's safe to modify this logic if needed.
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('controlled', () => {
|
||||
const { container, rerender } = render(<OTP value="BAMBOO" />);
|
||||
expect(getText(container)).toBe('BAMBOO');
|
||||
|
||||
rerender(<OTP value="LITTLE" />);
|
||||
expect(getText(container)).toBe('LITTLE');
|
||||
});
|
||||
|
||||
it('focus to selection', async () => {
|
||||
const { container } = render(<OTP defaultValue="BAMBOO" />);
|
||||
|
||||
const firstInput = container.querySelector('input')!;
|
||||
const selectSpy = jest.spyOn(firstInput, 'select');
|
||||
expect(selectSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Trigger focus
|
||||
firstInput.focus();
|
||||
await waitFakeTimer();
|
||||
|
||||
expect(selectSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('arrow key to switch', () => {
|
||||
const { container } = render(<OTP autoFocus />);
|
||||
|
||||
const inputList = Array.from(container.querySelectorAll('input'));
|
||||
expect(document.activeElement).toEqual(inputList[0]);
|
||||
|
||||
fireEvent.keyDown(document.activeElement!, { key: 'ArrowRight' });
|
||||
expect(document.activeElement).toEqual(inputList[1]);
|
||||
|
||||
fireEvent.keyDown(document.activeElement!, { key: 'ArrowLeft' });
|
||||
expect(document.activeElement).toEqual(inputList[0]);
|
||||
});
|
||||
|
||||
it('fill last cell', () => {
|
||||
const { container } = render(<OTP />);
|
||||
fireEvent.input(container.querySelectorAll('input')[5], { target: { value: '1' } });
|
||||
|
||||
expect(getText(container)).toBe(' 1');
|
||||
});
|
||||
|
||||
it('formatter', () => {
|
||||
const { container } = render(
|
||||
<OTP defaultValue="bamboo" formatter={(val) => val.toUpperCase()} />,
|
||||
);
|
||||
expect(getText(container)).toBe('BAMBOO');
|
||||
|
||||
// Type to trigger formatter
|
||||
fireEvent.input(container.querySelector('input')!, { target: { value: 'little' } });
|
||||
expect(getText(container)).toBe('LITTLE');
|
||||
});
|
||||
});
|
||||
7
components/input/demo/otp.md
Normal file
7
components/input/demo/otp.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
一次性密码输入框。
|
||||
|
||||
## en-US
|
||||
|
||||
One time password input.
|
||||
29
components/input/demo/otp.tsx
Normal file
29
components/input/demo/otp.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { Input, Space, Typography, type GetProp } from 'antd';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const App: React.FC = () => {
|
||||
const onChange: GetProp<typeof Input.OTP, 'onChange'> = (text) => {
|
||||
console.log('onChange:', text);
|
||||
};
|
||||
|
||||
const sharedProps = {
|
||||
onChange,
|
||||
};
|
||||
|
||||
return (
|
||||
<Space direction="vertical">
|
||||
<Title level={5}>With formatter (Upcase)</Title>
|
||||
<Input.OTP formatter={(str) => str.toUpperCase()} {...sharedProps} />
|
||||
<Title level={5}>With Disabled</Title>
|
||||
<Input.OTP disabled {...sharedProps} />
|
||||
<Title level={5}>With Length (8)</Title>
|
||||
<Input.OTP length={8} {...sharedProps} />
|
||||
<Title level={5}>With variant</Title>
|
||||
<Input.OTP variant="filled" {...sharedProps} />
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -28,6 +28,7 @@ demo:
|
||||
<code src="./demo/search-input-loading.tsx">Search box with loading</code>
|
||||
<code src="./demo/textarea.tsx">TextArea</code>
|
||||
<code src="./demo/autosize-textarea.tsx">Autosizing the height to fit the content</code>
|
||||
<code src="./demo/otp.tsx" version="5.16.0">OTP</code>
|
||||
<code src="./demo/tooltip.tsx">Format Tooltip Input</code>
|
||||
<code src="./demo/presuffix.tsx">prefix and suffix</code>
|
||||
<code src="./demo/password-input.tsx">Password box</code>
|
||||
@@ -102,7 +103,7 @@ Same as Input, and more:
|
||||
|
||||
The rest of the props of `Input.TextArea` are the same as the original [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea).
|
||||
|
||||
#### Input.Search
|
||||
### Input.Search
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -112,13 +113,29 @@ The rest of the props of `Input.TextArea` are the same as the original [textarea
|
||||
|
||||
Supports all props of `Input`.
|
||||
|
||||
#### Input.Password
|
||||
### Input.Password
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| iconRender | Custom toggle button | (visible) => ReactNode | (visible) => (visible ? <EyeOutlined /> : <EyeInvisibleOutlined />) | 4.3.0 |
|
||||
| visibilityToggle | Whether show toggle button or control password visible | boolean \| [VisibilityToggle](#visibilitytoggle) | true | |
|
||||
|
||||
### Input.OTP
|
||||
|
||||
Added in `5.16.0`.
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| defaultValue | Default value | string | - | |
|
||||
| disabled | Whether the input is disabled | boolean | false | |
|
||||
| formatter | Format display, blank fields will be filled with ` ` | (value: string) => string | - | |
|
||||
| length | The number of input elements | number | 6 | |
|
||||
| status | Set validation status | 'error' \| 'warning' | - | |
|
||||
| size | The size of the input box | `small` \| `middle` \| `large` | `middle` | |
|
||||
| variant | Variants of Input | `outlined` \| `borderless` \| `filled` | `outlined` | |
|
||||
| value | The input content value | string | - | |
|
||||
| onChange | Trigger when all the fields are filled | function(value: string) | - | |
|
||||
|
||||
#### VisibilityToggle
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type * as React from 'react';
|
||||
|
||||
import Group from './Group';
|
||||
import type { InputProps, InputRef } from './Input';
|
||||
import InternalInput from './Input';
|
||||
import OTP from './OTP';
|
||||
import Password from './Password';
|
||||
import Search from './Search';
|
||||
import TextArea from './TextArea';
|
||||
@@ -19,16 +21,14 @@ type CompoundedComponent = React.ForwardRefExoticComponent<
|
||||
Search: typeof Search;
|
||||
TextArea: typeof TextArea;
|
||||
Password: typeof Password;
|
||||
OTP: typeof OTP;
|
||||
};
|
||||
|
||||
const Input = InternalInput as CompoundedComponent;
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Input.displayName = 'Input';
|
||||
}
|
||||
|
||||
Input.Group = Group;
|
||||
Input.Search = Search;
|
||||
Input.TextArea = TextArea;
|
||||
Input.Password = Password;
|
||||
Input.OTP = OTP;
|
||||
export default Input;
|
||||
|
||||
@@ -29,6 +29,7 @@ demo:
|
||||
<code src="./demo/search-input-loading.tsx">搜索框 loading</code>
|
||||
<code src="./demo/textarea.tsx">文本域</code>
|
||||
<code src="./demo/autosize-textarea.tsx">适应文本高度的文本域</code>
|
||||
<code src="./demo/otp.tsx" version="5.16.0">一次性密码框</code>
|
||||
<code src="./demo/tooltip.tsx">输入时格式化展示</code>
|
||||
<code src="./demo/presuffix.tsx">前缀和后缀</code>
|
||||
<code src="./demo/password-input.tsx">密码框</code>
|
||||
@@ -103,7 +104,7 @@ interface CountConfig {
|
||||
|
||||
`Input.TextArea` 的其他属性和浏览器自带的 [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) 一致。
|
||||
|
||||
#### Input.Search
|
||||
### Input.Search
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -113,13 +114,29 @@ interface CountConfig {
|
||||
|
||||
其余属性和 Input 一致。
|
||||
|
||||
#### Input.Password
|
||||
### Input.Password
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| iconRender | 自定义切换按钮 | (visible) => ReactNode | (visible) => (visible ? <EyeOutlined /> : <EyeInvisibleOutlined />) | 4.3.0 |
|
||||
| visibilityToggle | 是否显示切换按钮或者控制密码显隐 | boolean \| [VisibilityToggle](#visibilitytoggle) | true | |
|
||||
|
||||
### Input.OTP
|
||||
|
||||
`5.16.0` 新增。
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| defaultValue | 默认值 | string | - | |
|
||||
| disabled | 是否禁用 | boolean | false | |
|
||||
| formatter | 格式化展示,留空字段会被 ` ` 填充 | (value: string) => string | - | |
|
||||
| length | 输入元素数量 | number | 6 | |
|
||||
| status | 设置校验状态 | 'error' \| 'warning' | - | |
|
||||
| size | 输入框大小 | `small` \| `middle` \| `large` | `middle` | |
|
||||
| variant | 形态变体 | `outlined` \| `borderless` \| `filled` | `outlined` | |
|
||||
| value | 输入框内容 | string | - | |
|
||||
| onChange | 当输入框内容全部填充时触发回调 | function(value: string) | - | |
|
||||
|
||||
#### VisibilityToggle
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
|
||||
47
components/input/style/otp.ts
Normal file
47
components/input/style/otp.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { GenerateStyle } from '../../theme/internal';
|
||||
import { genStyleHooks, mergeToken } from '../../theme/internal';
|
||||
import type { InputToken } from './token';
|
||||
import { initComponentToken, initInputToken } from './token';
|
||||
|
||||
// =============================== OTP ================================
|
||||
const genOTPStyle: GenerateStyle<InputToken> = (token) => {
|
||||
const { componentCls, paddingXS } = token;
|
||||
|
||||
return {
|
||||
[`${componentCls}`]: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'nowrap',
|
||||
columnGap: paddingXS,
|
||||
|
||||
'&-rtl': {
|
||||
direction: 'rtl',
|
||||
},
|
||||
|
||||
[`${componentCls}-input`]: {
|
||||
textAlign: 'center',
|
||||
paddingInline: token.paddingXXS,
|
||||
},
|
||||
|
||||
// ================= Size =================
|
||||
[`&${componentCls}-sm ${componentCls}-input`]: {
|
||||
paddingInline: token.calc(token.paddingXXS).div(2).equal(),
|
||||
},
|
||||
|
||||
[`&${componentCls}-lg ${componentCls}-input`]: {
|
||||
paddingInline: token.paddingXS,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// ============================== Export ==============================
|
||||
export default genStyleHooks(
|
||||
['Input', 'OTP'],
|
||||
(token) => {
|
||||
const inputToken = mergeToken<InputToken>(token, initInputToken(token));
|
||||
|
||||
return [genOTPStyle(inputToken)];
|
||||
},
|
||||
initComponentToken,
|
||||
);
|
||||
@@ -349,11 +349,11 @@
|
||||
"size-limit": [
|
||||
{
|
||||
"path": "./dist/antd.min.js",
|
||||
"limit": "336 KiB"
|
||||
"limit": "337 KiB"
|
||||
},
|
||||
{
|
||||
"path": "./dist/antd-with-locales.min.js",
|
||||
"limit": "383 KiB"
|
||||
"limit": "384 KiB"
|
||||
}
|
||||
],
|
||||
"title": "Ant Design",
|
||||
|
||||
@@ -116,9 +116,9 @@ exports[`site test Component components/image en Page 1`] = `4`;
|
||||
|
||||
exports[`site test Component components/image zh Page 1`] = `4`;
|
||||
|
||||
exports[`site test Component components/input en Page 1`] = `6`;
|
||||
exports[`site test Component components/input en Page 1`] = `7`;
|
||||
|
||||
exports[`site test Component components/input zh Page 1`] = `6`;
|
||||
exports[`site test Component components/input zh Page 1`] = `7`;
|
||||
|
||||
exports[`site test Component components/input-number en Page 1`] = `2`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user