@rainbow-oh/yee-c 0.14.5 → 0.14.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/Form/Field.js +20 -1
- package/dist/cjs/Form/Group.js +3 -1
- package/dist/cjs/Form/List.js +13 -3
- package/dist/cjs/Selector/style/index.less +2 -2
- package/dist/cjs/style/color.css +5 -5
- package/dist/cjs/style/color.gemini.css +1 -1
- package/dist/cjs/style/themes.css +2 -2
- package/dist/cjs/style/variables.css +6 -6
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/Form/Field.js +35 -4
- package/dist/esm/Form/Group.js +10 -6
- package/dist/esm/Form/List.js +26 -8
- package/dist/esm/Selector/style/index.less +2 -2
- package/dist/esm/style/color.css +5 -5
- package/dist/esm/style/color.gemini.css +1 -1
- package/dist/esm/style/themes.css +2 -2
- package/dist/esm/style/variables.css +6 -6
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/umd/yee-c.min.css +2 -2
- package/dist/umd/yee-c.min.js +1 -1
- package/package.json +1 -1
- package/src/Form/Field.required.test.tsx +146 -0
- package/src/Form/Field.tsx +32 -2
- package/src/Form/Group.tsx +7 -2
- package/src/Form/List.test.tsx +126 -0
- package/src/Form/List.tsx +24 -3
- package/src/Selector/style/index.less +2 -2
- package/src/style/color.css +5 -5
- package/src/style/color.gemini.css +1 -1
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { cleanup, render, waitFor } from '@testing-library/react';
|
|
4
|
+
import { act } from 'react';
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
// act 需要(仓库无全局 vitest setup,逐文件声明)
|
|
8
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
import Field from './Field';
|
|
10
|
+
import FieldContext from './FieldContext';
|
|
11
|
+
import FormContext from './FormContext';
|
|
12
|
+
import Group from './Group';
|
|
13
|
+
import { FormStore } from './form-store';
|
|
14
|
+
|
|
15
|
+
const formContextValue = { prefixCls: 'yee-form' } as const;
|
|
16
|
+
|
|
17
|
+
function Harness({ required }: { required: boolean }) {
|
|
18
|
+
return (
|
|
19
|
+
<Field name="username" required={required}>
|
|
20
|
+
<input />
|
|
21
|
+
</Field>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function GroupHarness({ required }: { required: boolean }) {
|
|
26
|
+
return (
|
|
27
|
+
<Group required={required} label="组">
|
|
28
|
+
<Field name="a">
|
|
29
|
+
<input />
|
|
30
|
+
</Field>
|
|
31
|
+
</Group>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function renderField(required: boolean) {
|
|
36
|
+
// 测试无 LocaleProvider,默认必填文案为空串(不渲染错误 DOM),手动设置
|
|
37
|
+
const formStore = new FormStore();
|
|
38
|
+
formStore.setDefaultValidateMessage('该字段必填');
|
|
39
|
+
const store = formStore.getForm();
|
|
40
|
+
const { container, rerender } = render(
|
|
41
|
+
<FormContext.Provider value={formContextValue}>
|
|
42
|
+
<FieldContext.Provider value={store}>
|
|
43
|
+
<Harness required={required} />
|
|
44
|
+
</FieldContext.Provider>
|
|
45
|
+
</FormContext.Provider>,
|
|
46
|
+
);
|
|
47
|
+
const rerenderWith = (nextRequired: boolean) =>
|
|
48
|
+
rerender(
|
|
49
|
+
<FormContext.Provider value={formContextValue}>
|
|
50
|
+
<FieldContext.Provider value={store}>
|
|
51
|
+
<Harness required={nextRequired} />
|
|
52
|
+
</FieldContext.Provider>
|
|
53
|
+
</FormContext.Provider>,
|
|
54
|
+
);
|
|
55
|
+
const errorEl = () => container.querySelector('.yee-form-item-additional-error');
|
|
56
|
+
return { store, rerenderWith, errorEl };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
cleanup();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// required 触发校验报错后,把 required 改为 false,错误应立即消失:
|
|
64
|
+
// 1) store 里注册的 entity 必须读到最新 props(旧实现是首帧快照,重校验仍失败)
|
|
65
|
+
// 2) required/rules 变化时需对已报错字段重新校验,否则旧错误无人清除
|
|
66
|
+
describe('Field required 运行时切换', () => {
|
|
67
|
+
it('报错后 required 改为 false,错误立即消失', async () => {
|
|
68
|
+
const { store, rerenderWith, errorEl } = renderField(true);
|
|
69
|
+
|
|
70
|
+
await act(async () => {
|
|
71
|
+
await store.validateField('username');
|
|
72
|
+
});
|
|
73
|
+
expect(store.getFieldValidate('username')).not.toBeNull();
|
|
74
|
+
expect(errorEl()).not.toBeNull();
|
|
75
|
+
|
|
76
|
+
rerenderWith(false);
|
|
77
|
+
|
|
78
|
+
expect(store.getFieldValidate('username')).toBeNull();
|
|
79
|
+
// 错误文案经 AnimatePresence 退出动画移除,jsdom 需等动画结束
|
|
80
|
+
await waitFor(() => expect(errorEl()).toBeNull());
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('required 改为 false 后再次校验通过', async () => {
|
|
84
|
+
const { store, rerenderWith } = renderField(true);
|
|
85
|
+
|
|
86
|
+
await act(async () => {
|
|
87
|
+
await store.validateField('username');
|
|
88
|
+
});
|
|
89
|
+
rerenderWith(false);
|
|
90
|
+
|
|
91
|
+
await act(async () => {
|
|
92
|
+
await store.validateField('username');
|
|
93
|
+
});
|
|
94
|
+
expect(store.getFieldValidate('username')).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('未报错字段在 rules 变化时保持无错(不骚扰未触碰字段)', () => {
|
|
98
|
+
const { store, rerenderWith } = renderField(true);
|
|
99
|
+
rerenderWith(false);
|
|
100
|
+
expect(store.getFieldValidate('username')).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('报错后 required 保持 true 且值仍为空,重校验仍报错', async () => {
|
|
104
|
+
const { store, rerenderWith, errorEl } = renderField(true);
|
|
105
|
+
|
|
106
|
+
await act(async () => {
|
|
107
|
+
await store.validateField('username');
|
|
108
|
+
});
|
|
109
|
+
rerenderWith(true);
|
|
110
|
+
|
|
111
|
+
expect(store.getFieldValidate('username')).not.toBeNull();
|
|
112
|
+
expect(errorEl()).not.toBeNull();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('Group required 运行时切换', () => {
|
|
117
|
+
it('报错后 required 改为 false,submit 不再报组错误', async () => {
|
|
118
|
+
const store = new FormStore().getForm();
|
|
119
|
+
const { rerender } = render(
|
|
120
|
+
<FormContext.Provider value={formContextValue}>
|
|
121
|
+
<FieldContext.Provider value={store}>
|
|
122
|
+
<GroupHarness required />
|
|
123
|
+
</FieldContext.Provider>
|
|
124
|
+
</FormContext.Provider>,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
let errors: unknown[] = [];
|
|
128
|
+
await act(async () => {
|
|
129
|
+
errors = await store.submit();
|
|
130
|
+
});
|
|
131
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
132
|
+
|
|
133
|
+
rerender(
|
|
134
|
+
<FormContext.Provider value={formContextValue}>
|
|
135
|
+
<FieldContext.Provider value={store}>
|
|
136
|
+
<GroupHarness required={false} />
|
|
137
|
+
</FieldContext.Provider>
|
|
138
|
+
</FormContext.Provider>,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
await act(async () => {
|
|
142
|
+
errors = await store.submit();
|
|
143
|
+
});
|
|
144
|
+
expect(errors).toHaveLength(0);
|
|
145
|
+
});
|
|
146
|
+
});
|
package/src/Form/Field.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
2
|
import { HelpCircle } from 'lucide-react';
|
|
3
3
|
import { AnimatePresence, motion } from 'motion/react';
|
|
4
|
-
import React, { useContext, useLayoutEffect, useMemo, useReducer } from 'react';
|
|
4
|
+
import React, { useContext, useLayoutEffect, useMemo, useReducer, useRef } from 'react';
|
|
5
5
|
import Tooltip from '../Tooltip';
|
|
6
6
|
import FieldContext from './FieldContext';
|
|
7
7
|
import FormContext from './FormContext';
|
|
@@ -45,6 +45,7 @@ const Field: React.FC<FieldProps> = (props) => {
|
|
|
45
45
|
const {
|
|
46
46
|
getCallbacks,
|
|
47
47
|
getFieldStatus,
|
|
48
|
+
getFieldValidate,
|
|
48
49
|
getFieldValue,
|
|
49
50
|
getFieldsValue,
|
|
50
51
|
setFieldsValue,
|
|
@@ -74,14 +75,43 @@ const Field: React.FC<FieldProps> = (props) => {
|
|
|
74
75
|
return stringifyPath(fullFieldNamePath);
|
|
75
76
|
}, [fullFieldNamePath]);
|
|
76
77
|
|
|
78
|
+
// Registration happens once per field name, but the store (validateField)
|
|
79
|
+
// reads validation config off entity.props on every validation pass — the
|
|
80
|
+
// getter keeps those reads on the latest render's props so runtime changes
|
|
81
|
+
// to required/rules take effect.
|
|
82
|
+
const propsRef = useRef(props);
|
|
83
|
+
propsRef.current = props;
|
|
84
|
+
|
|
77
85
|
useLayoutEffect(() => {
|
|
78
86
|
const unregister = registerFieldEntities?.({
|
|
79
|
-
props
|
|
87
|
+
get props() {
|
|
88
|
+
return { ...propsRef.current, name: normalizedName };
|
|
89
|
+
},
|
|
80
90
|
onStoreChange: forceUpdate,
|
|
81
91
|
});
|
|
82
92
|
return unregister;
|
|
83
93
|
}, [normalizedName, registerFieldEntities]);
|
|
84
94
|
|
|
95
|
+
// JSON signature (not reference) because rules is typically an inline array
|
|
96
|
+
// literal; validator functions are dropped, but the getter above already
|
|
97
|
+
// makes the next natural validation use them.
|
|
98
|
+
const validateSignature = useMemo(
|
|
99
|
+
() => JSON.stringify({ required, rules }),
|
|
100
|
+
[required, rules],
|
|
101
|
+
);
|
|
102
|
+
const prevValidateSignatureRef = useRef(validateSignature);
|
|
103
|
+
|
|
104
|
+
useLayoutEffect(() => {
|
|
105
|
+
const prev = prevValidateSignatureRef.current;
|
|
106
|
+
prevValidateSignatureRef.current = validateSignature;
|
|
107
|
+
if (prev === validateSignature) return;
|
|
108
|
+
// Only re-check fields already showing a validation message — pristine
|
|
109
|
+
// fields must not start erroring just because rules changed.
|
|
110
|
+
if (getFieldValidate?.(normalizedName)) {
|
|
111
|
+
void validateField?.(normalizedName);
|
|
112
|
+
}
|
|
113
|
+
}, [validateSignature, normalizedName]);
|
|
114
|
+
|
|
85
115
|
const isRequired = () => {
|
|
86
116
|
return (
|
|
87
117
|
(Array.isArray(rules) && rules.find((rule) => rule.required)) || required
|
package/src/Form/Group.tsx
CHANGED
|
@@ -107,11 +107,16 @@ const Group: React.FC<FieldGroupProps> = (props) => {
|
|
|
107
107
|
return () => unsubscribes.forEach((fn) => fn());
|
|
108
108
|
}, [childNames, formInstance]);
|
|
109
109
|
|
|
110
|
-
// Register group validator to FormStore
|
|
110
|
+
// Register group validator to FormStore. The closure is registered once, so
|
|
111
|
+
// it reads the latest validate (and its required/rules/childNames captures)
|
|
112
|
+
// through a ref — otherwise runtime prop changes never take effect.
|
|
113
|
+
const validateRef = useRef(validate);
|
|
114
|
+
validateRef.current = validate;
|
|
115
|
+
|
|
111
116
|
useEffect(() => {
|
|
112
117
|
const unregister = formInstance.registerGroupEntity(groupId, {
|
|
113
118
|
validate: async (force) => {
|
|
114
|
-
const result = await
|
|
119
|
+
const result = await validateRef.current(force);
|
|
115
120
|
setErrors(result);
|
|
116
121
|
forceUpdate();
|
|
117
122
|
return result;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { cleanup, render } from '@testing-library/react';
|
|
4
|
+
import { act } from 'react';
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
// act 需要(仓库无全局 vitest setup,逐文件声明)
|
|
8
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
import Field from './Field';
|
|
10
|
+
import FieldContext from './FieldContext';
|
|
11
|
+
import List from './List';
|
|
12
|
+
import { FormStore } from './form-store';
|
|
13
|
+
|
|
14
|
+
function renderListWithStore() {
|
|
15
|
+
const store = new FormStore().getForm();
|
|
16
|
+
const { getByTestId } = render(
|
|
17
|
+
<FieldContext.Provider value={store}>
|
|
18
|
+
<List name="options">
|
|
19
|
+
{(fields) => (
|
|
20
|
+
<div data-testid="rows">
|
|
21
|
+
{fields.map((field) => (
|
|
22
|
+
<Field key={field.key} name={[field.name, 'label']}>
|
|
23
|
+
<input />
|
|
24
|
+
</Field>
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
)}
|
|
28
|
+
</List>
|
|
29
|
+
</FieldContext.Provider>,
|
|
30
|
+
);
|
|
31
|
+
return { getByTestId, store };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
cleanup();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// List 只在自身 render 时读 store:外部 form.setFieldsValue 整体替换 List 值时
|
|
39
|
+
// 若不订阅自身 path,行数不刷新、新行永不挂载(app 侧 issue #99 首开不显示的根因)
|
|
40
|
+
describe('Form.List 外部 setFieldsValue 驱动重渲染', () => {
|
|
41
|
+
it('整体替换 List 值后行数更新', () => {
|
|
42
|
+
const { getByTestId, store } = renderListWithStore();
|
|
43
|
+
expect(getByTestId('rows').children.length).toBe(0);
|
|
44
|
+
|
|
45
|
+
act(() => {
|
|
46
|
+
store.setFieldsValue({ options: [{ label: 'a' }, { label: 'b' }, { label: 'c' }] });
|
|
47
|
+
});
|
|
48
|
+
expect(getByTestId('rows').children.length).toBe(3);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('缩减 List 值后行数同步减少', () => {
|
|
52
|
+
const { getByTestId, store } = renderListWithStore();
|
|
53
|
+
act(() => {
|
|
54
|
+
store.setFieldsValue({ options: [{ label: 'a' }, { label: 'b' }] });
|
|
55
|
+
});
|
|
56
|
+
expect(getByTestId('rows').children.length).toBe(2);
|
|
57
|
+
|
|
58
|
+
act(() => {
|
|
59
|
+
store.setFieldsValue({ options: [{ label: 'a' }] });
|
|
60
|
+
});
|
|
61
|
+
expect(getByTestId('rows').children.length).toBe(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('resetFields 后行数清零', () => {
|
|
65
|
+
const { getByTestId, store } = renderListWithStore();
|
|
66
|
+
act(() => {
|
|
67
|
+
store.setFieldsValue({ options: [{ label: 'a' }, { label: 'b' }] });
|
|
68
|
+
});
|
|
69
|
+
expect(getByTestId('rows').children.length).toBe(2);
|
|
70
|
+
|
|
71
|
+
act(() => {
|
|
72
|
+
store.resetFields();
|
|
73
|
+
});
|
|
74
|
+
expect(getByTestId('rows').children.length).toBe(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('行内编辑不重渲染 List(长度未变时跳过)', () => {
|
|
78
|
+
let renders = 0;
|
|
79
|
+
const store = new FormStore().getForm();
|
|
80
|
+
render(
|
|
81
|
+
<FieldContext.Provider value={store}>
|
|
82
|
+
<List name="options">
|
|
83
|
+
{() => {
|
|
84
|
+
renders += 1;
|
|
85
|
+
return null;
|
|
86
|
+
}}
|
|
87
|
+
</List>
|
|
88
|
+
</FieldContext.Provider>,
|
|
89
|
+
);
|
|
90
|
+
act(() => {
|
|
91
|
+
store.setFieldsValue({ options: [{ label: 'a' }, { label: 'b' }] });
|
|
92
|
+
});
|
|
93
|
+
const afterInit = renders;
|
|
94
|
+
|
|
95
|
+
act(() => {
|
|
96
|
+
store.setFieldsValue({ 'options.0.label': 'z' });
|
|
97
|
+
});
|
|
98
|
+
expect(renders).toBe(afterInit);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('订阅按 path 隔离:改无关字段不触发渲染,改本 List path 触发', () => {
|
|
102
|
+
let renders = 0;
|
|
103
|
+
const store = new FormStore().getForm();
|
|
104
|
+
render(
|
|
105
|
+
<FieldContext.Provider value={store}>
|
|
106
|
+
<List name="options">
|
|
107
|
+
{() => {
|
|
108
|
+
renders += 1;
|
|
109
|
+
return null;
|
|
110
|
+
}}
|
|
111
|
+
</List>
|
|
112
|
+
</FieldContext.Provider>,
|
|
113
|
+
);
|
|
114
|
+
const before = renders;
|
|
115
|
+
|
|
116
|
+
act(() => {
|
|
117
|
+
store.setFieldsValue({ other: 'x' });
|
|
118
|
+
});
|
|
119
|
+
expect(renders).toBe(before);
|
|
120
|
+
|
|
121
|
+
act(() => {
|
|
122
|
+
store.setFieldsValue({ options: [{ label: 'a' }] });
|
|
123
|
+
});
|
|
124
|
+
expect(renders).toBeGreaterThan(before);
|
|
125
|
+
});
|
|
126
|
+
});
|
package/src/Form/List.tsx
CHANGED
|
@@ -28,7 +28,7 @@ const List: React.FC<FormListProps> = ({
|
|
|
28
28
|
children,
|
|
29
29
|
initialValue = [],
|
|
30
30
|
}) => {
|
|
31
|
-
const { getFieldValue, setFieldsValue, validateFields } =
|
|
31
|
+
const { getFieldValue, setFieldsValue, validateFields, subscribe } =
|
|
32
32
|
useContext(FieldContext);
|
|
33
33
|
|
|
34
34
|
// Normalize list name path
|
|
@@ -49,6 +49,9 @@ const List: React.FC<FormListProps> = ({
|
|
|
49
49
|
// State for triggering re-renders
|
|
50
50
|
const [, forceUpdate] = useState(0);
|
|
51
51
|
|
|
52
|
+
// 上一次渲染的行数:订阅回调里跳过行内编辑(长度未变)触发的无效重渲染
|
|
53
|
+
const listLengthRef = useRef(0);
|
|
54
|
+
|
|
52
55
|
// Initialize list value - Fix: use prefixName instead of listNamePath
|
|
53
56
|
useLayoutEffect(() => {
|
|
54
57
|
const pathStr = prefixName.join('.');
|
|
@@ -59,6 +62,24 @@ const List: React.FC<FormListProps> = ({
|
|
|
59
62
|
}
|
|
60
63
|
}, [prefixName, initialValue, getFieldValue, setFieldsValue]);
|
|
61
64
|
|
|
65
|
+
// Subscribe to the list's own path: List re-reads the store only on its own render,
|
|
66
|
+
// so a whole-value replacement via form.setFieldsValue (row count changed) never
|
|
67
|
+
// re-rendered it and new rows never mounted. Field instances already refresh through
|
|
68
|
+
// registerFieldEntities, but the row count lives here. Aligned with rc-field-form.
|
|
69
|
+
useLayoutEffect(
|
|
70
|
+
() =>
|
|
71
|
+
subscribe?.(prefixName.join('.'), () => {
|
|
72
|
+
// isRelatedNamePath 双向前缀匹配,行内编辑(options.i.label)也命中本 watcher;
|
|
73
|
+
// 但 List 渲染输出只取决于行数,行内 Field 会经 registerFieldEntities 自行刷新,
|
|
74
|
+
// 长度未变则跳过,避免大列表逐键全列表重渲染
|
|
75
|
+
const next = getFieldValue?.(prefixName.join('.'));
|
|
76
|
+
const nextLen = Array.isArray(next) ? next.length : 0;
|
|
77
|
+
if (nextLen === listLengthRef.current) return;
|
|
78
|
+
forceUpdate((prev) => prev + 1);
|
|
79
|
+
}),
|
|
80
|
+
[prefixName, subscribe, getFieldValue],
|
|
81
|
+
);
|
|
82
|
+
|
|
62
83
|
// Get current list value
|
|
63
84
|
const getListValue = (): any[] => {
|
|
64
85
|
const pathStr = prefixName.join('.');
|
|
@@ -120,7 +141,6 @@ const List: React.FC<FormListProps> = ({
|
|
|
120
141
|
setFieldsValue?.({ [pathStr]: newList }, 'update' as any);
|
|
121
142
|
}
|
|
122
143
|
keyManager.id += 1;
|
|
123
|
-
forceUpdate((prev) => prev + 1);
|
|
124
144
|
};
|
|
125
145
|
|
|
126
146
|
const remove = (index: number | number[]) => {
|
|
@@ -137,7 +157,6 @@ const List: React.FC<FormListProps> = ({
|
|
|
137
157
|
// Update list values
|
|
138
158
|
const newList = currentValue.filter((_, i) => !indexSet.has(i));
|
|
139
159
|
setFieldsValue?.({ [pathStr]: newList }, 'update' as any);
|
|
140
|
-
forceUpdate((prev) => prev + 1);
|
|
141
160
|
};
|
|
142
161
|
|
|
143
162
|
const moveFunc = (from: number, to: number) => {
|
|
@@ -160,6 +179,7 @@ const List: React.FC<FormListProps> = ({
|
|
|
160
179
|
// Move list values
|
|
161
180
|
const newList = move(currentValue, from, to);
|
|
162
181
|
setFieldsValue?.({ [pathStr]: newList }, 'update' as any);
|
|
182
|
+
// 同长度重排不会触发上方订阅回调(长度未变被跳过),须显式重渲染
|
|
163
183
|
forceUpdate((prev) => prev + 1);
|
|
164
184
|
};
|
|
165
185
|
|
|
@@ -168,6 +188,7 @@ const List: React.FC<FormListProps> = ({
|
|
|
168
188
|
|
|
169
189
|
// Build field list - removed useMemo, compute directly
|
|
170
190
|
const listValue = getListValue();
|
|
191
|
+
listLengthRef.current = listValue.length;
|
|
171
192
|
|
|
172
193
|
// Ensure enough keys
|
|
173
194
|
while (keyManager.keys.length < listValue.length) {
|
package/src/style/color.css
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
--yee-color-text: #39364e;
|
|
30
30
|
--yee-color-text-secondary: #696969;
|
|
31
31
|
--yee-color-text-description: #7c7c7c;
|
|
32
|
-
--yee-color-text-disabled: #
|
|
32
|
+
--yee-color-text-disabled: #b8b8b8;
|
|
33
33
|
--yee-color-text-highlight: #f58220;
|
|
34
34
|
--yee-color-text-auxiliary: #d1d1d1;
|
|
35
35
|
--yee-color-split: #e7e7e7;
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
--yee-color-border-hover: #75b9f0;
|
|
38
38
|
--yee-color-border-focus: #75b9f0;
|
|
39
39
|
--yee-color-bg-highlight: rgba(245, 130, 32, 16%);
|
|
40
|
-
--yee-color-bg-disabled: #
|
|
40
|
+
--yee-color-bg-disabled: #f5f5f5;
|
|
41
41
|
--yee-color-bg-head: #f8f8f8;
|
|
42
42
|
--yee-color-bg-rail: #f5f5f5;
|
|
43
43
|
--yee-color-bg-fill: #ededed;
|
|
@@ -493,7 +493,7 @@
|
|
|
493
493
|
--yee-checkbox-checked-after-border-color: #fff;
|
|
494
494
|
--yee-checkbox-disabled-unchecked-bg-color: var(--yee-color-bg-disabled);
|
|
495
495
|
--yee-checkbox-disabled-checked-bg-color: var(--yee-color-bg-disabled);
|
|
496
|
-
--yee-checkbox-disabled-checked-border-color: var(--yee-color-
|
|
496
|
+
--yee-checkbox-disabled-checked-border-color: var(--yee-color-text-disabled);
|
|
497
497
|
--yee-checkbox-disabled-checked-after-border-color: var(
|
|
498
498
|
--yee-color-text-disabled
|
|
499
499
|
);
|
|
@@ -1223,7 +1223,7 @@
|
|
|
1223
1223
|
--yee-border-color-hover-focus: var(--yee-color-border-focus);
|
|
1224
1224
|
--yee-placeholder-color: var(--yee-color-placeholder);
|
|
1225
1225
|
|
|
1226
|
-
|
|
1226
|
+
/* deprecated:2026-09-11 hover/focus 拆分为 --yee-color-border-hover / --yee-color-border-focus */
|
|
1227
1227
|
--yee-color-border-hover-focus: var(--yee-color-border-focus);
|
|
1228
1228
|
/* deprecated:2026-09-11 更名 --yee-input-focus-border-color */
|
|
1229
1229
|
--yee-input-content-focus-border-color: var(--yee-input-focus-border-color);
|
|
@@ -1339,5 +1339,5 @@
|
|
|
1339
1339
|
--yee-timepicker-bg-color: var(--yee-time-picker-bg-color);
|
|
1340
1340
|
--yee-treeselect-dropdown-bg-color: var(--yee-tree-select-dropdown-bg-color);
|
|
1341
1341
|
|
|
1342
|
-
|
|
1342
|
+
/* deprecated-aliases:end */
|
|
1343
1343
|
}
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
/* ================= 背景 ================= */
|
|
80
80
|
--yee-color-bg-layout: #f1f2f4;
|
|
81
81
|
--yee-color-bg-head: #fff;
|
|
82
|
-
--yee-color-bg-disabled: #
|
|
82
|
+
--yee-color-bg-disabled: #f5f5f5;
|
|
83
83
|
/* 衍生:规范 --hover(极浅悬浮底) */
|
|
84
84
|
--yee-color-bg-hover: rgba(109, 93, 252, 3.5%);
|
|
85
85
|
|