rsuite 6.1.3 → 6.2.1

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 (66) hide show
  1. package/AutoComplete/styles/index.css +3 -0
  2. package/CHANGELOG.md +27 -0
  3. package/Cascader/styles/index.css +3 -0
  4. package/CheckPicker/styles/index.css +3 -0
  5. package/CheckTree/styles/index.css +3 -0
  6. package/CheckTreePicker/styles/index.css +3 -0
  7. package/DatePicker/styles/index.css +3 -0
  8. package/DateRangePicker/styles/index.css +3 -0
  9. package/InputPicker/styles/index.css +3 -0
  10. package/MultiCascadeTree/styles/index.css +3 -0
  11. package/MultiCascader/styles/index.css +3 -0
  12. package/Pagination/styles/index.css +3 -0
  13. package/SelectPicker/styles/index.css +3 -0
  14. package/TagInput/styles/index.css +3 -0
  15. package/TagPicker/styles/index.css +3 -0
  16. package/TimePicker/styles/index.css +3 -0
  17. package/TimeRangePicker/styles/index.css +3 -0
  18. package/Timeline/styles/index.css +11 -0
  19. package/Timeline/styles/index.scss +13 -0
  20. package/Tree/styles/index.css +3 -0
  21. package/TreePicker/styles/index.css +3 -0
  22. package/Uploader/styles/index.css +3 -0
  23. package/Uploader/styles/index.scss +3 -0
  24. package/cjs/AutoComplete/AutoComplete.d.ts +2 -0
  25. package/cjs/AutoComplete/AutoComplete.js +3 -1
  26. package/cjs/CheckTree/utils.js +2 -1
  27. package/cjs/Form/Form.d.ts +37 -0
  28. package/cjs/Form/Form.js +16 -1
  29. package/cjs/Form/hooks/useFormValidate.d.ts +2 -0
  30. package/cjs/Form/hooks/useFormValidate.js +117 -1
  31. package/cjs/Form/index.d.ts +1 -0
  32. package/cjs/Form/resolvers.d.ts +59 -0
  33. package/cjs/Form/resolvers.js +4 -0
  34. package/cjs/Timeline/Timeline.d.ts +5 -0
  35. package/cjs/Timeline/Timeline.js +13 -6
  36. package/cjs/Tree/hooks/useFlattenTree.js +5 -8
  37. package/cjs/Uploader/Uploader.d.ts +2 -0
  38. package/cjs/Uploader/Uploader.js +48 -2
  39. package/cjs/internals/Picker/PickerIndicator.js +4 -1
  40. package/cjs/toaster/toaster.js +38 -3
  41. package/dist/rsuite-no-reset.css +17 -0
  42. package/dist/rsuite-no-reset.min.css +1 -1
  43. package/dist/rsuite.css +17 -0
  44. package/dist/rsuite.js +9 -9
  45. package/dist/rsuite.min.css +1 -1
  46. package/dist/rsuite.min.js +1 -1
  47. package/dist/rsuite.min.js.map +1 -1
  48. package/esm/AutoComplete/AutoComplete.d.ts +2 -0
  49. package/esm/AutoComplete/AutoComplete.js +3 -1
  50. package/esm/CheckTree/utils.js +2 -1
  51. package/esm/Form/Form.d.ts +37 -0
  52. package/esm/Form/Form.js +16 -1
  53. package/esm/Form/hooks/useFormValidate.d.ts +2 -0
  54. package/esm/Form/hooks/useFormValidate.js +117 -1
  55. package/esm/Form/index.d.ts +1 -0
  56. package/esm/Form/resolvers.d.ts +59 -0
  57. package/esm/Form/resolvers.js +2 -0
  58. package/esm/Timeline/Timeline.d.ts +5 -0
  59. package/esm/Timeline/Timeline.js +13 -6
  60. package/esm/Tree/hooks/useFlattenTree.js +5 -8
  61. package/esm/Uploader/Uploader.d.ts +2 -0
  62. package/esm/Uploader/Uploader.js +48 -2
  63. package/esm/internals/Picker/PickerIndicator.js +4 -1
  64. package/esm/toaster/toaster.js +38 -3
  65. package/internals/Picker/styles/index.scss +3 -0
  66. package/package.json +1 -1
@@ -2223,6 +2223,9 @@
2223
2223
  align-items:center;
2224
2224
  }
2225
2225
  .rs-picker-toggle-indicator .rs-picker-clean{
2226
+ display:inline-flex;
2227
+ align-items:center;
2228
+ justify-content:center;
2226
2229
  color:var(--rs-text-secondary);
2227
2230
  transition:0.2s color linear;
2228
2231
  cursor:pointer;
package/CHANGELOG.md CHANGED
@@ -1,3 +1,30 @@
1
+ ## [6.2.1](https://github.com/rsuite/rsuite/compare/v6.2.0...v6.2.1) (2026-06-12)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **picker:** hide caret when clear button is shown ([#4581](https://github.com/rsuite/rsuite/issues/4581)) ([d52842b](https://github.com/rsuite/rsuite/commit/d52842b0c6444bc6765595899e3f9917de52af7a))
7
+
8
+
9
+
10
+ # [6.2.0](https://github.com/rsuite/rsuite/compare/v6.1.3...v6.2.0) (2026-06-12)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **CheckTreePicker,CheckTree:** fix infinite loop with cascade and defaultExpandAll ([#4579](https://github.com/rsuite/rsuite/issues/4579)) ([fc86644](https://github.com/rsuite/rsuite/commit/fc86644b595809c83f4aa453f7a5e07d00367522)), closes [#3973](https://github.com/rsuite/rsuite/issues/3973) [#3973](https://github.com/rsuite/rsuite/issues/3973) [#4541](https://github.com/rsuite/rsuite/issues/4541) [#4404](https://github.com/rsuite/rsuite/issues/4404) [#4424](https://github.com/rsuite/rsuite/issues/4424)
16
+ * **toaster:** prevent duplicate containers when push() is called synchronously in a loop ([#4571](https://github.com/rsuite/rsuite/issues/4571)) ([c507f5f](https://github.com/rsuite/rsuite/commit/c507f5f59a726dc25dce65ab37fad484563e502d))
17
+ * **Uploader:** prevent horizontal overflow and width increase ([#4580](https://github.com/rsuite/rsuite/issues/4580)) ([e064f4c](https://github.com/rsuite/rsuite/commit/e064f4cc27959f94d3354a9177a8ae0a9ad71460)), closes [#4536](https://github.com/rsuite/rsuite/issues/4536)
18
+
19
+
20
+ ### Features
21
+
22
+ * **Form:** add `resolver` prop for third-party validation library integration ([#4573](https://github.com/rsuite/rsuite/issues/4573)) ([4a9ff7b](https://github.com/rsuite/rsuite/commit/4a9ff7bef3979764fcf3b12e869ccbf5bf61c3bd))
23
+ * **Timeline:** add `reverse` prop to display items in reverse order ([#4569](https://github.com/rsuite/rsuite/issues/4569)) ([a0dd1f4](https://github.com/rsuite/rsuite/commit/a0dd1f4307756f283c05157cd05f8ea6f7885c36))
24
+ * **Uploader:** add `onCompletion` callback prop ([#4570](https://github.com/rsuite/rsuite/issues/4570)) ([7eb0748](https://github.com/rsuite/rsuite/commit/7eb0748f78d36e1f34ac940b4fac3b6c20bf1a52)), closes [#813](https://github.com/rsuite/rsuite/issues/813)
25
+
26
+
27
+
1
28
  ## [6.1.3](https://github.com/rsuite/rsuite/compare/v6.1.2...v6.1.3) (2026-04-22)
2
29
 
3
30
 
@@ -2384,6 +2384,9 @@
2384
2384
  align-items:center;
2385
2385
  }
2386
2386
  .rs-picker-toggle-indicator .rs-picker-clean{
2387
+ display:inline-flex;
2388
+ align-items:center;
2389
+ justify-content:center;
2387
2390
  color:var(--rs-text-secondary);
2388
2391
  transition:0.2s color linear;
2389
2392
  cursor:pointer;
@@ -2411,6 +2411,9 @@ label:hover .rs-checkbox-control .rs-checkbox-inner::before{
2411
2411
  align-items:center;
2412
2412
  }
2413
2413
  .rs-picker-toggle-indicator .rs-picker-clean{
2414
+ display:inline-flex;
2415
+ align-items:center;
2416
+ justify-content:center;
2414
2417
  color:var(--rs-text-secondary);
2415
2418
  transition:0.2s color linear;
2416
2419
  cursor:pointer;
@@ -2242,6 +2242,9 @@
2242
2242
  align-items:center;
2243
2243
  }
2244
2244
  .rs-picker-toggle-indicator .rs-picker-clean{
2245
+ display:inline-flex;
2246
+ align-items:center;
2247
+ justify-content:center;
2245
2248
  color:var(--rs-text-secondary);
2246
2249
  transition:0.2s color linear;
2247
2250
  cursor:pointer;
@@ -2243,6 +2243,9 @@
2243
2243
  align-items:center;
2244
2244
  }
2245
2245
  .rs-picker-toggle-indicator .rs-picker-clean{
2246
+ display:inline-flex;
2247
+ align-items:center;
2248
+ justify-content:center;
2246
2249
  color:var(--rs-text-secondary);
2247
2250
  transition:0.2s color linear;
2248
2251
  cursor:pointer;
@@ -2275,6 +2275,9 @@
2275
2275
  align-items:center;
2276
2276
  }
2277
2277
  .rs-picker-toggle-indicator .rs-picker-clean{
2278
+ display:inline-flex;
2279
+ align-items:center;
2280
+ justify-content:center;
2278
2281
  color:var(--rs-text-secondary);
2279
2282
  transition:0.2s color linear;
2280
2283
  cursor:pointer;
@@ -2277,6 +2277,9 @@
2277
2277
  align-items:center;
2278
2278
  }
2279
2279
  .rs-picker-toggle-indicator .rs-picker-clean{
2280
+ display:inline-flex;
2281
+ align-items:center;
2282
+ justify-content:center;
2280
2283
  color:var(--rs-text-secondary);
2281
2284
  transition:0.2s color linear;
2282
2285
  cursor:pointer;
@@ -2244,6 +2244,9 @@
2244
2244
  align-items:center;
2245
2245
  }
2246
2246
  .rs-picker-toggle-indicator .rs-picker-clean{
2247
+ display:inline-flex;
2248
+ align-items:center;
2249
+ justify-content:center;
2247
2250
  color:var(--rs-text-secondary);
2248
2251
  transition:0.2s color linear;
2249
2252
  cursor:pointer;
@@ -2564,6 +2564,9 @@ label:hover .rs-checkbox-control .rs-checkbox-inner::before{
2564
2564
  align-items:center;
2565
2565
  }
2566
2566
  .rs-picker-toggle-indicator .rs-picker-clean{
2567
+ display:inline-flex;
2568
+ align-items:center;
2569
+ justify-content:center;
2567
2570
  color:var(--rs-text-secondary);
2568
2571
  transition:0.2s color linear;
2569
2572
  cursor:pointer;
@@ -2564,6 +2564,9 @@ label:hover .rs-checkbox-control .rs-checkbox-inner::before{
2564
2564
  align-items:center;
2565
2565
  }
2566
2566
  .rs-picker-toggle-indicator .rs-picker-clean{
2567
+ display:inline-flex;
2568
+ align-items:center;
2569
+ justify-content:center;
2567
2570
  color:var(--rs-text-secondary);
2568
2571
  transition:0.2s color linear;
2569
2572
  cursor:pointer;
@@ -2309,6 +2309,9 @@
2309
2309
  align-items:center;
2310
2310
  }
2311
2311
  .rs-picker-toggle-indicator .rs-picker-clean{
2312
+ display:inline-flex;
2313
+ align-items:center;
2314
+ justify-content:center;
2312
2315
  color:var(--rs-text-secondary);
2313
2316
  transition:0.2s color linear;
2314
2317
  cursor:pointer;
@@ -2244,6 +2244,9 @@
2244
2244
  align-items:center;
2245
2245
  }
2246
2246
  .rs-picker-toggle-indicator .rs-picker-clean{
2247
+ display:inline-flex;
2248
+ align-items:center;
2249
+ justify-content:center;
2247
2250
  color:var(--rs-text-secondary);
2248
2251
  transition:0.2s color linear;
2249
2252
  cursor:pointer;
@@ -2256,6 +2256,9 @@
2256
2256
  align-items:center;
2257
2257
  }
2258
2258
  .rs-picker-toggle-indicator .rs-picker-clean{
2259
+ display:inline-flex;
2260
+ align-items:center;
2261
+ justify-content:center;
2259
2262
  color:var(--rs-text-secondary);
2260
2263
  transition:0.2s color linear;
2261
2264
  cursor:pointer;
@@ -2256,6 +2256,9 @@
2256
2256
  align-items:center;
2257
2257
  }
2258
2258
  .rs-picker-toggle-indicator .rs-picker-clean{
2259
+ display:inline-flex;
2260
+ align-items:center;
2261
+ justify-content:center;
2259
2262
  color:var(--rs-text-secondary);
2260
2263
  transition:0.2s color linear;
2261
2264
  cursor:pointer;
@@ -2275,6 +2275,9 @@
2275
2275
  align-items:center;
2276
2276
  }
2277
2277
  .rs-picker-toggle-indicator .rs-picker-clean{
2278
+ display:inline-flex;
2279
+ align-items:center;
2280
+ justify-content:center;
2278
2281
  color:var(--rs-text-secondary);
2279
2282
  transition:0.2s color linear;
2280
2283
  cursor:pointer;
@@ -2277,6 +2277,9 @@
2277
2277
  align-items:center;
2278
2278
  }
2279
2279
  .rs-picker-toggle-indicator .rs-picker-clean{
2280
+ display:inline-flex;
2281
+ align-items:center;
2282
+ justify-content:center;
2280
2283
  color:var(--rs-text-secondary);
2281
2284
  transition:0.2s color linear;
2282
2285
  cursor:pointer;
@@ -145,6 +145,17 @@
145
145
  height:auto;
146
146
  min-height:var(--rs-time-line-tail-min-height);
147
147
  }
148
+ .rs-timeline-reverse.rs-timeline-endless .rs-timeline-item:first-child .rs-timeline-item-tail{
149
+ top:0;
150
+ bottom:0;
151
+ height:auto;
152
+ }
153
+
154
+ .rs-timeline-reverse.rs-timeline-endless .rs-timeline-item:last-child .rs-timeline-item-tail{
155
+ height:calc(var(--rs-time-line-dot-center-gap) + var(--rs-time-line-dot-side-length));
156
+ min-height:0;
157
+ }
158
+
148
159
  .rs-timeline-item:only-child .rs-timeline-item-tail{
149
160
  display:none;
150
161
  }
@@ -8,6 +8,8 @@
8
8
  // --------------------------------------------------
9
9
 
10
10
  .rs-timeline {
11
+ $root: &;
12
+
11
13
  // CSS Variables
12
14
  --rs-time-line-tail-min-height: 2.375rem; // 20px + 18px
13
15
  --rs-time-line-item-content-margin: 12px;
@@ -87,6 +89,17 @@
87
89
  min-height: var(--rs-time-line-tail-min-height);
88
90
  }
89
91
 
92
+ @at-root #{$root}-reverse#{$root}-endless #{$root}-item:first-child #{$root}-item-tail {
93
+ top: 0;
94
+ bottom: 0;
95
+ height: auto;
96
+ }
97
+
98
+ @at-root #{$root}-reverse#{$root}-endless #{$root}-item:last-child #{$root}-item-tail {
99
+ height: calc(var(--rs-time-line-dot-center-gap) + var(--rs-time-line-dot-side-length));
100
+ min-height: 0;
101
+ }
102
+
90
103
  &-item:only-child &-item-tail {
91
104
  display: none;
92
105
  }
@@ -2242,6 +2242,9 @@
2242
2242
  align-items:center;
2243
2243
  }
2244
2244
  .rs-picker-toggle-indicator .rs-picker-clean{
2245
+ display:inline-flex;
2246
+ align-items:center;
2247
+ justify-content:center;
2245
2248
  color:var(--rs-text-secondary);
2246
2249
  transition:0.2s color linear;
2247
2250
  cursor:pointer;
@@ -2242,6 +2242,9 @@
2242
2242
  align-items:center;
2243
2243
  }
2244
2244
  .rs-picker-toggle-indicator .rs-picker-clean{
2245
+ display:inline-flex;
2246
+ align-items:center;
2247
+ justify-content:center;
2245
2248
  color:var(--rs-text-secondary);
2246
2249
  transition:0.2s color linear;
2247
2250
  cursor:pointer;
@@ -1272,6 +1272,7 @@
1272
1272
  text-overflow:ellipsis;
1273
1273
  white-space:nowrap;
1274
1274
  flex:1 1 auto;
1275
+ min-width:0;
1275
1276
  }
1276
1277
  .rs-uploader[data-list-type=text] .rs-uploader-file-item-size{
1277
1278
  flex:0 0 auto;
@@ -1318,6 +1319,7 @@
1318
1319
  .rs-uploader[data-list-type=picture]{
1319
1320
  display:inline-flex;
1320
1321
  flex-direction:row;
1322
+ flex-wrap:wrap;
1321
1323
  gap:var(--rs-uploader-item-spacing);
1322
1324
  }
1323
1325
  .rs-uploader[data-list-type=picture] .rs-uploader-trigger-btn{
@@ -1352,6 +1354,7 @@
1352
1354
  }
1353
1355
  .rs-uploader[data-list-type=picture] .rs-uploader-file-items{
1354
1356
  display:inline-flex;
1357
+ flex-wrap:wrap;
1355
1358
  gap:var(--rs-uploader-item-spacing);
1356
1359
  }
1357
1360
  .rs-uploader[data-list-type=picture] .rs-uploader-file-item{
@@ -109,6 +109,7 @@
109
109
  @include utils.ellipsis-basic;
110
110
 
111
111
  flex: 1 1 auto;
112
+ min-width: 0;
112
113
  }
113
114
 
114
115
  &-size {
@@ -169,6 +170,7 @@
169
170
  .rs-uploader[data-list-type='picture'] {
170
171
  display: inline-flex;
171
172
  flex-direction: row;
173
+ flex-wrap: wrap;
172
174
  gap: var(--rs-uploader-item-spacing);
173
175
 
174
176
  .rs-uploader-trigger-btn {
@@ -213,6 +215,7 @@
213
215
 
214
216
  .rs-uploader-file-items {
215
217
  display: inline-flex;
218
+ flex-wrap: wrap;
216
219
  gap: var(--rs-uploader-item-spacing);
217
220
  }
218
221
 
@@ -30,6 +30,8 @@ export interface AutoCompleteProps<T = string> extends FormControlPickerProps<T,
30
30
  onOpen?: () => void;
31
31
  /** Called on close */
32
32
  onClose?: () => void;
33
+ /** Ref to the input element */
34
+ inputRef?: React.Ref<HTMLInputElement>;
33
35
  }
34
36
  /**
35
37
  * Autocomplete function of input field.
@@ -57,6 +57,7 @@ const AutoComplete = (0, _utils.forwardRef)((props, ref) => {
57
57
  onFocus,
58
58
  onBlur,
59
59
  onMenuFocus,
60
+ inputRef,
60
61
  ...rest
61
62
  } = propsWithDefaults;
62
63
  const datalist = (0, _utils2.transformData)(data);
@@ -210,7 +211,8 @@ const AutoComplete = (0, _utils.forwardRef)((props, ref) => {
210
211
  onBlur: handleInputBlur,
211
212
  onFocus: handleInputFocus,
212
213
  onChange: handleChange,
213
- onKeyDown: handleKeyDownEvent
214
+ onKeyDown: handleKeyDownEvent,
215
+ inputRef: inputRef
214
216
  })));
215
217
  });
216
218
  AutoComplete.displayName = 'AutoComplete';
@@ -227,7 +227,8 @@ function getDisabledState(nodes, node, props) {
227
227
  */
228
228
  function getCheckTreeDefaultValue(value, uncheckableItemValues) {
229
229
  if (Array.isArray(value) && Array.isArray(uncheckableItemValues)) {
230
- return value.filter(v => !uncheckableItemValues.includes(v));
230
+ const filtered = value.filter(v => !uncheckableItemValues.includes(v));
231
+ return filtered.length === value.length ? value : filtered;
231
232
  }
232
233
  return value;
233
234
  }
@@ -3,6 +3,7 @@ import { FormControlComponent } from '../FormControl';
3
3
  import { FormInstance } from './hooks/useFormRef';
4
4
  import { Schema } from 'schema-typed';
5
5
  import type { WithAsProps, CheckTriggerType } from '../internals/types';
6
+ import type { Resolver } from './resolvers';
6
7
  export interface FormProps<V = Record<string, any>, M = any, E = {
7
8
  [P in keyof V]?: M;
8
9
  }> extends WithAsProps, Omit<FormHTMLAttributes<HTMLFormElement>, 'onChange' | 'onSubmit' | 'onError' | 'onReset'> {
@@ -40,6 +41,42 @@ export interface FormProps<V = Record<string, any>, M = any, E = {
40
41
  * @see https://github.com/rsuite/schema-typed
41
42
  */
42
43
  model?: Schema;
44
+ /**
45
+ * A resolver function for integrating third-party validation libraries such as
46
+ * Yup, Zod, AJV, Joi, Valibot, etc.
47
+ *
48
+ * When provided, the `resolver` takes precedence over the `model` prop for
49
+ * form-level validation (`check` / `checkAsync`). Field-level inline `rule`
50
+ * props on `<Form.Control>` components are still respected.
51
+ *
52
+ * The resolver receives the current form values and must return (or resolve to)
53
+ * a `{ errors }` object where each key is a field name and each value is an
54
+ * error message or error object. An empty `errors` object means the form is valid.
55
+ *
56
+ * **Note:** If the resolver is asynchronous, form-level sync validation
57
+ * (`check()`) will return `false` and log a warning. Use `checkAsync()` or
58
+ * rely on the `onSubmit` callback (which always awaits the resolver).
59
+ *
60
+ * @example
61
+ * ```tsx
62
+ * import * as yup from 'yup';
63
+ *
64
+ * const schema = yup.object({ name: yup.string().email().required() });
65
+ * const resolver = async (formValue) => {
66
+ * try {
67
+ * await schema.validate(formValue, { abortEarly: false });
68
+ * return { errors: {} };
69
+ * } catch (e) {
70
+ * const errors = {};
71
+ * e.inner.forEach(err => { if (err.path) errors[err.path] = err.message; });
72
+ * return { errors };
73
+ * }
74
+ * };
75
+ *
76
+ * <Form resolver={resolver} onSubmit={handleSubmit}>…</Form>
77
+ * ```
78
+ */
79
+ resolver?: Resolver<V>;
43
80
  /**
44
81
  * Make the form readonly
45
82
  */
package/cjs/Form/Form.js CHANGED
@@ -58,6 +58,7 @@ const Form = (0, _utils.forwardRef)((props, ref) => {
58
58
  fluid,
59
59
  layout,
60
60
  model: formModel = defaultSchema,
61
+ resolver,
61
62
  readOnly,
62
63
  plaintext,
63
64
  children,
@@ -88,7 +89,8 @@ const Form = (0, _utils.forwardRef)((props, ref) => {
88
89
  getCombinedModel,
89
90
  onCheck,
90
91
  onError,
91
- nestedField
92
+ nestedField,
93
+ resolver
92
94
  };
93
95
  const {
94
96
  formError,
@@ -104,6 +106,19 @@ const Form = (0, _utils.forwardRef)((props, ref) => {
104
106
  cleanErrorForField
105
107
  } = (0, _useFormValidate.default)(controlledFormError, formValidateProps);
106
108
  const submit = (0, _hooks.useEventCallback)(event => {
109
+ if (resolver) {
110
+ // When a resolver is provided, always use the async validation path so that
111
+ // both sync and async resolvers are handled correctly.
112
+ checkAsync().then(({
113
+ hasError
114
+ }) => {
115
+ if (!hasError) {
116
+ onSubmit?.(formValue, event);
117
+ }
118
+ });
119
+ return;
120
+ }
121
+
107
122
  // Check the form before submitting
108
123
  if (check()) {
109
124
  onSubmit?.(formValue, event);
@@ -1,9 +1,11 @@
1
+ import type { Resolver } from '../resolvers';
1
2
  export interface FormErrorProps {
2
3
  formValue: any;
3
4
  getCombinedModel: () => any;
4
5
  onCheck?: (formError: any) => void;
5
6
  onError?: (formError: any) => void;
6
7
  nestedField?: boolean;
8
+ resolver?: Resolver;
7
9
  }
8
10
  export default function useFormValidate(_formError: any, props: FormErrorProps): {
9
11
  formError: any;
@@ -15,7 +15,8 @@ function useFormValidate(_formError, props) {
15
15
  getCombinedModel,
16
16
  onCheck,
17
17
  onError,
18
- nestedField
18
+ nestedField,
19
+ resolver
19
20
  } = props;
20
21
  const [realFormError, setFormError] = (0, _hooks.useControlled)(_formError, {});
21
22
  const checkOptions = {
@@ -24,12 +25,64 @@ function useFormValidate(_formError, props) {
24
25
  const realFormErrorRef = (0, _react.useRef)(realFormError);
25
26
  realFormErrorRef.current = realFormError;
26
27
 
28
+ /**
29
+ * Returns true when an error value is considered non-empty (i.e. the field has an error).
30
+ */
31
+ const isValidError = error => error !== undefined && error !== null && error !== '';
32
+
33
+ /**
34
+ * Merges resolver errors into the current form error state, removing entries that
35
+ * are no longer invalid according to the latest resolver result.
36
+ */
37
+ const mergeResolverErrors = (current, resolverErrors) => {
38
+ const next = {
39
+ ...current
40
+ };
41
+ Object.keys({
42
+ ...current,
43
+ ...resolverErrors
44
+ }).forEach(key => {
45
+ if (isValidError(resolverErrors[key])) {
46
+ next[key] = resolverErrors[key];
47
+ } else {
48
+ delete next[key];
49
+ }
50
+ });
51
+ return next;
52
+ };
53
+
27
54
  /**
28
55
  * Validate the form data and return a boolean.
29
56
  * The error message after verification is returned in the callback.
57
+ *
58
+ * When a `resolver` is provided and the resolver returns a Promise (async resolver),
59
+ * this method cannot resolve the result synchronously. In that case it returns `false`
60
+ * immediately and you should use `checkAsync()` instead.
30
61
  * @param callback
31
62
  */
32
63
  const check = (0, _hooks.useEventCallback)(callback => {
64
+ if (resolver) {
65
+ const result = resolver(formValue || {});
66
+
67
+ // Async resolver: cannot handle synchronously
68
+ if (result instanceof Promise) {
69
+ if (process.env.NODE_ENV !== 'production') {
70
+ console.warn('[rsuite] The `resolver` provided to <Form> returns a Promise. ' + 'Use `checkAsync()` or rely on `onSubmit` for async validation.');
71
+ }
72
+ return false;
73
+ }
74
+ const {
75
+ errors
76
+ } = result;
77
+ const hasError = Object.keys(errors).length > 0;
78
+ setFormError(errors);
79
+ onCheck?.(errors);
80
+ callback?.(errors);
81
+ if (hasError) {
82
+ onError?.(errors);
83
+ }
84
+ return !hasError;
85
+ }
33
86
  const formError = {};
34
87
  let errorCount = 0;
35
88
  const model = getCombinedModel();
@@ -64,6 +117,35 @@ function useFormValidate(_formError, props) {
64
117
  return true;
65
118
  });
66
119
  const checkFieldForNextValue = (0, _hooks.useEventCallback)((fieldName, nextValue, callback) => {
120
+ if (resolver) {
121
+ const result = resolver(nextValue);
122
+ if (result instanceof Promise) {
123
+ if (process.env.NODE_ENV !== 'production') {
124
+ console.warn('[rsuite] The `resolver` provided to <Form> returns a Promise. ' + 'Use `checkAsync()` or `checkForFieldAsync()` for async validation.');
125
+ }
126
+ return false;
127
+ }
128
+ const {
129
+ errors
130
+ } = result;
131
+ const fieldError = errors[fieldName];
132
+ const hasFieldError = isValidError(fieldError);
133
+ // Merge resolver errors with existing errors, clearing fields that now pass
134
+ const nextFormError = mergeResolverErrors(realFormError, errors);
135
+ setFormError(nextFormError);
136
+ onCheck?.(nextFormError);
137
+ const callbackResult = {
138
+ hasError: hasFieldError,
139
+ errorMessage: fieldError
140
+ };
141
+ callback?.(hasFieldError ? callbackResult : {
142
+ hasError: false
143
+ });
144
+ if (Object.keys(nextFormError).length > 0) {
145
+ onError?.(nextFormError);
146
+ }
147
+ return !hasFieldError;
148
+ }
67
149
  const model = getCombinedModel();
68
150
  const resultOfCurrentField = model.checkForField(fieldName, nextValue, checkOptions);
69
151
  let nextFormError = {
@@ -122,6 +204,22 @@ function useFormValidate(_formError, props) {
122
204
  * Check form data asynchronously and return a Promise
123
205
  */
124
206
  const checkAsync = (0, _hooks.useEventCallback)(() => {
207
+ if (resolver) {
208
+ return Promise.resolve(resolver(formValue || {})).then(({
209
+ errors
210
+ }) => {
211
+ const hasError = Object.keys(errors).length > 0;
212
+ onCheck?.(errors);
213
+ setFormError(errors);
214
+ if (hasError) {
215
+ onError?.(errors);
216
+ }
217
+ return {
218
+ hasError,
219
+ formError: errors
220
+ };
221
+ });
222
+ }
125
223
  const promises = [];
126
224
  const keys = [];
127
225
  const model = getCombinedModel();
@@ -150,6 +248,24 @@ function useFormValidate(_formError, props) {
150
248
  });
151
249
  });
152
250
  const checkFieldAsyncForNextValue = (0, _hooks.useEventCallback)((fieldName, nextValue) => {
251
+ if (resolver) {
252
+ return Promise.resolve(resolver(nextValue)).then(({
253
+ errors
254
+ }) => {
255
+ const fieldError = errors[fieldName];
256
+ const hasFieldError = isValidError(fieldError);
257
+ const nextFormError = mergeResolverErrors(realFormError, errors);
258
+ onCheck?.(nextFormError);
259
+ setFormError(nextFormError);
260
+ if (Object.keys(nextFormError).length > 0) {
261
+ onError?.(nextFormError);
262
+ }
263
+ return {
264
+ hasError: hasFieldError,
265
+ errorMessage: fieldError
266
+ };
267
+ });
268
+ }
153
269
  const model = getCombinedModel();
154
270
  return model.checkForFieldAsync(fieldName, nextValue, checkOptions).then(resultOfCurrentField => {
155
271
  let nextFormError = {
@@ -1,5 +1,6 @@
1
1
  import Form from './Form';
2
2
  export type { FormProps } from './Form';
3
3
  export type { FormInstance } from './hooks/useFormRef';
4
+ export type { Resolver, ResolverResult } from './resolvers';
4
5
  export { Form };
5
6
  export default Form;