@wordpress/dataviews 14.1.1-next.v.202604091042.0 → 14.2.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 (179) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/build/components/dataform-controls/array.cjs +2 -2
  3. package/build/components/dataform-controls/array.cjs.map +2 -2
  4. package/build/components/dataform-controls/date.cjs +16 -5
  5. package/build/components/dataform-controls/date.cjs.map +3 -3
  6. package/build/components/dataform-controls/datetime.cjs +6 -2
  7. package/build/components/dataform-controls/datetime.cjs.map +3 -3
  8. package/build/components/dataform-controls/utils/use-disabled-date-matchers.cjs +48 -0
  9. package/build/components/dataform-controls/utils/use-disabled-date-matchers.cjs.map +7 -0
  10. package/build/components/dataform-layouts/card/index.cjs.map +2 -2
  11. package/build/components/dataform-layouts/panel/summary-button.cjs +0 -1
  12. package/build/components/dataform-layouts/panel/summary-button.cjs.map +2 -2
  13. package/build/components/dataviews-context/index.cjs.map +1 -1
  14. package/build/components/dataviews-filters/search-widget.cjs +2 -7
  15. package/build/components/dataviews-filters/search-widget.cjs.map +2 -2
  16. package/build/components/dataviews-layouts/activity/activity-item.cjs +2 -3
  17. package/build/components/dataviews-layouts/activity/activity-item.cjs.map +2 -2
  18. package/build/components/dataviews-layouts/grid/composite-grid.cjs +2 -2
  19. package/build/components/dataviews-layouts/grid/composite-grid.cjs.map +2 -2
  20. package/build/components/dataviews-layouts/list/index.cjs +2 -2
  21. package/build/components/dataviews-layouts/list/index.cjs.map +2 -2
  22. package/build/components/dataviews-layouts/picker-grid/index.cjs +3 -6
  23. package/build/components/dataviews-layouts/picker-grid/index.cjs.map +2 -2
  24. package/build/components/dataviews-layouts/picker-table/index.cjs +15 -12
  25. package/build/components/dataviews-layouts/picker-table/index.cjs.map +2 -2
  26. package/build/components/dataviews-layouts/table/index.cjs +0 -1
  27. package/build/components/dataviews-layouts/table/index.cjs.map +2 -2
  28. package/build/dataviews/index.cjs +10 -8
  29. package/build/dataviews/index.cjs.map +2 -2
  30. package/build/dataviews-picker/index.cjs +16 -9
  31. package/build/dataviews-picker/index.cjs.map +2 -2
  32. package/build/field-types/date.cjs +4 -1
  33. package/build/field-types/date.cjs.map +2 -2
  34. package/build/field-types/datetime.cjs +4 -1
  35. package/build/field-types/datetime.cjs.map +2 -2
  36. package/build/field-types/utils/get-is-valid.cjs +29 -24
  37. package/build/field-types/utils/get-is-valid.cjs.map +2 -2
  38. package/build/field-types/utils/is-valid-date-boundary.cjs +64 -0
  39. package/build/field-types/utils/is-valid-date-boundary.cjs.map +7 -0
  40. package/build/types/dataviews.cjs.map +1 -1
  41. package/build/types/field-api.cjs.map +1 -1
  42. package/build-module/components/dataform-controls/array.mjs +2 -2
  43. package/build-module/components/dataform-controls/array.mjs.map +2 -2
  44. package/build-module/components/dataform-controls/date.mjs +16 -5
  45. package/build-module/components/dataform-controls/date.mjs.map +2 -2
  46. package/build-module/components/dataform-controls/datetime.mjs +6 -2
  47. package/build-module/components/dataform-controls/datetime.mjs.map +2 -2
  48. package/build-module/components/dataform-controls/utils/use-disabled-date-matchers.mjs +27 -0
  49. package/build-module/components/dataform-controls/utils/use-disabled-date-matchers.mjs.map +7 -0
  50. package/build-module/components/dataform-layouts/card/index.mjs.map +2 -2
  51. package/build-module/components/dataform-layouts/panel/summary-button.mjs +0 -1
  52. package/build-module/components/dataform-layouts/panel/summary-button.mjs.map +2 -2
  53. package/build-module/components/dataviews-context/index.mjs.map +1 -1
  54. package/build-module/components/dataviews-filters/search-widget.mjs +3 -13
  55. package/build-module/components/dataviews-filters/search-widget.mjs.map +2 -2
  56. package/build-module/components/dataviews-layouts/activity/activity-item.mjs +2 -3
  57. package/build-module/components/dataviews-layouts/activity/activity-item.mjs.map +2 -2
  58. package/build-module/components/dataviews-layouts/grid/composite-grid.mjs +2 -2
  59. package/build-module/components/dataviews-layouts/grid/composite-grid.mjs.map +2 -2
  60. package/build-module/components/dataviews-layouts/list/index.mjs +2 -3
  61. package/build-module/components/dataviews-layouts/list/index.mjs.map +2 -2
  62. package/build-module/components/dataviews-layouts/picker-grid/index.mjs +3 -6
  63. package/build-module/components/dataviews-layouts/picker-grid/index.mjs.map +2 -2
  64. package/build-module/components/dataviews-layouts/picker-table/index.mjs +15 -12
  65. package/build-module/components/dataviews-layouts/picker-table/index.mjs.map +2 -2
  66. package/build-module/components/dataviews-layouts/table/index.mjs +0 -1
  67. package/build-module/components/dataviews-layouts/table/index.mjs.map +2 -2
  68. package/build-module/dataviews/index.mjs +10 -8
  69. package/build-module/dataviews/index.mjs.map +2 -2
  70. package/build-module/dataviews-picker/index.mjs +16 -9
  71. package/build-module/dataviews-picker/index.mjs.map +2 -2
  72. package/build-module/field-types/date.mjs +4 -1
  73. package/build-module/field-types/date.mjs.map +2 -2
  74. package/build-module/field-types/datetime.mjs +4 -1
  75. package/build-module/field-types/datetime.mjs.map +2 -2
  76. package/build-module/field-types/utils/get-is-valid.mjs +29 -24
  77. package/build-module/field-types/utils/get-is-valid.mjs.map +2 -2
  78. package/build-module/field-types/utils/is-valid-date-boundary.mjs +38 -0
  79. package/build-module/field-types/utils/is-valid-date-boundary.mjs.map +7 -0
  80. package/build-style/style-rtl.css +12 -13
  81. package/build-style/style.css +12 -13
  82. package/build-types/components/dataform-controls/array.d.ts.map +1 -1
  83. package/build-types/components/dataform-controls/date.d.ts.map +1 -1
  84. package/build-types/components/dataform-controls/datetime.d.ts.map +1 -1
  85. package/build-types/components/dataform-controls/utils/use-disabled-date-matchers.d.ts +16 -0
  86. package/build-types/components/dataform-controls/utils/use-disabled-date-matchers.d.ts.map +1 -0
  87. package/build-types/components/dataform-layouts/card/index.d.ts.map +1 -1
  88. package/build-types/components/dataform-layouts/panel/summary-button.d.ts.map +1 -1
  89. package/build-types/components/dataviews-context/index.d.ts +2 -2
  90. package/build-types/components/dataviews-context/index.d.ts.map +1 -1
  91. package/build-types/components/dataviews-filters/search-widget.d.ts.map +1 -1
  92. package/build-types/components/dataviews-layouts/activity/activity-item.d.ts.map +1 -1
  93. package/build-types/components/dataviews-layouts/list/index.d.ts.map +1 -1
  94. package/build-types/components/dataviews-layouts/picker-grid/index.d.ts.map +1 -1
  95. package/build-types/components/dataviews-layouts/picker-table/index.d.ts.map +1 -1
  96. package/build-types/components/dataviews-layouts/table/index.d.ts.map +1 -1
  97. package/build-types/dataform/stories/index.story.d.ts.map +1 -1
  98. package/build-types/dataform/stories/layout-regular.d.ts.map +1 -1
  99. package/build-types/dataform/stories/validation.d.ts.map +1 -1
  100. package/build-types/dataviews/index.d.ts +1 -1
  101. package/build-types/dataviews/index.d.ts.map +1 -1
  102. package/build-types/dataviews/stories/free-composition.d.ts.map +1 -1
  103. package/build-types/dataviews/stories/index.story.d.ts.map +1 -1
  104. package/build-types/dataviews/stories/layout-activity.d.ts.map +1 -1
  105. package/build-types/dataviews/stories/layout-grid.d.ts.map +1 -1
  106. package/build-types/dataviews/stories/layout-list.d.ts.map +1 -1
  107. package/build-types/dataviews/stories/layout-table.d.ts.map +1 -1
  108. package/build-types/dataviews/stories/with-card.d.ts.map +1 -1
  109. package/build-types/dataviews-picker/index.d.ts +3 -2
  110. package/build-types/dataviews-picker/index.d.ts.map +1 -1
  111. package/build-types/dataviews-picker/stories/index.story.d.ts.map +1 -1
  112. package/build-types/field-types/date.d.ts +3 -0
  113. package/build-types/field-types/date.d.ts.map +1 -1
  114. package/build-types/field-types/datetime.d.ts +3 -0
  115. package/build-types/field-types/datetime.d.ts.map +1 -1
  116. package/build-types/field-types/utils/get-is-valid.d.ts.map +1 -1
  117. package/build-types/field-types/utils/is-valid-date-boundary.d.ts +7 -0
  118. package/build-types/field-types/utils/is-valid-date-boundary.d.ts.map +1 -0
  119. package/build-types/types/dataviews.d.ts +8 -0
  120. package/build-types/types/dataviews.d.ts.map +1 -1
  121. package/build-types/types/field-api.d.ts +11 -9
  122. package/build-types/types/field-api.d.ts.map +1 -1
  123. package/build-wp/index.js +1173 -1017
  124. package/package.json +16 -16
  125. package/src/components/dataform-controls/array.tsx +3 -2
  126. package/src/components/dataform-controls/date.tsx +17 -2
  127. package/src/components/dataform-controls/datetime.tsx +15 -1
  128. package/src/components/dataform-controls/utils/use-disabled-date-matchers.ts +48 -0
  129. package/src/components/dataform-layouts/card/index.tsx +0 -3
  130. package/src/components/dataform-layouts/panel/style.scss +4 -5
  131. package/src/components/dataform-layouts/panel/summary-button.tsx +0 -1
  132. package/src/components/dataviews-context/index.ts +2 -2
  133. package/src/components/dataviews-filters/search-widget.tsx +4 -14
  134. package/src/components/dataviews-filters/style.scss +2 -2
  135. package/src/components/dataviews-layouts/activity/activity-item.tsx +2 -3
  136. package/src/components/dataviews-layouts/activity/style.scss +1 -1
  137. package/src/components/dataviews-layouts/grid/composite-grid.tsx +3 -3
  138. package/src/components/dataviews-layouts/grid/style.scss +1 -1
  139. package/src/components/dataviews-layouts/list/index.tsx +2 -3
  140. package/src/components/dataviews-layouts/list/style.scss +1 -1
  141. package/src/components/dataviews-layouts/picker-grid/index.tsx +5 -9
  142. package/src/components/dataviews-layouts/picker-grid/style.scss +1 -1
  143. package/src/components/dataviews-layouts/picker-table/index.tsx +9 -7
  144. package/src/components/dataviews-layouts/picker-table/style.scss +1 -1
  145. package/src/components/dataviews-layouts/table/index.tsx +0 -2
  146. package/src/dataform/stories/content.story.tsx +1 -1
  147. package/src/dataform/stories/data-adapter.tsx +6 -6
  148. package/src/dataform/stories/layout-card.tsx +8 -8
  149. package/src/dataform/stories/layout-details.tsx +5 -5
  150. package/src/dataform/stories/layout-panel.tsx +9 -9
  151. package/src/dataform/stories/layout-regular.tsx +16 -9
  152. package/src/dataform/stories/layout-row.tsx +9 -9
  153. package/src/dataform/stories/validation.tsx +25 -10
  154. package/src/dataviews/index.tsx +11 -7
  155. package/src/dataviews/stories/empty.tsx +6 -6
  156. package/src/dataviews/stories/fixtures.tsx +2 -2
  157. package/src/dataviews/stories/free-composition.tsx +10 -13
  158. package/src/dataviews/stories/infinite-scroll.tsx +4 -4
  159. package/src/dataviews/stories/layout-activity.tsx +7 -9
  160. package/src/dataviews/stories/layout-custom.tsx +1 -1
  161. package/src/dataviews/stories/layout-grid.tsx +5 -7
  162. package/src/dataviews/stories/layout-list.tsx +6 -8
  163. package/src/dataviews/stories/layout-table.tsx +5 -7
  164. package/src/dataviews/stories/minimal-ui.tsx +1 -1
  165. package/src/dataviews/stories/with-card.tsx +4 -7
  166. package/src/dataviews/style.scss +1 -1
  167. package/src/dataviews/test/dataviews.tsx +73 -6
  168. package/src/dataviews-picker/index.tsx +17 -7
  169. package/src/dataviews-picker/stories/index.story.tsx +1 -5
  170. package/src/dataviews-picker/test/dataviews-picker.tsx +79 -2
  171. package/src/field-types/date.tsx +3 -0
  172. package/src/field-types/datetime.tsx +3 -0
  173. package/src/field-types/stories/index.story.tsx +1 -1
  174. package/src/field-types/test/normalize-fields.ts +44 -0
  175. package/src/field-types/utils/get-is-valid.ts +44 -31
  176. package/src/field-types/utils/is-valid-date-boundary.ts +80 -0
  177. package/src/hooks/test/use-form-validity.ts +479 -0
  178. package/src/types/dataviews.ts +9 -0
  179. package/src/types/field-api.ts +11 -9
@@ -4,13 +4,42 @@
4
4
  import type { Field, NormalizedRules } from '../../types';
5
5
  import type { FieldType } from '../../types/private';
6
6
 
7
+ function supportsNumericRangeConstraint( type?: string ) {
8
+ return type === 'integer' || type === 'number';
9
+ }
10
+
11
+ function supportsDateRangeConstraint( type?: string ) {
12
+ return type === 'date' || type === 'datetime';
13
+ }
14
+
15
+ function normalizeRangeRule< Item >(
16
+ value: number | string | undefined,
17
+ fieldType: FieldType< Item >,
18
+ key: 'min' | 'max'
19
+ ): NormalizedRules< Item >[ 'min' ] {
20
+ const validator = fieldType.validate[ key ];
21
+ if (
22
+ validator &&
23
+ ( ( typeof value === 'number' &&
24
+ supportsNumericRangeConstraint( fieldType.type ) ) ||
25
+ ( typeof value === 'string' &&
26
+ supportsDateRangeConstraint( fieldType.type ) ) )
27
+ ) {
28
+ return { constraint: value, validate: validator } as NonNullable<
29
+ NormalizedRules< Item >[ typeof key ]
30
+ >;
31
+ }
32
+ return undefined;
33
+ }
34
+
7
35
  export default function getIsValid< Item >(
8
36
  field: Field< Item >,
9
37
  fieldType: FieldType< Item >
10
38
  ): NormalizedRules< Item > {
39
+ const rules = field.isValid;
11
40
  let required;
12
41
  if (
13
- field.isValid?.required === true &&
42
+ rules?.required === true &&
14
43
  fieldType.validate.required !== undefined
15
44
  ) {
16
45
  required = {
@@ -21,9 +50,9 @@ export default function getIsValid< Item >(
21
50
 
22
51
  let elements;
23
52
  if (
24
- ( field.isValid?.elements === true ||
53
+ ( rules?.elements === true ||
25
54
  // elements is enabled unless the field opts-out
26
- ( field.isValid?.elements === undefined &&
55
+ ( rules?.elements === undefined &&
27
56
  ( !! field.elements || !! field.getElements ) ) ) &&
28
57
  fieldType.validate.elements !== undefined
29
58
  ) {
@@ -33,62 +62,46 @@ export default function getIsValid< Item >(
33
62
  };
34
63
  }
35
64
 
36
- let min;
37
- if (
38
- typeof field.isValid?.min === 'number' &&
39
- fieldType.validate.min !== undefined
40
- ) {
41
- min = {
42
- constraint: field.isValid.min,
43
- validate: fieldType.validate.min,
44
- };
45
- }
46
-
47
- let max;
48
- if (
49
- typeof field.isValid?.max === 'number' &&
50
- fieldType.validate.max !== undefined
51
- ) {
52
- max = {
53
- constraint: field.isValid.max,
54
- validate: fieldType.validate.max,
55
- };
56
- }
65
+ const min = normalizeRangeRule( rules?.min, fieldType, 'min' );
66
+ const max = normalizeRangeRule( rules?.max, fieldType, 'max' );
57
67
 
68
+ const minLengthValue = rules?.minLength;
58
69
  let minLength;
59
70
  if (
60
- typeof field.isValid?.minLength === 'number' &&
71
+ typeof minLengthValue === 'number' &&
61
72
  fieldType.validate.minLength !== undefined
62
73
  ) {
63
74
  minLength = {
64
- constraint: field.isValid.minLength,
75
+ constraint: minLengthValue,
65
76
  validate: fieldType.validate.minLength,
66
77
  };
67
78
  }
68
79
 
80
+ const maxLengthValue = rules?.maxLength;
69
81
  let maxLength;
70
82
  if (
71
- typeof field.isValid?.maxLength === 'number' &&
83
+ typeof maxLengthValue === 'number' &&
72
84
  fieldType.validate.maxLength !== undefined
73
85
  ) {
74
86
  maxLength = {
75
- constraint: field.isValid.maxLength,
87
+ constraint: maxLengthValue,
76
88
  validate: fieldType.validate.maxLength,
77
89
  };
78
90
  }
79
91
 
92
+ const patternValue = rules?.pattern;
80
93
  let pattern;
81
94
  if (
82
- field.isValid?.pattern !== undefined &&
95
+ patternValue !== undefined &&
83
96
  fieldType.validate.pattern !== undefined
84
97
  ) {
85
98
  pattern = {
86
- constraint: field.isValid?.pattern,
99
+ constraint: patternValue,
87
100
  validate: fieldType.validate.pattern,
88
101
  };
89
102
  }
90
103
 
91
- const custom = field.isValid?.custom ?? fieldType.validate.custom;
104
+ const custom = rules?.custom ?? fieldType.validate.custom;
92
105
 
93
106
  return {
94
107
  required,
@@ -0,0 +1,80 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { isValid as isValidDate } from 'date-fns';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { getDate } from '@wordpress/date';
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import type { NormalizedField } from '../../types';
15
+
16
+ type Boundary = 'min' | 'max';
17
+
18
+ function parseDateLike( value?: string ) {
19
+ if ( ! value ) {
20
+ return null;
21
+ }
22
+
23
+ // Pre-check to avoid passing unparseable strings to getDate,
24
+ // which uses moment.js and emits deprecation warnings.
25
+ if ( ! isValidDate( new Date( value ) ) ) {
26
+ return null;
27
+ }
28
+
29
+ const parsed = getDate( value );
30
+ return parsed && isValidDate( parsed ) ? parsed : null;
31
+ }
32
+
33
+ function validateDateLikeBoundary< Item >(
34
+ item: Item,
35
+ field: NormalizedField< Item >,
36
+ boundary: Boundary
37
+ ): boolean {
38
+ const constraint = field.isValid[ boundary ]?.constraint;
39
+ if ( typeof constraint !== 'string' ) {
40
+ return false;
41
+ }
42
+
43
+ const value = field.getValue( { item } );
44
+ const boundaryValue = Array.isArray( value )
45
+ ? value[ boundary === 'min' ? 0 : value.length - 1 ]
46
+ : value;
47
+
48
+ if (
49
+ boundaryValue === undefined ||
50
+ boundaryValue === null ||
51
+ boundaryValue === ''
52
+ ) {
53
+ return true;
54
+ }
55
+
56
+ const parsedConstraint = parseDateLike( constraint );
57
+ const parsedValue = parseDateLike( String( boundaryValue ) );
58
+
59
+ return (
60
+ !! parsedConstraint &&
61
+ !! parsedValue &&
62
+ ( boundary === 'min'
63
+ ? parsedValue.getTime() >= parsedConstraint.getTime()
64
+ : parsedValue.getTime() <= parsedConstraint.getTime() )
65
+ );
66
+ }
67
+
68
+ export function isValidMinDate< Item >(
69
+ item: Item,
70
+ field: NormalizedField< Item >
71
+ ): boolean {
72
+ return validateDateLikeBoundary( item, field, 'min' );
73
+ }
74
+
75
+ export function isValidMaxDate< Item >(
76
+ item: Item,
77
+ field: NormalizedField< Item >
78
+ ): boolean {
79
+ return validateDateLikeBoundary( item, field, 'max' );
80
+ }
@@ -1039,6 +1039,485 @@ describe( 'useFormValidity', () => {
1039
1039
  } );
1040
1040
  } );
1041
1041
 
1042
+ describe( 'isValid.min (date)', () => {
1043
+ const MIN_MESSAGE = {
1044
+ min: {
1045
+ type: 'invalid',
1046
+ message: 'Value is below the minimum.',
1047
+ },
1048
+ };
1049
+
1050
+ it( 'date is valid when value is at min', () => {
1051
+ const item = { id: 1, eventDate: '2026-04-01' };
1052
+ const fields: Field< {} >[] = [
1053
+ {
1054
+ id: 'eventDate',
1055
+ type: 'date',
1056
+ isValid: {
1057
+ min: '2026-04-01',
1058
+ },
1059
+ },
1060
+ ];
1061
+ const form = { fields: [ 'eventDate' ] };
1062
+ const {
1063
+ result: {
1064
+ current: { validity, isValid },
1065
+ },
1066
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1067
+ expect( validity ).toEqual( undefined );
1068
+ expect( isValid ).toBe( true );
1069
+ } );
1070
+
1071
+ it( 'date is invalid when value is before min', () => {
1072
+ const item = { id: 1, eventDate: '2026-03-15' };
1073
+ const fields: Field< {} >[] = [
1074
+ {
1075
+ id: 'eventDate',
1076
+ type: 'date',
1077
+ isValid: {
1078
+ min: '2026-04-01',
1079
+ },
1080
+ },
1081
+ ];
1082
+ const form = { fields: [ 'eventDate' ] };
1083
+ const {
1084
+ result: {
1085
+ current: { validity, isValid },
1086
+ },
1087
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1088
+ expect( validity?.eventDate ).toEqual( MIN_MESSAGE );
1089
+ expect( isValid ).toBe( false );
1090
+ } );
1091
+
1092
+ it( 'date is valid when value is empty and min is defined', () => {
1093
+ const item = { id: 1, eventDate: undefined };
1094
+ const fields: Field< {} >[] = [
1095
+ {
1096
+ id: 'eventDate',
1097
+ type: 'date',
1098
+ isValid: {
1099
+ min: '2026-04-01',
1100
+ },
1101
+ },
1102
+ ];
1103
+ const form = { fields: [ 'eventDate' ] };
1104
+ const {
1105
+ result: {
1106
+ current: { validity, isValid },
1107
+ },
1108
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1109
+ expect( validity ).toEqual( undefined );
1110
+ expect( isValid ).toBe( true );
1111
+ } );
1112
+
1113
+ it( 'date range: from date is validated against min', () => {
1114
+ const item = {
1115
+ id: 1,
1116
+ dateRange: [ '2026-03-15', '2026-04-10' ],
1117
+ };
1118
+ const fields: Field< {} >[] = [
1119
+ {
1120
+ id: 'dateRange',
1121
+ type: 'date',
1122
+ isValid: {
1123
+ min: '2026-04-01',
1124
+ },
1125
+ },
1126
+ ];
1127
+ const form = { fields: [ 'dateRange' ] };
1128
+ const {
1129
+ result: {
1130
+ current: { validity, isValid },
1131
+ },
1132
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1133
+ expect( validity?.dateRange ).toEqual( MIN_MESSAGE );
1134
+ expect( isValid ).toBe( false );
1135
+ } );
1136
+ } );
1137
+
1138
+ describe( 'isValid.max (date)', () => {
1139
+ const MAX_MESSAGE = {
1140
+ max: {
1141
+ type: 'invalid',
1142
+ message: 'Value is above the maximum.',
1143
+ },
1144
+ };
1145
+
1146
+ it( 'date is valid when value is at max', () => {
1147
+ const item = { id: 1, eventDate: '2026-04-20' };
1148
+ const fields: Field< {} >[] = [
1149
+ {
1150
+ id: 'eventDate',
1151
+ type: 'date',
1152
+ isValid: {
1153
+ max: '2026-04-20',
1154
+ },
1155
+ },
1156
+ ];
1157
+ const form = { fields: [ 'eventDate' ] };
1158
+ const {
1159
+ result: {
1160
+ current: { validity, isValid },
1161
+ },
1162
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1163
+ expect( validity ).toEqual( undefined );
1164
+ expect( isValid ).toBe( true );
1165
+ } );
1166
+
1167
+ it( 'date is invalid when value is after max', () => {
1168
+ const item = { id: 1, eventDate: '2026-05-01' };
1169
+ const fields: Field< {} >[] = [
1170
+ {
1171
+ id: 'eventDate',
1172
+ type: 'date',
1173
+ isValid: {
1174
+ max: '2026-04-20',
1175
+ },
1176
+ },
1177
+ ];
1178
+ const form = { fields: [ 'eventDate' ] };
1179
+ const {
1180
+ result: {
1181
+ current: { validity, isValid },
1182
+ },
1183
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1184
+ expect( validity?.eventDate ).toEqual( MAX_MESSAGE );
1185
+ expect( isValid ).toBe( false );
1186
+ } );
1187
+
1188
+ it( 'date range: to date is validated against max', () => {
1189
+ const item = {
1190
+ id: 1,
1191
+ dateRange: [ '2026-04-05', '2026-04-25' ],
1192
+ };
1193
+ const fields: Field< {} >[] = [
1194
+ {
1195
+ id: 'dateRange',
1196
+ type: 'date',
1197
+ isValid: {
1198
+ max: '2026-04-20',
1199
+ },
1200
+ },
1201
+ ];
1202
+ const form = { fields: [ 'dateRange' ] };
1203
+ const {
1204
+ result: {
1205
+ current: { validity, isValid },
1206
+ },
1207
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1208
+ expect( validity?.dateRange ).toEqual( MAX_MESSAGE );
1209
+ expect( isValid ).toBe( false );
1210
+ } );
1211
+ } );
1212
+
1213
+ describe( 'isValid.min (datetime)', () => {
1214
+ const MIN_MESSAGE = {
1215
+ min: {
1216
+ type: 'invalid',
1217
+ message: 'Value is below the minimum.',
1218
+ },
1219
+ };
1220
+
1221
+ it( 'datetime is valid when value is at min', () => {
1222
+ const item = {
1223
+ id: 1,
1224
+ createdAt: '2026-04-01T10:00:00.000Z',
1225
+ };
1226
+ const fields: Field< {} >[] = [
1227
+ {
1228
+ id: 'createdAt',
1229
+ type: 'datetime',
1230
+ isValid: {
1231
+ min: '2026-04-01T10:00:00.000Z',
1232
+ },
1233
+ },
1234
+ ];
1235
+ const form = { fields: [ 'createdAt' ] };
1236
+ const {
1237
+ result: {
1238
+ current: { validity, isValid },
1239
+ },
1240
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1241
+ expect( validity ).toEqual( undefined );
1242
+ expect( isValid ).toBe( true );
1243
+ } );
1244
+
1245
+ it( 'datetime is invalid when value is before min', () => {
1246
+ const item = {
1247
+ id: 1,
1248
+ createdAt: '2026-03-31T23:59:59.000Z',
1249
+ };
1250
+ const fields: Field< {} >[] = [
1251
+ {
1252
+ id: 'createdAt',
1253
+ type: 'datetime',
1254
+ isValid: {
1255
+ min: '2026-04-01T10:00:00.000Z',
1256
+ },
1257
+ },
1258
+ ];
1259
+ const form = { fields: [ 'createdAt' ] };
1260
+ const {
1261
+ result: {
1262
+ current: { validity, isValid },
1263
+ },
1264
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1265
+ expect( validity?.createdAt ).toEqual( MIN_MESSAGE );
1266
+ expect( isValid ).toBe( false );
1267
+ } );
1268
+
1269
+ it( 'datetime is valid when min uses an offset-based ISO string', () => {
1270
+ const item = {
1271
+ id: 1,
1272
+ createdAt: '2026-04-01T09:30:00.000Z',
1273
+ };
1274
+ const fields: Field< {} >[] = [
1275
+ {
1276
+ id: 'createdAt',
1277
+ type: 'datetime',
1278
+ isValid: {
1279
+ min: '2026-04-01T10:00:00+02:00',
1280
+ },
1281
+ },
1282
+ ];
1283
+ const form = { fields: [ 'createdAt' ] };
1284
+ const {
1285
+ result: {
1286
+ current: { validity, isValid },
1287
+ },
1288
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1289
+ expect( validity ).toEqual( undefined );
1290
+ expect( isValid ).toBe( true );
1291
+ } );
1292
+ } );
1293
+
1294
+ describe( 'isValid.max (datetime)', () => {
1295
+ const MAX_MESSAGE = {
1296
+ max: {
1297
+ type: 'invalid',
1298
+ message: 'Value is above the maximum.',
1299
+ },
1300
+ };
1301
+
1302
+ it( 'datetime is valid when value is at max', () => {
1303
+ const item = {
1304
+ id: 1,
1305
+ createdAt: '2026-04-30T23:59:59.000Z',
1306
+ };
1307
+ const fields: Field< {} >[] = [
1308
+ {
1309
+ id: 'createdAt',
1310
+ type: 'datetime',
1311
+ isValid: {
1312
+ max: '2026-04-30T23:59:59.000Z',
1313
+ },
1314
+ },
1315
+ ];
1316
+ const form = { fields: [ 'createdAt' ] };
1317
+ const {
1318
+ result: {
1319
+ current: { validity, isValid },
1320
+ },
1321
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1322
+ expect( validity ).toEqual( undefined );
1323
+ expect( isValid ).toBe( true );
1324
+ } );
1325
+
1326
+ it( 'datetime is invalid when value is after max', () => {
1327
+ const item = {
1328
+ id: 1,
1329
+ createdAt: '2026-05-01T00:00:00.000Z',
1330
+ };
1331
+ const fields: Field< {} >[] = [
1332
+ {
1333
+ id: 'createdAt',
1334
+ type: 'datetime',
1335
+ isValid: {
1336
+ max: '2026-04-30T23:59:59.000Z',
1337
+ },
1338
+ },
1339
+ ];
1340
+ const form = { fields: [ 'createdAt' ] };
1341
+ const {
1342
+ result: {
1343
+ current: { validity, isValid },
1344
+ },
1345
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1346
+ expect( validity?.createdAt ).toEqual( MAX_MESSAGE );
1347
+ expect( isValid ).toBe( false );
1348
+ } );
1349
+
1350
+ it( 'datetime is invalid when max uses an offset-based ISO string', () => {
1351
+ const item = {
1352
+ id: 1,
1353
+ createdAt: '2026-04-01T09:30:00.000Z',
1354
+ };
1355
+ const fields: Field< {} >[] = [
1356
+ {
1357
+ id: 'createdAt',
1358
+ type: 'datetime',
1359
+ isValid: {
1360
+ max: '2026-04-01T10:00:00+02:00',
1361
+ },
1362
+ },
1363
+ ];
1364
+ const form = { fields: [ 'createdAt' ] };
1365
+ const {
1366
+ result: {
1367
+ current: { validity, isValid },
1368
+ },
1369
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1370
+ expect( validity?.createdAt ).toEqual( MAX_MESSAGE );
1371
+ expect( isValid ).toBe( false );
1372
+ } );
1373
+
1374
+ it( 'datetime is invalid when value cannot be parsed', () => {
1375
+ const item = {
1376
+ id: 1,
1377
+ createdAt: 'not-a-date',
1378
+ };
1379
+ const fields: Field< {} >[] = [
1380
+ {
1381
+ id: 'createdAt',
1382
+ type: 'datetime',
1383
+ isValid: {
1384
+ max: '2026-04-30T23:59:59.000Z',
1385
+ },
1386
+ },
1387
+ ];
1388
+ const form = { fields: [ 'createdAt' ] };
1389
+ const {
1390
+ result: {
1391
+ current: { validity, isValid },
1392
+ },
1393
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1394
+ expect( validity?.createdAt ).toEqual( MAX_MESSAGE );
1395
+ expect( isValid ).toBe( false );
1396
+ } );
1397
+ } );
1398
+
1399
+ describe( 'isValid combined min and max (date)', () => {
1400
+ it( 'date is valid when value is within range', () => {
1401
+ const item = { id: 1, eventDate: '2026-04-10' };
1402
+ const fields: Field< {} >[] = [
1403
+ {
1404
+ id: 'eventDate',
1405
+ type: 'date',
1406
+ isValid: {
1407
+ min: '2026-04-01',
1408
+ max: '2026-04-30',
1409
+ },
1410
+ },
1411
+ ];
1412
+ const form = { fields: [ 'eventDate' ] };
1413
+ const {
1414
+ result: {
1415
+ current: { validity, isValid },
1416
+ },
1417
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1418
+ expect( validity ).toEqual( undefined );
1419
+ expect( isValid ).toBe( true );
1420
+ } );
1421
+
1422
+ it( 'date is invalid when value is below min with both min and max', () => {
1423
+ const item = { id: 1, eventDate: '2026-03-15' };
1424
+ const fields: Field< {} >[] = [
1425
+ {
1426
+ id: 'eventDate',
1427
+ type: 'date',
1428
+ isValid: {
1429
+ min: '2026-04-01',
1430
+ max: '2026-04-30',
1431
+ },
1432
+ },
1433
+ ];
1434
+ const form = { fields: [ 'eventDate' ] };
1435
+ const {
1436
+ result: {
1437
+ current: { validity, isValid },
1438
+ },
1439
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1440
+ expect( validity?.eventDate ).toEqual( {
1441
+ min: {
1442
+ type: 'invalid',
1443
+ message: 'Value is below the minimum.',
1444
+ },
1445
+ } );
1446
+ expect( isValid ).toBe( false );
1447
+ } );
1448
+
1449
+ it( 'date is invalid when value is above max with both min and max', () => {
1450
+ const item = { id: 1, eventDate: '2026-05-15' };
1451
+ const fields: Field< {} >[] = [
1452
+ {
1453
+ id: 'eventDate',
1454
+ type: 'date',
1455
+ isValid: {
1456
+ min: '2026-04-01',
1457
+ max: '2026-04-30',
1458
+ },
1459
+ },
1460
+ ];
1461
+ const form = { fields: [ 'eventDate' ] };
1462
+ const {
1463
+ result: {
1464
+ current: { validity, isValid },
1465
+ },
1466
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1467
+ expect( validity?.eventDate ).toEqual( {
1468
+ max: {
1469
+ type: 'invalid',
1470
+ message: 'Value is above the maximum.',
1471
+ },
1472
+ } );
1473
+ expect( isValid ).toBe( false );
1474
+ } );
1475
+ } );
1476
+
1477
+ describe( 'isValid empty array (date)', () => {
1478
+ it( 'empty array is valid for min validation', () => {
1479
+ const item = { id: 1, dateRange: [] as string[] };
1480
+ const fields: Field< {} >[] = [
1481
+ {
1482
+ id: 'dateRange',
1483
+ type: 'date',
1484
+ isValid: {
1485
+ min: '2026-04-01',
1486
+ },
1487
+ },
1488
+ ];
1489
+ const form = { fields: [ 'dateRange' ] };
1490
+ const {
1491
+ result: {
1492
+ current: { validity, isValid },
1493
+ },
1494
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1495
+ expect( validity ).toEqual( undefined );
1496
+ expect( isValid ).toBe( true );
1497
+ } );
1498
+
1499
+ it( 'empty array is valid for max validation', () => {
1500
+ const item = { id: 1, dateRange: [] as string[] };
1501
+ const fields: Field< {} >[] = [
1502
+ {
1503
+ id: 'dateRange',
1504
+ type: 'date',
1505
+ isValid: {
1506
+ max: '2026-04-30',
1507
+ },
1508
+ },
1509
+ ];
1510
+ const form = { fields: [ 'dateRange' ] };
1511
+ const {
1512
+ result: {
1513
+ current: { validity, isValid },
1514
+ },
1515
+ } = renderHook( () => useFormValidity( item, fields, form ) );
1516
+ expect( validity ).toEqual( undefined );
1517
+ expect( isValid ).toBe( true );
1518
+ } );
1519
+ } );
1520
+
1042
1521
  describe( 'isValid.minLength', () => {
1043
1522
  const MIN_LENGTH_MESSAGE = {
1044
1523
  minLength: {
@@ -527,6 +527,15 @@ export type ViewPickerProps< Item > =
527
527
  | ViewPickerTableProps< Item >;
528
528
 
529
529
  export interface SupportedLayouts {
530
+ list?: Omit< ViewList, 'type' > | true;
531
+ grid?: Omit< ViewGrid, 'type' > | true;
532
+ table?: Omit< ViewTable, 'type' > | true;
533
+ activity?: Omit< ViewActivity, 'type' > | true;
534
+ pickerGrid?: Omit< ViewPickerGrid, 'type' > | true;
535
+ pickerTable?: Omit< ViewPickerTable, 'type' > | true;
536
+ }
537
+
538
+ export interface NormalizedSupportedLayouts {
530
539
  list?: Omit< ViewList, 'type' >;
531
540
  grid?: Omit< ViewGrid, 'type' >;
532
541
  table?: Omit< ViewTable, 'type' >;