Skip to content

Commit fb65991

Browse files
committed
wip: add event
1 parent 421df2f commit fb65991

File tree

5 files changed

+110
-14
lines changed

5 files changed

+110
-14
lines changed

components/input/OTP/OTPInput.tsx

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,63 @@
11
import Input from '../Input';
2-
import { computed, defineComponent, shallowRef } from 'vue';
2+
import { PropType, computed, defineComponent, nextTick, shallowRef } from 'vue';
3+
import inputProps from '../inputProps';
4+
import omit from '../../_util/omit';
5+
import { type ChangeEventHandler } from '../../_util/EventInterface';
36

47
export default defineComponent({
58
compatConfig: { MODE: 3 },
69
name: 'OTPInput',
710
inheritAttrs: false,
811
props: {
12+
...inputProps(),
913
index: Number,
1014
value: { type: String, default: undefined },
1115
mask: { type: [Boolean, String], default: false },
16+
onChange: { type: Function as PropType<(index: number, value: string) => void> },
1217
},
13-
setup(props, { attrs }) {
18+
setup(props, { attrs, expose }) {
1419
const inputRef = shallowRef();
1520
const internalValue = computed(() => {
16-
const { value, mask, ...resetProps } = props;
21+
const { value, mask } = props;
1722
return value && typeof mask === 'string' ? mask : value;
1823
});
1924

20-
return () => <Input ref={inputRef} class={attrs.class} value={internalValue.value} />;
25+
const syncSelection = () => {
26+
requestAnimationFrame(() => {
27+
inputRef.value.select();
28+
});
29+
};
30+
const handleSyncMouseDown = e => {
31+
e.preventDefault();
32+
syncSelection();
33+
};
34+
// ======================= Event handlers =================
35+
const onInternalChange: ChangeEventHandler = e => {
36+
props.onChange(props.index, e.target.value);
37+
};
38+
39+
const focus = () => {
40+
inputRef.value?.focus();
41+
syncSelection();
42+
};
43+
44+
expose({
45+
focus,
46+
});
47+
48+
return () => (
49+
<Input
50+
ref={inputRef}
51+
{...attrs}
52+
{...omit(props, ['index', 'value', 'mask', 'onChange'])}
53+
class={attrs.class}
54+
value={internalValue.value}
55+
onInput={onInternalChange}
56+
onMousedown={handleSyncMouseDown}
57+
onMouseUp={handleSyncMouseDown}
58+
onKeydown={syncSelection}
59+
onKeyup={syncSelection}
60+
/>
61+
);
2162
},
2263
});

components/input/OTP/index.tsx

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PropType, defineComponent, reactive, ref } from 'vue';
1+
import { PropType, defineComponent, nextTick, reactive, ref } from 'vue';
22
import inputProps from '../inputProps';
33
import { FormItemInputContext } from 'ant-design-vue/es/form/FormItemContext';
44
import useConfigInject from '../../config-provider/hooks/useConfigInject';
@@ -13,25 +13,77 @@ export default defineComponent({
1313
props: {
1414
...inputProps(),
1515
length: { type: Number, default: 6 },
16+
onChange: { type: Function as PropType<(value: string) => void>, default: undefined },
1617
formatter: { type: Function as PropType<(arg: string) => string>, default: undefined },
1718
defaultValue: { type: String, default: undefined },
1819
},
1920
setup(props, { attrs }) {
2021
const { prefixCls, direction, size } = useConfigInject('otp', props);
2122
// Style
2223
const [wrapSSR, hashId] = useStyle(prefixCls);
23-
24+
// ==================== Provider =========================
2425
const proxyFormContext = reactive({
2526
// TODO:
2627
});
2728
FormItemInputContext.useProvide(proxyFormContext);
2829

29-
const { defaultValue } = props;
30+
const refs = ref([]);
3031
const strToArr = (str: string) => (str || '').split('');
3132
// keep reactive
3233
const internalFormatter = (txt: string) => (props.formatter ? props.formatter(txt) : txt);
34+
const valueCells = ref<string[]>(strToArr(internalFormatter(props.defaultValue || '')));
35+
const patchValue = (index: number, txt: string) => {
36+
let nextCells = valueCells.value.slice();
37+
38+
for (let i = 0; i < index; i += 1) {
39+
if (!nextCells[i]) {
40+
nextCells[i] = '';
41+
}
42+
}
43+
44+
if (txt.length <= 1) {
45+
nextCells[index] = txt;
46+
} else {
47+
nextCells = nextCells.slice(0, index).concat(strToArr(txt));
48+
}
49+
50+
nextCells = nextCells.slice(0, props.length);
51+
for (let i = nextCells.length - 1; i >= 0; i -= 1) {
52+
if (nextCells[i]) {
53+
break;
54+
}
55+
nextCells.pop();
56+
}
57+
58+
const formattedValue = internalFormatter(nextCells.map(c => c || ' ').join(''));
59+
nextCells = strToArr(formattedValue).map((c, i) => {
60+
if (c === ' ' && !nextCells[i]) {
61+
return nextCells[i];
62+
}
63+
return c;
64+
});
3365

34-
const valueCells = ref<string[]>(strToArr(internalFormatter(defaultValue || '')));
66+
return nextCells;
67+
};
68+
69+
// ======================= Change handlers =================
70+
const onInputChange = (index: number, value: string) => {
71+
const nextValueCells = patchValue(index, value);
72+
const nextIndex = Math.min(index + value.length, props.length);
73+
if (nextIndex !== index) {
74+
refs.value[nextIndex]?.focus();
75+
}
76+
77+
if (
78+
props.onChange &&
79+
nextValueCells.length === props.length &&
80+
nextValueCells.every(v => v) &&
81+
nextValueCells.some((v, i) => v !== valueCells.value[i])
82+
) {
83+
props.onChange(nextValueCells.join(''));
84+
}
85+
valueCells.value = nextValueCells.slice();
86+
};
3587

3688
return () => {
3789
const cls = classNames(
@@ -44,19 +96,22 @@ export default defineComponent({
4496
attrs.class,
4597
hashId.value,
4698
);
47-
const { length } = props;
99+
const { length, autofocus } = props;
48100
return wrapSSR(
49101
<div class={cls}>
50102
{Array.from({ length }).map((_, index) => {
51103
const key = `opt-${index}`;
52104
const singleValue = valueCells.value[index];
53-
54105
return (
55106
<OTPInput
107+
ref={ref => (refs.value[index] = ref)}
56108
key={key}
57109
index={index}
58110
class={`${prefixCls.value}-input`}
59111
value={singleValue}
112+
htmlSize={1}
113+
onChange={onInputChange}
114+
autofocus={index === 0 && autofocus}
60115
/>
61116
);
62117
})}

components/input/style/otp.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ const genOTPInputStyle: GenerateStyle<InputToken> = (token: InputToken) => {
1010
alignItems: 'center',
1111
flexWrap: 'nowrap',
1212
columnGap: paddingXS,
13-
padding: 0,
14-
// border: 'none',
1513

1614
'&-rtl': {
1715
direction: 'rtl',

components/vc-input/Input.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export default defineComponent({
181181
),
182182
ref: inputRef,
183183
key: 'ant-input',
184-
size: htmlSize,
184+
size: htmlSize ? String(htmlSize) : undefined,
185185
type,
186186
lazy: props.lazy,
187187
};
@@ -191,7 +191,7 @@ export default defineComponent({
191191
if (!inputProps.autofocus) {
192192
delete inputProps.autofocus;
193193
}
194-
const inputNode = <BaseInputCore {...omit(inputProps, ['size'])} />;
194+
const inputNode = <BaseInputCore {...inputProps} />;
195195
return inputNode;
196196
};
197197
const getSuffix = () => {

components/vc-input/inputProps.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ export const inputProps = () => ({
9191
onPressEnter: Function as PropType<KeyboardEventHandler>,
9292
onKeydown: Function as PropType<KeyboardEventHandler>,
9393
onKeyup: Function as PropType<KeyboardEventHandler>,
94+
onMousedown: { type: Function as PropType<MouseEventHandler>, default: undefined },
95+
onMouseUp: { type: Function as PropType<MouseEventHandler>, default: undefined },
9496
onFocus: Function as PropType<FocusEventHandler>,
9597
onBlur: Function as PropType<FocusEventHandler>,
9698
onChange: Function as PropType<ChangeEventHandler>,

0 commit comments

Comments
 (0)