Add Inner Block Rendering and Atomic Block Styles (https://github.com/woocommerce/woocommerce-blocks/pull/2607)

* Register Atomic Blocks and save some block content

* renderInnerBlocks utility

* Frontend Rendering

* Clean up atomic block classnames

* Move shared styles

* Create a hoc for attribute mapping

* Rename some unpluralised class names

* Remove prefixes from atomic component class names

* Updated styles

* Update styles from master

* Revert product list styles

* 2020 fixes

* Separate renderFrontend from renderInnerBlocks

* Lazy loading of components

* Tweak loading classes

* FIx all products loading state

* Revert lazy implementation - creates too many unneccessary files due to webpack config

* Cleanup

* Remove wcBlocksBuildUrl

* Move call to register_atomic_blocks

* Remove duplicate key

* reuse render frontend

* Corectly handle frontend attribute mapping to keep editor working

* Style updates

* Update side effects

* Remove width style from rating to fix alignment

* Move ssr grid styles to main stylesheet

* Put back prefixed classnames

* 2020 styling fixes

* Create frontend files instead of doing it all in block map

* Update assets/js/atomic/utils/get-block-map.js

Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com>

* Update assets/js/atomic/utils/render-parent-block.js

Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com>

* Fix last child alignment regardless of block type

* More specificity fixes

* 2020 button alignment

* static fix to prevent offsets

* fix placeholder image in firefox

* Issues reported in feedback

Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com>
This commit is contained in:
Mike Jolley 2020-06-05 13:18:16 +01:00 committed by GitHub
parent 2e8a3c8d6e
commit da58a8b44f
54 changed files with 1109 additions and 627 deletions

View File

@ -1,3 +1,282 @@
.wc-block-link-button { .wc-block-link-button {
@include link-button(); @include link-button();
} }
// These styles are for the server side rendered product grid blocks.
.wc-block-grid__products .wc-block-grid__product-image {
text-decoration: none;
display: block;
position: relative;
a {
text-decoration: none;
border: 0;
outline: 0;
box-shadow: none;
}
img {
width: 100%;
&[hidden] {
display: none;
}
}
}
.edit-post-visual-editor .editor-block-list__block .wc-block-grid__product-title,
.editor-styles-wrapper .wc-block-grid__product-title,
.wc-block-grid__product-title {
font-family: inherit;
line-height: 1.2em;
font-weight: 700;
padding: 0;
color: inherit;
font-size: inherit;
display: block;
}
.wc-block-grid__product-price {
display: block;
.wc-block-grid__product-price__regular {
margin-right: 0.5em;
}
}
.wc-block-grid__product-add-to-cart {
word-break: break-word;
white-space: normal;
a,
button {
word-break: break-word;
white-space: normal;
margin: 0 auto !important;
display: inline-flex;
justify-content: center;
&.loading {
opacity: 0.25;
}
&::after {
margin-left: 0.5em;
display: inline-block;
}
&.added::after {
font-family: WooCommerce; /* stylelint-disable-line */
content: "\e017";
}
&.loading::after {
font-family: WooCommerce; /* stylelint-disable-line */
content: "\e031";
animation: spin 2s linear infinite;
}
}
}
.wc-block-grid__product-rating {
display: block;
.wc-block-grid__product-rating__stars,
.star-rating {
overflow: hidden;
position: relative;
width: 5.3em;
height: 1.618em;
line-height: 1.618;
font-size: 1em;
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
font-family: star;
font-weight: 400;
margin: 0 auto;
text-align: left;
&::before {
content: "\53\53\53\53\53";
top: 0;
left: 0;
right: 0;
position: absolute;
opacity: 0.5;
color: #aaa;
white-space: nowrap;
}
span {
overflow: hidden;
top: 0;
left: 0;
right: 0;
position: absolute;
padding-top: 1.5em;
}
span::before {
content: "\53\53\53\53\53";
top: 0;
left: 0;
right: 0;
position: absolute;
color: #000;
white-space: nowrap;
}
}
}
.wc-block-grid__product-onsale {
@include font-size(small);
padding: em($gap-smallest) em($gap-small);
display: inline-block;
width: auto;
border: 1px solid #43454b;
border-radius: 3px;
color: #43454b;
background: #fff;
text-align: center;
text-transform: uppercase;
font-weight: 600;
z-index: 9;
position: relative;
}
// Element spacing.
.wc-block-grid__product {
.wc-block-grid__product-image,
.wc-block-grid__product-title {
margin: 0 0 $gap-small;
}
// If centered when toggling alignment on, use auto margins to prevent flexbox stretching it.
.wc-block-grid__product-price,
.wc-block-grid__product-rating,
.wc-block-grid__product-add-to-cart,
.wc-block-grid__product-onsale {
margin: 0 auto $gap-small;
}
}
.theme-twentysixteen {
.wc-block-grid {
// Prevent white theme styles.
.price ins {
color: #77a464;
}
}
}
.theme-twentynineteen {
.wc-block-grid__product {
font-size: 0.88889em;
}
// Change the title font to match headings.
.wc-block-grid__product-title,
.wc-block-grid__product-onsale,
.wc-block-components-product-title,
.wc-block-components-product-sale-badge {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
}
.wc-block-grid__product-title::before {
display: none;
}
.wc-block-grid__product-onsale,
.wc-block-components-product-sale-badge {
line-height: 1;
}
}
.theme-twentytwenty {
$twentytwenty-headings: -apple-system, blinkmacsystemfont, "Helvetica Neue", helvetica, sans-serif;
$twentytwenty-highlights-color: #cd2653;
.wc-block-grid__product-link {
color: #000;
}
.wc-block-grid__product-title,
.wc-block-components-product-title {
font-family: $twentytwenty-headings;
color: #000;
font-size: 1.2em;
}
.wp-block-columns .wc-block-components-product-title {
margin-top: 0;
}
.wc-block-grid__product-price,
.wc-block-components-product-price {
&__value,
.woocommerce-Price-amount {
font-family: $twentytwenty-headings;
font-size: 0.9em;
}
del {
opacity: 0.5;
}
ins {
text-decoration: none;
}
}
.wc-block-grid__product-rating,
.star-rating {
font-size: 0.7em;
.wc-block-grid__product-rating__stars,
.wc-block-components-product-rating__stars {
line-height: 1;
}
}
.wc-block-grid__product-add-to-cart > .wp-block-button__link,
.wc-block-components-product-button > .wp-block-button__link {
font-family: $twentytwenty-headings;
}
.wc-block-grid__products .wc-block-grid__product-onsale,
.wc-block-layout .wc-block-components-product-sale-badge {
background: $twentytwenty-highlights-color;
color: #fff;
font-family: $twentytwenty-headings;
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.2;
text-transform: uppercase;
}
// These styles are not applied to the All Products atomic block, so it can be positioned normally.
.wc-block-grid__products .wc-block-grid__product-onsale {
position: absolute;
right: 4px;
top: 4px;
z-index: 1;
}
// Override style from WC Core that set its position to absolute.
// These rulesets can be removed once https://github.com/woocommerce/woocommerce/pull/26516 is released.
.wc-block-grid__products .wc-block-components-product-sale-badge {
position: static;
}
.wc-block-grid__products .wc-block-grid__product-image .wc-block-components-product-sale-badge {
position: absolute;
}
// These styles are not applied to the All Products atomic block, so it can be positioned normally.
.wc-block-grid__products .wc-block-grid__product-onsale:not(.wc-block-components-product-sale-badge) {
position: absolute;
right: 4px;
top: 4px;
z-index: 1;
}
@media only screen and (min-width: 768px) {
.wc-block-grid__products .wc-block-grid__product-onsale {
@include font-size(small);
padding: em($gap-smaller);
}
}
@media only screen and (min-width: 1168px) {
.wc-block-grid__products .wc-block-grid__product-onsale {
@include font-size(small);
padding: em($gap-smaller);
}
}
}

View File

@ -1,7 +0,0 @@
export { default as ProductButton } from './button/block';
export { default as ProductImage } from './image/block';
export { default as ProductPrice } from './price/block';
export { default as ProductRating } from './rating/block';
export { default as ProductSaleBadge } from './sale-badge/block';
export { default as ProductSummary } from './summary/block';
export { default as ProductTitle } from './title/block';

View File

@ -13,6 +13,11 @@ import {
useProductDataContext, useProductDataContext,
} from '@woocommerce/shared-context'; } from '@woocommerce/shared-context';
/**
* Internal dependencies
*/
import './style.scss';
/** /**
* Product Button Block Component. * Product Button Block Component.
* *
@ -22,22 +27,18 @@ import {
* this is not provided. * this is not provided.
* @return {*} The component. * @return {*} The component.
*/ */
const ProductButton = ( { className, ...props } ) => { const Block = ( { className, ...props } ) => {
const { parentClassName } = useInnerBlockLayoutContext();
const productDataContext = useProductDataContext(); const productDataContext = useProductDataContext();
const product = props.product || productDataContext.product; const product = props.product || productDataContext.product;
const { layoutStyleClassPrefix } = useInnerBlockLayoutContext();
const componentClass = `${ layoutStyleClassPrefix }__product-add-to-cart`;
return ( return (
<div <div
className={ classnames( className={ classnames(
className, className,
componentClass,
'wp-block-button', 'wp-block-button',
{ 'wc-block-components-product-button',
'is-loading': ! product, `${ parentClassName }__product-add-to-cart`
}
) } ) }
> >
{ product ? ( { product ? (
@ -61,12 +62,7 @@ const AddToCartButton = ( { product } ) => {
is_in_stock: isInStock, is_in_stock: isInStock,
} = product; } = product;
const { const { cartQuantity, addingToCart, addToCart } = useStoreAddToCart( id );
cartQuantity,
addingToCart,
cartIsLoading,
addToCart,
} = useStoreAddToCart( id );
useEffect( () => { useEffect( () => {
// Avoid running on first mount when cart quantity is first set. // Avoid running on first mount when cart quantity is first set.
@ -77,10 +73,6 @@ const AddToCartButton = ( { product } ) => {
triggerFragmentRefresh(); triggerFragmentRefresh();
}, [ cartQuantity ] ); }, [ cartQuantity ] );
if ( cartIsLoading ) {
return <AddToCartButtonPlaceholder />;
}
const addedToCart = Number.isFinite( cartQuantity ) && cartQuantity > 0; const addedToCart = Number.isFinite( cartQuantity ) && cartQuantity > 0;
const allowAddToCart = ! hasOptions && isPurchasable && isInStock; const allowAddToCart = ! hasOptions && isPurchasable && isInStock;
const buttonAriaLabel = decodeEntities( const buttonAriaLabel = decodeEntities(
@ -102,42 +94,33 @@ const AddToCartButton = ( { product } ) => {
__( 'Add to cart', 'woo-gutenberg-products-block' ) __( 'Add to cart', 'woo-gutenberg-products-block' )
); );
const ButtonTag = allowAddToCart ? 'button' : 'a';
const buttonProps = {};
if ( ! allowAddToCart ) { if ( ! allowAddToCart ) {
return ( buttonProps.href = permalink;
<a buttonProps.rel = 'nofollow';
href={ permalink } } else {
aria-label={ buttonAriaLabel } buttonProps.onClick = addToCart;
className={ classnames(
'wp-block-button__link',
'add_to_cart_button',
{
loading: addingToCart,
added: addedToCart,
}
) }
rel="nofollow"
>
{ buttonText }
</a>
);
} }
return ( return (
<button <ButtonTag
onClick={ addToCart }
aria-label={ buttonAriaLabel } aria-label={ buttonAriaLabel }
className={ classnames( className={ classnames(
'wp-block-button__link', 'wp-block-button__link',
'add_to_cart_button', 'add_to_cart_button',
'wc-block-components-product-button__button',
{ {
loading: addingToCart, loading: addingToCart,
added: addedToCart, added: addedToCart,
} }
) } ) }
disabled={ addingToCart } disabled={ addingToCart }
{ ...buttonProps }
> >
{ buttonText } { buttonText }
</button> </ButtonTag>
); );
}; };
@ -146,16 +129,18 @@ const AddToCartButtonPlaceholder = () => {
<button <button
className={ classnames( className={ classnames(
'wp-block-button__link', 'wp-block-button__link',
'add_to_cart_button' 'add_to_cart_button',
'wc-block-components-product-button__button',
'wc-block-components-product-button__button--placeholder'
) } ) }
disabled={ true } disabled={ true }
/> />
); );
}; };
ProductButton.propTypes = { Block.propTypes = {
className: PropTypes.string, className: PropTypes.string,
product: PropTypes.object, product: PropTypes.object,
}; };
export default ProductButton; export default Block;

View File

@ -0,0 +1,26 @@
.wc-block-layout .wc-block-components-product-button {
word-break: break-word;
white-space: normal;
margin-top: 0;
margin-bottom: $gap-small;
.wc-block-components-product-button__button {
word-break: break-word;
white-space: normal;
margin: 0 auto;
display: inline-flex;
justify-content: center;
}
.wc-block-components-product-button__button--placeholder {
@include placeholder();
min-width: 8em;
min-height: 3em;
}
}
.wc-block-layout--is-loading .wc-block-components-product-button > .wc-block-components-product-button__button {
@include placeholder();
min-width: 8em;
min-height: 3em;
}

View File

@ -13,7 +13,8 @@ import {
/** /**
* Internal dependencies * Internal dependencies
*/ */
import ProductSaleBadge from '../sale-badge/block.js'; import ProductSaleBadge from './../sale-badge/block';
import './style.scss';
/** /**
* Product Image Block Component. * Product Image Block Component.
@ -27,19 +28,16 @@ import ProductSaleBadge from '../sale-badge/block.js';
* this is not provided. * this is not provided.
* @return {*} The component. * @return {*} The component.
*/ */
const ProductImage = ( { const Block = ( {
className, className,
productLink = true, productLink = true,
showSaleBadge = true, showSaleBadge,
saleBadgeAlign = 'right', saleBadgeAlign = 'right',
...props ...props
} ) => { } ) => {
const { parentClassName } = useInnerBlockLayoutContext();
const productDataContext = useProductDataContext(); const productDataContext = useProductDataContext();
const product = props.product || productDataContext.product; const product = props.product || productDataContext.product;
const { layoutStyleClassPrefix } = useInnerBlockLayoutContext();
const componentClass = `${ layoutStyleClassPrefix }__product-image`;
const [ imageLoaded, setImageLoaded ] = useState( false ); const [ imageLoaded, setImageLoaded ] = useState( false );
if ( ! product ) { if ( ! product ) {
@ -47,11 +45,12 @@ const ProductImage = ( {
<div <div
className={ classnames( className={ classnames(
className, className,
componentClass, 'wc-block-components-product-image',
'is-loading' 'wc-block-components-product-image--placeholder',
`${ parentClassName }__product-image`
) } ) }
> >
<ImagePlaceholder componentClass={ componentClass } /> <ImagePlaceholder />
</div> </div>
); );
} }
@ -60,14 +59,22 @@ const ProductImage = ( {
product?.images && product.images.length ? product.images[ 0 ] : null; product?.images && product.images.length ? product.images[ 0 ] : null;
return ( return (
<div className={ classnames( className, componentClass ) }> <div
className={ classnames(
className,
'wc-block-components-product-image',
`${ parentClassName }__product-image`
) }
>
{ productLink ? ( { productLink ? (
<a href={ product.permalink } rel="nofollow"> <a href={ product.permalink } rel="nofollow">
{ showSaleBadge && ( { !! showSaleBadge && (
<ProductSaleBadge align={ saleBadgeAlign } /> <ProductSaleBadge
align={ saleBadgeAlign }
product={ product }
/>
) } ) }
<Image <Image
componentClass={ componentClass }
image={ image } image={ image }
onLoad={ () => setImageLoaded( true ) } onLoad={ () => setImageLoaded( true ) }
loaded={ imageLoaded } loaded={ imageLoaded }
@ -75,11 +82,13 @@ const ProductImage = ( {
</a> </a>
) : ( ) : (
<> <>
{ showSaleBadge && ( { !! showSaleBadge && (
<ProductSaleBadge align={ saleBadgeAlign } /> <ProductSaleBadge
align={ saleBadgeAlign }
product={ product }
/>
) } ) }
<Image <Image
componentClass={ componentClass }
image={ image } image={ image }
onLoad={ () => setImageLoaded( true ) } onLoad={ () => setImageLoaded( true ) }
loaded={ imageLoaded } loaded={ imageLoaded }
@ -90,26 +99,16 @@ const ProductImage = ( {
); );
}; };
const ImagePlaceholder = ( { componentClass } ) => { const ImagePlaceholder = () => {
return ( return <img src={ PLACEHOLDER_IMG_SRC } alt="" />;
<img
className={ classnames(
`${ componentClass }__image`,
`${ componentClass }__image_placeholder`
) }
src={ PLACEHOLDER_IMG_SRC }
alt=""
/>
);
}; };
const Image = ( { componentClass, image, onLoad, loaded } ) => { const Image = ( { image, onLoad, loaded } ) => {
const { thumbnail, srcset, sizes, alt } = image || {}; const { thumbnail, srcset, sizes, alt } = image || {};
return ( return (
<> <>
<img <img
className={ classnames( `${ componentClass }__image` ) }
src={ thumbnail } src={ thumbnail }
srcSet={ srcset } srcSet={ srcset }
sizes={ sizes } sizes={ sizes }
@ -117,14 +116,12 @@ const Image = ( { componentClass, image, onLoad, loaded } ) => {
onLoad={ onLoad } onLoad={ onLoad }
hidden={ ! loaded } hidden={ ! loaded }
/> />
{ ! loaded && ( { ! loaded && <ImagePlaceholder /> }
<ImagePlaceholder componentClass={ componentClass } />
) }
</> </>
); );
}; };
ProductImage.propTypes = { Block.propTypes = {
className: PropTypes.string, className: PropTypes.string,
product: PropTypes.object, product: PropTypes.object,
productLink: PropTypes.bool, productLink: PropTypes.bool,
@ -132,4 +129,4 @@ ProductImage.propTypes = {
saleBadgeAlign: PropTypes.string, saleBadgeAlign: PropTypes.string,
}; };
export default ProductImage; export default Block;

View File

@ -0,0 +1,13 @@
/**
* External dependencies
*/
import { compose } from '@wordpress/compose';
import withFilteredAttributes from '@woocommerce/base-hocs/with-filtered-attributes';
/**
* Internal dependencies
*/
import Block from './block';
import attributes from './attributes';
export default compose( withFilteredAttributes( attributes ) )( Block );

View File

@ -0,0 +1,53 @@
.editor-styles-wrapper .wc-block-layout .wc-block-grid__products .wc-block-grid__product .wc-block-components-product-image,
.wc-block-layout .wc-block-components-product-image {
margin-top: 0;
margin-bottom: $gap-small;
text-decoration: none;
display: block;
position: relative;
a {
text-decoration: none;
border: 0;
outline: 0;
box-shadow: none;
}
img {
vertical-align: middle;
width: 100%;
&[hidden] {
display: none;
}
}
.wc-block-components-product-sale-badge {
&--alignleft {
position: absolute;
left: $gap-smaller/2;
top: $gap-smaller/2;
right: auto;
margin: 0;
}
&--aligncenter {
position: absolute;
top: $gap-smaller/2;
left: 50%;
right: auto;
transform: translateX(-50%);
margin: 0;
}
&--alignright {
position: absolute;
right: $gap-smaller/2;
top: $gap-smaller/2;
left: auto;
margin: 0;
}
}
}
.wc-block-layout--is-loading .wc-block-components-product-image {
@include placeholder();
}

View File

@ -10,6 +10,11 @@ import {
useProductDataContext, useProductDataContext,
} from '@woocommerce/shared-context'; } from '@woocommerce/shared-context';
/**
* Internal dependencies
*/
import './style.scss';
/** /**
* Product Price Block Component. * Product Price Block Component.
* *
@ -19,21 +24,19 @@ import {
* this is not provided. * this is not provided.
* @return {*} The component. * @return {*} The component.
*/ */
const ProductPrice = ( { className, ...props } ) => { const Block = ( { className, ...props } ) => {
const { parentClassName } = useInnerBlockLayoutContext();
const productDataContext = useProductDataContext(); const productDataContext = useProductDataContext();
const product = props.product || productDataContext.product; const product = props.product || productDataContext.product;
const { layoutStyleClassPrefix } = useInnerBlockLayoutContext();
const componentClass = `${ layoutStyleClassPrefix }__product-price`;
if ( ! product ) { if ( ! product ) {
return ( return (
<div <div
className={ classnames( className={ classnames(
className, className,
componentClass,
'price', 'price',
'is-loading' 'wc-block-components-product-price',
`${ parentClassName }__product-price`
) } ) }
/> />
); );
@ -43,17 +46,22 @@ const ProductPrice = ( { className, ...props } ) => {
const currency = getCurrencyFromPriceResponse( prices ); const currency = getCurrencyFromPriceResponse( prices );
return ( return (
<div className={ classnames( className, componentClass, 'price' ) }> <div
className={ classnames(
className,
'price',
'wc-block-components-product-price',
`${ parentClassName }__product-price`
) }
>
{ hasPriceRange( prices ) ? ( { hasPriceRange( prices ) ? (
<PriceRange <PriceRange
componentClass={ componentClass }
currency={ currency } currency={ currency }
minAmount={ prices.price_range.min_amount } minAmount={ prices.price_range.min_amount }
maxAmount={ prices.price_range.max_amount } maxAmount={ prices.price_range.max_amount }
/> />
) : ( ) : (
<Price <Price
componentClass={ componentClass }
currency={ currency } currency={ currency }
price={ prices.price } price={ prices.price }
regularPrice={ prices.regular_price } regularPrice={ prices.regular_price }
@ -71,9 +79,16 @@ const hasPriceRange = ( prices ) => {
); );
}; };
const PriceRange = ( { componentClass, currency, minAmount, maxAmount } ) => { const PriceRange = ( { currency, minAmount, maxAmount } ) => {
const { parentClassName } = useInnerBlockLayoutContext();
return ( return (
<span className={ `${ componentClass }__value` }> <span
className={ classnames(
'wc-block-components-product-price__value',
`${ parentClassName }__product-price__value`
) }
>
<FormattedMonetaryAmount <FormattedMonetaryAmount
currency={ currency } currency={ currency }
value={ minAmount } value={ minAmount }
@ -87,18 +102,30 @@ const PriceRange = ( { componentClass, currency, minAmount, maxAmount } ) => {
); );
}; };
const Price = ( { componentClass, currency, price, regularPrice } ) => { const Price = ( { currency, price, regularPrice } ) => {
const { parentClassName } = useInnerBlockLayoutContext();
return ( return (
<> <>
{ regularPrice !== price && ( { regularPrice !== price && (
<del className={ `${ componentClass }__regular` }> <del
className={ classnames(
'wc-block-components-product-price__regular',
`${ parentClassName }__product-price__regular`
) }
>
<FormattedMonetaryAmount <FormattedMonetaryAmount
currency={ currency } currency={ currency }
value={ regularPrice } value={ regularPrice }
/> />
</del> </del>
) } ) }
<span className={ `${ componentClass }__value` }> <span
className={ classnames(
'wc-block-components-product-price__value',
`${ parentClassName }__product-price__value`
) }
>
<FormattedMonetaryAmount <FormattedMonetaryAmount
currency={ currency } currency={ currency }
value={ price } value={ price }
@ -108,9 +135,9 @@ const Price = ( { componentClass, currency, price, regularPrice } ) => {
); );
}; };
ProductPrice.propTypes = { Block.propTypes = {
className: PropTypes.string, className: PropTypes.string,
product: PropTypes.object, product: PropTypes.object,
}; };
export default ProductPrice; export default Block;

View File

@ -0,0 +1,19 @@
.wc-block-layout {
.wc-block-components-product-price {
margin-top: 0;
margin-bottom: $gap-small;
display: block;
&__regular {
margin-right: 0.5em;
}
}
&--is-loading {
.wc-block-components-product-price::before {
@include placeholder();
content: ".";
display: inline-block;
width: 5em;
}
}
}

View File

@ -9,6 +9,11 @@ import {
useProductDataContext, useProductDataContext,
} from '@woocommerce/shared-context'; } from '@woocommerce/shared-context';
/**
* Internal dependencies
*/
import './style.scss';
/** /**
* Product Rating Block Component. * Product Rating Block Component.
* *
@ -18,13 +23,10 @@ import {
* this is not provided. * this is not provided.
* @return {*} The component. * @return {*} The component.
*/ */
const ProductRating = ( { className, ...props } ) => { const Block = ( { className, ...props } ) => {
const { parentClassName } = useInnerBlockLayoutContext();
const productDataContext = useProductDataContext(); const productDataContext = useProductDataContext();
const product = props.product || productDataContext.product; const product = props.product || productDataContext.product;
const { layoutStyleClassPrefix } = useInnerBlockLayoutContext();
const componentClass = `${ layoutStyleClassPrefix }__product-rating`;
const rating = getAverageRating( product ); const rating = getAverageRating( product );
if ( ! rating ) { if ( ! rating ) {
@ -42,10 +44,18 @@ const ProductRating = ( { className, ...props } ) => {
return ( return (
<div <div
className={ classnames( className, componentClass, 'star-rating' ) } className={ classnames(
className,
'star-rating',
'wc-block-components-product-rating',
`${ parentClassName }__product-rating`
) }
> >
<div <div
className={ `${ componentClass }__stars` } className={ classnames(
'wc-block-components-product-rating__stars',
`${ parentClassName }__product-rating__stars`
) }
role="img" role="img"
aria-label={ ratingText } aria-label={ ratingText }
> >
@ -62,9 +72,9 @@ const getAverageRating = ( product ) => {
return Number.isFinite( rating ) && rating > 0 ? rating : 0; return Number.isFinite( rating ) && rating > 0 ? rating : 0;
}; };
ProductRating.propTypes = { Block.propTypes = {
className: PropTypes.string, className: PropTypes.string,
product: PropTypes.object, product: PropTypes.object,
}; };
export default ProductRating; export default Block;

View File

@ -0,0 +1,54 @@
.wc-block-layout {
.wc-block-components-product-rating {
display: block;
margin-top: 0;
margin-bottom: $gap-small;
&__stars {
overflow: hidden;
position: relative;
width: 5.3em;
height: 1.618em;
line-height: 1.618;
font-size: 1em;
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
font-family: star;
font-weight: 400;
margin: 0 auto;
text-align: left;
&::before {
content: "\53\53\53\53\53";
top: 0;
left: 0;
right: 0;
position: absolute;
opacity: 0.5;
color: #aaa;
white-space: nowrap;
}
span {
overflow: hidden;
top: 0;
left: 0;
right: 0;
position: absolute;
padding-top: 1.5em;
}
span::before {
content: "\53\53\53\53\53";
top: 0;
left: 0;
right: 0;
position: absolute;
color: #000;
white-space: nowrap;
}
}
}
.wc-block-single-product {
.wc-block-components-product-rating__stars {
margin: 0;
}
}
}

View File

@ -10,6 +10,11 @@ import {
useProductDataContext, useProductDataContext,
} from '@woocommerce/shared-context'; } from '@woocommerce/shared-context';
/**
* Internal dependencies
*/
import './style.scss';
/** /**
* Product Sale Badge Block Component. * Product Sale Badge Block Component.
* *
@ -20,27 +25,27 @@ import {
* this is not provided. * this is not provided.
* @return {*} The component. * @return {*} The component.
*/ */
const ProductSaleBadge = ( { className, align, ...props } ) => { const Block = ( { className, align, ...props } ) => {
const { parentClassName } = useInnerBlockLayoutContext();
const productDataContext = useProductDataContext(); const productDataContext = useProductDataContext();
const product = props.product || productDataContext.product; const product = props.product || productDataContext.product;
const { layoutStyleClassPrefix } = useInnerBlockLayoutContext();
const componentClass = `${ layoutStyleClassPrefix }__product-onsale`;
if ( ! product || ! product.on_sale ) { if ( ! product || ! product.on_sale ) {
return null; return null;
} }
const alignClass = const alignClass =
typeof align === 'string' ? `${ componentClass }--align${ align }` : ''; typeof align === 'string'
? `wc-block-components-product-sale-badge--align${ align }`
: '';
return ( return (
<div <div
className={ classnames( className={ classnames(
'wc-block-component__sale-badge', 'wc-block-components-product-sale-badge',
className, className,
alignClass, alignClass,
componentClass `${ parentClassName }__product-onsale`
) } ) }
> >
<Label <Label
@ -54,10 +59,10 @@ const ProductSaleBadge = ( { className, align, ...props } ) => {
); );
}; };
ProductSaleBadge.propTypes = { Block.propTypes = {
className: PropTypes.string, className: PropTypes.string,
align: PropTypes.string, align: PropTypes.string,
product: PropTypes.object, product: PropTypes.object,
}; };
export default ProductSaleBadge; export default Block;

View File

@ -0,0 +1,16 @@
.wc-block-components-product-sale-badge {
margin: 0 auto $gap-small;
@include font-size(small);
padding: em($gap-smallest) em($gap-small);
display: inline-block;
width: auto;
border: 1px solid #43454b;
border-radius: 3px;
color: #43454b;
background: #fff;
text-align: center;
text-transform: uppercase;
font-weight: 600;
z-index: 9;
position: static;
}

View File

@ -0,0 +1,12 @@
/**
* External dependencies
*/
import classnames from 'classnames';
const save = ( { attributes } ) => {
return (
<div className={ classnames( 'is-loading', attributes.className ) } />
);
};
export default save;

View File

@ -4,6 +4,11 @@
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { Icon, grid } from '@woocommerce/icons'; import { Icon, grid } from '@woocommerce/icons';
/**
* Internal dependencies
*/
import save from './save';
/** /**
* Holds default config for this collection of blocks. * Holds default config for this collection of blocks.
*/ */
@ -18,5 +23,10 @@ export default {
html: false, html: false,
}, },
parent: [ 'woocommerce/all-products', 'woocommerce/single-product' ], parent: [ 'woocommerce/all-products', 'woocommerce/single-product' ],
save() {}, save,
deprecated: [
{
save() {},
},
],
}; };

View File

@ -10,6 +10,11 @@ import {
useProductDataContext, useProductDataContext,
} from '@woocommerce/shared-context'; } from '@woocommerce/shared-context';
/**
* Internal dependencies
*/
import './style.scss';
/** /**
* Product Summary Block Component. * Product Summary Block Component.
* *
@ -19,20 +24,17 @@ import {
* this is not provided. * this is not provided.
* @return {*} The component. * @return {*} The component.
*/ */
const ProductSummary = ( { className, ...props } ) => { const Block = ( { className, ...props } ) => {
const { parentClassName } = useInnerBlockLayoutContext();
const productDataContext = useProductDataContext(); const productDataContext = useProductDataContext();
const { product } = productDataContext || props; const { product } = productDataContext || props;
const { layoutStyleClassPrefix } = useInnerBlockLayoutContext();
const componentClass = `${ layoutStyleClassPrefix }__product-summary`;
if ( ! product ) { if ( ! product ) {
return ( return (
<div <div
className={ classnames( className={ classnames(
className, className,
componentClass, `wc-block-components-product-summary`
'is-loading'
) } ) }
/> />
); );
@ -50,7 +52,11 @@ const ProductSummary = ( { className, ...props } ) => {
return ( return (
<Summary <Summary
className={ classnames( className, componentClass ) } className={ classnames(
className,
`wc-block-components-product-summary`,
`${ parentClassName }__product-summary`
) }
source={ source } source={ source }
maxLength={ 150 } maxLength={ 150 }
countType={ countType } countType={ countType }
@ -58,9 +64,9 @@ const ProductSummary = ( { className, ...props } ) => {
); );
}; };
ProductSummary.propTypes = { Block.propTypes = {
className: PropTypes.string, className: PropTypes.string,
product: PropTypes.object, product: PropTypes.object,
}; };
export default ProductSummary; export default Block;

View File

@ -0,0 +1,12 @@
.wc-block-layout .wc-block-components-product-summary {
margin-top: 0;
margin-bottom: $gap-small;
}
.wc-block-layout--is-loading .wc-block-components-product-summary::before {
@include placeholder();
content: ".";
display: block;
width: 100%;
height: 6em;
}

View File

@ -9,6 +9,11 @@ import {
useProductDataContext, useProductDataContext,
} from '@woocommerce/shared-context'; } from '@woocommerce/shared-context';
/**
* Internal dependencies
*/
import './style.scss';
/** /**
* Product Title Block Component. * Product Title Block Component.
* *
@ -20,18 +25,15 @@ import {
* this is not provided. * this is not provided.
* @return {*} The component. * @return {*} The component.
*/ */
const ProductTitle = ( { export const Block = ( {
className, className,
headingLevel = 2, headingLevel = 2,
productLink = true, productLink = true,
...props ...props
} ) => { } ) => {
const { parentClassName } = useInnerBlockLayoutContext();
const productDataContext = useProductDataContext(); const productDataContext = useProductDataContext();
const product = props.product || productDataContext.product; const product = props.product || productDataContext.product;
const { layoutStyleClassPrefix } = useInnerBlockLayoutContext();
const componentClass = `${ layoutStyleClassPrefix }__product-title`;
const TagName = `h${ headingLevel }`; const TagName = `h${ headingLevel }`;
if ( ! product ) { if ( ! product ) {
@ -40,8 +42,8 @@ const ProductTitle = ( {
// @ts-ignore // @ts-ignore
className={ classnames( className={ classnames(
className, className,
componentClass, 'wc-block-components-product-title',
'is-loading' `${ parentClassName }__product-title`
) } ) }
/> />
); );
@ -51,7 +53,13 @@ const ProductTitle = ( {
return ( return (
// @ts-ignore // @ts-ignore
<TagName className={ classnames( className, componentClass ) }> <TagName
className={ classnames(
className,
'wc-block-components-product-title',
`${ parentClassName }__product-title`
) }
>
{ productLink ? ( { productLink ? (
<a href={ product.permalink } rel="nofollow"> <a href={ product.permalink } rel="nofollow">
{ productName } { productName }
@ -63,11 +71,11 @@ const ProductTitle = ( {
); );
}; };
ProductTitle.propTypes = { Block.propTypes = {
className: PropTypes.string, className: PropTypes.string,
product: PropTypes.object, product: PropTypes.object,
headingLevel: PropTypes.number, headingLevel: PropTypes.number,
productLink: PropTypes.bool, productLink: PropTypes.bool,
}; };
export default ProductTitle; export default Block;

View File

@ -23,7 +23,7 @@ export default ( { attributes, setAttributes } ) => {
<p>{ __( 'Level', 'woo-gutenberg-products-block' ) }</p> <p>{ __( 'Level', 'woo-gutenberg-products-block' ) }</p>
<HeadingToolbar <HeadingToolbar
isCollapsed={ false } isCollapsed={ false }
minLevel={ 2 } minLevel={ 1 }
maxLevel={ 7 } maxLevel={ 7 }
selectedLevel={ headingLevel } selectedLevel={ headingLevel }
onChange={ ( newLevel ) => onChange={ ( newLevel ) =>

View File

@ -0,0 +1,13 @@
/**
* External dependencies
*/
import { compose } from '@wordpress/compose';
import withFilteredAttributes from '@woocommerce/base-hocs/with-filtered-attributes';
/**
* Internal dependencies
*/
import Block from './block';
import attributes from './attributes';
export default compose( withFilteredAttributes( attributes ) )( Block );

View File

@ -0,0 +1,25 @@
.wc-block-layout {
.wc-block-components-product-title {
margin-top: 0;
margin-bottom: $gap-small;
}
.wc-block-grid .wc-block-components-product-title {
line-height: 1.2em;
font-weight: 700;
padding: 0;
color: inherit;
font-size: inherit;
display: block;
}
&--is-loading {
.wc-block-components-product-title::before {
@include placeholder();
content: ".";
display: inline-block;
width: 7em;
}
.wc-block-grid .wc-block-components-product-title::before {
width: 10em;
}
}
}

View File

@ -6,18 +6,16 @@ import { getRegisteredInnerBlocks } from '@woocommerce/blocks-registry';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { import ProductButton from '../blocks/product/button/block';
ProductTitle, import ProductImage from '../blocks/product/image/frontend';
ProductPrice, import ProductPrice from '../blocks/product/price/block';
ProductButton, import ProductRating from '../blocks/product/rating/block';
ProductImage, import ProductSaleBadge from '../blocks/product/sale-badge/block';
ProductRating, import ProductSummary from '../blocks/product/summary/block';
ProductSummary, import ProductTitle from '../blocks/product/title/frontend';
ProductSaleBadge,
} from '../blocks/product/block-components';
/** /**
* Map blocks names to components. * Map blocks to components suitable for use on the frontend.
* *
* @param {string} blockName Name of the parent block. Used to get extension children. * @param {string} blockName Name of the parent block. Used to get extension children.
*/ */

View File

@ -1,2 +1,4 @@
export * from './get-block-map.js'; export * from './get-block-map.js';
export * from './create-blocks-from-template.js'; export * from './create-blocks-from-template.js';
export * from './render-parent-block.js';
export * from './render-inner-blocks.js';

View File

@ -0,0 +1,65 @@
/**
* External dependencies
*/
import { cloneElement, isValidElement } from '@wordpress/element';
import parse from 'html-react-parser';
/**
* Internal dependencies
*/
import { getBlockMap } from './get-block-map';
/**
* Replaces saved block HTML markup with Inner Block Components.
*
* @param {Object} props Render props.
* @param {Array} props.children Children/inner blocks to render.
* @param {string} props.blockName Parent Block Name used to get the block map and for keys.
* @param {number} [props.depth] Depth of inner blocks being rendered.
*/
export const renderInnerBlocks = ( {
children,
blockName: parentBlockName,
depth = 1,
} ) => {
const blockMap = getBlockMap( parentBlockName );
return Array.from( children ).map( ( el, index ) => {
const componentProps = {
...el.dataset,
key: `${ parentBlockName }_${ depth }_${ index }`,
};
const componentChildren =
el.children && el.children.length
? renderInnerBlocks( {
children: el.children,
blockName: parentBlockName,
depth: depth + 1,
} )
: null;
const LayoutComponent =
componentProps.blockName && blockMap[ componentProps.blockName ]
? blockMap[ componentProps.blockName ]
: null;
if ( ! LayoutComponent ) {
const element = parse( el.outerHTML );
if ( isValidElement( element ) ) {
return componentChildren
? cloneElement( element, componentProps, componentChildren )
: cloneElement( element, componentProps );
}
return null;
}
return (
// eslint-disable-next-line react/jsx-key
<LayoutComponent { ...componentProps }>
{ componentChildren }
</LayoutComponent>
);
} );
};

View File

@ -0,0 +1,41 @@
/**
* External dependencies
*/
import { renderFrontend } from '@woocommerce/base-utils';
/**
* Internal dependencies
*/
import { renderInnerBlocks } from './render-inner-blocks';
/**
* Renders a block component in the place of a specified set of selectors.
*
* @param {Object} props Render props.
* @param {Function} props.Block React component to use as a replacement.
* @param {string} props.selector CSS selector to match the elements to replace.
* @param {string} [props.blockName] Optional Block Name. Used for inner block component mapping.
* @param {Function} [props.getProps] Function to generate the props object for the block.
*/
export const renderParentBlock = ( {
Block,
selector,
blockName = '',
getProps = () => {},
} ) => {
const getPropsWithChildren = ( el, i ) => {
const children =
el.children && el.children.length
? renderInnerBlocks( {
blockName,
children: el.children,
} )
: null;
return { ...getProps( el, i ), children };
};
renderFrontend( {
Block,
selector,
getProps: getPropsWithChildren,
} );
};

View File

@ -15,20 +15,20 @@ import './style.scss';
const ExpressCheckoutContainer = ( { children } ) => { const ExpressCheckoutContainer = ( { children } ) => {
return ( return (
<> <>
<div className="wc-block-component-express-checkout"> <div className="wc-block-components-express-checkout">
<Title <Title
className="wc-block-component-express-checkout__title" className="wc-block-components-express-checkout__title"
headingLevel="2" headingLevel="2"
> >
{ __( 'Express checkout', 'woo-gutenberg-products-block' ) } { __( 'Express checkout', 'woo-gutenberg-products-block' ) }
</Title> </Title>
<div className="wc-block-component-express-checkout__content"> <div className="wc-block-components-express-checkout__content">
<StoreNoticesProvider context="wc/express-payment-area"> <StoreNoticesProvider context="wc/express-payment-area">
{ children } { children }
</StoreNoticesProvider> </StoreNoticesProvider>
</div> </div>
</div> </div>
<div className="wc-block-component-express-checkout-continue-rule"> <div className="wc-block-components-express-checkout-continue-rule">
{ __( 'Or continue below', 'woo-gutenberg-products-block' ) } { __( 'Or continue below', 'woo-gutenberg-products-block' ) }
</div> </div>
</> </>

View File

@ -59,7 +59,7 @@ const ExpressPaymentMethods = () => {
<li key="noneRegistered">No registered Payment Methods</li> <li key="noneRegistered">No registered Payment Methods</li>
); );
return ( return (
<ul className="wc-block-component-express-checkout-payment-event-buttons"> <ul className="wc-block-components-express-checkout-payment-event-buttons">
{ content } { content }
</ul> </ul>
); );

View File

@ -1,11 +1,11 @@
.wc-block-component-express-checkout { .wc-block-components-express-checkout {
margin: auto; margin: auto;
border: 2px solid $black; border: 2px solid $black;
border-radius: 5px; border-radius: 5px;
padding: 8px; padding: 8px;
position: relative; position: relative;
.wc-block-component-express-checkout__title { .wc-block-components-express-checkout__title {
background-color: $white; background-color: $white;
padding-left: $gap-small; padding-left: $gap-small;
padding-right: $gap-small; padding-right: $gap-small;
@ -17,11 +17,11 @@
top: 0; top: 0;
} }
.wc-block-component-express-checkout__content { .wc-block-components-express-checkout__content {
padding: $gap $gap-large 0; padding: $gap $gap-large 0;
} }
.wc-block-component-express-checkout-payment-event-buttons { .wc-block-components-express-checkout-payment-event-buttons {
list-style: none; list-style: none;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -190,7 +190,7 @@
// For Twenty Twenty we need to increase specificity of the title. // For Twenty Twenty we need to increase specificity of the title.
.theme-twentytwenty { .theme-twentytwenty {
.wc-block-component-express-checkout .wc-block-component-express-checkout__title { .wc-block-components-express-checkout .wc-block-components-express-checkout__title {
padding-left: $gap-small; padding-left: $gap-small;
padding-right: $gap-small; padding-right: $gap-small;
margin-left: $gap-small; margin-left: $gap-small;

View File

@ -13,10 +13,11 @@ import { renderProductLayout } from './utils';
const ProductListItem = ( { product, attributes, instanceId } ) => { const ProductListItem = ( { product, attributes, instanceId } ) => {
const { layoutConfig } = attributes; const { layoutConfig } = attributes;
const { layoutStyleClassPrefix, parentName } = useInnerBlockLayoutContext(); const { parentClassName, parentName } = useInnerBlockLayoutContext();
const isLoading = Object.keys( product ).length === 0; const isLoading = Object.keys( product ).length === 0;
const classes = classnames( `${ layoutStyleClassPrefix }__product`, { const classes = classnames( `${ parentClassName }__product`, {
'is-loading': isLoading, 'is-loading': isLoading,
'wc-block-layout--is-loading': isLoading, // This can be removed when switching to inner block rendering.
} ); } );
return ( return (

View File

@ -114,7 +114,7 @@ const ProductList = ( {
const { products, totalProducts, productsLoading } = useStoreProducts( const { products, totalProducts, productsLoading } = useStoreProducts(
queryState queryState
); );
const { layoutStyleClassPrefix } = useInnerBlockLayoutContext(); const { parentClassName } = useInnerBlockLayoutContext();
const totalQuery = extractPaginationAndSortAttributes( queryState ); const totalQuery = extractPaginationAndSortAttributes( queryState );
// These are possible filters. // These are possible filters.
@ -162,7 +162,7 @@ const ProductList = ( {
const alignClass = typeof align !== 'undefined' ? 'align' + align : ''; const alignClass = typeof align !== 'undefined' ? 'align' + align : '';
return classnames( return classnames(
layoutStyleClassPrefix, parentClassName,
alignClass, alignClass,
'has-' + columns + '-columns', 'has-' + columns + '-columns',
{ {
@ -206,7 +206,7 @@ const ProductList = ( {
) } ) }
{ ! hasProducts && ! hasFilters && <NoProducts /> } { ! hasProducts && ! hasFilters && <NoProducts /> }
{ hasProducts && ( { hasProducts && (
<ul className={ `${ layoutStyleClassPrefix }__products` }> <ul className={ `${ parentClassName }__products` }>
{ listProducts.map( ( product = {}, i ) => ( { listProducts.map( ( product = {}, i ) => (
<ProductListItem <ProductListItem
key={ product.id || i } key={ product.id || i }

View File

@ -6,23 +6,19 @@ import { useInnerBlockLayoutContext } from '@woocommerce/shared-context';
import { Icon, search } from '@woocommerce/icons'; import { Icon, search } from '@woocommerce/icons';
const NoMatchingProducts = ( { resetCallback = () => {} } ) => { const NoMatchingProducts = ( { resetCallback = () => {} } ) => {
const { layoutStyleClassPrefix } = useInnerBlockLayoutContext(); const { parentClassName } = useInnerBlockLayoutContext();
return ( return (
<div className={ `${ layoutStyleClassPrefix }__no-products` }> <div className={ `${ parentClassName }__no-products` }>
<Icon <Icon
className={ `${ layoutStyleClassPrefix }__no-products-image` } className={ `${ parentClassName }__no-products-image` }
alt="" alt=""
srcElement={ search } srcElement={ search }
size={ 100 } size={ 100 }
/> />
<strong <strong className={ `${ parentClassName }__no-products-title` }>
className={ `${ layoutStyleClassPrefix }__no-products-title` }
>
{ __( 'No products found', 'woo-gutenberg-products-block' ) } { __( 'No products found', 'woo-gutenberg-products-block' ) }
</strong> </strong>
<p <p className={ `${ parentClassName }__no-products-description` }>
className={ `${ layoutStyleClassPrefix }__no-products-description` }
>
{ __( { __(
'We were unable to find any results based on your search.', 'We were unable to find any results based on your search.',
'woo-gutenberg-products-block' 'woo-gutenberg-products-block'

View File

@ -6,23 +6,19 @@ import { useInnerBlockLayoutContext } from '@woocommerce/shared-context';
import { Icon, notice } from '@woocommerce/icons'; import { Icon, notice } from '@woocommerce/icons';
const NoProducts = () => { const NoProducts = () => {
const { layoutStyleClassPrefix } = useInnerBlockLayoutContext(); const { parentClassName } = useInnerBlockLayoutContext();
return ( return (
<div className={ `${ layoutStyleClassPrefix }__no-products` }> <div className={ `${ parentClassName }__no-products` }>
<Icon <Icon
className={ `${ layoutStyleClassPrefix }__no-products-image` } className={ `${ parentClassName }__no-products-image` }
alt="" alt=""
srcElement={ notice } srcElement={ notice }
size={ 100 } size={ 100 }
/> />
<strong <strong className={ `${ parentClassName }__no-products-title` }>
className={ `${ layoutStyleClassPrefix }__no-products-title` }
>
{ __( 'No products', 'woo-gutenberg-products-block' ) } { __( 'No products', 'woo-gutenberg-products-block' ) }
</strong> </strong>
<p <p className={ `${ parentClassName }__no-products-description` }>
className={ `${ layoutStyleClassPrefix }__no-products-description` }
>
{ __( { __(
'There are currently no products available to display.', 'There are currently no products available to display.',
'woo-gutenberg-products-block' 'woo-gutenberg-products-block'

View File

@ -55,217 +55,14 @@
list-style: none; list-style: none;
} }
// Extra specificity to avoid editor styles on linked images. .theme-twentytwenty .wc-block-grid,
.wc-block-grid__products .wc-block-grid__product-image {
text-decoration: none;
display: block;
position: relative;
a {
text-decoration: none;
border: 0;
outline: 0;
box-shadow: none;
}
img {
width: 100%;
&[hidden] {
display: none;
}
.is-loading & {
@include placeholder();
height: 0;
padding-bottom: 100%;
}
}
}
.edit-post-visual-editor .editor-block-list__block .wc-block-grid__product-title,
.editor-styles-wrapper .wc-block-grid__product-title,
.wc-block-grid__product-title {
font-family: inherit;
line-height: 1.2em;
font-weight: 700;
padding: 0;
color: inherit;
font-size: inherit;
display: block;
.is-loading &::before {
@include placeholder();
content: ".";
display: inline-block;
width: 6em;
}
}
.wc-block-grid__product-price {
display: block;
.wc-block-grid__product-price__value {
margin-left: 0.5em;
.is-loading &::before {
@include placeholder();
content: ".";
display: inline-block;
width: 3em;
}
}
}
.wc-block-grid__product-add-to-cart {
word-break: break-word;
white-space: normal;
a,
button {
word-break: break-word;
white-space: normal;
margin: 0 auto !important;
display: inline-flex;
justify-content: center;
&.loading {
opacity: 0.25;
}
&::after {
margin-left: 0.5em;
display: inline-block;
}
&.added::after {
font-family: WooCommerce; /* stylelint-disable-line */
content: "\e017";
}
&.loading::after {
font-family: WooCommerce; /* stylelint-disable-line */
content: "\e031";
animation: spin 2s linear infinite;
}
.is-loading & {
@include placeholder();
min-width: 7em;
}
}
}
.wc-block-grid__product-rating {
display: block;
.wc-block-grid__product-rating__stars,
.star-rating {
overflow: hidden;
position: relative;
width: 5.3em;
height: 1.618em;
line-height: 1.618;
font-size: 1em;
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
font-family: star;
font-weight: 400;
margin: 0 auto;
text-align: left;
&::before {
content: "\53\53\53\53\53";
top: 0;
left: 0;
right: 0;
position: absolute;
opacity: 0.5;
color: #aaa;
white-space: nowrap;
}
span {
overflow: hidden;
top: 0;
left: 0;
right: 0;
position: absolute;
padding-top: 1.5em;
}
span::before {
content: "\53\53\53\53\53";
top: 0;
left: 0;
right: 0;
position: absolute;
color: #000;
white-space: nowrap;
}
}
}
.wc-block-grid__product-onsale {
border: 1px solid #43454b;
color: #43454b;
background: #fff;
padding: 0.202em 0.6180469716em;
font-size: 0.875rem;
text-align: center;
text-transform: uppercase;
font-weight: 600;
display: inline-block;
width: auto;
border-radius: 3px;
z-index: 9;
position: relative;
margin: $gap-smaller auto;
}
.editor-styles-wrapper .wc-block-grid__products .wc-block-grid__product .wc-block-grid__product-image,
.wc-block-grid__product-image {
.wc-block-grid__product-onsale {
&.wc-block-grid__product-onsale--alignleft {
position: absolute;
left: $gap-smaller/2;
top: $gap-smaller/2;
right: auto;
margin: 0;
}
&.wc-block-grid__product-onsale--aligncenter {
position: absolute;
top: $gap-smaller/2;
left: 50%;
transform: translateX(-50%);
margin: 0;
}
&.wc-block-grid__product-onsale--alignright {
position: absolute;
right: $gap-smaller/2;
top: $gap-smaller/2;
left: auto;
margin: 0;
}
}
}
// Element spacing.
.wc-block-grid__product {
.wc-block-grid__product-image,
.wc-block-grid__product-title,
.wc-block-grid__product-price,
.wc-block-grid__product-rating,
.wc-block-grid__product-add-to-cart {
margin-top: 0;
margin-bottom: $gap-small;
}
}
.wc-block-grid { .wc-block-grid {
&.has-aligned-buttons { &.has-aligned-buttons {
.wc-block-grid__product { .wc-block-grid__product {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.wc-block-grid__product > .wc-block-grid__product-title:last-child, .wc-block-grid__product > :last-child {
.wc-block-grid__product > div:last-child {
margin-top: auto; margin-top: auto;
margin-bottom: 0; margin-bottom: 0;
padding-bottom: $gap-small; padding-bottom: $gap-small;
@ -288,7 +85,6 @@
} }
} }
// Responsive media styles.
@include breakpoint( "<480px" ) { @include breakpoint( "<480px" ) {
.wc-block-grid { .wc-block-grid {
@for $i from 2 to 9 { @for $i from 2 to 9 {
@ -305,11 +101,9 @@
} }
} }
} }
.wc-block-grid__product-image img {
width: 100%;
}
} }
} }
@include breakpoint( "480px-600px" ) { @include breakpoint( "480px-600px" ) {
.wc-block-grid { .wc-block-grid {
@for $i from 2 to 9 { @for $i from 2 to 9 {
@ -331,120 +125,5 @@
} }
} }
} }
.wc-block-grid__product-image img {
width: 100%;
}
}
}
.theme-twentysixteen {
.wc-block-grid {
// Prevent white theme styles.
.price ins {
color: #77a464;
}
}
}
.theme-twentynineteen {
.wc-block-grid__product {
font-size: 0.88889em;
}
// Change the title font to match headings.
.wc-block-grid__product-title,
.wc-block-grid__product-onsale {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
}
.wc-block-grid__product-title::before {
display: none;
}
.wc-block-grid__product-onsale {
line-height: 1;
}
}
.theme-twentytwenty {
$twentytwenty-headings: -apple-system, blinkmacsystemfont, "Helvetica Neue", helvetica, sans-serif;
$twentytwenty-highlights-color: #cd2653;
.wc-block-grid__product-link {
color: #000;
}
.wc-block-grid__product-title {
font-family: $twentytwenty-headings;
color: #000;
font-size: 1.2em;
}
.wc-block-grid__product-price {
.wc-block-grid__product-price__value,
.woocommerce-Price-amount {
font-family: $twentytwenty-headings;
font-size: 0.9em;
}
del {
opacity: 0.5;
}
ins {
text-decoration: none;
}
}
.wc-block-grid__product-rating {
.wc-block-grid__product-rating__stars,
.star-rating {
font-size: 0.7em;
}
}
.wc-block-grid__product-add-to-cart > .wp-block-button__link {
font-family: $twentytwenty-headings;
}
.wc-block-grid__products .wc-block-grid__product-onsale {
background: $twentytwenty-highlights-color;
color: #fff;
font-family: $twentytwenty-headings;
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.2;
text-transform: uppercase;
font-size: 1.5rem;
}
// Override style from WC Core that set its position to absolute.
// These rulesets can be removed once https://github.com/woocommerce/woocommerce/pull/26516 is released.
.wc-block-grid__products .wc-block-component__sale-badge {
position: static;
}
.wc-block-grid__products .wc-block-grid__product-image .wc-block-component__sale-badge {
position: absolute;
}
// These styles are not applied to the All Products atomic block, so it can be positioned normally.
.wc-block-grid__products .wc-block-grid__product-onsale:not(.wc-block-component__sale-badge) {
position: absolute;
right: 4px;
top: 4px;
margin: 0;
padding: 1rem;
z-index: 1;
}
@media only screen and (min-width: 768px) {
.wc-block-grid__product-onsale {
font-size: 0.75em;
padding: 1rem;
}
}
@media only screen and (min-width: 1168px) {
.wc-block-grid__product-onsale {
font-size: 0.85em;
padding: 0.75em;
}
} }
} }

View File

@ -0,0 +1,24 @@
/**
* External dependencies
*/
import { getValidBlockAttributes } from '@woocommerce/base-utils';
/**
* HOC that filters given attributes by valid block attribute values, or uses defaults if undefined.
*
* @param {Object} blockAttributes Component being wrapped.
*/
const withFilteredAttributes = ( blockAttributes ) => ( OriginalComponent ) => {
return ( ownProps ) => {
const validBlockAttributes = getValidBlockAttributes(
blockAttributes,
ownProps
);
return (
<OriginalComponent { ...ownProps } { ...validBlockAttributes } />
);
};
};
export default withFilteredAttributes;

View File

@ -0,0 +1,36 @@
/**
* Given some block attributes, gets attributes from the dataset or uses defaults.
*
* @param {Object} blockAttributes Object containing block attributes.
* @param {Array} rawAttributes Dataset from DOM.
* @return {Array} Array of parsed attributes.
*/
export const getValidBlockAttributes = ( blockAttributes, rawAttributes ) => {
const attributes = [];
Object.keys( blockAttributes ).forEach( ( key ) => {
if ( typeof rawAttributes[ key ] !== 'undefined' ) {
switch ( blockAttributes[ key ].type ) {
case 'boolean':
attributes[ key ] = rawAttributes[ key ] !== 'false';
break;
case 'number':
attributes[ key ] = parseInt( rawAttributes[ key ], 10 );
break;
case 'array':
case 'object':
attributes[ key ] = JSON.parse( rawAttributes[ key ] );
break;
default:
attributes[ key ] = rawAttributes[ key ];
break;
}
} else {
attributes[ key ] = blockAttributes[ key ].default;
}
} );
return attributes;
};
export default getValidBlockAttributes;

View File

@ -4,3 +4,4 @@ export * from './address';
export * from './shipping-rates'; export * from './shipping-rates';
export * from './legacy-events'; export * from './legacy-events';
export * from './render-frontend'; export * from './render-frontend';
export * from './get-valid-block-attributes';

View File

@ -4,49 +4,18 @@
import { render } from 'react-dom'; import { render } from 'react-dom';
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary'; import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
/**
* Given some block attributes, gets attributes from the dataset or uses defaults.
*
* @param {Object} blockAttributes Object containing block attributes.
* @param {Array} dataset Dataset from DOM.
* @return {Array} Array of parsed attributes.
*/
export const getAttributesFromDataset = ( blockAttributes, dataset ) => {
const attributes = [];
Object.keys( blockAttributes ).forEach( ( key ) => {
if ( typeof dataset[ key ] !== 'undefined' ) {
switch ( blockAttributes[ key ].type ) {
case 'boolean':
attributes[ key ] = dataset[ key ] !== 'false';
break;
case 'number':
attributes[ key ] = parseInt( dataset[ key ], 10 );
break;
default:
attributes[ key ] = dataset[ key ];
break;
}
} else {
attributes[ key ] = blockAttributes[ key ].default;
}
} );
return attributes;
};
/** /**
* Renders a block component in the place of a specified set of selectors. * Renders a block component in the place of a specified set of selectors.
* *
* @param {Object} props Render props. * @param {Object} props Render props.
* @param {string} props.selector CSS selector to match the elements to replace.
* @param {Function} props.Block React component to use as a replacement. * @param {Function} props.Block React component to use as a replacement.
* @param {string} props.selector CSS selector to match the elements to replace.
* @param {Function} [props.getProps ] Function to generate the props object for the block. * @param {Function} [props.getProps ] Function to generate the props object for the block.
* @param {Function} [props.getErrorBoundaryProps] Function to generate the props object for the error boundary. * @param {Function} [props.getErrorBoundaryProps] Function to generate the props object for the error boundary.
*/ */
export const renderFrontend = ( { export const renderFrontend = ( {
selector,
Block, Block,
selector,
getProps = () => {}, getProps = () => {},
getErrorBoundaryProps = () => {}, getErrorBoundaryProps = () => {},
} ) => { } ) => {
@ -61,7 +30,6 @@ export const renderFrontend = ( {
...el.dataset, ...el.dataset,
...props.attributes, ...props.attributes,
}; };
el.classList.remove( 'is-loading' ); el.classList.remove( 'is-loading' );
render( render(

View File

@ -11,7 +11,7 @@ import { CURRENT_USER_IS_ADMIN } from '@woocommerce/block-settings';
import { __experimentalCreateInterpolateElement } from 'wordpress-element'; import { __experimentalCreateInterpolateElement } from 'wordpress-element';
import { import {
renderFrontend, renderFrontend,
getAttributesFromDataset, getValidBlockAttributes,
} from '@woocommerce/base-utils'; } from '@woocommerce/base-utils';
/** /**
@ -37,7 +37,7 @@ const CartFrontend = ( props ) => {
const getProps = ( el ) => { const getProps = ( el ) => {
return { return {
emptyCart: el.innerHTML, emptyCart: el.innerHTML,
attributes: getAttributesFromDataset( blockAttributes, el.dataset ), attributes: getValidBlockAttributes( blockAttributes, el.dataset ),
}; };
}; };

View File

@ -16,7 +16,7 @@ import { CURRENT_USER_IS_ADMIN } from '@woocommerce/block-settings';
import { __experimentalCreateInterpolateElement } from 'wordpress-element'; import { __experimentalCreateInterpolateElement } from 'wordpress-element';
import { import {
renderFrontend, renderFrontend,
getAttributesFromDataset, getValidBlockAttributes,
} from '@woocommerce/base-utils'; } from '@woocommerce/base-utils';
/** /**
@ -75,7 +75,7 @@ const CheckoutFrontend = ( props ) => {
const getProps = ( el ) => { const getProps = ( el ) => {
return { return {
attributes: getAttributesFromDataset( blockAttributes, el.dataset ), attributes: getValidBlockAttributes( blockAttributes, el.dataset ),
}; };
}; };

View File

@ -112,7 +112,7 @@
} }
} }
.wc-block-component-express-checkout-continue-rule { .wc-block-components-express-checkout-continue-rule {
display: flex; display: flex;
align-items: center; align-items: center;
text-align: center; text-align: center;
@ -160,15 +160,15 @@
// Loading placeholder state. // Loading placeholder state.
.wc-block-checkout--is-loading { .wc-block-checkout--is-loading {
.wc-block-component-express-checkout, .wc-block-components-express-checkout,
.wc-block-checkout__actions button { .wc-block-checkout__actions button {
@include placeholder(); @include placeholder();
@include force-content(); @include force-content();
} }
.wc-block-component-express-checkout { .wc-block-components-express-checkout {
min-height: 150px; min-height: 150px;
} }
.wc-block-component-express-checkout-continue-rule span { .wc-block-components-express-checkout-continue-rule span {
@include placeholder(); @include placeholder();
@include force-content(); @include force-content();
width: 150px; width: 150px;

View File

@ -34,7 +34,7 @@ class Block extends Component {
return ( return (
<InnerBlockLayoutContextProvider <InnerBlockLayoutContextProvider
parentName="woocommerce/all-products" parentName="woocommerce/all-products"
layoutStyleClassPrefix="wc-block-grid" parentClassName="wc-block-grid"
> >
<ProductListContainer <ProductListContainer
attributes={ attributes } attributes={ attributes }

View File

@ -204,17 +204,22 @@ class Editor extends Component {
'woo-gutenberg-products-block' 'woo-gutenberg-products-block'
) } ) }
</Tip> </Tip>
<div className="wc-block-grid has-1-columns"> <InnerBlockLayoutContextProvider
<ul className="wc-block-grid__products"> parentName="woocommerce/all-products"
<li className="wc-block-grid__product"> parentClassName="wc-block-grid"
<ProductDataContextProvider >
product={ previewProducts[ 0 ] } <div className="wc-block-grid has-1-columns">
> <ul className="wc-block-grid__products">
<InnerBlocks { ...InnerBlockProps } /> <li className="wc-block-grid__product">
</ProductDataContextProvider> <ProductDataContextProvider
</li> product={ previewProducts[ 0 ] }
</ul> >
</div> <InnerBlocks { ...InnerBlockProps } />
</ProductDataContextProvider>
</li>
</ul>
</div>
</InnerBlockLayoutContextProvider>
<div className="wc-block-all-products__actions"> <div className="wc-block-all-products__actions">
<Button <Button
className="wc-block-all-products__done-button" className="wc-block-all-products__done-button"
@ -280,23 +285,16 @@ class Editor extends Component {
} }
return ( return (
<InnerBlockLayoutContextProvider <div
parentName="woocommerce/all-products" className={ getBlockClassName(
layoutStyleClassPrefix="wc-block-grid" 'wc-block-all-products',
attributes
) }
> >
<div { this.getBlockControls() }
className={ getBlockClassName( { this.getInspectorControls() }
'wc-block-all-products', { isEditing ? this.renderEditMode() : this.renderViewMode() }
attributes </div>
) }
>
{ this.getBlockControls() }
{ this.getInspectorControls() }
{ isEditing
? this.renderEditMode()
: this.renderViewMode() }
</div>
</InnerBlockLayoutContextProvider>
); );
}; };
} }

View File

@ -2,12 +2,33 @@
* External dependencies * External dependencies
*/ */
import { withProduct } from '@woocommerce/block-hocs'; import { withProduct } from '@woocommerce/block-hocs';
import {
InnerBlockLayoutContextProvider,
ProductDataContextProvider,
} from '@woocommerce/shared-context';
/**
* Internal dependencies
*/
import { BLOCK_NAME } from './constants';
/** /**
* The Single Product Block. * The Single Product Block.
*/ */
const Block = () => { const Block = ( { isLoading, product, children } ) => {
return null; const className = 'wc-block-single-product';
return (
<InnerBlockLayoutContextProvider
parentName={ BLOCK_NAME }
parentClassName={ className }
isLoading={ isLoading }
>
<ProductDataContextProvider product={ product }>
<div className={ className }>{ children }</div>
</ProductDataContextProvider>
</InnerBlockLayoutContextProvider>
);
}; };
export default withProduct( Block ); export default withProduct( Block );

View File

@ -31,7 +31,7 @@ export const DEFAULT_INNER_BLOCKS = [
{}, {},
[ [
[ 'woocommerce/product-sale-badge' ], [ 'woocommerce/product-sale-badge' ],
[ 'woocommerce/product-title' ], [ 'woocommerce/product-title', { headingLevel: 1 } ],
[ 'woocommerce/product-rating' ], [ 'woocommerce/product-rating' ],
[ 'woocommerce/product-price' ], [ 'woocommerce/product-price' ],
[ 'woocommerce/product-summary' ], [ 'woocommerce/product-summary' ],

View File

@ -10,7 +10,6 @@ import {
ProductDataContextProvider, ProductDataContextProvider,
} from '@woocommerce/shared-context'; } from '@woocommerce/shared-context';
import { createBlocksFromTemplate } from '@woocommerce/atomic-utils'; import { createBlocksFromTemplate } from '@woocommerce/atomic-utils';
import classnames from 'classnames';
import { PanelBody, Button } from '@wordpress/components'; import { PanelBody, Button } from '@wordpress/components';
import { Icon, restore } from '@woocommerce/icons'; import { Icon, restore } from '@woocommerce/icons';
@ -26,7 +25,7 @@ import {
/** /**
* Component to handle edit mode of the "Single Product Block". * Component to handle edit mode of the "Single Product Block".
*/ */
const LayoutEditor = ( { product, clientId, isLoading } ) => { const LayoutEditor = ( { isLoading, product, clientId } ) => {
const baseClassName = 'wc-block-single-product'; const baseClassName = 'wc-block-single-product';
const { replaceInnerBlocks } = useDispatch( 'core/block-editor' ); const { replaceInnerBlocks } = useDispatch( 'core/block-editor' );
@ -41,7 +40,8 @@ const LayoutEditor = ( { product, clientId, isLoading } ) => {
return ( return (
<InnerBlockLayoutContextProvider <InnerBlockLayoutContextProvider
parentName={ BLOCK_NAME } parentName={ BLOCK_NAME }
layoutStyleClassPrefix={ baseClassName } parentClassName={ baseClassName }
isLoading={ isLoading }
> >
<ProductDataContextProvider product={ product }> <ProductDataContextProvider product={ product }>
<InspectorControls> <InspectorControls>
@ -66,11 +66,7 @@ const LayoutEditor = ( { product, clientId, isLoading } ) => {
</Button> </Button>
</PanelBody> </PanelBody>
</InspectorControls> </InspectorControls>
<div <div className={ baseClassName }>
className={ classnames( baseClassName, {
'is-loading': isLoading,
} ) }
>
<InnerBlocks <InnerBlocks
template={ DEFAULT_INNER_BLOCKS } template={ DEFAULT_INNER_BLOCKS }
allowedBlocks={ ALLOWED_INNER_BLOCKS } allowedBlocks={ ALLOWED_INNER_BLOCKS }

View File

@ -2,16 +2,15 @@
* External dependencies * External dependencies
*/ */
import { StoreNoticesProvider } from '@woocommerce/base-context'; import { StoreNoticesProvider } from '@woocommerce/base-context';
import { import { getValidBlockAttributes } from '@woocommerce/base-utils';
renderFrontend, import { renderParentBlock } from '@woocommerce/atomic-utils';
getAttributesFromDataset,
} from '@woocommerce/base-utils';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import Block from './block'; import Block from './block';
import blockAttributes from './attributes'; import blockAttributes from './attributes';
import { BLOCK_NAME } from './constants';
/** /**
* Wrapper component to supply the notice provider. * Wrapper component to supply the notice provider.
@ -28,12 +27,13 @@ const FrontendBlock = ( props ) => {
const getProps = ( el ) => { const getProps = ( el ) => {
return { return {
attributes: getAttributesFromDataset( blockAttributes, el.dataset ), attributes: getValidBlockAttributes( blockAttributes, el.dataset ),
}; };
}; };
renderFrontend( { renderParentBlock( {
selector: '.wp-block-woocommerce-single-product',
Block: FrontendBlock, Block: FrontendBlock,
blockName: BLOCK_NAME,
selector: '.wp-block-woocommerce-single-product',
getProps, getProps,
} ); } );

View File

@ -1,6 +1,6 @@
// Ensure textarea bg color is transparent for block titles. // Ensure textarea bg color is transparent for block titles.
// Some themes (e.g. Twenty Twenty) set a non-white background for the editor, and Gutenberg sets white background for text inputs, creating this issue. // Some themes (e.g. Twenty Twenty) set a non-white background for the editor, and Gutenberg sets white background for text inputs, creating this issue.
// https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/1204 // https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/1204
.wc-block-component-title { .wc-block-components-title {
background-color: transparent; background-color: transparent;
} }

View File

@ -16,7 +16,7 @@ const BlockTitle = ( { className, headingLevel, onChange, heading } ) => {
<TagName> <TagName>
<PlainText <PlainText
className={ classnames( className={ classnames(
'wc-block-component-title', 'wc-block-components-title',
className className
) } ) }
value={ heading } value={ heading }

View File

@ -3,6 +3,7 @@
*/ */
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { createContext, useContext } from '@wordpress/element'; import { createContext, useContext } from '@wordpress/element';
import classnames from 'classnames';
/** /**
* This context is a configuration object used for connecting * This context is a configuration object used for connecting
@ -13,7 +14,8 @@ import { createContext, useContext } from '@wordpress/element';
*/ */
const InnerBlockLayoutContext = createContext( { const InnerBlockLayoutContext = createContext( {
parentName: '', parentName: '',
layoutStyleClassPrefix: '', parentClassName: '',
isLoading: false,
} ); } );
export const useInnerBlockLayoutContext = () => export const useInnerBlockLayoutContext = () =>
@ -21,17 +23,25 @@ export const useInnerBlockLayoutContext = () =>
export const InnerBlockLayoutContextProvider = ( { export const InnerBlockLayoutContextProvider = ( {
parentName = '', parentName = '',
layoutStyleClassPrefix = '', parentClassName = '',
isLoading = false,
children, children,
} ) => { } ) => {
const contextValue = { const contextValue = {
parentName, parentName,
layoutStyleClassPrefix, parentClassName,
isLoading,
}; };
return ( return (
<InnerBlockLayoutContext.Provider value={ contextValue }> <InnerBlockLayoutContext.Provider value={ contextValue }>
{ children } <div
className={ classnames( 'wc-block-layout', {
'wc-block-layout--is-loading': isLoading,
} ) }
>
{ children }
</div>
</InnerBlockLayoutContext.Provider> </InnerBlockLayoutContext.Provider>
); );
}; };
@ -39,5 +49,5 @@ export const InnerBlockLayoutContextProvider = ( {
InnerBlockLayoutContextProvider.propTypes = { InnerBlockLayoutContextProvider.propTypes = {
children: PropTypes.node, children: PropTypes.node,
parentName: PropTypes.string, parentName: PropTypes.string,
layoutStyleClassPrefix: PropTypes.string, parentClassName: PropTypes.string,
}; };

View File

@ -7581,6 +7581,11 @@
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
}, },
"@types/domhandler": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@types/domhandler/-/domhandler-2.4.1.tgz",
"integrity": "sha512-cfBw6q6tT5sa1gSPFSRKzF/xxYrrmeiut7E0TxNBObiLSBTuFEHibcfEe3waQPEDbqBsq+ql/TOniw65EyDFMA=="
},
"@types/events": { "@types/events": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
@ -15823,7 +15828,6 @@
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
"integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
"dev": true,
"requires": { "requires": {
"domelementtype": "^2.0.1", "domelementtype": "^2.0.1",
"entities": "^2.0.0" "entities": "^2.0.0"
@ -15832,14 +15836,12 @@
"domelementtype": { "domelementtype": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
"integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==", "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ=="
"dev": true
}, },
"entities": { "entities": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.2.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.2.tgz",
"integrity": "sha512-dmD3AvJQBUjKpcNkoqr+x+IF0SdRtPz9Vk0uTy4yWqga9ibB6s4v++QFWNohjiUGoMlF552ZvNyXDxz5iW0qmw==", "integrity": "sha512-dmD3AvJQBUjKpcNkoqr+x+IF0SdRtPz9Vk0uTy4yWqga9ibB6s4v++QFWNohjiUGoMlF552ZvNyXDxz5iW0qmw=="
"dev": true
} }
} }
}, },
@ -15858,8 +15860,7 @@
"domelementtype": { "domelementtype": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="
"dev": true
}, },
"domexception": { "domexception": {
"version": "2.0.1", "version": "2.0.1",
@ -15880,7 +15881,6 @@
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
"integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
"dev": true,
"requires": { "requires": {
"domelementtype": "1" "domelementtype": "1"
} }
@ -15889,7 +15889,6 @@
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
"dev": true,
"requires": { "requires": {
"dom-serializer": "0", "dom-serializer": "0",
"domelementtype": "1" "domelementtype": "1"
@ -16119,8 +16118,7 @@
"entities": { "entities": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
"dev": true
}, },
"enzyme": { "enzyme": {
"version": "3.11.0", "version": "3.11.0",
@ -19127,6 +19125,16 @@
"integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==",
"dev": true "dev": true
}, },
"html-dom-parser": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-0.2.3.tgz",
"integrity": "sha512-GdzE63/U0IQEvcpAz0cUdYx2zQx0Ai+HWvE9TXEgwP27+SymUzKa7iB4DhjYpf2IdNLfTTOBuMS5nxeWOosSMQ==",
"requires": {
"@types/domhandler": "2.4.1",
"domhandler": "2.4.2",
"htmlparser2": "3.10.1"
}
},
"html-element-map": { "html-element-map": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.2.0.tgz", "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.2.0.tgz",
@ -19171,6 +19179,17 @@
"terser": "^4.6.3" "terser": "^4.6.3"
} }
}, },
"html-react-parser": {
"version": "0.10.5",
"resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-0.10.5.tgz",
"integrity": "sha512-rtMWZ7KZjd9sO8jyIX7am0vGCIL45tCmctTnassT/BrTGeTaAZ4nQyqoGcx2v+vB8CAY+Q5PZiiV6eOiowq8dQ==",
"requires": {
"@types/domhandler": "2.4.1",
"html-dom-parser": "0.2.3",
"react-property": "1.0.1",
"style-to-object": "0.3.0"
}
},
"html-tags": { "html-tags": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz",
@ -19236,7 +19255,6 @@
"version": "3.10.1", "version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
"integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
"dev": true,
"requires": { "requires": {
"domelementtype": "^1.3.1", "domelementtype": "^1.3.1",
"domhandler": "^2.3.0", "domhandler": "^2.3.0",
@ -19250,7 +19268,6 @@
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": { "requires": {
"inherits": "^2.0.3", "inherits": "^2.0.3",
"string_decoder": "^1.1.1", "string_decoder": "^1.1.1",
@ -19660,8 +19677,7 @@
"inherits": { "inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
"dev": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -19672,8 +19688,7 @@
"inline-style-parser": { "inline-style-parser": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
"integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==", "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="
"dev": true
}, },
"inquirer": { "inquirer": {
"version": "7.1.0", "version": "7.1.0",
@ -31356,6 +31371,11 @@
"prop-types": "^15.5.8" "prop-types": "^15.5.8"
} }
}, },
"react-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/react-property/-/react-property-1.0.1.tgz",
"integrity": "sha512-1tKOwxFn3dXVomH6pM9IkLkq2Y8oh+fh/lYW3MJ/B03URswUTqttgckOlbxY2XHF3vPG6uanSc4dVsLW/wk3wQ=="
},
"react-redux": { "react-redux": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.0.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.0.tgz",
@ -34555,7 +34575,6 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": { "requires": {
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
}, },
@ -34563,8 +34582,7 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
"dev": true
} }
} }
}, },
@ -34672,7 +34690,6 @@
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz",
"integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==", "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==",
"dev": true,
"requires": { "requires": {
"inline-style-parser": "0.1.1" "inline-style-parser": "0.1.1"
} }
@ -36559,8 +36576,7 @@
"util-deprecate": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
"dev": true
}, },
"util.promisify": { "util.promisify": {
"version": "1.0.1", "version": "1.0.1",
@ -37877,7 +37893,7 @@
} }
}, },
"woocommerce": { "woocommerce": {
"version": "git+https://github.com/woocommerce/woocommerce.git#5213b05d57b0e0e703eaed3519694fa98a51516f", "version": "git+https://github.com/woocommerce/woocommerce.git#5a746a0775d8407127e314ffde73d5ebb15284bf",
"from": "git+https://github.com/woocommerce/woocommerce.git#release/4.1", "from": "git+https://github.com/woocommerce/woocommerce.git#release/4.1",
"dev": true "dev": true
}, },

View File

@ -13,7 +13,7 @@
"sideEffects": [ "sideEffects": [
"*.css", "*.css",
"*.scss", "*.scss",
"./assets/js/atomic/blocks/product/**", "./assets/js/atomic/blocks/**",
"./assets/js/filters/**", "./assets/js/filters/**",
"./assets/js/settings/blocks/**", "./assets/js/settings/blocks/**",
"./assets/js/middleware/**" "./assets/js/middleware/**"
@ -165,6 +165,7 @@
"config": "3.3.1", "config": "3.3.1",
"dinero.js": "1.8.1", "dinero.js": "1.8.1",
"downshift": "4.1.0", "downshift": "4.1.0",
"html-react-parser": "^0.10.5",
"jest-environment-jsdom-sixteen": "1.0.3", "jest-environment-jsdom-sixteen": "1.0.3",
"react-number-format": "4.4.1", "react-number-format": "4.4.1",
"reakit": "1.0.2", "reakit": "1.0.2",

View File

@ -0,0 +1,50 @@
<?php
/**
* Atomic blocks.
*
* @package WooCommerce/Blocks
*/
namespace Automattic\WooCommerce\Blocks\BlockTypes;
defined( 'ABSPATH' ) || exit;
/**
* AtomicBlock class.
*/
class AtomicBlock extends AbstractBlock {
/**
* Inject attributes and block name.
*
* @param array|\WP_Block $attributes Block attributes, or an instance of a WP_Block. Defaults to an empty array.
* @param string $content Block content. Default empty string.
* @return string Rendered block type output.
*/
public function render( $attributes = [], $content = '' ) {
$block_attributes = is_a( $attributes, '\WP_Block' ) ? $attributes->attributes : $attributes;
return $this->inject_html_data_attributes( $content, $block_attributes );
}
/**
* Registers the block type with WordPress.
*/
public function register_block_type() {
register_block_type(
$this->namespace . '/' . $this->block_name,
array(
'render_callback' => array( $this, 'render' ),
)
);
}
/**
* Converts block attributes to HTML data attributes.
*
* @param array $attributes Key value pairs of attributes.
* @return string Rendered HTML attributes.
*/
protected function get_html_data_attributes( array $attributes ) {
$data = parent::get_html_data_attributes( $attributes );
return trim( $data . ' data-block-name="' . esc_attr( $this->namespace . '/' . $this->block_name ) . '"' );
}
}

View File

@ -172,8 +172,8 @@ class Checkout extends AbstractBlock {
return ' return '
<div class="wc-block-skeleton wc-block-sidebar-layout wc-block-checkout wc-block-checkout--is-loading wc-block-checkout--skeleton hidden" aria-hidden="true"> <div class="wc-block-skeleton wc-block-sidebar-layout wc-block-checkout wc-block-checkout--is-loading wc-block-checkout--skeleton hidden" aria-hidden="true">
<div class="wc-block-main wc-block-checkout__main"> <div class="wc-block-main wc-block-checkout__main">
<div class="wc-block-component-express-checkout"></div> <div class="wc-block-components-express-checkout"></div>
<div class="wc-block-component-express-checkout-continue-rule"><span></span></div> <div class="wc-block-components-express-checkout-continue-rule"><span></span></div>
<form class="wc-block-checkout-form"> <form class="wc-block-checkout-form">
<fieldset class="wc-block-checkout__contact-fields wc-block-checkout-step"> <fieldset class="wc-block-checkout__contact-fields wc-block-checkout-step">
<span></span> <span></span>

View File

@ -83,6 +83,26 @@ class Library {
$instance = new $class(); $instance = new $class();
$instance->register_block_type(); $instance->register_block_type();
} }
self::register_atomic_blocks();
}
/**
* Register atomic blocks on the PHP side.
*/
protected static function register_atomic_blocks() {
$atomic_blocks = [
'product-title',
'product-button',
'product-image',
'product-price',
'product-rating',
'product-sale-badge',
'product-summary',
];
foreach ( $atomic_blocks as $atomic_block ) {
$instance = new \Automattic\WooCommerce\Blocks\BlockTypes\AtomicBlock( $atomic_block );
$instance->register_block_type();
}
} }
/** /**