Skip to content

Commit

Permalink
Add: Media field changing ui to Dataviews and content preview field t…
Browse files Browse the repository at this point in the history
…o posts and pages (#67278)

Co-authored-by: jorgefilipecosta <jorgefilipecosta@git.wordpress.org>
Co-authored-by: ntsekouras <ntsekouras@git.wordpress.org>
Co-authored-by: youknowriad <youknowriad@git.wordpress.org>
Co-authored-by: oandregal <oandregal@git.wordpress.org>
Co-authored-by: jameskoster <jameskoster@git.wordpress.org>
Co-authored-by: jasmussen <joen@git.wordpress.org>
  • Loading branch information
7 people authored Jan 7, 2025
1 parent cc8f8a4 commit dd70c03
Show file tree
Hide file tree
Showing 14 changed files with 366 additions and 50 deletions.
212 changes: 176 additions & 36 deletions packages/dataviews/src/components/dataviews-view-config/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/**
* External dependencies
*/
import type { ChangeEvent } from 'react';
import type { ChangeEvent, ReactNode } from 'react';
import clsx from 'clsx';

/**
* WordPress dependencies
Expand All @@ -26,14 +27,15 @@ import {
Icon,
} from '@wordpress/components';
import { __, _x, sprintf } from '@wordpress/i18n';
import { memo, useContext, useMemo } from '@wordpress/element';
import { memo, useContext, useMemo, useState } from '@wordpress/element';
import {
chevronDown,
chevronUp,
cog,
seen,
unseen,
lock,
moreVertical,
} from '@wordpress/icons';
import warning from '@wordpress/warning';
import { useInstanceId } from '@wordpress/compose';
Expand Down Expand Up @@ -253,25 +255,92 @@ function ItemsPerPageControl() {
);
}

function PreviewOptions( {
previewOptions,
onChangePreviewOption,
onMenuOpenChange,
activeOption,
}: {
previewOptions?: Array< { label: string; id: string } >;
onChangePreviewOption?: ( newPreviewOption: string ) => void;
onMenuOpenChange: ( isOpen: boolean ) => void;
activeOption?: string;
} ) {
const focusPreviewOptionsField = ( id: string ) => {
// Focus the visibility button to avoid focus loss.
// Our code is safe against the component being unmounted, so we don't need to worry about cleaning the timeout.
// eslint-disable-next-line @wordpress/react-no-unsafe-timeout
setTimeout( () => {
const element = document.querySelector(
`.dataviews-field-control__field-${ id } .dataviews-field-control__field-preview-options-button`
);
if ( element instanceof HTMLElement ) {
element.focus();
}
}, 50 );
};
return (
<Menu onOpenChange={ onMenuOpenChange }>
<Menu.TriggerButton
render={
<Button
className="dataviews-field-control__field-preview-options-button"
size="compact"
icon={ moreVertical }
label={ __( 'Preview' ) }
/>
}
/>
<Menu.Popover>
{ previewOptions?.map( ( { id, label } ) => {
return (
<Menu.RadioItem
key={ id }
value={ id }
checked={ id === activeOption }
onChange={ () => {
onChangePreviewOption?.( id );
focusPreviewOptionsField( id );
} }
>
<Menu.ItemLabel>{ label }</Menu.ItemLabel>
</Menu.RadioItem>
);
} ) }
</Menu.Popover>
</Menu>
);
}
function FieldItem( {
field,
label,
description,
isVisible,
isFirst,
isLast,
canMove = true,
onToggleVisibility,
onMoveUp,
onMoveDown,
previewOptions,
onChangePreviewOption,
}: {
field: NormalizedField< any >;
label?: string;
description?: string;
isVisible: boolean;
isFirst?: boolean;
isLast?: boolean;
canMove?: boolean;
onToggleVisibility?: () => void;
onMoveUp?: () => void;
onMoveDown?: () => void;
previewOptions?: Array< { label: string; id: string } >;
onChangePreviewOption?: ( newPreviewOption: string ) => void;
} ) {
const [ isChangingPreviewOption, setIsChangingPreviewOption ] =
useState< boolean >( false );

const focusVisibilityField = () => {
// Focus the visibility button to avoid focus loss.
// Our code is safe against the component being unmounted, so we don't need to worry about cleaning the timeout.
Expand All @@ -290,16 +359,33 @@ function FieldItem( {
<Item>
<HStack
expanded
className={ `dataviews-field-control__field dataviews-field-control__field-${ field.id }` }
className={ clsx(
'dataviews-field-control__field',
`dataviews-field-control__field-${ field.id }`,
// The actions are hidden when the mouse is not hovering the item, or focus
// is outside the item.
// For actions that require a popover, a menu etc, that would mean that when the interactive element
// opens and the focus goes there the actions would be hidden.
// To avoid that we add a class to the item, that makes sure actions are visible while there is some
// interaction with the item.
{ 'is-interacting': isChangingPreviewOption }
) }
justify="flex-start"
>
<span className="dataviews-field-control__icon">
{ ! canMove && ! field.enableHiding && (
<Icon icon={ lock } />
) }
</span>
<span className="dataviews-field-control__label">
{ field.label }
<span className="dataviews-field-control__label-sub-label-container">
<span className="dataviews-field-control__label">
{ label || field.label }
</span>
{ description && (
<span className="dataviews-field-control__sub-label">
{ description }
</span>
) }
</span>
<HStack
justify="flex-end"
Expand Down Expand Up @@ -368,6 +454,14 @@ function FieldItem( {
}
/>
) }
{ previewOptions && (
<PreviewOptions
previewOptions={ previewOptions }
onChangePreviewOption={ onChangePreviewOption }
onMenuOpenChange={ setIsChangingPreviewOption }
activeOption={ field.id }
/>
) }
</HStack>
</HStack>
</Item>
Expand Down Expand Up @@ -461,7 +555,8 @@ function FieldControl() {
const hiddenFields = fields.filter(
( f ) =>
! visibleFieldIds.includes( f.id ) &&
! togglableFields.includes( f.id )
! togglableFields.includes( f.id ) &&
f.type !== 'media'
);
const visibleFields = visibleFieldIds
.map( ( fieldId ) => fields.find( ( f ) => f.id === fieldId ) )
Expand All @@ -471,18 +566,50 @@ function FieldControl() {
return null;
}
const titleField = fields.find( ( f ) => f.id === view.titleField );
const mediaField = fields.find( ( f ) => f.id === view.mediaField );
const previewField = fields.find( ( f ) => f.id === view.mediaField );
const descriptionField = fields.find(
( f ) => f.id === view.descriptionField
);

const previewFields = fields.filter( ( f ) => f.type === 'media' );

let previewFieldUI;
if ( previewFields.length > 1 ) {
const isPreviewFieldVisible =
isDefined( previewField ) && ( view.showMedia ?? true );
previewFieldUI = isDefined( previewField ) && (
<FieldItem
key={ previewField.id }
field={ previewField }
label={ __( 'Preview' ) }
description={ previewField.label }
isVisible={ isPreviewFieldVisible }
onToggleVisibility={ () => {
onChangeView( {
...view,
showMedia: ! isPreviewFieldVisible,
} );
} }
canMove={ false }
previewOptions={ previewFields.map( ( field ) => ( {
label: field.label,
id: field.id,
} ) ) }
onChangePreviewOption={ ( newPreviewId ) =>
onChangeView( { ...view, mediaField: newPreviewId } )
}
/>
);
}
const lockedFields = [
{
field: titleField,
isVisibleFlag: 'showTitle',
},
{
field: mediaField,
field: previewField,
isVisibleFlag: 'showMedia',
ui: previewFieldUI,
},
{
field: descriptionField,
Expand All @@ -493,12 +620,20 @@ function FieldControl() {
( { field, isVisibleFlag } ) =>
// @ts-expect-error
isDefined( field ) && ( view[ isVisibleFlag ] ?? true )
) as Array< { field: NormalizedField< any >; isVisibleFlag: string } >;
) as Array< {
field: NormalizedField< any >;
isVisibleFlag: string;
ui?: ReactNode;
} >;
const hiddenLockedFields = lockedFields.filter(
( { field, isVisibleFlag } ) =>
// @ts-expect-error
isDefined( field ) && ! ( view[ isVisibleFlag ] ?? true )
) as Array< { field: NormalizedField< any >; isVisibleFlag: string } >;
) as Array< {
field: NormalizedField< any >;
isVisibleFlag: string;
ui?: ReactNode;
} >;

return (
<VStack className="dataviews-field-control" spacing={ 6 }>
Expand All @@ -507,20 +642,22 @@ function FieldControl() {
!! visibleFields?.length ) && (
<ItemGroup isBordered isSeparated>
{ visibleLockedFields.map(
( { field, isVisibleFlag } ) => {
( { field, isVisibleFlag, ui } ) => {
return (
<FieldItem
key={ field.id }
field={ field }
isVisible
onToggleVisibility={ () => {
onChangeView( {
...view,
[ isVisibleFlag ]: false,
} );
} }
canMove={ false }
/>
ui ?? (
<FieldItem
key={ field.id }
field={ field }
isVisible
onToggleVisibility={ () => {
onChangeView( {
...view,
[ isVisibleFlag ]: false,
} );
} }
canMove={ false }
/>
)
);
}
) }
Expand Down Expand Up @@ -550,20 +687,23 @@ function FieldControl() {
<ItemGroup isBordered isSeparated>
{ hiddenLockedFields.length > 0 &&
hiddenLockedFields.map(
( { field, isVisibleFlag } ) => {
( { field, isVisibleFlag, ui } ) => {
return (
<FieldItem
key={ field.id }
field={ field }
isVisible={ false }
onToggleVisibility={ () => {
onChangeView( {
...view,
[ isVisibleFlag ]: true,
} );
} }
canMove={ false }
/>
ui ?? (
<FieldItem
key={ field.id }
field={ field }
isVisible={ false }
onToggleVisibility={ () => {
onChangeView( {
...view,
[ isVisibleFlag ]:
true,
} );
} }
canMove={ false }
/>
)
);
}
) }
Expand Down
17 changes: 15 additions & 2 deletions packages/dataviews/src/components/dataviews-view-config/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
}

.dataviews-field-control__field:hover,
.dataviews-field-control__field:focus-within {
.dataviews-field-control__field:focus-within,
.dataviews-field-control__field.is-interacting {
.dataviews-field-control__actions {
position: unset;
top: unset;
Expand All @@ -80,6 +81,18 @@
width: $icon-size;
}

.dataviews-field-control__label {
.dataviews-field-control__label-sub-label-container {
flex-grow: 1;
}

.dataviews-field-control__label {
display: block;
}

.dataviews-field-control__sub-label {
margin-top: $grid-unit-10;
margin-bottom: 0;
font-size: 11px;
font-style: normal;
color: $gray-700;
}
2 changes: 1 addition & 1 deletion packages/dataviews/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export type Operator =
| 'isAll'
| 'isNotAll';

export type FieldType = 'text' | 'integer' | 'datetime';
export type FieldType = 'text' | 'integer' | 'datetime' | 'media';

export type ValidationContext = {
elements?: Option[];
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ _Parameters_
- _props.post_ `[Object]`: The post object to edit. This is required.
- _props.\_\_unstableTemplate_ `[Object]`: The template object wrapper the edited post. This is optional and can only be used when the post type supports templates (like posts and pages).
- _props.settings_ `[Object]`: The settings object to use for the editor. This is optional and can be used to override the default settings.
- _props.children_ `[Element]`: Children elements for which the BlockEditorProvider context should apply. This is optional.
- _props.children_ `[React.ReactNode]`: Children elements for which the BlockEditorProvider context should apply. This is optional.

_Returns_

Expand Down
Loading

1 comment on commit dd70c03

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flaky tests detected in dd70c03.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/12653577882
📝 Reported issues:

Please sign in to comment.