fix(GroupTable): prevent Input cursor jumping to end on keystroke (#4208)

Refactor updateRow/addRow/removeRow to use functional setRows(prev => ...)
and ref-based onChange/duplicateNames access, making columns useMemo stable
across keystrokes so Semi UI Table does not re-mount Input components.
This commit is contained in:
萧邦
2026-04-13 14:41:40 +08:00
committed by GitHub
parent 8b22161527
commit c20060931b
@@ -1,4 +1,4 @@
import React, { useState, useCallback, useMemo } from 'react';
import React, { useState, useCallback, useMemo, useRef } from 'react';
import {
Button,
Input,
@@ -61,60 +61,63 @@ export function serializeGroupTable(rows) {
};
}
export default function GroupTable({
groupRatio,
userUsableGroups,
onChange,
}) {
export default function GroupTable({ groupRatio, userUsableGroups, onChange }) {
const { t } = useTranslation();
const [rows, setRows] = useState(() =>
buildRows(groupRatio, userUsableGroups),
);
const emitChange = useCallback(
(newRows) => {
setRows(newRows);
onChange?.(serializeGroupTable(newRows));
},
[onChange],
);
// Use functional setRows to keep updateRow/addRow/removeRow referentially
// stable, preventing columns useMemo from rebuilding on every keystroke
// which causes the Input cursor to jump to end (cursor reset bug).
const onChangeRef = useRef(onChange);
onChangeRef.current = onChange;
const emitAndSet = useCallback((updater) => {
setRows((prev) => {
const next = typeof updater === 'function' ? updater(prev) : updater;
onChangeRef.current?.(serializeGroupTable(next));
return next;
});
}, []);
const updateRow = useCallback(
(id, field, value) => {
const next = rows.map((r) =>
r._id === id ? { ...r, [field]: value } : r,
emitAndSet((prev) =>
prev.map((r) => (r._id === id ? { ...r, [field]: value } : r)),
);
emitChange(next);
},
[rows, emitChange],
[emitAndSet],
);
const addRow = useCallback(() => {
const existingNames = new Set(rows.map((r) => r.name));
let counter = 1;
let newName = `group_${counter}`;
while (existingNames.has(newName)) {
counter++;
newName = `group_${counter}`;
}
emitChange([
...rows,
{
_id: uid(),
name: newName,
ratio: 1,
selectable: true,
description: '',
},
]);
}, [rows, emitChange]);
emitAndSet((prev) => {
const existingNames = new Set(prev.map((r) => r.name));
let counter = 1;
let newName = `group_${counter}`;
while (existingNames.has(newName)) {
counter++;
newName = `group_${counter}`;
}
return [
...prev,
{
_id: uid(),
name: newName,
ratio: 1,
selectable: true,
description: '',
},
];
});
}, [emitAndSet]);
const removeRow = useCallback(
(id) => {
emitChange(rows.filter((r) => r._id !== id));
emitAndSet((prev) => prev.filter((r) => r._id !== id));
},
[rows, emitChange],
[emitAndSet],
);
const groupNames = useMemo(() => rows.map((r) => r.name), [rows]);
@@ -127,6 +130,11 @@ export default function GroupTable({
return new Set(Object.keys(counts).filter((k) => counts[k] > 1));
}, [groupNames]);
// Use ref so column render functions always read the latest duplicate set
// without adding duplicateNames to columns deps (which would break cursor).
const duplicateNamesRef = useRef(duplicateNames);
duplicateNamesRef.current = duplicateNames;
const columns = useMemo(
() => [
{
@@ -138,7 +146,9 @@ export default function GroupTable({
<Input
size='small'
value={record.name}
status={duplicateNames.has(record.name) ? 'warning' : undefined}
status={
duplicateNamesRef.current.has(record.name) ? 'warning' : undefined
}
onChange={(v) => updateRow(record._id, 'name', v)}
/>
),
@@ -212,7 +222,7 @@ export default function GroupTable({
),
},
],
[t, duplicateNames, updateRow, removeRow],
[t, updateRow, removeRow],
);
return (
@@ -223,9 +233,7 @@ export default function GroupTable({
rowKey='_id'
hidePagination
size='small'
empty={
<Text type='tertiary'>{t('暂无分组,点击下方按钮添加')}</Text>
}
empty={<Text type='tertiary'>{t('暂无分组,点击下方按钮添加')}</Text>}
/>
<div className='mt-3 flex justify-center'>
<Button icon={<IconPlus />} theme='outline' onClick={addRow}>
@@ -234,7 +242,8 @@ export default function GroupTable({
</div>
{duplicateNames.size > 0 && (
<Text type='warning' size='small' className='mt-2 block'>
{t('存在重复的分组名称:')}{Array.from(duplicateNames).join(', ')}
{t('存在重复的分组名称:')}
{Array.from(duplicateNames).join(', ')}
</Text>
)}
</div>