( {
+ data,
+ field,
+ onChange,
+}: FieldLayoutProps< Item > ) {
+ // Use internal state instead of a ref to make sure that the component
+ // re-renders when the popover's anchor updates.
+ const [ popoverAnchor, setPopoverAnchor ] = useState< HTMLElement | null >(
+ null
+ );
+ // Memoize popoverProps to avoid returning a new object every time.
+ const popoverProps = useMemo(
+ () => ( {
+ anchor: popoverAnchor,
+ placement: 'left-start' as const,
+ offset: 36,
+ shift: true,
+ } ),
+ [ popoverAnchor ]
+ );
+
+ const { fieldDefinition, fieldLabel, summaryFields } =
+ useFieldFromFormField( field );
+
+ if ( ! fieldDefinition || ! fieldDefinition.Edit ) {
+ return null;
+ }
+
+ return (
+
+ (
+
+ ) }
+ renderContent={ ( { onClose } ) => (
+
+ ) }
+ />
+
+ );
+}
+
+function MenuGroupContent< Item >( {
+ data,
+ fieldDefinition,
+ onChange,
+ onClose,
+}: {
+ data: Item;
+ fieldDefinition: NormalizedField< Item >;
+ onChange: FieldLayoutProps< Item >[ 'onChange' ];
+ onClose: () => void;
+} ) {
+ const wrappedOnChange = useCallback(
+ ( value: any ) => {
+ onChange( value );
+ onClose();
+ },
+ [ onChange, onClose ]
+ );
+
+ const contextValue = useMemo( () => ( { onClose } ), [ onClose ] );
+
+ // We know Edit is non-null because PanelMenu checks before rendering.
+ const EditComponent = fieldDefinition.Edit!;
+
+ return (
+
+
+
+ );
+}
+
+export default PanelMenu;
diff --git a/packages/dataviews/src/components/dataform-layouts/panel/style.scss b/packages/dataviews/src/components/dataform-layouts/panel/style.scss
index ea3e8466a268a1..2d1566c9af47ac 100644
--- a/packages/dataviews/src/components/dataform-layouts/panel/style.scss
+++ b/packages/dataviews/src/components/dataform-layouts/panel/style.scss
@@ -159,6 +159,11 @@
margin-top: $grid-unit-20;
}
+.dataforms-layouts-panel__field-dropdown--menu .components-popover__content {
+ padding: 0;
+ min-width: 200px;
+}
+
.components-popover.components-dropdown__content.dataforms-layouts-panel__field-dropdown {
z-index: z-index(".components-popover.components-dropdown__content.dataforms-layouts-panel__field-dropdown");
}
diff --git a/packages/dataviews/src/components/dataform-layouts/panel/summary-button.tsx b/packages/dataviews/src/components/dataform-layouts/panel/summary-button.tsx
index 64eb207dc3e660..477ed2e3d2c6c3 100644
--- a/packages/dataviews/src/components/dataform-layouts/panel/summary-button.tsx
+++ b/packages/dataviews/src/components/dataform-layouts/panel/summary-button.tsx
@@ -34,6 +34,7 @@ export default function SummaryButton< Item >( {
disabled,
onClick,
'aria-expanded': ariaExpanded,
+ 'aria-haspopup': ariaHasPopup = 'dialog',
}: {
data: Item;
field: NormalizedFormField;
@@ -44,6 +45,7 @@ export default function SummaryButton< Item >( {
disabled?: boolean;
onClick: () => void;
'aria-expanded'?: boolean;
+ 'aria-haspopup'?: 'dialog' | 'menu';
} ) {
const labelPosition = ( field.layout as NormalizedPanelLayout )
.labelPosition;
@@ -127,7 +129,7 @@ export default function SummaryButton< Item >( {
className="dataforms-layouts-panel__field-trigger-icon"
aria-label={ ariaLabel }
aria-expanded={ ariaExpanded }
- aria-haspopup="dialog"
+ aria-haspopup={ ariaHasPopup }
aria-describedby={ `${ controlId }` }
onClick={ onClick }
>
diff --git a/packages/dataviews/src/dataform/stories/index.story.tsx b/packages/dataviews/src/dataform/stories/index.story.tsx
index c6cf2e068b097f..e440efeee0690b 100644
--- a/packages/dataviews/src/dataform/stories/index.story.tsx
+++ b/packages/dataviews/src/dataform/stories/index.story.tsx
@@ -60,7 +60,7 @@ export const LayoutPanel = {
openAs: {
control: { type: 'select' },
description: 'Chooses how to open the panel.',
- options: [ 'default', 'dropdown', 'modal' ],
+ options: [ 'default', 'dropdown', 'modal', 'menu' ],
},
},
};
diff --git a/packages/dataviews/src/dataform/stories/layout-panel.tsx b/packages/dataviews/src/dataform/stories/layout-panel.tsx
index 9f13077d6fb5be..a2142d6e3cf14c 100644
--- a/packages/dataviews/src/dataform/stories/layout-panel.tsx
+++ b/packages/dataviews/src/dataform/stories/layout-panel.tsx
@@ -264,7 +264,7 @@ const getPanelLayoutFromStoryArgs = ( {
}: {
summary?: string[];
labelPosition?: 'default' | 'top' | 'side' | 'none';
- openAs?: 'default' | 'dropdown' | 'modal';
+ openAs?: 'default' | 'dropdown' | 'modal' | 'menu';
} ): Layout | undefined => {
const panelLayout: PanelLayout = {
type: 'panel',
@@ -291,7 +291,7 @@ const LayoutPanelComponent = ( {
}: {
type: 'default' | 'regular' | 'panel' | 'card';
labelPosition: 'default' | 'top' | 'side' | 'none';
- openAs: 'default' | 'dropdown' | 'modal';
+ openAs: 'default' | 'dropdown' | 'modal' | 'menu';
} ) => {
const [ post, setPost ] = useState< SamplePost >( {
title: 'Hello, World!',
diff --git a/packages/dataviews/src/index.ts b/packages/dataviews/src/index.ts
index 6e10b72674b0b2..db6a89c1e69a54 100644
--- a/packages/dataviews/src/index.ts
+++ b/packages/dataviews/src/index.ts
@@ -3,5 +3,6 @@ export { default as DataViewsPicker } from './dataviews-picker';
export { default as DataForm } from './dataform';
export { default as filterSortAndPaginate } from './utils/filter-sort-and-paginate';
export { useFormValidity } from './hooks';
+export { usePanelMenuOnClose } from './components/dataform-layouts/panel/context';
export { VIEW_LAYOUTS } from './components/dataviews-layouts';
export type * from './types';
diff --git a/packages/dataviews/src/types/dataform.ts b/packages/dataviews/src/types/dataform.ts
index db568f5e432fad..3f096fee50c198 100644
--- a/packages/dataviews/src/types/dataform.ts
+++ b/packages/dataviews/src/types/dataform.ts
@@ -24,13 +24,13 @@ export type NormalizedRegularLayout = {
export type PanelLayout = {
type: 'panel';
labelPosition?: LabelPosition;
- openAs?: 'dropdown' | 'modal';
+ openAs?: 'dropdown' | 'modal' | 'menu';
summary?: PanelSummaryField;
};
export type NormalizedPanelLayout = {
type: 'panel';
labelPosition: LabelPosition;
- openAs: 'dropdown' | 'modal';
+ openAs: 'dropdown' | 'modal' | 'menu';
summary: NormalizedPanelSummaryField;
};
diff --git a/packages/edit-site/src/components/post-list/quick-edit-modal.js b/packages/edit-site/src/components/post-list/quick-edit-modal.js
index bf4184581bcbb8..aab017fb286c50 100644
--- a/packages/edit-site/src/components/post-list/quick-edit-modal.js
+++ b/packages/edit-site/src/components/post-list/quick-edit-modal.js
@@ -100,8 +100,8 @@ export function QuickEditModal( { postType, postId, closeModal } ) {
label: __( 'Template' ),
id: 'template',
layout: {
- type: 'regular',
- labelPosition: 'side',
+ type: 'panel',
+ openAs: 'menu',
},
},
];
diff --git a/packages/fields/src/fields/template/index.ts b/packages/fields/src/fields/template/index.ts
index dd4cbb28f94382..db6b7616015dd7 100644
--- a/packages/fields/src/fields/template/index.ts
+++ b/packages/fields/src/fields/template/index.ts
@@ -9,11 +9,13 @@ import type { Field } from '@wordpress/dataviews';
import { __ } from '@wordpress/i18n';
import type { BasePost } from '../../types';
import { TemplateEdit } from './template-edit';
+import { TemplateView } from './template-view';
const templateField: Field< BasePost > = {
id: 'template',
type: 'text',
label: __( 'Template' ),
+ render: TemplateView,
Edit: TemplateEdit,
enableSorting: false,
filterBy: false,
diff --git a/packages/fields/src/fields/template/template-edit.tsx b/packages/fields/src/fields/template/template-edit.tsx
index eef5b0734c9977..44707b3abee130 100644
--- a/packages/fields/src/fields/template/template-edit.tsx
+++ b/packages/fields/src/fields/template/template-edit.tsx
@@ -7,26 +7,21 @@ import { parse } from '@wordpress/blocks';
import type { WpTemplate } from '@wordpress/core-data';
import { store as coreStore } from '@wordpress/core-data';
import type { DataFormControlProps } from '@wordpress/dataviews';
+import { usePanelMenuOnClose } from '@wordpress/dataviews';
/**
* Internal dependencies
*/
-// @ts-expect-error block-editor is not typed correctly.
-import { __experimentalBlockPatternsList as BlockPatternsList } from '@wordpress/block-editor';
-import {
- Button,
- Dropdown,
- MenuGroup,
- MenuItem,
- Modal,
-} from '@wordpress/components';
+import { __experimentalBlockPatternsList } from '@wordpress/block-editor';
+import { MenuGroup, MenuItem, Modal } from '@wordpress/components';
import { useAsyncList } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
import { decodeEntities } from '@wordpress/html-entities';
import { __ } from '@wordpress/i18n';
-import { getItemTitle } from '../../actions/utils';
-import type { BasePost } from '../../types';
import { unlock } from '../../lock-unlock';
+import type { BasePost } from '../../types';
+
+const BlockPatternsList = __experimentalBlockPatternsList as any;
const EMPTY_ARRAY: [] = [];
@@ -39,9 +34,8 @@ export const TemplateEdit = ( {
const postType = data.type;
const postId =
typeof data.id === 'number' ? data.id : parseInt( data.id, 10 );
- const slug = data.slug;
- const { canSwitchTemplate, templates } = useSelect(
+ const { templates, canSwitchTemplate } = useSelect(
( select ) => {
const allTemplates =
select( coreStore ).getEntityRecords< WpTemplate >(
@@ -94,47 +88,9 @@ export const TemplateEdit = ( {
const shownTemplates = useAsyncList( templatesAsPatterns );
const value = field.getValue( { item: data } );
- const foundTemplate = templates.find(
- ( template ) => template.slug === value
- );
-
- const currentTemplate = useSelect(
- ( select ) => {
- if ( foundTemplate ) {
- return foundTemplate;
- }
-
- let slugToCheck;
- // In `draft` status we might not have a slug available, so we use the `single`
- // post type templates slug(ex page, single-post, single-product etc..).
- // Pages do not need the `single` prefix in the slug to be prioritized
- // through template hierarchy.
- if ( slug ) {
- slugToCheck =
- postType === 'page'
- ? `${ postType }-${ slug }`
- : `single-${ postType }-${ slug }`;
- } else {
- slugToCheck =
- postType === 'page' ? 'page' : `single-${ postType }`;
- }
-
- if ( postType ) {
- const templateId = select( coreStore ).getDefaultTemplateId( {
- slug: slugToCheck,
- } );
-
- return select( coreStore ).getEntityRecord(
- 'postType',
- 'wp_template',
- templateId
- );
- }
- },
- [ foundTemplate, postType, slug ]
- );
const [ showModal, setShowModal ] = useState( false );
+ const onClosePanelMenu = usePanelMenuOnClose();
const onChangeControl = useCallback(
( newValue: string ) =>
@@ -145,47 +101,29 @@ export const TemplateEdit = ( {
);
return (
-
+ >
);
};
diff --git a/packages/fields/src/fields/template/template-view.tsx b/packages/fields/src/fields/template/template-view.tsx
new file mode 100644
index 00000000000000..4e8ee6a3d3aa6f
--- /dev/null
+++ b/packages/fields/src/fields/template/template-view.tsx
@@ -0,0 +1,74 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect } from '@wordpress/data';
+import { store as coreStore } from '@wordpress/core-data';
+import type { WpTemplate } from '@wordpress/core-data';
+import type { DataViewRenderFieldProps } from '@wordpress/dataviews';
+import { decodeEntities } from '@wordpress/html-entities';
+
+/**
+ * Internal dependencies
+ */
+import type { BasePost } from '../../types';
+
+export const TemplateView = ( {
+ item,
+}: DataViewRenderFieldProps< BasePost > ) => {
+ const template = item.template;
+ const postType = item.type;
+ const slug = item.slug;
+
+ const templateTitle = useSelect(
+ ( select ) => {
+ const { getEntityRecords, getDefaultTemplateId, getEntityRecord } =
+ select( coreStore );
+
+ // If a specific template slug is set, find it directly.
+ if ( template ) {
+ const templates = getEntityRecords< WpTemplate >(
+ 'postType',
+ 'wp_template',
+ { per_page: -1, post_type: postType }
+ );
+ const found = templates?.find( ( t ) => t.slug === template );
+ if ( found ) {
+ return decodeEntities( found.title.rendered );
+ }
+ }
+
+ // Resolve the default template for this post type.
+ let slugToCheck;
+ if ( slug ) {
+ slugToCheck =
+ postType === 'page'
+ ? `${ postType }-${ slug }`
+ : `single-${ postType }-${ slug }`;
+ } else {
+ slugToCheck =
+ postType === 'page' ? 'page' : `single-${ postType }`;
+ }
+
+ if ( postType ) {
+ const templateId = getDefaultTemplateId( {
+ slug: slugToCheck,
+ } );
+ if ( templateId ) {
+ const defaultTemplate = getEntityRecord< WpTemplate >(
+ 'postType',
+ 'wp_template',
+ templateId
+ );
+ if ( defaultTemplate ) {
+ return decodeEntities( defaultTemplate.title.rendered );
+ }
+ }
+ }
+
+ return '';
+ },
+ [ template, postType, slug ]
+ );
+
+ return <>{ templateTitle }>;
+};