@wordpress/dataviews 0.7.0 → 0.9.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 (79) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +49 -18
  3. package/build/constants.js +28 -10
  4. package/build/constants.js.map +1 -1
  5. package/build/dataviews.js +3 -7
  6. package/build/dataviews.js.map +1 -1
  7. package/build/filter-and-sort-data-view.js +147 -0
  8. package/build/filter-and-sort-data-view.js.map +1 -0
  9. package/build/filter-summary.js +33 -12
  10. package/build/filter-summary.js.map +1 -1
  11. package/build/filters.js +11 -16
  12. package/build/filters.js.map +1 -1
  13. package/build/index.js +3 -9
  14. package/build/index.js.map +1 -1
  15. package/build/item-actions.js +20 -39
  16. package/build/item-actions.js.map +1 -1
  17. package/build/normalize-fields.js +25 -0
  18. package/build/normalize-fields.js.map +1 -0
  19. package/build/pagination.js +2 -2
  20. package/build/pagination.js.map +1 -1
  21. package/build/search-widget.js +34 -10
  22. package/build/search-widget.js.map +1 -1
  23. package/build/utils.js +25 -67
  24. package/build/utils.js.map +1 -1
  25. package/build/view-grid.js +25 -12
  26. package/build/view-grid.js.map +1 -1
  27. package/build/view-list.js +122 -58
  28. package/build/view-list.js.map +1 -1
  29. package/build/view-table.js +53 -8
  30. package/build/view-table.js.map +1 -1
  31. package/build-module/constants.js +27 -9
  32. package/build-module/constants.js.map +1 -1
  33. package/build-module/dataviews.js +3 -7
  34. package/build-module/dataviews.js.map +1 -1
  35. package/build-module/filter-and-sort-data-view.js +139 -0
  36. package/build-module/filter-and-sort-data-view.js.map +1 -0
  37. package/build-module/filter-summary.js +34 -13
  38. package/build-module/filter-summary.js.map +1 -1
  39. package/build-module/filters.js +12 -17
  40. package/build-module/filters.js.map +1 -1
  41. package/build-module/index.js +1 -1
  42. package/build-module/index.js.map +1 -1
  43. package/build-module/item-actions.js +20 -39
  44. package/build-module/item-actions.js.map +1 -1
  45. package/build-module/normalize-fields.js +19 -0
  46. package/build-module/normalize-fields.js.map +1 -0
  47. package/build-module/pagination.js +2 -2
  48. package/build-module/pagination.js.map +1 -1
  49. package/build-module/search-widget.js +35 -11
  50. package/build-module/search-widget.js.map +1 -1
  51. package/build-module/utils.js +25 -66
  52. package/build-module/utils.js.map +1 -1
  53. package/build-module/view-grid.js +26 -13
  54. package/build-module/view-grid.js.map +1 -1
  55. package/build-module/view-list.js +124 -60
  56. package/build-module/view-list.js.map +1 -1
  57. package/build-module/view-table.js +55 -10
  58. package/build-module/view-table.js.map +1 -1
  59. package/build-style/style-rtl.css +41 -11
  60. package/build-style/style.css +41 -11
  61. package/package.json +11 -11
  62. package/src/constants.js +35 -9
  63. package/src/dataviews.js +3 -7
  64. package/src/filter-and-sort-data-view.js +154 -0
  65. package/src/filter-summary.js +76 -23
  66. package/src/filters.js +20 -26
  67. package/src/index.js +1 -1
  68. package/src/item-actions.js +19 -55
  69. package/src/normalize-fields.js +17 -0
  70. package/src/pagination.js +2 -2
  71. package/src/search-widget.js +63 -21
  72. package/src/stories/fixtures.js +87 -2
  73. package/src/stories/index.story.js +5 -74
  74. package/src/style.scss +53 -14
  75. package/src/test/filter-and-sort-data-view.js +276 -0
  76. package/src/utils.js +38 -56
  77. package/src/view-grid.js +36 -11
  78. package/src/view-list.js +159 -69
  79. package/src/view-table.js +71 -9
package/src/view-list.js CHANGED
@@ -6,17 +6,135 @@ import classNames from 'classnames';
6
6
  /**
7
7
  * WordPress dependencies
8
8
  */
9
- import { useAsyncList } from '@wordpress/compose';
9
+ import { useAsyncList, useInstanceId } from '@wordpress/compose';
10
10
  import {
11
11
  __experimentalHStack as HStack,
12
12
  __experimentalVStack as VStack,
13
+ privateApis as componentsPrivateApis,
13
14
  Button,
14
15
  Spinner,
16
+ VisuallyHidden,
15
17
  } from '@wordpress/components';
16
- import { ENTER, SPACE } from '@wordpress/keycodes';
18
+ import { useCallback, useEffect, useRef } from '@wordpress/element';
17
19
  import { info } from '@wordpress/icons';
18
20
  import { __ } from '@wordpress/i18n';
19
21
 
22
+ /**
23
+ * Internal dependencies
24
+ */
25
+ import { unlock } from './lock-unlock';
26
+
27
+ const {
28
+ useCompositeStoreV2: useCompositeStore,
29
+ CompositeV2: Composite,
30
+ CompositeItemV2: CompositeItem,
31
+ CompositeRowV2: CompositeRow,
32
+ } = unlock( componentsPrivateApis );
33
+
34
+ function ListItem( {
35
+ id,
36
+ item,
37
+ isSelected,
38
+ onSelect,
39
+ onDetailsChange,
40
+ mediaField,
41
+ primaryField,
42
+ visibleFields,
43
+ } ) {
44
+ const itemRef = useRef( null );
45
+ const labelId = `${ id }-label`;
46
+ const descriptionId = `${ id }-description`;
47
+
48
+ useEffect( () => {
49
+ if ( isSelected ) {
50
+ itemRef.current?.scrollIntoView( {
51
+ behavior: 'auto',
52
+ block: 'nearest',
53
+ inline: 'nearest',
54
+ } );
55
+ }
56
+ }, [ isSelected ] );
57
+
58
+ return (
59
+ <CompositeRow
60
+ ref={ itemRef }
61
+ render={ <li /> }
62
+ role="row"
63
+ className={ classNames( {
64
+ 'is-selected': isSelected,
65
+ } ) }
66
+ >
67
+ <HStack className="dataviews-view-list__item-wrapper">
68
+ <div role="gridcell">
69
+ <CompositeItem
70
+ render={ <div /> }
71
+ role="button"
72
+ id={ id }
73
+ aria-pressed={ isSelected }
74
+ aria-labelledby={ labelId }
75
+ aria-describedby={ descriptionId }
76
+ className="dataviews-view-list__item"
77
+ onClick={ () => onSelect( item ) }
78
+ >
79
+ <HStack
80
+ spacing={ 3 }
81
+ justify="start"
82
+ alignment="flex-start"
83
+ >
84
+ <div className="dataviews-view-list__media-wrapper">
85
+ { mediaField?.render( { item } ) || (
86
+ <div className="dataviews-view-list__media-placeholder"></div>
87
+ ) }
88
+ </div>
89
+ <VStack spacing={ 1 }>
90
+ <span
91
+ className="dataviews-view-list__primary-field"
92
+ id={ labelId }
93
+ >
94
+ { primaryField?.render( { item } ) }
95
+ </span>
96
+ <div
97
+ className="dataviews-view-list__fields"
98
+ id={ descriptionId }
99
+ >
100
+ { visibleFields.map( ( field ) => (
101
+ <div
102
+ key={ field.id }
103
+ className="dataviews-view-list__field"
104
+ >
105
+ <VisuallyHidden
106
+ as="span"
107
+ className="dataviews-view-list__field-label"
108
+ >
109
+ { field.header }
110
+ </VisuallyHidden>
111
+ <span className="dataviews-view-list__field-value">
112
+ { field.render( { item } ) }
113
+ </span>
114
+ </div>
115
+ ) ) }
116
+ </div>
117
+ </VStack>
118
+ </HStack>
119
+ </CompositeItem>
120
+ </div>
121
+ { onDetailsChange && (
122
+ <div role="gridcell">
123
+ <CompositeItem
124
+ render={ <Button /> }
125
+ className="dataviews-view-list__details-button"
126
+ onClick={ () => onDetailsChange( [ item ] ) }
127
+ icon={ info }
128
+ label={ __( 'View details' ) }
129
+ size="compact"
130
+ />
131
+ </div>
132
+ ) }
133
+ </HStack>
134
+ </CompositeRow>
135
+ );
136
+ }
137
+
20
138
  export default function ViewList( {
21
139
  view,
22
140
  fields,
@@ -27,9 +145,15 @@ export default function ViewList( {
27
145
  onDetailsChange,
28
146
  selection,
29
147
  deferredRendering,
148
+ id: preferredId,
30
149
  } ) {
150
+ const baseId = useInstanceId( ViewList, 'view-list', preferredId );
31
151
  const shownData = useAsyncList( data, { step: 3 } );
32
152
  const usedData = deferredRendering ? shownData : data;
153
+ const selectedItem = usedData?.findLast( ( item ) =>
154
+ selection.includes( item.id )
155
+ );
156
+
33
157
  const mediaField = fields.find(
34
158
  ( field ) => field.id === view.layout.mediaField
35
159
  );
@@ -44,12 +168,19 @@ export default function ViewList( {
44
168
  )
45
169
  );
46
170
 
47
- const onEnter = ( item ) => ( event ) => {
48
- const { keyCode } = event;
49
- if ( [ ENTER, SPACE ].includes( keyCode ) ) {
50
- onSelectionChange( [ item ] );
51
- }
52
- };
171
+ const onSelect = useCallback(
172
+ ( item ) => onSelectionChange( [ item ] ),
173
+ [ onSelectionChange ]
174
+ );
175
+
176
+ const getItemDomId = useCallback(
177
+ ( item ) => ( item ? `${ baseId }-${ getItemId( item ) }` : undefined ),
178
+ [ baseId, getItemId ]
179
+ );
180
+
181
+ const store = useCompositeStore( {
182
+ defaultActiveId: getItemDomId( selectedItem ),
183
+ } );
53
184
 
54
185
  const hasData = usedData?.length;
55
186
  if ( ! hasData ) {
@@ -68,70 +199,29 @@ export default function ViewList( {
68
199
  }
69
200
 
70
201
  return (
71
- <ul className="dataviews-view-list">
202
+ <Composite
203
+ id={ baseId }
204
+ render={ <ul /> }
205
+ className="dataviews-view-list"
206
+ role="grid"
207
+ store={ store }
208
+ >
72
209
  { usedData.map( ( item ) => {
210
+ const id = getItemDomId( item );
73
211
  return (
74
- <li
75
- key={ getItemId( item ) }
76
- className={ classNames( {
77
- 'is-selected': selection.includes( item.id ),
78
- } ) }
79
- >
80
- <HStack className="dataviews-view-list__item-wrapper">
81
- <div
82
- role="button"
83
- tabIndex={ 0 }
84
- aria-pressed={ selection.includes( item.id ) }
85
- onKeyDown={ onEnter( item ) }
86
- className="dataviews-view-list__item"
87
- onClick={ () => onSelectionChange( [ item ] ) }
88
- >
89
- <HStack
90
- spacing={ 3 }
91
- justify="start"
92
- alignment="flex-start"
93
- >
94
- <div className="dataviews-view-list__media-wrapper">
95
- { mediaField?.render( { item } ) || (
96
- <div className="dataviews-view-list__media-placeholder"></div>
97
- ) }
98
- </div>
99
- <VStack spacing={ 1 }>
100
- <span className="dataviews-view-list__primary-field">
101
- { primaryField?.render( { item } ) }
102
- </span>
103
- <div className="dataviews-view-list__fields">
104
- { visibleFields.map( ( field ) => {
105
- return (
106
- <span
107
- key={ field.id }
108
- className="dataviews-view-list__field"
109
- >
110
- { field.render( {
111
- item,
112
- } ) }
113
- </span>
114
- );
115
- } ) }
116
- </div>
117
- </VStack>
118
- </HStack>
119
- </div>
120
- { onDetailsChange && (
121
- <Button
122
- className="dataviews-view-list__details-button"
123
- onClick={ () =>
124
- onDetailsChange( [ item ] )
125
- }
126
- icon={ info }
127
- label={ __( 'View details' ) }
128
- size="compact"
129
- />
130
- ) }
131
- </HStack>
132
- </li>
212
+ <ListItem
213
+ key={ id }
214
+ id={ id }
215
+ item={ item }
216
+ isSelected={ item === selectedItem }
217
+ onSelect={ onSelect }
218
+ onDetailsChange={ onDetailsChange }
219
+ mediaField={ mediaField }
220
+ primaryField={ primaryField }
221
+ visibleFields={ visibleFields }
222
+ />
133
223
  );
134
224
  } ) }
135
- </ul>
225
+ </Composite>
136
226
  );
137
227
  }
package/src/view-table.js CHANGED
@@ -22,9 +22,9 @@ import {
22
22
  useId,
23
23
  useRef,
24
24
  useState,
25
+ useMemo,
25
26
  Children,
26
27
  Fragment,
27
- useMemo,
28
28
  } from '@wordpress/element';
29
29
 
30
30
  /**
@@ -34,7 +34,7 @@ import SingleSelectionCheckbox from './single-selection-checkbox';
34
34
  import { unlock } from './lock-unlock';
35
35
  import ItemActions from './item-actions';
36
36
  import { sanitizeOperators } from './utils';
37
- import { ENUMERATION_TYPE, SORTING_DIRECTIONS } from './constants';
37
+ import { SORTING_DIRECTIONS } from './constants';
38
38
  import {
39
39
  useSomeItemHasAPossibleBulkAction,
40
40
  useHasAPossibleBulkAction,
@@ -49,7 +49,7 @@ const {
49
49
  DropdownMenuSeparatorV2: DropdownMenuSeparator,
50
50
  } = unlock( componentsPrivateApis );
51
51
 
52
- function WithSeparators( { children } ) {
52
+ function WithDropDownMenuSeparators( { children } ) {
53
53
  return Children.toArray( children )
54
54
  .filter( Boolean )
55
55
  .map( ( child, i ) => (
@@ -76,7 +76,7 @@ const HeaderMenu = forwardRef( function HeaderMenu(
76
76
  // 3. If it's not primary. If it is, it should be already visible.
77
77
  const canAddFilter =
78
78
  ! view.filters?.some( ( _filter ) => field.id === _filter.field ) &&
79
- field.type === ENUMERATION_TYPE &&
79
+ !! field.elements?.length &&
80
80
  !! operators.length &&
81
81
  ! field.filterBy?.isPrimary;
82
82
  if ( ! isSortable && ! isHidable && ! canAddFilter ) {
@@ -102,7 +102,7 @@ const HeaderMenu = forwardRef( function HeaderMenu(
102
102
  }
103
103
  style={ { minWidth: '240px' } }
104
104
  >
105
- <WithSeparators>
105
+ <WithDropDownMenuSeparators>
106
106
  { isSortable && (
107
107
  <DropdownMenuGroup>
108
108
  { Object.entries( SORTING_DIRECTIONS ).map(
@@ -187,7 +187,7 @@ const HeaderMenu = forwardRef( function HeaderMenu(
187
187
  </DropdownMenuItemLabel>
188
188
  </DropdownMenuItem>
189
189
  ) }
190
- </WithSeparators>
190
+ </WithDropDownMenuSeparators>
191
191
  </DropdownMenu>
192
192
  );
193
193
  } );
@@ -237,12 +237,63 @@ function TableRow( {
237
237
  data,
238
238
  } ) {
239
239
  const hasPossibleBulkAction = useHasAPossibleBulkAction( actions, item );
240
+ const isSelected = selection.includes( id );
241
+
242
+ const [ isHovered, setIsHovered ] = useState( false );
243
+
244
+ const handleMouseEnter = () => {
245
+ setIsHovered( true );
246
+ };
247
+
248
+ const handleMouseLeave = () => {
249
+ setIsHovered( false );
250
+ };
251
+
252
+ // Will be set to true if `onTouchStart` fires. This happens before
253
+ // `onClick` and can be used to exclude touchscreen devices from certain
254
+ // behaviours.
255
+ const isTouchDevice = useRef( false );
256
+
240
257
  return (
241
258
  <tr
242
259
  className={ classnames( 'dataviews-view-table__row', {
243
- 'is-selected':
244
- hasPossibleBulkAction && selection.includes( id ),
260
+ 'is-selected': hasPossibleBulkAction && isSelected,
261
+ 'is-hovered': isHovered,
262
+ 'has-bulk-actions': hasPossibleBulkAction,
245
263
  } ) }
264
+ onMouseEnter={ handleMouseEnter }
265
+ onMouseLeave={ handleMouseLeave }
266
+ onTouchStart={ () => {
267
+ isTouchDevice.current = true;
268
+ } }
269
+ onClick={ () => {
270
+ if (
271
+ ! isTouchDevice.current &&
272
+ document.getSelection().type !== 'Range'
273
+ ) {
274
+ if ( ! isSelected ) {
275
+ onSelectionChange(
276
+ data.filter( ( _item ) => {
277
+ const itemId = getItemId?.( _item );
278
+ return (
279
+ itemId === id ||
280
+ selection.includes( itemId )
281
+ );
282
+ } )
283
+ );
284
+ } else {
285
+ onSelectionChange(
286
+ data.filter( ( _item ) => {
287
+ const itemId = getItemId?.( _item );
288
+ return (
289
+ itemId !== id &&
290
+ selection.includes( itemId )
291
+ );
292
+ } )
293
+ );
294
+ }
295
+ }
296
+ } }
246
297
  >
247
298
  { hasBulkActions && (
248
299
  <td
@@ -291,9 +342,20 @@ function TableRow( {
291
342
  </td>
292
343
  ) ) }
293
344
  { !! actions?.length && (
294
- <td className="dataviews-view-table__actions-column">
345
+ // Disable reason: we are not making the element interactive,
346
+ // but preventing any click events from bubbling up to the
347
+ // table row. This allows us to add a click handler to the row
348
+ // itself (to toggle row selection) without erroneously
349
+ // intercepting click events from ItemActions.
350
+
351
+ /* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
352
+ <td
353
+ className="dataviews-view-table__actions-column"
354
+ onClick={ ( e ) => e.stopPropagation() }
355
+ >
295
356
  <ItemActions item={ item } actions={ actions } />
296
357
  </td>
358
+ /* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
297
359
  ) }
298
360
  </tr>
299
361
  );