@wordpress/dataviews 1.1.0 → 2.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 (176) hide show
  1. package/CHANGELOG.md +27 -5
  2. package/README.md +33 -30
  3. package/build/add-filter.js +30 -22
  4. package/build/add-filter.js.map +1 -1
  5. package/build/bulk-actions-toolbar.js +187 -0
  6. package/build/bulk-actions-toolbar.js.map +1 -0
  7. package/build/bulk-actions.js +75 -62
  8. package/build/bulk-actions.js.map +1 -1
  9. package/build/constants.js +17 -10
  10. package/build/constants.js.map +1 -1
  11. package/build/dataviews.js +64 -50
  12. package/build/dataviews.js.map +1 -1
  13. package/build/filter-and-sort-data-view.js +2 -2
  14. package/build/filter-and-sort-data-view.js.map +1 -1
  15. package/build/filter-summary.js +106 -96
  16. package/build/filter-summary.js.map +1 -1
  17. package/build/filters.js +18 -17
  18. package/build/filters.js.map +1 -1
  19. package/build/index.js.map +1 -1
  20. package/build/item-actions.js +101 -69
  21. package/build/item-actions.js.map +1 -1
  22. package/build/layouts.js.map +1 -1
  23. package/build/lock-unlock.js.map +1 -1
  24. package/build/normalize-fields.js.map +1 -1
  25. package/build/pagination.js +66 -57
  26. package/build/pagination.js.map +1 -1
  27. package/build/reset-filters.js +9 -4
  28. package/build/reset-filters.js.map +1 -1
  29. package/build/search-widget.js +108 -89
  30. package/build/search-widget.js.map +1 -1
  31. package/build/search.js +13 -6
  32. package/build/search.js.map +1 -1
  33. package/build/single-selection-checkbox.js +6 -2
  34. package/build/single-selection-checkbox.js.map +1 -1
  35. package/build/types.js.map +1 -1
  36. package/build/utils.js +3 -15
  37. package/build/utils.js.map +1 -1
  38. package/build/view-actions.js +168 -120
  39. package/build/view-actions.js.map +1 -1
  40. package/build/view-grid.js +119 -106
  41. package/build/view-grid.js.map +1 -1
  42. package/build/view-list.js +217 -83
  43. package/build/view-list.js.map +1 -1
  44. package/build/view-table.js +227 -199
  45. package/build/view-table.js.map +1 -1
  46. package/build-module/add-filter.js +30 -22
  47. package/build-module/add-filter.js.map +1 -1
  48. package/build-module/bulk-actions-toolbar.js +182 -0
  49. package/build-module/bulk-actions-toolbar.js.map +1 -0
  50. package/build-module/bulk-actions.js +77 -62
  51. package/build-module/bulk-actions.js.map +1 -1
  52. package/build-module/constants.js +16 -9
  53. package/build-module/constants.js.map +1 -1
  54. package/build-module/dataviews.js +65 -50
  55. package/build-module/dataviews.js.map +1 -1
  56. package/build-module/filter-and-sort-data-view.js +2 -2
  57. package/build-module/filter-and-sort-data-view.js.map +1 -1
  58. package/build-module/filter-summary.js +107 -97
  59. package/build-module/filter-summary.js.map +1 -1
  60. package/build-module/filters.js +18 -17
  61. package/build-module/filters.js.map +1 -1
  62. package/build-module/index.js.map +1 -1
  63. package/build-module/item-actions.js +102 -71
  64. package/build-module/item-actions.js.map +1 -1
  65. package/build-module/layouts.js.map +1 -1
  66. package/build-module/lock-unlock.js.map +1 -1
  67. package/build-module/normalize-fields.js.map +1 -1
  68. package/build-module/pagination.js +67 -57
  69. package/build-module/pagination.js.map +1 -1
  70. package/build-module/reset-filters.js +9 -4
  71. package/build-module/reset-filters.js.map +1 -1
  72. package/build-module/search-widget.js +109 -89
  73. package/build-module/search-widget.js.map +1 -1
  74. package/build-module/search.js +13 -6
  75. package/build-module/search.js.map +1 -1
  76. package/build-module/single-selection-checkbox.js +6 -2
  77. package/build-module/single-selection-checkbox.js.map +1 -1
  78. package/build-module/types.js.map +1 -1
  79. package/build-module/utils.js +2 -13
  80. package/build-module/utils.js.map +1 -1
  81. package/build-module/view-actions.js +170 -121
  82. package/build-module/view-actions.js.map +1 -1
  83. package/build-module/view-grid.js +121 -106
  84. package/build-module/view-grid.js.map +1 -1
  85. package/build-module/view-list.js +219 -85
  86. package/build-module/view-list.js.map +1 -1
  87. package/build-module/view-table.js +230 -201
  88. package/build-module/view-table.js.map +1 -1
  89. package/build-style/style-rtl.css +168 -44
  90. package/build-style/style.css +168 -44
  91. package/build-types/add-filter.d.ts +11 -0
  92. package/build-types/add-filter.d.ts.map +1 -0
  93. package/build-types/bulk-actions-toolbar.d.ts +12 -0
  94. package/build-types/bulk-actions-toolbar.d.ts.map +1 -0
  95. package/build-types/bulk-actions.d.ts +14 -0
  96. package/build-types/bulk-actions.d.ts.map +1 -0
  97. package/build-types/constants.d.ts +19 -32
  98. package/build-types/constants.d.ts.map +1 -1
  99. package/build-types/dataviews.d.ts +22 -0
  100. package/build-types/dataviews.d.ts.map +1 -0
  101. package/build-types/filter-and-sort-data-view.d.ts +3 -3
  102. package/build-types/filter-and-sort-data-view.d.ts.map +1 -1
  103. package/build-types/filter-summary.d.ts +14 -0
  104. package/build-types/filter-summary.d.ts.map +1 -0
  105. package/build-types/filters.d.ts +13 -0
  106. package/build-types/filters.d.ts.map +1 -0
  107. package/build-types/index.d.ts +4 -0
  108. package/build-types/index.d.ts.map +1 -0
  109. package/build-types/item-actions.d.ts +35 -0
  110. package/build-types/item-actions.d.ts.map +1 -0
  111. package/build-types/layouts.d.ts +24 -0
  112. package/build-types/layouts.d.ts.map +1 -0
  113. package/build-types/lock-unlock.d.ts +2 -0
  114. package/build-types/lock-unlock.d.ts.map +1 -0
  115. package/build-types/normalize-fields.d.ts +2 -2
  116. package/build-types/normalize-fields.d.ts.map +1 -1
  117. package/build-types/pagination.d.ts +16 -0
  118. package/build-types/pagination.d.ts.map +1 -0
  119. package/build-types/reset-filters.d.ts +13 -0
  120. package/build-types/reset-filters.d.ts.map +1 -0
  121. package/build-types/search-widget.d.ts +10 -0
  122. package/build-types/search-widget.d.ts.map +1 -0
  123. package/build-types/search.d.ts +13 -0
  124. package/build-types/search.d.ts.map +1 -0
  125. package/build-types/single-selection-checkbox.d.ts +17 -0
  126. package/build-types/single-selection-checkbox.d.ts.map +1 -0
  127. package/build-types/stories/fixtures.d.ts +114 -0
  128. package/build-types/stories/fixtures.d.ts.map +1 -0
  129. package/build-types/stories/index.story.d.ts +15 -0
  130. package/build-types/stories/index.story.d.ts.map +1 -0
  131. package/build-types/types.d.ts +221 -21
  132. package/build-types/types.d.ts.map +1 -1
  133. package/build-types/utils.d.ts +3 -0
  134. package/build-types/utils.d.ts.map +1 -0
  135. package/build-types/view-actions.d.ts +12 -0
  136. package/build-types/view-actions.d.ts.map +1 -0
  137. package/build-types/view-grid.d.ts +4 -0
  138. package/build-types/view-grid.d.ts.map +1 -0
  139. package/build-types/view-list.d.ts +4 -0
  140. package/build-types/view-list.d.ts.map +1 -0
  141. package/build-types/view-table.d.ts +5 -0
  142. package/build-types/view-table.d.ts.map +1 -0
  143. package/package.json +12 -13
  144. package/src/{add-filter.js → add-filter.tsx} +17 -1
  145. package/src/bulk-actions-toolbar.tsx +272 -0
  146. package/src/{bulk-actions.js → bulk-actions.tsx} +77 -17
  147. package/src/constants.ts +12 -5
  148. package/src/{dataviews.js → dataviews.tsx} +54 -14
  149. package/src/filter-and-sort-data-view.ts +13 -8
  150. package/src/{filter-summary.js → filter-summary.tsx} +38 -9
  151. package/src/{filters.js → filters.tsx} +18 -6
  152. package/src/{item-actions.js → item-actions.tsx} +119 -30
  153. package/src/normalize-fields.ts +4 -2
  154. package/src/{pagination.js → pagination.tsx} +29 -8
  155. package/src/{reset-filters.js → reset-filters.tsx} +17 -2
  156. package/src/{search-widget.js → search-widget.tsx} +27 -7
  157. package/src/{search.js → search.tsx} +22 -5
  158. package/src/{single-selection-checkbox.js → single-selection-checkbox.tsx} +17 -2
  159. package/src/style.scss +166 -43
  160. package/src/types.ts +286 -21
  161. package/src/{utils.js → utils.ts} +5 -13
  162. package/src/{view-actions.js → view-actions.tsx} +105 -49
  163. package/src/{view-grid.js → view-grid.tsx} +31 -18
  164. package/src/view-list.tsx +410 -0
  165. package/src/{view-table.js → view-table.tsx} +99 -40
  166. package/tsconfig.json +3 -4
  167. package/tsconfig.tsbuildinfo +1 -1
  168. package/build/dropdown-menu-helper.js +0 -71
  169. package/build/dropdown-menu-helper.js.map +0 -1
  170. package/build-module/dropdown-menu-helper.js +0 -64
  171. package/build-module/dropdown-menu-helper.js.map +0 -1
  172. package/src/dropdown-menu-helper.js +0 -61
  173. package/src/view-list.js +0 -207
  174. /package/src/{index.js → index.ts} +0 -0
  175. /package/src/{layouts.js → layouts.ts} +0 -0
  176. /package/src/{lock-unlock.js → lock-unlock.ts} +0 -0
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import classnames from 'classnames';
4
+ import clsx from 'clsx';
5
5
 
6
6
  /**
7
7
  * WordPress dependencies
@@ -21,10 +21,24 @@ import { __ } from '@wordpress/i18n';
21
21
  */
22
22
  import ItemActions from './item-actions';
23
23
  import SingleSelectionCheckbox from './single-selection-checkbox';
24
-
25
24
  import { useHasAPossibleBulkAction } from './bulk-actions';
25
+ import type { Action, AnyItem, NormalizedField, ViewGridProps } from './types';
26
+
27
+ interface GridItemProps< Item extends AnyItem > {
28
+ selection: string[];
29
+ data: Item[];
30
+ onSelectionChange: ( items: Item[] ) => void;
31
+ getItemId: ( item: Item ) => string;
32
+ item: Item;
33
+ actions: Action< Item >[];
34
+ mediaField?: NormalizedField< Item >;
35
+ primaryField?: NormalizedField< Item >;
36
+ visibleFields: NormalizedField< Item >[];
37
+ badgeFields: NormalizedField< Item >[];
38
+ columnFields?: string[];
39
+ }
26
40
 
27
- function GridItem( {
41
+ function GridItem< Item extends AnyItem >( {
28
42
  selection,
29
43
  data,
30
44
  onSelectionChange,
@@ -36,7 +50,7 @@ function GridItem( {
36
50
  visibleFields,
37
51
  badgeFields,
38
52
  columnFields,
39
- } ) {
53
+ }: GridItemProps< Item > ) {
40
54
  const hasBulkAction = useHasAPossibleBulkAction( actions, item );
41
55
  const id = getItemId( item );
42
56
  const isSelected = selection.includes( id );
@@ -44,7 +58,7 @@ function GridItem( {
44
58
  <VStack
45
59
  spacing={ 0 }
46
60
  key={ id }
47
- className={ classnames( 'dataviews-view-grid__card', {
61
+ className={ clsx( 'dataviews-view-grid__card', {
48
62
  'is-selected': hasBulkAction && isSelected,
49
63
  } ) }
50
64
  onClickCapture={ ( event ) => {
@@ -86,7 +100,6 @@ function GridItem( {
86
100
  className="dataviews-view-grid__title-actions"
87
101
  >
88
102
  <SingleSelectionCheckbox
89
- id={ id }
90
103
  item={ item }
91
104
  selection={ selection }
92
105
  onSelectionChange={ onSelectionChange }
@@ -105,7 +118,7 @@ function GridItem( {
105
118
  className="dataviews-view-grid__badge-fields"
106
119
  spacing={ 2 }
107
120
  wrap
108
- align="top"
121
+ alignment="top"
109
122
  justify="flex-start"
110
123
  >
111
124
  { badgeFields.map( ( field ) => {
@@ -118,7 +131,7 @@ function GridItem( {
118
131
  return (
119
132
  <FlexItem
120
133
  key={ field.id }
121
- className={ 'dataviews-view-grid__field-value' }
134
+ className="dataviews-view-grid__field-value"
122
135
  >
123
136
  { renderedValue }
124
137
  </FlexItem>
@@ -137,7 +150,7 @@ function GridItem( {
137
150
  }
138
151
  return (
139
152
  <Flex
140
- className={ classnames(
153
+ className={ clsx(
141
154
  'dataviews-view-grid__field',
142
155
  columnFields?.includes( field.id )
143
156
  ? 'is-column'
@@ -174,16 +187,16 @@ function GridItem( {
174
187
  );
175
188
  }
176
189
 
177
- export default function ViewGrid( {
190
+ export default function ViewGrid< Item extends AnyItem >( {
191
+ actions,
178
192
  data,
179
193
  fields,
180
- view,
181
- actions,
182
- isLoading,
183
194
  getItemId,
184
- selection,
195
+ isLoading,
185
196
  onSelectionChange,
186
- } ) {
197
+ selection,
198
+ view,
199
+ }: ViewGridProps< Item > ) {
187
200
  const mediaField = fields.find(
188
201
  ( field ) => field.id === view.layout.mediaField
189
202
  );
@@ -191,7 +204,7 @@ export default function ViewGrid( {
191
204
  ( field ) => field.id === view.layout.primaryField
192
205
  );
193
206
  const { visibleFields, badgeFields } = fields.reduce(
194
- ( accumulator, field ) => {
207
+ ( accumulator: Record< string, NormalizedField< Item >[] >, field ) => {
195
208
  if (
196
209
  view.hiddenFields.includes( field.id ) ||
197
210
  [ view.layout.mediaField, view.layout.primaryField ].includes(
@@ -215,7 +228,7 @@ export default function ViewGrid( {
215
228
  <>
216
229
  { hasData && (
217
230
  <Grid
218
- gap={ 6 }
231
+ gap={ 8 }
219
232
  columns={ 2 }
220
233
  alignment="top"
221
234
  className="dataviews-view-grid"
@@ -243,7 +256,7 @@ export default function ViewGrid( {
243
256
  ) }
244
257
  { ! hasData && (
245
258
  <div
246
- className={ classnames( {
259
+ className={ clsx( {
247
260
  'dataviews-loading': isLoading,
248
261
  'dataviews-no-results': ! isLoading,
249
262
  } ) }
@@ -0,0 +1,410 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import clsx from 'clsx';
5
+ // Import CompositeStore type, which is not exported from @wordpress/components.
6
+ // eslint-disable-next-line no-restricted-imports
7
+ import type { CompositeStore } from '@ariakit/react';
8
+
9
+ /**
10
+ * WordPress dependencies
11
+ */
12
+ import { useInstanceId } from '@wordpress/compose';
13
+ import {
14
+ __experimentalHStack as HStack,
15
+ __experimentalVStack as VStack,
16
+ Button,
17
+ privateApis as componentsPrivateApis,
18
+ Spinner,
19
+ VisuallyHidden,
20
+ } from '@wordpress/components';
21
+ import {
22
+ useCallback,
23
+ useEffect,
24
+ useMemo,
25
+ useRef,
26
+ useState,
27
+ } from '@wordpress/element';
28
+ import { __ } from '@wordpress/i18n';
29
+ import { moreVertical } from '@wordpress/icons';
30
+
31
+ /**
32
+ * Internal dependencies
33
+ */
34
+ import { unlock } from './lock-unlock';
35
+ import type { Action, AnyItem, NormalizedField, ViewListProps } from './types';
36
+
37
+ import { ActionsDropdownMenuGroup, ActionModal } from './item-actions';
38
+
39
+ interface ListViewItemProps< Item extends AnyItem > {
40
+ actions: Action< Item >[];
41
+ id?: string;
42
+ isSelected: boolean;
43
+ item: Item;
44
+ mediaField?: NormalizedField< Item >;
45
+ onSelect: ( item: Item ) => void;
46
+ primaryField?: NormalizedField< Item >;
47
+ store: CompositeStore;
48
+ visibleFields: NormalizedField< Item >[];
49
+ }
50
+
51
+ const {
52
+ useCompositeStoreV2: useCompositeStore,
53
+ CompositeV2: Composite,
54
+ CompositeItemV2: CompositeItem,
55
+ CompositeRowV2: CompositeRow,
56
+ DropdownMenuV2: DropdownMenu,
57
+ } = unlock( componentsPrivateApis );
58
+
59
+ function ListItem< Item extends AnyItem >( {
60
+ actions,
61
+ id,
62
+ isSelected,
63
+ item,
64
+ mediaField,
65
+ onSelect,
66
+ primaryField,
67
+ store,
68
+ visibleFields,
69
+ }: ListViewItemProps< Item > ) {
70
+ const itemRef = useRef< HTMLElement >( null );
71
+ const labelId = `${ id }-label`;
72
+ const descriptionId = `${ id }-description`;
73
+
74
+ const [ isHovered, setIsHovered ] = useState( false );
75
+ const handleMouseEnter = () => {
76
+ setIsHovered( true );
77
+ };
78
+ const handleMouseLeave = () => {
79
+ setIsHovered( false );
80
+ };
81
+
82
+ useEffect( () => {
83
+ if ( isSelected ) {
84
+ itemRef.current?.scrollIntoView( {
85
+ behavior: 'auto',
86
+ block: 'nearest',
87
+ inline: 'nearest',
88
+ } );
89
+ }
90
+ }, [ isSelected ] );
91
+
92
+ const { primaryAction, eligibleActions } = useMemo( () => {
93
+ // If an action is eligible for all items, doesn't need
94
+ // to provide the `isEligible` function.
95
+ const _eligibleActions = actions.filter(
96
+ ( action ) => ! action.isEligible || action.isEligible( item )
97
+ );
98
+ const _primaryActions = _eligibleActions.filter(
99
+ ( action ) => action.isPrimary && !! action.icon
100
+ );
101
+ return {
102
+ primaryAction: _primaryActions?.[ 0 ],
103
+ eligibleActions: _eligibleActions,
104
+ };
105
+ }, [ actions, item ] );
106
+
107
+ const [ isModalOpen, setIsModalOpen ] = useState( false );
108
+ const primaryActionLabel =
109
+ primaryAction &&
110
+ ( typeof primaryAction.label === 'string'
111
+ ? primaryAction.label
112
+ : primaryAction.label( [ item ] ) );
113
+
114
+ return (
115
+ <CompositeRow
116
+ ref={ itemRef }
117
+ render={ <li /> }
118
+ role="row"
119
+ className={ clsx( {
120
+ 'is-selected': isSelected,
121
+ 'is-hovered': isHovered,
122
+ } ) }
123
+ onMouseEnter={ handleMouseEnter }
124
+ onMouseLeave={ handleMouseLeave }
125
+ >
126
+ <HStack
127
+ className="dataviews-view-list__item-wrapper"
128
+ alignment="center"
129
+ spacing={ 0 }
130
+ >
131
+ <div role="gridcell">
132
+ <CompositeItem
133
+ store={ store }
134
+ render={ <div /> }
135
+ role="button"
136
+ id={ id }
137
+ aria-pressed={ isSelected }
138
+ aria-labelledby={ labelId }
139
+ aria-describedby={ descriptionId }
140
+ className="dataviews-view-list__item"
141
+ onClick={ () => onSelect( item ) }
142
+ >
143
+ <HStack
144
+ spacing={ 3 }
145
+ justify="start"
146
+ alignment="flex-start"
147
+ >
148
+ <div className="dataviews-view-list__media-wrapper">
149
+ { mediaField?.render( { item } ) || (
150
+ <div className="dataviews-view-list__media-placeholder"></div>
151
+ ) }
152
+ </div>
153
+ <VStack spacing={ 0 }>
154
+ <span
155
+ className="dataviews-view-list__primary-field"
156
+ id={ labelId }
157
+ >
158
+ { primaryField?.render( { item } ) }
159
+ </span>
160
+ <div
161
+ className="dataviews-view-list__fields"
162
+ id={ descriptionId }
163
+ >
164
+ { visibleFields.map( ( field ) => (
165
+ <div
166
+ key={ field.id }
167
+ className="dataviews-view-list__field"
168
+ >
169
+ <VisuallyHidden
170
+ as="span"
171
+ className="dataviews-view-list__field-label"
172
+ >
173
+ { field.header }
174
+ </VisuallyHidden>
175
+ <span className="dataviews-view-list__field-value">
176
+ { field.render( { item } ) }
177
+ </span>
178
+ </div>
179
+ ) ) }
180
+ </div>
181
+ </VStack>
182
+ </HStack>
183
+ </CompositeItem>
184
+ </div>
185
+ { actions?.length > 0 && (
186
+ <HStack
187
+ spacing={ 1 }
188
+ justify="flex-end"
189
+ className="dataviews-view-list__item-actions"
190
+ style={ {
191
+ flexShrink: '0',
192
+ width: 'auto',
193
+ } }
194
+ >
195
+ { primaryAction && 'RenderModal' in primaryAction && (
196
+ <div role="gridcell">
197
+ <CompositeItem
198
+ store={ store }
199
+ render={
200
+ <Button
201
+ label={ primaryActionLabel }
202
+ icon={ primaryAction.icon }
203
+ isDestructive={
204
+ primaryAction.isDestructive
205
+ }
206
+ size="compact"
207
+ onClick={ () =>
208
+ setIsModalOpen( true )
209
+ }
210
+ />
211
+ }
212
+ >
213
+ { isModalOpen && (
214
+ <ActionModal< Item >
215
+ action={ primaryAction }
216
+ items={ [ item ] }
217
+ closeModal={ () =>
218
+ setIsModalOpen( false )
219
+ }
220
+ />
221
+ ) }
222
+ </CompositeItem>
223
+ </div>
224
+ ) }
225
+ { primaryAction &&
226
+ ! ( 'RenderModal' in primaryAction ) && (
227
+ <div role="gridcell" key={ primaryAction.id }>
228
+ <CompositeItem
229
+ store={ store }
230
+ render={
231
+ <Button
232
+ label={ primaryActionLabel }
233
+ icon={ primaryAction.icon }
234
+ isDestructive={
235
+ primaryAction.isDestructive
236
+ }
237
+ size="compact"
238
+ onClick={ () =>
239
+ primaryAction.callback( [
240
+ item,
241
+ ] )
242
+ }
243
+ />
244
+ }
245
+ />
246
+ </div>
247
+ ) }
248
+ <div role="gridcell">
249
+ <DropdownMenu
250
+ trigger={
251
+ <CompositeItem
252
+ store={ store }
253
+ render={
254
+ <Button
255
+ size="compact"
256
+ icon={ moreVertical }
257
+ label={ __( 'Actions' ) }
258
+ disabled={ ! actions.length }
259
+ onKeyDown={ ( event: {
260
+ key: string;
261
+ preventDefault: () => void;
262
+ } ) => {
263
+ if (
264
+ event.key ===
265
+ 'ArrowDown'
266
+ ) {
267
+ // Prevent the default behaviour (open dropdown menu) and go down.
268
+ event.preventDefault();
269
+ store.move(
270
+ store.down()
271
+ );
272
+ }
273
+ if (
274
+ event.key === 'ArrowUp'
275
+ ) {
276
+ // Prevent the default behavior (open dropdown menu) and go up.
277
+ event.preventDefault();
278
+ store.move(
279
+ store.up()
280
+ );
281
+ }
282
+ } }
283
+ />
284
+ }
285
+ />
286
+ }
287
+ placement="bottom-end"
288
+ >
289
+ <ActionsDropdownMenuGroup
290
+ actions={ eligibleActions }
291
+ item={ item }
292
+ />
293
+ </DropdownMenu>
294
+ </div>
295
+ </HStack>
296
+ ) }
297
+ </HStack>
298
+ </CompositeRow>
299
+ );
300
+ }
301
+
302
+ export default function ViewList< Item extends AnyItem >(
303
+ props: ViewListProps< Item >
304
+ ) {
305
+ const {
306
+ actions,
307
+ data,
308
+ fields,
309
+ getItemId,
310
+ isLoading,
311
+ onSelectionChange,
312
+ selection,
313
+ view,
314
+ } = props;
315
+ const baseId = useInstanceId( ViewList, 'view-list' );
316
+ const selectedItem = data?.findLast( ( item ) =>
317
+ selection.includes( item.id )
318
+ );
319
+
320
+ const mediaField = fields.find(
321
+ ( field ) => field.id === view.layout.mediaField
322
+ );
323
+ const primaryField = fields.find(
324
+ ( field ) => field.id === view.layout.primaryField
325
+ );
326
+ const visibleFields = fields.filter(
327
+ ( field ) =>
328
+ ! view.hiddenFields.includes( field.id ) &&
329
+ ! [ view.layout.primaryField, view.layout.mediaField ].includes(
330
+ field.id
331
+ )
332
+ );
333
+
334
+ const onSelect = useCallback(
335
+ ( item: Item ) => onSelectionChange( [ item ] ),
336
+ [ onSelectionChange ]
337
+ );
338
+
339
+ const getItemDomId = useCallback(
340
+ ( item?: Item ) =>
341
+ item ? `${ baseId }-${ getItemId( item ) }` : undefined,
342
+ [ baseId, getItemId ]
343
+ );
344
+
345
+ const store = useCompositeStore( {
346
+ defaultActiveId: getItemDomId( selectedItem ),
347
+ } );
348
+
349
+ // Manage focused item, when the active one is removed from the list.
350
+ const isActiveIdInList = store.useState(
351
+ ( state: { items: any[]; activeId: any } ) =>
352
+ state.items.some(
353
+ ( item: { id: any } ) => item.id === state.activeId
354
+ )
355
+ );
356
+ useEffect( () => {
357
+ if ( ! isActiveIdInList ) {
358
+ // Prefer going down, except if there is no item below (last item), then go up (last item in list).
359
+ if ( store.down() ) {
360
+ store.move( store.down() );
361
+ } else if ( store.up() ) {
362
+ store.move( store.up() );
363
+ }
364
+ }
365
+ }, [ isActiveIdInList ] );
366
+
367
+ const hasData = data?.length;
368
+ if ( ! hasData ) {
369
+ return (
370
+ <div
371
+ className={ clsx( {
372
+ 'dataviews-loading': isLoading,
373
+ 'dataviews-no-results': ! hasData && ! isLoading,
374
+ } ) }
375
+ >
376
+ { ! hasData && (
377
+ <p>{ isLoading ? <Spinner /> : __( 'No results' ) }</p>
378
+ ) }
379
+ </div>
380
+ );
381
+ }
382
+
383
+ return (
384
+ <Composite
385
+ id={ baseId }
386
+ render={ <ul /> }
387
+ className="dataviews-view-list"
388
+ role="grid"
389
+ store={ store }
390
+ >
391
+ { data.map( ( item ) => {
392
+ const id = getItemDomId( item );
393
+ return (
394
+ <ListItem
395
+ key={ id }
396
+ id={ id }
397
+ actions={ actions }
398
+ item={ item }
399
+ isSelected={ item === selectedItem }
400
+ onSelect={ onSelect }
401
+ mediaField={ mediaField }
402
+ primaryField={ primaryField }
403
+ store={ store }
404
+ visibleFields={ visibleFields }
405
+ />
406
+ );
407
+ } ) }
408
+ </Composite>
409
+ );
410
+ }