@wordpress/dataviews 10.3.1-next.2f1c7c01b.0 → 10.4.1-next.dc3f6d3c1.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 (149) hide show
  1. package/CHANGELOG.md +23 -5
  2. package/README.md +413 -148
  3. package/build/components/dataviews-filters/filter.js +11 -1
  4. package/build/components/dataviews-filters/filter.js.map +2 -2
  5. package/build/components/dataviews-view-config/index.js +11 -396
  6. package/build/components/dataviews-view-config/index.js.map +3 -3
  7. package/build/components/dataviews-view-config/properties-section.js +177 -0
  8. package/build/components/dataviews-view-config/properties-section.js.map +7 -0
  9. package/build/constants.js +3 -0
  10. package/build/constants.js.map +2 -2
  11. package/build/dataform-controls/date.js +23 -7
  12. package/build/dataform-controls/date.js.map +2 -2
  13. package/build/dataform-controls/email.js +1 -1
  14. package/build/dataform-controls/email.js.map +1 -1
  15. package/build/dataform-layouts/details/index.js +78 -0
  16. package/build/dataform-layouts/details/index.js.map +7 -0
  17. package/build/dataform-layouts/index.js +5 -0
  18. package/build/dataform-layouts/index.js.map +3 -3
  19. package/build/dataform-layouts/normalize-form.js +5 -0
  20. package/build/dataform-layouts/normalize-form.js.map +2 -2
  21. package/build/dataviews-layouts/index.js +9 -0
  22. package/build/dataviews-layouts/index.js.map +3 -3
  23. package/build/dataviews-layouts/picker-table/index.js +422 -0
  24. package/build/dataviews-layouts/picker-table/index.js.map +7 -0
  25. package/build/dataviews-layouts/table/column-header-menu.js.map +2 -2
  26. package/build/dataviews-layouts/table/column-primary.js +1 -6
  27. package/build/dataviews-layouts/table/column-primary.js.map +2 -2
  28. package/build/dataviews-layouts/table/index.js +47 -2
  29. package/build/dataviews-layouts/table/index.js.map +2 -2
  30. package/build/field-types/date.js +4 -2
  31. package/build/field-types/date.js.map +2 -2
  32. package/build/types/dataform.js.map +1 -1
  33. package/build/types/dataviews.js.map +1 -1
  34. package/build/types/field-api.js.map +1 -1
  35. package/build/utils/normalize-fields.js +23 -3
  36. package/build/utils/normalize-fields.js.map +2 -2
  37. package/build/utils/week-starts-on.js +59 -0
  38. package/build/utils/week-starts-on.js.map +7 -0
  39. package/build-module/components/dataviews-filters/filter.js +11 -1
  40. package/build-module/components/dataviews-filters/filter.js.map +2 -2
  41. package/build-module/components/dataviews-view-config/index.js +15 -412
  42. package/build-module/components/dataviews-view-config/index.js.map +2 -2
  43. package/build-module/components/dataviews-view-config/properties-section.js +149 -0
  44. package/build-module/components/dataviews-view-config/properties-section.js.map +7 -0
  45. package/build-module/constants.js +2 -0
  46. package/build-module/constants.js.map +2 -2
  47. package/build-module/dataform-controls/date.js +23 -7
  48. package/build-module/dataform-controls/date.js.map +2 -2
  49. package/build-module/dataform-controls/email.js +2 -2
  50. package/build-module/dataform-controls/email.js.map +1 -1
  51. package/build-module/dataform-layouts/details/index.js +47 -0
  52. package/build-module/dataform-layouts/details/index.js.map +7 -0
  53. package/build-module/dataform-layouts/index.js +5 -0
  54. package/build-module/dataform-layouts/index.js.map +2 -2
  55. package/build-module/dataform-layouts/normalize-form.js +5 -0
  56. package/build-module/dataform-layouts/normalize-form.js.map +2 -2
  57. package/build-module/dataviews-layouts/index.js +11 -1
  58. package/build-module/dataviews-layouts/index.js.map +2 -2
  59. package/build-module/dataviews-layouts/picker-table/index.js +397 -0
  60. package/build-module/dataviews-layouts/picker-table/index.js.map +7 -0
  61. package/build-module/dataviews-layouts/table/column-header-menu.js.map +2 -2
  62. package/build-module/dataviews-layouts/table/column-primary.js +1 -6
  63. package/build-module/dataviews-layouts/table/column-primary.js.map +2 -2
  64. package/build-module/dataviews-layouts/table/index.js +48 -3
  65. package/build-module/dataviews-layouts/table/index.js.map +2 -2
  66. package/build-module/field-types/date.js +5 -3
  67. package/build-module/field-types/date.js.map +2 -2
  68. package/build-module/utils/normalize-fields.js +23 -3
  69. package/build-module/utils/normalize-fields.js.map +2 -2
  70. package/build-module/utils/week-starts-on.js +32 -0
  71. package/build-module/utils/week-starts-on.js.map +7 -0
  72. package/build-style/style-rtl.css +58 -54
  73. package/build-style/style.css +58 -54
  74. package/build-types/components/dataviews-filters/filter.d.ts.map +1 -1
  75. package/build-types/components/dataviews-filters/utils.d.ts.map +1 -1
  76. package/build-types/components/dataviews-view-config/index.d.ts.map +1 -1
  77. package/build-types/components/dataviews-view-config/properties-section.d.ts +4 -0
  78. package/build-types/components/dataviews-view-config/properties-section.d.ts.map +1 -0
  79. package/build-types/constants.d.ts +1 -0
  80. package/build-types/constants.d.ts.map +1 -1
  81. package/build-types/dataform-controls/date.d.ts.map +1 -1
  82. package/build-types/dataform-layouts/details/index.d.ts +6 -0
  83. package/build-types/dataform-layouts/details/index.d.ts.map +1 -0
  84. package/build-types/dataform-layouts/get-summary-fields.d.ts.map +1 -1
  85. package/build-types/dataform-layouts/index.d.ts +5 -0
  86. package/build-types/dataform-layouts/index.d.ts.map +1 -1
  87. package/build-types/dataform-layouts/normalize-form.d.ts.map +1 -1
  88. package/build-types/dataviews-layouts/index.d.ts +8 -0
  89. package/build-types/dataviews-layouts/index.d.ts.map +1 -1
  90. package/build-types/dataviews-layouts/picker-table/index.d.ts +4 -0
  91. package/build-types/dataviews-layouts/picker-table/index.d.ts.map +1 -0
  92. package/build-types/dataviews-layouts/table/column-header-menu.d.ts +3 -3
  93. package/build-types/dataviews-layouts/table/column-header-menu.d.ts.map +1 -1
  94. package/build-types/dataviews-layouts/table/column-primary.d.ts.map +1 -1
  95. package/build-types/dataviews-layouts/table/index.d.ts.map +1 -1
  96. package/build-types/field-types/date.d.ts.map +1 -1
  97. package/build-types/stories/dataform.story.d.ts +3 -0
  98. package/build-types/stories/dataform.story.d.ts.map +1 -1
  99. package/build-types/stories/dataviews-picker.story.d.ts +2 -0
  100. package/build-types/stories/dataviews-picker.story.d.ts.map +1 -1
  101. package/build-types/stories/dataviews.story.d.ts +7 -1
  102. package/build-types/stories/dataviews.story.d.ts.map +1 -1
  103. package/build-types/stories/field-types.story.d.ts +27 -1
  104. package/build-types/stories/field-types.story.d.ts.map +1 -1
  105. package/build-types/types/dataform.d.ts +11 -3
  106. package/build-types/types/dataform.d.ts.map +1 -1
  107. package/build-types/types/dataviews.d.ts +23 -2
  108. package/build-types/types/dataviews.d.ts.map +1 -1
  109. package/build-types/types/field-api.d.ts +28 -1
  110. package/build-types/types/field-api.d.ts.map +1 -1
  111. package/build-types/utils/normalize-fields.d.ts.map +1 -1
  112. package/build-types/utils/week-starts-on.d.ts +20 -0
  113. package/build-types/utils/week-starts-on.d.ts.map +1 -0
  114. package/build-wp/index.js +1497 -1188
  115. package/package.json +15 -15
  116. package/src/components/dataviews/style.scss +2 -0
  117. package/src/components/dataviews-filters/filter.tsx +11 -1
  118. package/src/components/dataviews-footer/style.scss +1 -1
  119. package/src/components/dataviews-view-config/index.tsx +8 -504
  120. package/src/components/dataviews-view-config/properties-section.tsx +201 -0
  121. package/src/components/dataviews-view-config/style.scss +2 -39
  122. package/src/constants.ts +1 -0
  123. package/src/dataform-controls/date.tsx +24 -6
  124. package/src/dataform-controls/email.tsx +2 -2
  125. package/src/dataform-layouts/details/index.tsx +71 -0
  126. package/src/dataform-layouts/details/style.scss +5 -0
  127. package/src/dataform-layouts/index.tsx +5 -0
  128. package/src/dataform-layouts/normalize-form.ts +6 -0
  129. package/src/dataviews-layouts/index.ts +10 -0
  130. package/src/dataviews-layouts/list/style.scss +1 -0
  131. package/src/dataviews-layouts/picker-table/index.tsx +487 -0
  132. package/src/dataviews-layouts/picker-table/style.scss +47 -0
  133. package/src/dataviews-layouts/table/column-header-menu.tsx +3 -2
  134. package/src/dataviews-layouts/table/column-primary.tsx +4 -7
  135. package/src/dataviews-layouts/table/index.tsx +55 -2
  136. package/src/dataviews-layouts/table/style.scss +36 -19
  137. package/src/field-types/date.tsx +11 -5
  138. package/src/stories/dataform.story.tsx +84 -0
  139. package/src/stories/dataviews-picker.story.tsx +11 -6
  140. package/src/stories/dataviews.story.tsx +10 -2
  141. package/src/stories/field-types.story.tsx +67 -2
  142. package/src/style.scss +2 -0
  143. package/src/test/normalize-fields.ts +53 -0
  144. package/src/types/dataform.ts +18 -3
  145. package/src/types/dataviews.ts +36 -2
  146. package/src/types/field-api.ts +42 -1
  147. package/src/utils/normalize-fields.ts +51 -2
  148. package/src/utils/week-starts-on.ts +46 -0
  149. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,487 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import clsx from 'clsx';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { __, sprintf } from '@wordpress/i18n';
10
+ import { Spinner, Composite } from '@wordpress/components';
11
+ import {
12
+ useContext,
13
+ useEffect,
14
+ useId,
15
+ useRef,
16
+ useState,
17
+ } from '@wordpress/element';
18
+
19
+ /**
20
+ * Internal dependencies
21
+ */
22
+ import DataViewsContext from '../../components/dataviews-context';
23
+ import DataViewsSelectionCheckbox from '../../components/dataviews-selection-checkbox';
24
+ import { useIsMultiselectPicker } from '../../components/dataviews-picker/footer';
25
+ import { BulkSelectionCheckbox } from '../../components/dataviews-bulk-actions';
26
+ import { sortValues } from '../../constants';
27
+ import type {
28
+ NormalizedField,
29
+ ViewPickerTable as ViewPickerTableType,
30
+ ViewPickerTableProps,
31
+ } from '../../types';
32
+ import type { SetSelection } from '../../types/private';
33
+ import ColumnHeaderMenu from '../table/column-header-menu';
34
+ import ColumnPrimary from '../table/column-primary';
35
+ import getDataByGroup from '../utils/get-data-by-group';
36
+
37
+ interface TableColumnFieldProps< Item > {
38
+ fields: NormalizedField< Item >[];
39
+ column: string;
40
+ item: Item;
41
+ align?: 'start' | 'center' | 'end';
42
+ }
43
+
44
+ interface TableRowProps< Item > {
45
+ item: Item;
46
+ fields: NormalizedField< Item >[];
47
+ id: string;
48
+ view: ViewPickerTableType;
49
+ titleField?: NormalizedField< Item >;
50
+ mediaField?: NormalizedField< Item >;
51
+ descriptionField?: NormalizedField< Item >;
52
+ selection: string[];
53
+ getItemId: ( item: Item ) => string;
54
+ onChangeSelection: SetSelection;
55
+ multiselect: boolean;
56
+ posinset?: number;
57
+ }
58
+
59
+ function TableColumnField< Item >( {
60
+ item,
61
+ fields,
62
+ column,
63
+ align,
64
+ }: TableColumnFieldProps< Item > ) {
65
+ const field = fields.find( ( f ) => f.id === column );
66
+
67
+ if ( ! field ) {
68
+ return null;
69
+ }
70
+
71
+ const className = clsx( 'dataviews-view-table__cell-content-wrapper', {
72
+ 'dataviews-view-table__cell-align-end': align === 'end',
73
+ 'dataviews-view-table__cell-align-center': align === 'center',
74
+ } );
75
+
76
+ return (
77
+ <div className={ className }>
78
+ <field.render item={ item } field={ field } />
79
+ </div>
80
+ );
81
+ }
82
+
83
+ function TableRow< Item >( {
84
+ item,
85
+ fields,
86
+ id,
87
+ view,
88
+ titleField,
89
+ mediaField,
90
+ descriptionField,
91
+ selection,
92
+ getItemId,
93
+ onChangeSelection,
94
+ multiselect,
95
+ posinset,
96
+ }: TableRowProps< Item > ) {
97
+ const { paginationInfo } = useContext( DataViewsContext );
98
+ const isSelected = selection.includes( id );
99
+ const [ isHovered, setIsHovered ] = useState( false );
100
+ const {
101
+ showTitle = true,
102
+ showMedia = true,
103
+ showDescription = true,
104
+ infiniteScrollEnabled,
105
+ } = view;
106
+ const handleMouseEnter = () => {
107
+ setIsHovered( true );
108
+ };
109
+ const handleMouseLeave = () => {
110
+ setIsHovered( false );
111
+ };
112
+
113
+ const columns = view.fields ?? [];
114
+ const hasPrimaryColumn =
115
+ ( titleField && showTitle ) ||
116
+ ( mediaField && showMedia ) ||
117
+ ( descriptionField && showDescription );
118
+
119
+ return (
120
+ <Composite.Item
121
+ key={ id }
122
+ render={ ( { children, ...props } ) => (
123
+ <tr
124
+ className={ clsx( 'dataviews-view-table__row', {
125
+ 'is-selected': isSelected,
126
+ 'is-hovered': isHovered,
127
+ } ) }
128
+ onMouseEnter={ handleMouseEnter }
129
+ onMouseLeave={ handleMouseLeave }
130
+ children={ children }
131
+ { ...props }
132
+ />
133
+ ) }
134
+ aria-selected={ isSelected }
135
+ aria-setsize={ paginationInfo.totalItems || undefined }
136
+ aria-posinset={ posinset }
137
+ role={ infiniteScrollEnabled ? 'article' : 'option' }
138
+ onClick={ () => {
139
+ if ( isSelected ) {
140
+ onChangeSelection(
141
+ selection.filter( ( itemId ) => id !== itemId )
142
+ );
143
+ } else {
144
+ const newSelection = multiselect
145
+ ? [ ...selection, id ]
146
+ : [ id ];
147
+ onChangeSelection( newSelection );
148
+ }
149
+ } }
150
+ >
151
+ <td
152
+ className="dataviews-view-table__checkbox-column"
153
+ role="presentation"
154
+ >
155
+ <div className="dataviews-view-table__cell-content-wrapper">
156
+ <DataViewsSelectionCheckbox
157
+ item={ item }
158
+ selection={ selection }
159
+ onChangeSelection={ onChangeSelection }
160
+ getItemId={ getItemId }
161
+ titleField={ titleField }
162
+ disabled={ false }
163
+ aria-hidden
164
+ tabIndex={ -1 }
165
+ />
166
+ </div>
167
+ </td>
168
+
169
+ { hasPrimaryColumn && (
170
+ <td role="presentation">
171
+ <ColumnPrimary
172
+ item={ item }
173
+ titleField={ showTitle ? titleField : undefined }
174
+ mediaField={ showMedia ? mediaField : undefined }
175
+ descriptionField={
176
+ showDescription ? descriptionField : undefined
177
+ }
178
+ isItemClickable={ () => false }
179
+ />
180
+ </td>
181
+ ) }
182
+ { columns.map( ( column: string ) => {
183
+ // Explicit picks the supported styles.
184
+ const { width, maxWidth, minWidth, align } =
185
+ view.layout?.styles?.[ column ] ?? {};
186
+
187
+ return (
188
+ <td
189
+ key={ column }
190
+ style={ {
191
+ width,
192
+ maxWidth,
193
+ minWidth,
194
+ } }
195
+ role="presentation"
196
+ >
197
+ <TableColumnField
198
+ fields={ fields }
199
+ item={ item }
200
+ column={ column }
201
+ align={ align }
202
+ />
203
+ </td>
204
+ );
205
+ } ) }
206
+ </Composite.Item>
207
+ );
208
+ }
209
+
210
+ function ViewPickerTable< Item >( {
211
+ actions,
212
+ data,
213
+ fields,
214
+ getItemId,
215
+ isLoading = false,
216
+ onChangeView,
217
+ onChangeSelection,
218
+ selection,
219
+ setOpenedFilter,
220
+ view,
221
+ className,
222
+ empty,
223
+ }: ViewPickerTableProps< Item > ) {
224
+ const headerMenuRefs = useRef<
225
+ Map< string, { node: HTMLButtonElement; fallback: string } >
226
+ >( new Map() );
227
+ const headerMenuToFocusRef = useRef< HTMLButtonElement >();
228
+ const [ nextHeaderMenuToFocus, setNextHeaderMenuToFocus ] =
229
+ useState< HTMLButtonElement >();
230
+ const isMultiselect = useIsMultiselectPicker( actions ) ?? false;
231
+
232
+ useEffect( () => {
233
+ if ( headerMenuToFocusRef.current ) {
234
+ headerMenuToFocusRef.current.focus();
235
+ headerMenuToFocusRef.current = undefined;
236
+ }
237
+ } );
238
+
239
+ const tableNoticeId = useId();
240
+
241
+ if ( nextHeaderMenuToFocus ) {
242
+ // If we need to force focus, we short-circuit rendering here
243
+ // to prevent any additional work while we handle that.
244
+ // Clearing out the focus directive is necessary to make sure
245
+ // future renders don't cause unexpected focus jumps.
246
+ headerMenuToFocusRef.current = nextHeaderMenuToFocus;
247
+ setNextHeaderMenuToFocus( undefined );
248
+ return;
249
+ }
250
+
251
+ const onHide = ( field: NormalizedField< Item > ) => {
252
+ const hidden = headerMenuRefs.current.get( field.id );
253
+ const fallback = hidden
254
+ ? headerMenuRefs.current.get( hidden.fallback )
255
+ : undefined;
256
+ setNextHeaderMenuToFocus( fallback?.node );
257
+ };
258
+
259
+ const hasData = !! data?.length;
260
+
261
+ const titleField = fields.find( ( field ) => field.id === view.titleField );
262
+ const mediaField = fields.find( ( field ) => field.id === view.mediaField );
263
+ const descriptionField = fields.find(
264
+ ( field ) => field.id === view.descriptionField
265
+ );
266
+
267
+ const groupField = view.groupByField
268
+ ? fields.find( ( f ) => f.id === view.groupByField )
269
+ : null;
270
+ const dataByGroup = groupField ? getDataByGroup( data, groupField ) : null;
271
+ const { showTitle = true, showMedia = true, showDescription = true } = view;
272
+ const hasPrimaryColumn =
273
+ ( titleField && showTitle ) ||
274
+ ( mediaField && showMedia ) ||
275
+ ( descriptionField && showDescription );
276
+ const columns = view.fields ?? [];
277
+ const headerMenuRef =
278
+ ( column: string, index: number ) => ( node: HTMLButtonElement ) => {
279
+ if ( node ) {
280
+ headerMenuRefs.current.set( column, {
281
+ node,
282
+ fallback: columns[ index > 0 ? index - 1 : 1 ],
283
+ } );
284
+ } else {
285
+ headerMenuRefs.current.delete( column );
286
+ }
287
+ };
288
+ const isInfiniteScroll = view.infiniteScrollEnabled && ! dataByGroup;
289
+
290
+ return (
291
+ <>
292
+ <table
293
+ className={ clsx(
294
+ 'dataviews-view-table',
295
+ 'dataviews-view-picker-table',
296
+ className,
297
+ {
298
+ [ `has-${ view.layout?.density }-density` ]:
299
+ view.layout?.density &&
300
+ [ 'compact', 'comfortable' ].includes(
301
+ view.layout.density
302
+ ),
303
+ }
304
+ ) }
305
+ aria-busy={ isLoading }
306
+ aria-describedby={ tableNoticeId }
307
+ role={ isInfiniteScroll ? 'feed' : 'listbox' }
308
+ >
309
+ <thead role="presentation">
310
+ <tr
311
+ className="dataviews-view-table__row"
312
+ role="presentation"
313
+ >
314
+ <th className="dataviews-view-table__checkbox-column">
315
+ { isMultiselect && (
316
+ <BulkSelectionCheckbox
317
+ selection={ selection }
318
+ onChangeSelection={ onChangeSelection }
319
+ data={ data }
320
+ actions={ actions }
321
+ getItemId={ getItemId }
322
+ />
323
+ ) }
324
+ </th>
325
+ { hasPrimaryColumn && (
326
+ <th>
327
+ { titleField && (
328
+ <ColumnHeaderMenu
329
+ ref={ headerMenuRef(
330
+ titleField.id,
331
+ 0
332
+ ) }
333
+ fieldId={ titleField.id }
334
+ view={ view }
335
+ fields={ fields }
336
+ onChangeView={ onChangeView }
337
+ onHide={ onHide }
338
+ setOpenedFilter={ setOpenedFilter }
339
+ canMove={ false }
340
+ />
341
+ ) }
342
+ </th>
343
+ ) }
344
+ { columns.map( ( column, index ) => {
345
+ // Explicit picks the supported styles.
346
+ const { width, maxWidth, minWidth, align } =
347
+ view.layout?.styles?.[ column ] ?? {};
348
+ return (
349
+ <th
350
+ key={ column }
351
+ style={ {
352
+ width,
353
+ maxWidth,
354
+ minWidth,
355
+ textAlign: align,
356
+ } }
357
+ aria-sort={
358
+ view.sort?.direction &&
359
+ view.sort?.field === column
360
+ ? sortValues[ view.sort.direction ]
361
+ : undefined
362
+ }
363
+ scope="col"
364
+ >
365
+ <ColumnHeaderMenu
366
+ ref={ headerMenuRef( column, index ) }
367
+ fieldId={ column }
368
+ view={ view }
369
+ fields={ fields }
370
+ onChangeView={ onChangeView }
371
+ onHide={ onHide }
372
+ setOpenedFilter={ setOpenedFilter }
373
+ canMove={
374
+ view.layout?.enableMoving ?? true
375
+ }
376
+ />
377
+ </th>
378
+ );
379
+ } ) }
380
+ </tr>
381
+ </thead>
382
+ { /* Render grouped data if groupByField is specified */ }
383
+ { hasData && groupField && dataByGroup ? (
384
+ Array.from( dataByGroup.entries() ).map(
385
+ ( [ groupName, groupItems ] ) => (
386
+ <Composite
387
+ key={ `group-${ groupName }` }
388
+ virtualFocus
389
+ orientation="vertical"
390
+ render={ <tbody role="group" /> }
391
+ >
392
+ <tr
393
+ className="dataviews-view-table__group-header-row"
394
+ role="presentation"
395
+ >
396
+ <td
397
+ colSpan={
398
+ columns.length +
399
+ ( hasPrimaryColumn ? 1 : 0 ) +
400
+ 1
401
+ }
402
+ className="dataviews-view-table__group-header-cell"
403
+ role="presentation"
404
+ >
405
+ { sprintf(
406
+ // translators: 1: The label of the field e.g. "Date". 2: The value of the field, e.g.: "May 2022".
407
+ __( '%1$s: %2$s' ),
408
+ groupField.label,
409
+ groupName
410
+ ) }
411
+ </td>
412
+ </tr>
413
+ { groupItems.map( ( item, index ) => (
414
+ <TableRow
415
+ key={ getItemId( item ) }
416
+ item={ item }
417
+ fields={ fields }
418
+ id={
419
+ getItemId( item ) ||
420
+ index.toString()
421
+ }
422
+ view={ view }
423
+ titleField={ titleField }
424
+ mediaField={ mediaField }
425
+ descriptionField={ descriptionField }
426
+ selection={ selection }
427
+ getItemId={ getItemId }
428
+ onChangeSelection={ onChangeSelection }
429
+ multiselect={ isMultiselect }
430
+ />
431
+ ) ) }
432
+ </Composite>
433
+ )
434
+ )
435
+ ) : (
436
+ <Composite
437
+ render={ <tbody role="presentation" /> }
438
+ virtualFocus
439
+ orientation="vertical"
440
+ >
441
+ { hasData &&
442
+ data.map( ( item, index ) => (
443
+ <TableRow
444
+ key={ getItemId( item ) }
445
+ item={ item }
446
+ fields={ fields }
447
+ id={ getItemId( item ) || index.toString() }
448
+ view={ view }
449
+ titleField={ titleField }
450
+ mediaField={ mediaField }
451
+ descriptionField={ descriptionField }
452
+ selection={ selection }
453
+ getItemId={ getItemId }
454
+ onChangeSelection={ onChangeSelection }
455
+ multiselect={ isMultiselect }
456
+ posinset={ index + 1 }
457
+ />
458
+ ) ) }
459
+ </Composite>
460
+ ) }
461
+ </table>
462
+ <div
463
+ className={ clsx( {
464
+ 'dataviews-loading': isLoading,
465
+ 'dataviews-no-results': ! hasData && ! isLoading,
466
+ } ) }
467
+ id={ tableNoticeId }
468
+ >
469
+ { ! hasData &&
470
+ ( isLoading ? (
471
+ <p>
472
+ <Spinner />
473
+ </p>
474
+ ) : (
475
+ empty
476
+ ) ) }
477
+ { hasData && isLoading && (
478
+ <p className="dataviews-loading-more">
479
+ <Spinner />
480
+ </p>
481
+ ) }
482
+ </div>
483
+ </>
484
+ );
485
+ }
486
+
487
+ export default ViewPickerTable;
@@ -0,0 +1,47 @@
1
+ // Picker-specific table styles
2
+ // The table picker mainly reuses the regular table styles from ../table/style.scss
3
+ // Add any picker-specific overrides here if needed in the future.
4
+
5
+ .dataviews-view-picker-table {
6
+ background-color: inherit;
7
+
8
+ tbody:focus-visible {
9
+ // Only show one focus outline at a time. When focus is on a child element,
10
+ // hide the table's own focus outline.
11
+ &[aria-activedescendant] {
12
+ outline: none;
13
+ }
14
+
15
+ [data-active-item="true"] {
16
+ outline: 2px solid var(--wp-admin-theme-color);
17
+ }
18
+ }
19
+
20
+ .dataviews-selection-checkbox {
21
+ // Added specificity to override table styles.
22
+ .components-checkbox-control__input.components-checkbox-control__input {
23
+ // When the dataview is used as a picker, ensure the checkbox can't be clicked on.
24
+ // Only the row itself is clickable.
25
+ pointer-events: none;
26
+ opacity: 1;
27
+ }
28
+ }
29
+
30
+ // When used in picker context, ensure row selection works as expected
31
+ .dataviews-view-table__row {
32
+ cursor: pointer;
33
+
34
+ &.is-selected {
35
+ // Ensure selected rows are visually distinct
36
+ background-color: rgba(var(--wp-admin-theme-color--rgb), 0.04);
37
+ }
38
+
39
+ &.is-hovered {
40
+ background-color: rgba(var(--wp-admin-theme-color--rgb), 0.08);
41
+ }
42
+
43
+ &.is-selected.is-hovered {
44
+ background-color: rgba(var(--wp-admin-theme-color--rgb), 0.12);
45
+ }
46
+ }
47
+ }
@@ -24,6 +24,7 @@ import type {
24
24
  NormalizedField,
25
25
  SortDirection,
26
26
  ViewTable as ViewTableType,
27
+ ViewPickerTable as ViewPickerTableType,
27
28
  Operator,
28
29
  } from '../../types';
29
30
 
@@ -31,9 +32,9 @@ const { Menu } = unlock( componentsPrivateApis );
31
32
 
32
33
  interface HeaderMenuProps< Item > {
33
34
  fieldId: string;
34
- view: ViewTableType;
35
+ view: ViewTableType | ViewPickerTableType;
35
36
  fields: NormalizedField< Item >[];
36
- onChangeView: ( view: ViewTableType ) => void;
37
+ onChangeView: ( view: ViewTableType | ViewPickerTableType ) => void;
37
38
  onHide: ( field: NormalizedField< Item > ) => void;
38
39
  setOpenedFilter: ( fieldId: string ) => void;
39
40
  canMove?: boolean;
@@ -16,7 +16,6 @@ import {
16
16
  */
17
17
  import type { NormalizedField } from '../../types';
18
18
  import { ItemClickWrapper } from '../utils/item-click-wrapper';
19
- import { sprintf, __ } from '@wordpress/i18n';
20
19
 
21
20
  function ColumnPrimary< Item >( {
22
21
  item,
@@ -51,12 +50,10 @@ function ColumnPrimary< Item >( {
51
50
  renderItemLink={ renderItemLink }
52
51
  className="dataviews-view-table__cell-content-wrapper dataviews-column-primary__media"
53
52
  aria-label={
54
- titleField
55
- ? sprintf(
56
- // translators: %s is the item title.
57
- __( 'Click item: %s' ),
58
- titleField.getValue?.( { item } )
59
- )
53
+ isItemClickable( item ) &&
54
+ ( !! onClickItem || !! renderItemLink ) &&
55
+ !! titleField
56
+ ? titleField.getValue?.( { item } )
60
57
  : undefined
61
58
  }
62
59
  >
@@ -8,7 +8,7 @@ import type { ComponentProps, ReactElement } from 'react';
8
8
  * WordPress dependencies
9
9
  */
10
10
  import { __, sprintf } from '@wordpress/i18n';
11
- import { Spinner } from '@wordpress/components';
11
+ import { Spinner, Popover } from '@wordpress/components';
12
12
  import {
13
13
  useContext,
14
14
  useEffect,
@@ -41,6 +41,7 @@ import ColumnHeaderMenu from './column-header-menu';
41
41
  import ColumnPrimary from './column-primary';
42
42
  import { useIsHorizontalScrollEnd } from './use-is-horizontal-scroll-end';
43
43
  import getDataByGroup from '../utils/get-data-by-group';
44
+ import { PropertiesSection } from '../../components/dataviews-view-config/properties-section';
44
45
 
45
46
  interface TableColumnFieldProps< Item > {
46
47
  fields: NormalizedField< Item >[];
@@ -296,6 +297,9 @@ function ViewTable< Item >( {
296
297
  const [ nextHeaderMenuToFocus, setNextHeaderMenuToFocus ] =
297
298
  useState< HTMLButtonElement >();
298
299
  const hasBulkActions = useSomeItemHasAPossibleBulkAction( actions, data );
300
+ const [ contextMenuAnchor, setContextMenuAnchor ] = useState< {
301
+ getBoundingClientRect: () => DOMRect;
302
+ } | null >( null );
299
303
 
300
304
  useEffect( () => {
301
305
  if ( headerMenuToFocusRef.current ) {
@@ -329,6 +333,27 @@ function ViewTable< Item >( {
329
333
  setNextHeaderMenuToFocus( fallback?.node );
330
334
  };
331
335
 
336
+ const handleHeaderContextMenu = ( event: React.MouseEvent ) => {
337
+ event.preventDefault();
338
+ event.stopPropagation();
339
+ const virtualAnchor = {
340
+ getBoundingClientRect: () => ( {
341
+ x: event.clientX,
342
+ y: event.clientY,
343
+ top: event.clientY,
344
+ left: event.clientX,
345
+ right: event.clientX,
346
+ bottom: event.clientY,
347
+ width: 0,
348
+ height: 0,
349
+ toJSON: () => ( {} ),
350
+ } ),
351
+ };
352
+ window.requestAnimationFrame( () => {
353
+ setContextMenuAnchor( virtualAnchor );
354
+ } );
355
+ };
356
+
332
357
  const hasData = !! data?.length;
333
358
 
334
359
  const titleField = fields.find( ( field ) => field.id === view.titleField );
@@ -369,17 +394,45 @@ function ViewTable< Item >( {
369
394
  [ 'compact', 'comfortable' ].includes(
370
395
  view.layout.density
371
396
  ),
397
+ 'has-bulk-actions': hasBulkActions,
372
398
  } ) }
373
399
  aria-busy={ isLoading }
374
400
  aria-describedby={ tableNoticeId }
375
401
  role={ isInfiniteScroll ? 'feed' : undefined }
376
402
  >
377
- <thead>
403
+ <colgroup>
404
+ { hasBulkActions && (
405
+ <col className="dataviews-view-table__col-checkbox" />
406
+ ) }
407
+ { hasPrimaryColumn && (
408
+ <col className="dataviews-view-table__col-primary" />
409
+ ) }
410
+ { columns.map( ( column ) => (
411
+ <col
412
+ key={ `col-${ column }` }
413
+ className={ `dataviews-view-table__col-${ column }` }
414
+ />
415
+ ) ) }
416
+ { !! actions?.length && (
417
+ <col className="dataviews-view-table__col-actions" />
418
+ ) }
419
+ </colgroup>
420
+ { contextMenuAnchor && (
421
+ <Popover
422
+ anchor={ contextMenuAnchor }
423
+ onClose={ () => setContextMenuAnchor( null ) }
424
+ placement="bottom-start"
425
+ >
426
+ <PropertiesSection showLabel={ false } />
427
+ </Popover>
428
+ ) }
429
+ <thead onContextMenu={ handleHeaderContextMenu }>
378
430
  <tr className="dataviews-view-table__row">
379
431
  { hasBulkActions && (
380
432
  <th
381
433
  className="dataviews-view-table__checkbox-column"
382
434
  scope="col"
435
+ onContextMenu={ handleHeaderContextMenu }
383
436
  >
384
437
  <BulkSelectionCheckbox
385
438
  selection={ selection }