@wordpress/dataviews 0.8.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 (59) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -5
  3. package/build/constants.js +1 -4
  4. package/build/constants.js.map +1 -1
  5. package/build/dataviews.js +2 -12
  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/filters.js +11 -17
  10. package/build/filters.js.map +1 -1
  11. package/build/index.js +3 -9
  12. package/build/index.js.map +1 -1
  13. package/build/normalize-fields.js +25 -0
  14. package/build/normalize-fields.js.map +1 -0
  15. package/build/utils.js +1 -65
  16. package/build/utils.js.map +1 -1
  17. package/build/view-grid.js +21 -11
  18. package/build/view-grid.js.map +1 -1
  19. package/build/view-list.js +122 -58
  20. package/build/view-list.js.map +1 -1
  21. package/build/view-table.js +27 -13
  22. package/build/view-table.js.map +1 -1
  23. package/build-module/constants.js +0 -3
  24. package/build-module/constants.js.map +1 -1
  25. package/build-module/dataviews.js +2 -12
  26. package/build-module/dataviews.js.map +1 -1
  27. package/build-module/filter-and-sort-data-view.js +139 -0
  28. package/build-module/filter-and-sort-data-view.js.map +1 -0
  29. package/build-module/filters.js +12 -18
  30. package/build-module/filters.js.map +1 -1
  31. package/build-module/index.js +1 -1
  32. package/build-module/index.js.map +1 -1
  33. package/build-module/normalize-fields.js +19 -0
  34. package/build-module/normalize-fields.js.map +1 -0
  35. package/build-module/utils.js +0 -63
  36. package/build-module/utils.js.map +1 -1
  37. package/build-module/view-grid.js +22 -12
  38. package/build-module/view-grid.js.map +1 -1
  39. package/build-module/view-list.js +124 -60
  40. package/build-module/view-list.js.map +1 -1
  41. package/build-module/view-table.js +28 -14
  42. package/build-module/view-table.js.map +1 -1
  43. package/build-style/style-rtl.css +32 -8
  44. package/build-style/style.css +32 -8
  45. package/package.json +11 -11
  46. package/src/constants.js +0 -3
  47. package/src/dataviews.js +2 -12
  48. package/src/filter-and-sort-data-view.js +154 -0
  49. package/src/filters.js +20 -32
  50. package/src/index.js +1 -1
  51. package/src/normalize-fields.js +17 -0
  52. package/src/stories/fixtures.js +75 -1
  53. package/src/stories/index.story.js +5 -113
  54. package/src/style.scss +44 -11
  55. package/src/test/filter-and-sort-data-view.js +276 -0
  56. package/src/utils.js +0 -52
  57. package/src/view-grid.js +32 -10
  58. package/src/view-list.js +159 -69
  59. package/src/view-table.js +29 -13
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
@@ -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,
@@ -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 ) {
@@ -237,7 +237,6 @@ function TableRow( {
237
237
  data,
238
238
  } ) {
239
239
  const hasPossibleBulkAction = useHasAPossibleBulkAction( actions, item );
240
-
241
240
  const isSelected = selection.includes( id );
242
241
 
243
242
  const [ isHovered, setIsHovered ] = useState( false );
@@ -250,22 +249,28 @@ function TableRow( {
250
249
  setIsHovered( false );
251
250
  };
252
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
+
253
257
  return (
254
258
  <tr
255
259
  className={ classnames( 'dataviews-view-table__row', {
256
- 'is-selected':
257
- hasPossibleBulkAction && selection.includes( id ),
260
+ 'is-selected': hasPossibleBulkAction && isSelected,
258
261
  'is-hovered': isHovered,
262
+ 'has-bulk-actions': hasPossibleBulkAction,
259
263
  } ) }
260
264
  onMouseEnter={ handleMouseEnter }
261
265
  onMouseLeave={ handleMouseLeave }
262
- onClickCapture={ ( event ) => {
263
- if ( event.ctrlKey || event.metaKey ) {
264
- event.stopPropagation();
265
- event.preventDefault();
266
- if ( ! hasPossibleBulkAction ) {
267
- return;
268
- }
266
+ onTouchStart={ () => {
267
+ isTouchDevice.current = true;
268
+ } }
269
+ onClick={ () => {
270
+ if (
271
+ ! isTouchDevice.current &&
272
+ document.getSelection().type !== 'Range'
273
+ ) {
269
274
  if ( ! isSelected ) {
270
275
  onSelectionChange(
271
276
  data.filter( ( _item ) => {
@@ -337,9 +342,20 @@ function TableRow( {
337
342
  </td>
338
343
  ) ) }
339
344
  { !! actions?.length && (
340
- <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
+ >
341
356
  <ItemActions item={ item } actions={ actions } />
342
357
  </td>
358
+ /* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
343
359
  ) }
344
360
  </tr>
345
361
  );