* Add classnames package

* Add background image & initial styles

* Only show inspector controls when not in edit-mode

* Add overlay color + opacity controls

* Add content alignment

* Fix display of content

* Add content toggles

* Update styles from feedback

* Display the "Featured Product" block on the frontend (https://github.com/woocommerce/woocommerce-blocks/pull/310)

* Add render_callback to dynamically render product

* Set a size for the product description

* Remove fallback placeholder image

* Reset the background color for the placeholder component

Previously this was overriding storefront's custom background color CSS

* Update nested conditional

* Remove unnecessary style

* "Featured Product" Block: Add link to the product to the block (https://github.com/woocommerce/woocommerce-blocks/pull/311)

* Featured Product Block: Add link to the product to the block

* Add a better link label for screen reader users

* Match core button styles, fix alignment of button-link
This commit is contained in:
Kelly Dwan 2019-01-13 12:22:15 -05:00 committed by GitHub
parent dbb04d5f1a
commit 0d720183aa
8 changed files with 504 additions and 30 deletions

View File

@ -5,3 +5,15 @@ $gap: 16px;
$gap-small: 12px;
$gap-smaller: 8px;
$gap-smallest: 4px;
// Variables pulled from Gutenberg.
// Editor Widths
$sidebar-width: 280px;
$content-width: 610px; // For the visual width, subtract 30px (2 * $block-padding + 2px borders). This comes to 580px, which is optimized for 70 characters.
// Blocks
$block-padding: 14px; // Space between block footprint and focus boundaries. These are drawn outside the block footprint, and do not affect the size.
$block-spacing: 4px; // Vertical space between blocks.
$block-side-ui-width: 28px; // Width of the movers/drag handle UI.
$block-side-ui-clearance: 2px; // Space between movers/drag handle UI, and block.
$block-container-side-padding: $block-side-ui-width + $block-padding + 2 * $block-side-ui-clearance; // Total space left and right of the block footprint.

View File

@ -3,16 +3,27 @@
*/
import { __ } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import { BlockControls, InspectorControls } from '@wordpress/editor';
import {
AlignmentToolbar,
BlockControls,
InspectorControls,
PanelColorSettings,
RichText,
withColors,
} from '@wordpress/editor';
import {
Button,
PanelBody,
Placeholder,
RangeControl,
Spinner,
ToggleControl,
Toolbar,
withSpokenMessages,
} from '@wordpress/components';
import classnames from 'classnames';
import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { debounce } from 'lodash';
import PropTypes from 'prop-types';
@ -21,7 +32,21 @@ import PropTypes from 'prop-types';
*/
import { IconStar } from '../../components/icons';
import ProductControl from '../../components/product-control';
import ProductPreview from '../../components/product-preview';
// Copied from core/cover, updated for product.
function backgroundImageStyles( { images = [] } ) {
if ( images.length ) {
const url = images[ 0 ].src;
return { backgroundImage: `url(${ url })` };
}
return {};
}
function dimRatioToClass( ratio ) {
return ratio === 0 || ratio === 50 ?
null :
`has-background-dim-${ 10 * Math.round( ratio / 10 ) }`;
}
/**
* Component to handle edit mode of "Featured Product".
@ -66,7 +91,12 @@ class FeaturedProduct extends Component {
}
getInspectorControls() {
const { attributes, setAttributes } = this.props;
const {
attributes,
setAttributes,
overlayColor,
setOverlayColor,
} = this.props;
return (
<InspectorControls key="inspector">
@ -82,6 +112,37 @@ class FeaturedProduct extends Component {
} }
/>
</PanelBody>
<PanelBody title={ __( 'Content', 'woo-gutenberg-products-block' ) }>
<ToggleControl
label="Show description"
checked={ attributes.showDesc }
onChange={ () => setAttributes( { showDesc: ! attributes.showDesc } ) }
/>
<ToggleControl
label="Show price"
checked={ attributes.showPrice }
onChange={ () => setAttributes( { showPrice: ! attributes.showPrice } ) }
/>
</PanelBody>
<PanelColorSettings
title={ __( 'Overlay', 'woo-gutenberg-products-block' ) }
colorSettings={ [
{
value: overlayColor.color,
onChange: setOverlayColor,
label: __( 'Overlay Color', 'woo-gutenberg-products-block' ),
},
] }
>
<RangeControl
label={ __( 'Background Opacity', 'woo-gutenberg-products-block' ) }
value={ attributes.dimRatio }
onChange={ ( ratio ) => setAttributes( { dimRatio: ratio } ) }
min={ 0 }
max={ 100 }
step={ 10 }
/>
</PanelColorSettings>
</InspectorControls>
);
}
@ -125,21 +186,41 @@ class FeaturedProduct extends Component {
}
render() {
const { attributes, setAttributes } = this.props;
const { editMode } = attributes;
const { attributes, setAttributes, overlayColor } = this.props;
const {
contentAlign,
dimRatio,
editMode,
linkText,
showDesc,
showPrice,
} = attributes;
const { loaded, product } = this.state;
const classes = [ 'wc-block-featured-product' ];
if ( ! product ) {
if ( ! loaded ) {
classes.push( 'is-loading' );
} else {
classes.push( 'is-not-found' );
}
const classes = classnames(
'wc-block-featured-product',
{
'is-loading': ! product && ! loaded,
'is-not-found': ! product && loaded,
'has-background-dim': dimRatio !== 0,
},
dimRatioToClass( dimRatio ),
contentAlign !== 'center' && `has-${ contentAlign }-content`
);
const style = !! product ? backgroundImageStyles( product ) : {};
if ( overlayColor.color ) {
style.backgroundColor = overlayColor.color;
}
return (
<Fragment>
<BlockControls>
<AlignmentToolbar
value={ contentAlign }
onChange={ ( nextAlign ) => {
setAttributes( { contentAlign: nextAlign } );
} }
/>
<Toolbar
controls={ [
{
@ -151,15 +232,44 @@ class FeaturedProduct extends Component {
] }
/>
</BlockControls>
{ this.getInspectorControls() }
{ ! attributes.editMode && this.getInspectorControls() }
{ editMode ? (
this.renderEditMode()
) : (
<div className={ classes.join( ' ' ) }>
<Fragment>
{ !! product ? (
<ProductPreview product={ product } key={ product.id } />
<div className={ classes } style={ style }>
<h2 className="wc-block-featured-product__title">
{ product.name }
</h2>
{ showDesc && (
<div
className="wc-block-featured-product__description"
dangerouslySetInnerHTML={ {
__html:
'<p>Black cotton top with matching striped skirt. </p>\n',
} }
/>
) }
{ showPrice && (
<div
className="wc-block-featured-product__price"
dangerouslySetInnerHTML={ { __html: product.price_html } }
/>
) }
<div className="wc-block-featured-product__link wp-block-button">
<RichText
value={ linkText }
onChange={ ( value ) => setAttributes( { linkText: value } ) }
formattingControls={ [ 'bold', 'italic', 'strikethrough' ] }
className="wp-block-button__link"
keepPlaceholderOnFocus
/>
</div>
</div>
) : (
<Placeholder
className="wc-block-featured-product"
icon={ <IconStar /> }
label={ __( 'Featured Product', 'woo-gutenberg-products-block' ) }
>
@ -170,7 +280,7 @@ class FeaturedProduct extends Component {
) }
</Placeholder>
) }
</div>
</Fragment>
) }
</Fragment>
);
@ -190,8 +300,14 @@ FeaturedProduct.propTypes = {
* A callback to update attributes
*/
setAttributes: PropTypes.func.isRequired,
// from withColors
overlayColor: PropTypes.object,
setOverlayColor: PropTypes.func.isRequired,
// from withSpokenMessages
debouncedSpeak: PropTypes.func.isRequired,
};
export default withSpokenMessages( FeaturedProduct );
export default compose( [
withColors( { overlayColor: 'background-color' } ),
withSpokenMessages,
] )( FeaturedProduct );

View File

@ -7,6 +7,7 @@ import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import './style.scss';
import Block from './block';
import { IconStar } from '../../components/icons';
@ -26,6 +27,22 @@ registerBlockType( 'woocommerce/featured-product', {
align: [ 'wide', 'full' ],
},
attributes: {
/**
* Alignment of content inside block.
*/
contentAlign: {
type: 'string',
default: 'center',
},
/**
* Percentage opacity of overlay.
*/
dimRatio: {
type: 'number',
default: 50,
},
/**
* Toggle for edit mode in the block preview.
*/
@ -35,11 +52,49 @@ registerBlockType( 'woocommerce/featured-product', {
},
/**
* The product ID to display
* The overlay color, from the color list.
*/
overlayColor: {
type: 'string',
},
/**
* The overlay color, if a custom color value.
*/
customOverlayColor: {
type: 'string',
},
/**
* Text for the product link.
*/
linkText: {
type: 'string',
default: __( 'Shop now', 'woo-gutenberg-products-block' ),
},
/**
* The product ID to display.
*/
productId: {
type: 'number',
},
/**
* Show the product description.
*/
showDesc: {
type: 'boolean',
default: true,
},
/**
* Show the product price.
*/
showPrice: {
type: 'boolean',
default: true,
},
},
/**

View File

@ -0,0 +1,124 @@
.wc-block-featured-product {
position: relative;
background-color: $black;
background-size: cover;
background-position: center center;
min-height: 430px;
width: 100%;
margin: 0 0 1.5em 0;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
flex-wrap: wrap;
align-content: center;
&.components-placeholder {
// Reset the background for the placeholders.
background-color: rgba( 139, 139, 150, .1 );
}
&.has-left-content {
justify-content: flex-start;
.wc-block-featured-product__title,
.wc-block-featured-product__description,
.wc-block-featured-product__price,
.wc-block-featured-product__link {
margin-left: 0;
text-align: left;
}
}
&.has-right-content {
justify-content: flex-end;
.wc-block-featured-product__title,
.wc-block-featured-product__description,
.wc-block-featured-product__price,
.wc-block-featured-product__link {
margin-right: 0;
text-align: right;
}
}
.wc-block-featured-product__title,
.wc-block-featured-product__description,
.wc-block-featured-product__price,
.wc-block-featured-product__link {
color: $white;
line-height: 1.25;
z-index: 1;
margin-bottom: 0;
width: 100%;
padding: 0 48px 16px 48px;
text-align: center;
a,
a:hover,
a:focus,
a:active {
color: $white;
}
}
.wc-block-featured-product__title {
margin-top: 0;
&:before {
display: none;
}
}
.wc-block-featured-product__description {
p {
margin: 0;
}
}
&.has-background-dim::before {
content: "";
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: inherit;
opacity: 0.5;
z-index: 1;
}
@for $i from 1 through 10 {
&.has-background-dim.has-background-dim-#{ $i * 10 }::before {
opacity: $i * 0.1;
}
}
// Apply max-width to floated items that have no intrinsic width
&.alignleft,
&.alignright {
max-width: $content-width / 2;
width: 100%;
}
// Using flexbox without an assigned height property breaks vertical center alignment in IE11.
// Appending an empty ::after element tricks IE11 into giving the cover image an implicit height, which sidesteps this issue.
&::after {
display: block;
content: "";
font-size: 0;
min-height: inherit;
// IE doesn't support flex so omit that.
@supports (position: sticky) {
content: none;
}
}
// Aligned cover blocks should not use our global alignment rules
&.aligncenter,
&.alignleft,
&.alignright {
display: flex;
}
}

View File

@ -0,0 +1,163 @@
<?php
/**
* Display the featured product block in the content.
*
* @package WooCommerce\Blocks
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Wrapper class for Featured Product callback.
*/
class WC_Block_Featured_Product {
/**
* Block name.
*
* @var string
*/
protected static $block_name = 'featured-product';
/**
* Default attribute values, should match what's set in JS `registerBlockType`.
*
* @var array
*/
protected static $defaults = array(
'align' => 'none',
'contentAlign' => 'center',
'dimRatio' => 50,
'linkText' => false,
'showDesc' => true,
'showPrice' => true,
);
/**
* Render the Featured Product block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @return string Rendered block type output.
*/
public static function render( $attributes, $content ) {
$id = (int) $attributes['productId'];
$product = wc_get_product( $id );
if ( ! $product ) {
return '';
}
$attributes = wp_parse_args( $attributes, self::$defaults );
if ( ! $attributes['linkText'] ) {
$attributes['linkText'] = __( 'Shop now', 'woo-gutenberg-products-block' );
}
$title = sprintf(
'<h2 class="wc-block-featured-product__title">%s</h2>',
esc_html( $product->get_title() )
);
$desc_str = sprintf(
'<div class="wc-block-featured-product__description">%s</div>',
apply_filters( 'woocommerce_short_description', $product->get_short_description() )
);
$price_str = sprintf(
'<div class="wc-block-featured-product__price">%s</div>',
$product->get_price_html()
);
$link_str = sprintf(
'<div class="wc-block-featured-product__link wp-block-button"><a class="wp-block-button__link" href="%1$s" aria-label="%2$s">%3$s</a></div>',
$product->get_permalink(),
/* translators: %s is product name */
sprintf( __( 'View product %s', 'woo-gutenberg-products-block' ), $product->get_name() ),
$attributes['linkText']
);
$output = sprintf( '<div class="%1$s" style="%2$s">', self::get_classes( $attributes ), self::get_styles( $attributes, $product ) );
$output .= $title;
if ( $attributes['showDesc'] ) {
$output .= $desc_str;
}
if ( $attributes['showPrice'] ) {
$output .= $price_str;
}
$output .= $link_str;
$output .= '</div>';
return $output;
}
/**
* Get the styles for the wrapper element (background image, color).
*
* @param array $attributes Block attributes. Default empty array.
* @param WC_Product $product Product object.
* @return string
*/
public static function get_styles( $attributes, $product ) {
$image = self::get_image( $product, ( 'none' !== $attributes['align'] ) ? 'large' : 'full' );
$style = sprintf( 'background-image:url(%s);', esc_url( $image ) );
if ( isset( $attributes['customOverlayColor'] ) ) {
$style .= sprintf( 'background-color:%s;', esc_attr( $attributes['customOverlayColor'] ) );
}
return $style;
}
/**
* Get class names for the block container.
*
* @param array $attributes Block attributes. Default empty array.
* @return string
*/
public static function get_classes( $attributes ) {
$classes = array( 'wc-block-' . self::$block_name );
if ( isset( $attributes['align'] ) ) {
$classes[] = "align{$attributes['align']}";
}
if ( isset( $attributes['dimRatio'] ) && ( 0 !== $attributes['dimRatio'] ) ) {
$classes[] = 'has-background-dim';
if ( 50 !== $attributes['dimRatio'] ) {
$classes[] = 'has-background-dim-' . 10 * round( $attributes['dimRatio'] / 10 );
}
}
if ( isset( $attributes['contentAlign'] ) && 'center' !== $attributes['contentAlign'] ) {
$classes[] = "has-{$attributes['contentAlign']}-content";
}
if ( isset( $attributes['overlayColor'] ) ) {
$classes[] = "has-{$attributes['overlayColor']}-background-color";
}
return implode( $classes, ' ' );
}
/**
* Returns the main product image URL.
*
* @param WC_Product $product Product object.
* @param string $size Image size, defaults to 'full'.
* @return string
*/
public static function get_image( $product, $size = 'full' ) {
$image = '';
if ( $product->get_image_id() ) {
$image = wp_get_attachment_image_url( $product->get_image_id(), $size );
} elseif ( $product->get_parent_id() ) {
$parent_product = wc_get_product( $product->get_parent_id() );
if ( $parent_product ) {
$image = wp_get_attachment_image_url( $parent_product->get_image_id(), $size );
}
}
return $image;
}
}

View File

@ -603,7 +603,7 @@
"dependencies": {
"jsesc": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
"dev": true
},
@ -769,7 +769,7 @@
"dependencies": {
"jsesc": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
"dev": true
},
@ -1014,7 +1014,7 @@
"dependencies": {
"jsesc": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
"dev": true
},
@ -2611,7 +2611,7 @@
"dependencies": {
"acorn-jsx": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz",
"resolved": "http://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz",
"integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=",
"dev": true,
"requires": {
@ -2620,7 +2620,7 @@
"dependencies": {
"acorn": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
"resolved": "http://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
"integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=",
"dev": true
}
@ -2669,7 +2669,7 @@
},
"eslint": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz",
"resolved": "http://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz",
"integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==",
"dev": true,
"requires": {
@ -2725,7 +2725,7 @@
},
"espree": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz",
"resolved": "http://registry.npmjs.org/espree/-/espree-3.5.4.tgz",
"integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==",
"dev": true,
"requires": {
@ -2735,7 +2735,7 @@
},
"external-editor": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz",
"resolved": "http://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz",
"integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==",
"dev": true,
"requires": {
@ -2746,7 +2746,7 @@
},
"fast-deep-equal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
"resolved": "http://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
"integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=",
"dev": true
},
@ -2786,7 +2786,7 @@
},
"regexpp": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz",
"resolved": "http://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz",
"integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==",
"dev": true
},

View File

@ -39,6 +39,7 @@
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "10.0.1",
"babel-loader": "8.0.5",
"classnames": "^2.2.6",
"clean-webpack-plugin": "1.0.0",
"core-js": "2.6.2",
"cross-env": "5.2.0",

View File

@ -77,6 +77,8 @@ function wgpb_add_block_category( $categories ) {
* Register the Products block and its scripts.
*/
function wgpb_register_blocks() {
include_once dirname( __FILE__ ) . '/includes/blocks/class-wc-block-featured-product.php';
// Legacy block.
register_block_type(
'woocommerce/products',
@ -131,8 +133,9 @@ function wgpb_register_blocks() {
register_block_type(
'woocommerce/featured-product',
array(
'editor_script' => 'wc-featured-product',
'editor_style' => 'wc-featured-product-editor',
'render_callback' => array( 'WC_Block_Featured_Product', 'render' ),
'editor_script' => 'wc-featured-product',
'style' => 'wc-featured-product-editor',
)
);
}