Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
cc87232
Create DataPicker component as a sibling to DataViews
talldan Jul 21, 2025
2afca04
Remove DataPicker components
talldan Aug 13, 2025
f0e24d8
Add simple isPicker prop to dataviews, and use it to start configurin…
talldan Aug 13, 2025
d571cdb
Split into separate components
talldan Aug 25, 2025
67b13c2
Limit the type of views that can be provided to DataViews and DataVie…
talldan Aug 26, 2025
7f4f8aa
Switch to isPicker prop in the view layout config for filtering view …
talldan Aug 26, 2025
487d355
Fix type name
talldan Aug 26, 2025
45b1896
Share layout component between picker and regular dataviews
talldan Aug 26, 2025
817587b
Fix story and broken css
talldan Aug 26, 2025
1341b9a
Extract grid styling to a shared component
talldan Aug 26, 2025
23472bf
Switch back to a single View type
talldan Aug 27, 2025
103c689
Fix config type after rebase
talldan Aug 27, 2025
69cf7e2
Use context for `isPicker` rather than a prop to the layout component…
talldan Aug 27, 2025
5e0c8c9
Remove todo from test
talldan Aug 27, 2025
8827304
Allow custom content in footer and use multiselect property of picker…
talldan Aug 27, 2025
70e4390
Adjust footer styling and update story
talldan Aug 27, 2025
c933dfa
Update tests
talldan Aug 27, 2025
6d2dc02
Use actions API
talldan Aug 29, 2025
dc5c179
Make a controlled selection required for DataViewsPicker
talldan Aug 29, 2025
a8b9552
Update docs
talldan Aug 29, 2025
fc34b2e
Add changelog entry
talldan Aug 29, 2025
20dc4ae
Fix BulkSelectionCheckbox
talldan Aug 29, 2025
55f530b
Share the PreviewSizePicker between grid and pickerGrid
talldan Aug 29, 2025
9838725
Undo change to defaultLayouts
talldan Aug 29, 2025
3219ca1
Tidy up main DataViewsPicker component
talldan Aug 29, 2025
2a65f80
Remove extra tab stop on fields caused by tooltip
talldan Sep 1, 2025
274b28b
Remove note about importing DataViews
talldan Sep 1, 2025
403a8fe
Remove picker internal selection state
talldan Sep 1, 2025
568fb63
Align footer buttons to the right
talldan Sep 2, 2025
984ed47
Update action button style. Make non-primary button a tertiary varian…
talldan Sep 2, 2025
b5f8866
Align bulk selection to the left on mobile
talldan Sep 2, 2025
913601e
Remove outdated comment
talldan Sep 2, 2025
ef151d3
Remove change that was reverted in trunk after components diverged (s…
talldan Sep 3, 2025
2f4e5d8
Remove selection property from action type
talldan Sep 3, 2025
eabd7fb
Remove promise return type from ActionButton
talldan Sep 3, 2025
69490be
Remove unused props for picker
talldan Sep 3, 2025
ffd6e09
Limit actions to ActionButton via type
talldan Sep 3, 2025
73d5182
Update wrapper classname
talldan Sep 3, 2025
05f1e00
Filter picker/non-picker layouts in main DataViews components
talldan Sep 3, 2025
e71299b
Move label prop to DataViewsPicker instead of View
talldan Sep 3, 2025
b948c7a
Rename label to itemListLabel
talldan Sep 4, 2025
383bed7
Add missing changes to ViewPickerBaseProps
talldan Sep 4, 2025
9bca0d4
Update label name in docs
talldan Sep 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions packages/dataviews/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

- Introduce a new `DataViewsPicker` component. [#70971](https://github.com/WordPress/gutenberg/pull/70971)

## 8.0.0 (2025-09-03)

### Breaking changes
Expand All @@ -11,6 +15,7 @@
### Enhancements

- DataForm: add description support for the combined fields and show the description in the Card layout ([#71380](https://github.com/WordPress/gutenberg/pull/71380)).
- Add support for hiding the `title` in Grid layouts, with the actions menu rendered over the media preview. [#71369](https://github.com/WordPress/gutenberg/pull/71369)

### Internal

Expand All @@ -21,10 +26,6 @@
- DataViews: Fix incorrect documentation for `defaultLayouts` prop. [#71334](https://github.com/WordPress/gutenberg/pull/71334)
- DataViews: Fix mismatched padding on mobile viewports for grid layout [#71455](https://github.com/WordPress/gutenberg/pull/71455)

### Enhancements

- Add support for hiding the `title` in Grid layouts, with the actions menu rendered over the media preview. [#71369](https://github.com/WordPress/gutenberg/pull/71369)

## 7.0.0 (2025-08-20)

### Breaking changes
Expand Down
76 changes: 76 additions & 0 deletions packages/dataviews/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,12 +381,16 @@ The list of selected items' ids.

If `selection` and `onChangeSelection` are provided, the `DataViews` component behaves like a controlled component. Otherwise, it behaves like an uncontrolled component.

Note: `DataViews` still requires at least one bulk action to make items selectable.

#### `onChangeSelection`: `function`

Callback that signals the user selected one of more items. It receives the list of selected items' IDs as a parameter.

If `selection` and `onChangeSelection` are provided, the `DataViews` component behaves like a controlled component. Otherwise, it behaves like an uncontrolled component.

Note: `DataViews` still requires at least one bulk action to make items selectable.

#### `isItemClickable`: `function`

A function that determines if a media field or a primary field is clickable. It receives an item as an argument and returns a boolean value indicating whether the item can be clicked.
Expand Down Expand Up @@ -496,6 +500,78 @@ Developers don't need to worry about the internal accessibility logic for indivi

`FiltersToggle` controls the visibility of the filters panel, and `Filters` renders the actual filters inside it. They work together and should always be used as a pair. While their internal behavior is accessible by default, how they’re positioned and grouped in custom layouts may affect the overall experience — especially for assistive technologies. Extra care is recommended.

## `DataViewsPicker`

<div class="callout callout-info">At <a href="https://wordpress.github.io/gutenberg/">WordPress Gutenberg's Storybook</a> there's an <a href="https://wordpress.github.io/gutenberg/?path=/docs/dataviews-dataviewspicker--docs">example implementation of the DataviewsPicker component</a>.</div>

### Usage

The `DataViewsPicker` component is very similar to the regular `DataViews` component, but is optimized for selection or picking of items.

The component behaves differently to a regular `DataViews` component in the following ways:

- The items in the view are rendered using the `listbox` and `option` aria roles.
- Holding the `ctrl` or `cmd` key isn't required for multi-selection of items. The entire item can be clicked to select or deselect it.
- Individual items do not display any actions. All actions appear in the footer as text buttons.
- Selection is maintained across multiple pages when the component is paginated.

There are also a few differences in the implementation:

- Currently only the `pickerGrid` layout is supported for `DataViewsPicker`. This layout is very similar to the regular `grid` layout.
- The picker component is used as a 'controlled' component, so `selection` and `onChangeSelection` should be provided as props. This is so that implementers can access the full range of selected items across pages.
- An optional `itemListLabel` prop can be supplied to the `DataViewsPicker` component. This is added as an `aria-label` to the `listbox` element, and should be supplied if there's no heading element associated with the `DataViewsPicker` UI.
- The `isItemClickable`, `renderItemLink` and `onClickItem` prop are unsupported for `DataViewsPicker`.
- To implement a multi-selection picker, ensure all actions are declared with `supportsBulk: true`. For single selection use `supportsBulk: false`. When a mixture of bulk and non-bulk actions are provided, the component falls back to single selection.
- Only the `callback` style of action is supported. `RenderModal` is unsupported.
- The `isEligible` callback for actions is unsupported.
- The `isPrimary` option for an action is used to render a `primary` variant of `Button` that can be used as a main call to action.

Example:

```jsx
const Example = () => {
// When using DataViewsPicker, `selection` should be managed so that the component is 'controlled'.
const [ selection, setSelection ] = useState( [] );

// Both actions have `supportsBulk: true`, so the `DataViewsPicker` will allow multi-selection.
const actions = [
{
id: 'confirm',
label: 'Confirm',
isPrimary: true,
supportsBulk: true,
callback() {
window.alert( selection.join( ', ' ) );
},
},
{
id: 'cancel',
label: 'Cancel',
supportsBulk: true,
callback() {
setSelection( [] );
},
},
];

return (
<DataViewsPicker
actions={ actions }
data={ data }
fields={ fields }
view={ view }
onChangeView={ onChangeView }
defaultLayouts={ defaultLayouts }
paginationInfo={ paginationInfo }
selection={ selection }
onChangeSelection={ setSelection }
/>
);
};
```

### Properties

## `DataForm`

<div class="callout callout-info">At <a href="https://wordpress.github.io/gutenberg/">WordPress Gutenberg's Storybook</a> there's and <a href="https://wordpress.github.io/gutenberg/?path=/docs/dataviews-dataform--docs">example implementation of the DataForm component</a>.</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type DataViewsContextType< Item > = {
config: { perPageSizes: number[] };
empty?: ReactNode;
hasInfiniteScrollHandler: boolean;
itemListLabel?: string;
};

const DataViewsContext = createContext< DataViewsContextType< any > >( {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ export default function DataViewsLayout( { className }: DataViewsLayoutProps ) {
onClickItem,
isItemClickable,
renderItemLink,
defaultLayouts,
empty = __( 'No results' ),
} = useContext( DataViewsContext );

const ViewComponent = VIEW_LAYOUTS.find( ( v ) => v.type === view.type )
?.component as ComponentType< ViewBaseProps< any > >;
const ViewComponent = VIEW_LAYOUTS.find(
( v ) => v.type === view.type && defaultLayouts[ v.type ]
)?.component as ComponentType< ViewBaseProps< any > >;

return (
<ViewComponent
Expand Down
207 changes: 207 additions & 0 deletions packages/dataviews/src/components/dataviews-picker/footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/**
* WordPress dependencies
*/
import {
Button,
CheckboxControl,
__experimentalHStack as HStack,
} from '@wordpress/components';
import { useRegistry } from '@wordpress/data';
import { useContext, useMemo, useState } from '@wordpress/element';
import { __, sprintf, _n } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import DataViewsPagination from '../dataviews-pagination';
import DataViewsContext from '../dataviews-context';
import type { SetSelection } from '../../private-types';
import type { Action } from '../../types';

const EMPTY_ARRAY: [] = [];

export function useIsMultiselectPicker< Item >(
actions: Action< Item >[] | undefined
) {
return useMemo( () => {
return actions?.every( ( action ) => action.supportsBulk );
}, [ actions ] );
}

function BulkSelectionCheckbox< Item >( {
selection,
selectedItems,
onChangeSelection,
data,
getItemId,
}: {
selection: string[];
selectedItems: Item[];
onChangeSelection: SetSelection;
data: Item[];
getItemId: ( item: Item ) => string;
} ) {
const areAllSelected = selectedItems.length === data.length;

return (
<CheckboxControl
className="dataviews-view-table-selection-checkbox"
__nextHasNoMarginBottom
checked={ areAllSelected }
indeterminate={ ! areAllSelected && !! selectedItems.length }
onChange={ () => {
if ( areAllSelected ) {
// Deselect all - remove the current page from the total selection.
onChangeSelection(
selection.filter(
( id ) =>
! data.some(
( item ) => id === getItemId( item )
)
)
);
} else {
// Select all - merge the current page into the total selection.
const selectionSet = new Set( [
...selection,
...data.map( ( item ) => getItemId( item ) ),
] );
onChangeSelection( Array.from( selectionSet ) );
}
} }
aria-label={
areAllSelected ? __( 'Deselect all' ) : __( 'Select all' )
}
/>
);
}

function ActionButtons< Item >( {
actions,
items,
selection,
}: {
actions: Action< Item >[];
items: Item[];
selection: string[];
} ) {
const registry = useRegistry();
const [ actionInProgress, setActionInProgress ] = useState< string | null >(
null
);

return (
<HStack expanded={ false } spacing={ 1 }>
{ actions.map( ( action ) => {
// Only support actions with callbacks for DataViewsPicker.
// This is because many use cases of the picker will be already within modals.
if ( ! ( 'callback' in action ) ) {
return null;
}

const { id, label, icon, isPrimary, isDestructive, callback } =
action;

const _label =
typeof label === 'string' ? label : label( items );
const variant = isPrimary ? 'primary' : 'tertiary';
const isInProgress = id === actionInProgress;

return (
<Button
key={ id }
accessibleWhenDisabled
icon={ icon }
disabled={ isInProgress || ! selection?.length }
isBusy={ isInProgress }
onClick={ async () => {
setActionInProgress( id );
await callback( items, {
registry,
} );
setActionInProgress( null );
} }
size="compact"
isDestructive={ isDestructive }
variant={ variant }
>
{ _label }
</Button>
);
} ) }
</HStack>
);
}

export function DataViewsPickerFooter() {
const {
data,
selection,
onChangeSelection,
getItemId,
actions = EMPTY_ARRAY,
} = useContext( DataViewsContext );

const selectionCount = selection.length;
const isMultiselect = useIsMultiselectPicker( actions );

const message =
selectionCount > 0
? sprintf(
/* translators: %d: number of items. */
_n(
'%d Item selected',
'%d Items selected',
selectionCount
),
selectionCount
)
: sprintf(
/* translators: %d: number of items. */
_n( '%d Item', '%d Items', data.length ),
data.length
);

const selectedItems = useMemo(
() =>
data.filter( ( item ) => selection.includes( getItemId( item ) ) ),
[ selection, getItemId, data ]
);

return (
<HStack
expanded={ false }
justify="space-between"
className="dataviews-footer"
>
<HStack
className="dataviews-picker-footer__bulk-selection"
expanded={ false }
spacing={ 3 }
>
{ isMultiselect && (
<BulkSelectionCheckbox
selection={ selection }
selectedItems={ selectedItems }
onChangeSelection={ onChangeSelection }
data={ data }
getItemId={ getItemId }
/>
) }
<span className="dataviews-bulk-actions-footer__item-count">
{ message }
</span>
</HStack>
<DataViewsPagination />
{ Boolean( actions?.length ) && (
<div className="dataviews-picker-footer__actions">
<ActionButtons
actions={ actions }
items={ selectedItems }
selection={ selection }
/>
</div>
) }
</HStack>
);
}
Loading
Loading