Product Collection: Redesigned Collection Insertion Selection (#48911)

* Updated Collection Selection Buttons

Rather than using normal buttons we're going to replace these with cards
that we want to use instead.

* Reworked Product Catalog Creation

* Added Dropdown Collection Option

* Changelog

* Added Collection Dashicon Support

* Fixed Collection Change Modal

This is going to get replaced soon but it may as well look nicer than it
does right now.

* Type Fix

* Fixed `:focus` Hover Border

* Simplified Click Handler

* Style Fixes

* Gutenberg Style Fixes

* E2E Fixes

* Fixed E2E Test

* Added Dropdown Inserter E2E Support

* Logging

* Fixed Default Insertion Options

* Prevent Premature Rendering

* E2E Fix Attempt

* Lint Fix

* E2E Fix

* Fix test chaking if custom registred collections are available in the collection chooser

* Improve logic of choosing collection to cover both dropdown and placeholder

---------

Co-authored-by: Karol Manijak <20098064+kmanijak@users.noreply.github.com>
This commit is contained in:
Christopher Allford 2024-07-09 04:53:48 -07:00 committed by GitHub
parent dacee984fa
commit 7d71e2235a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 329 additions and 120 deletions

View File

@ -1,7 +1,7 @@
/**
* External dependencies
*/
import type { InnerBlockTemplate, BlockIcon } from '@wordpress/blocks';
import type { InnerBlockTemplate } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { Icon, chartBar } from '@wordpress/icons';
@ -14,7 +14,7 @@ import { CoreCollectionNames, CoreFilterNames } from '../types';
const collection = {
name: CoreCollectionNames.BEST_SELLERS,
title: __( 'Best Sellers', 'woocommerce' ),
icon: ( <Icon icon={ chartBar } /> ) as BlockIcon,
icon: <Icon icon={ chartBar } />,
description: __( 'Recommend your best-selling products.', 'woocommerce' ),
keywords: [ 'best selling', 'product collection' ],
scope: [],

View File

@ -1,7 +1,7 @@
/**
* External dependencies
*/
import type { InnerBlockTemplate, BlockIcon } from '@wordpress/blocks';
import type { InnerBlockTemplate } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { Icon, starFilled } from '@wordpress/icons';
@ -14,7 +14,7 @@ import { CoreCollectionNames, CoreFilterNames } from '../types';
const collection = {
name: CoreCollectionNames.FEATURED,
title: __( 'Featured', 'woocommerce' ),
icon: ( <Icon icon={ starFilled } /> ) as BlockIcon,
icon: <Icon icon={ starFilled } />,
description: __( 'Showcase your featured products.', 'woocommerce' ),
keywords: [ 'product collection' ],
scope: [],

View File

@ -1,7 +1,7 @@
/**
* External dependencies
*/
import type { InnerBlockTemplate, BlockIcon } from '@wordpress/blocks';
import type { InnerBlockTemplate } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { Icon, calendar } from '@wordpress/icons';
@ -18,7 +18,7 @@ import {
const collection = {
name: CoreCollectionNames.NEW_ARRIVALS,
title: __( 'New Arrivals', 'woocommerce' ),
icon: ( <Icon icon={ calendar } /> ) as BlockIcon,
icon: <Icon icon={ calendar } />,
description: __( 'Recommend your newest products.', 'woocommerce' ),
keywords: [ 'newest products', 'product collection' ],
scope: [],

View File

@ -1,7 +1,7 @@
/**
* External dependencies
*/
import type { InnerBlockTemplate, BlockIcon } from '@wordpress/blocks';
import type { InnerBlockTemplate } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { Icon, percent } from '@wordpress/icons';
@ -14,7 +14,7 @@ import { CoreCollectionNames, CoreFilterNames } from '../types';
const collection = {
name: CoreCollectionNames.ON_SALE,
title: __( 'On Sale', 'woocommerce' ),
icon: ( <Icon icon={ percent } /> ) as BlockIcon,
icon: <Icon icon={ percent } />,
description: __(
'Highlight products that are currently on sale.',
'woocommerce'

View File

@ -1,7 +1,7 @@
/**
* External dependencies
*/
import type { InnerBlockTemplate, BlockIcon } from '@wordpress/blocks';
import type { InnerBlockTemplate } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { Icon, loop } from '@wordpress/icons';
@ -14,7 +14,7 @@ import { CoreCollectionNames } from '../types';
const collection = {
name: CoreCollectionNames.PRODUCT_CATALOG,
title: __( 'Product Catalog', 'woocommerce' ),
icon: ( <Icon icon={ loop } /> ) as BlockIcon,
icon: <Icon icon={ loop } />,
description:
'Display all products in your catalog. Results can (change to) match the current template, page, or search term.',
keywords: [ 'all products' ],

View File

@ -1,7 +1,7 @@
/**
* External dependencies
*/
import type { InnerBlockTemplate, BlockIcon } from '@wordpress/blocks';
import type { InnerBlockTemplate } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { Icon, starEmpty } from '@wordpress/icons';
@ -14,7 +14,7 @@ import { CoreCollectionNames, CoreFilterNames } from '../types';
const collection = {
name: CoreCollectionNames.TOP_RATED,
title: __( 'Top Rated', 'woocommerce' ),
icon: ( <Icon icon={ starEmpty } /> ) as BlockIcon,
icon: <Icon icon={ starEmpty } />,
description: __(
'Recommend products with the highest review ratings.',
'woocommerce'

View File

@ -1,8 +1,11 @@
/**
* External dependencies
*/
import { useMemo } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { Button } from '@wordpress/components';
import { Button, Dropdown, Icon, Tooltip } from '@wordpress/components';
import { useResizeObserver } from '@wordpress/compose';
import { __ } from '@wordpress/i18n';
import {
BlockInstance,
createBlock,
@ -10,6 +13,8 @@ import {
createBlocksFromInnerBlocksTemplate,
// @ts-expect-error Type definitions for this function are missing in Guteberg
store as blocksStore,
BlockVariation,
BlockIcon,
} from '@wordpress/blocks';
/**
@ -21,13 +26,19 @@ import { getCollectionByName } from '../collections';
import { getDefaultProductCollection } from '../utils';
type CollectionButtonProps = {
active?: boolean;
title: string;
icon: string;
description: string;
icon: BlockIcon | undefined;
description: string | undefined;
onClick: () => void;
};
type CollectionOptionsProps = {
chosenCollection?: CollectionName | undefined;
catalogVariation: BlockVariation;
collectionVariations: BlockVariation[];
onCollectionClick: ( name: string ) => void;
};
export const applyCollection = (
collectionName: CollectionName,
clientId: string,
@ -54,65 +65,165 @@ export const applyCollection = (
};
const CollectionButton = ( {
active = false,
title,
icon,
description,
onClick,
}: CollectionButtonProps ) => {
const variant = active ? 'primary' : 'secondary';
return (
<Button
className="wc-blocks-product-collection__collection-button"
variant={ variant }
onClick={ onClick }
>
<div className="wc-blocks-product-collection__collection-button-icon">
{ icon }
</div>
<div className="wc-blocks-product-collection__collection-button-text">
<Tooltip text={ description } placement="top">
<Button
className="wc-blocks-product-collection__collection-button"
onClick={ onClick }
>
<div className="wc-blocks-product-collection__collection-button-icon">
<Icon icon={ icon as Icon.IconType< BlockIcon > } />
</div>
<p className="wc-blocks-product-collection__collection-button-title">
{ title }
</p>
<p className="wc-blocks-product-collection__collection-button-description">
{ description }
</p>
</div>
</Button>
</Button>
</Tooltip>
);
};
const CollectionChooser = ( props: {
chosenCollection?: CollectionName | undefined;
onCollectionClick: ( name: string ) => void;
} ) => {
const { chosenCollection, onCollectionClick } = props;
// Get Collections
const blockCollections = [
...useSelect( ( select ) => {
// @ts-expect-error Type definitions are missing
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/wordpress__blocks/store/selectors.d.ts
const { getBlockVariations } = select( blocksStore );
return getBlockVariations( blockJson.name );
}, [] ),
];
const CreateCollectionButton = ( props: CollectionButtonProps ) => {
const { description, onClick } = props;
return (
<div className="wc-blocks-product-collection__collections-section">
{ blockCollections.map( ( { name, title, icon, description } ) => (
<CollectionButton
active={ chosenCollection === name }
key={ name }
title={ title }
description={ description }
icon={ icon }
onClick={ () => onCollectionClick( name ) }
/>
) ) }
<div className="wc-blocks-product-collection__collections-create">
<span>{ __( 'or', 'woocommerce' ) }</span>
<Tooltip text={ description } placement="top">
<Button onClick={ onClick }>
{ __( 'create your own', 'woocommerce' ) }
</Button>
</Tooltip>
</div>
);
};
const GridCollectionOptions = ( props: CollectionOptionsProps ) => {
const { onCollectionClick, catalogVariation, collectionVariations } = props;
return (
<div className="wc-blocks-product-collection__collections-grid">
<div className="wc-blocks-product-collection__collections-section">
{ collectionVariations.map(
( { name, title, icon, description } ) => (
<CollectionButton
key={ name }
title={ title }
description={ description }
icon={ icon }
onClick={ () => onCollectionClick( name ) }
/>
)
) }
</div>
<CreateCollectionButton
title={ catalogVariation.title }
description={ catalogVariation.description }
icon={ catalogVariation.icon }
onClick={ () => onCollectionClick( catalogVariation.name ) }
/>
</div>
);
};
const DropdownCollectionOptions = ( props: CollectionOptionsProps ) => {
const { onCollectionClick, catalogVariation, collectionVariations } = props;
return (
<div className="wc-blocks-product-collection__collections-dropdown">
<Dropdown
className="wc-blocks-product-collection__collections-dropdown-toggle"
contentClassName="wc-blocks-product-collection__collections-dropdown-content"
renderToggle={ ( { isOpen, onToggle } ) => (
<Button
variant="secondary"
onClick={ onToggle }
aria-expanded={ isOpen }
>
{ __( 'Choose collection', 'woocommerce' ) }
</Button>
) }
renderContent={ () => (
<>
{ collectionVariations.map(
( { name, title, icon, description } ) => (
<CollectionButton
key={ name }
title={ title }
description={ description }
icon={ icon }
onClick={ () => onCollectionClick( name ) }
/>
)
) }
</>
) }
></Dropdown>
<CreateCollectionButton
title={ catalogVariation.title }
description={ catalogVariation.description }
icon={ catalogVariation.icon }
onClick={ () => onCollectionClick( catalogVariation.name ) }
/>
</div>
);
};
const CollectionChooser = (
props: Pick<
CollectionOptionsProps,
'chosenCollection' | 'onCollectionClick'
>
) => {
// Get Collections
const blockCollections = useSelect( ( select ) => {
// @ts-expect-error Type definitions are missing
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/wordpress__blocks/store/selectors.d.ts
const { getBlockVariations } = select( blocksStore );
return getBlockVariations( blockJson.name );
}, [] ) as BlockVariation[];
const productCatalog = useMemo(
() =>
blockCollections.find(
( { name } ) => name === CoreCollectionNames.PRODUCT_CATALOG
) as BlockVariation,
[ blockCollections ]
);
const collectionVariations = useMemo(
() =>
blockCollections.filter(
( { name } ) => name !== CoreCollectionNames.PRODUCT_CATALOG
) as BlockVariation[],
[ blockCollections ]
);
const [ resizeListener, { width } ] = useResizeObserver();
let OptionsComponent;
if ( width !== null && width >= 600 ) {
OptionsComponent = GridCollectionOptions;
} else {
OptionsComponent = DropdownCollectionOptions;
}
return (
<>
{ resizeListener }
{ !! width && (
<OptionsComponent
{ ...props }
catalogVariation={ productCatalog }
collectionVariations={ collectionVariations }
/>
) }
</>
);
};
export default CollectionChooser;

View File

@ -36,18 +36,12 @@ const PatternSelectionModal = ( props: {
return (
<Modal
overlayClassName="wc-blocks-product-collection__modal"
title={ __( 'Choose a collection', 'woocommerce' ) }
title={ __( 'What products do you want to show?', 'woocommerce' ) }
onRequestClose={ props.closePatternSelectionModal }
// @ts-expect-error Type definitions are missing in the version we are using i.e. 19.1.5,
size={ 'large' }
>
<div className="wc-blocks-product-collection__content">
<p className="wc-blocks-product-collection__subtitle">
{ __(
"Pick what products are shown. Don't worry, you can switch and tweak this collection any time.",
'woocommerce'
) }
</p>
<CollectionChooser
chosenCollection={ chosenCollection }
onCollectionClick={ selectCollectionName }

View File

@ -1,8 +1,6 @@
$max-columns: 3;
$min-button-width: 250px;
$gap-count: calc(#{$max-columns} - 1);
$total-gap-width: calc(#{$gap-count} * #{$gap-small});
$max-button-width: calc((100% - #{$total-gap-width}) / #{$max-columns});
$max-button-columns: 6;
$min-button-width: 80px;
$max-button-width: calc(100% / #{$max-button-columns});
.wc-block-editor-product-collection-inspector-toolspanel__filters {
.wc-block-editor-product-collection-inspector__taxonomy-control:not(:last-child) {
@ -17,11 +15,18 @@ $max-button-width: calc((100% - #{$total-gap-width}) / #{$max-columns});
}
// Need to override high-specificity styles
.wc-blocks-product-collection__placeholder.is-medium {
.wc-blocks-product-collection__placeholder {
.components-placeholder__fieldset {
display: block;
}
&.is-medium,
&.is-small {
.components-button {
width: auto;
}
}
.components-button.wc-blocks-product-collection__collection-button {
margin: 0;
}
@ -29,58 +34,87 @@ $max-button-width: calc((100% - #{$total-gap-width}) / #{$max-columns});
.wc-blocks-product-collection__placeholder,
.wc-blocks-product-collection__modal {
.components-placeholder__instructions {
width: 100%;
margin: $gap-small 0;
text-align: center;
}
.wc-blocks-product-collection__collections-grid {
.wc-blocks-product-collection__collections-create {
width: 100%;
}
}
.wc-blocks-product-collection__collections-dropdown {
display: flex;
justify-content: center;
flex-wrap: wrap;
width: 100%;
}
.wc-blocks-product-collection__selection-subtitle {
margin-bottom: $gap-large;
}
.wc-blocks-product-collection__collections-section {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(max(#{$min-button-width}, #{$max-button-width}), 1fr));
grid-template-columns: repeat(auto-fit, minmax(max(#{$min-button-width}, #{$max-button-width}), 1fr));
grid-auto-rows: 1fr;
grid-gap: $gap-small;
margin: $gap-large auto;
margin: $gap-small auto;
max-width: 1000px;
width: 100%;
}
.wc-blocks-product-collection__collection-button {
color: var(--wp-admin-theme-color);
box-shadow: none;
display: flex;
align-items: flex-start;
flex-wrap: wrap;
align-items: center;
justify-items: center;
height: auto;
border-radius: $universal-border-radius;
box-sizing: border-box;
padding: $gap-smallest $gap-small;
padding: $gap-small;
margin: 0;
&.is-primary {
box-shadow: 0 0 0 2px var(--wp-admin-theme-color, #3858e9);
color: var(--wp-admin-theme-color-darker-20);
background-color: var(--wc-content-bg, #fff);
&:hover {
color: var(--wp-admin-theme-color);
background-color: rgba(var(--wp-admin-theme-color--rgb), 0.04);
&:hover {
background-color: var(--wc-content-bg, #fff);
color: var(--wp-admin-theme-color-darker-20);
&:not(:focus) {
box-shadow: none;
}
}
.wc-blocks-product-collection__collection-button-icon {
margin: 1em 0;
&:focus {
color: var(--wp-admin-theme-color);
background-color: rgba(var(--wp-admin-theme-color--rgb), 0.08);
}
.wc-blocks-product-collection__collection-button-text {
padding: 0 $gap-small;
text-align: left;
white-space: break-spaces;
&-icon {
width: 100%;
}
.wc-blocks-product-collection__collection-button-title {
@include font-size(large);
line-height: 1;
&-title {
@include font-size(small);
width: 100%;
text-wrap: balance;
text-align: center;
margin: 0;
}
}
.wc-blocks-product-collection__collection-button-description {
white-space: wrap;
.wc-blocks-product-collection__collections-create {
padding: 0 $gap-smallest;
text-align: center;
button {
color: var(--wp-admin-theme-color);
padding: 0;
margin-left: $gap-smallest;
&:hover {
color: var(--wp-admin-theme-color-darker-20);
}
}
}
@ -90,6 +124,15 @@ $max-button-width: calc((100% - #{$total-gap-width}) / #{$max-columns});
}
}
.wc-blocks-product-collection__collections-dropdown-content {
.components-popover__content {
width: 20em;
display: flex;
flex: flex-grow;
flex-direction: column;
}
}
// Price Range Filter
.wc-block-product-price-range-control {
input {

View File

@ -19,7 +19,6 @@ import type {
CollectionName,
ProductCollectionEditComponentProps,
} from '../types';
import Icon from '../icon';
const ProductCollectionPlaceholder = (
props: ProductCollectionEditComponentProps
@ -47,10 +46,8 @@ const ProductCollectionPlaceholder = (
<div { ...blockProps }>
<Placeholder
className="wc-blocks-product-collection__placeholder"
icon={ Icon }
label={ __( 'Product Collection', 'woocommerce' ) }
instructions={ __(
"Choose a collection to get started. Don't worry, you can change and tweak this any time.",
'What products do you want to show?',
'woocommerce'
) }
>

View File

@ -1419,9 +1419,15 @@ test.describe( 'Testing registerProductCollection', () => {
pageObject,
editor,
admin,
page,
} ) => {
await admin.createNewPost();
await editor.insertBlockUsingGlobalInserter( pageObject.BLOCK_NAME );
await page
.getByRole( 'button', {
name: 'Choose collection',
} )
.click();
// Get text of all buttons in the collection chooser
const collectionChooserButtonsTexts = await editor.page

View File

@ -60,6 +60,8 @@ export const SELECTORS = {
max: 'MAX',
},
previewButtonTestID: 'product-collection-preview-button',
collectionPlaceholder:
'[data-type="woocommerce/product-collection"] .components-placeholder',
};
export type Collections =
@ -74,18 +76,16 @@ export type Collections =
| 'myCustomCollectionWithAdvancedPreview';
const collectionToButtonNameMap = {
newArrivals: 'New Arrivals Recommend your newest products.',
topRated: 'Top Rated Recommend products with the highest review ratings.',
bestSellers: 'Best Sellers Recommend your best-selling products.',
onSale: 'On Sale Highlight products that are currently on sale.',
featured: 'Featured Showcase your featured products.',
productCatalog:
'Product Catalog Display all products in your catalog. Results can (change to) match the current template, page, or search term.',
myCustomCollection: 'My Custom Collection This is a custom collection.',
myCustomCollectionWithPreview:
'My Custom Collection with Preview This is a custom collection with preview.',
newArrivals: 'New Arrivals',
topRated: 'Top Rated',
bestSellers: 'Best Sellers',
onSale: 'On Sale',
featured: 'Featured',
productCatalog: 'create your own',
myCustomCollection: 'My Custom Collection',
myCustomCollectionWithPreview: 'My Custom Collection with Preview',
myCustomCollectionWithAdvancedPreview:
'My Custom Collection with Advanced Preview This is a custom collection with advanced preview.',
'My Custom Collection with Advanced Preview',
};
class ProductCollectionPage {
@ -121,9 +121,35 @@ class ProductCollectionPage {
? collectionToButtonNameMap[ collection ]
: collectionToButtonNameMap.productCatalog;
await this.admin.page
.getByRole( 'button', { name: buttonName } )
.click();
const placeholderSelector = this.admin.page.locator(
SELECTORS.collectionPlaceholder
);
const chooseCollectionFromPlaceholder = async () => {
await placeholderSelector
.getByRole( 'button', { name: buttonName, exact: true } )
.click();
};
const chooseCollectionFromDropdown = async () => {
await placeholderSelector
.getByRole( 'button', {
name: 'Choose collection',
} )
.click();
await this.admin.page
.locator(
'.wc-blocks-product-collection__collections-dropdown-content'
)
.getByRole( 'button', { name: buttonName, exact: true } )
.click();
};
await Promise.any( [
chooseCollectionFromPlaceholder(),
chooseCollectionFromDropdown(),
] );
}
async chooseCollectionInTemplate( collection?: Collections ) {
@ -131,9 +157,34 @@ class ProductCollectionPage {
? collectionToButtonNameMap[ collection ]
: collectionToButtonNameMap.productCatalog;
await this.editor.canvas
.getByRole( 'button', { name: buttonName } )
.click();
const inserterClass = await this.editor.canvas
.locator( SELECTORS.collectionPlaceholder )
.locator(
'.wc-blocks-product-collection__collections-grid, .wc-blocks-product-collection__collections-dropdown'
)
.getAttribute( 'class' );
const isDropdown = inserterClass?.includes(
'wc-blocks-product-collection__collections-dropdown'
);
if ( isDropdown ) {
await this.editor.canvas
.getByRole( 'button', { name: 'Choose collection' } )
.click();
await this.editor.canvas
.locator(
'.wc-blocks-product-collection__collections-dropdown-content'
)
.getByRole( 'button', { name: buttonName, exact: true } )
.click();
} else {
await this.editor.canvas
.locator( SELECTORS.collectionPlaceholder )
.getByRole( 'button', { name: buttonName, exact: true } )
.click();
}
}
async createNewPostAndInsertBlock( collection?: Collections ) {

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Redesigned the Product Collection block's insertion journey.

View File

@ -299,8 +299,11 @@ test.describe(
// Product Collection requires choosing some collection.
await page
.locator(
'[data-type="woocommerce/product-collection"] .components-placeholder'
)
.getByRole( 'button', {
name: 'Product Catalog Display all products in your catalog. Results can (change to) match the current template, page, or search term.',
name: 'create your own',
} )
.click();