@wordpress/dataviews 0.2.0 → 0.3.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 (87) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/LICENSE.md +1 -1
  3. package/README.md +30 -6
  4. package/build/add-filter.js +109 -49
  5. package/build/add-filter.js.map +1 -1
  6. package/build/constants.js +24 -2
  7. package/build/constants.js.map +1 -1
  8. package/build/dataviews.js +12 -9
  9. package/build/dataviews.js.map +1 -1
  10. package/build/dropdown-menu-helper.js +72 -0
  11. package/build/dropdown-menu-helper.js.map +1 -0
  12. package/build/filter-summary.js +43 -54
  13. package/build/filter-summary.js.map +1 -1
  14. package/build/filters.js +27 -17
  15. package/build/filters.js.map +1 -1
  16. package/build/index.js +13 -0
  17. package/build/index.js.map +1 -1
  18. package/build/item-actions.js +12 -12
  19. package/build/item-actions.js.map +1 -1
  20. package/build/pagination.js +31 -65
  21. package/build/pagination.js.map +1 -1
  22. package/build/reset-filters.js +8 -8
  23. package/build/reset-filters.js.map +1 -1
  24. package/build/search.js +8 -6
  25. package/build/search.js.map +1 -1
  26. package/build/utils.js +71 -0
  27. package/build/utils.js.map +1 -0
  28. package/build/view-actions.js +72 -95
  29. package/build/view-actions.js.map +1 -1
  30. package/build/view-grid.js +4 -6
  31. package/build/view-grid.js.map +1 -1
  32. package/build/view-list.js +26 -13
  33. package/build/view-list.js.map +1 -1
  34. package/build/view-table.js +153 -154
  35. package/build/view-table.js.map +1 -1
  36. package/build-module/add-filter.js +113 -53
  37. package/build-module/add-filter.js.map +1 -1
  38. package/build-module/constants.js +20 -0
  39. package/build-module/constants.js.map +1 -1
  40. package/build-module/dataviews.js +13 -10
  41. package/build-module/dataviews.js.map +1 -1
  42. package/build-module/dropdown-menu-helper.js +64 -0
  43. package/build-module/dropdown-menu-helper.js.map +1 -0
  44. package/build-module/filter-summary.js +45 -56
  45. package/build-module/filter-summary.js.map +1 -1
  46. package/build-module/filters.js +26 -17
  47. package/build-module/filters.js.map +1 -1
  48. package/build-module/index.js +1 -0
  49. package/build-module/index.js.map +1 -1
  50. package/build-module/item-actions.js +12 -12
  51. package/build-module/item-actions.js.map +1 -1
  52. package/build-module/pagination.js +35 -69
  53. package/build-module/pagination.js.map +1 -1
  54. package/build-module/reset-filters.js +6 -6
  55. package/build-module/reset-filters.js.map +1 -1
  56. package/build-module/search.js +7 -6
  57. package/build-module/search.js.map +1 -1
  58. package/build-module/utils.js +63 -0
  59. package/build-module/utils.js.map +1 -0
  60. package/build-module/view-actions.js +73 -97
  61. package/build-module/view-actions.js.map +1 -1
  62. package/build-module/view-grid.js +4 -6
  63. package/build-module/view-grid.js.map +1 -1
  64. package/build-module/view-list.js +27 -14
  65. package/build-module/view-list.js.map +1 -1
  66. package/build-module/view-table.js +156 -157
  67. package/build-module/view-table.js.map +1 -1
  68. package/build-style/style-rtl.css +180 -70
  69. package/build-style/style.css +180 -70
  70. package/package.json +11 -10
  71. package/src/add-filter.js +227 -68
  72. package/src/constants.js +16 -0
  73. package/src/dataviews.js +19 -12
  74. package/src/dropdown-menu-helper.js +61 -0
  75. package/src/filter-summary.js +70 -103
  76. package/src/filters.js +41 -24
  77. package/src/index.js +1 -0
  78. package/src/item-actions.js +30 -25
  79. package/src/pagination.js +75 -123
  80. package/src/reset-filters.js +5 -5
  81. package/src/search.js +8 -6
  82. package/src/style.scss +182 -48
  83. package/src/utils.js +51 -0
  84. package/src/view-actions.js +113 -114
  85. package/src/view-grid.js +4 -4
  86. package/src/view-list.js +42 -28
  87. package/src/view-table.js +280 -238
package/src/view-table.js CHANGED
@@ -1,123 +1,132 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import classNames from 'classnames';
5
+
1
6
  /**
2
7
  * WordPress dependencies
3
8
  */
4
9
  import { __ } from '@wordpress/i18n';
5
10
  import { useAsyncList } from '@wordpress/compose';
6
- import {
7
- chevronDown,
8
- chevronUp,
9
- unseen,
10
- check,
11
- arrowUp,
12
- arrowDown,
13
- chevronRightSmall,
14
- funnel,
15
- } from '@wordpress/icons';
11
+ import { unseen, funnel } from '@wordpress/icons';
16
12
  import {
17
13
  Button,
18
14
  Icon,
19
15
  privateApis as componentsPrivateApis,
20
16
  } from '@wordpress/components';
21
- import { Children, Fragment } from '@wordpress/element';
17
+ import {
18
+ Children,
19
+ Fragment,
20
+ forwardRef,
21
+ useEffect,
22
+ useId,
23
+ useRef,
24
+ useState,
25
+ } from '@wordpress/element';
22
26
 
23
27
  /**
24
28
  * Internal dependencies
25
29
  */
26
30
  import { unlock } from './lock-unlock';
27
31
  import ItemActions from './item-actions';
28
- import { ENUMERATION_TYPE, OPERATOR_IN, OPERATOR_NOT_IN } from './constants';
32
+ import { ENUMERATION_TYPE, OPERATORS, SORTING_DIRECTIONS } from './constants';
33
+ import { DropdownMenuRadioItemCustom } from './dropdown-menu-helper';
29
34
 
30
35
  const {
31
36
  DropdownMenuV2: DropdownMenu,
32
37
  DropdownMenuGroupV2: DropdownMenuGroup,
33
38
  DropdownMenuItemV2: DropdownMenuItem,
39
+ DropdownMenuRadioItemV2: DropdownMenuRadioItem,
34
40
  DropdownMenuSeparatorV2: DropdownMenuSeparator,
35
- DropdownSubMenuV2: DropdownSubMenu,
36
- DropdownSubMenuTriggerV2: DropdownSubMenuTrigger,
41
+ DropdownMenuItemLabelV2: DropdownMenuItemLabel,
42
+ DropdownMenuItemHelpTextV2: DropdownMenuItemHelpText,
37
43
  } = unlock( componentsPrivateApis );
38
44
 
39
- const sortingItemsInfo = {
40
- asc: { icon: arrowUp, label: __( 'Sort ascending' ) },
41
- desc: { icon: arrowDown, label: __( 'Sort descending' ) },
45
+ const sortArrows = { asc: '↑', desc: '↓' };
46
+
47
+ const sanitizeOperators = ( field ) => {
48
+ let operators = field.filterBy?.operators;
49
+ if ( ! operators || ! Array.isArray( operators ) ) {
50
+ operators = Object.keys( OPERATORS );
51
+ }
52
+ return operators.filter( ( operator ) =>
53
+ Object.keys( OPERATORS ).includes( operator )
54
+ );
42
55
  };
43
- const sortIcons = { asc: chevronUp, desc: chevronDown };
44
56
 
45
- function HeaderMenu( { field, view, onChangeView } ) {
46
- const isSortable = field.enableSorting !== false;
57
+ const HeaderMenu = forwardRef( function HeaderMenu(
58
+ { field, view, onChangeView, onHide },
59
+ ref
60
+ ) {
47
61
  const isHidable = field.enableHiding !== false;
48
- if ( ! isSortable && ! isHidable ) {
49
- return field.header;
50
- }
62
+
63
+ const isSortable = field.enableSorting !== false;
51
64
  const isSorted = view.sort?.field === field.id;
52
- let filter, filterInView;
53
- const otherFilters = [];
54
- if ( field.type === ENUMERATION_TYPE ) {
55
- let columnOperators = field.filterBy?.operators;
56
- if ( ! columnOperators || ! Array.isArray( columnOperators ) ) {
57
- columnOperators = [ OPERATOR_IN, OPERATOR_NOT_IN ];
58
- }
59
- const operators = columnOperators.filter( ( operator ) =>
60
- [ OPERATOR_IN, OPERATOR_NOT_IN ].includes( operator )
65
+
66
+ let filter, filterInView, activeElement, activeOperator, otherFilters;
67
+ const operators = sanitizeOperators( field );
68
+ if ( field.type === ENUMERATION_TYPE && operators.length > 0 ) {
69
+ filter = {
70
+ field: field.id,
71
+ operators,
72
+ elements: field.elements || [],
73
+ };
74
+ filterInView = view.filters.find( ( f ) => f.field === filter.field );
75
+ otherFilters = view.filters.filter( ( f ) => f.field !== filter.field );
76
+ activeElement = filter.elements.find(
77
+ ( element ) => element.value === filterInView?.value
61
78
  );
62
- if ( operators.length >= 0 ) {
63
- filter = {
64
- field: field.id,
65
- operators,
66
- elements: field.elements || [],
67
- };
68
- filterInView = {
69
- field: filter.field,
70
- operator: filter.operators[ 0 ],
71
- value: undefined,
72
- };
73
- }
79
+ activeOperator = filterInView?.operator || filter.operators[ 0 ];
74
80
  }
75
81
  const isFilterable = !! filter;
76
82
 
77
- if ( isFilterable ) {
78
- const columnFilters = view.filters;
79
- columnFilters.forEach( ( columnFilter ) => {
80
- if ( columnFilter.field === filter.field ) {
81
- filterInView = {
82
- ...columnFilter,
83
- };
84
- } else {
85
- otherFilters.push( columnFilter );
86
- }
87
- } );
83
+ if ( ! isSortable && ! isHidable && ! isFilterable ) {
84
+ return field.header;
88
85
  }
86
+
89
87
  return (
90
88
  <DropdownMenu
91
89
  align="start"
92
90
  trigger={
93
91
  <Button
94
- icon={ isSorted && sortIcons[ view.sort.direction ] }
95
- iconPosition="right"
96
- text={ field.header }
97
- style={ { padding: 0 } }
98
92
  size="compact"
99
- />
93
+ className="dataviews-view-table-header-button"
94
+ ref={ ref }
95
+ variant="tertiary"
96
+ >
97
+ { field.header }
98
+ { isSorted && (
99
+ <span aria-hidden="true">
100
+ { isSorted && sortArrows[ view.sort.direction ] }
101
+ </span>
102
+ ) }
103
+ </Button>
100
104
  }
105
+ style={ { minWidth: '240px' } }
101
106
  >
102
107
  <WithSeparators>
103
108
  { isSortable && (
104
109
  <DropdownMenuGroup>
105
- { Object.entries( sortingItemsInfo ).map(
110
+ { Object.entries( SORTING_DIRECTIONS ).map(
106
111
  ( [ direction, info ] ) => {
107
- const isActive =
112
+ const isChecked =
108
113
  isSorted &&
109
114
  view.sort.direction === direction;
115
+
116
+ const value = `${ field.id }-${ direction }`;
117
+
110
118
  return (
111
- <DropdownMenuItem
112
- key={ direction }
113
- role="menuitemradio"
114
- aria-checked={ isActive }
115
- prefix={ <Icon icon={ info.icon } /> }
116
- suffix={
117
- isActive && <Icon icon={ check } />
118
- }
119
- onSelect={ ( event ) => {
120
- event.preventDefault();
119
+ <DropdownMenuRadioItem
120
+ key={ value }
121
+ // All sorting radio items share the same name, so that
122
+ // selecting a sorting option automatically deselects the
123
+ // previously selected one, even if it is displayed in
124
+ // another submenu. The field and direction are passed via
125
+ // the `value` prop.
126
+ name="view-table-sorting"
127
+ value={ value }
128
+ checked={ isChecked }
129
+ onChange={ () => {
121
130
  onChangeView( {
122
131
  ...view,
123
132
  sort: {
@@ -127,8 +136,10 @@ function HeaderMenu( { field, view, onChangeView } ) {
127
136
  } );
128
137
  } }
129
138
  >
130
- { info.label }
131
- </DropdownMenuItem>
139
+ <DropdownMenuItemLabel>
140
+ { info.label }
141
+ </DropdownMenuItemLabel>
142
+ </DropdownMenuRadioItem>
132
143
  );
133
144
  }
134
145
  ) }
@@ -136,11 +147,9 @@ function HeaderMenu( { field, view, onChangeView } ) {
136
147
  ) }
137
148
  { isHidable && (
138
149
  <DropdownMenuItem
139
- role="menuitemradio"
140
- aria-checked={ false }
141
150
  prefix={ <Icon icon={ unseen } /> }
142
- onSelect={ ( event ) => {
143
- event.preventDefault();
151
+ onClick={ () => {
152
+ onHide( field );
144
153
  onChangeView( {
145
154
  ...view,
146
155
  hiddenFields: view.hiddenFields.concat(
@@ -149,57 +158,56 @@ function HeaderMenu( { field, view, onChangeView } ) {
149
158
  } );
150
159
  } }
151
160
  >
152
- { __( 'Hide' ) }
161
+ <DropdownMenuItemLabel>
162
+ { __( 'Hide' ) }
163
+ </DropdownMenuItemLabel>
153
164
  </DropdownMenuItem>
154
165
  ) }
155
166
  { isFilterable && (
156
167
  <DropdownMenuGroup>
157
- <DropdownSubMenu
168
+ <DropdownMenu
158
169
  key={ filter.field }
159
170
  trigger={
160
- <DropdownSubMenuTrigger
171
+ <DropdownMenuItem
161
172
  prefix={ <Icon icon={ funnel } /> }
162
173
  suffix={
163
- <Icon icon={ chevronRightSmall } />
174
+ activeElement && (
175
+ <span aria-hidden="true">
176
+ { activeOperator in OPERATORS &&
177
+ `${ OPERATORS[ activeOperator ].label } ` }
178
+ { activeElement?.label }
179
+ </span>
180
+ )
164
181
  }
165
182
  >
166
- { __( 'Filter by' ) }
167
- </DropdownSubMenuTrigger>
183
+ <DropdownMenuItemLabel>
184
+ { __( 'Filter by' ) }
185
+ </DropdownMenuItemLabel>
186
+ </DropdownMenuItem>
168
187
  }
169
188
  >
170
189
  <WithSeparators>
171
190
  <DropdownMenuGroup>
172
191
  { filter.elements.map( ( element ) => {
173
- let isActive = false;
174
- if ( filterInView ) {
175
- // Intentionally use loose comparison, so it does type conversion.
176
- // This covers the case where a top-level filter for the same field converts a number into a string.
177
- /* eslint-disable eqeqeq */
178
- isActive =
179
- element.value ==
180
- filterInView.value;
181
- /* eslint-enable eqeqeq */
182
- }
183
-
192
+ const isActive =
193
+ activeElement?.value ===
194
+ element.value;
184
195
  return (
185
- <DropdownMenuItem
196
+ <DropdownMenuRadioItemCustom
186
197
  key={ element.value }
187
- role="menuitemradio"
188
- aria-checked={ isActive }
189
- prefix={
190
- isActive && (
191
- <Icon icon={ check } />
192
- )
193
- }
194
- onSelect={ () => {
198
+ name={ `view-table-${ filter.field }` }
199
+ value={ element.value }
200
+ checked={ isActive }
201
+ onClick={ () => {
195
202
  onChangeView( {
196
203
  ...view,
204
+ page: 1,
197
205
  filters: [
198
206
  ...otherFilters,
199
207
  {
200
208
  field: filter.field,
201
209
  operator:
202
- filterInView?.operator,
210
+ activeOperator,
203
211
  value: isActive
204
212
  ? undefined
205
213
  : element.value,
@@ -208,103 +216,84 @@ function HeaderMenu( { field, view, onChangeView } ) {
208
216
  } );
209
217
  } }
210
218
  >
211
- { element.label }
212
- </DropdownMenuItem>
219
+ <DropdownMenuItemLabel>
220
+ { element.label }
221
+ </DropdownMenuItemLabel>
222
+ { !! element.description && (
223
+ <DropdownMenuItemHelpText>
224
+ { element.description }
225
+ </DropdownMenuItemHelpText>
226
+ ) }
227
+ </DropdownMenuRadioItemCustom>
213
228
  );
214
229
  } ) }
215
230
  </DropdownMenuGroup>
216
231
  { filter.operators.length > 1 && (
217
- <DropdownSubMenu
232
+ <DropdownMenu
218
233
  trigger={
219
- <DropdownSubMenuTrigger
234
+ <DropdownMenuItem
220
235
  suffix={
221
- <>
222
- { filterInView.operator ===
223
- OPERATOR_IN
224
- ? __( 'Is' )
225
- : __( 'Is not' ) }
226
- <Icon
227
- icon={
228
- chevronRightSmall
229
- }
230
- />{ ' ' }
231
- </>
236
+ <span aria-hidden="true">
237
+ {
238
+ OPERATORS[
239
+ activeOperator
240
+ ]?.label
241
+ }
242
+ </span>
232
243
  }
233
244
  >
234
- { __( 'Conditions' ) }
235
- </DropdownSubMenuTrigger>
245
+ <DropdownMenuItemLabel>
246
+ { __( 'Conditions' ) }
247
+ </DropdownMenuItemLabel>
248
+ </DropdownMenuItem>
236
249
  }
237
250
  >
238
- <DropdownMenuItem
239
- key="in-filter"
240
- role="menuitemradio"
241
- aria-checked={
242
- filterInView?.operator ===
243
- OPERATOR_IN
244
- }
245
- prefix={
246
- filterInView?.operator ===
247
- OPERATOR_IN && (
248
- <Icon icon={ check } />
249
- )
250
- }
251
- onSelect={ () =>
252
- onChangeView( {
253
- ...view,
254
- filters: [
255
- ...otherFilters,
256
- {
257
- field: filter.field,
258
- operator:
259
- OPERATOR_IN,
260
- value: filterInView?.value,
261
- },
262
- ],
263
- } )
264
- }
265
- >
266
- { __( 'Is' ) }
267
- </DropdownMenuItem>
268
- <DropdownMenuItem
269
- key="not-in-filter"
270
- role="menuitemradio"
271
- aria-checked={
272
- filterInView?.operator ===
273
- OPERATOR_NOT_IN
274
- }
275
- prefix={
276
- filterInView?.operator ===
277
- OPERATOR_NOT_IN && (
278
- <Icon icon={ check } />
279
- )
280
- }
281
- onSelect={ () =>
282
- onChangeView( {
283
- ...view,
284
- filters: [
285
- ...otherFilters,
286
- {
287
- field: filter.field,
288
- operator:
289
- OPERATOR_NOT_IN,
290
- value: filterInView?.value,
291
- },
292
- ],
293
- } )
294
- }
295
- >
296
- { __( 'Is not' ) }
297
- </DropdownMenuItem>
298
- </DropdownSubMenu>
251
+ { Object.entries( OPERATORS ).map(
252
+ ( [
253
+ operator,
254
+ { label, key },
255
+ ] ) => (
256
+ <DropdownMenuRadioItem
257
+ key={ key }
258
+ name={ `view-table-${ filter.field }-conditions` }
259
+ value={ operator }
260
+ checked={
261
+ activeOperator ===
262
+ operator
263
+ }
264
+ onChange={ ( e ) =>
265
+ onChangeView( {
266
+ ...view,
267
+ page: 1,
268
+ filters: [
269
+ ...otherFilters,
270
+ {
271
+ field: filter.field,
272
+ operator:
273
+ e.target
274
+ .value,
275
+ value: filterInView?.value,
276
+ },
277
+ ],
278
+ } )
279
+ }
280
+ >
281
+ <DropdownMenuItemLabel>
282
+ { label }
283
+ </DropdownMenuItemLabel>
284
+ </DropdownMenuRadioItem>
285
+ )
286
+ ) }
287
+ </DropdownMenu>
299
288
  ) }
300
289
  </WithSeparators>
301
- </DropdownSubMenu>
290
+ </DropdownMenu>
302
291
  </DropdownMenuGroup>
303
292
  ) }
304
293
  </WithSeparators>
305
294
  </DropdownMenu>
306
295
  );
307
- }
296
+ } );
308
297
 
309
298
  function WithSeparators( { children } ) {
310
299
  return Children.toArray( children )
@@ -327,6 +316,35 @@ function ViewTable( {
327
316
  isLoading = false,
328
317
  deferredRendering,
329
318
  } ) {
319
+ const headerMenuRefs = useRef( new Map() );
320
+ const headerMenuToFocusRef = useRef();
321
+ const [ nextHeaderMenuToFocus, setNextHeaderMenuToFocus ] = useState();
322
+
323
+ useEffect( () => {
324
+ if ( headerMenuToFocusRef.current ) {
325
+ headerMenuToFocusRef.current.focus();
326
+ headerMenuToFocusRef.current = undefined;
327
+ }
328
+ } );
329
+
330
+ const asyncData = useAsyncList( data );
331
+ const tableNoticeId = useId();
332
+
333
+ if ( nextHeaderMenuToFocus ) {
334
+ // If we need to force focus, we short-circuit rendering here
335
+ // to prevent any additional work while we handle that.
336
+ // Clearing out the focus directive is necessary to make sure
337
+ // future renders don't cause unexpected focus jumps.
338
+ headerMenuToFocusRef.current = nextHeaderMenuToFocus;
339
+ setNextHeaderMenuToFocus();
340
+ return;
341
+ }
342
+
343
+ const onHide = ( field ) => {
344
+ const hidden = headerMenuRefs.current.get( field.id );
345
+ const fallback = headerMenuRefs.current.get( hidden.fallback );
346
+ setNextHeaderMenuToFocus( fallback?.node );
347
+ };
330
348
  const visibleFields = fields.filter(
331
349
  ( field ) =>
332
350
  ! view.hiddenFields.includes( field.id ) &&
@@ -334,56 +352,75 @@ function ViewTable( {
334
352
  field.id
335
353
  )
336
354
  );
337
- const shownData = useAsyncList( data );
338
- const usedData = deferredRendering ? shownData : data;
355
+ const usedData = deferredRendering ? asyncData : data;
339
356
  const hasData = !! usedData?.length;
340
- if ( isLoading ) {
341
- // TODO:Add spinner or progress bar..
342
- return (
343
- <div className="dataviews-loading">
344
- <h3>{ __( 'Loading' ) }</h3>
345
- </div>
346
- );
347
- }
348
357
  const sortValues = { asc: 'ascending', desc: 'descending' };
358
+
349
359
  return (
350
- <div className="dataviews-table-view-wrapper">
351
- { hasData && (
352
- <table className="dataviews-table-view">
353
- <thead>
354
- <tr>
355
- { visibleFields.map( ( field ) => (
356
- <th
357
- key={ field.id }
358
- style={ {
359
- width: field.width || undefined,
360
- minWidth: field.minWidth || undefined,
361
- maxWidth: field.maxWidth || undefined,
360
+ <div>
361
+ <table
362
+ className="dataviews-view-table"
363
+ aria-busy={ isLoading }
364
+ aria-describedby={ tableNoticeId }
365
+ >
366
+ <thead>
367
+ <tr>
368
+ { visibleFields.map( ( field, index ) => (
369
+ <th
370
+ key={ field.id }
371
+ style={ {
372
+ width: field.width || undefined,
373
+ minWidth: field.minWidth || undefined,
374
+ maxWidth: field.maxWidth || undefined,
375
+ } }
376
+ data-field-id={ field.id }
377
+ aria-sort={
378
+ view.sort?.field === field.id &&
379
+ sortValues[ view.sort.direction ]
380
+ }
381
+ scope="col"
382
+ >
383
+ <HeaderMenu
384
+ ref={ ( node ) => {
385
+ if ( node ) {
386
+ headerMenuRefs.current.set(
387
+ field.id,
388
+ {
389
+ node,
390
+ fallback:
391
+ visibleFields[
392
+ index > 0
393
+ ? index - 1
394
+ : 1
395
+ ]?.id,
396
+ }
397
+ );
398
+ } else {
399
+ headerMenuRefs.current.delete(
400
+ field.id
401
+ );
402
+ }
362
403
  } }
363
- data-field-id={ field.id }
364
- aria-sort={
365
- view.sort?.field === field.id &&
366
- sortValues[ view.sort.direction ]
367
- }
368
- scope="col"
369
- >
370
- <HeaderMenu
371
- field={ field }
372
- view={ view }
373
- onChangeView={ onChangeView }
374
- />
375
- </th>
376
- ) ) }
377
- { !! actions?.length && (
378
- <th data-field-id="actions">
404
+ field={ field }
405
+ view={ view }
406
+ onChangeView={ onChangeView }
407
+ onHide={ onHide }
408
+ />
409
+ </th>
410
+ ) ) }
411
+ { !! actions?.length && (
412
+ <th data-field-id="actions">
413
+ <span className="dataviews-view-table-header">
379
414
  { __( 'Actions' ) }
380
- </th>
381
- ) }
382
- </tr>
383
- </thead>
384
- <tbody>
385
- { usedData.map( ( item, index ) => (
386
- <tr key={ getItemId?.( item ) || index }>
415
+ </span>
416
+ </th>
417
+ ) }
418
+ </tr>
419
+ </thead>
420
+ <tbody>
421
+ { hasData &&
422
+ usedData.map( ( item ) => (
423
+ <tr key={ getItemId( item ) }>
387
424
  { visibleFields.map( ( field ) => (
388
425
  <td
389
426
  key={ field.id }
@@ -410,14 +447,19 @@ function ViewTable( {
410
447
  ) }
411
448
  </tr>
412
449
  ) ) }
413
- </tbody>
414
- </table>
415
- ) }
416
- { ! hasData && (
417
- <div className="dataviews-no-results">
418
- <p>{ __( 'No results' ) }</p>
419
- </div>
420
- ) }
450
+ </tbody>
451
+ </table>
452
+ <div
453
+ className={ classNames( {
454
+ 'dataviews-loading': isLoading,
455
+ 'dataviews-no-results': ! hasData && ! isLoading,
456
+ } ) }
457
+ id={ tableNoticeId }
458
+ >
459
+ { ! hasData && (
460
+ <p>{ isLoading ? __( 'Loading…' ) : __( 'No results' ) }</p>
461
+ ) }
462
+ </div>
421
463
  </div>
422
464
  );
423
465
  }