@rainbow-oh/yee-c 0.8.19 → 0.9.0

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.
Files changed (101) hide show
  1. package/dist/cjs/Anchor/anchor.js +4 -1
  2. package/dist/cjs/Checkbox/checkbox.js +2 -1
  3. package/dist/cjs/Checkbox/style/index.less +3 -0
  4. package/dist/cjs/Form/Field.js +6 -6
  5. package/dist/cjs/Form/demo/field-status.d.ts +3 -0
  6. package/dist/cjs/Form/demo/field-status.js +116 -0
  7. package/dist/cjs/Form/form-store.d.ts +6 -1
  8. package/dist/cjs/Form/form-store.js +74 -2
  9. package/dist/cjs/Form/index.d.ts +1 -1
  10. package/dist/cjs/Form/interface.d.ts +17 -1
  11. package/dist/cjs/Form/style/index.less +26 -0
  12. package/dist/cjs/Progress/circle.js +46 -35
  13. package/dist/cjs/Progress/line.js +20 -11
  14. package/dist/cjs/Search/demo/basic.js +38 -3
  15. package/dist/cjs/Search/interface.d.ts +2 -2
  16. package/dist/cjs/Search/search.js +52 -32
  17. package/dist/cjs/Search/style/index.less +20 -3
  18. package/dist/cjs/Table/style/index.less +4 -2
  19. package/dist/cjs/Table/tbody/cell.js +2 -2
  20. package/dist/cjs/Transfer/interface.d.ts +0 -1
  21. package/dist/cjs/Transfer/style/index.less +4 -0
  22. package/dist/cjs/Transfer/transfer-list.js +1 -0
  23. package/dist/cjs/message/use-message.js +1 -3
  24. package/dist/cjs/notice/notice.js +1 -1
  25. package/dist/cjs/style/themes.css +1 -1
  26. package/dist/cjs/style/variables.css +1 -1
  27. package/dist/cjs/version.d.ts +1 -1
  28. package/dist/cjs/version.js +1 -1
  29. package/dist/esm/Checkbox/checkbox.js +2 -1
  30. package/dist/esm/Checkbox/style/index.less +3 -0
  31. package/dist/esm/Form/Field.js +6 -6
  32. package/dist/esm/Form/demo/field-status.d.ts +3 -0
  33. package/dist/esm/Form/demo/field-status.js +104 -0
  34. package/dist/esm/Form/form-store.d.ts +6 -1
  35. package/dist/esm/Form/form-store.js +88 -6
  36. package/dist/esm/Form/index.d.ts +1 -1
  37. package/dist/esm/Form/interface.d.ts +17 -1
  38. package/dist/esm/Form/style/index.less +26 -0
  39. package/dist/esm/Search/demo/basic.js +61 -7
  40. package/dist/esm/Search/interface.d.ts +2 -2
  41. package/dist/esm/Search/search.js +22 -15
  42. package/dist/esm/Search/style/index.less +20 -3
  43. package/dist/esm/Table/style/index.less +4 -2
  44. package/dist/esm/Table/tbody/cell.js +2 -2
  45. package/dist/esm/Transfer/interface.d.ts +0 -1
  46. package/dist/esm/Transfer/style/index.less +4 -0
  47. package/dist/esm/Transfer/transfer-list.js +1 -0
  48. package/dist/esm/notice/notice.js +1 -1
  49. package/dist/esm/style/themes.css +1 -1
  50. package/dist/esm/style/variables.css +1 -1
  51. package/dist/esm/version.d.ts +1 -1
  52. package/dist/esm/version.js +1 -1
  53. package/dist/umd/yee-c.min.css +1 -1
  54. package/dist/umd/yee-c.min.js +1 -1
  55. package/package.json +1 -1
  56. package/src/Anchor/anchor.tsx +9 -5
  57. package/src/Checkbox/checkbox.tsx +1 -0
  58. package/src/Checkbox/style/index.less +3 -0
  59. package/src/Form/Field.tsx +7 -8
  60. package/src/Form/demo/field-status.tsx +108 -0
  61. package/src/Form/form-store.ts +92 -2
  62. package/src/Form/index.en-US.md +6 -1
  63. package/src/Form/index.ts +2 -0
  64. package/src/Form/index.zh-CN.md +6 -1
  65. package/src/Form/interface.ts +19 -1
  66. package/src/Form/style/index.less +26 -0
  67. package/src/Progress/circle.tsx +5 -1
  68. package/src/Progress/line.tsx +5 -1
  69. package/src/Progress/progress.tsx +5 -1
  70. package/src/Search/demo/basic.tsx +43 -7
  71. package/src/Search/index.en-US.md +1 -1
  72. package/src/Search/index.zh-CN.md +1 -1
  73. package/src/Search/interface.ts +2 -2
  74. package/src/Search/search.tsx +40 -23
  75. package/src/Search/style/index.less +20 -3
  76. package/src/Spin/indicator.tsx +1 -1
  77. package/src/Table/style/index.less +4 -2
  78. package/src/Table/tbody/cell.tsx +2 -2
  79. package/src/Transfer/interface.ts +0 -1
  80. package/src/Transfer/style/index.less +4 -0
  81. package/src/Transfer/transfer-list.tsx +1 -0
  82. package/src/Tree/interface.ts +6 -1
  83. package/src/Tree/tree-node.tsx +7 -6
  84. package/src/Tree/utils/tree-tools.ts +4 -1
  85. package/src/Trigger/interface.ts +2 -1
  86. package/src/message/demo/base.tsx +1 -1
  87. package/src/message/interface.ts +1 -5
  88. package/src/message/message.test.tsx +15 -5
  89. package/src/message/message.tsx +10 -4
  90. package/src/message/use-message.tsx +2 -8
  91. package/src/message/wrapper.tsx +1 -2
  92. package/src/notice/interface.ts +1 -5
  93. package/src/notice/notice.test.tsx +14 -6
  94. package/src/notice/notice.tsx +12 -6
  95. package/src/notice/wrapper.tsx +1 -2
  96. package/src/version.ts +1 -1
  97. package/dist/cjs/Search/style/index.d.ts +0 -1
  98. package/dist/cjs/Search/style/index.js +0 -2
  99. package/dist/esm/Search/style/index.d.ts +0 -1
  100. package/dist/esm/Search/style/index.js +0 -1
  101. package/src/Search/style/index.ts +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainbow-oh/yee-c",
3
- "version": "0.8.19",
3
+ "version": "0.9.0",
4
4
  "description": "yee basic components",
5
5
  "keywords": [
6
6
  "react",
@@ -78,9 +78,9 @@ const Anchor = forwardRef<HTMLDivElement, AnchorProps>((baseprops, ref) => {
78
78
  const navigationTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
79
79
  null,
80
80
  );
81
- const navigationSettleTimeoutRef = useRef<
82
- ReturnType<typeof setTimeout> | null
83
- >(null);
81
+ const navigationSettleTimeoutRef = useRef<ReturnType<
82
+ typeof setTimeout
83
+ > | null>(null);
84
84
 
85
85
  useEffect(() => {
86
86
  activeKeyRef.current = mergedActiveKey;
@@ -361,7 +361,10 @@ const Anchor = forwardRef<HTMLDivElement, AnchorProps>((baseprops, ref) => {
361
361
  'pointerdown',
362
362
  onUserInterrupt as EventListener,
363
363
  );
364
- container.removeEventListener('keydown', onUserInterrupt as EventListener);
364
+ container.removeEventListener(
365
+ 'keydown',
366
+ onUserInterrupt as EventListener,
367
+ );
365
368
  if (rafId !== null) {
366
369
  cancelAnimationFrame(rafId);
367
370
  }
@@ -419,7 +422,8 @@ const Anchor = forwardRef<HTMLDivElement, AnchorProps>((baseprops, ref) => {
419
422
 
420
423
  // Handles instant scrolling and targets that are already in position.
421
424
  handleScroll();
422
- }, [
425
+ },
426
+ [
423
427
  startNavigationLock,
424
428
  commitActiveKey,
425
429
  scrollToAnchor,
@@ -81,6 +81,7 @@ const Checkbox = (baseprops: CheckboxProps) => {
81
81
  <span
82
82
  className={clsx(`${prefixCls}-label`, classNames?.label)}
83
83
  style={styles?.label}
84
+ title={typeof children === 'string' ? children : undefined}
84
85
  >
85
86
  {children}
86
87
  </span>
@@ -93,6 +93,9 @@
93
93
  &-label {
94
94
  padding: var(--yee-checkbox-label-padding);
95
95
  user-select: none;
96
+ white-space: nowrap;
97
+ text-overflow: ellipsis;
98
+ overflow: hidden;
96
99
  }
97
100
 
98
101
  &-indeterminate {
@@ -44,7 +44,7 @@ const Field: React.FC<FieldProps> = (props) => {
44
44
  } = props;
45
45
  const {
46
46
  getCallbacks,
47
- getFieldValidate,
47
+ getFieldStatus,
48
48
  getFieldValue,
49
49
  getFieldsValue,
50
50
  setFieldsValue,
@@ -88,7 +88,7 @@ const Field: React.FC<FieldProps> = (props) => {
88
88
  );
89
89
  };
90
90
 
91
- const validate = getFieldValidate?.(normalizedName);
91
+ const fieldStatus = getFieldStatus?.(normalizedName);
92
92
 
93
93
  // Normalize tooltip into { icon, ...tooltipProps }, or null when not provided.
94
94
  // Everything except `icon` is forwarded to <Tooltip>, so any Tooltip prop
@@ -152,8 +152,7 @@ const Field: React.FC<FieldProps> = (props) => {
152
152
  `${prefixCls}-item`,
153
153
  `${prefixCls}-item-${layout}`,
154
154
  {
155
- [`${prefixCls}-item-${validate?.status}`]:
156
- validate && validate.status,
155
+ [`${prefixCls}-item-${fieldStatus?.status}`]: fieldStatus?.status,
157
156
  },
158
157
  className,
159
158
  )}
@@ -200,18 +199,18 @@ const Field: React.FC<FieldProps> = (props) => {
200
199
  </div>
201
200
  </div>
202
201
  <AnimatePresence>
203
- {validate && (
202
+ {fieldStatus?.message && (
204
203
  <motion.small
205
204
  initial={{ opacity: 0, y: -10 }}
206
205
  animate={{ opacity: 1, y: 0 }}
207
206
  exit={{ opacity: 0, y: -10 }}
208
207
  transition={{ duration: 0.16 }}
209
208
  className={clsx(`${prefixCls}-item-additional`, {
210
- [`${prefixCls}-item-additional-${validate.status}`]:
211
- validate.status,
209
+ [`${prefixCls}-item-additional-${fieldStatus.status}`]:
210
+ fieldStatus.status,
212
211
  })}
213
212
  >
214
- {validate.message}
213
+ {fieldStatus.message}
215
214
  </motion.small>
216
215
  )}
217
216
  </AnimatePresence>
@@ -0,0 +1,108 @@
1
+ import {
2
+ Button,
3
+ Form,
4
+ Input,
5
+ Space,
6
+ type FieldStatusData,
7
+ } from '@rainbow-oh/yee-c';
8
+ import React, { useState } from 'react';
9
+
10
+ export default () => {
11
+ const [form] = Form.useForm();
12
+ const [currentStatus, setCurrentStatus] = useState<FieldStatusData | null>(
13
+ null,
14
+ );
15
+ const [submitResult, setSubmitResult] = useState('');
16
+
17
+ const applyStatus = (status: FieldStatusData | null) => {
18
+ form.setFieldStatus('username', status);
19
+ setCurrentStatus(form.getFieldStatus('username'));
20
+ setSubmitResult('');
21
+ };
22
+
23
+ return (
24
+ <Form
25
+ form={form}
26
+ initialValues={{ username: 'demo-user' }}
27
+ onFinish={() => setSubmitResult('Submit succeeded')}
28
+ onFinishFailed={() =>
29
+ setSubmitResult('Submit blocked by the blocking error')
30
+ }
31
+ >
32
+ <Form.Field
33
+ name="username"
34
+ label="Username"
35
+ rules={[{ required: true, message: 'Please input your username!' }]}
36
+ >
37
+ <Input placeholder="Please input your username" />
38
+ </Form.Field>
39
+
40
+ <Space style={{ marginTop: 28, flexWrap: 'wrap' }}>
41
+ <Button
42
+ onClick={() =>
43
+ applyStatus({
44
+ status: 'error',
45
+ message: 'This username is unavailable',
46
+ })
47
+ }
48
+ >
49
+ Error
50
+ </Button>
51
+ <Button
52
+ onClick={() =>
53
+ applyStatus({
54
+ status: 'error',
55
+ message: 'This error prevents submission',
56
+ blocking: true,
57
+ })
58
+ }
59
+ >
60
+ Blocking error
61
+ </Button>
62
+ <Button
63
+ onClick={() =>
64
+ applyStatus({
65
+ status: 'warning',
66
+ message: 'This username is commonly used',
67
+ })
68
+ }
69
+ >
70
+ Warning
71
+ </Button>
72
+ <Button
73
+ onClick={() =>
74
+ applyStatus({
75
+ status: 'success',
76
+ message: 'This username is available',
77
+ })
78
+ }
79
+ >
80
+ Success
81
+ </Button>
82
+ <Button
83
+ onClick={() =>
84
+ applyStatus({
85
+ status: 'info',
86
+ message: 'The username can be changed later',
87
+ })
88
+ }
89
+ >
90
+ Info
91
+ </Button>
92
+ <Button onClick={() => applyStatus(null)}>Clear</Button>
93
+ <Button type="primary" htmlType="submit">
94
+ Submit
95
+ </Button>
96
+ </Space>
97
+
98
+ <pre style={{ marginTop: 16 }}>
99
+ Current status: {JSON.stringify(currentStatus, null, 2)}
100
+ </pre>
101
+ {submitResult ? <div>{submitResult}</div> : null}
102
+ <div style={{ marginTop: 8, color: '#999' }}>
103
+ Tip: set Success, then clear the input — the rule error takes display
104
+ precedence over the manually set status.
105
+ </div>
106
+ </Form>
107
+ );
108
+ };
@@ -9,6 +9,7 @@ import validate from './utils/validate';
9
9
  import type {
10
10
  Callbacks,
11
11
  FieldEntity,
12
+ FieldStatusData,
12
13
  FormInstance,
13
14
  FormListField,
14
15
  FormListOperation,
@@ -43,6 +44,7 @@ export class FormStore {
43
44
  private fieldEntities: FieldEntity[] = [];
44
45
  private fieldMap: Map<Name, FieldEntity> = new Map();
45
46
  private validateMessages: Map<Name, ValidateMessage[]> = new Map();
47
+ private fieldStatuses: Map<Name, FieldStatusData> = new Map();
46
48
  private initialized = false;
47
49
  private initialValues: Store = {};
48
50
  // List key manager
@@ -114,6 +116,76 @@ export class FormStore {
114
116
  return null;
115
117
  };
116
118
 
119
+ getFieldStatus = (name: NamePath): FieldStatusData | null => {
120
+ const normalizedName = stringifyPath(getNamePath(name));
121
+ const manualStatus = this.fieldStatuses.get(normalizedName);
122
+ const validateMessage = this.validateMessages.get(normalizedName)?.[0];
123
+
124
+ // Rule errors reflect the current value and auto-clear once validation
125
+ // passes, so they always take display precedence — a stale manually set
126
+ // status must never hide why submission failed.
127
+ if (validateMessage?.status === 'error') {
128
+ return {
129
+ status: 'error',
130
+ message: validateMessage.message,
131
+ };
132
+ }
133
+
134
+ if (manualStatus) {
135
+ return { ...manualStatus };
136
+ }
137
+
138
+ if (validateMessage) {
139
+ return {
140
+ status: validateMessage.status,
141
+ message: validateMessage.message,
142
+ };
143
+ }
144
+
145
+ return null;
146
+ };
147
+
148
+ setFieldStatus = (name: NamePath, status: FieldStatusData | null) => {
149
+ const normalizedName = stringifyPath(getNamePath(name));
150
+ if (!normalizedName) return;
151
+
152
+ if (status) {
153
+ this.fieldStatuses.set(normalizedName, { ...status });
154
+ } else {
155
+ this.fieldStatuses.delete(normalizedName);
156
+ }
157
+
158
+ this.fieldMap.get(normalizedName)?.onStoreChange();
159
+ };
160
+
161
+ // Delete entries whose key equals or nests under one of `names`
162
+ // (no names = delete all). Keeps rule messages and manual statuses in
163
+ // sync with the fields that reset/clear actually touched.
164
+ private deleteRelatedEntries = <T>(
165
+ entries: Map<Name, T>,
166
+ names?: NamePath[],
167
+ ): void => {
168
+ if (!names) {
169
+ entries.clear();
170
+ return;
171
+ }
172
+
173
+ const normalizedNames = names.map((name) =>
174
+ stringifyPath(getNamePath(name)),
175
+ );
176
+ Array.from(entries.keys()).forEach((fieldName) => {
177
+ if (
178
+ normalizedNames.some((name) => isRelatedNamePath(fieldName, name))
179
+ ) {
180
+ entries.delete(fieldName);
181
+ }
182
+ });
183
+ };
184
+
185
+ private clearFieldStatuses = (names?: NamePath[]) => {
186
+ this.deleteRelatedEntries(this.fieldStatuses, names);
187
+ };
188
+
117
189
  setFieldsValue = (newStore: Store, trigger: TRIGGER = 'onChange') => {
118
190
  if (trigger === 'reset' || trigger === 'clear') {
119
191
  this.store = newStore;
@@ -196,7 +268,10 @@ export class FormStore {
196
268
  });
197
269
  }
198
270
  this.callbacks?.onReset?.();
199
- this.validateMessages.clear();
271
+ // A named reset only clears rule messages of the fields it touched;
272
+ // unrelated fields keep their (still valid) validation state.
273
+ this.deleteRelatedEntries(this.validateMessages, name);
274
+ this.clearFieldStatuses(name);
200
275
  this.groupEntities.forEach((entity) => entity.reset());
201
276
  this.refreshField();
202
277
  };
@@ -208,6 +283,7 @@ export class FormStore {
208
283
  acc[name] = undefined;
209
284
  return acc;
210
285
  }, {} as Store);
286
+ this.clearFieldStatuses(name);
211
287
  this.setFieldsValue(clearStore, 'clear');
212
288
  };
213
289
 
@@ -567,7 +643,19 @@ export class FormStore {
567
643
  const { onFinish, onFinishFailed } = this.callbacks;
568
644
  const fieldErrors = await this.validateFields();
569
645
  const groupErrors = await this.validateGroups(true);
570
- const allErrors = [...fieldErrors, ...groupErrors];
646
+ const statusErrors = Array.from(this.fieldStatuses.entries())
647
+ .filter(
648
+ ([, status]) => status.status === 'error' && status.blocking === true,
649
+ )
650
+ .map(
651
+ ([name, status]): ValidateMessage => ({
652
+ name,
653
+ message: status.message ?? '',
654
+ value: this.getFieldValue(name),
655
+ status: 'error',
656
+ }),
657
+ );
658
+ const allErrors = [...fieldErrors, ...groupErrors, ...statusErrors];
571
659
  if (allErrors.length === 0) {
572
660
  onFinish?.(this.getFieldsValue());
573
661
  } else {
@@ -580,6 +668,8 @@ export class FormStore {
580
668
  getForm(): FormInstance {
581
669
  return {
582
670
  getFieldValidate: this.getFieldValidate,
671
+ getFieldStatus: this.getFieldStatus,
672
+ setFieldStatus: this.setFieldStatus,
583
673
  getFieldsValue: this.getFieldsValue,
584
674
  getFieldValue: this.getFieldValue,
585
675
  setFieldsValue: this.setFieldsValue,
@@ -21,6 +21,7 @@ High performance form component with data scope management.
21
21
  <code src="./demo/layout.tsx" title="Layout" description="Different form layouts"></code>
22
22
  <code src="./demo/validate.tsx" title="Validation" description="Form validation"></code>
23
23
  <code src="./demo/async-validate.tsx" title="Async Validation" description="async validator for server-side checks like uniqueness"></code>
24
+ <code src="./demo/field-status.tsx" title="Dynamic Field Status" description="Set error, warning, success, and info feedback dynamically with setFieldStatus"></code>
24
25
  <code src="./demo/list.tsx" title="Form List" description="Form list field usage"></code>
25
26
  <code src="./demo/form-table.tsx" title="Form Table" description="Form table field usage"></code>
26
27
  <code src="./demo/dynamic.tsx" title="Dynamic Form" description="Dynamic form items"></code>
@@ -52,12 +53,14 @@ High performance form component with data scope management.
52
53
  | Property | Type | Description |
53
54
  | --------------------- | ------------------------------------------------------------------------------------------ | ------------------------- |
54
55
  | getFieldValidate | `(name: Name) => ValidateMessage \| null` | Get field validation (may lag before async validation completes; updates via onStoreChange) |
56
+ | getFieldStatus | `(name: NamePath) => FieldStatusData \| null` | Get the displayed field status; rule validation errors take display precedence, otherwise a manually set status overrides validation |
57
+ | setFieldStatus | `(name: NamePath, status: FieldStatusData \| null) => void` | Set field status and message dynamically, or pass `null` to clear it; only blocking errors prevent submission |
55
58
  | getFieldValue | `(name: Name) => StoreValue` | Get field value |
56
59
  | submit | `() => Promise<ValidateMessage[]>` | Submit form (async, triggers onFinish only after validation completes) |
57
60
  | getFieldsValue | `() => Values` | Get all field values |
58
61
  | setFieldsValue | `(newStore: Store, trigger?: TRIGGER) => void` | Set field values |
59
62
  | setCallbacks | `(callbacks: Callbacks) => void` | Set callbacks |
60
- | resetFields | `(name?: Name[]) => void` | Reset fields |
63
+ | resetFields | `(name?: Name[]) => void` | Reset fields (values and validation state; passing name only affects related fields) |
61
64
  | clearFields | `(name?: Name[]) => void` | Clear fields |
62
65
  | validateField | `(name: Name, trigger?: 'onChange' \| 'onBlur') => Promise<ValidateMessage[]>` | Validate single field |
63
66
  | validateFields | `(names?: Name[], trigger?: TRIGGER) => Promise<ValidateMessage[]>` | Validate multiple fields |
@@ -129,6 +132,8 @@ High performance form component with data scope management.
129
132
  | Store | `Record<string, StoreValue>` |
130
133
  | Name | `string` |
131
134
  | NamePath | `string \| string[] \| number \| (string \| number)[]` |
135
+ | FieldStatus | `'error' \| 'success' \| 'warning' \| 'info'` |
136
+ | FieldStatusData | `{ status: FieldStatus; message?: string; blocking?: boolean }` |
132
137
  | ValidateMessage | `{ name: NamePath \| NamePath[] \| undefined; message: string; value: unknown; status: 'error' \| 'success' \| 'warning' \| 'info' }` |
133
138
  | FieldEntity | `{ props: fieldProps & { name?: NamePath }; onStoreChange: () => void }` |
134
139
  | TRIGGER | `'onChange' \| 'onBlur' \| 'onSubmit' \| 'reset' \| 'clear' \| 'update'` |
package/src/Form/index.ts CHANGED
@@ -29,6 +29,8 @@ Form.List = List;
29
29
 
30
30
  export type {
31
31
  FieldGroupProps,
32
+ FieldStatus,
33
+ FieldStatusData,
32
34
  FormInstance,
33
35
  FormListProps,
34
36
  FormProps,
@@ -23,6 +23,7 @@ toc: 'content'
23
23
  <code src="./demo/layout.tsx" title="表单布局" description="不同的表单布局"></code>
24
24
  <code src="./demo/validate.tsx" title="表单校验" description="表单校验功能"></code>
25
25
  <code src="./demo/async-validate.tsx" title="异步校验" description="async validator 实现服务端唯一性校验等异步场景"></code>
26
+ <code src="./demo/field-status.tsx" title="动态字段状态" description="通过 setFieldStatus 动态设置 error、warning、success 和 info 状态及提示"></code>
26
27
 
27
28
  ## API
28
29
 
@@ -51,12 +52,14 @@ toc: 'content'
51
52
  | 属性名 | 类型 | 描述 |
52
53
  | --------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------ |
53
54
  | getFieldValidate | `(name: Name) => ValidateMessage \| null` | 获取字段验证(异步校验未完成时可能读到旧值,最终会随 onStoreChange 更新) |
55
+ | getFieldStatus | `(name: NamePath) => FieldStatusData \| null` | 获取字段当前展示状态;规则校验错误优先展示,其余情况手动设置的状态优先于校验结果 |
56
+ | setFieldStatus | `(name: NamePath, status: FieldStatusData \| null) => void` | 动态设置字段状态和提示,传入 `null` 清除;仅 `blocking: true` 的 error 会阻止提交 |
54
57
  | getFieldValue | `(name: Name) => StoreValue` | 获取字段值 |
55
58
  | submit | `() => Promise<ValidateMessage[]>` | 提交表单(异步,校验完成后才触发 onFinish) |
56
59
  | getFieldsValue | `() => Values` | 获取所有字段值 |
57
60
  | setFieldsValue | `(newStore: Store, trigger?: TRIGGER) => void` | 设置字段值 |
58
61
  | setCallbacks | `(callbacks: Callbacks) => void` | 设置回调 |
59
- | resetFields | `(name?: Name[]) => void` | 重置字段 |
62
+ | resetFields | `(name?: Name[]) => void` | 重置字段(值与校验状态;指定 name 时只影响相关字段) |
60
63
  | clearFields | `(name?: Name[]) => void` | 清空字段 |
61
64
  | validateField | `(name: Name, trigger?: 'onChange' \| 'onBlur') => Promise<ValidateMessage[]>` | 校验单个字段 |
62
65
  | validateFields | `(names?: Name[], trigger?: TRIGGER) => Promise<ValidateMessage[]>` | 校验多个字段 |
@@ -128,6 +131,8 @@ toc: 'content'
128
131
  | Store | `Record<string, StoreValue>` |
129
132
  | Name | `string` |
130
133
  | NamePath | `string \| string[] \| number \| (string \| number)[]` |
134
+ | FieldStatus | `'error' \| 'success' \| 'warning' \| 'info'` |
135
+ | FieldStatusData | `{ status: FieldStatus; message?: string; blocking?: boolean }` |
131
136
  | ValidateMessage | `{ name: NamePath \| NamePath[] \| undefined; message: string; value: unknown; status: 'error' \| 'success' \| 'warning' \| 'info' }` |
132
137
  | FieldEntity | `{ props: fieldProps & { name?: NamePath }; onStoreChange: () => void }` |
133
138
  | TRIGGER | `'onChange' \| 'onBlur' \| 'onSubmit' \| 'reset' \| 'clear' \| 'update'` |
@@ -106,6 +106,15 @@ export interface FormInstance<Values = any> {
106
106
  * Get field validation info
107
107
  */
108
108
  getFieldValidate: (name: Name) => ValidateMessage | null;
109
+ /**
110
+ * Get the displayed field status. Rule validation errors take display
111
+ * precedence; otherwise a manually set status overrides validation.
112
+ */
113
+ getFieldStatus: (name: NamePath) => FieldStatusData | null;
114
+ /**
115
+ * Set or clear a field's displayed status. Pass null to clear it.
116
+ */
117
+ setFieldStatus: (name: NamePath, status: FieldStatusData | null) => void;
109
118
  /**
110
119
  * Get field value
111
120
  */
@@ -253,11 +262,20 @@ export type Rule = {
253
262
  | Array<'onBlur' | 'onChange' | 'onSubmit'>;
254
263
  };
255
264
 
265
+ export type FieldStatus = 'error' | 'success' | 'warning' | 'info';
266
+
267
+ export type FieldStatusData = {
268
+ status: FieldStatus;
269
+ message?: string;
270
+ /** Whether an error status should prevent form submission. */
271
+ blocking?: boolean;
272
+ };
273
+
256
274
  export type ValidateMessage = {
257
275
  name: NamePath | NamePath[] | undefined;
258
276
  message: string;
259
277
  value: unknown;
260
- status: 'error' | 'success' | 'warning' | 'info';
278
+ status: FieldStatus;
261
279
  };
262
280
 
263
281
  export type FieldConfigurableTooltip = Omit<TooltipProps, 'children'> & {
@@ -79,6 +79,18 @@
79
79
  &-error {
80
80
  color: var(--yee-error-color);
81
81
  }
82
+
83
+ &-warning {
84
+ color: var(--yee-warning-color);
85
+ }
86
+
87
+ &-success {
88
+ color: var(--yee-success-color);
89
+ }
90
+
91
+ &-info {
92
+ color: var(--yee-info-color);
93
+ }
82
94
  }
83
95
 
84
96
  // 表单校验态:在此重定义控件读取的 border / box-shadow 变量,
@@ -97,5 +109,19 @@
97
109
  --yee-input-focus-bs-color: var(--yee-warning-color-border);
98
110
  --yee-checkbox-border-color: var(--yee-warning-color);
99
111
  }
112
+
113
+ &&-success {
114
+ --yee-input-border-color: var(--yee-success-color);
115
+ --yee-input-hover-border-color: var(--yee-success-color-hover);
116
+ --yee-input-focus-bs-color: var(--yee-success-color-border);
117
+ --yee-checkbox-border-color: var(--yee-success-color);
118
+ }
119
+
120
+ &&-info {
121
+ --yee-input-border-color: var(--yee-info-color);
122
+ --yee-input-hover-border-color: var(--yee-info-color-hover);
123
+ --yee-input-focus-bs-color: var(--yee-info-color-border);
124
+ --yee-checkbox-border-color: var(--yee-info-color);
125
+ }
100
126
  }
101
127
  }
@@ -111,7 +111,11 @@ const Circle = React.forwardRef<HTMLDivElement, CircleProps>((props, ref) => {
111
111
  style={{ width, height: width }}
112
112
  ref={ref}
113
113
  >
114
- <svg className={`${prefixCls}-circle`} viewBox="0 0 120 120" aria-hidden="true">
114
+ <svg
115
+ className={`${prefixCls}-circle`}
116
+ viewBox="0 0 120 120"
117
+ aria-hidden="true"
118
+ >
115
119
  <circle
116
120
  className={`${prefixCls}-circle-trail`}
117
121
  cx="60"
@@ -36,7 +36,11 @@ const Line = React.forwardRef<HTMLDivElement, LineProps>((props, ref) => {
36
36
 
37
37
  return (
38
38
  <div className={`${prefixCls}-outer`} ref={ref}>
39
- <div className={`${prefixCls}-inner`} style={{ height: strokeWidth }} aria-hidden="true">
39
+ <div
40
+ className={`${prefixCls}-inner`}
41
+ style={{ height: strokeWidth }}
42
+ aria-hidden="true"
43
+ >
40
44
  <div
41
45
  className={`${prefixCls}-bg`}
42
46
  style={{ width: percent + '%', ...progressColor }}
@@ -107,7 +107,11 @@ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
107
107
  aria-valuemin={0}
108
108
  aria-valuemax={100}
109
109
  aria-valuetext={`${percent}%`}
110
- aria-label={status === 'error' ? `Progress: ${percent}%, error` : `Progress: ${percent}%`}
110
+ aria-label={
111
+ status === 'error'
112
+ ? `Progress: ${percent}%, error`
113
+ : `Progress: ${percent}%`
114
+ }
111
115
  ref={ref}
112
116
  {...dataAttrs}
113
117
  >
@@ -1,14 +1,50 @@
1
- import React from 'react';
1
+ import React, { useMemo, useState } from 'react';
2
2
  import Search from '../index';
3
-
3
+ import type { SearchOption } from '../interface';
4
+ const people: SearchOption[] = [
5
+ { key: 'ada', value: 'ada', label: 'Ada Lovelace' },
6
+ { key: 'grace', value: 'grace', label: 'Grace Hopper' },
7
+ { key: 'alan', value: 'alan', label: 'Alan Turing' },
8
+ { key: 'margaret', value: 'margaret', label: 'Margaret Hamilton' },
9
+ ];
4
10
  export default () => {
5
- const onSearch = (value: string) => {
6
- console.log('search value:', value);
11
+ const [keyword, setKeyword] = useState('');
12
+ const [selected, setSelected] = useState<SearchOption | null>(null);
13
+ const results = useMemo(() => {
14
+ const normalizedKeyword = keyword.trim().toLowerCase();
15
+ const matched = people.filter((person) =>
16
+ String(person.label).toLowerCase().includes(normalizedKeyword),
17
+ );
18
+ return matched.length > 0
19
+ ? matched
20
+ : [
21
+ {
22
+ key: 'empty',
23
+ value: 'empty',
24
+ label: 'No results found',
25
+ disabled: true,
26
+ },
27
+ ];
28
+ }, [keyword]);
29
+ const handleSelect = (option: SearchOption | null) => {
30
+ setSelected(option);
31
+ if (option) {
32
+ setKeyword(String(option.label));
33
+ }
7
34
  };
8
-
9
35
  return (
10
- <div style={{ width: 300 }}>
11
- <Search placeholder="input search text" onSearch={onSearch} />
36
+ <div style={{ width: 320 }}>
37
+ <Search
38
+ allowClear
39
+ value={keyword}
40
+ options={results}
41
+ placeholder="Search people"
42
+ onSearch={setKeyword}
43
+ onChange={handleSelect}
44
+ />
45
+ <div style={{ marginTop: 12 }}>
46
+ Selected: {selected ? String(selected.label) : 'None'}
47
+ </div>
12
48
  </div>
13
49
  );
14
50
  };
@@ -14,7 +14,7 @@ A search input component for querying data.
14
14
 
15
15
  ## Code Demo
16
16
 
17
- <code src="./demo/basic.tsx" title="Basic" description="Basic usage of Search"></code>
17
+ <code src="./demo/basic.tsx" title="Basic" description="Filter results as you type and select from the dropdown"></code>
18
18
  <code src="./demo/size.tsx" title="Size" description="Different sizes of Search"></code>
19
19
  <code src="./demo/allowClear.tsx" title="Allow Clear" description="Search with clear icon"></code>
20
20
  <code src="./demo/borderless.tsx" title="Borderless" description="Search without border"></code>
@@ -14,7 +14,7 @@ toc: 'content'
14
14
 
15
15
  ## 代码演示
16
16
 
17
- <code src="./demo/basic.tsx" title="基础用法" description="搜索框的基础用法"></code>
17
+ <code src="./demo/basic.tsx" title="基础用法" description="输入关键词筛选结果,并从下拉列表中选择"></code>
18
18
  <code src="./demo/size.tsx" title="尺寸" description="不同尺寸的搜索框"></code>
19
19
  <code src="./demo/allowClear.tsx" title="可清空" description="带清空图标的搜索框"></code>
20
20
  <code src="./demo/borderless.tsx" title="无边框" description="无边框的搜索框"></code>
@@ -68,7 +68,7 @@ export interface SearchProps
68
68
  /**
69
69
  * @description Suffix content
70
70
  */
71
- suffix?: React.ReactNode;
71
+ suffix?: React.ReactNode | false;
72
72
 
73
73
  /**
74
74
  * @description Whether disabled
@@ -96,7 +96,7 @@ export interface SearchProps
96
96
  * @description trigger search event
97
97
  * @default 'typing'
98
98
  */
99
- searchOnAction?: 'typing' | 'enter';
99
+ searchOnAction?: 'typing' | 'enter' | 'button';
100
100
 
101
101
  /**
102
102
  * @description suggest data