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 (WordPress#67278)

Co-authored-by: jorgefilipecosta <[email protected]>
Co-authored-by: ntsekouras <[email protected]>
Co-authored-by: youknowriad <[email protected]>
Co-authored-by: oandregal <[email protected]>
Co-authored-by: jameskoster <[email protected]>
Co-authored-by: jasmussen <[email protected]>
  • 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

0 comments on commit dd70c03

Please sign in to comment.