diff --git a/.eslintrc.js b/.eslintrc.js index f55c80679d1..98009a4eb13 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -14,6 +14,10 @@ module.exports = { ], rules: { '@wordpress/dependency-group': 'off', + 'camelcase': [ 'error', { + allow: [ 'wc_product_block_data' ], + properties: 'never', + } ], 'valid-jsdoc': 'off', } }; diff --git a/assets/css/abstracts/_mixins.scss b/assets/css/abstracts/_mixins.scss index 851aeac531a..749c94d94ba 100644 --- a/assets/css/abstracts/_mixins.scss +++ b/assets/css/abstracts/_mixins.scss @@ -15,6 +15,18 @@ } } +@keyframes loading-fade { + 0% { + opacity: 0.7; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.7; + } +} + // Adds animation to placeholder section @mixin placeholder( $lighten-percentage: 30% ) { animation: loading-fade 1.6s ease-in-out infinite; @@ -24,6 +36,10 @@ &::after { content: "\00a0"; } + + @media screen and (prefers-reduced-motion: reduce) { + animation: none; + } } // Adds animation to transforms diff --git a/assets/css/style.scss b/assets/css/style.scss index 38f66e7f3a6..3e6d04d616b 100644 --- a/assets/css/style.scss +++ b/assets/css/style.scss @@ -1,3 +1,60 @@ +.wc-block-form-button, +.wc-block-form-text-input, +input[type="text"].wc-block-form-text-input { + font-size: 1em; + border-radius: 0; + background: none; + text-decoration: none; + font-weight: normal; + text-shadow: none; + display: inline-block; + appearance: none; + + &:focus { + outline: 2px solid #9f6893; + } +} + +.wc-block-form-text-input, +input[type="text"].wc-block-form-text-input { + border-radius: 4px; + border: 1px solid #aaa; + background: #fff; + padding: 0.6180469716em; + color: #333; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.125); + + &:focus { + background: inherit; + } + &:disabled, + &[aria-disabled="true"] { + opacity: 0.3; + background-color: #eee; + } +} +.wc-block-form-button { + border: 1px solid #eee; + background-color: #eee; + color: #333; + cursor: pointer; + word-wrap: normal; + padding: 0.6180469716em 1.41575em; + font-weight: 600; + + &:focus, + &:hover { + background-color: #d5d5d5; + border-color: #d5d5d5; + color: #333; + } + &:disabled, + &[aria-disabled="true"] { + cursor: default; + opacity: 0.3; + } +} + .wc-block-grid__products { display: flex; flex-wrap: wrap; @@ -112,6 +169,7 @@ font-weight: 400; display: inline-block; margin: 0 auto; + text-align: left; &::before { content: "\53\53\53\53\53"; diff --git a/assets/js/base/components/load-more-button/index.js b/assets/js/base/components/load-more-button/index.js new file mode 100644 index 00000000000..f1eb708228a --- /dev/null +++ b/assets/js/base/components/load-more-button/index.js @@ -0,0 +1,47 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Fragment } from 'react'; +import PropTypes from 'prop-types'; + +/** + * Internal dependencies + */ +import './style.scss'; + +export const LoadMoreButton = ( { onClick, label, screenReaderLabel } ) => { + const labelNode = ( screenReaderLabel && label !== screenReaderLabel ) ? ( + + + { label } + + + { screenReaderLabel } + + + ) : label; + + return ( +
+ +
+ ); +}; + +LoadMoreButton.propTypes = { + label: PropTypes.string, + onClick: PropTypes.func, + screenReaderLabel: PropTypes.string, +}; + +LoadMoreButton.defaultProps = { + label: __( 'Load more', 'woo-gutenberg-products-block' ), +}; + +export default LoadMoreButton; diff --git a/assets/js/base/components/load-more-button/style.scss b/assets/js/base/components/load-more-button/style.scss new file mode 100644 index 00000000000..33dd65364f5 --- /dev/null +++ b/assets/js/base/components/load-more-button/style.scss @@ -0,0 +1,4 @@ +.wc-block-load-more { + text-align: center; + width: 100%; +} diff --git a/assets/js/base/components/price-slider/handles.sketch b/assets/js/base/components/price-slider/handles.sketch new file mode 100644 index 00000000000..6a7e82ebf2d Binary files /dev/null and b/assets/js/base/components/price-slider/handles.sketch differ diff --git a/assets/js/frontend-components/price-slider/index.js b/assets/js/base/components/price-slider/index.js similarity index 77% rename from assets/js/frontend-components/price-slider/index.js rename to assets/js/base/components/price-slider/index.js index aadbe014f20..490d20e19ef 100644 --- a/assets/js/frontend-components/price-slider/index.js +++ b/assets/js/base/components/price-slider/index.js @@ -4,6 +4,7 @@ import { sprintf, __ } from '@wordpress/i18n'; import { Component, createRef, Fragment } from 'react'; import PropTypes from 'prop-types'; +import classnames from 'classnames'; /** * Internal dependencies @@ -163,34 +164,42 @@ class PriceSlider extends Component { } render() { - const { min, max, step, showInputFields } = this.props; + const { min, max, step, showInputFields, showFilterButton } = this.props; const { inputMin, inputMax, currentMin, currentMax } = this.state; + + const classes = classnames( + 'wc-block-price-filter', + showInputFields && 'wc-block-price-filter--has-input-fields', + showFilterButton && 'wc-block-price-filter--has-filter-button', + ); return ( -
- { showInputFields && ( - - - - - ) } +
+
+ { showInputFields && ( + + + + + ) } +
+
+
+ { sprintf( __( 'Price: %s — %s', 'woo-gutenberg-products-block' ), inputMin, inputMax ) } +
+ { showFilterButton && ( + + ) } +
); } @@ -253,7 +275,11 @@ PriceSlider.propTypes = { /** * Whether or not to show input fields above the slider. */ - showInputFields: PropTypes.boolean, + showInputFields: PropTypes.bool, + /** + * Whether or not to show filter button above the slider. + */ + showFilterButton: PropTypes.bool, }; PriceSlider.defaultProps = { @@ -262,6 +288,7 @@ PriceSlider.defaultProps = { currencySymbol: '$', priceFormat: '%1$s%2$s', showInputFields: true, + showFilterButton: false, }; export default PriceSlider; diff --git a/assets/js/frontend-components/price-slider/style.scss b/assets/js/base/components/price-slider/style.scss similarity index 89% rename from assets/js/frontend-components/price-slider/style.scss rename to assets/js/base/components/price-slider/style.scss index 126de12aa6e..de878a658d1 100644 --- a/assets/js/frontend-components/price-slider/style.scss +++ b/assets/js/base/components/price-slider/style.scss @@ -47,17 +47,29 @@ } .wc-block-price-filter { - .wc-block-price-filter__amount { - display: inline-block; - margin-bottom: 20px; - border-radius: 4px; - width: auto; - - &.wc-block-price-filter__amount--min { - float: left; - } - &.wc-block-price-filter__amount--max { - float: right; + .wc-block-price-filter__controls { + display: flex; + flex-flow: row nowrap; + justify-content: flex-end; + align-items: center; + margin: 0 0 10px; + + .wc-block-price-filter__amount { + display: inline-block; + margin: 0; + border-radius: 4px; + width: auto; + flex-grow: 2; + max-width: 100px; + + &.wc-block-price-filter__amount--min { + order: 1; + margin-right: 10px; + } + &.wc-block-price-filter__amount--max { + order: 2; + margin-left: auto; + } } } .wc-block-price-filter__range-input-wrapper { @@ -67,6 +79,8 @@ position: relative; box-shadow: 0 0 0 1px inset rgba(0, 0, 0, 0.1); background: #e1e1e1; + margin: 15px 0; + .wc-block-price-filter__range-input-progress { height: 9px; width: 100%; @@ -78,6 +92,17 @@ background: var(--track-background); } } + .wc-block-price-filter__range-button-wrapper { + display: flex; + flex-flow: row nowrap; + justify-content: flex-end; + margin: 0 0 20px; + align-items: center; + + .wc-block-price-filter__button { + margin-left: auto; + } + } .wc-block-price-filter__range-input { @include reset; width: 100%; diff --git a/assets/js/frontend-components/price-slider/utils.js b/assets/js/base/components/price-slider/utils.js similarity index 100% rename from assets/js/frontend-components/price-slider/utils.js rename to assets/js/base/components/price-slider/utils.js diff --git a/assets/js/base/components/read-more/index.js b/assets/js/base/components/read-more/index.js new file mode 100644 index 00000000000..3b736ddebf9 --- /dev/null +++ b/assets/js/base/components/read-more/index.js @@ -0,0 +1,163 @@ +/** + * Show text based content, limited to a number of lines, with a read more link. + * + * Based on https://github.com/zoltantothcom/react-clamp-lines. + */ +import React, { createRef, Component } from 'react'; +import PropTypes from 'prop-types'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { clampLines } from './utils'; + +class ReadMore extends Component { + constructor( props ) { + super( ...arguments ); + + this.state = { + /** + * This is true when read more has been pressed and the full review is shown. + */ + isExpanded: false, + /** + * True if we are clamping content. False if the review is short. Null during init. + */ + clampEnabled: null, + /** + * Content is passed in via children. + */ + content: props.children, + /** + * Summary content generated from content HTML. + */ + summary: '.', + }; + + this.reviewSummary = createRef(); + this.reviewContent = createRef(); + this.getButton = this.getButton.bind( this ); + this.onClick = this.onClick.bind( this ); + } + + componentDidMount() { + if ( this.props.children ) { + const { maxLines, ellipsis } = this.props; + + const lineHeight = this.reviewSummary.current.clientHeight + 1; + const reviewHeight = this.reviewContent.current.clientHeight + 1; + const maxHeight = ( lineHeight * maxLines ) + 1; + const clampEnabled = reviewHeight > maxHeight; + + this.setState( { + clampEnabled, + } ); + + if ( clampEnabled ) { + this.setState( { + summary: clampLines( this.reviewContent.current.innerHTML, this.reviewSummary.current, maxHeight, ellipsis ), + } ); + } + } + } + + getButton() { + const { isExpanded } = this.state; + const { className, lessText, moreText } = this.props; + + const buttonText = isExpanded ? lessText : moreText; + + if ( ! buttonText ) { + return; + } + + return ( + + { buttonText } + + ); + } + + /** + * Handles the click event for the read more/less button. + * + * @param {obj} e event + */ + onClick( e ) { + e.preventDefault(); + + const { isExpanded } = this.state; + + this.setState( { + isExpanded: ! isExpanded, + } ); + } + + render() { + const { className } = this.props; + const { content, summary, clampEnabled, isExpanded } = this.state; + + if ( ! content ) { + return null; + } + + if ( false === clampEnabled ) { + return ( +
+
+ { content } +
+
+ ); + } + + return ( +
+ { ( ! isExpanded || null === clampEnabled ) && ( +
+ ) } + { ( isExpanded || null === clampEnabled ) && ( +
+ { content } +
+ ) } + { this.getButton() } +
+ ); + } +} + +ReadMore.propTypes = { + children: PropTypes.node.isRequired, + maxLines: PropTypes.number, + ellipsis: PropTypes.string, + moreText: PropTypes.string, + lessText: PropTypes.string, + className: PropTypes.string, +}; + +ReadMore.defaultProps = { + maxLines: 3, + ellipsis: '…', + moreText: __( 'Read more', 'woo-gutenberg-products-block' ), + lessText: __( 'Read less', 'woo-gutenberg-products-block' ), + className: 'read-more-content', +}; + +export default ReadMore; diff --git a/assets/js/base/components/read-more/test/index.js b/assets/js/base/components/read-more/test/index.js new file mode 100644 index 00000000000..d3e193c716c --- /dev/null +++ b/assets/js/base/components/read-more/test/index.js @@ -0,0 +1,30 @@ +/** + * Internal dependencies + */ +import { truncateHtml } from '../utils'; +const shortContent = + '

Lorem ipsum dolor sit amet, consectetur..

'; + +const longContent = + '

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam a condimentum diam. Donec finibus enim eros, et lobortis magna varius quis. Nulla lacinia tellus ac neque aliquet, in porttitor metus interdum. Maecenas vestibulum nisi et auctor vestibulum. Maecenas vehicula, lacus et pellentesque tempor, orci nulla mattis purus, id porttitor augue magna et metus. Aenean hendrerit aliquet massa ac convallis. Mauris vestibulum neque in condimentum porttitor. Donec viverra, orci a accumsan vehicula, dui massa lobortis lorem, et cursus est purus pulvinar elit. Vestibulum vitae tincidunt ex, ut vulputate nisi.

' + + '

Morbi tristique iaculis felis, sed porta urna tincidunt vitae. Etiam nisl sem, eleifend non varius quis, placerat a arcu. Donec consectetur nunc at orci fringilla pulvinar. Nam hendrerit tellus in est aliquet varius id in diam. Donec eu ullamcorper ante. Ut ultricies, felis vel sodales aliquet, nibh massa vestibulum ipsum, sed dignissim mi nunc eget lacus. Curabitur mattis placerat magna a aliquam. Nullam diam elit, cursus nec erat ullamcorper, tempor eleifend mauris. Nunc placerat nunc ut enim ornare tempus. Fusce porta molestie ante eget faucibus. Fusce eu lectus sit amet diam auctor lacinia et in diam. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Mauris eu lacus lobortis, faucibus est vel, pulvinar odio. Duis feugiat tortor quis dui euismod varius.

'; + +describe( 'ReadMore Component', () => { + describe( 'Test the truncateHtml function', () => { + it( 'Truncate long HTML content to length of 10', async () => { + const truncatedContent = truncateHtml( longContent, 10 ); + + expect( truncatedContent ).toEqual( '

Lorem ipsum...

' ); + } ); + it( 'Truncate long HTML content, but avoid cutting off HTML tags.', async () => { + const truncatedContent = truncateHtml( longContent, 40 ); + + expect( truncatedContent ).toEqual( '

Lorem ipsum dolor sit amet, consectetur...

' ); + } ); + it( 'No need to truncate short HTML content.', async () => { + const truncatedContent = truncateHtml( shortContent, 100 ); + + expect( truncatedContent ).toEqual( '

Lorem ipsum dolor sit amet, consectetur..

' ); + } ); + } ); +} ); diff --git a/assets/js/base/components/read-more/utils.js b/assets/js/base/components/read-more/utils.js new file mode 100644 index 00000000000..75866340a49 --- /dev/null +++ b/assets/js/base/components/read-more/utils.js @@ -0,0 +1,76 @@ +import trimHtml from 'trim-html'; + +/** + * Truncate some HTML content to a given length. + * + * @param {string} html HTML that will be truncated. + * @param {int} length Legth to truncate the string to. + * @param {string} ellipsis Character to append to truncated content. + */ +export const truncateHtml = ( html, length, ellipsis = '...' ) => { + const trimmed = trimHtml( html, { + suffix: ellipsis, + limit: length, + } ); + + return trimmed.html; +}; + +/** + * Clamp lines calculates the height of a line of text and then limits it to the + * value of the lines prop. Content is updated once limited. + * + * @param {string} originalContent Content to be clamped. + * @param {object} targetElement Element which will contain the clamped content. + * @param {integer} maxHeight Max height of the clamped content. + * @param {string} ellipsis Character to append to clamped content. + * @return {string} clamped content + */ +export const clampLines = ( originalContent, targetElement, maxHeight, ellipsis ) => { + const length = calculateLength( originalContent, targetElement, maxHeight ); + + return truncateHtml( originalContent, length - ellipsis.length, ellipsis ); +}; + +/** + * Calculate how long the content can be based on the maximum number of lines allowed, and client height. + * + * @param {string} originalContent Content to be clamped. + * @param {object} targetElement Element which will contain the clamped content. + * @param {integer} maxHeight Max height of the clamped content. + */ +const calculateLength = ( originalContent, targetElement, maxHeight ) => { + let markers = { + start: 0, + middle: 0, + end: originalContent.length, + }; + + while ( markers.start <= markers.end ) { + markers.middle = Math.floor( ( markers.start + markers.end ) / 2 ); + + // We set the innerHTML directly in the DOM here so we can reliably check the clientHeight later in moveMarkers. + targetElement.innerHTML = truncateHtml( originalContent, markers.middle ); + + markers = moveMarkers( markers, targetElement.clientHeight, maxHeight ); + } + + return markers.middle; +}; + +/** + * Move string markers. Used by calculateLength. + * + * @param {object} markers Markers for clamped content. + * @param {integer} currentHeight Current height of clamped content. + * @param {integer} maxHeight Max height of the clamped content. + */ +const moveMarkers = ( markers, currentHeight, maxHeight ) => { + if ( currentHeight <= maxHeight ) { + markers.start = markers.middle + 1; + } else { + markers.end = markers.middle - 1; + } + + return markers; +}; diff --git a/assets/js/base/components/review-list-item/index.js b/assets/js/base/components/review-list-item/index.js new file mode 100644 index 00000000000..c35f8c445b7 --- /dev/null +++ b/assets/js/base/components/review-list-item/index.js @@ -0,0 +1,138 @@ +/** + * External dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import PropTypes from 'prop-types'; + +/** + * Internal dependencies + */ +import ReadMore from '../read-more'; +import './style.scss'; + +function getReviewClasses( isLoading ) { + const classArray = [ 'wc-block-review-list-item__item' ]; + + if ( isLoading ) { + classArray.push( 'is-loading' ); + } + + return classArray.join( ' ' ); +} + +function getReviewImage( review, imageType, isLoading ) { + if ( isLoading || ! review ) { + return ( +
+ ); + } + + return ( +
+ { imageType === 'product' ? ( + + ) : ( + + ) } + { review.verified && ( +
{ __( 'Verified buyer', 'woo-gutenberg-products-block' ) }
+ ) } +
+ ); +} + +function getReviewContent( review ) { + return ( + +
+ + ); +} + +function getReviewProductName( review ) { + return ( + + ); +} + +function getReviewerName( review ) { + const { reviewer = '' } = review; + return ( +
+ { reviewer } +
+ ); +} + +function getReviewDate( review ) { + const { date_created: dateCreated, formatted_date_created: formattedDateCreated } = review; + return ( + + ); +} + +function getReviewRating( review ) { + const { rating } = review; + const starStyle = { + width: ( rating / 5 * 100 ) + '%', /* stylelint-disable-line */ + }; + return ( +
+
+ { sprintf( __( 'Rated %d out of 5', 'woo-gutenberg-products-block' ), rating ) } +
+
+ ); +} + +const ReviewListItem = ( { attributes, review = {} } ) => { + const { imageType, showReviewDate, showReviewerName, showReviewImage, showReviewRating: showReviewRatingAttr, showReviewContent, showProductName } = attributes; + const { rating } = review; + const isLoading = ! Object.keys( review ).length > 0; + const showReviewRating = Number.isFinite( rating ) && showReviewRatingAttr; + const classes = getReviewClasses( isLoading ); + + return ( +
  • + { ( showProductName || showReviewDate || showReviewerName || showReviewImage || showReviewRating ) && ( +
    + { showReviewImage && getReviewImage( review, imageType, isLoading ) } + { ( showProductName || showReviewerName || showReviewRating || showReviewDate ) && ( +
    + { showReviewRating && getReviewRating( review ) } + { showProductName && getReviewProductName( review ) } + { showReviewerName && getReviewerName( review ) } + { showReviewDate && getReviewDate( review ) } +
    + ) } +
    + ) } + { showReviewContent && getReviewContent( review ) } +
  • + ); +}; + +ReviewListItem.propTypes = { + attributes: PropTypes.object.isRequired, + review: PropTypes.object, +}; + +export default ReviewListItem; diff --git a/assets/js/base/components/review-list-item/style.scss b/assets/js/base/components/review-list-item/style.scss new file mode 100644 index 00000000000..4c528b4ca7c --- /dev/null +++ b/assets/js/base/components/review-list-item/style.scss @@ -0,0 +1,197 @@ +.is-loading { + .wc-block-review-list-item__text { + @include placeholder(); + display: block; + width: 60%; + } + + .wc-block-review-list-item__info { + .wc-block-review-list-item__image { + @include placeholder(); + } + + .wc-block-review-list-item__meta { + .wc-block-review-list-item__author { + @include placeholder(); + font-size: 1em; + width: 80px; + } + + .wc-block-review-list-item__product { + display: none; + } + + .wc-block-review-list-item__rating { + .wc-block-review-list-item__rating__stars > span { + display: none; + } + } + } + + .wc-block-review-list-item__published-date { + @include placeholder(); + height: 1em; + width: 120px; + } + } +} + +.editor-styles-wrapper .wc-block-review-list-item__item, +.wc-block-review-list-item__item { + margin: 0 0 $gap-large * 2; + list-style: none; +} + +.wc-block-review-list-item__info { + display: grid; + grid-template-columns: 1fr; + margin-bottom: $gap-large; +} + +.wc-block-review-list-item__meta { + grid-column: 1; + grid-row: 1; +} + +.has-image { + .wc-block-review-list-item__info { + grid-template-columns: #{48px + $gap} 1fr; + } + .wc-block-review-list-item__meta { + grid-column: 2; + } +} + +.wc-block-review-list-item__image { + height: 48px; + grid-column: 1; + grid-row: 1 / 3; + width: 48px; + position: relative; + + img { + width: 100%; + height: 100%; + display: block; + } +} + +.wc-block-review-list-item__verified { + width: 21px; + height: 21px; + text-indent: 21px; + margin: 0; + line-height: 21px; + overflow: hidden; + position: absolute; + right: -7px; + bottom: -7px; + + &::before { + width: 21px; + height: 21px; + background: transparent url('data:image/svg+xml;utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="21" height="21" fill="none"%3E%3Ccircle cx="10.5" cy="10.5" r="10.5" fill="%23fff"/%3E%3Cpath fill="%23008A21" fill-rule="evenodd" d="M2.1667 10.5003c0-4.6 3.7333-8.3333 8.3333-8.3333s8.3334 3.7333 8.3334 8.3333S15.1 18.8337 10.5 18.8337s-8.3333-3.7334-8.3333-8.3334zm2.5 0l4.1666 4.1667 7.5001-7.5-1.175-1.1833-6.325 6.325-2.9917-2.9834-1.175 1.175z" clip-rule="evenodd"/%3E%3Cmask id="a" width="17" height="17" x="2" y="2" maskUnits="userSpaceOnUse"%3E%3Cpath fill="%23fff" fill-rule="evenodd" d="M2.1667 10.5003c0-4.6 3.7333-8.3333 8.3333-8.3333s8.3334 3.7333 8.3334 8.3333S15.1 18.8337 10.5 18.8337s-8.3333-3.7334-8.3333-8.3334zm2.5 0l4.1666 4.1667 7.5001-7.5-1.175-1.1833-6.325 6.325-2.9917-2.9834-1.175 1.175z" clip-rule="evenodd"/%3E%3C/mask%3E%3Cg mask="url(%23a)"%3E%3Cpath fill="%23008A21" d="M.5.5h20v20H.5z"/%3E%3C/g%3E%3C/svg%3E') center center no-repeat; /* stylelint-disable-line */ + display: block; + content: ""; + } +} + +.wc-block-review-list-item__meta { + display: flex; + align-items: center; + flex-flow: row wrap; + + &::after { + // Force wrap after star rating. + order: 3; + content: ""; + flex-basis: 100%; + } +} + +.wc-block-review-list-item__product { + display: block; + font-weight: bold; + order: 1; + margin-right: $gap/2; +} + +.wc-block-review-list-item__author { + display: block; + font-weight: bold; + order: 1; + margin-right: $gap/2; +} + +.wc-block-review-list-item__product + .wc-block-review-list-item__author { + font-weight: normal; + color: #808080; + order: 4; +} + +.wc-block-review-list-item__published-date { + color: #808080; + order: 5; +} + +.wc-block-review-list-item__author + .wc-block-review-list-item__published-date { + &::before { + content: ""; + display: inline-block; + margin-right: $gap/2; + border-right: 1px solid #ddd; + height: 1em; + vertical-align: middle; + } +} + +.wc-block-review-list-item__author:first-child + .wc-block-review-list-item__published-date, +.wc-block-review-list-item__rating + .wc-block-review-list-item__author + .wc-block-review-list-item__published-date { + &::before { + display: none; + } +} + +.wc-block-review-list-item__rating { + order: 2; + + > .wc-block-review-list-item__rating__stars { + display: inline-block; + top: 0; + overflow: hidden; + position: relative; + height: 1.618em; + line-height: 1.618; + font-size: 1em; + width: 5.3em; + font-family: star; /* stylelint-disable-line */ + font-weight: 400; + vertical-align: top; + } + + > .wc-block-review-list-item__rating__stars::before { + content: "\53\53\53\53\53"; + opacity: 0.25; + float: left; + top: 0; + left: 0; + position: absolute; + } + + > .wc-block-review-list-item__rating__stars span { + overflow: hidden; + float: left; + top: 0; + left: 0; + position: absolute; + padding-top: 1.5em; + } + + > .wc-block-review-list-item__rating__stars span::before { + content: "\53\53\53\53\53"; + top: 0; + position: absolute; + left: 0; + color: #e6a237; + } +} diff --git a/assets/js/base/components/review-list/index.js b/assets/js/base/components/review-list/index.js new file mode 100644 index 00000000000..880c066fbd0 --- /dev/null +++ b/assets/js/base/components/review-list/index.js @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * Internal dependencies + */ +import ReviewListItem from '../review-list-item'; +import { ENABLE_REVIEW_RATING, SHOW_AVATARS } from '../../../constants'; +import './style.scss'; + +const ReviewList = ( { attributes, componentId, reviews } ) => { + const showReviewImage = ( SHOW_AVATARS || attributes.imageType === 'product' ) && attributes.showReviewImage; + const showReviewRating = ENABLE_REVIEW_RATING && attributes.showReviewRating; + const attrs = { + ...attributes, + showReviewImage, + showReviewRating, + }; + + return ( +
      + { reviews.length === 0 ? + ( + + ) : ( + reviews.map( ( review, i ) => ( + + ) ) + ) + } +
    + ); +}; + +ReviewList.propTypes = { + attributes: PropTypes.object.isRequired, + componentId: PropTypes.number.isRequired, + reviews: PropTypes.array.isRequired, +}; + +export default ReviewList; diff --git a/assets/js/base/components/review-list/style.scss b/assets/js/base/components/review-list/style.scss new file mode 100644 index 00000000000..b4c8959b600 --- /dev/null +++ b/assets/js/base/components/review-list/style.scss @@ -0,0 +1,4 @@ +.wc-block-review-list, +.editor-styles .wc-block-review-list { + margin: 0; +} diff --git a/assets/js/base/components/review-order-select/index.js b/assets/js/base/components/review-order-select/index.js new file mode 100644 index 00000000000..81c762a137b --- /dev/null +++ b/assets/js/base/components/review-order-select/index.js @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import PropTypes from 'prop-types'; + +/** + * Internal dependencies + */ +import './style.scss'; + +const ReviewOrderSelect = ( { componentId, onChange, readOnly, value } ) => { + const selectId = `wc-block-review-order-select__select-${ componentId }`; + + return ( +

    + + +

    + ); +}; + +ReviewOrderSelect.propTypes = { + componentId: PropTypes.number.isRequired, + onChange: PropTypes.func, + readOnly: PropTypes.bool, + value: PropTypes.oneOf( [ 'most-recent', 'highest-rating', 'lowest-rating' ] ), +}; + +export default ReviewOrderSelect; diff --git a/assets/js/base/components/review-order-select/style.scss b/assets/js/base/components/review-order-select/style.scss new file mode 100644 index 00000000000..be5e57584f3 --- /dev/null +++ b/assets/js/base/components/review-order-select/style.scss @@ -0,0 +1,10 @@ +.wc-block-review-order-select { + margin-bottom: $gap-small; + text-align: right; +} + +.wc-block-review-order-select__label { + margin-right: $gap-small; + display: inline-block; + font-weight: normal; +} diff --git a/assets/js/hocs/with-component-id.js b/assets/js/base/hocs/with-component-id.js similarity index 100% rename from assets/js/hocs/with-component-id.js rename to assets/js/base/hocs/with-component-id.js diff --git a/assets/js/blocks/featured-category/block.js b/assets/js/blocks/featured-category/block.js index e7eaafca767..6398461a534 100644 --- a/assets/js/blocks/featured-category/block.js +++ b/assets/js/blocks/featured-category/block.js @@ -2,7 +2,6 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import apiFetch from '@wordpress/api-fetch'; import { AlignmentToolbar, BlockControls, @@ -27,130 +26,77 @@ import { withSpokenMessages, } from '@wordpress/components'; import classnames from 'classnames'; -import { Component, Fragment } from '@wordpress/element'; +import { Fragment } from '@wordpress/element'; import { compose } from '@wordpress/compose'; -import { debounce, isObject } from 'lodash'; import PropTypes from 'prop-types'; import { IconFolderStar } from '../../components/icons'; /** * Internal dependencies */ -import { ENDPOINTS } from '../../constants'; +import { MIN_HEIGHT } from '../../constants'; import ProductCategoryControl from '../../components/product-category-control'; - -/** - * The min-height for the block content. - */ -const MIN_HEIGHT = wc_product_block_data.min_height; - -/** - * Get the src from a category object, unless null (no image). - * - * @param {object|null} category A product category object from the API. - * @return {string} The src of the category image. - */ -function getCategoryImageSrc( category ) { - if ( isObject( category.image ) ) { - return category.image.src; - } - return ''; -} - -/** - * Get the attachment ID from a category object, unless null (no image). - * - * @param {object|null} category A product category object from the API. - * @return {number} The id of the category image. - */ -function getCategoryImageID( category ) { - if ( isObject( category.image ) ) { - return category.image.id; - } - return 0; -} - -/** - * Generate a style object given either a product category image from the API or URL to an image. - * - * @param {string} url An image URL. - * @return {Object} A style object with a backgroundImage set (if a valid image is provided). - */ -function backgroundImageStyles( url ) { - if ( url ) { - return { backgroundImage: `url(${ url })` }; - } - return {}; -} - -/** - * Convert the selected ratio to the correct background class. - * - * @param {number} ratio Selected opacity from 0 to 100. - * @return {string} The class name, if applicable (not used for ratio 0 or 50). - */ -function dimRatioToClass( ratio ) { - return ratio === 0 || ratio === 50 ? - null : - `has-background-dim-${ 10 * Math.round( ratio / 10 ) }`; -} +import ApiErrorPlaceholder from '../../components/api-error-placeholder'; +import { + dimRatioToClass, + getBackgroundImageStyles, + getCategoryImageId, + getCategoryImageSrc, +} from './utils'; +import { withCategory } from '../../hocs'; /** * Component to handle edit mode of "Featured Category". */ -class FeaturedCategory extends Component { - constructor() { - super( ...arguments ); - this.state = { - category: false, - loaded: false, - }; - - this.debouncedGetCategory = debounce( this.getCategory.bind( this ), 200 ); - } +const FeaturedCategory = ( { attributes, isSelected, setAttributes, error, getCategory, isLoading, category, overlayColor, setOverlayColor, debouncedSpeak } ) => { + const renderApiError = () => ( + + ); - componentDidMount() { - this.getCategory(); - } - - componentWillUnmount() { - this.debouncedGetCategory.cancel(); - } + const getBlockControls = () => { + const { contentAlign } = attributes; + const mediaId = attributes.mediaId || getCategoryImageId( category ); - componentDidUpdate( prevProps ) { - if ( prevProps.attributes.categoryId !== this.props.attributes.categoryId ) { - this.debouncedGetCategory(); - } - } - - getCategory() { - const { categoryId } = this.props.attributes; - if ( ! categoryId ) { - // We've removed the selected product, or no product is selected yet. - this.setState( { category: false, loaded: true } ); - return; - } - apiFetch( { - path: `${ ENDPOINTS.products }/categories/${ categoryId }`, - } ) - .then( ( category ) => { - this.setState( { category, loaded: true } ); - } ) - .catch( () => { - this.setState( { category: false, loaded: true } ); - } ); - } - - getInspectorControls() { - const { - attributes, - setAttributes, - overlayColor, - setOverlayColor, - } = this.props; + return ( + + { + setAttributes( { contentAlign: nextAlign } ); + } } + /> + + + { + setAttributes( { mediaId: media.id, mediaSrc: media.url } ); + } } + allowedTypes={ [ 'image' ] } + value={ mediaId } + render={ ( { open } ) => ( + + ) } + /> + + + + ); + }; + const getInspectorControls = () => { const url = - attributes.mediaSrc || getCategoryImageSrc( this.state.category ); + attributes.mediaSrc || getCategoryImageSrc( category ); const { focalPoint = { x: 0.5, y: 0.5 } } = attributes; // FocalPointPicker was introduced in Gutenberg 5.0 (WordPress 5.2), // so we need to check if it exists before using it. @@ -198,10 +144,9 @@ class FeaturedCategory extends Component { ); - } + }; - renderEditMode() { - const { attributes, debouncedSpeak, setAttributes } = this.props; + const renderEditMode = () => { const onDone = () => { setAttributes( { editMode: false } ); debouncedSpeak( @@ -237,36 +182,32 @@ class FeaturedCategory extends Component {
    ); - } + }; - render() { - const { attributes, isSelected, overlayColor, setAttributes } = this.props; + const renderCategory = () => { const { className, contentAlign, dimRatio, - editMode, focalPoint, height, showDesc, } = attributes; - const { loaded, category } = this.state; const classes = classnames( 'wc-block-featured-category', { 'is-selected': isSelected, - 'is-loading': ! category && ! loaded, - 'is-not-found': ! category && loaded, + 'is-loading': ! category && isLoading, + 'is-not-found': ! category && ! isLoading, 'has-background-dim': dimRatio !== 0, }, dimRatioToClass( dimRatio ), contentAlign !== 'center' && `has-${ contentAlign }-content`, className, ); - const mediaId = attributes.mediaId || getCategoryImageID( category ); - const mediaSrc = attributes.mediaSrc || getCategoryImageSrc( this.state.category ); + const mediaSrc = attributes.mediaSrc || getCategoryImageSrc( category ); const style = !! category ? - backgroundImageStyles( mediaSrc ) : + getBackgroundImageStyles( mediaSrc ) : {}; if ( overlayColor.color ) { style.backgroundColor = overlayColor.color; @@ -281,103 +222,88 @@ class FeaturedCategory extends Component { }; return ( - - - { - setAttributes( { contentAlign: nextAlign } ); + +
    +

    - - - { - setAttributes( { mediaId: media.id, mediaSrc: media.url } ); - } } - allowedTypes={ [ 'image' ] } - value={ mediaId } - render={ ( { open } ) => ( - - ) } - /> - - - - { ! attributes.editMode && this.getInspectorControls() } - { editMode ? ( - this.renderEditMode() - ) : ( - - { !! category ? ( - -
    -

    - { showDesc && ( -
    - ) } -
    - -
    -
    - - ) : ( - } - label={ __( 'Featured Category', 'woo-gutenberg-products-block' ) } - > - { ! loaded ? ( - - ) : ( - __( 'No product category is selected.', 'woo-gutenberg-products-block' ) - ) } - - ) } - - ) } - + { showDesc && ( +
    + ) } +
    + +
    +
    + ); + }; + + const renderNoCategory = () => ( + } + label={ __( 'Featured Category', 'woo-gutenberg-products-block' ) } + > + { isLoading ? ( + + ) : ( + __( 'No product category is selected.', 'woo-gutenberg-products-block' ) + ) } + + ); + + const { editMode } = attributes; + + if ( error ) { + return renderApiError(); + } + + if ( editMode ) { + return renderEditMode(); } -} + + return ( + + { getBlockControls() } + { getInspectorControls() } + { category ? ( + renderCategory() + ) : ( + renderNoCategory() + ) } + + ); +}; FeaturedCategory.propTypes = { /** @@ -396,6 +322,15 @@ FeaturedCategory.propTypes = { * A callback to update attributes. */ setAttributes: PropTypes.func.isRequired, + // from withCategory + error: PropTypes.object, + getCategory: PropTypes.func, + isLoading: PropTypes.bool, + category: PropTypes.shape( { + name: PropTypes.node, + description: PropTypes.node, + permalink: PropTypes.string, + } ), // from withColors overlayColor: PropTypes.object, setOverlayColor: PropTypes.func.isRequired, @@ -404,6 +339,7 @@ FeaturedCategory.propTypes = { }; export default compose( [ + withCategory, withColors( { overlayColor: 'background-color' } ), withSpokenMessages, ] )( FeaturedCategory ); diff --git a/assets/js/blocks/featured-category/index.js b/assets/js/blocks/featured-category/index.js index 2f4a7851468..0e12b969122 100644 --- a/assets/js/blocks/featured-category/index.js +++ b/assets/js/blocks/featured-category/index.js @@ -12,6 +12,7 @@ import './style.scss'; import './editor.scss'; import Block from './block'; import { IconFolderStar } from '../../components/icons'; +import { DEFAULT_HEIGHT } from '../../constants'; /** * Register and run the "Featured Category" block. @@ -69,7 +70,7 @@ registerBlockType( 'woocommerce/featured-category', { */ height: { type: 'number', - default: wc_product_block_data.default_height, + default: DEFAULT_HEIGHT, }, /** diff --git a/assets/js/blocks/featured-category/utils.js b/assets/js/blocks/featured-category/utils.js new file mode 100644 index 00000000000..967fc9b7151 --- /dev/null +++ b/assets/js/blocks/featured-category/utils.js @@ -0,0 +1,57 @@ +/** + * External dependencies + */ +import { isObject } from 'lodash'; + +/** + * Get the src from a category object, unless null (no image). + * + * @param {object|null} category A product category object from the API. + * @return {string} The src of the category image. + */ +function getCategoryImageSrc( category ) { + if ( category && isObject( category.image ) ) { + return category.image.src; + } + return ''; +} + +/** + * Get the attachment ID from a category object, unless null (no image). + * + * @param {object|null} category A product category object from the API. + * @return {number} The id of the category image. + */ +function getCategoryImageId( category ) { + if ( category && isObject( category.image ) ) { + return category.image.id; + } + return 0; +} + +/** + * Generate a style object given either a product category image from the API or URL to an image. + * + * @param {string} url An image URL. + * @return {Object} A style object with a backgroundImage set (if a valid image is provided). + */ +function getBackgroundImageStyles( url ) { + if ( url ) { + return { backgroundImage: `url(${ url })` }; + } + return {}; +} + +/** + * Convert the selected ratio to the correct background class. + * + * @param {number} ratio Selected opacity from 0 to 100. + * @return {string} The class name, if applicable (not used for ratio 0 or 50). + */ +function dimRatioToClass( ratio ) { + return ratio === 0 || ratio === 50 ? + null : + `has-background-dim-${ 10 * Math.round( ratio / 10 ) }`; +} + +export { getCategoryImageSrc, getCategoryImageId, getBackgroundImageStyles, dimRatioToClass }; diff --git a/assets/js/blocks/featured-product/block.js b/assets/js/blocks/featured-product/block.js index 2f447c3382f..a26f99054f0 100644 --- a/assets/js/blocks/featured-product/block.js +++ b/assets/js/blocks/featured-product/block.js @@ -45,11 +45,7 @@ import { getImageIdFromProduct, } from '../../utils/products'; import { withProduct } from '../../hocs'; - -/** - * The min-height for the block content. - */ -const MIN_HEIGHT = wc_product_block_data.min_height; +import { MIN_HEIGHT } from '../../constants'; /** * Component to handle edit mode of "Featured Product". diff --git a/assets/js/blocks/featured-product/index.js b/assets/js/blocks/featured-product/index.js index 08339aeb39c..9cc1113511c 100644 --- a/assets/js/blocks/featured-product/index.js +++ b/assets/js/blocks/featured-product/index.js @@ -11,6 +11,7 @@ import { registerBlockType } from '@wordpress/blocks'; import './style.scss'; import './editor.scss'; import Block from './block'; +import { DEFAULT_HEIGHT } from '../../constants'; /** * Register and run the "Featured Product" block. @@ -68,7 +69,7 @@ registerBlockType( 'woocommerce/featured-product', { */ height: { type: 'number', - default: wc_product_block_data.default_height, + default: DEFAULT_HEIGHT, }, /** diff --git a/assets/js/blocks/handpicked-products/block.js b/assets/js/blocks/handpicked-products/block.js index 985fb2d7664..9f581b8b206 100644 --- a/assets/js/blocks/handpicked-products/block.js +++ b/assets/js/blocks/handpicked-products/block.js @@ -27,6 +27,7 @@ import GridContentControl from '../../components/grid-content-control'; import { IconWidgets } from '../../components/icons'; import ProductsControl from '../../components/products-control'; import ProductOrderbyControl from '../../components/product-orderby-control'; +import { MAX_COLUMNS, MIN_COLUMNS } from '../../constants'; /** * Component to handle edit mode of "Hand-picked Products". @@ -46,8 +47,8 @@ class ProductsBlock extends Component { label={ __( 'Columns', 'woo-gutenberg-products-block' ) } value={ columns } onChange={ ( value ) => setAttributes( { columns: value } ) } - min={ wc_product_block_data.min_columns } - max={ wc_product_block_data.max_columns } + min={ MIN_COLUMNS } + max={ MAX_COLUMNS } /> -

    Current Min: { min }

    -

    Current Max: { max }

    {} } />

    ); diff --git a/assets/js/blocks/price-filter/edit.js b/assets/js/blocks/price-filter/edit.js index af07c2e4e41..3c00f446ab0 100644 --- a/assets/js/blocks/price-filter/edit.js +++ b/assets/js/blocks/price-filter/edit.js @@ -1,17 +1,84 @@ /** * External dependencies */ +import { __ } from '@wordpress/i18n'; import { Fragment } from '@wordpress/element'; +import { InspectorControls } from '@wordpress/editor'; +import { Placeholder, Disabled, PanelBody, ToggleControl, Button } from '@wordpress/components'; /** * Internal dependencies */ import Block from './block.js'; +import './editor.scss'; +import { IconMoney, IconExternal } from '../../components/icons'; +import { adminUrl, blockData } from '@woocommerce/settings'; + +export default function( { attributes, setAttributes } ) { + const getInspectorControls = () => { + const { showInputFields, showFilterButton } = attributes; + + return ( + + + setAttributes( { showInputFields: ! showInputFields } ) } + /> + setAttributes( { showFilterButton: ! showFilterButton } ) } + /> + + + ); + }; + + const noProductsPlaceholder = () => ( + } + label={ __( 'Filter Products by Price', 'woo-gutenberg-products-block' ) } + instructions={ __( 'Display a slider to filter products in your store by price.', 'woo-gutenberg-products-block' ) } + > +

    + { __( "Products with prices are needed for filtering by price. You haven't created any products yet.", 'woo-gutenberg-products-block' ) } +

    + + +
    + ); -export default function( { attributes } ) { return ( - + { 0 === blockData.productCount ? noProductsPlaceholder() : ( + + { getInspectorControls() } + + + + + ) } ); } diff --git a/assets/js/blocks/price-filter/editor.scss b/assets/js/blocks/price-filter/editor.scss new file mode 100644 index 00000000000..a0cf35103e3 --- /dev/null +++ b/assets/js/blocks/price-filter/editor.scss @@ -0,0 +1,48 @@ +.components-disabled .wc-block-price-filter__range-input-wrapper .wc-block-price-filter__range-input { + &::-webkit-slider-thumb { + pointer-events: none; + } + &::-moz-range-thumb { + pointer-events: none; + } + &::-ms-thumb { + pointer-events: none; + } +} +.wc-block-price-slider { + .components-placeholder__instructions { + border-bottom: 1px solid #e0e2e6; + width: 100%; + padding-bottom: 1em; + margin-bottom: 2em; + } + .components-placeholder__label svg { + fill: currentColor; + margin-right: 1ch; + } + .components-placeholder__fieldset { + display: block; /* Disable flex box */ + + p { + font-size: 14px; + } + } + .wc-block-price-slider__add_product_button { + margin: 0 0 1em; + line-height: 24px; + vertical-align: middle; + height: auto; + font-size: 14px; + padding: 0.5em 1em; + + svg { + fill: currentColor; + margin-left: 0.5ch; + vertical-align: middle; + } + } + .wc-block-price-slider__read_more_button { + display: block; + margin-bottom: 1em; + } +} diff --git a/assets/js/blocks/price-filter/frontend.js b/assets/js/blocks/price-filter/frontend.js index b78360c51a3..12dc489041e 100644 --- a/assets/js/blocks/price-filter/frontend.js +++ b/assets/js/blocks/price-filter/frontend.js @@ -14,8 +14,12 @@ const containers = document.querySelectorAll( if ( containers.length ) { Array.prototype.forEach.call( containers, ( el ) => { + const attributes = { + showInputFields: el.dataset.showinputfields === 'true', + showFilterButton: el.dataset.showfilterbutton === 'true', + }; el.classList.remove( 'is-loading' ); - render( , el ); + render( , el ); } ); } diff --git a/assets/js/blocks/price-filter/index.js b/assets/js/blocks/price-filter/index.js index 7ffdd4b94db..c7c2838840b 100644 --- a/assets/js/blocks/price-filter/index.js +++ b/assets/js/blocks/price-filter/index.js @@ -8,12 +8,12 @@ import { registerBlockType } from '@wordpress/blocks'; * Internal dependencies */ import edit from './edit.js'; -import { IconFolder } from '../../components/icons'; +import { IconMoney } from '../../components/icons'; registerBlockType( 'woocommerce/price-filter', { title: __( 'Filter Products by Price', 'woo-gutenberg-products-block' ), icon: { - src: , + src: , foreground: '#96588a', }, category: 'woocommerce', @@ -26,15 +26,28 @@ registerBlockType( 'woocommerce/price-filter', { align: [ 'wide', 'full' ], }, - attributes: {}, + attributes: { + showInputFields: { + type: 'boolean', + default: true, + }, + showFilterButton: { + type: 'boolean', + default: false, + }, + }, edit, /** * Save the props to post content. */ - save() { - const data = {}; + save( { attributes } ) { + const { showInputFields, showFilterButton } = attributes; + const data = { + 'data-showinputfields': showInputFields, + 'data-showfilterbutton': showFilterButton, + }; return (
    diff --git a/assets/js/blocks/product-categories/block.js b/assets/js/blocks/product-categories/block.js index b1a01168225..19b92ad3a9c 100644 --- a/assets/js/blocks/product-categories/block.js +++ b/assets/js/blocks/product-categories/block.js @@ -8,7 +8,8 @@ import classnames from 'classnames'; /** * Internal dependencies */ -import withComponentId from '../../hocs/with-component-id'; +import withComponentId from '../../base/hocs/with-component-id'; +import { HOME_URL } from '../../constants'; /** * Component displaying the categories as dropdown or list. @@ -28,7 +29,7 @@ class ProductCategoriesBlock extends Component { if ( 'false' === url ) { return; } - const home = wc_product_block_data.homeUrl; + const home = HOME_URL; if ( ! isPreview && 0 === url.indexOf( home ) ) { document.location.href = url; diff --git a/assets/js/blocks/product-categories/get-categories.js b/assets/js/blocks/product-categories/get-categories.js index bb435e5c10d..66c830a6ecd 100644 --- a/assets/js/blocks/product-categories/get-categories.js +++ b/assets/js/blocks/product-categories/get-categories.js @@ -2,12 +2,13 @@ * Internal dependencies */ import { buildTermsTree } from './hierarchy'; +import { PRODUCT_CATEGORIES } from '../../constants'; /** * Returns categories in tree form. */ export default function( { hasEmpty, isHierarchical } ) { - const categories = wc_product_block_data.productCategories.filter( + const categories = PRODUCT_CATEGORIES.filter( ( cat ) => hasEmpty || !! cat.count ); return isHierarchical ? diff --git a/assets/js/blocks/product-categories/style.scss b/assets/js/blocks/product-categories/style.scss index 410166f6d74..291b872cff7 100644 --- a/assets/js/blocks/product-categories/style.scss +++ b/assets/js/blocks/product-categories/style.scss @@ -37,9 +37,6 @@ fill: currentColor; outline: none; } - .screen-reader-text { - height: auto; - } &:active { color: currentColor; } diff --git a/assets/js/blocks/product-search/block.js b/assets/js/blocks/product-search/block.js index b0d2d3f902f..9201baec616 100644 --- a/assets/js/blocks/product-search/block.js +++ b/assets/js/blocks/product-search/block.js @@ -11,6 +11,7 @@ import { PlainText } from '@wordpress/editor'; /** * Internal dependencies */ +import { HOME_URL } from '../../constants'; import './editor.scss'; import './style.scss'; @@ -20,7 +21,6 @@ import './style.scss'; class ProductSearchBlock extends Component { renderView() { const { attributes: { label, placeholder, formId, className, hasLabel, align } } = this.props; - const home = wc_product_block_data.homeUrl; const classes = classnames( 'wc-block-product-search', align ? 'align' + align : '', @@ -29,7 +29,7 @@ class ProductSearchBlock extends Component { return (
    -
    +