From 28cd670beecc3798eb1bc9cabe2fa0bbea12d255 Mon Sep 17 00:00:00 2001 From: Luigi Teschio Date: Tue, 12 Nov 2024 12:52:16 +0100 Subject: [PATCH] DataViews: Implement `isItemClickable` and `onClickItem` props (#66365) Co-authored-by: gigitux Co-authored-by: oandregal Co-authored-by: louwie17 Co-authored-by: youknowriad Co-authored-by: mcsf Co-authored-by: jameskoster --- packages/dataviews/README.md | 8 ++++ .../src/components/dataviews-context/index.ts | 4 ++ .../src/components/dataviews-layout/index.tsx | 4 ++ .../src/components/dataviews/index.tsx | 11 +++++- .../src/components/dataviews/style.scss | 12 ++++-- .../src/dataviews-layouts/grid/index.tsx | 34 ++++++++++++++--- .../src/dataviews-layouts/grid/style.scss | 5 +++ .../src/dataviews-layouts/table/index.tsx | 37 +++++++++++++++++-- .../utils/get-clickable-item-props.ts | 22 +++++++++++ packages/dataviews/src/types.ts | 2 + .../src/components/post-fields/index.js | 36 ++++-------------- .../src/components/post-list/index.js | 12 ++++-- .../src/components/post-list/style.scss | 32 ++++++++++++++-- packages/edit-site/src/style.scss | 4 +- packages/fields/src/style.scss | 2 + packages/fields/src/styles.scss | 1 - 16 files changed, 175 insertions(+), 51 deletions(-) create mode 100644 packages/dataviews/src/dataviews-layouts/utils/get-clickable-item-props.ts create mode 100644 packages/fields/src/style.scss delete mode 100644 packages/fields/src/styles.scss diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index ff20386862929e..621e3c7ba71ce2 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -358,6 +358,14 @@ Callback that signals the user selected one of more items. It receives the list If `selection` and `onChangeSelection` are provided, the `DataViews` component behaves as a controlled component, otherwise, it behaves like an uncontrolled component. +### `isItemClickable`: `function` + +A function that determines if a media field or a primary field are clickable. It receives an item as an argument and returns a boolean value indicating whether the item can be clicked. + +### `onClickItem`: `function` + +A callback function that is triggered when a user clicks on a media field or primary field. This function is currently implemented only in the `grid` and `list` views. + #### `header`: React component React component to be rendered next to the view config button. diff --git a/packages/dataviews/src/components/dataviews-context/index.ts b/packages/dataviews/src/components/dataviews-context/index.ts index 3936288b3095b0..87acade73bc819 100644 --- a/packages/dataviews/src/components/dataviews-context/index.ts +++ b/packages/dataviews/src/components/dataviews-context/index.ts @@ -26,6 +26,8 @@ type DataViewsContextType< Item > = { openedFilter: string | null; setOpenedFilter: ( openedFilter: string | null ) => void; getItemId: ( item: Item ) => string; + onClickItem: ( item: Item ) => void; + isItemClickable: ( item: Item ) => boolean; density: number; }; @@ -43,6 +45,8 @@ const DataViewsContext = createContext< DataViewsContextType< any > >( { setOpenedFilter: () => {}, openedFilter: null, getItemId: ( item ) => item.id, + onClickItem: () => {}, + isItemClickable: () => false, density: 0, } ); diff --git a/packages/dataviews/src/components/dataviews-layout/index.tsx b/packages/dataviews/src/components/dataviews-layout/index.tsx index bae4071fe2f773..4ef0125b1f64b5 100644 --- a/packages/dataviews/src/components/dataviews-layout/index.tsx +++ b/packages/dataviews/src/components/dataviews-layout/index.tsx @@ -28,6 +28,8 @@ export default function DataViewsLayout() { onChangeSelection, setOpenedFilter, density, + onClickItem, + isItemClickable, } = useContext( DataViewsContext ); const ViewComponent = VIEW_LAYOUTS.find( ( v ) => v.type === view.type ) @@ -44,6 +46,8 @@ export default function DataViewsLayout() { onChangeSelection={ onChangeSelection } selection={ selection } setOpenedFilter={ setOpenedFilter } + onClickItem={ onClickItem } + isItemClickable={ isItemClickable } view={ view } density={ density } /> diff --git a/packages/dataviews/src/components/dataviews/index.tsx b/packages/dataviews/src/components/dataviews/index.tsx index da60ab15ecadec..77a5cb8740f712 100644 --- a/packages/dataviews/src/components/dataviews/index.tsx +++ b/packages/dataviews/src/components/dataviews/index.tsx @@ -44,12 +44,17 @@ type DataViewsProps< Item > = { defaultLayouts: SupportedLayouts; selection?: string[]; onChangeSelection?: ( items: string[] ) => void; + onClickItem?: ( item: Item ) => void; + isItemClickable?: ( item: Item ) => boolean; header?: ReactNode; } & ( Item extends ItemWithId ? { getItemId?: ( item: Item ) => string } : { getItemId: ( item: Item ) => string } ); const defaultGetItemId = ( item: ItemWithId ) => item.id; +const defaultIsItemClickable = () => false; +const defaultOnClickItem = () => {}; +const EMPTY_ARRAY: any[] = []; export default function DataViews< Item >( { view, @@ -57,7 +62,7 @@ export default function DataViews< Item >( { fields, search = true, searchLabel = undefined, - actions = [], + actions = EMPTY_ARRAY, data, getItemId = defaultGetItemId, isLoading = false, @@ -65,6 +70,8 @@ export default function DataViews< Item >( { defaultLayouts, selection: selectionProperty, onChangeSelection, + onClickItem = defaultOnClickItem, + isItemClickable = defaultIsItemClickable, header, }: DataViewsProps< Item > ) { const [ selectionState, setSelectionState ] = useState< string[] >( [] ); @@ -110,6 +117,8 @@ export default function DataViews< Item >( { openedFilter, setOpenedFilter, getItemId, + isItemClickable, + onClickItem, density, } } > diff --git a/packages/dataviews/src/components/dataviews/style.scss b/packages/dataviews/src/components/dataviews/style.scss index aa8fbcfb009c05..bd75a1ff9e2a18 100644 --- a/packages/dataviews/src/components/dataviews/style.scss +++ b/packages/dataviews/src/components/dataviews/style.scss @@ -19,7 +19,7 @@ position: sticky; left: 0; transition: padding ease-out 0.1s; - @include reduce-motion("transition"); + @include reduce-motion( "transition" ); } .dataviews-view-list__primary-field, @@ -62,6 +62,13 @@ } } +.dataviews-view-list__primary-field--clickable, +.dataviews-view-grid__primary-field--clickable, +.dataviews-view-grid__media--clickable, +.dataviews-view-table__primary-field > .dataviews-view-table__cell-content--clickable { + cursor: pointer; +} + .dataviews-no-results, .dataviews-loading { padding: 0 $grid-unit-60; @@ -70,7 +77,7 @@ align-items: center; justify-content: center; transition: padding ease-out 0.1s; - @include reduce-motion("transition"); + @include reduce-motion( "transition" ); } /* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ @@ -86,4 +93,3 @@ padding-right: $grid-unit-30; } } - diff --git a/packages/dataviews/src/dataviews-layouts/grid/index.tsx b/packages/dataviews/src/dataviews-layouts/grid/index.tsx index 230ffe0dc50b5c..91cc87ec7b35b6 100644 --- a/packages/dataviews/src/dataviews-layouts/grid/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/grid/index.tsx @@ -24,11 +24,14 @@ import SingleSelectionCheckbox from '../../components/dataviews-selection-checkb import { useHasAPossibleBulkAction } from '../../components/dataviews-bulk-actions'; import type { Action, NormalizedField, ViewGridProps } from '../../types'; import type { SetSelection } from '../../private-types'; +import getClickableItemProps from '../utils/get-clickable-item-props'; interface GridItemProps< Item > { selection: string[]; onChangeSelection: SetSelection; getItemId: ( item: Item ) => string; + onClickItem: ( item: Item ) => void; + isItemClickable: ( item: Item ) => boolean; item: Item; actions: Action< Item >[]; mediaField?: NormalizedField< Item >; @@ -41,6 +44,8 @@ interface GridItemProps< Item > { function GridItem< Item >( { selection, onChangeSelection, + onClickItem, + isItemClickable, getItemId, item, actions, @@ -59,6 +64,21 @@ function GridItem< Item >( { const renderedPrimaryField = primaryField?.render ? ( ) : null; + + const clickableMediaItemProps = getClickableItemProps( + item, + isItemClickable, + onClickItem, + 'dataviews-view-grid__media' + ); + + const clickablePrimaryItemProps = getClickableItemProps( + item, + isItemClickable, + onClickItem, + 'dataviews-view-grid__primary-field' + ); + return ( ( { } } } > -
- { renderedMediaField } -
+
{ renderedMediaField }
( { justify="space-between" className="dataviews-view-grid__title-actions" > - - { renderedPrimaryField } + +
+ { renderedPrimaryField } +
@@ -170,6 +190,8 @@ export default function ViewGrid< Item >( { getItemId, isLoading, onChangeSelection, + onClickItem, + isItemClickable, selection, view, density, @@ -223,6 +245,8 @@ export default function ViewGrid< Item >( { key={ getItemId( item ) } selection={ selection } onChangeSelection={ onChangeSelection } + onClickItem={ onClickItem } + isItemClickable={ isItemClickable } getItemId={ getItemId } item={ item } actions={ actions } diff --git a/packages/dataviews/src/dataviews-layouts/grid/style.scss b/packages/dataviews/src/dataviews-layouts/grid/style.scss index 6286ed94685a04..55768240a18714 100644 --- a/packages/dataviews/src/dataviews-layouts/grid/style.scss +++ b/packages/dataviews/src/dataviews-layouts/grid/style.scss @@ -17,8 +17,13 @@ .dataviews-view-grid__primary-field { min-height: $grid-unit-40; // Preserve layout when there is no ellipsis button + + &--clickable { + width: fit-content; + } } + &.is-selected { .dataviews-view-grid__fields .dataviews-view-grid__field .dataviews-view-grid__field-value { color: $gray-900; diff --git a/packages/dataviews/src/dataviews-layouts/table/index.tsx b/packages/dataviews/src/dataviews-layouts/table/index.tsx index 4e1607b01489c2..8ef41db1c38798 100644 --- a/packages/dataviews/src/dataviews-layouts/table/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/table/index.tsx @@ -35,11 +35,14 @@ import type { import type { SetSelection } from '../../private-types'; import ColumnHeaderMenu from './column-header-menu'; import { getVisibleFieldIds } from '../index'; +import getClickableItemProps from '../utils/get-clickable-item-props'; interface TableColumnFieldProps< Item > { primaryField?: NormalizedField< Item >; field: NormalizedField< Item >; item: Item; + isItemClickable: ( item: Item ) => boolean; + onClickItem: ( item: Item ) => void; } interface TableColumnCombinedProps< Item > { @@ -48,6 +51,8 @@ interface TableColumnCombinedProps< Item > { field: CombinedField; item: Item; view: ViewTableType; + isItemClickable: ( item: Item ) => boolean; + onClickItem: ( item: Item ) => void; } interface TableColumnProps< Item > { @@ -56,6 +61,8 @@ interface TableColumnProps< Item > { item: Item; column: string; view: ViewTableType; + isItemClickable: ( item: Item ) => boolean; + onClickItem: ( item: Item ) => void; } interface TableRowProps< Item > { @@ -69,6 +76,8 @@ interface TableRowProps< Item > { selection: string[]; getItemId: ( item: Item ) => string; onChangeSelection: SetSelection; + isItemClickable: ( item: Item ) => boolean; + onClickItem: ( item: Item ) => void; } function TableColumn< Item >( { @@ -102,15 +111,29 @@ function TableColumnField< Item >( { primaryField, item, field, + isItemClickable, + onClickItem, }: TableColumnFieldProps< Item > ) { + const isPrimaryField = primaryField?.id === field.id; + const isItemClickableField = ( i: Item ) => + isItemClickable( i ) && isPrimaryField; + + const clickableProps = getClickableItemProps( + item, + isItemClickableField, + onClickItem, + 'dataviews-view-table__cell-content' + ); + return (
- +
+ +
); } @@ -139,6 +162,8 @@ function TableRow< Item >( { primaryField, selection, getItemId, + isItemClickable, + onClickItem, onChangeSelection, }: TableRowProps< Item > ) { const hasPossibleBulkAction = useHasAPossibleBulkAction( actions, item ); @@ -214,6 +239,8 @@ function TableRow< Item >( { ( { onChangeSelection, selection, setOpenedFilter, + onClickItem, + isItemClickable, view, }: ViewTableProps< Item > ) { const headerMenuRefs = useRef< @@ -392,6 +421,8 @@ function ViewTable< Item >( { selection={ selection } getItemId={ getItemId } onChangeSelection={ onChangeSelection } + onClickItem={ onClickItem } + isItemClickable={ isItemClickable } /> ) ) } diff --git a/packages/dataviews/src/dataviews-layouts/utils/get-clickable-item-props.ts b/packages/dataviews/src/dataviews-layouts/utils/get-clickable-item-props.ts new file mode 100644 index 00000000000000..e2a6081a68fa3e --- /dev/null +++ b/packages/dataviews/src/dataviews-layouts/utils/get-clickable-item-props.ts @@ -0,0 +1,22 @@ +export default function getClickableItemProps< Item >( + item: Item, + isItemClickable: ( item: Item ) => boolean, + onClickItem: ( item: Item ) => void, + className: string +) { + if ( ! isItemClickable( item ) ) { + return { className }; + } + + return { + className: `${ className } ${ className }--clickable`, + role: 'button', + tabIndex: 0, + onClick: () => onClickItem( item ), + onKeyDown: ( event: React.KeyboardEvent ) => { + if ( event.key === 'Enter' || event.key === '' ) { + onClickItem( item ); + } + }, + }; +} diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 0ea0965704d18c..71990f72d4eecd 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -498,6 +498,8 @@ export interface ViewBaseProps< Item > { onChangeSelection: SetSelection; selection: string[]; setOpenedFilter: ( fieldId: string ) => void; + onClickItem: ( item: Item ) => void; + isItemClickable: ( item: Item ) => boolean; view: View; density: number; } diff --git a/packages/edit-site/src/components/post-fields/index.js b/packages/edit-site/src/components/post-fields/index.js index e659a4f96f23f6..54f47052b144cf 100644 --- a/packages/edit-site/src/components/post-fields/index.js +++ b/packages/edit-site/src/components/post-fields/index.js @@ -36,12 +36,7 @@ import { useEntityRecords, store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies */ -import { - LAYOUT_GRID, - LAYOUT_TABLE, - OPERATOR_IS_ANY, -} from '../../utils/constants'; -import { default as Link } from '../routes/link'; +import { OPERATOR_IS_ANY } from '../../utils/constants'; // See https://github.com/WordPress/gutenberg/issues/55886 // We do not support custom statutes at the moment. @@ -139,7 +134,7 @@ function PostAuthorField( { item } ) { ); } -function usePostFields( viewType ) { +function usePostFields() { const { records: authors, isResolving: isLoadingAuthors } = useEntityRecords( 'root', 'user', { per_page: -1 } ); @@ -164,30 +159,10 @@ function usePostFields( viewType ) { ? item.title : item.title?.raw, render: ( { item } ) => { - const addLink = - [ LAYOUT_TABLE, LAYOUT_GRID ].includes( viewType ) && - item.status !== 'trash'; const renderedTitle = typeof item.title === 'string' ? item.title : item.title?.rendered; - const title = addLink ? ( - - { decodeEntities( renderedTitle ) || - __( '(no title)' ) } - - ) : ( - - { decodeEntities( renderedTitle ) || - __( '(no title)' ) } - - ); let suffix = ''; if ( item.id === frontPageId ) { @@ -210,7 +185,10 @@ function usePostFields( viewType ) { alignment="center" justify="flex-start" > - { title } + + { decodeEntities( renderedTitle ) || + __( '(no title)' ) } + { suffix } ); @@ -355,7 +333,7 @@ function usePostFields( viewType ) { }, passwordField, ], - [ authors, viewType, frontPageId, postsPageId ] + [ authors, frontPageId, postsPageId ] ); return { diff --git a/packages/edit-site/src/components/post-list/index.js b/packages/edit-site/src/components/post-list/index.js index 4985af3050bd8d..4639cb3c950b76 100644 --- a/packages/edit-site/src/components/post-list/index.js +++ b/packages/edit-site/src/components/post-list/index.js @@ -208,9 +208,7 @@ export default function PostList( { postType } ) { return found?.filters ?? []; }; - const { isLoading: isLoadingFields, fields: _fields } = usePostFields( - view.type - ); + const { isLoading: isLoadingFields, fields: _fields } = usePostFields(); const fields = useMemo( () => { const activeViewFilters = getActiveViewFilters( defaultViews, @@ -402,6 +400,14 @@ export default function PostList( { postType } ) { onChangeView={ setView } selection={ selection } onChangeSelection={ onChangeSelection } + isItemClickable={ ( item ) => item.status !== 'trash' } + onClickItem={ ( { id } ) => { + history.push( { + postId: id, + postType, + canvas: 'edit', + } ); + } } getItemId={ getItemId } defaultLayouts={ defaultLayouts } header={ diff --git a/packages/edit-site/src/components/post-list/style.scss b/packages/edit-site/src/components/post-list/style.scss index db6a32408c7921..14bb11b41d4450 100644 --- a/packages/edit-site/src/components/post-list/style.scss +++ b/packages/edit-site/src/components/post-list/style.scss @@ -9,7 +9,9 @@ width: 100%; border-radius: $grid-unit-05; - &.is-layout-table:not(:has(.edit-site-post-list__featured-image-button)), + &.is-layout-table:not( + :has(.edit-site-post-list__featured-image-button) +), &.is-layout-table .edit-site-post-list__featured-image-button { width: $grid-unit-40; height: $grid-unit-40; @@ -46,7 +48,9 @@ border-radius: $grid-unit-05; &:focus-visible { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + box-shadow: + 0 0 0 var(--wp-admin-border-width-focus) + var(--wp-admin-theme-color); // Windows High Contrast mode will show this outline, but not the box-shadow. outline: 2px solid transparent; } @@ -54,7 +58,9 @@ .dataviews-view-grid__card.is-selected { .edit-site-post-list__featured-image-button::after { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + box-shadow: + inset 0 0 0 var(--wp-admin-border-width-focus) + var(--wp-admin-theme-color); background: rgba(var(--wp-admin-theme-color--rgb), 0.04); } } @@ -64,6 +70,26 @@ overflow: hidden; } +.dataviews-view-grid__primary-field.dataviews-view-grid__primary-field--clickable +.edit-site-post-list__title +span, +.dataviews-view-table__primary-field > .dataviews-view-table__cell-content--clickable +.edit-site-post-list__title +span { + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + display: block; + flex-grow: 0; + color: $gray-900; + + &:hover { + color: var(--wp-admin-theme-color); + } + @include link-reset(); +} + .edit-site-post-list__title-badge { background: $gray-100; color: $gray-800; diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 03ec43648f120f..0e5744fe362e35 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -1,7 +1,5 @@ @import "../../dataviews/src/style.scss"; -@import "../../fields/src/styles.scss"; -@import "../../fields/src/fields/featured-image/style.scss"; - +@import "../../fields/src/style.scss"; @import "./components/add-new-template/style.scss"; @import "./components/block-editor/style.scss"; @import "./components/canvas-loader/style.scss"; diff --git a/packages/fields/src/style.scss b/packages/fields/src/style.scss new file mode 100644 index 00000000000000..1639f455ba093e --- /dev/null +++ b/packages/fields/src/style.scss @@ -0,0 +1,2 @@ +@import "./fields/slug/style.scss"; +@import "./fields/featured-image/style.scss"; diff --git a/packages/fields/src/styles.scss b/packages/fields/src/styles.scss deleted file mode 100644 index cdb130337f1cd9..00000000000000 --- a/packages/fields/src/styles.scss +++ /dev/null @@ -1 +0,0 @@ -@import "./fields/slug/style.scss";