@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
@@ -58,7 +58,6 @@
58
58
 
59
59
  .dataviews-view-grid__media {
60
60
  width: 100%;
61
- min-height: 200px;
62
61
  aspect-ratio: 1/1;
63
62
  background-color: $white;
64
63
  border-radius: $radius-medium;
@@ -105,6 +104,9 @@
105
104
  .dataviews-view-grid__field-name {
106
105
  width: 35%;
107
106
  color: $gray-700;
107
+ overflow: hidden;
108
+ text-overflow: ellipsis;
109
+ white-space: nowrap;
108
110
  }
109
111
 
110
112
  .dataviews-view-grid__field-value {
@@ -22,6 +22,7 @@ import {
22
22
  useMemo,
23
23
  useRef,
24
24
  useState,
25
+ useContext,
25
26
  } from '@wordpress/element';
26
27
  import { __ } from '@wordpress/i18n';
27
28
  import { moreVertical } from '@wordpress/icons';
@@ -35,6 +36,7 @@ import {
35
36
  ActionsMenuGroup,
36
37
  ActionModal,
37
38
  } from '../../components/dataviews-item-actions';
39
+ import DataViewsContext from '../../components/dataviews-context';
38
40
  import type {
39
41
  Action,
40
42
  NormalizedField,
@@ -55,6 +57,7 @@ interface ListViewItemProps< Item > {
55
57
  onSelect: ( item: Item ) => void;
56
58
  otherFields: NormalizedField< Item >[];
57
59
  onDropdownTriggerKeyDown: React.KeyboardEventHandler< HTMLButtonElement >;
60
+ posinset?: number;
58
61
  }
59
62
 
60
63
  const { Menu } = unlock( componentsPrivateApis );
@@ -153,8 +156,14 @@ function ListItem< Item >( {
153
156
  onSelect,
154
157
  otherFields,
155
158
  onDropdownTriggerKeyDown,
159
+ posinset,
156
160
  }: ListViewItemProps< Item > ) {
157
- const { showTitle = true, showMedia = true, showDescription = true } = view;
161
+ const {
162
+ showTitle = true,
163
+ showMedia = true,
164
+ showDescription = true,
165
+ infiniteScrollEnabled,
166
+ } = view;
158
167
  const itemRef = useRef< HTMLDivElement >( null );
159
168
  const labelId = `${ idPrefix }-label`;
160
169
  const descriptionId = `${ idPrefix }-description`;
@@ -169,6 +178,7 @@ function ListItem< Item >( {
169
178
  setIsHovered( isHover );
170
179
  };
171
180
 
181
+ const { paginationInfo } = useContext( DataViewsContext );
172
182
  useEffect( () => {
173
183
  if ( isSelected ) {
174
184
  itemRef.current?.scrollIntoView( {
@@ -269,8 +279,18 @@ function ListItem< Item >( {
269
279
  return (
270
280
  <Composite.Row
271
281
  ref={ itemRef }
272
- render={ <div /> }
273
- role="row"
282
+ render={
283
+ /* aria-posinset breaks Composite.Row if passed to it directly. */
284
+ <div
285
+ aria-posinset={ posinset }
286
+ aria-setsize={
287
+ infiniteScrollEnabled
288
+ ? paginationInfo.totalItems
289
+ : undefined
290
+ }
291
+ />
292
+ }
293
+ role={ infiniteScrollEnabled ? 'article' : 'row' }
274
294
  className={ clsx( {
275
295
  'is-selected': isSelected,
276
296
  'is-hovered': isHovered,
@@ -498,33 +518,47 @@ export default function ViewList< Item >( props: ViewListProps< Item > ) {
498
518
  }
499
519
 
500
520
  return (
501
- <Composite
502
- id={ baseId }
503
- render={ <div /> }
504
- className={ clsx( 'dataviews-view-list', className ) }
505
- role="grid"
506
- activeId={ activeCompositeId }
507
- setActiveId={ setActiveCompositeId }
508
- >
509
- { data.map( ( item ) => {
510
- const id = generateCompositeItemIdPrefix( item );
511
- return (
512
- <ListItem
513
- key={ id }
514
- view={ view }
515
- idPrefix={ id }
516
- actions={ actions }
517
- item={ item }
518
- isSelected={ item === selectedItem }
519
- onSelect={ onSelect }
520
- mediaField={ mediaField }
521
- titleField={ titleField }
522
- descriptionField={ descriptionField }
523
- otherFields={ otherFields }
524
- onDropdownTriggerKeyDown={ onDropdownTriggerKeyDown }
525
- />
526
- );
527
- } ) }
528
- </Composite>
521
+ <>
522
+ <Composite
523
+ id={ baseId }
524
+ render={ <div /> }
525
+ className={ clsx( 'dataviews-view-list', className ) }
526
+ role={ view.infiniteScrollEnabled ? 'feed' : 'grid' }
527
+ activeId={ activeCompositeId }
528
+ setActiveId={ setActiveCompositeId }
529
+ >
530
+ { data.map( ( item, index ) => {
531
+ const id = generateCompositeItemIdPrefix( item );
532
+ return (
533
+ <ListItem
534
+ key={ id }
535
+ view={ view }
536
+ idPrefix={ id }
537
+ actions={ actions }
538
+ item={ item }
539
+ isSelected={ item === selectedItem }
540
+ onSelect={ onSelect }
541
+ mediaField={ mediaField }
542
+ titleField={ titleField }
543
+ descriptionField={ descriptionField }
544
+ otherFields={ otherFields }
545
+ onDropdownTriggerKeyDown={
546
+ onDropdownTriggerKeyDown
547
+ }
548
+ posinset={
549
+ view.infiniteScrollEnabled
550
+ ? index + 1
551
+ : undefined
552
+ }
553
+ />
554
+ );
555
+ } ) }
556
+ </Composite>
557
+ { hasData && isLoading && (
558
+ <p className="dataviews-loading-more">
559
+ <Spinner />
560
+ </p>
561
+ ) }
562
+ </>
529
563
  );
530
564
  }
@@ -5,7 +5,8 @@ div.dataviews-view-list {
5
5
  .dataviews-view-list {
6
6
  margin: 0 0 auto;
7
7
 
8
- div[role="row"] {
8
+ div[role="row"],
9
+ div[role="article"] {
9
10
  margin: 0;
10
11
  border-top: 1px solid $gray-100;
11
12
 
@@ -57,7 +58,8 @@ div.dataviews-view-list {
57
58
  &.is-selected.is-selected {
58
59
  border-top: 1px solid rgba(var(--wp-admin-theme-color--rgb), 0.12);
59
60
 
60
- & + div[role="row"] {
61
+ & + div[role="row"],
62
+ & + div[role="article"] {
61
63
  border-top: 1px solid rgba(var(--wp-admin-theme-color--rgb), 0.12);
62
64
  }
63
65
  }
@@ -82,7 +84,9 @@ div.dataviews-view-list {
82
84
  }
83
85
 
84
86
  div[role="row"].is-selected,
85
- div[role="row"].is-selected:focus-within {
87
+ div[role="row"].is-selected:focus-within,
88
+ div[role="article"].is-selected,
89
+ div[role="article"].is-selected:focus-within {
86
90
  .dataviews-view-list__item-wrapper {
87
91
  background-color: rgba(var(--wp-admin-theme-color--rgb), 0.04);
88
92
  color: $gray-900;
@@ -94,6 +94,10 @@ const _HeaderMenu = forwardRef( function HeaderMenu< Item >(
94
94
  field.filterBy !== false &&
95
95
  ! field.filterBy?.isPrimary;
96
96
 
97
+ if ( ! isSortable && ! canMove && ! isHidable && ! canAddFilter ) {
98
+ return header;
99
+ }
100
+
97
101
  return (
98
102
  <Menu>
99
103
  <Menu.TriggerButton
@@ -70,6 +70,7 @@ interface TableRowProps< Item > {
70
70
  } & ComponentProps< 'a' >
71
71
  ) => ReactElement;
72
72
  isActionsColumnSticky?: boolean;
73
+ posinset?: number;
73
74
  }
74
75
 
75
76
  function TableColumnField< Item >( {
@@ -114,11 +115,18 @@ function TableRow< Item >( {
114
115
  renderItemLink,
115
116
  onChangeSelection,
116
117
  isActionsColumnSticky,
118
+ posinset,
117
119
  }: TableRowProps< Item > ) {
120
+ const { paginationInfo } = useContext( DataViewsContext );
118
121
  const hasPossibleBulkAction = useHasAPossibleBulkAction( actions, item );
119
122
  const isSelected = hasPossibleBulkAction && selection.includes( id );
120
123
  const [ isHovered, setIsHovered ] = useState( false );
121
- const { showTitle = true, showMedia = true, showDescription = true } = view;
124
+ const {
125
+ showTitle = true,
126
+ showMedia = true,
127
+ showDescription = true,
128
+ infiniteScrollEnabled,
129
+ } = view;
122
130
  const handleMouseEnter = () => {
123
131
  setIsHovered( true );
124
132
  };
@@ -148,6 +156,11 @@ function TableRow< Item >( {
148
156
  onTouchStart={ () => {
149
157
  isTouchDeviceRef.current = true;
150
158
  } }
159
+ aria-setsize={
160
+ infiniteScrollEnabled ? paginationInfo.totalItems : undefined
161
+ }
162
+ aria-posinset={ posinset }
163
+ role={ infiniteScrollEnabled ? 'article' : undefined }
151
164
  onClick={ ( event ) => {
152
165
  if ( ! hasPossibleBulkAction ) {
153
166
  return;
@@ -356,6 +369,7 @@ function ViewTable< Item >( {
356
369
  headerMenuRefs.current.delete( column );
357
370
  }
358
371
  };
372
+ const isInfiniteScroll = view.infiniteScrollEnabled && ! dataByGroup;
359
373
 
360
374
  return (
361
375
  <>
@@ -369,6 +383,7 @@ function ViewTable< Item >( {
369
383
  } ) }
370
384
  aria-busy={ isLoading }
371
385
  aria-describedby={ tableNoticeId }
386
+ role={ isInfiniteScroll ? 'feed' : undefined }
372
387
  >
373
388
  <thead>
374
389
  <tr className="dataviews-view-table__row">
@@ -434,6 +449,9 @@ function ViewTable< Item >( {
434
449
  onChangeView={ onChangeView }
435
450
  onHide={ onHide }
436
451
  setOpenedFilter={ setOpenedFilter }
452
+ canMove={
453
+ view.layout?.enableMoving ?? true
454
+ }
437
455
  />
438
456
  </th>
439
457
  );
@@ -545,6 +563,9 @@ function ViewTable< Item >( {
545
563
  isActionsColumnSticky={
546
564
  ! isHorizontalScrollEnd
547
565
  }
566
+ posinset={
567
+ isInfiniteScroll ? index + 1 : undefined
568
+ }
548
569
  />
549
570
  ) ) }
550
571
  </tbody>
@@ -558,6 +579,11 @@ function ViewTable< Item >( {
558
579
  id={ tableNoticeId }
559
580
  >
560
581
  { ! hasData && <p>{ isLoading ? <Spinner /> : empty }</p> }
582
+ { hasData && isLoading && (
583
+ <p className="dataviews-loading-more">
584
+ <Spinner />
585
+ </p>
586
+ ) }
561
587
  </div>
562
588
  </>
563
589
  );
@@ -70,7 +70,7 @@ const arrayFieldType: FieldTypeDefinition< any > = {
70
70
  return null;
71
71
  },
72
72
  },
73
- Edit: null, // Not implemented yet
73
+ Edit: 'array', // Use array control
74
74
  render,
75
75
  enableSorting: true,
76
76
  filterBy: {
@@ -1,42 +1,88 @@
1
1
  /**
2
2
  * Internal dependencies
3
3
  */
4
- import type { Form } from './types';
4
+ import type {
5
+ Form,
6
+ Layout,
7
+ NormalizedLayout,
8
+ NormalizedRegularLayout,
9
+ NormalizedPanelLayout,
10
+ NormalizedCardLayout,
11
+ } from './types';
5
12
 
6
13
  interface NormalizedFormField {
7
14
  id: string;
8
- layout: 'regular' | 'panel';
9
- labelPosition: 'side' | 'top' | 'none';
15
+ layout: Layout;
16
+ }
17
+
18
+ export const DEFAULT_LAYOUT: NormalizedLayout = {
19
+ type: 'regular',
20
+ labelPosition: 'top',
21
+ };
22
+
23
+ /**
24
+ * Normalizes a layout configuration based on its type.
25
+ *
26
+ * @param layout The layout object to normalize.
27
+ * @return The normalized layout object.
28
+ */
29
+ export function normalizeLayout( layout?: Layout ): NormalizedLayout {
30
+ let normalizedLayout = DEFAULT_LAYOUT;
31
+
32
+ if ( layout?.type === 'regular' ) {
33
+ normalizedLayout = {
34
+ type: 'regular',
35
+ labelPosition: layout?.labelPosition ?? 'top',
36
+ } satisfies NormalizedRegularLayout;
37
+ } else if ( layout?.type === 'panel' ) {
38
+ normalizedLayout = {
39
+ type: 'panel',
40
+ labelPosition: layout?.labelPosition ?? 'side',
41
+ openAs: layout?.openAs ?? 'dropdown',
42
+ } satisfies NormalizedPanelLayout;
43
+ } else if ( layout?.type === 'card' ) {
44
+ if ( layout.withHeader === false ) {
45
+ // Don't let isOpened be false if withHeader is false.
46
+ // Otherwise, the card will not be visible.
47
+ normalizedLayout = {
48
+ type: 'card',
49
+ withHeader: false,
50
+ isOpened: true,
51
+ } satisfies NormalizedCardLayout;
52
+ } else {
53
+ normalizedLayout = {
54
+ type: 'card',
55
+ withHeader: true,
56
+ isOpened:
57
+ typeof layout.isOpened === 'boolean'
58
+ ? layout.isOpened
59
+ : true,
60
+ } satisfies NormalizedCardLayout;
61
+ }
62
+ }
63
+
64
+ return normalizedLayout;
10
65
  }
11
66
 
12
67
  export default function normalizeFormFields(
13
68
  form: Form
14
69
  ): NormalizedFormField[] {
15
- let layout: 'regular' | 'panel' = 'regular';
16
- if ( [ 'regular', 'panel' ].includes( form.type ?? '' ) ) {
17
- layout = form.type as 'regular' | 'panel';
18
- }
19
-
20
- const labelPosition =
21
- form.labelPosition ?? ( layout === 'regular' ? 'top' : 'side' );
70
+ const formLayout = normalizeLayout( form?.layout );
22
71
 
23
72
  return ( form.fields ?? [] ).map( ( field ) => {
24
73
  if ( typeof field === 'string' ) {
25
74
  return {
26
75
  id: field,
27
- layout,
28
- labelPosition,
76
+ layout: formLayout,
29
77
  };
30
78
  }
31
79
 
32
- const fieldLayout = field.layout ?? layout;
33
- const fieldLabelPosition =
34
- field.labelPosition ??
35
- ( fieldLayout === 'regular' ? 'top' : 'side' );
80
+ const fieldLayout = field.layout
81
+ ? normalizeLayout( field.layout )
82
+ : formLayout;
36
83
  return {
37
84
  ...field,
38
85
  layout: fieldLayout,
39
- labelPosition: fieldLabelPosition,
40
86
  };
41
87
  } );
42
88
  }
@@ -149,7 +149,10 @@ describe( 'DataForm component', () => {
149
149
  <Dataform
150
150
  onChange={ noop }
151
151
  fields={ fields }
152
- form={ { ...form, labelPosition: 'side' } }
152
+ form={ {
153
+ ...form,
154
+ layout: { type: 'regular', labelPosition: 'side' },
155
+ } }
153
156
  data={ data }
154
157
  />
155
158
  );
@@ -191,7 +194,10 @@ describe( 'DataForm component', () => {
191
194
  describe( 'in panel mode', () => {
192
195
  const formPanelMode = {
193
196
  ...form,
194
- type: 'panel' as const,
197
+ layout: {
198
+ type: 'panel',
199
+ labelPosition: 'side',
200
+ } as const,
195
201
  };
196
202
  it( 'should display fields', async () => {
197
203
  render(
@@ -212,6 +218,175 @@ describe( 'DataForm component', () => {
212
218
  }
213
219
  } );
214
220
 
221
+ it( 'should use dropdown panel type by default', async () => {
222
+ render(
223
+ <Dataform
224
+ onChange={ noop }
225
+ fields={ fields }
226
+ form={ formPanelMode }
227
+ data={ data }
228
+ />
229
+ );
230
+
231
+ const user = await userEvent.setup();
232
+ const titleButton = fieldsSelector.title.view();
233
+ await user.click( titleButton );
234
+
235
+ // Should show dropdown content (not modal)
236
+ expect(
237
+ screen.getByRole( 'textbox', { name: /title/i } )
238
+ ).toBeInTheDocument();
239
+ // Should not have modal dialog
240
+ expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument();
241
+ // Should not have modal buttons (Cancel/Apply)
242
+ expect(
243
+ screen.queryByRole( 'button', { name: /cancel/i } )
244
+ ).not.toBeInTheDocument();
245
+ expect(
246
+ screen.queryByRole( 'button', { name: /apply/i } )
247
+ ).not.toBeInTheDocument();
248
+ } );
249
+
250
+ it( 'should use dropdown panel type when explicitly set', async () => {
251
+ const formWithDropdownPanel = {
252
+ ...form,
253
+ layout: {
254
+ type: 'panel',
255
+ labelPosition: 'side',
256
+ openAs: 'dropdown',
257
+ } as const,
258
+ };
259
+
260
+ render(
261
+ <Dataform
262
+ onChange={ noop }
263
+ fields={ fields }
264
+ form={ formWithDropdownPanel }
265
+ data={ data }
266
+ />
267
+ );
268
+
269
+ const user = await userEvent.setup();
270
+ const titleButton = fieldsSelector.title.view();
271
+ await user.click( titleButton );
272
+
273
+ // Should show dropdown content
274
+ expect(
275
+ screen.getByRole( 'textbox', { name: /title/i } )
276
+ ).toBeInTheDocument();
277
+ } );
278
+
279
+ it( 'should use modal panel type when set', async () => {
280
+ const formWithModalPanel = {
281
+ ...form,
282
+ layout: {
283
+ type: 'panel',
284
+ labelPosition: 'side',
285
+ openAs: 'modal',
286
+ } as const,
287
+ };
288
+
289
+ render(
290
+ <Dataform
291
+ onChange={ noop }
292
+ fields={ fields }
293
+ form={ formWithModalPanel }
294
+ data={ data }
295
+ />
296
+ );
297
+
298
+ const user = await userEvent.setup();
299
+ const titleButton = fieldsSelector.title.view();
300
+ await user.click( titleButton );
301
+
302
+ // Should show modal content
303
+ expect( screen.getByRole( 'dialog' ) ).toBeInTheDocument();
304
+ expect(
305
+ screen.getByRole( 'textbox', { name: /title/i } )
306
+ ).toBeInTheDocument();
307
+ } );
308
+
309
+ it( 'should close modal when cancel button is clicked', async () => {
310
+ const formWithModalPanel = {
311
+ ...form,
312
+ layout: {
313
+ type: 'panel',
314
+ labelPosition: 'side',
315
+ openAs: 'modal',
316
+ } as const,
317
+ };
318
+
319
+ render(
320
+ <Dataform
321
+ onChange={ noop }
322
+ fields={ fields }
323
+ form={ formWithModalPanel }
324
+ data={ data }
325
+ />
326
+ );
327
+
328
+ const user = await userEvent.setup();
329
+ const titleButton = fieldsSelector.title.view();
330
+ await user.click( titleButton );
331
+
332
+ // Modal should be open
333
+ expect( screen.getByRole( 'dialog' ) ).toBeInTheDocument();
334
+
335
+ // Click cancel button
336
+ const cancelButton = screen.getByRole( 'button', {
337
+ name: /cancel/i,
338
+ } );
339
+ await user.click( cancelButton );
340
+
341
+ // Modal should be closed
342
+ expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument();
343
+ } );
344
+
345
+ it( 'should apply changes and close modal when apply button is clicked', async () => {
346
+ const onChange = jest.fn();
347
+ const formWithModalPanel = {
348
+ ...form,
349
+ layout: {
350
+ type: 'panel',
351
+ labelPosition: 'side',
352
+ openAs: 'modal',
353
+ } as const,
354
+ };
355
+
356
+ render(
357
+ <Dataform
358
+ onChange={ onChange }
359
+ fields={ fields }
360
+ form={ formWithModalPanel }
361
+ data={ data }
362
+ />
363
+ );
364
+
365
+ const user = await userEvent.setup();
366
+ const titleButton = fieldsSelector.title.view();
367
+ await user.click( titleButton );
368
+
369
+ // Modal should be open
370
+ expect( screen.getByRole( 'dialog' ) ).toBeInTheDocument();
371
+
372
+ // Type in the input
373
+ const titleInput = screen.getByRole( 'textbox', {
374
+ name: /title/i,
375
+ } );
376
+ await user.clear( titleInput );
377
+ await user.type( titleInput, 'New Title' );
378
+
379
+ // Click apply button
380
+ const applyButton = screen.getByRole( 'button', {
381
+ name: /apply/i,
382
+ } );
383
+ await user.click( applyButton );
384
+
385
+ // Modal should be closed and onChange should be called
386
+ expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument();
387
+ expect( onChange ).toHaveBeenCalledWith( { title: 'New Title' } );
388
+ } );
389
+
215
390
  it( 'should call onChange with the correct value for each typed character', async () => {
216
391
  const onChange = jest.fn();
217
392
  render(
@@ -243,7 +418,10 @@ describe( 'DataForm component', () => {
243
418
  <Dataform
244
419
  onChange={ noop }
245
420
  fields={ fields }
246
- form={ { ...formPanelMode, labelPosition: 'side' } }
421
+ form={ {
422
+ ...formPanelMode,
423
+ layout: { type: 'panel', labelPosition: 'side' },
424
+ } }
247
425
  data={ data }
248
426
  />
249
427
  );
@@ -147,6 +147,23 @@ function DataViewWrapper( {
147
147
 
148
148
  // jest.useFakeTimers();
149
149
 
150
+ // Tests run against a DataView which is 500px wide.
151
+ jest.mock( '@wordpress/compose', () => {
152
+ return {
153
+ ...jest.requireActual( '@wordpress/compose' ),
154
+ useResizeObserver: jest.fn( ( callback ) => {
155
+ setTimeout( () => {
156
+ callback( [
157
+ {
158
+ borderBoxSize: [ { inlineSize: 500 } ],
159
+ },
160
+ ] );
161
+ }, 0 );
162
+ return () => {};
163
+ } ),
164
+ };
165
+ } );
166
+
150
167
  describe( 'DataViews component', () => {
151
168
  it( 'should show "No results" if data is empty', () => {
152
169
  render( <DataViewWrapper data={ [] } /> );
@@ -492,6 +509,27 @@ describe( 'DataViews component', () => {
492
509
 
493
510
  await user.keyboard( '{/Control}' );
494
511
  } );
512
+
513
+ it( 'accepts an invalid previewSize and the preview size picker falls back to another size', async () => {
514
+ render(
515
+ <DataViewWrapper
516
+ view={ {
517
+ type: 'grid',
518
+ mediaField: 'image',
519
+ layout: { previewSize: 13 },
520
+ } }
521
+ />
522
+ );
523
+ const user = userEvent.setup();
524
+ await user.click(
525
+ screen.getByRole( 'button', { name: 'View options' } )
526
+ );
527
+ const previewSizeSlider = screen.getByRole( 'slider', {
528
+ name: 'Preview size',
529
+ } );
530
+ expect( previewSizeSlider ).toBeInTheDocument();
531
+ expect( previewSizeSlider ).toHaveValue( '0' ); // Falls back to the smallest size, which is the first one.
532
+ } );
495
533
  } );
496
534
 
497
535
  describe( 'in list view', () => {