@andreyfedkovich/cozy-ui 0.9.0 → 0.10.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  Формат основан на [Keep a Changelog](https://keepachangelog.com/).
4
4
  Версии соответствуют [Semantic Versioning](https://semver.org/) и git-тегам `v*`.
5
5
 
6
+ ## 0.10.0 - 2026-06-04
7
+
8
+ - **feat:** New `ShowErrorPolicy` presets — `draftFriendly`, `wizardStep`, `savedInvalid`, `onBlurOrSubmit`. Recommended for draft-friendly and wizard forms; legacy `default` unchanged (shows on `hasValue` alone).
9
+ - **feat:** Extended `FieldMeta` — `stepSubmitted`, `validationPending`, `errorKind`. `resolveDisplayError` suppresses stale `required` when value is non-empty.
10
+ - **feat:** `useFormFields` + `FieldBinding` — replaces typical per-app binding glue. `markStepSubmitted`, `markFormSubmitted`, `resetInteraction`.
11
+ - **feat:** `useValidationRequest` — async validate with generation/stale guard and `validationPending`.
12
+ - **feat:** `attemptWizardStep`, `attemptFormSubmit` — validate-on-click for wizard and submit (no `disabled={!isValid}`).
13
+ - **feat:** `suppressError` prop on field components; dev warning when `error={null}` suppresses invalid `fieldMeta`.
14
+ - **feat:** Export `useFieldPresentation`, `FieldErrorCaption`, `FormField`, `hasFieldValue`.
15
+ - **docs:** `docs/validation-recipes.md` — recipes, anti-patterns, migration guide and consumer verification checklist.
16
+
6
17
  ## 0.9.0 - 2026-06-04
7
18
 
8
19
  - **feat:** Единый контракт валидации полей — headless API: `FieldMeta`, `ShowErrorPolicy`, `resolveShowError`, `resolveFieldError`, `resolveFieldMessage`, `useFieldState`, `resolveValueChangeHandler`.
package/README.md CHANGED
@@ -37,6 +37,7 @@ npm i @andreyfedkovich/cozy-ui
37
37
  - [Utility](#utility) — `Tag`, `CopyTextTrigger`
38
38
  - [Workflow](#workflow) — `ApprovalRoute`, `CommentFeed`, `DetailView`, `SideNav`, `SettingsView`, `Switch`, `ImageSegmented`
39
39
  - [Hooks & helpers](#hooks--helpers)
40
+ - [Validation recipes](https://github.com/AndreyFedkovich/cozy-ui-components/blob/main/docs/validation-recipes.md)
40
41
  - [Icons](#icons)
41
42
  - [TypeScript](#typescript)
42
43
  - [SSR & framework support](#ssr--framework-support)
@@ -402,14 +403,19 @@ Form state stays in your app (React Hook Form, TanStack Form, or `useState`). Co
402
403
 
403
404
  | Export | Description |
404
405
  | ------ | ----------- |
405
- | `FieldMeta` | `touched`, `dirty`, `submitted`, `hasValue`, `invalid`, `errorMessage` |
406
- | `ShowErrorPolicy` | `"default"` \| `"onBlur"` \| `"onSubmit"` \| `"always"` \| custom `(meta) => boolean` |
407
- | `resolveShowError`, `resolveFieldError`, `resolveFieldMessage` | Pure functions (SSR-safe) |
408
- | `useFieldState` | React hook wrapping the resolvers |
406
+ | `FieldMeta` | `touched`, `dirty`, `submitted`, `stepSubmitted`, `hasValue`, `invalid`, `errorMessage`, `validationPending`, `errorKind` |
407
+ | `ShowErrorPolicy` | `"default"` (legacy) \| `"draftFriendly"` \| `"wizardStep"` \| `"onBlur"` \| `"onSubmit"` \| custom |
408
+ | `resolveShowError`, `resolveFieldError`, `resolveFieldMessage`, `resolveDisplayError` | Pure functions (SSR-safe) |
409
+ | `useFieldState`, `useFormFields`, `useValidationRequest` | React hooks |
410
+ | `attemptWizardStep`, `attemptFormSubmit` | Validate-on-click helpers |
409
411
 
410
- **Default policy:** `invalid && (touched || submitted || hasValue)`.
412
+ **Recommended policy for draft-friendly forms:** `draftFriendly` no flash on first keystroke; saved invalid visible on load.
411
413
 
412
- **Props on fields:** `error` (explicit override), `fieldMeta`, `showErrorPolicy`.
414
+ **Legacy `default` policy:** `invalid && (touched || submitted || hasValue)`.
415
+
416
+ **Props on fields:** `error`, `suppressError`, `fieldMeta`, `showErrorPolicy`.
417
+
418
+ See [Validation recipes](https://github.com/AndreyFedkovich/cozy-ui-components/blob/main/docs/validation-recipes.md) for step-by-step recipes and expected form behavior.
413
419
 
414
420
  **Callback families:**
415
421
 
@@ -1170,8 +1176,12 @@ import {
1170
1176
  type ShowErrorPolicy,
1171
1177
  resolveFieldError,
1172
1178
  resolveFieldMessage,
1179
+ resolveDisplayError,
1173
1180
  resolveShowError,
1174
1181
  useFieldState,
1182
+ useFormFields,
1183
+ useValidationRequest,
1184
+ attemptWizardStep,
1175
1185
  } from "@andreyfedkovich/cozy-ui";
1176
1186
  ```
1177
1187
 
@@ -113,6 +113,37 @@ export { ArrowDownIcon }
113
113
 
114
114
  export { ArrowRightIcon }
115
115
 
116
+ /** Validate-on-click for form Submit. */
117
+ export declare function attemptFormSubmit<TValidation>(options: AttemptFormSubmitOptions<TValidation>): Promise<AttemptFormSubmitResult<TValidation>>;
118
+
119
+ export declare type AttemptFormSubmitOptions<TValidation> = {
120
+ markFormSubmitted: () => void;
121
+ validate: () => Promise<TValidation> | TValidation;
122
+ hasErrors: (validation: TValidation) => boolean;
123
+ };
124
+
125
+ export declare type AttemptFormSubmitResult<TValidation> = {
126
+ ok: boolean;
127
+ validation: TValidation;
128
+ };
129
+
130
+ /**
131
+ * Validate-on-click for wizard "Next" — does not require disabled={!isValid}.
132
+ */
133
+ export declare function attemptWizardStep<TValidation>(options: AttemptWizardStepOptions<TValidation>): Promise<AttemptWizardStepResult<TValidation>>;
134
+
135
+ export declare type AttemptWizardStepOptions<TValidation> = {
136
+ markStepSubmitted: (step: number) => void;
137
+ validate: () => Promise<TValidation> | TValidation;
138
+ step: number;
139
+ hasStepErrors: (validation: TValidation, step: number) => boolean;
140
+ };
141
+
142
+ export declare type AttemptWizardStepResult<TValidation> = {
143
+ ok: boolean;
144
+ validation: TValidation;
145
+ };
146
+
116
147
  export declare const BaseBlock: FC<BaseBlockProps>;
117
148
 
118
149
  declare interface BaseBlockProps {
@@ -138,7 +169,7 @@ declare type ButtonSize = "small" | "medium" | "large";
138
169
 
139
170
  declare type ButtonVariant = "default" | "primary" | "secondary" | "text" | "link" | "danger";
140
171
 
141
- export declare const Calendar: ({ label, required, value, onValueChange, onChange, minDate, error, fieldMeta, showErrorPolicy, disabled, onBlur, onFocus, tooltipContent, tooltipPopperClassName, className, }: CalendarProps) => JSX.Element;
172
+ export declare const Calendar: ({ label, required, value, onValueChange, onChange, minDate, error, suppressError, fieldMeta, showErrorPolicy, disabled, onBlur, onFocus, tooltipContent, tooltipPopperClassName, className, }: CalendarProps) => JSX.Element;
142
173
 
143
174
  export declare interface CalendarProps extends ValueFieldCallbacks<string | null>, FieldValidationProps {
144
175
  label: string;
@@ -483,7 +514,7 @@ export declare interface DetailViewProps {
483
514
  id?: string;
484
515
  }
485
516
 
486
- export declare const DialogSelect: <T, S extends string | number>({ value, placeholder, loadOptions, onValueChange, onChange, onBlur, onFocus, onClear, columns, label, tooltipContent, tooltipPopperClassName, title, searchPlaceholder, selectButtonText, closeButtonText, manualButtonText, onManualAdd, pageSize, debounceMs, disabled, error, fieldMeta, showErrorPolicy, className, inputClassName, selectedOptionRender, }: DialogSelectProps<T, S>) => JSX.Element;
517
+ export declare const DialogSelect: <T, S extends string | number>({ value, placeholder, loadOptions, onValueChange, onChange, onBlur, onFocus, onClear, columns, label, tooltipContent, tooltipPopperClassName, title, searchPlaceholder, selectButtonText, closeButtonText, manualButtonText, onManualAdd, pageSize, debounceMs, disabled, error, suppressError, fieldMeta, showErrorPolicy, className, inputClassName, selectedOptionRender, }: DialogSelectProps<T, S>) => JSX.Element;
487
518
 
488
519
  export declare type DialogSelectColumn<T, S extends string | number> = {
489
520
  key: string;
@@ -556,26 +587,61 @@ export { EnvelopIcon }
556
587
 
557
588
  export { FeedbackIcon }
558
589
 
590
+ export declare type FieldBinding = {
591
+ fieldMeta: FieldMeta;
592
+ showErrorPolicy?: ShowErrorPolicy;
593
+ onBlur: () => void;
594
+ onDirty?: () => void;
595
+ };
596
+
559
597
  declare interface FieldComponentProps extends Omit<DetailField, "value"> {
560
598
  value?: ReactNode;
561
599
  children?: ReactNode;
562
600
  className?: string;
563
601
  }
564
602
 
603
+ export declare function FieldErrorCaption({ id, message }: FieldErrorCaptionProps): ReactNode;
604
+
605
+ declare type FieldErrorCaptionProps = {
606
+ id: string;
607
+ message: string | null;
608
+ };
609
+
610
+ export declare type FieldErrorKind = "required" | "semantic" | "custom";
611
+
612
+ /**
613
+ * Headless field state for policy-based error display.
614
+ *
615
+ * - `touched` — user blurred the field
616
+ * - `dirty` — user changed the value in the current session (any change, not only clear)
617
+ * - `submitted` — form-level submit failed (`markFormSubmitted`)
618
+ * - `stepSubmitted` — wizard step "Next" failed (`markStepSubmitted`)
619
+ * - `hasValue` — non-empty value (trimmed string, number, selected option, etc.)
620
+ * - `validationPending` — async validate in flight; optionally suppresses display
621
+ * - `errorKind` — drives `resolveDisplayError` (stale required suppression)
622
+ */
565
623
  export declare type FieldMeta = {
566
624
  touched?: boolean;
567
625
  dirty?: boolean;
568
626
  submitted?: boolean;
627
+ stepSubmitted?: boolean;
569
628
  hasValue?: boolean;
570
629
  invalid?: boolean;
571
630
  errorMessage?: string | null;
631
+ validationPending?: boolean;
632
+ errorKind?: FieldErrorKind;
572
633
  };
573
634
 
574
635
  declare const FieldRow: FC<FieldComponentProps>;
575
636
 
576
637
  export declare type FieldValidationProps = {
577
- /** Explicit error overrides {@link fieldMeta} + policy when set. */
638
+ /**
639
+ * Explicit error string overrides `fieldMeta` + policy.
640
+ * Omit to use `fieldMeta`. Do not pass `null` — use {@link suppressError} instead.
641
+ */
578
642
  error?: string | null;
643
+ /** Force-hide any error from `fieldMeta` or explicit `error`. */
644
+ suppressError?: boolean;
579
645
  fieldMeta?: FieldMeta;
580
646
  showErrorPolicy?: ShowErrorPolicy;
581
647
  };
@@ -588,12 +654,29 @@ export { FilterIcon }
588
654
 
589
655
  export { FolderEditIcon }
590
656
 
657
+ /**
658
+ * Optional wrapper that spreads fieldMeta, showErrorPolicy, and onBlur into a field control.
659
+ */
660
+ export declare function FormField({ bind, children }: FormFieldProps): ReactNode;
661
+
662
+ export declare type FormFieldProps = {
663
+ bind: FieldBinding;
664
+ label?: ReactNode;
665
+ required?: boolean;
666
+ children: (props: FieldBinding) => ReactNode;
667
+ };
668
+
669
+ export declare type FormFieldsApi<TValidation> = ReturnType<typeof useFormFields<TValidation>>;
670
+
591
671
  export { GraduateIcon }
592
672
 
593
673
  export { GridIcon }
594
674
 
595
675
  declare const GroupBlock: FC<SettingsGroup>;
596
676
 
677
+ /** Whether a field value counts as non-empty for `FieldMeta.hasValue`. */
678
+ export declare function hasFieldValue(value: unknown): boolean;
679
+
597
680
  export { HeartIcon }
598
681
 
599
682
  export { HelpIcon }
@@ -799,6 +882,19 @@ declare interface RadioGroupButtonProps<T extends string | number> {
799
882
 
800
883
  export { ReloadIcon }
801
884
 
885
+ /**
886
+ * Suppresses stale or in-flight errors before display.
887
+ * WHY: API may still return "required" while the value is already non-empty.
888
+ */
889
+ export declare function resolveDisplayError(options: {
890
+ errorMessage?: string | null;
891
+ errorKind?: FieldErrorKind;
892
+ hasValue?: boolean;
893
+ validationPending?: boolean;
894
+ }): string | null;
895
+
896
+ export declare function resolveDisplayErrorFromMeta(meta: FieldMeta): string | null;
897
+
802
898
  export declare function resolveFieldError(meta: FieldMeta | undefined, policy?: ShowErrorPolicy): string | null;
803
899
 
804
900
  /**
@@ -806,6 +902,7 @@ export declare function resolveFieldError(meta: FieldMeta | undefined, policy?:
806
902
  */
807
903
  export declare function resolveFieldMessage(options: {
808
904
  error?: string | null;
905
+ suppressError?: boolean;
809
906
  fieldMeta?: FieldMeta;
810
907
  showErrorPolicy?: ShowErrorPolicy;
811
908
  }): string | null;
@@ -830,7 +927,7 @@ declare interface SectionComponentProps extends Omit<DetailSection, "fields"> {
830
927
  declare interface SectionProps extends SideNavSection {
831
928
  }
832
929
 
833
- export declare const Select: <T, S extends string | number>({ options, value, mode, placeholder, onValueChange, onChange, onBlur, onFocus, dropdownRender, optionRender, selectedOptionRender, dropdownIcon, tagRender, dropDownClassName, optionClassName, inputClassName, deleteIconClassName, onDelete, onClear, label, tooltipContent, tooltipPopperClassName, onSearch, searchClassName, searchPlaceholder, isLoading, disabled, onClose, portalTarget, error, fieldMeta, showErrorPolicy, template, columns, total, }: CustomSelectProps<T, S>) => JSX.Element;
930
+ export declare const Select: <T, S extends string | number>({ options, value, mode, placeholder, onValueChange, onChange, onBlur, onFocus, dropdownRender, optionRender, selectedOptionRender, dropdownIcon, tagRender, dropDownClassName, optionClassName, inputClassName, deleteIconClassName, onDelete, onClear, label, tooltipContent, tooltipPopperClassName, onSearch, searchClassName, searchPlaceholder, isLoading, disabled, onClose, portalTarget, error, suppressError, fieldMeta, showErrorPolicy, template, columns, total, }: CustomSelectProps<T, S>) => JSX.Element;
834
931
 
835
932
  export declare type SelectColumn<T, S> = {
836
933
  key: string;
@@ -910,7 +1007,7 @@ export declare interface SettingsViewProps {
910
1007
  id?: string;
911
1008
  }
912
1009
 
913
- export declare type ShowErrorPolicy = "default" | "onBlur" | "onSubmit" | "always" | ((meta: FieldMeta) => boolean);
1010
+ export declare type ShowErrorPolicy = "default" | "onBlur" | "onSubmit" | "always" | "draftFriendly" | "wizardStep" | "savedInvalid" | "onBlurOrSubmit" | ((meta: FieldMeta) => boolean);
914
1011
 
915
1012
  export declare const SideNav: SideNavComponent;
916
1013
 
@@ -1131,7 +1228,7 @@ export declare type TooltipTrigger = "hover" | "click";
1131
1228
  /** `yyyy-MM-dd` for API / form state */
1132
1229
  export declare const toYmdString: (d: Date) => string;
1133
1230
 
1134
- export declare const TreeDialogSelect: <T, S extends string | number>({ value, placeholder, loadChildren: loadChildrenProp, loadNodes, searchNodes, onValueChange, onChange, onBlur, onFocus, onClear, label, tooltipContent, tooltipPopperClassName, title, searchPlaceholder, selectButtonText, closeButtonText, confirmButtonText, debounceMs, disabled, error, fieldMeta, showErrorPolicy, className, inputClassName, selectedOptionRender, nodeRender, leafConfirmOnly, }: TreeDialogSelectProps<T, S>) => JSX.Element;
1231
+ export declare const TreeDialogSelect: <T, S extends string | number>({ value, placeholder, loadChildren: loadChildrenProp, loadNodes, searchNodes, onValueChange, onChange, onBlur, onFocus, onClear, label, tooltipContent, tooltipPopperClassName, title, searchPlaceholder, selectButtonText, closeButtonText, confirmButtonText, debounceMs, disabled, error, suppressError, fieldMeta, showErrorPolicy, className, inputClassName, selectedOptionRender, nodeRender, leafConfirmOnly, }: TreeDialogSelectProps<T, S>) => JSX.Element;
1135
1232
 
1136
1233
  /** Pass either {@link loadNodes} or {@link loadChildren} (deprecated alias). */
1137
1234
  export declare type TreeDialogSelectProps<T, S extends string | number> = TreeDialogSelectShared<T, S> & ({
@@ -1209,12 +1306,54 @@ declare interface UseDropdownPositionProps {
1209
1306
  onAnchorFrame?: (placement: DropdownPosition) => void;
1210
1307
  }
1211
1308
 
1212
- export declare function useFieldState(fieldMeta: FieldMeta | undefined, policy?: ShowErrorPolicy, explicitError?: string | null): {
1309
+ export declare function useFieldPresentation(options: UseFieldPresentationOptions): {
1310
+ controlId: string;
1311
+ errorId: string;
1312
+ hintId: string | undefined;
1313
+ errorMessage: string | null;
1314
+ showError: boolean;
1315
+ ariaInvalid: boolean | undefined;
1316
+ ariaDescribedBy: string | undefined;
1317
+ };
1318
+
1319
+ export declare type UseFieldPresentationOptions = {
1320
+ error?: string | null;
1321
+ suppressError?: boolean;
1322
+ fieldMeta?: FieldMeta;
1323
+ showErrorPolicy?: ShowErrorPolicy;
1324
+ /** When set and no error, included in aria-describedby */
1325
+ hintId?: string;
1326
+ idPrefix?: string;
1327
+ };
1328
+
1329
+ export declare function useFieldState(fieldMeta: FieldMeta | undefined, policy?: ShowErrorPolicy, explicitError?: string | null, suppressError?: boolean): {
1213
1330
  showError: boolean;
1214
1331
  errorMessage: string | null;
1215
1332
  showErrorByPolicy: boolean;
1216
1333
  };
1217
1334
 
1335
+ export declare function useFormFields<TValidation>(options?: UseFormFieldsOptions<TValidation>): {
1336
+ bindField: (path: string, value: unknown) => FieldBinding;
1337
+ markFieldTouched: (path: string) => void;
1338
+ markFieldDirty: (path: string) => void;
1339
+ markStepSubmitted: (step: number) => void;
1340
+ markFormSubmitted: () => void;
1341
+ resetInteraction: () => void;
1342
+ formSubmitted: boolean;
1343
+ submittedSteps: Set<number>;
1344
+ showErrorPolicy: ShowErrorPolicy;
1345
+ };
1346
+
1347
+ export declare type UseFormFieldsOptions<TValidation> = {
1348
+ showErrorPolicy?: ShowErrorPolicy;
1349
+ validation?: TValidation;
1350
+ getFieldError?: (validation: TValidation | undefined, path: string) => string | null;
1351
+ getFieldErrorKind?: (validation: TValidation | undefined, path: string) => FieldErrorKind | undefined;
1352
+ /** Maps field path to wizard step index for stepSubmitted. */
1353
+ stepForField?: (path: string) => number | undefined;
1354
+ validationPending?: boolean;
1355
+ };
1356
+
1218
1357
  export declare const useMeasureElement: (element?: HTMLElement | null) => {
1219
1358
  height: number;
1220
1359
  width: number;
@@ -1222,6 +1361,15 @@ export declare const useMeasureElement: (element?: HTMLElement | null) => {
1222
1361
 
1223
1362
  export { UserSwitchIcon }
1224
1363
 
1364
+ export declare function useValidationRequest<T>(validateFn: () => Promise<T>): {
1365
+ validate: () => Promise<T>;
1366
+ validationPending: boolean;
1367
+ validationGeneration: number;
1368
+ isStale: (generation: number) => boolean;
1369
+ };
1370
+
1371
+ export declare type ValidationRequestApi<T> = ReturnType<typeof useValidationRequest<T>>;
1372
+
1225
1373
  /** Picker controls: canonical value callback + deprecated alias. */
1226
1374
  export declare type ValueFieldCallbacks<T> = {
1227
1375
  onValueChange?: (value: T) => void;