Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Categories List block: Add dropdown for taxonomies #65272

Merged
merged 24 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
6 changes: 3 additions & 3 deletions docs/reference-guides/core-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ A calendar of your site’s posts. ([Source](https://github.com/WordPress/gutenb
- **Supports:** align, color (background, link, text), interactivity (clientNavigation), typography (fontSize, lineHeight)
- **Attributes:** month, year

## Categories List
## Terms List

Display a list of all categories. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/categories))
Display a list of all terms of a given taxonomy. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/categories))

- **Name:** core/categories
- **Category:** widgets
- **Supports:** align, interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~
- **Attributes:** displayAsDropdown, label, showEmpty, showHierarchy, showLabel, showOnlyTopLevel, showPostCounts
- **Attributes:** displayAsDropdown, label, showEmpty, showHierarchy, showLabel, showOnlyTopLevel, showPostCounts, taxonomy

## Code

Expand Down
9 changes: 7 additions & 2 deletions packages/block-library/src/categories/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "core/categories",
"title": "Categories List",
"title": "Terms List",
Copy link
Member

Choose a reason for hiding this comment

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

Given that we are changing the name of the block here I would recommend we at least add Categories to the keywords array so it still appears in search.


My personal recommendation however would be that for backwards compatibility we register one singular block variation for the category taxonomy. This way we are not renaming an existing block. Users still get the same result. But they now have an additional control to change the block to query other taxonomies.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Given that we are changing the name of the block here I would recommend we at least add Categories to the keywords array so it still appears in search.

Good point 👍

My personal recommendation however would be that for backwards compatibility we register one singular block variation for the category taxonomy. This way we are not renaming an existing block. Users still get the same result. But they now have an additional control to change the block to query other taxonomies.

Interesting. I gave this a quick spin (see below diff). I'm assuming you mean with scope set to exclude transform, so that there won't be the dropdown to transform variations (as there is in #64805) as it'd be otherwise confusing to have two dropdowns to control which variation to display.

This posed the following problem: For back-compat, we need to keep category as the default value for the taxonomy attribute, so that any existing occurrences of <!-- wp:categories /--> will continue to be rendered as the Categories List variation.

But that raises the question of the default value for that attribute when inserting a new instance of the generic Terms List variation. It cannot be omitted, as that would translate to category (per the above), causing the block to be promptly rendered as the Categories List variation.

So we need a different default value for the Terms List variation. I guess Tags -- the only other built-in and public taxonomy -- is the logical contender here, so this is what I chose.

Furthermore, there's the question of the dropdown. IMO, it needs to be hidden altogether in case of the Categories List variation, as it would otherwise allow to transform it to the Terms List variation. In the same vein, the "Categories" option needs to be removed from the dropdown in case of the Terms List variation, to prevent transforming into the Categories List variation.

But that means that the Terms List block is now solely for Tags and custom taxonomies, whereas Categories get their own variation (which looks like a separate block, for all intents and purposed). This feels a bit arbitrary IMO; I'm not sure it's that much better in terms of UX than not having the separate Categories List block variation.

Diff
diff --git a/packages/block-library/src/categories/edit.js b/packages/block-library/src/categories/edit.js
index 866feb884c8..bafb2a3273a 100644
--- a/packages/block-library/src/categories/edit.js
+++ b/packages/block-library/src/categories/edit.js
@@ -46,7 +46,11 @@ export default function CategoriesEdit( {
 		'taxonomy'
 	);
 
-	const taxonomies = allTaxonomies?.filter( ( t ) => t.visibility.public );
+	const taxonomies = allTaxonomies?.filter(
+		( t ) =>
+			t.visibility.public &&
+			( taxonomySlug === 'category' ) === ( t.slug === 'category' )
+	);
 
 	const taxonomy = taxonomies?.find( ( t ) => t.slug === taxonomySlug );
 
@@ -185,21 +189,24 @@ export default function CategoriesEdit( {
 		<TagName { ...blockProps }>
 			<InspectorControls>
 				<PanelBody title={ __( 'Settings' ) }>
-					{ Array.isArray( taxonomies ) && (
-						<SelectControl
-							__nextHasNoMarginBottom
-							__next40pxDefaultSize
-							label={ __( 'Taxonomy' ) }
-							options={ taxonomies.map( ( t ) => ( {
-								label: t.name,
-								value: t.slug,
-							} ) ) }
-							value={ taxonomySlug }
-							onChange={ ( selectedTaxonomy ) =>
-								setAttributes( { taxonomy: selectedTaxonomy } )
-							}
-						/>
-					) }
+					{ taxonomySlug !== 'category' && // For back-compat, the Category taxonomy has its own block variation.
+						Array.isArray( taxonomies ) && (
+							<SelectControl
+								__nextHasNoMarginBottom
+								__next40pxDefaultSize
+								label={ __( 'Taxonomy' ) }
+								options={ taxonomies.map( ( t ) => ( {
+									label: t.name,
+									value: t.slug,
+								} ) ) }
+								value={ taxonomySlug }
+								onChange={ ( selectedTaxonomy ) =>
+									setAttributes( {
+										taxonomy: selectedTaxonomy,
+									} )
+								}
+							/>
+						) }
 					<ToggleControl
 						__nextHasNoMarginBottom
 						label={ __( 'Display as dropdown' ) }
diff --git a/packages/block-library/src/categories/index.js b/packages/block-library/src/categories/index.js
index 8cdcad45086..d30c55667d1 100644
--- a/packages/block-library/src/categories/index.js
+++ b/packages/block-library/src/categories/index.js
@@ -9,6 +9,7 @@ import { category as icon } from '@wordpress/icons';
 import initBlock from '../utils/init-block';
 import metadata from './block.json';
 import edit from './edit';
+import variations from './variations';
 
 const { name } = metadata;
 
@@ -18,6 +19,7 @@ export const settings = {
 	icon,
 	example: {},
 	edit,
+	variations,
 };
 
 export const init = () => initBlock( { name, metadata, settings } );
diff --git a/packages/block-library/src/categories/variations.js b/packages/block-library/src/categories/variations.js
new file mode 100644
index 00000000000..06003ec6420
--- /dev/null
+++ b/packages/block-library/src/categories/variations.js
@@ -0,0 +1,36 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { category as icon } from '@wordpress/icons';
+
+const variations = [
+	{
+		name: 'terms',
+		title: __( 'Terms List' ),
+		icon,
+		attributes: {
+			// We need to set an attribute here that will be set when inserting the block.
+			// We cannot leave this empty, as that would be interpreted as the default value,
+			// which is `category` -- for which we're defining a distinct variation below,
+			// for backwards compatibility reasons.
+			// The logical fallback is thus the only other built-in and public taxonomy: Tags.
+			taxonomy: 'post_tag',
+		},
+		isActive: ( blockAttributes ) =>
+			// This variation is used for any taxonomy other than `category`.
+			blockAttributes.taxonomy !== 'category',
+	},
+	{
+		name: 'categories',
+		title: __( 'Categories List' ),
+		description: __( 'Display a list of all categories.' ),
+		icon,
+		attributes: {
+			taxonomy: 'category',
+		},
+		isActive: [ 'taxonomy' ],
+	},
+];
+
+export default variations;

separate-categories-variation

Copy link
Member

Choose a reason for hiding this comment

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

Looking at the diff I would not remove the category option from the dropdown of available taxonomies. If a user happens to insert the "Term List" block and then selects the "Category" taxonomy in the drop-down I'd be fine with that changing the name of the block to the correct variation.

Copy link
Member

Choose a reason for hiding this comment

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

At the same time I would be fine with users switching from the Category List to another term :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TBH, that would feel inconsistent to me in terms of UX; I think it would be somewhat surprising to the user if selecting "Categories" as the taxonomy from that dropdown gets special treatment, while all other taxonomies don't 😕

dropdown-includes everything

If it's not too much of a dealbreaker for you, I'd rather consider the idea of a dedicated variation for the Categories List block separately -- i.e. in a follow-up PR, where it can also get the attention by designers that it deserves, without conflating it with the principal subject of this PR of adding the taxonomy attribute and dropdown in the first place 😊

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you!

I've added "keywords": [ "categories" ] in 95a6cf3.

Copy link
Member

Choose a reason for hiding this comment

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

It looks like block variations were discussed extensively in:

One drawback is that users won't see Categories in the inserters when typing categories term. The icon remains the same, so maybe that isn't that much of an issue, as with the keywords, it will remain discoverable. Although, we should follow the feedback closely during beta and RC phase of the release.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a fair point and one that I hadn't considered; I've shared some more thoughts below.

I'll add one more note for posterity: With the suggested change we've been discussing in this thread -- i.e. adding two block variations, one for Categories specifically, and a "generic" one for all other cases -- there can also be some confusion in the inserter: I've called the "generic" variation "Terms List", just like the block itself; but since both the block itself and all its variations are shown in the inserter, there would be two "Terms List" blocks:

image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've filed a draft PR for this regardless, in case we'd like to revisit this: #65434

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll add one more note for posterity: With the suggested change we've been discussing in this thread -- i.e. adding two block variations, one for Categories specifically, and a "generic" one for all other cases -- there can also be some confusion in the inserter: I've called the "generic" variation "Terms List", just like the block itself; but since both the block itself and all its variations are shown in the inserter, there would be two "Terms List" blocks:

image

Turns out I was wrong about this one: #65434 (comment) 😅

"category": "widgets",
"description": "Display a list of all categories.",
"description": "Display a list of all terms of a given taxonomy.",
"keywords": [ "categories" ],
"textdomain": "default",
"attributes": {
"taxonomy": {
"type": "string",
"default": "category"
},
"displayAsDropdown": {
"type": "boolean",
"default": false
Expand Down
91 changes: 66 additions & 25 deletions packages/block-library/src/categories/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import clsx from 'clsx';
import {
PanelBody,
Placeholder,
SelectControl,
Spinner,
ToggleControl,
VisuallyHidden,
Expand All @@ -20,7 +21,7 @@ import {
RichText,
} from '@wordpress/block-editor';
import { decodeEntities } from '@wordpress/html-entities';
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { pin } from '@wordpress/icons';
import { useEntityRecords } from '@wordpress/core-data';

Expand All @@ -33,19 +34,33 @@ export default function CategoriesEdit( {
showEmpty,
label,
showLabel,
taxonomy: taxonomySlug,
},
setAttributes,
className,
} ) {
const selectId = useInstanceId( CategoriesEdit, 'blocks-category-select' );

const { records: allTaxonomies, isResolvingTaxonomies } = useEntityRecords(
'root',
'taxonomy'
);

const taxonomies = allTaxonomies?.filter( ( t ) => t.visibility.public );

const taxonomy = taxonomies?.find( ( t ) => t.slug === taxonomySlug );

const isHierarchicalTaxonomy =
! isResolvingTaxonomies && taxonomy?.hierarchical;

const query = { per_page: -1, hide_empty: ! showEmpty, context: 'view' };
if ( showOnlyTopLevel ) {
if ( isHierarchicalTaxonomy && showOnlyTopLevel ) {
query.parent = 0;
}

const { records: categories, isResolving } = useEntityRecords(
'taxonomy',
'category',
taxonomySlug,
query
);

Expand All @@ -66,7 +81,7 @@ export default function CategoriesEdit( {
! name ? __( '(Untitled)' ) : decodeEntities( name ).trim();

const renderCategoryList = () => {
const parentId = showHierarchy ? 0 : null;
const parentId = isHierarchicalTaxonomy && showHierarchy ? 0 : null;
const categoriesList = getCategoriesList( parentId );
return categoriesList.map( ( category ) =>
renderCategoryListItem( category )
Expand All @@ -82,27 +97,29 @@ export default function CategoriesEdit( {
{ renderCategoryName( name ) }
</a>
{ showPostCounts && ` (${ count })` }
{ showHierarchy && !! childCategories.length && (
<ul className="children">
{ childCategories.map( ( childCategory ) =>
renderCategoryListItem( childCategory )
) }
</ul>
) }
{ isHierarchicalTaxonomy &&
showHierarchy &&
!! childCategories.length && (
<ul className="children">
{ childCategories.map( ( childCategory ) =>
renderCategoryListItem( childCategory )
) }
</ul>
) }
</li>
);
};

const renderCategoryDropdown = () => {
const parentId = showHierarchy ? 0 : null;
const parentId = isHierarchicalTaxonomy && showHierarchy ? 0 : null;
const categoriesList = getCategoriesList( parentId );
return (
<>
{ showLabel ? (
<RichText
className="wp-block-categories__label"
aria-label={ __( 'Label text' ) }
placeholder={ __( 'Categories' ) }
placeholder={ taxonomy.name }
withoutInteractiveFormatting
value={ label }
onChange={ ( html ) =>
Expand All @@ -111,11 +128,17 @@ export default function CategoriesEdit( {
/>
) : (
<VisuallyHidden as="label" htmlFor={ selectId }>
{ label ? label : __( 'Categories' ) }
{ label ? label : taxonomy.name }
</VisuallyHidden>
) }
<select id={ selectId }>
<option>{ __( 'Select Category' ) }</option>
<option>
{ sprintf(
/* translators: %s: taxonomy's singular name */
__( 'Select %s' ),
taxonomy.labels.singular_name
) }
</option>
{ categoriesList.map( ( category ) =>
renderCategoryDropdownItem( category, 0 )
) }
Expand All @@ -133,7 +156,8 @@ export default function CategoriesEdit( {
{ renderCategoryName( name ) }
{ showPostCounts && ` (${ count })` }
</option>,
showHierarchy &&
isHierarchicalTaxonomy &&
showHierarchy &&
!! childCategories.length &&
childCategories.map( ( childCategory ) =>
renderCategoryDropdownItem( childCategory, level + 1 )
Expand Down Expand Up @@ -161,6 +185,21 @@ export default function CategoriesEdit( {
<TagName { ...blockProps }>
<InspectorControls>
<PanelBody title={ __( 'Settings' ) }>
{ Array.isArray( taxonomies ) && (
<SelectControl
__nextHasNoMarginBottom
__next40pxDefaultSize
label={ __( 'Taxonomy' ) }
options={ taxonomies.map( ( t ) => ( {
label: t.name,
value: t.slug,
} ) ) }
value={ taxonomySlug }
onChange={ ( selectedTaxonomy ) =>
setAttributes( { taxonomy: selectedTaxonomy } )
}
/>
) }
<ToggleControl
__nextHasNoMarginBottom
label={ __( 'Display as dropdown' ) }
Expand All @@ -182,19 +221,21 @@ export default function CategoriesEdit( {
checked={ showPostCounts }
onChange={ toggleAttribute( 'showPostCounts' ) }
/>
{ isHierarchicalTaxonomy && (
<ToggleControl
__nextHasNoMarginBottom
label={ __( 'Show only top level terms' ) }
checked={ showOnlyTopLevel }
onChange={ toggleAttribute( 'showOnlyTopLevel' ) }
/>
) }
<ToggleControl
__nextHasNoMarginBottom
label={ __( 'Show only top level categories' ) }
checked={ showOnlyTopLevel }
onChange={ toggleAttribute( 'showOnlyTopLevel' ) }
/>
<ToggleControl
__nextHasNoMarginBottom
label={ __( 'Show empty categories' ) }
label={ __( 'Show empty terms' ) }
checked={ showEmpty }
onChange={ toggleAttribute( 'showEmpty' ) }
/>
{ ! showOnlyTopLevel && (
{ isHierarchicalTaxonomy && ! showOnlyTopLevel && (
<ToggleControl
__nextHasNoMarginBottom
label={ __( 'Show hierarchy' ) }
Expand All @@ -205,7 +246,7 @@ export default function CategoriesEdit( {
</PanelBody>
</InspectorControls>
{ isResolving && (
<Placeholder icon={ pin } label={ __( 'Categories' ) }>
<Placeholder icon={ pin } label={ __( 'Terms' ) }>
<Spinner />
</Placeholder>
) }
Expand Down
28 changes: 19 additions & 9 deletions packages/block-library/src/categories/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ function render_block_core_categories( $attributes, $content, $block ) {
static $block_id = 0;
++$block_id;

$taxonomy = get_taxonomy( $attributes['taxonomy'] );

$args = array(
'echo' => false,
'hierarchical' => ! empty( $attributes['showHierarchy'] ),
'orderby' => 'name',
'show_count' => ! empty( $attributes['showPostCounts'] ),
'taxonomy' => $attributes['taxonomy'],
'title_li' => '',
'hide_empty' => empty( $attributes['showEmpty'] ),
);
Expand All @@ -36,13 +39,20 @@ function render_block_core_categories( $attributes, $content, $block ) {
if ( ! empty( $attributes['displayAsDropdown'] ) ) {
$id = 'wp-block-categories-' . $block_id;
$args['id'] = $id;
$args['show_option_none'] = __( 'Select Category' );
$show_label = empty( $attributes['showLabel'] ) ? ' screen-reader-text' : '';
$default_label = __( 'Categories' );
$label_text = ! empty( $attributes['label'] ) ? $attributes['label'] : $default_label;
$wrapper_markup = '<div %1$s><label class="wp-block-categories__label' . $show_label . '" for="' . esc_attr( $id ) . '">' . $label_text . '</label>%2$s</div>';
$items_markup = wp_dropdown_categories( $args );
$type = 'dropdown';
$args['name'] = $taxonomy->query_var;
$args['value_field'] = 'slug';
$args['show_option_none'] = sprintf(
/* translators: %s: taxonomy's singular name */
__( 'Select %s' ),
$taxonomy->labels->singular_name
);

$show_label = empty( $attributes['showLabel'] ) ? ' screen-reader-text' : '';
$default_label = $taxonomy->label;
$label_text = ! empty( $attributes['label'] ) ? $attributes['label'] : $default_label;
$wrapper_markup = '<div %1$s><label class="wp-block-categories__label' . $show_label . '" for="' . esc_attr( $id ) . '">' . $label_text . '</label>%2$s</div>';
Copy link
Member

Choose a reason for hiding this comment

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

See comment from @peterwilsoncc WordPress/wordpress-develop#7226 (comment)

Escaping label text

It was a preexisting issue, though.

$items_markup = wp_dropdown_categories( $args );
$type = 'dropdown';

if ( ! is_admin() ) {
// Inject the dropdown script immediately after the select dropdown.
Expand Down Expand Up @@ -92,8 +102,8 @@ function build_dropdown_script_block_core_categories( $dropdown_id ) {
( function() {
var dropdown = document.getElementById( '<?php echo esc_js( $dropdown_id ); ?>' );
function onCatChange() {
if ( dropdown.options[ dropdown.selectedIndex ].value > 0 ) {
location.href = "<?php echo esc_url( home_url() ); ?>/?cat=" + dropdown.options[ dropdown.selectedIndex ].value;
if ( dropdown.options[ dropdown.selectedIndex ].value !== -1 ) {
Copy link
Member

Choose a reason for hiding this comment

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

There is an opportunity to refactor this block to use Interactivity API. @DAreRodz and @michalczaplinski, was it on the radar?

location.href = "<?php echo esc_url( home_url() ); ?>/?" + dropdown.name + '=' + dropdown.options[ dropdown.selectedIndex ].value;
}
}
dropdown.onchange = onCatChange;
Expand Down
1 change: 1 addition & 0 deletions test/integration/fixtures/blocks/core__categories.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"name": "core/categories",
"isValid": true,
"attributes": {
"taxonomy": "category",
"displayAsDropdown": false,
"showHierarchy": false,
"showPostCounts": false,
Expand Down
Loading