@wordpress/dataviews 13.0.0 → 13.1.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 (156) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +2 -1
  3. package/build/components/dataform-controls/date.cjs +11 -1
  4. package/build/components/dataform-controls/date.cjs.map +2 -2
  5. package/build/components/dataform-controls/datetime.cjs +23 -32
  6. package/build/components/dataform-controls/datetime.cjs.map +2 -2
  7. package/build/components/dataform-controls/utils/relative-date-control.cjs +2 -1
  8. package/build/components/dataform-controls/utils/relative-date-control.cjs.map +2 -2
  9. package/build/components/dataform-layouts/normalize-form.cjs +19 -1
  10. package/build/components/dataform-layouts/normalize-form.cjs.map +2 -2
  11. package/build/components/dataform-layouts/panel/index.cjs +1 -1
  12. package/build/components/dataform-layouts/panel/index.cjs.map +2 -2
  13. package/build/components/dataform-layouts/panel/modal.cjs +4 -3
  14. package/build/components/dataform-layouts/panel/modal.cjs.map +2 -2
  15. package/build/components/dataviews-layout/index.cjs +12 -3
  16. package/build/components/dataviews-layout/index.cjs.map +2 -2
  17. package/build/components/dataviews-layouts/grid/composite-grid.cjs +5 -1
  18. package/build/components/dataviews-layouts/grid/composite-grid.cjs.map +2 -2
  19. package/build/components/dataviews-layouts/index.cjs +3 -3
  20. package/build/components/dataviews-layouts/index.cjs.map +3 -3
  21. package/build/components/dataviews-layouts/picker-grid/index.cjs +13 -2
  22. package/build/components/dataviews-layouts/picker-grid/index.cjs.map +2 -2
  23. package/build/components/dataviews-layouts/table/index.cjs +98 -89
  24. package/build/components/dataviews-layouts/table/index.cjs.map +2 -2
  25. package/build/components/dataviews-layouts/table/{use-is-horizontal-scroll-end.cjs → use-scroll-state.cjs} +29 -33
  26. package/build/components/dataviews-layouts/table/use-scroll-state.cjs.map +7 -0
  27. package/build/components/dataviews-layouts/utils/density-picker.cjs.map +2 -2
  28. package/build/components/dataviews-layouts/utils/grid-config-options.cjs +45 -0
  29. package/build/components/dataviews-layouts/utils/grid-config-options.cjs.map +7 -0
  30. package/build/dataviews/index.cjs +12 -8
  31. package/build/dataviews/index.cjs.map +2 -2
  32. package/build/dataviews-picker/index.cjs +1 -1
  33. package/build/dataviews-picker/index.cjs.map +2 -2
  34. package/build/types/dataform.cjs.map +1 -1
  35. package/build/types/dataviews.cjs.map +1 -1
  36. package/build-module/components/dataform-controls/date.mjs +11 -1
  37. package/build-module/components/dataform-controls/date.mjs.map +2 -2
  38. package/build-module/components/dataform-controls/datetime.mjs +24 -33
  39. package/build-module/components/dataform-controls/datetime.mjs.map +2 -2
  40. package/build-module/components/dataform-controls/utils/relative-date-control.mjs +2 -1
  41. package/build-module/components/dataform-controls/utils/relative-date-control.mjs.map +2 -2
  42. package/build-module/components/dataform-layouts/normalize-form.mjs +19 -1
  43. package/build-module/components/dataform-layouts/normalize-form.mjs.map +2 -2
  44. package/build-module/components/dataform-layouts/panel/index.mjs +1 -1
  45. package/build-module/components/dataform-layouts/panel/index.mjs.map +2 -2
  46. package/build-module/components/dataform-layouts/panel/modal.mjs +4 -3
  47. package/build-module/components/dataform-layouts/panel/modal.mjs.map +2 -2
  48. package/build-module/components/dataviews-layout/index.mjs +12 -3
  49. package/build-module/components/dataviews-layout/index.mjs.map +2 -2
  50. package/build-module/components/dataviews-layouts/grid/composite-grid.mjs +5 -1
  51. package/build-module/components/dataviews-layouts/grid/composite-grid.mjs.map +2 -2
  52. package/build-module/components/dataviews-layouts/index.mjs +3 -3
  53. package/build-module/components/dataviews-layouts/index.mjs.map +2 -2
  54. package/build-module/components/dataviews-layouts/picker-grid/index.mjs +13 -2
  55. package/build-module/components/dataviews-layouts/picker-grid/index.mjs.map +2 -2
  56. package/build-module/components/dataviews-layouts/table/index.mjs +98 -89
  57. package/build-module/components/dataviews-layouts/table/index.mjs.map +2 -2
  58. package/build-module/components/dataviews-layouts/table/use-scroll-state.mjs +46 -0
  59. package/build-module/components/dataviews-layouts/table/use-scroll-state.mjs.map +7 -0
  60. package/build-module/components/dataviews-layouts/utils/density-picker.mjs.map +2 -2
  61. package/build-module/components/dataviews-layouts/utils/grid-config-options.mjs +14 -0
  62. package/build-module/components/dataviews-layouts/utils/grid-config-options.mjs.map +7 -0
  63. package/build-module/dataviews/index.mjs +12 -8
  64. package/build-module/dataviews/index.mjs.map +2 -2
  65. package/build-module/dataviews-picker/index.mjs +1 -1
  66. package/build-module/dataviews-picker/index.mjs.map +2 -2
  67. package/build-style/style-rtl.css +47 -5
  68. package/build-style/style.css +47 -5
  69. package/build-types/components/dataform-controls/date.d.ts.map +1 -1
  70. package/build-types/components/dataform-controls/datetime.d.ts.map +1 -1
  71. package/build-types/components/dataform-controls/utils/relative-date-control.d.ts.map +1 -1
  72. package/build-types/components/dataform-layouts/normalize-form.d.ts.map +1 -1
  73. package/build-types/components/dataform-layouts/panel/modal.d.ts.map +1 -1
  74. package/build-types/components/dataviews-layout/index.d.ts.map +1 -1
  75. package/build-types/components/dataviews-layouts/grid/composite-grid.d.ts.map +1 -1
  76. package/build-types/components/dataviews-layouts/index.d.ts +3 -3
  77. package/build-types/components/dataviews-layouts/index.d.ts.map +1 -1
  78. package/build-types/components/dataviews-layouts/picker-grid/index.d.ts.map +1 -1
  79. package/build-types/components/dataviews-layouts/table/index.d.ts.map +1 -1
  80. package/build-types/components/dataviews-layouts/table/use-scroll-state.d.ts +25 -0
  81. package/build-types/components/dataviews-layouts/table/use-scroll-state.d.ts.map +1 -0
  82. package/build-types/components/dataviews-layouts/utils/density-picker.d.ts.map +1 -1
  83. package/build-types/components/dataviews-layouts/utils/grid-config-options.d.ts +2 -0
  84. package/build-types/components/dataviews-layouts/utils/grid-config-options.d.ts.map +1 -0
  85. package/build-types/dataform/stories/index.story.d.ts +26 -1
  86. package/build-types/dataform/stories/index.story.d.ts.map +1 -1
  87. package/build-types/dataform/stories/layout-panel.d.ts +3 -1
  88. package/build-types/dataform/stories/layout-panel.d.ts.map +1 -1
  89. package/build-types/dataviews/index.d.ts.map +1 -1
  90. package/build-types/dataviews/stories/empty.d.ts +1 -2
  91. package/build-types/dataviews/stories/empty.d.ts.map +1 -1
  92. package/build-types/dataviews/stories/free-composition.d.ts.map +1 -1
  93. package/build-types/dataviews/stories/index.story.d.ts +18 -10
  94. package/build-types/dataviews/stories/index.story.d.ts.map +1 -1
  95. package/build-types/dataviews/stories/infinite-scroll.d.ts.map +1 -1
  96. package/build-types/dataviews/stories/layout-activity.d.ts.map +1 -1
  97. package/build-types/dataviews/stories/layout-custom.d.ts +3 -1
  98. package/build-types/dataviews/stories/layout-custom.d.ts.map +1 -1
  99. package/build-types/dataviews/stories/layout-grid.d.ts.map +1 -1
  100. package/build-types/dataviews/stories/layout-list.d.ts.map +1 -1
  101. package/build-types/dataviews/stories/layout-table.d.ts.map +1 -1
  102. package/build-types/dataviews/stories/with-card.d.ts +3 -1
  103. package/build-types/dataviews/stories/with-card.d.ts.map +1 -1
  104. package/build-types/types/dataform.d.ts +17 -2
  105. package/build-types/types/dataform.d.ts.map +1 -1
  106. package/build-types/types/dataviews.d.ts +8 -0
  107. package/build-types/types/dataviews.d.ts.map +1 -1
  108. package/build-wp/index.js +883 -866
  109. package/package.json +19 -19
  110. package/src/components/dataform-controls/date.tsx +11 -1
  111. package/src/components/dataform-controls/datetime.tsx +28 -44
  112. package/src/components/dataform-controls/utils/relative-date-control.tsx +2 -1
  113. package/src/components/dataform-layouts/normalize-form.ts +24 -1
  114. package/src/components/dataform-layouts/panel/index.tsx +1 -1
  115. package/src/components/dataform-layouts/panel/modal.tsx +7 -3
  116. package/src/components/dataform-layouts/panel/style.scss +1 -1
  117. package/src/components/dataform-layouts/test/normalize-form.ts +98 -5
  118. package/src/components/dataviews-layout/index.tsx +41 -19
  119. package/src/components/dataviews-layout/style.scss +8 -0
  120. package/src/components/dataviews-layouts/grid/composite-grid.tsx +7 -1
  121. package/src/components/dataviews-layouts/grid/style.scss +18 -2
  122. package/src/components/dataviews-layouts/index.ts +3 -3
  123. package/src/components/dataviews-layouts/picker-grid/index.tsx +17 -2
  124. package/src/components/dataviews-layouts/picker-grid/style.scss +10 -0
  125. package/src/components/dataviews-layouts/table/index.tsx +11 -5
  126. package/src/components/dataviews-layouts/table/style.scss +13 -0
  127. package/src/components/dataviews-layouts/table/use-scroll-state.ts +79 -0
  128. package/src/components/dataviews-layouts/utils/density-picker.tsx +12 -2
  129. package/src/components/dataviews-layouts/utils/grid-config-options.tsx +14 -0
  130. package/src/components/dataviews-layouts/utils/grid-items.scss +9 -1
  131. package/src/dataform/stories/index.story.tsx +15 -0
  132. package/src/dataform/stories/layout-panel.tsx +19 -4
  133. package/src/dataviews/index.tsx +17 -9
  134. package/src/dataviews/stories/empty.tsx +1 -3
  135. package/src/dataviews/stories/free-composition.tsx +32 -34
  136. package/src/dataviews/stories/index.story.tsx +31 -8
  137. package/src/dataviews/stories/infinite-scroll.tsx +0 -6
  138. package/src/dataviews/stories/layout-activity.tsx +1 -0
  139. package/src/dataviews/stories/layout-custom.tsx +7 -3
  140. package/src/dataviews/stories/layout-grid.tsx +1 -0
  141. package/src/dataviews/stories/layout-list.tsx +1 -0
  142. package/src/dataviews/stories/layout-table.tsx +1 -0
  143. package/src/dataviews/stories/style.css +0 -5
  144. package/src/dataviews/stories/with-card.tsx +6 -2
  145. package/src/dataviews/style.scss +0 -1
  146. package/src/dataviews/test/dataviews.tsx +42 -1
  147. package/src/dataviews-picker/index.tsx +1 -1
  148. package/src/style.scss +1 -0
  149. package/src/types/dataform.ts +15 -2
  150. package/src/types/dataviews.ts +10 -0
  151. package/build/components/dataviews-layouts/table/use-is-horizontal-scroll-end.cjs.map +0 -7
  152. package/build-module/components/dataviews-layouts/table/use-is-horizontal-scroll-end.mjs +0 -50
  153. package/build-module/components/dataviews-layouts/table/use-is-horizontal-scroll-end.mjs.map +0 -7
  154. package/build-types/components/dataviews-layouts/table/use-is-horizontal-scroll-end.d.ts +0 -19
  155. package/build-types/components/dataviews-layouts/table/use-is-horizontal-scroll-end.d.ts.map +0 -1
  156. package/src/components/dataviews-layouts/table/use-is-horizontal-scroll-end.ts +0 -82
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/dataviews",
3
- "version": "13.0.0",
3
+ "version": "13.1.0",
4
4
  "description": "DataViews is a component that provides an API to render datasets using different types of layouts (table, grid, list, etc.).",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -53,20 +53,20 @@
53
53
  "sideEffects": false,
54
54
  "dependencies": {
55
55
  "@ariakit/react": "^0.4.21",
56
- "@wordpress/base-styles": "^6.17.0",
57
- "@wordpress/components": "^32.3.0",
58
- "@wordpress/compose": "^7.41.0",
59
- "@wordpress/data": "^10.41.0",
60
- "@wordpress/date": "^5.41.0",
61
- "@wordpress/deprecated": "^4.41.0",
62
- "@wordpress/element": "^6.41.0",
63
- "@wordpress/i18n": "^6.14.0",
64
- "@wordpress/icons": "^11.8.0",
65
- "@wordpress/keycodes": "^4.41.0",
66
- "@wordpress/primitives": "^4.41.0",
67
- "@wordpress/private-apis": "^1.41.0",
68
- "@wordpress/ui": "^0.8.0",
69
- "@wordpress/warning": "^3.41.0",
56
+ "@wordpress/base-styles": "^6.18.0",
57
+ "@wordpress/components": "^32.4.0",
58
+ "@wordpress/compose": "^7.42.0",
59
+ "@wordpress/data": "^10.42.0",
60
+ "@wordpress/date": "^5.42.0",
61
+ "@wordpress/deprecated": "^4.42.0",
62
+ "@wordpress/element": "^6.42.0",
63
+ "@wordpress/i18n": "^6.15.0",
64
+ "@wordpress/icons": "^12.0.0",
65
+ "@wordpress/keycodes": "^4.42.0",
66
+ "@wordpress/primitives": "^4.42.0",
67
+ "@wordpress/private-apis": "^1.42.0",
68
+ "@wordpress/ui": "^0.9.0",
69
+ "@wordpress/warning": "^3.42.0",
70
70
  "clsx": "^2.1.1",
71
71
  "colord": "^2.7.0",
72
72
  "date-fns": "^4.1.0",
@@ -75,12 +75,12 @@
75
75
  "remove-accents": "^0.5.0"
76
76
  },
77
77
  "devDependencies": {
78
- "@storybook/addon-docs": "^10.1.11",
79
- "@storybook/react-vite": "^10.1.11",
78
+ "@storybook/addon-docs": "^10.2.8",
79
+ "@storybook/react-vite": "^10.2.8",
80
80
  "@testing-library/jest-dom": "^6.9.1",
81
81
  "@types/jest": "^29.5.14",
82
82
  "esbuild": "^0.27.2",
83
- "storybook": "^10.1.11"
83
+ "storybook": "^10.2.8"
84
84
  },
85
85
  "peerDependencies": {
86
86
  "react": "^18.0.0",
@@ -92,5 +92,5 @@
92
92
  "scripts": {
93
93
  "build:wp": "node build.cjs"
94
94
  },
95
- "gitHead": "8bfc179b9aed74c0a6dd6e8edf7a49e40e4f87cc"
95
+ "gitHead": "c20787b1778ae64c2db65643b1c236309d68e6ba"
96
96
  }
@@ -293,6 +293,7 @@ function CalendarDateControl< Item >( {
293
293
  const {
294
294
  id,
295
295
  label,
296
+ description,
296
297
  setValue,
297
298
  getValue,
298
299
  isValid,
@@ -385,6 +386,7 @@ function CalendarDateControl< Item >( {
385
386
  id={ id }
386
387
  className="dataviews-controls__date"
387
388
  label={ displayLabel }
389
+ help={ description }
388
390
  hideLabelFromVision={ hideLabelFromVision }
389
391
  >
390
392
  <Stack direction="column" gap="lg">
@@ -462,7 +464,14 @@ function CalendarDateRangeControl< Item >( {
462
464
  markWhenOptional,
463
465
  validity,
464
466
  }: DataFormControlProps< Item > ) {
465
- const { id, label, getValue, setValue, format: fieldFormat } = field;
467
+ const {
468
+ id,
469
+ label,
470
+ description,
471
+ getValue,
472
+ setValue,
473
+ format: fieldFormat,
474
+ } = field;
466
475
  let value: DateRange;
467
476
  const fieldValue = getValue( { item: data } );
468
477
  if (
@@ -597,6 +606,7 @@ function CalendarDateRangeControl< Item >( {
597
606
  id={ id }
598
607
  className="dataviews-controls__date"
599
608
  label={ displayLabel }
609
+ help={ description }
600
610
  hideLabelFromVision={ hideLabelFromVision }
601
611
  >
602
612
  <Stack direction="column" gap="lg">
@@ -1,8 +1,3 @@
1
- /**
2
- * External dependencies
3
- */
4
- import { format } from 'date-fns';
5
-
6
1
  /**
7
2
  * WordPress dependencies
8
3
  */
@@ -12,7 +7,7 @@ import {
12
7
  } from '@wordpress/components';
13
8
  import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
14
9
  import { __ } from '@wordpress/i18n';
15
- import { getSettings } from '@wordpress/date';
10
+ import { dateI18n, getDate, getSettings } from '@wordpress/date';
16
11
  import { Stack } from '@wordpress/ui';
17
12
 
18
13
  /**
@@ -27,15 +22,12 @@ import { unlock } from '../../lock-unlock';
27
22
 
28
23
  const { DateCalendar, ValidatedInputControl } = unlock( componentsPrivateApis );
29
24
 
30
- const formatDateTime = ( date?: Date | string ): string => {
31
- if ( ! date ) {
25
+ const formatDateTime = ( value?: string ): string => {
26
+ if ( ! value ) {
32
27
  return '';
33
28
  }
34
- if ( typeof date === 'string' ) {
35
- return date;
36
- }
37
- // Format as datetime-local input expects: YYYY-MM-DDTHH:mm
38
- return format( date, "yyyy-MM-dd'T'HH:mm" );
29
+ // Format in WordPress timezone for datetime-local input: YYYY-MM-DDTHH:mm
30
+ return dateI18n( 'Y-m-d\\TH:i', getDate( value ) );
39
31
  };
40
32
 
41
33
  function CalendarDateTimeControl< Item >( {
@@ -79,21 +71,19 @@ function CalendarDateTimeControl< Item >( {
79
71
  ( newDate: Date | undefined | null ) => {
80
72
  let dateTimeValue: string | undefined;
81
73
  if ( newDate ) {
82
- // Preserve time if it exists in current value, otherwise use current time
83
- let finalDateTime = newDate;
74
+ // Extract the date part in WP timezone from the calendar selection
75
+ const wpDate = dateI18n( 'Y-m-d', newDate );
84
76
 
77
+ // Preserve time if it exists in current value, otherwise use current time
78
+ let wpTime: string;
85
79
  if ( value ) {
86
- const currentDateTime = parseDateTime( value );
87
- if ( currentDateTime ) {
88
- // Preserve the time part
89
- finalDateTime = new Date( newDate );
90
- finalDateTime.setHours( currentDateTime.getHours() );
91
- finalDateTime.setMinutes(
92
- currentDateTime.getMinutes()
93
- );
94
- }
80
+ wpTime = dateI18n( 'H:i', getDate( value ) );
81
+ } else {
82
+ wpTime = dateI18n( 'H:i', newDate );
95
83
  }
96
84
 
85
+ // Combine date and time in WP timezone and convert to ISO
86
+ const finalDateTime = getDate( `${ wpDate }T${ wpTime }` );
97
87
  dateTimeValue = finalDateTime.toISOString();
98
88
  onChangeCallback( dateTimeValue );
99
89
 
@@ -133,8 +123,8 @@ function CalendarDateTimeControl< Item >( {
133
123
  const handleManualDateTimeChange = useCallback(
134
124
  ( newValue?: string ) => {
135
125
  if ( newValue ) {
136
- // Convert from datetime-local format to ISO string
137
- const dateTime = new Date( newValue );
126
+ // Interpret the datetime-local value in WordPress timezone
127
+ const dateTime = getDate( newValue );
138
128
  onChangeCallback( dateTime.toISOString() );
139
129
 
140
130
  // Update calendar month to match
@@ -176,6 +166,18 @@ function CalendarDateTimeControl< Item >( {
176
166
  hideLabelFromVision={ hideLabelFromVision }
177
167
  >
178
168
  <Stack direction="column" gap="lg">
169
+ { /* Manual datetime input */ }
170
+ <ValidatedInputControl
171
+ ref={ inputControlRef }
172
+ __next40pxDefaultSize
173
+ required={ !! isValid?.required }
174
+ customValidity={ getCustomValidity( isValid, validity ) }
175
+ type="datetime-local"
176
+ label={ __( 'Date time' ) }
177
+ hideLabelFromVision
178
+ value={ formatDateTime( value ) }
179
+ onChange={ handleManualDateTimeChange }
180
+ />
179
181
  { /* Calendar widget */ }
180
182
  <DateCalendar
181
183
  style={ { width: '100%' } }
@@ -188,24 +190,6 @@ function CalendarDateTimeControl< Item >( {
188
190
  timeZone={ timezoneString || undefined }
189
191
  weekStartsOn={ weekStartsOn }
190
192
  />
191
- { /* Manual datetime input */ }
192
- <ValidatedInputControl
193
- ref={ inputControlRef }
194
- __next40pxDefaultSize
195
- required={ !! isValid?.required }
196
- customValidity={ getCustomValidity( isValid, validity ) }
197
- type="datetime-local"
198
- label={ __( 'Date time' ) }
199
- hideLabelFromVision
200
- value={
201
- value
202
- ? formatDateTime(
203
- parseDateTime( value ) || undefined
204
- )
205
- : ''
206
- }
207
- onChange={ handleManualDateTimeChange }
208
- />
209
193
  </Stack>
210
194
  </BaseControl>
211
195
  );
@@ -58,7 +58,7 @@ export default function RelativeDateControl< Item >( {
58
58
  operator === OPERATOR_IN_THE_PAST ? 'inThePast' : 'over'
59
59
  ];
60
60
 
61
- const { id, label, getValue, setValue } = field;
61
+ const { id, label, description, getValue, setValue } = field;
62
62
  const fieldValue = getValue( { item: data } );
63
63
  const { value: relValue = '', unit = options[ 0 ].value } =
64
64
  fieldValue && typeof fieldValue === 'object' ? fieldValue : {};
@@ -91,6 +91,7 @@ export default function RelativeDateControl< Item >( {
91
91
  className={ clsx( className, 'dataviews-controls__relative-date' ) }
92
92
  label={ label }
93
93
  hideLabelFromVision={ hideLabelFromVision }
94
+ help={ description }
94
95
  >
95
96
  <Stack direction="row" gap="sm">
96
97
  <NumberControl
@@ -1,3 +1,8 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+
1
6
  /**
2
7
  * Internal dependencies
3
8
  */
@@ -55,10 +60,28 @@ function normalizeLayout( layout?: Layout ): NormalizedLayout {
55
60
  ? summary
56
61
  : [ summary ];
57
62
 
63
+ const openAs = layout?.openAs;
64
+ let normalizedOpenAs: NormalizedPanelLayout[ 'openAs' ];
65
+ if ( typeof openAs === 'object' && openAs.type === 'modal' ) {
66
+ normalizedOpenAs = {
67
+ type: 'modal',
68
+ applyLabel: openAs.applyLabel?.trim() || __( 'Apply' ),
69
+ cancelLabel: openAs.cancelLabel?.trim() || __( 'Cancel' ),
70
+ };
71
+ } else if ( openAs === 'modal' ) {
72
+ normalizedOpenAs = {
73
+ type: 'modal',
74
+ applyLabel: __( 'Apply' ),
75
+ cancelLabel: __( 'Cancel' ),
76
+ };
77
+ } else {
78
+ normalizedOpenAs = { type: 'dropdown' };
79
+ }
80
+
58
81
  normalizedLayout = {
59
82
  type: 'panel',
60
83
  labelPosition: layout?.labelPosition ?? 'side',
61
- openAs: layout?.openAs ?? 'dropdown',
84
+ openAs: normalizedOpenAs,
62
85
  summary: normalizedSummary,
63
86
  editVisibility: layout?.editVisibility ?? 'on-hover',
64
87
  } satisfies NormalizedPanelLayout;
@@ -13,7 +13,7 @@ export default function FormPanelField< Item >( {
13
13
  }: FieldLayoutProps< Item > ) {
14
14
  const layout = field.layout as NormalizedPanelLayout;
15
15
 
16
- if ( layout.openAs === 'modal' ) {
16
+ if ( layout.openAs.type === 'modal' ) {
17
17
  return (
18
18
  <PanelModal
19
19
  data={ data }
@@ -11,7 +11,7 @@ import {
11
11
  Button,
12
12
  Modal,
13
13
  } from '@wordpress/components';
14
- import { __ } from '@wordpress/i18n';
14
+
15
15
  import { useContext, useMemo, useRef, useState } from '@wordpress/element';
16
16
  import { useFocusOnMount, useMergeRefs } from '@wordpress/compose';
17
17
  import { Stack } from '@wordpress/ui';
@@ -23,6 +23,8 @@ import type {
23
23
  Field,
24
24
  NormalizedForm,
25
25
  NormalizedFormField,
26
+ NormalizedPanelLayout,
27
+ PanelOpenAsModal,
26
28
  FieldLayoutProps,
27
29
  } from '../../../types';
28
30
  import { DataFormLayout } from '../data-form-layout';
@@ -48,6 +50,8 @@ function ModalContent< Item >( {
48
50
  fieldLabel: string;
49
51
  touched: boolean;
50
52
  } ) {
53
+ const { openAs } = field.layout as NormalizedPanelLayout;
54
+ const { applyLabel, cancelLabel } = openAs as PanelOpenAsModal;
51
55
  const { fields } = useContext( DataFormContext );
52
56
  const [ changes, setChanges ] = useState< Partial< Item > >( {} );
53
57
  const modalData = useMemo( () => {
@@ -147,14 +151,14 @@ function ModalContent< Item >( {
147
151
  onClick={ onClose }
148
152
  __next40pxDefaultSize
149
153
  >
150
- { __( 'Cancel' ) }
154
+ { cancelLabel }
151
155
  </Button>
152
156
  <Button
153
157
  variant="primary"
154
158
  onClick={ onApply }
155
159
  __next40pxDefaultSize
156
160
  >
157
- { __( 'Apply' ) }
161
+ { applyLabel }
158
162
  </Button>
159
163
  </Stack>
160
164
  </Modal>
@@ -152,7 +152,7 @@
152
152
  }
153
153
 
154
154
  .dataforms-layouts-panel__field-dropdown .components-popover__content {
155
- min-width: 320px;
155
+ min-width: 256px;
156
156
  padding: $grid-unit-20;
157
157
  }
158
158
 
@@ -137,7 +137,7 @@ describe( 'normalizeFormFields', () => {
137
137
  layout: {
138
138
  labelPosition: 'side',
139
139
  type: 'panel',
140
- openAs: 'dropdown',
140
+ openAs: { type: 'dropdown' },
141
141
  summary: [],
142
142
  editVisibility: 'on-hover',
143
143
  },
@@ -147,7 +147,7 @@ describe( 'normalizeFormFields', () => {
147
147
  layout: {
148
148
  type: 'panel',
149
149
  labelPosition: 'side',
150
- openAs: 'dropdown',
150
+ openAs: { type: 'dropdown' },
151
151
  summary: [],
152
152
  editVisibility: 'on-hover',
153
153
  },
@@ -166,7 +166,7 @@ describe( 'normalizeFormFields', () => {
166
166
  layout: {
167
167
  labelPosition: 'top',
168
168
  type: 'panel',
169
- openAs: 'dropdown',
169
+ openAs: { type: 'dropdown' },
170
170
  summary: [],
171
171
  editVisibility: 'on-hover',
172
172
  },
@@ -176,7 +176,7 @@ describe( 'normalizeFormFields', () => {
176
176
  layout: {
177
177
  type: 'panel',
178
178
  labelPosition: 'top',
179
- openAs: 'dropdown',
179
+ openAs: { type: 'dropdown' },
180
180
  summary: [],
181
181
  editVisibility: 'on-hover',
182
182
  },
@@ -185,6 +185,99 @@ describe( 'normalizeFormFields', () => {
185
185
  } );
186
186
  } );
187
187
 
188
+ it( 'panel: openAs string "modal" normalizes to object with defaults', () => {
189
+ const form: Form = {
190
+ layout: { type: 'panel', openAs: 'modal' },
191
+ fields: [ 'field1' ],
192
+ };
193
+ const result = normalizeForm( form );
194
+ expect( result.layout ).toEqual( {
195
+ type: 'panel',
196
+ labelPosition: 'side',
197
+ openAs: {
198
+ type: 'modal',
199
+ applyLabel: 'Apply',
200
+ cancelLabel: 'Cancel',
201
+ },
202
+ summary: [],
203
+ editVisibility: 'on-hover',
204
+ } );
205
+ } );
206
+
207
+ it( 'panel: openAs object preserves labels', () => {
208
+ const form: Form = {
209
+ layout: {
210
+ type: 'panel',
211
+ openAs: {
212
+ type: 'modal',
213
+ applyLabel: 'Save',
214
+ cancelLabel: 'Dismiss',
215
+ },
216
+ },
217
+ fields: [ 'field1' ],
218
+ };
219
+ const result = normalizeForm( form );
220
+ expect( result.layout ).toEqual( {
221
+ type: 'panel',
222
+ labelPosition: 'side',
223
+ openAs: {
224
+ type: 'modal',
225
+ applyLabel: 'Save',
226
+ cancelLabel: 'Dismiss',
227
+ },
228
+ summary: [],
229
+ editVisibility: 'on-hover',
230
+ } );
231
+ } );
232
+
233
+ it( 'panel: openAs object without labels gets defaults', () => {
234
+ const form: Form = {
235
+ layout: {
236
+ type: 'panel',
237
+ openAs: { type: 'modal' },
238
+ },
239
+ fields: [ 'field1' ],
240
+ };
241
+ const result = normalizeForm( form );
242
+ expect( result.layout ).toEqual( {
243
+ type: 'panel',
244
+ labelPosition: 'side',
245
+ openAs: {
246
+ type: 'modal',
247
+ applyLabel: 'Apply',
248
+ cancelLabel: 'Cancel',
249
+ },
250
+ summary: [],
251
+ editVisibility: 'on-hover',
252
+ } );
253
+ } );
254
+
255
+ it( 'panel: openAs object trims whitespace and falls back to defaults', () => {
256
+ const form: Form = {
257
+ layout: {
258
+ type: 'panel',
259
+ openAs: {
260
+ type: 'modal',
261
+ applyLabel: ' ',
262
+ cancelLabel: '',
263
+ },
264
+ },
265
+ fields: [ 'field1' ],
266
+ };
267
+ const result = normalizeForm( form );
268
+ expect( result.layout ).toEqual( {
269
+ type: 'panel',
270
+ labelPosition: 'side',
271
+ openAs: {
272
+ type: 'modal',
273
+ applyLabel: 'Apply',
274
+ cancelLabel: 'Cancel',
275
+ },
276
+ summary: [],
277
+ editVisibility: 'on-hover',
278
+ } );
279
+ } );
280
+
188
281
  it( 'card: with default layout options', () => {
189
282
  const form: Form = {
190
283
  layout: { type: 'card' },
@@ -360,7 +453,7 @@ describe( 'normalizeFormFields', () => {
360
453
  layout: {
361
454
  type: 'panel',
362
455
  labelPosition: 'side',
363
- openAs: 'dropdown',
456
+ openAs: { type: 'dropdown' },
364
457
  summary: [],
365
458
  editVisibility: 'on-hover',
366
459
  },
@@ -7,6 +7,7 @@ import type { ComponentType } from 'react';
7
7
  * WordPress dependencies
8
8
  */
9
9
  import { useContext } from '@wordpress/element';
10
+ import { Spinner } from '@wordpress/components';
10
11
  import { __ } from '@wordpress/i18n';
11
12
 
12
13
  /**
@@ -14,6 +15,7 @@ import { __ } from '@wordpress/i18n';
14
15
  */
15
16
  import DataViewsContext from '../dataviews-context';
16
17
  import { VIEW_LAYOUTS } from '../dataviews-layouts';
18
+ import { useDelayedLoading } from '../../hooks/use-delayed-loading';
17
19
  import type { ViewBaseProps } from '../../types';
18
20
 
19
21
  type DataViewsLayoutProps = {
@@ -38,11 +40,29 @@ export default function DataViewsLayout( { className }: DataViewsLayoutProps ) {
38
40
  isItemClickable,
39
41
  renderItemLink,
40
42
  defaultLayouts,
43
+ containerRef,
41
44
  empty = <p>{ __( 'No results' ) }</p>,
42
45
  } = useContext( DataViewsContext );
43
46
 
47
+ const isDelayedInitialLoading = useDelayedLoading( ! hasInitiallyLoaded, {
48
+ delay: 200,
49
+ } );
50
+ // Until the initial data load completes, show a spinner (or nothing if fast).
51
+ // After that, render the layout component which preserves previous data
52
+ // while loading subsequent requests.
44
53
  if ( ! hasInitiallyLoaded ) {
45
- return null;
54
+ // If the initial data load is fast, don't show the loading state at all.
55
+ if ( ! isDelayedInitialLoading ) {
56
+ return null;
57
+ }
58
+ // If the initial data load takes more than 200ms, show the loading state.
59
+ return (
60
+ <div className="dataviews-loading">
61
+ <p>
62
+ <Spinner />
63
+ </p>
64
+ </div>
65
+ );
46
66
  }
47
67
 
48
68
  const ViewComponent = VIEW_LAYOUTS.find(
@@ -50,23 +70,25 @@ export default function DataViewsLayout( { className }: DataViewsLayoutProps ) {
50
70
  )?.component as ComponentType< ViewBaseProps< any > >;
51
71
 
52
72
  return (
53
- <ViewComponent
54
- className={ className }
55
- actions={ actions }
56
- data={ data }
57
- fields={ fields }
58
- getItemId={ getItemId }
59
- getItemLevel={ getItemLevel }
60
- isLoading={ isLoading }
61
- onChangeView={ onChangeView }
62
- onChangeSelection={ onChangeSelection }
63
- selection={ selection }
64
- setOpenedFilter={ setOpenedFilter }
65
- onClickItem={ onClickItem }
66
- renderItemLink={ renderItemLink }
67
- isItemClickable={ isItemClickable }
68
- view={ view }
69
- empty={ empty }
70
- />
73
+ <div className="dataviews-layout__container" ref={ containerRef }>
74
+ <ViewComponent
75
+ className={ className }
76
+ actions={ actions }
77
+ data={ data }
78
+ fields={ fields }
79
+ getItemId={ getItemId }
80
+ getItemLevel={ getItemLevel }
81
+ isLoading={ isLoading }
82
+ onChangeView={ onChangeView }
83
+ onChangeSelection={ onChangeSelection }
84
+ selection={ selection }
85
+ setOpenedFilter={ setOpenedFilter }
86
+ onClickItem={ onClickItem }
87
+ renderItemLink={ renderItemLink }
88
+ isItemClickable={ isItemClickable }
89
+ view={ view }
90
+ empty={ empty }
91
+ />
92
+ </div>
71
93
  );
72
94
  }
@@ -0,0 +1,8 @@
1
+ .dataviews-layout__container {
2
+ flex: 1;
3
+ min-height: 0;
4
+ overflow: auto;
5
+ display: flex;
6
+ flex-direction: column;
7
+ background-color: inherit;
8
+ }
@@ -372,7 +372,13 @@ export default function CompositeGrid< Item >( {
372
372
  return (
373
373
  <Composite
374
374
  role={ isInfiniteScroll ? 'feed' : 'grid' }
375
- className={ clsx( 'dataviews-view-grid', className ) }
375
+ className={ clsx( 'dataviews-view-grid', className, {
376
+ [ `has-${ view.layout?.density }-density` ]:
377
+ view.layout?.density &&
378
+ [ 'compact', 'comfortable' ].includes(
379
+ view.layout.density
380
+ ),
381
+ } ) }
376
382
  focusWrap
377
383
  aria-busy={ isLoading }
378
384
  aria-rowcount={ isInfiniteScroll ? undefined : totalRows }
@@ -9,7 +9,7 @@
9
9
  padding: 0 $grid-unit-30 $grid-unit-30;
10
10
  display: flex;
11
11
  flex-direction: column;
12
- gap: $grid-unit-40;
12
+ gap: $grid-unit-30;
13
13
  container-type: inline-size;
14
14
  margin-bottom: auto;
15
15
 
@@ -17,9 +17,25 @@
17
17
  transition: padding ease-out 0.1s;
18
18
  }
19
19
 
20
+ &.has-compact-density {
21
+ gap: $grid-unit-20;
22
+
23
+ .dataviews-view-grid__row {
24
+ gap: $grid-unit-20;
25
+ }
26
+ }
27
+
28
+ &.has-comfortable-density {
29
+ gap: $grid-unit-40;
30
+
31
+ .dataviews-view-grid__row {
32
+ gap: $grid-unit-40;
33
+ }
34
+ }
35
+
20
36
  .dataviews-view-grid__row {
21
37
  display: grid;
22
- gap: $grid-unit-40;
38
+ gap: $grid-unit-30;
23
39
 
24
40
  .dataviews-view-grid__row__gridcell {
25
41
  border-radius: $radius-medium;
@@ -27,8 +27,8 @@ import {
27
27
  LAYOUT_PICKER_GRID,
28
28
  LAYOUT_PICKER_TABLE,
29
29
  } from '../../constants';
30
- import PreviewSizePicker from './utils/preview-size-picker';
31
30
  import DensityPicker from './utils/density-picker';
31
+ import GridConfigOptions from './utils/grid-config-options';
32
32
 
33
33
  export const VIEW_LAYOUTS = [
34
34
  {
@@ -43,7 +43,7 @@ export const VIEW_LAYOUTS = [
43
43
  label: __( 'Grid' ),
44
44
  component: ViewGrid,
45
45
  icon: category,
46
- viewConfigOptions: PreviewSizePicker,
46
+ viewConfigOptions: GridConfigOptions,
47
47
  },
48
48
  {
49
49
  type: LAYOUT_LIST,
@@ -64,7 +64,7 @@ export const VIEW_LAYOUTS = [
64
64
  label: __( 'Grid' ),
65
65
  component: ViewPickerGrid,
66
66
  icon: category,
67
- viewConfigOptions: PreviewSizePicker,
67
+ viewConfigOptions: GridConfigOptions,
68
68
  isPicker: true,
69
69
  },
70
70
  {