@wordpress/dataviews 6.0.0 → 7.0.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 (194) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +42 -14
  3. package/build/components/dataviews/index.js +38 -6
  4. package/build/components/dataviews/index.js.map +1 -1
  5. package/build/components/dataviews-context/index.js +4 -1
  6. package/build/components/dataviews-context/index.js.map +1 -1
  7. package/build/components/dataviews-item-actions/index.js +1 -10
  8. package/build/components/dataviews-item-actions/index.js.map +1 -1
  9. package/build/components/dataviews-pagination/index.js +1 -1
  10. package/build/components/dataviews-pagination/index.js.map +1 -1
  11. package/build/components/dataviews-view-config/index.js +8 -5
  12. package/build/components/dataviews-view-config/index.js.map +1 -1
  13. package/build/components/dataviews-view-config/infinite-scroll-toggle.js +47 -0
  14. package/build/components/dataviews-view-config/infinite-scroll-toggle.js.map +1 -0
  15. package/build/dataform-controls/array.js +70 -0
  16. package/build/dataform-controls/array.js.map +1 -0
  17. package/build/dataform-controls/boolean.js +15 -7
  18. package/build/dataform-controls/boolean.js.map +1 -1
  19. package/build/dataform-controls/email.js +14 -7
  20. package/build/dataform-controls/email.js.map +1 -1
  21. package/build/dataform-controls/index.js +3 -1
  22. package/build/dataform-controls/index.js.map +1 -1
  23. package/build/dataform-controls/integer.js +14 -7
  24. package/build/dataform-controls/integer.js.map +1 -1
  25. package/build/dataform-controls/text.js +14 -7
  26. package/build/dataform-controls/text.js.map +1 -1
  27. package/build/dataforms-layouts/card/index.js +137 -0
  28. package/build/dataforms-layouts/card/index.js.map +1 -0
  29. package/build/dataforms-layouts/data-form-layout.js +2 -2
  30. package/build/dataforms-layouts/data-form-layout.js.map +1 -1
  31. package/build/dataforms-layouts/index.js +4 -0
  32. package/build/dataforms-layouts/index.js.map +1 -1
  33. package/build/dataforms-layouts/panel/dropdown.js +124 -0
  34. package/build/dataforms-layouts/panel/dropdown.js.map +1 -0
  35. package/build/dataforms-layouts/panel/index.js +34 -149
  36. package/build/dataforms-layouts/panel/index.js.map +1 -1
  37. package/build/dataforms-layouts/panel/modal.js +125 -0
  38. package/build/dataforms-layouts/panel/modal.js.map +1 -0
  39. package/build/dataforms-layouts/regular/index.js +10 -21
  40. package/build/dataforms-layouts/regular/index.js.map +1 -1
  41. package/build/dataviews-layouts/grid/index.js +24 -7
  42. package/build/dataviews-layouts/grid/index.js.map +1 -1
  43. package/build/dataviews-layouts/grid/preview-size-picker.js +11 -11
  44. package/build/dataviews-layouts/grid/preview-size-picker.js.map +1 -1
  45. package/build/dataviews-layouts/list/index.js +45 -27
  46. package/build/dataviews-layouts/list/index.js.map +1 -1
  47. package/build/dataviews-layouts/table/column-header-menu.js +3 -0
  48. package/build/dataviews-layouts/table/column-header-menu.js.map +1 -1
  49. package/build/dataviews-layouts/table/index.js +23 -8
  50. package/build/dataviews-layouts/table/index.js.map +1 -1
  51. package/build/field-types/array.js +2 -2
  52. package/build/field-types/array.js.map +1 -1
  53. package/build/normalize-form-fields.js +52 -13
  54. package/build/normalize-form-fields.js.map +1 -1
  55. package/build/types.js.map +1 -1
  56. package/build-module/components/dataviews/index.js +40 -8
  57. package/build-module/components/dataviews/index.js.map +1 -1
  58. package/build-module/components/dataviews-context/index.js +4 -1
  59. package/build-module/components/dataviews-context/index.js.map +1 -1
  60. package/build-module/components/dataviews-item-actions/index.js +1 -10
  61. package/build-module/components/dataviews-item-actions/index.js.map +1 -1
  62. package/build-module/components/dataviews-pagination/index.js +1 -1
  63. package/build-module/components/dataviews-pagination/index.js.map +1 -1
  64. package/build-module/components/dataviews-view-config/index.js +8 -5
  65. package/build-module/components/dataviews-view-config/index.js.map +1 -1
  66. package/build-module/components/dataviews-view-config/infinite-scroll-toggle.js +39 -0
  67. package/build-module/components/dataviews-view-config/infinite-scroll-toggle.js.map +1 -0
  68. package/build-module/dataform-controls/array.js +63 -0
  69. package/build-module/dataform-controls/array.js.map +1 -0
  70. package/build-module/dataform-controls/boolean.js +15 -7
  71. package/build-module/dataform-controls/boolean.js.map +1 -1
  72. package/build-module/dataform-controls/email.js +15 -8
  73. package/build-module/dataform-controls/email.js.map +1 -1
  74. package/build-module/dataform-controls/index.js +3 -1
  75. package/build-module/dataform-controls/index.js.map +1 -1
  76. package/build-module/dataform-controls/integer.js +15 -8
  77. package/build-module/dataform-controls/integer.js.map +1 -1
  78. package/build-module/dataform-controls/text.js +15 -8
  79. package/build-module/dataform-controls/text.js.map +1 -1
  80. package/build-module/dataforms-layouts/card/index.js +128 -0
  81. package/build-module/dataforms-layouts/card/index.js.map +1 -0
  82. package/build-module/dataforms-layouts/data-form-layout.js +2 -2
  83. package/build-module/dataforms-layouts/data-form-layout.js.map +1 -1
  84. package/build-module/dataforms-layouts/index.js +4 -0
  85. package/build-module/dataforms-layouts/index.js.map +1 -1
  86. package/build-module/dataforms-layouts/panel/dropdown.js +118 -0
  87. package/build-module/dataforms-layouts/panel/dropdown.js.map +1 -0
  88. package/build-module/dataforms-layouts/panel/index.js +37 -152
  89. package/build-module/dataforms-layouts/panel/index.js.map +1 -1
  90. package/build-module/dataforms-layouts/panel/modal.js +119 -0
  91. package/build-module/dataforms-layouts/panel/modal.js.map +1 -0
  92. package/build-module/dataforms-layouts/regular/index.js +10 -21
  93. package/build-module/dataforms-layouts/regular/index.js.map +1 -1
  94. package/build-module/dataviews-layouts/grid/index.js +25 -8
  95. package/build-module/dataviews-layouts/grid/index.js.map +1 -1
  96. package/build-module/dataviews-layouts/grid/preview-size-picker.js +11 -11
  97. package/build-module/dataviews-layouts/grid/preview-size-picker.js.map +1 -1
  98. package/build-module/dataviews-layouts/list/index.js +47 -29
  99. package/build-module/dataviews-layouts/list/index.js.map +1 -1
  100. package/build-module/dataviews-layouts/table/column-header-menu.js +3 -0
  101. package/build-module/dataviews-layouts/table/column-header-menu.js.map +1 -1
  102. package/build-module/dataviews-layouts/table/index.js +23 -8
  103. package/build-module/dataviews-layouts/table/index.js.map +1 -1
  104. package/build-module/field-types/array.js +2 -2
  105. package/build-module/field-types/array.js.map +1 -1
  106. package/build-module/normalize-form-fields.js +50 -13
  107. package/build-module/normalize-form-fields.js.map +1 -1
  108. package/build-module/types.js.map +1 -1
  109. package/build-style/style-rtl.css +53 -16
  110. package/build-style/style.css +53 -16
  111. package/build-types/components/dataform/stories/index.story.d.ts +41 -17
  112. package/build-types/components/dataform/stories/index.story.d.ts.map +1 -1
  113. package/build-types/components/dataviews/index.d.ts +5 -2
  114. package/build-types/components/dataviews/index.d.ts.map +1 -1
  115. package/build-types/components/dataviews/stories/fixtures.d.ts.map +1 -1
  116. package/build-types/components/dataviews/stories/index.story.d.ts +2 -1
  117. package/build-types/components/dataviews/stories/index.story.d.ts.map +1 -1
  118. package/build-types/components/dataviews-context/index.d.ts +4 -1
  119. package/build-types/components/dataviews-context/index.d.ts.map +1 -1
  120. package/build-types/components/dataviews-item-actions/index.d.ts.map +1 -1
  121. package/build-types/components/dataviews-view-config/index.d.ts.map +1 -1
  122. package/build-types/components/dataviews-view-config/infinite-scroll-toggle.d.ts +2 -0
  123. package/build-types/components/dataviews-view-config/infinite-scroll-toggle.d.ts.map +1 -0
  124. package/build-types/dataform-controls/array.d.ts +6 -0
  125. package/build-types/dataform-controls/array.d.ts.map +1 -0
  126. package/build-types/dataform-controls/boolean.d.ts.map +1 -1
  127. package/build-types/dataform-controls/email.d.ts.map +1 -1
  128. package/build-types/dataform-controls/index.d.ts.map +1 -1
  129. package/build-types/dataform-controls/integer.d.ts.map +1 -1
  130. package/build-types/dataform-controls/text.d.ts.map +1 -1
  131. package/build-types/dataforms-layouts/card/index.d.ts +13 -0
  132. package/build-types/dataforms-layouts/card/index.d.ts.map +1 -0
  133. package/build-types/dataforms-layouts/index.d.ts.map +1 -1
  134. package/build-types/dataforms-layouts/panel/dropdown.d.ts +14 -0
  135. package/build-types/dataforms-layouts/panel/dropdown.d.ts.map +1 -0
  136. package/build-types/dataforms-layouts/panel/index.d.ts.map +1 -1
  137. package/build-types/dataforms-layouts/panel/modal.d.ts +13 -0
  138. package/build-types/dataforms-layouts/panel/modal.d.ts.map +1 -0
  139. package/build-types/dataforms-layouts/regular/index.d.ts.map +1 -1
  140. package/build-types/dataviews-layouts/grid/index.d.ts.map +1 -1
  141. package/build-types/dataviews-layouts/grid/preview-size-picker.d.ts +1 -1
  142. package/build-types/dataviews-layouts/grid/preview-size-picker.d.ts.map +1 -1
  143. package/build-types/dataviews-layouts/list/index.d.ts.map +1 -1
  144. package/build-types/dataviews-layouts/table/column-header-menu.d.ts.map +1 -1
  145. package/build-types/dataviews-layouts/table/index.d.ts.map +1 -1
  146. package/build-types/field-types/boolean.d.ts +1 -1
  147. package/build-types/normalize-form-fields.d.ts +10 -3
  148. package/build-types/normalize-form-fields.d.ts.map +1 -1
  149. package/build-types/test/normalize-form-fields.d.ts +2 -0
  150. package/build-types/test/normalize-form-fields.d.ts.map +1 -0
  151. package/build-types/types.d.ts +54 -6
  152. package/build-types/types.d.ts.map +1 -1
  153. package/build-wp/index.js +3062 -1147
  154. package/package.json +15 -15
  155. package/src/components/dataform/stories/index.story.tsx +478 -91
  156. package/src/components/dataviews/index.tsx +50 -14
  157. package/src/components/dataviews/stories/fixtures.tsx +98 -7
  158. package/src/components/dataviews/stories/index.story.tsx +137 -4
  159. package/src/components/dataviews/style.scss +4 -0
  160. package/src/components/dataviews-context/index.ts +6 -2
  161. package/src/components/dataviews-item-actions/index.tsx +7 -16
  162. package/src/components/dataviews-pagination/index.tsx +1 -1
  163. package/src/components/dataviews-view-config/index.tsx +13 -5
  164. package/src/components/dataviews-view-config/infinite-scroll-toggle.tsx +39 -0
  165. package/src/dataform-controls/array.tsx +85 -0
  166. package/src/dataform-controls/boolean.tsx +24 -10
  167. package/src/dataform-controls/email.tsx +24 -11
  168. package/src/dataform-controls/index.tsx +3 -1
  169. package/src/dataform-controls/integer.tsx +27 -13
  170. package/src/dataform-controls/text.tsx +24 -11
  171. package/src/dataforms-layouts/card/index.tsx +154 -0
  172. package/src/dataforms-layouts/card/style.scss +3 -0
  173. package/src/dataforms-layouts/data-form-layout.tsx +2 -2
  174. package/src/dataforms-layouts/index.tsx +5 -0
  175. package/src/dataforms-layouts/panel/dropdown.tsx +160 -0
  176. package/src/dataforms-layouts/panel/index.tsx +49 -189
  177. package/src/dataforms-layouts/panel/modal.tsx +165 -0
  178. package/src/dataforms-layouts/panel/style.scss +4 -0
  179. package/src/dataforms-layouts/regular/index.tsx +20 -23
  180. package/src/dataviews-layouts/grid/index.tsx +32 -5
  181. package/src/dataviews-layouts/grid/preview-size-picker.tsx +15 -13
  182. package/src/dataviews-layouts/grid/style.scss +3 -1
  183. package/src/dataviews-layouts/list/index.tsx +65 -31
  184. package/src/dataviews-layouts/list/style.scss +7 -3
  185. package/src/dataviews-layouts/table/column-header-menu.tsx +4 -0
  186. package/src/dataviews-layouts/table/index.tsx +27 -1
  187. package/src/field-types/array.tsx +1 -1
  188. package/src/normalize-form-fields.ts +63 -17
  189. package/src/test/dataform.tsx +181 -3
  190. package/src/test/dataviews.tsx +38 -0
  191. package/src/test/filter-and-sort-data-view.js +123 -64
  192. package/src/test/normalize-form-fields.ts +247 -0
  193. package/src/types.ts +72 -6
  194. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,85 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { FormTokenField } from '@wordpress/components';
5
+ import { useCallback, useMemo } from '@wordpress/element';
6
+
7
+ /**
8
+ * Internal dependencies
9
+ */
10
+ import type { DataFormControlProps } from '../types';
11
+
12
+ export default function ArrayControl< Item >( {
13
+ data,
14
+ field,
15
+ onChange,
16
+ hideLabelFromVision,
17
+ }: DataFormControlProps< Item > ) {
18
+ const { id, label, placeholder, elements } = field;
19
+ const value = field.getValue( { item: data } );
20
+
21
+ const findElementByValue = useCallback(
22
+ ( suggestionValue: string ) => {
23
+ return elements?.find(
24
+ ( suggestion ) => suggestion.value === suggestionValue
25
+ );
26
+ },
27
+ [ elements ]
28
+ );
29
+
30
+ const findElementByLabel = useCallback(
31
+ ( suggestionLabel: string ) => {
32
+ return elements?.find(
33
+ ( suggestion ) => suggestion.label === suggestionLabel
34
+ );
35
+ },
36
+ [ elements ]
37
+ );
38
+
39
+ // Ensure value is an array
40
+ const arrayValue = useMemo(
41
+ () =>
42
+ Array.isArray( value )
43
+ ? value.map( ( token ) => {
44
+ const tokenLabel = findElementByValue( token )?.label;
45
+ return tokenLabel || token;
46
+ } )
47
+ : [],
48
+ [ value, findElementByValue ]
49
+ );
50
+
51
+ const onChangeControl = useCallback(
52
+ ( tokens: ( string | { value: string } )[] ) => {
53
+ // Convert TokenItem objects to strings
54
+ const stringTokens = tokens.map( ( token ) => {
55
+ if ( typeof token !== 'string' ) {
56
+ return token.value;
57
+ }
58
+
59
+ const tokenByLabel = findElementByLabel( token );
60
+
61
+ return tokenByLabel?.value || token;
62
+ } );
63
+
64
+ onChange( {
65
+ [ id ]: stringTokens,
66
+ } );
67
+ },
68
+ [ id, onChange, findElementByLabel ]
69
+ );
70
+
71
+ return (
72
+ <FormTokenField
73
+ label={ hideLabelFromVision ? undefined : label }
74
+ value={ arrayValue }
75
+ onChange={ onChangeControl }
76
+ placeholder={ placeholder }
77
+ suggestions={
78
+ elements?.map( ( suggestion ) => suggestion.label ) ?? []
79
+ }
80
+ __experimentalExpandOnFocus={ elements && elements.length > 0 }
81
+ __next40pxDefaultSize
82
+ __nextHasNoMarginBottom
83
+ />
84
+ );
85
+ }
@@ -2,6 +2,7 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { privateApis } from '@wordpress/components';
5
+ import { useState } from '@wordpress/element';
5
6
 
6
7
  /**
7
8
  * Internal dependencies
@@ -18,23 +19,36 @@ export default function Boolean< Item >( {
18
19
  hideLabelFromVision,
19
20
  }: DataFormControlProps< Item > ) {
20
21
  const { id, getValue, label } = field;
22
+ const [ customValidity, setCustomValidity ] =
23
+ useState<
24
+ React.ComponentProps<
25
+ typeof ValidatedToggleControl
26
+ >[ 'customValidity' ]
27
+ >( undefined );
21
28
 
22
29
  return (
23
30
  <ValidatedToggleControl
24
31
  required={ !! field.isValid.required }
25
- customValidator={ ( newValue: any ) => {
26
- if ( field.isValid?.custom ) {
27
- return field.isValid.custom(
28
- {
29
- ...data,
30
- [ id ]: newValue,
31
- },
32
- field
33
- );
32
+ onValidate={ ( newValue: any ) => {
33
+ const message = field.isValid?.custom?.(
34
+ {
35
+ ...data,
36
+ [ id ]: newValue,
37
+ },
38
+ field
39
+ );
40
+
41
+ if ( message ) {
42
+ setCustomValidity( {
43
+ type: 'invalid',
44
+ message,
45
+ } );
46
+ return;
34
47
  }
35
48
 
36
- return null;
49
+ setCustomValidity( undefined );
37
50
  } }
51
+ customValidity={ customValidity }
38
52
  hidden={ hideLabelFromVision }
39
53
  __nextHasNoMarginBottom
40
54
  label={ label }
@@ -2,7 +2,7 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { privateApis } from '@wordpress/components';
5
- import { useCallback } from '@wordpress/element';
5
+ import { useCallback, useState } from '@wordpress/element';
6
6
 
7
7
  /**
8
8
  * Internal dependencies
@@ -20,6 +20,12 @@ export default function Email< Item >( {
20
20
  }: DataFormControlProps< Item > ) {
21
21
  const { id, label, placeholder, description } = field;
22
22
  const value = field.getValue( { item: data } );
23
+ const [ customValidity, setCustomValidity ] =
24
+ useState<
25
+ React.ComponentProps<
26
+ typeof ValidatedTextControl
27
+ >[ 'customValidity' ]
28
+ >( undefined );
23
29
 
24
30
  const onChangeControl = useCallback(
25
31
  ( newValue: string ) =>
@@ -32,19 +38,26 @@ export default function Email< Item >( {
32
38
  return (
33
39
  <ValidatedTextControl
34
40
  required={ !! field.isValid?.required }
35
- customValidator={ ( newValue: any ) => {
36
- if ( field.isValid?.custom ) {
37
- return field.isValid.custom(
38
- {
39
- ...data,
40
- [ id ]: newValue,
41
- },
42
- field
43
- );
41
+ onValidate={ ( newValue: any ) => {
42
+ const message = field.isValid?.custom?.(
43
+ {
44
+ ...data,
45
+ [ id ]: newValue,
46
+ },
47
+ field
48
+ );
49
+
50
+ if ( message ) {
51
+ setCustomValidity( {
52
+ type: 'invalid',
53
+ message,
54
+ } );
55
+ return;
44
56
  }
45
57
 
46
- return null;
58
+ setCustomValidity( undefined );
47
59
  } }
60
+ customValidity={ customValidity }
48
61
  type="email"
49
62
  label={ label }
50
63
  placeholder={ placeholder }
@@ -21,12 +21,14 @@ import select from './select';
21
21
  import text from './text';
22
22
  import toggleGroup from './toggle-group';
23
23
  import boolean from './boolean';
24
+ import array from './array';
24
25
 
25
26
  interface FormControls {
26
27
  [ key: string ]: ComponentType< DataFormControlProps< any > >;
27
28
  }
28
29
 
29
30
  const FORM_CONTROLS: FormControls = {
31
+ array,
30
32
  boolean,
31
33
  checkbox,
32
34
  datetime,
@@ -51,7 +53,7 @@ export function getControl< Item >(
51
53
  return getControlByType( field.Edit );
52
54
  }
53
55
 
54
- if ( field.elements ) {
56
+ if ( field.elements && field.type !== 'array' ) {
55
57
  return getControlByType( 'select' );
56
58
  }
57
59
 
@@ -7,7 +7,7 @@ import {
7
7
  __experimentalNumberControl as NumberControl,
8
8
  privateApis,
9
9
  } from '@wordpress/components';
10
- import { useCallback } from '@wordpress/element';
10
+ import { useCallback, useState } from '@wordpress/element';
11
11
  import { __ } from '@wordpress/i18n';
12
12
 
13
13
  /**
@@ -84,6 +84,13 @@ export default function Integer< Item >( {
84
84
  }: DataFormControlProps< Item > ) {
85
85
  const { id, label, description } = field;
86
86
  const value = field.getValue( { item: data } ) ?? '';
87
+ const [ customValidity, setCustomValidity ] =
88
+ useState<
89
+ React.ComponentProps<
90
+ typeof ValidatedNumberControl
91
+ >[ 'customValidity' ]
92
+ >( undefined );
93
+
87
94
  const onChangeControl = useCallback(
88
95
  ( newValue: string | undefined ) => {
89
96
  onChange( {
@@ -112,21 +119,28 @@ export default function Integer< Item >( {
112
119
  return (
113
120
  <ValidatedNumberControl
114
121
  required={ !! field.isValid?.required }
115
- customValidator={ ( newValue: any ) => {
116
- if ( field.isValid?.custom ) {
117
- return field.isValid.custom(
118
- {
119
- ...data,
120
- [ id ]: [ undefined, '', null ].includes( newValue )
121
- ? undefined
122
- : Number( newValue ),
123
- },
124
- field
125
- );
122
+ onValidate={ ( newValue: any ) => {
123
+ const message = field.isValid?.custom?.(
124
+ {
125
+ ...data,
126
+ [ id ]: [ undefined, '', null ].includes( newValue )
127
+ ? undefined
128
+ : Number( newValue ),
129
+ },
130
+ field
131
+ );
132
+
133
+ if ( message ) {
134
+ setCustomValidity( {
135
+ type: 'invalid',
136
+ message,
137
+ } );
138
+ return;
126
139
  }
127
140
 
128
- return null;
141
+ setCustomValidity( undefined );
129
142
  } }
143
+ customValidity={ customValidity }
130
144
  label={ label }
131
145
  help={ description }
132
146
  value={ value }
@@ -2,7 +2,7 @@
2
2
  * WordPress dependencies
3
3
  */
4
4
  import { privateApis } from '@wordpress/components';
5
- import { useCallback } from '@wordpress/element';
5
+ import { useCallback, useState } from '@wordpress/element';
6
6
 
7
7
  /**
8
8
  * Internal dependencies
@@ -20,6 +20,12 @@ export default function Text< Item >( {
20
20
  }: DataFormControlProps< Item > ) {
21
21
  const { id, label, placeholder, description } = field;
22
22
  const value = field.getValue( { item: data } );
23
+ const [ customValidity, setCustomValidity ] =
24
+ useState<
25
+ React.ComponentProps<
26
+ typeof ValidatedTextControl
27
+ >[ 'customValidity' ]
28
+ >( undefined );
23
29
 
24
30
  const onChangeControl = useCallback(
25
31
  ( newValue: string ) =>
@@ -32,19 +38,26 @@ export default function Text< Item >( {
32
38
  return (
33
39
  <ValidatedTextControl
34
40
  required={ !! field.isValid?.required }
35
- customValidator={ ( newValue: any ) => {
36
- if ( field.isValid?.custom ) {
37
- return field.isValid.custom(
38
- {
39
- ...data,
40
- [ id ]: newValue,
41
- },
42
- field
43
- );
41
+ onValidate={ ( newValue: any ) => {
42
+ const message = field.isValid?.custom?.(
43
+ {
44
+ ...data,
45
+ [ id ]: newValue,
46
+ },
47
+ field
48
+ );
49
+
50
+ if ( message ) {
51
+ setCustomValidity( {
52
+ type: 'invalid',
53
+ message,
54
+ } );
55
+ return;
44
56
  }
45
57
 
46
- return null;
58
+ setCustomValidity( undefined );
47
59
  } }
60
+ customValidity={ customValidity }
48
61
  label={ label }
49
62
  placeholder={ placeholder }
50
63
  value={ value ?? '' }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+
5
+ /**
6
+ * WordPress dependencies
7
+ */
8
+ import { Button, Card, CardBody, CardHeader } from '@wordpress/components';
9
+ import { useCallback, useContext, useMemo, useState } from '@wordpress/element';
10
+ import { chevronDown, chevronUp } from '@wordpress/icons';
11
+
12
+ /**
13
+ * Internal dependencies
14
+ */
15
+ import { getFormFieldLayout } from '..';
16
+ import DataFormContext from '../../components/dataform-context';
17
+ import type { NormalizedCardLayout, FieldLayoutProps, Form } from '../../types';
18
+ import { DataFormLayout } from '../data-form-layout';
19
+ import { isCombinedField } from '../is-combined-field';
20
+ import { DEFAULT_LAYOUT, normalizeLayout } from '../../normalize-form-fields';
21
+
22
+ export function useCollapsibleCard( initialIsOpen: boolean = true ) {
23
+ const [ isOpen, setIsOpen ] = useState( initialIsOpen );
24
+
25
+ const toggle = useCallback( () => {
26
+ setIsOpen( ( prev ) => ! prev );
27
+ }, [] );
28
+
29
+ const CollapsibleCardHeader = useCallback(
30
+ ( {
31
+ children,
32
+ ...props
33
+ }: {
34
+ children: React.ReactNode;
35
+ [ key: string ]: any;
36
+ } ) => (
37
+ <CardHeader
38
+ { ...props }
39
+ onClick={ toggle }
40
+ style={ {
41
+ cursor: 'pointer',
42
+ ...props.style,
43
+ } }
44
+ >
45
+ <div
46
+ style={ {
47
+ width: '100%',
48
+ display: 'flex',
49
+ justifyContent: 'space-between',
50
+ alignItems: 'center',
51
+ } }
52
+ >
53
+ { children }
54
+ </div>
55
+ <Button
56
+ __next40pxDefaultSize
57
+ variant="tertiary"
58
+ icon={ isOpen ? chevronUp : chevronDown }
59
+ aria-expanded={ isOpen }
60
+ aria-label={ isOpen ? 'Collapse' : 'Expand' }
61
+ />
62
+ </CardHeader>
63
+ ),
64
+ [ toggle, isOpen ]
65
+ );
66
+
67
+ return { isOpen, CollapsibleCardHeader };
68
+ }
69
+
70
+ export default function FormCardField< Item >( {
71
+ data,
72
+ field,
73
+ onChange,
74
+ hideLabelFromVision,
75
+ }: FieldLayoutProps< Item > ) {
76
+ const { fields } = useContext( DataFormContext );
77
+
78
+ const layout: NormalizedCardLayout = normalizeLayout( {
79
+ ...field.layout,
80
+ type: 'card',
81
+ } ) as NormalizedCardLayout;
82
+
83
+ const form: Form = useMemo(
84
+ (): Form => ( {
85
+ layout: DEFAULT_LAYOUT,
86
+ fields: isCombinedField( field ) ? field.children : [],
87
+ } ),
88
+ [ field ]
89
+ );
90
+
91
+ const { isOpen, CollapsibleCardHeader } = useCollapsibleCard(
92
+ layout.isOpened
93
+ );
94
+ if ( isCombinedField( field ) ) {
95
+ const withHeader = !! field.label && layout.withHeader;
96
+ return (
97
+ <Card className="dataforms-layouts-card__field">
98
+ { withHeader && (
99
+ <CollapsibleCardHeader className="dataforms-layouts-card__field-label">
100
+ { field.label }
101
+ </CollapsibleCardHeader>
102
+ ) }
103
+ { ( isOpen || ! withHeader ) && (
104
+ // If it doesn't have a header, keep it open.
105
+ // Otherwise, the card will not be visible.
106
+ <CardBody className="dataforms-layouts-card__field-control">
107
+ <DataFormLayout
108
+ data={ data }
109
+ form={ form }
110
+ onChange={ onChange }
111
+ />
112
+ </CardBody>
113
+ ) }
114
+ </Card>
115
+ );
116
+ }
117
+
118
+ const fieldDefinition = fields.find(
119
+ ( fieldDef ) => fieldDef.id === field.id
120
+ );
121
+
122
+ if ( ! fieldDefinition || ! fieldDefinition.Edit ) {
123
+ return null;
124
+ }
125
+
126
+ const RegularLayout = getFormFieldLayout( 'regular' )?.component;
127
+ if ( ! RegularLayout ) {
128
+ return null;
129
+ }
130
+ const withHeader = !! fieldDefinition.label && layout.withHeader;
131
+ return (
132
+ <Card className="dataforms-layouts-card__field">
133
+ { withHeader && (
134
+ <CollapsibleCardHeader className="dataforms-layouts-card__field-label">
135
+ { fieldDefinition.label }
136
+ </CollapsibleCardHeader>
137
+ ) }
138
+ { ( isOpen || ! withHeader ) && (
139
+ // If it doesn't have a header, keep it open.
140
+ // Otherwise, the card will not be visible.
141
+ <CardBody className="dataforms-layouts-card__field-control">
142
+ <RegularLayout
143
+ data={ data }
144
+ field={ field }
145
+ onChange={ onChange }
146
+ hideLabelFromVision={
147
+ hideLabelFromVision || withHeader
148
+ }
149
+ />
150
+ </CardBody>
151
+ ) }
152
+ </Card>
153
+ );
154
+ }
@@ -0,0 +1,3 @@
1
+ .dataforms-layouts-card__field {
2
+ width: 100%;
3
+ }
@@ -48,9 +48,9 @@ export function DataFormLayout< Item >( {
48
48
  );
49
49
 
50
50
  return (
51
- <VStack spacing={ form?.type === 'panel' ? 2 : 4 }>
51
+ <VStack spacing={ form.layout?.type === 'panel' ? 2 : 4 }>
52
52
  { normalizedFormFields.map( ( formField ) => {
53
- const FieldLayout = getFormFieldLayout( formField.layout )
53
+ const FieldLayout = getFormFieldLayout( formField.layout.type )
54
54
  ?.component;
55
55
 
56
56
  if ( ! FieldLayout ) {
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import FormRegularField from './regular';
5
5
  import FormPanelField from './panel';
6
+ import FormCardField from './card';
6
7
 
7
8
  const FORM_FIELD_LAYOUTS = [
8
9
  {
@@ -13,6 +14,10 @@ const FORM_FIELD_LAYOUTS = [
13
14
  type: 'panel',
14
15
  component: FormPanelField,
15
16
  },
17
+ {
18
+ type: 'card',
19
+ component: FormCardField,
20
+ },
16
21
  ];
17
22
 
18
23
  export function getFormFieldLayout( type: string ) {
@@ -0,0 +1,160 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import {
5
+ __experimentalVStack as VStack,
6
+ __experimentalHStack as HStack,
7
+ __experimentalHeading as Heading,
8
+ __experimentalSpacer as Spacer,
9
+ Dropdown,
10
+ Button,
11
+ } from '@wordpress/components';
12
+ import { sprintf, __, _x } from '@wordpress/i18n';
13
+ import { useMemo } from '@wordpress/element';
14
+ import { closeSmall } from '@wordpress/icons';
15
+
16
+ /**
17
+ * Internal dependencies
18
+ */
19
+ import type { Form, FormField, NormalizedField } from '../../types';
20
+ import { DataFormLayout } from '../data-form-layout';
21
+ import { isCombinedField } from '../is-combined-field';
22
+ import { DEFAULT_LAYOUT } from '../../normalize-form-fields';
23
+
24
+ function DropdownHeader( {
25
+ title,
26
+ onClose,
27
+ }: {
28
+ title?: string;
29
+ onClose: () => void;
30
+ } ) {
31
+ return (
32
+ <VStack
33
+ className="dataforms-layouts-panel__dropdown-header"
34
+ spacing={ 4 }
35
+ >
36
+ <HStack alignment="center">
37
+ { title && (
38
+ <Heading level={ 2 } size={ 13 }>
39
+ { title }
40
+ </Heading>
41
+ ) }
42
+ <Spacer />
43
+ { onClose && (
44
+ <Button
45
+ label={ __( 'Close' ) }
46
+ icon={ closeSmall }
47
+ onClick={ onClose }
48
+ size="small"
49
+ />
50
+ ) }
51
+ </HStack>
52
+ </VStack>
53
+ );
54
+ }
55
+
56
+ function PanelDropdown< Item >( {
57
+ fieldDefinition,
58
+ popoverAnchor,
59
+ labelPosition = 'side',
60
+ data,
61
+ onChange,
62
+ field,
63
+ }: {
64
+ fieldDefinition: NormalizedField< Item >;
65
+ popoverAnchor: HTMLElement | null;
66
+ labelPosition: 'side' | 'top' | 'none';
67
+ data: Item;
68
+ onChange: ( value: any ) => void;
69
+ field: FormField;
70
+ } ) {
71
+ const fieldLabel = isCombinedField( field )
72
+ ? field.label
73
+ : fieldDefinition?.label;
74
+
75
+ const form: Form = useMemo(
76
+ (): Form => ( {
77
+ layout: DEFAULT_LAYOUT,
78
+ fields: isCombinedField( field )
79
+ ? field.children
80
+ : // If not explicit children return the field id itself.
81
+ [ { id: field.id } ],
82
+ } ),
83
+ [ field ]
84
+ );
85
+
86
+ // Memoize popoverProps to avoid returning a new object every time.
87
+ const popoverProps = useMemo(
88
+ () => ( {
89
+ // Anchor the popover to the middle of the entire row so that it doesn't
90
+ // move around when the label changes.
91
+ anchor: popoverAnchor,
92
+ placement: 'left-start',
93
+ offset: 36,
94
+ shift: true,
95
+ } ),
96
+ [ popoverAnchor ]
97
+ );
98
+
99
+ return (
100
+ <Dropdown
101
+ contentClassName="dataforms-layouts-panel__field-dropdown"
102
+ popoverProps={ popoverProps }
103
+ focusOnMount
104
+ toggleProps={ {
105
+ size: 'compact',
106
+ variant: 'tertiary',
107
+ tooltipPosition: 'middle left',
108
+ } }
109
+ renderToggle={ ( { isOpen, onToggle } ) => (
110
+ <Button
111
+ className="dataforms-layouts-panel__field-control"
112
+ size="compact"
113
+ variant={
114
+ [ 'none', 'top' ].includes( labelPosition )
115
+ ? 'link'
116
+ : 'tertiary'
117
+ }
118
+ aria-expanded={ isOpen }
119
+ aria-label={ sprintf(
120
+ // translators: %s: Field name.
121
+ _x( 'Edit %s', 'field' ),
122
+ fieldLabel || ''
123
+ ) }
124
+ onClick={ onToggle }
125
+ disabled={ fieldDefinition.readOnly === true }
126
+ accessibleWhenDisabled
127
+ >
128
+ <fieldDefinition.render
129
+ item={ data }
130
+ field={ fieldDefinition }
131
+ />
132
+ </Button>
133
+ ) }
134
+ renderContent={ ( { onClose } ) => (
135
+ <>
136
+ <DropdownHeader title={ fieldLabel } onClose={ onClose } />
137
+ <DataFormLayout
138
+ data={ data }
139
+ form={ form }
140
+ onChange={ onChange }
141
+ >
142
+ { ( FieldLayout, nestedField ) => (
143
+ <FieldLayout
144
+ key={ nestedField.id }
145
+ data={ data }
146
+ field={ nestedField }
147
+ onChange={ onChange }
148
+ hideLabelFromVision={
149
+ ( form?.fields ?? [] ).length < 2
150
+ }
151
+ />
152
+ ) }
153
+ </DataFormLayout>
154
+ </>
155
+ ) }
156
+ />
157
+ );
158
+ }
159
+
160
+ export default PanelDropdown;