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 {
@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,
} from '@woocommerce/shared-context';
/**
* Internal dependencies
*/
import './style.scss';
/**
* Product Button Block Component.
*
@ -22,22 +27,18 @@ import {
* this is not provided.
* @return {*} The component.
*/
const ProductButton = ( { className, ...props } ) => {
const Block = ( { className, ...props } ) => {
const { parentClassName } = useInnerBlockLayoutContext();
const productDataContext = useProductDataContext();
const product = props.product || productDataContext.product;
const { layoutStyleClassPrefix } = useInnerBlockLayoutContext();
const componentClass = `${ layoutStyleClassPrefix }__product-add-to-cart`;
return (
<div
className={ classnames(
className,
componentClass,
'wp-block-button',
{
'is-loading': ! product,
}
'wc-block-components-product-button',
`${ parentClassName }__product-add-to-cart`
) }
>
{ product ? (
@ -61,12 +62,7 @@ const AddToCartButton = ( { product } ) => {
is_in_stock: isInStock,
} = product;
const {
cartQuantity,
addingToCart,
cartIsLoading,
addToCart,
} = useStoreAddToCart( id );
const { cartQuantity, addingToCart, addToCart } = useStoreAddToCart( id );
useEffect( () => {
// Avoid running on first mount when cart quantity is first set.
@ -77,10 +73,6 @@ const AddToCartButton = ( { product } ) => {
triggerFragmentRefresh();
}, [ cartQuantity ] );
if ( cartIsLoading ) {
return <AddToCartButtonPlaceholder />;
}
const addedToCart = Number.isFinite( cartQuantity ) && cartQuantity > 0;
const allowAddToCart = ! hasOptions && isPurchasable && isInStock;
const buttonAriaLabel = decodeEntities(
@ -102,42 +94,33 @@ const AddToCartButton = ( { product } ) => {
__( 'Add to cart', 'woo-gutenberg-products-block' )
);
const ButtonTag = allowAddToCart ? 'button' : 'a';
const buttonProps = {};
if ( ! allowAddToCart ) {
return (
<a
href={ permalink }
aria-label={ buttonAriaLabel }
className={ classnames(
'wp-block-button__link',
'add_to_cart_button',
{
loading: addingToCart,
added: addedToCart,
}
) }
rel="nofollow"
>
{ buttonText }
</a>
);
buttonProps.href = permalink;
buttonProps.rel = 'nofollow';
} else {
buttonProps.onClick = addToCart;
}
return (
<button
onClick={ addToCart }
<ButtonTag
aria-label={ buttonAriaLabel }
className={ classnames(
'wp-block-button__link',
'add_to_cart_button',
'wc-block-components-product-button__button',
{
loading: addingToCart,
added: addedToCart,
}
) }
disabled={ addingToCart }
{ ...buttonProps }
>
{ buttonText }
</button>
</ButtonTag>
);
};
@ -146,16 +129,18 @@ const AddToCartButtonPlaceholder = () => {
<button
className={ classnames(
'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 }
/>
);
};
ProductButton.propTypes = {
Block.propTypes = {
className: PropTypes.string,
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
*/
import ProductSaleBadge from '../sale-badge/block.js';
import ProductSaleBadge from './../sale-badge/block';
import './style.scss';
/**
* Product Image Block Component.
@ -27,19 +28,16 @@ import ProductSaleBadge from '../sale-badge/block.js';
* this is not provided.
* @return {*} The component.
*/
const ProductImage = ( {
const Block = ( {
className,
productLink = true,
showSaleBadge = true,
showSaleBadge,
saleBadgeAlign = 'right',
...props
} ) => {
const { parentClassName } = useInnerBlockLayoutContext();
const productDataContext = useProductDataContext();
const product = props.product || productDataContext.product;
const { layoutStyleClassPrefix } = useInnerBlockLayoutContext();
const componentClass = `${ layoutStyleClassPrefix }__product-image`;
const [ imageLoaded, setImageLoaded ] = useState( false );
if ( ! product ) {
@ -47,11 +45,12 @@ const ProductImage = ( {
<div
className={ classnames(
className,
componentClass,
'is-loading'
'wc-block-components-product-image',
'wc-block-components-product-image--placeholder',
`${ parentClassName }__product-image`
) }
>
<ImagePlaceholder componentClass={ componentClass } />
<ImagePlaceholder />
</div>
);
}
@ -60,14 +59,22 @@ const ProductImage = ( {
product?.images && product.images.length ? product.images[ 0 ] : null;
return (
<div className={ classnames( className, componentClass ) }>
<div
className={ classnames(
className,
'wc-block-components-product-image',
`${ parentClassName }__product-image`
) }
>
{ productLink ? (
<a href={ product.permalink } rel="nofollow">
{ showSaleBadge && (
<ProductSaleBadge align={ saleBadgeAlign } />
{ !! showSaleBadge && (
<ProductSaleBadge
align={ saleBadgeAlign }
product={ product }
/>
) }
<Image
componentClass={ componentClass }
image={ image }
onLoad={ () => setImageLoaded( true ) }
loaded={ imageLoaded }
@ -75,11 +82,13 @@ const ProductImage = ( {
</a>
) : (
<>
{ showSaleBadge && (
<ProductSaleBadge align={ saleBadgeAlign } />
{ !! showSaleBadge && (
<ProductSaleBadge
align={ saleBadgeAlign }
product={ product }
/>
) }
<Image
componentClass={ componentClass }
image={ image }
onLoad={ () => setImageLoaded( true ) }
loaded={ imageLoaded }
@ -90,26 +99,16 @@ const ProductImage = ( {
);
};
const ImagePlaceholder = ( { componentClass } ) => {
return (
<img
className={ classnames(
`${ componentClass }__image`,
`${ componentClass }__image_placeholder`
) }
src={ PLACEHOLDER_IMG_SRC }
alt=""
/>
);
const ImagePlaceholder = () => {
return <img src={ PLACEHOLDER_IMG_SRC } alt="" />;
};
const Image = ( { componentClass, image, onLoad, loaded } ) => {
const Image = ( { image, onLoad, loaded } ) => {
const { thumbnail, srcset, sizes, alt } = image || {};
return (
<>
<img
className={ classnames( `${ componentClass }__image` ) }
src={ thumbnail }
srcSet={ srcset }
sizes={ sizes }
@ -117,14 +116,12 @@ const Image = ( { componentClass, image, onLoad, loaded } ) => {
onLoad={ onLoad }
hidden={ ! loaded }
/>
{ ! loaded && (
<ImagePlaceholder componentClass={ componentClass } />
) }
{ ! loaded && <ImagePlaceholder /> }
</>
);
};
ProductImage.propTypes = {
Block.propTypes = {
className: PropTypes.string,
product: PropTypes.object,
productLink: PropTypes.bool,
@ -132,4 +129,4 @@ ProductImage.propTypes = {
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,
} from '@woocommerce/shared-context';
/**
* Internal dependencies
*/
import './style.scss';
/**
* Product Price Block Component.
*
@ -19,21 +24,19 @@ import {
* this is not provided.
* @return {*} The component.
*/
const ProductPrice = ( { className, ...props } ) => {
const Block = ( { className, ...props } ) => {
const { parentClassName } = useInnerBlockLayoutContext();
const productDataContext = useProductDataContext();
const product = props.product || productDataContext.product;
const { layoutStyleClassPrefix } = useInnerBlockLayoutContext();
const componentClass = `${ layoutStyleClassPrefix }__product-price`;
if ( ! product ) {
return (
<div
className={ classnames(
className,
componentClass,
'price',
'is-loading'
'wc-block-components-product-price',
`${ parentClassName }__product-price`
) }
/>
);
@ -43,17 +46,22 @@ const ProductPrice = ( { className, ...props } ) => {
const currency = getCurrencyFromPriceResponse( prices );
return (
<div className={ classnames( className, componentClass, 'price' ) }>
<div
className={ classnames(
className,
'price',
'wc-block-components-product-price',
`${ parentClassName }__product-price`
) }
>
{ hasPriceRange( prices ) ? (
<PriceRange
componentClass={ componentClass }
currency={ currency }
minAmount={ prices.price_range.min_amount }
maxAmount={ prices.price_range.max_amount }
/>
) : (
<Price
componentClass={ componentClass }
currency={ currency }
price={ prices.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 (
<span className={ `${ componentClass }__value` }>
<span
className={ classnames(
'wc-block-components-product-price__value',
`${ parentClassName }__product-price__value`
) }
>
<FormattedMonetaryAmount
currency={ currency }
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 (
<>
{ regularPrice !== price && (
<del className={ `${ componentClass }__regular` }>
<del
className={ classnames(
'wc-block-components-product-price__regular',
`${ parentClassName }__product-price__regular`
) }
>
<FormattedMonetaryAmount
currency={ currency }
value={ regularPrice }
/>
</del>
) }
<span className={ `${ componentClass }__value` }>
<span
className={ classnames(
'wc-block-components-product-price__value',
`${ parentClassName }__product-price__value`
) }
>
<FormattedMonetaryAmount
currency={ currency }
value={ price }
@ -108,9 +135,9 @@ const Price = ( { componentClass, currency, price, regularPrice } ) => {
);
};
ProductPrice.propTypes = {
Block.propTypes = {
className: PropTypes.string,
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,
} from '@woocommerce/shared-context';
/**
* Internal dependencies
*/
import './style.scss';
/**
* Product Rating Block Component.
*
@ -18,13 +23,10 @@ import {
* this is not provided.
* @return {*} The component.
*/
const ProductRating = ( { className, ...props } ) => {
const Block = ( { className, ...props } ) => {
const { parentClassName } = useInnerBlockLayoutContext();
const productDataContext = useProductDataContext();
const product = props.product || productDataContext.product;
const { layoutStyleClassPrefix } = useInnerBlockLayoutContext();
const componentClass = `${ layoutStyleClassPrefix }__product-rating`;
const rating = getAverageRating( product );
if ( ! rating ) {
@ -42,10 +44,18 @@ const ProductRating = ( { className, ...props } ) => {
return (
<div
className={ classnames( className, componentClass, 'star-rating' ) }
className={ classnames(
className,
'star-rating',
'wc-block-components-product-rating',
`${ parentClassName }__product-rating`
) }
>
<div
className={ `${ componentClass }__stars` }
className={ classnames(
'wc-block-components-product-rating__stars',
`${ parentClassName }__product-rating__stars`
) }
role="img"
aria-label={ ratingText }
>
@ -62,9 +72,9 @@ const getAverageRating = ( product ) => {
return Number.isFinite( rating ) && rating > 0 ? rating : 0;
};
ProductRating.propTypes = {
Block.propTypes = {
className: PropTypes.string,
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,
} from '@woocommerce/shared-context';
/**
* Internal dependencies
*/
import './style.scss';
/**
* Product Sale Badge Block Component.
*
@ -20,27 +25,27 @@ import {
* this is not provided.
* @return {*} The component.
*/
const ProductSaleBadge = ( { className, align, ...props } ) => {
const Block = ( { className, align, ...props } ) => {
const { parentClassName } = useInnerBlockLayoutContext();
const productDataContext = useProductDataContext();
const product = props.product || productDataContext.product;
const { layoutStyleClassPrefix } = useInnerBlockLayoutContext();
const componentClass = `${ layoutStyleClassPrefix }__product-onsale`;
if ( ! product || ! product.on_sale ) {
return null;
}
const alignClass =
typeof align === 'string' ? `${ componentClass }--align${ align }` : '';
typeof align === 'string'
? `wc-block-components-product-sale-badge--align${ align }`
: '';
return (
<div
className={ classnames(
'wc-block-component__sale-badge',
'wc-block-components-product-sale-badge',
className,
alignClass,
componentClass
`${ parentClassName }__product-onsale`
) }
>
<Label
@ -54,10 +59,10 @@ const ProductSaleBadge = ( { className, align, ...props } ) => {
);
};
ProductSaleBadge.propTypes = {
Block.propTypes = {
className: PropTypes.string,
align: PropTypes.string,
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 { Icon, grid } from '@woocommerce/icons';
/**
* Internal dependencies
*/
import save from './save';
/**
* Holds default config for this collection of blocks.
*/
@ -18,5 +23,10 @@ export default {
html: false,
},
parent: [ 'woocommerce/all-products', 'woocommerce/single-product' ],
save() {},
save,
deprecated: [
{
save() {},
},
],
};

View File

@ -10,6 +10,11 @@ import {
useProductDataContext,
} from '@woocommerce/shared-context';
/**
* Internal dependencies
*/
import './style.scss';
/**
* Product Summary Block Component.
*
@ -19,20 +24,17 @@ import {
* this is not provided.
* @return {*} The component.
*/
const ProductSummary = ( { className, ...props } ) => {
const Block = ( { className, ...props } ) => {
const { parentClassName } = useInnerBlockLayoutContext();
const productDataContext = useProductDataContext();
const { product } = productDataContext || props;
const { layoutStyleClassPrefix } = useInnerBlockLayoutContext();
const componentClass = `${ layoutStyleClassPrefix }__product-summary`;
if ( ! product ) {
return (
<div
className={ classnames(
className,
componentClass,
'is-loading'
`wc-block-components-product-summary`
) }
/>
);
@ -50,7 +52,11 @@ const ProductSummary = ( { className, ...props } ) => {
return (
<Summary
className={ classnames( className, componentClass ) }
className={ classnames(
className,
`wc-block-components-product-summary`,
`${ parentClassName }__product-summary`
) }
source={ source }
maxLength={ 150 }
countType={ countType }
@ -58,9 +64,9 @@ const ProductSummary = ( { className, ...props } ) => {
);
};
ProductSummary.propTypes = {
Block.propTypes = {
className: PropTypes.string,
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,
} from '@woocommerce/shared-context';
/**
* Internal dependencies
*/
import './style.scss';
/**
* Product Title Block Component.
*
@ -20,18 +25,15 @@ import {
* this is not provided.
* @return {*} The component.
*/
const ProductTitle = ( {
export const Block = ( {
className,
headingLevel = 2,
productLink = true,
...props
} ) => {
const { parentClassName } = useInnerBlockLayoutContext();
const productDataContext = useProductDataContext();
const product = props.product || productDataContext.product;
const { layoutStyleClassPrefix } = useInnerBlockLayoutContext();
const componentClass = `${ layoutStyleClassPrefix }__product-title`;
const TagName = `h${ headingLevel }`;
if ( ! product ) {
@ -40,8 +42,8 @@ const ProductTitle = ( {
// @ts-ignore
className={ classnames(
className,
componentClass,
'is-loading'
'wc-block-components-product-title',
`${ parentClassName }__product-title`
) }
/>
);
@ -51,7 +53,13 @@ const ProductTitle = ( {
return (
// @ts-ignore
<TagName className={ classnames( className, componentClass ) }>
<TagName
className={ classnames(
className,
'wc-block-components-product-title',
`${ parentClassName }__product-title`
) }
>
{ productLink ? (
<a href={ product.permalink } rel="nofollow">
{ productName }
@ -63,11 +71,11 @@ const ProductTitle = ( {
);
};
ProductTitle.propTypes = {
Block.propTypes = {
className: PropTypes.string,
product: PropTypes.object,
headingLevel: PropTypes.number,
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>
<HeadingToolbar
isCollapsed={ false }
minLevel={ 2 }
minLevel={ 1 }
maxLevel={ 7 }
selectedLevel={ headingLevel }
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
*/
import {
ProductTitle,
ProductPrice,
ProductButton,
ProductImage,
ProductRating,
ProductSummary,
ProductSaleBadge,
} from '../blocks/product/block-components';
import ProductButton from '../blocks/product/button/block';
import ProductImage from '../blocks/product/image/frontend';
import ProductPrice from '../blocks/product/price/block';
import ProductRating from '../blocks/product/rating/block';
import ProductSaleBadge from '../blocks/product/sale-badge/block';
import ProductSummary from '../blocks/product/summary/block';
import ProductTitle from '../blocks/product/title/frontend';
/**
* 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.
*/

View File

@ -1,2 +1,4 @@
export * from './get-block-map.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 } ) => {
return (
<>
<div className="wc-block-component-express-checkout">
<div className="wc-block-components-express-checkout">
<Title
className="wc-block-component-express-checkout__title"
className="wc-block-components-express-checkout__title"
headingLevel="2"
>
{ __( 'Express checkout', 'woo-gutenberg-products-block' ) }
</Title>
<div className="wc-block-component-express-checkout__content">
<div className="wc-block-components-express-checkout__content">
<StoreNoticesProvider context="wc/express-payment-area">
{ children }
</StoreNoticesProvider>
</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' ) }
</div>
</>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -55,217 +55,14 @@
list-style: none;
}
// Extra specificity to avoid editor styles on linked images.
.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;
}
}
.theme-twentytwenty .wc-block-grid,
.wc-block-grid {
&.has-aligned-buttons {
.wc-block-grid__product {
display: flex;
flex-direction: column;
}
.wc-block-grid__product > .wc-block-grid__product-title:last-child,
.wc-block-grid__product > div:last-child {
.wc-block-grid__product > :last-child {
margin-top: auto;
margin-bottom: 0;
padding-bottom: $gap-small;
@ -288,7 +85,6 @@
}
}
// Responsive media styles.
@include breakpoint( "<480px" ) {
.wc-block-grid {
@for $i from 2 to 9 {
@ -305,11 +101,9 @@
}
}
}
.wc-block-grid__product-image img {
width: 100%;
}
}
}
@include breakpoint( "480px-600px" ) {
.wc-block-grid {
@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 './legacy-events';
export * from './render-frontend';
export * from './get-valid-block-attributes';

View File

@ -4,49 +4,18 @@
import { render } from 'react-dom';
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.
*
* @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 {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.getErrorBoundaryProps] Function to generate the props object for the error boundary.
*/
export const renderFrontend = ( {
selector,
Block,
selector,
getProps = () => {},
getErrorBoundaryProps = () => {},
} ) => {
@ -61,7 +30,6 @@ export const renderFrontend = ( {
...el.dataset,
...props.attributes,
};
el.classList.remove( 'is-loading' );
render(

View File

@ -11,7 +11,7 @@ import { CURRENT_USER_IS_ADMIN } from '@woocommerce/block-settings';
import { __experimentalCreateInterpolateElement } from 'wordpress-element';
import {
renderFrontend,
getAttributesFromDataset,
getValidBlockAttributes,
} from '@woocommerce/base-utils';
/**
@ -37,7 +37,7 @@ const CartFrontend = ( props ) => {
const getProps = ( el ) => {
return {
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 {
renderFrontend,
getAttributesFromDataset,
getValidBlockAttributes,
} from '@woocommerce/base-utils';
/**
@ -75,7 +75,7 @@ const CheckoutFrontend = ( props ) => {
const getProps = ( el ) => {
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;
align-items: center;
text-align: center;
@ -160,15 +160,15 @@
// Loading placeholder state.
.wc-block-checkout--is-loading {
.wc-block-component-express-checkout,
.wc-block-components-express-checkout,
.wc-block-checkout__actions button {
@include placeholder();
@include force-content();
}
.wc-block-component-express-checkout {
.wc-block-components-express-checkout {
min-height: 150px;
}
.wc-block-component-express-checkout-continue-rule span {
.wc-block-components-express-checkout-continue-rule span {
@include placeholder();
@include force-content();
width: 150px;

View File

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

View File

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

View File

@ -2,12 +2,33 @@
* External dependencies
*/
import { withProduct } from '@woocommerce/block-hocs';
import {
InnerBlockLayoutContextProvider,
ProductDataContextProvider,
} from '@woocommerce/shared-context';
/**
* Internal dependencies
*/
import { BLOCK_NAME } from './constants';
/**
* The Single Product Block.
*/
const Block = () => {
return null;
const Block = ( { isLoading, product, children } ) => {
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 );

View File

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

View File

@ -10,7 +10,6 @@ import {
ProductDataContextProvider,
} from '@woocommerce/shared-context';
import { createBlocksFromTemplate } from '@woocommerce/atomic-utils';
import classnames from 'classnames';
import { PanelBody, Button } from '@wordpress/components';
import { Icon, restore } from '@woocommerce/icons';
@ -26,7 +25,7 @@ import {
/**
* 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 { replaceInnerBlocks } = useDispatch( 'core/block-editor' );
@ -41,7 +40,8 @@ const LayoutEditor = ( { product, clientId, isLoading } ) => {
return (
<InnerBlockLayoutContextProvider
parentName={ BLOCK_NAME }
layoutStyleClassPrefix={ baseClassName }
parentClassName={ baseClassName }
isLoading={ isLoading }
>
<ProductDataContextProvider product={ product }>
<InspectorControls>
@ -66,11 +66,7 @@ const LayoutEditor = ( { product, clientId, isLoading } ) => {
</Button>
</PanelBody>
</InspectorControls>
<div
className={ classnames( baseClassName, {
'is-loading': isLoading,
} ) }
>
<div className={ baseClassName }>
<InnerBlocks
template={ DEFAULT_INNER_BLOCKS }
allowedBlocks={ ALLOWED_INNER_BLOCKS }

View File

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

View File

@ -1,6 +1,6 @@
// 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.
// https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/1204
.wc-block-component-title {
.wc-block-components-title {
background-color: transparent;
}

View File

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

View File

@ -3,6 +3,7 @@
*/
import PropTypes from 'prop-types';
import { createContext, useContext } from '@wordpress/element';
import classnames from 'classnames';
/**
* This context is a configuration object used for connecting
@ -13,7 +14,8 @@ import { createContext, useContext } from '@wordpress/element';
*/
const InnerBlockLayoutContext = createContext( {
parentName: '',
layoutStyleClassPrefix: '',
parentClassName: '',
isLoading: false,
} );
export const useInnerBlockLayoutContext = () =>
@ -21,17 +23,25 @@ export const useInnerBlockLayoutContext = () =>
export const InnerBlockLayoutContextProvider = ( {
parentName = '',
layoutStyleClassPrefix = '',
parentClassName = '',
isLoading = false,
children,
} ) => {
const contextValue = {
parentName,
layoutStyleClassPrefix,
parentClassName,
isLoading,
};
return (
<InnerBlockLayoutContext.Provider value={ contextValue }>
{ children }
<div
className={ classnames( 'wc-block-layout', {
'wc-block-layout--is-loading': isLoading,
} ) }
>
{ children }
</div>
</InnerBlockLayoutContext.Provider>
);
};
@ -39,5 +49,5 @@ export const InnerBlockLayoutContextProvider = ( {
InnerBlockLayoutContextProvider.propTypes = {
children: PropTypes.node,
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",
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
@ -15823,7 +15828,6 @@
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
"integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
"dev": true,
"requires": {
"domelementtype": "^2.0.1",
"entities": "^2.0.0"
@ -15832,14 +15836,12 @@
"domelementtype": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
"integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ==",
"dev": true
"integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ=="
},
"entities": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.2.tgz",
"integrity": "sha512-dmD3AvJQBUjKpcNkoqr+x+IF0SdRtPz9Vk0uTy4yWqga9ibB6s4v++QFWNohjiUGoMlF552ZvNyXDxz5iW0qmw==",
"dev": true
"integrity": "sha512-dmD3AvJQBUjKpcNkoqr+x+IF0SdRtPz9Vk0uTy4yWqga9ibB6s4v++QFWNohjiUGoMlF552ZvNyXDxz5iW0qmw=="
}
}
},
@ -15858,8 +15860,7 @@
"domelementtype": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
"dev": true
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="
},
"domexception": {
"version": "2.0.1",
@ -15880,7 +15881,6 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
"integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
"dev": true,
"requires": {
"domelementtype": "1"
}
@ -15889,7 +15889,6 @@
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
"dev": true,
"requires": {
"dom-serializer": "0",
"domelementtype": "1"
@ -16119,8 +16118,7 @@
"entities": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
"dev": true
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
},
"enzyme": {
"version": "3.11.0",
@ -19127,6 +19125,16 @@
"integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==",
"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": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.2.0.tgz",
@ -19171,6 +19179,17 @@
"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": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz",
@ -19236,7 +19255,6 @@
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
"integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
"dev": true,
"requires": {
"domelementtype": "^1.3.1",
"domhandler": "^2.3.0",
@ -19250,7 +19268,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@ -19660,8 +19677,7 @@
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ini": {
"version": "1.3.5",
@ -19672,8 +19688,7 @@
"inline-style-parser": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
"integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==",
"dev": true
"integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="
},
"inquirer": {
"version": "7.1.0",
@ -31356,6 +31371,11 @@
"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": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.0.tgz",
@ -34555,7 +34575,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.0"
},
@ -34563,8 +34582,7 @@
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}
}
},
@ -34672,7 +34690,6 @@
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz",
"integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==",
"dev": true,
"requires": {
"inline-style-parser": "0.1.1"
}
@ -36559,8 +36576,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"util.promisify": {
"version": "1.0.1",
@ -37877,7 +37893,7 @@
}
},
"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",
"dev": true
},

View File

@ -13,7 +13,7 @@
"sideEffects": [
"*.css",
"*.scss",
"./assets/js/atomic/blocks/product/**",
"./assets/js/atomic/blocks/**",
"./assets/js/filters/**",
"./assets/js/settings/blocks/**",
"./assets/js/middleware/**"
@ -165,6 +165,7 @@
"config": "3.3.1",
"dinero.js": "1.8.1",
"downshift": "4.1.0",
"html-react-parser": "^0.10.5",
"jest-environment-jsdom-sixteen": "1.0.3",
"react-number-format": "4.4.1",
"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 '
<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-component-express-checkout"></div>
<div class="wc-block-component-express-checkout-continue-rule"><span></span></div>
<div class="wc-block-components-express-checkout"></div>
<div class="wc-block-components-express-checkout-continue-rule"><span></span></div>
<form class="wc-block-checkout-form">
<fieldset class="wc-block-checkout__contact-fields wc-block-checkout-step">
<span></span>

View File

@ -83,6 +83,26 @@ class Library {
$instance = new $class();
$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();
}
}
/**