Merged in feature/MAW-855-import-code-into-aws (pull request #2)
code import from pantheon * code import from pantheon
This commit is contained in:
@@ -54,6 +54,24 @@ registerBlockComponent( {
|
||||
),
|
||||
} );
|
||||
|
||||
registerBlockComponent( {
|
||||
blockName: 'woocommerce/product-rating-counter',
|
||||
component: lazy( () =>
|
||||
import(
|
||||
/* webpackChunkName: "product-rating-counter" */ './product-elements/rating-counter/block'
|
||||
)
|
||||
),
|
||||
} );
|
||||
|
||||
registerBlockComponent( {
|
||||
blockName: 'woocommerce/product-average-rating',
|
||||
component: lazy( () =>
|
||||
import(
|
||||
/* webpackChunkName: "product-average-rating" */ './product-elements/average-rating/block'
|
||||
)
|
||||
),
|
||||
} );
|
||||
|
||||
registerBlockComponent( {
|
||||
blockName: 'woocommerce/product-button',
|
||||
component: lazy( () =>
|
||||
|
||||
@@ -6,6 +6,8 @@ import './product-elements/price';
|
||||
import './product-elements/image';
|
||||
import './product-elements/rating';
|
||||
import './product-elements/rating-stars';
|
||||
import './product-elements/rating-counter';
|
||||
import './product-elements/average-rating';
|
||||
import './product-elements/button';
|
||||
import './product-elements/summary';
|
||||
import './product-elements/sale-badge';
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
padding: 0.618em;
|
||||
background: $white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
border-radius: $universal-border-radius;
|
||||
color: #43454b;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.125);
|
||||
text-align: center;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "woocommerce/product-average-rating",
|
||||
"version": "1.0.0",
|
||||
"title": "Product Average Rating (Beta)",
|
||||
"description": "Display the average rating of a product",
|
||||
"attributes": {
|
||||
"textAlign": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"category": "woocommerce",
|
||||
"keywords": [ "WooCommerce" ],
|
||||
"ancestor": [ "woocommerce/single-product" ],
|
||||
"textdomain": "woocommerce",
|
||||
"apiVersion": 2,
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { useProductDataContext } from '@woocommerce/shared-context';
|
||||
import { useStyleProps } from '@woocommerce/base-hooks';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { withProductDataContext } from '@woocommerce/shared-hocs';
|
||||
|
||||
type ProductAverageRatingProps = {
|
||||
className?: string;
|
||||
textAlign?: string;
|
||||
};
|
||||
|
||||
export const Block = ( props: ProductAverageRatingProps ): JSX.Element => {
|
||||
const { textAlign } = props;
|
||||
const styleProps = useStyleProps( props );
|
||||
const { product } = useProductDataContext();
|
||||
|
||||
const className = classnames(
|
||||
styleProps.className,
|
||||
'wc-block-components-product-average-rating',
|
||||
{
|
||||
[ `has-text-align-${ textAlign }` ]: textAlign,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={ className } style={ styleProps.style }>
|
||||
{ Number( product.average_rating ) > 0
|
||||
? product.average_rating
|
||||
: __( 'No ratings', 'woo-gutenberg-products-block' ) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default withProductDataContext( Block );
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
AlignmentToolbar,
|
||||
BlockControls,
|
||||
useBlockProps,
|
||||
} from '@wordpress/block-editor';
|
||||
import type { BlockEditProps } from '@wordpress/blocks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
|
||||
export interface BlockAttributes {
|
||||
textAlign: string;
|
||||
}
|
||||
|
||||
const Edit = ( props: BlockEditProps< BlockAttributes > ): JSX.Element => {
|
||||
const { attributes, setAttributes } = props;
|
||||
const blockProps = useBlockProps( {
|
||||
className: 'wp-block-woocommerce-product-average-rating',
|
||||
} );
|
||||
|
||||
return (
|
||||
<>
|
||||
<BlockControls>
|
||||
<AlignmentToolbar
|
||||
value={ attributes.textAlign }
|
||||
onChange={ ( newAlign ) => {
|
||||
setAttributes( { textAlign: newAlign || '' } );
|
||||
} }
|
||||
/>
|
||||
</BlockControls>
|
||||
<div { ...blockProps }>
|
||||
<Block { ...attributes } />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Edit;
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
import { Icon, starHalf } from '@wordpress/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import metadata from './block.json';
|
||||
import edit from './edit';
|
||||
import { supports } from './support';
|
||||
|
||||
registerBlockType( metadata, {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ starHalf }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
supports,
|
||||
edit,
|
||||
} );
|
||||
@@ -0,0 +1,26 @@
|
||||
/* eslint-disable @wordpress/no-unsafe-wp-apis */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
|
||||
|
||||
export const supports = {
|
||||
...( isFeaturePluginBuild() && {
|
||||
color: {
|
||||
text: true,
|
||||
background: true,
|
||||
__experimentalSkipSerialization: true,
|
||||
},
|
||||
spacing: {
|
||||
margin: true,
|
||||
padding: true,
|
||||
__experimentalSkipSerialization: true,
|
||||
},
|
||||
typography: {
|
||||
fontSize: true,
|
||||
__experimentalFontWeight: true,
|
||||
__experimentalSkipSerialization: true,
|
||||
},
|
||||
__experimentalSelector: '.wc-block-components-product-average-rating',
|
||||
} ),
|
||||
};
|
||||
@@ -34,6 +34,7 @@
|
||||
"background": false,
|
||||
"link": true
|
||||
},
|
||||
"interactivity": true,
|
||||
"html": false,
|
||||
"typography": {
|
||||
"fontSize": true,
|
||||
@@ -57,6 +58,9 @@
|
||||
"label": "Outline"
|
||||
}
|
||||
],
|
||||
"viewScript": [
|
||||
"wc-product-button-interactivity-frontend"
|
||||
],
|
||||
"apiVersion": 2,
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json"
|
||||
}
|
||||
|
||||
@@ -27,22 +27,10 @@ import type {
|
||||
AddToCartButtonPlaceholderAttributes,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Product Button Block Component.
|
||||
*
|
||||
* @param {Object} props Incoming props.
|
||||
* @param {Object} [props.product] Product.
|
||||
* @param {Object} [props.style] Object contains CSS Styles.
|
||||
* @param {string} [props.className] String contains CSS class.
|
||||
* @param {Object} [props.textAlign] Text alignment.
|
||||
*
|
||||
* @return {*} The component.
|
||||
*/
|
||||
const AddToCartButton = ( {
|
||||
product,
|
||||
className,
|
||||
style,
|
||||
textAlign,
|
||||
}: AddToCartButtonAttributes ): JSX.Element => {
|
||||
const {
|
||||
id,
|
||||
@@ -117,9 +105,6 @@ const AddToCartButton = ( {
|
||||
{
|
||||
loading: addingToCart,
|
||||
added: addedToCart,
|
||||
},
|
||||
{
|
||||
[ `has-text-align-${ textAlign }` ]: textAlign,
|
||||
}
|
||||
) }
|
||||
style={ style }
|
||||
@@ -129,15 +114,6 @@ const AddToCartButton = ( {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Product Button Block Component.
|
||||
*
|
||||
* @param {Object} props Incoming props.
|
||||
* @param {Object} [props.style] Object contains CSS Styles.
|
||||
* @param {string} [props.className] String contains CSS class.
|
||||
*
|
||||
* @return {*} The component.
|
||||
*/
|
||||
const AddToCartButtonPlaceholder = ( {
|
||||
className,
|
||||
style,
|
||||
@@ -158,14 +134,6 @@ const AddToCartButtonPlaceholder = ( {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Product Button Block Component.
|
||||
*
|
||||
* @param {Object} props Incoming props.
|
||||
* @param {string} [props.className] CSS Class name for the component.
|
||||
* @param {string} [props.textAlign] Text alignment.
|
||||
* @return {*} The component.
|
||||
*/
|
||||
export const Block = ( props: BlockAttributes ): JSX.Element => {
|
||||
const { className, textAlign } = props;
|
||||
const styleProps = useStyleProps( props );
|
||||
@@ -181,9 +149,7 @@ export const Block = ( props: BlockAttributes ): JSX.Element => {
|
||||
{
|
||||
[ `${ parentClassName }__product-add-to-cart` ]:
|
||||
parentClassName,
|
||||
},
|
||||
{
|
||||
[ `has-text-align-${ textAlign }` ]: textAlign,
|
||||
[ `align-${ textAlign }` ]: textAlign,
|
||||
}
|
||||
) }
|
||||
>
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
import { store as interactivityStore } from '@woocommerce/interactivity';
|
||||
import { dispatch, select, subscribe } from '@wordpress/data';
|
||||
import { Cart } from '@woocommerce/type-defs/cart';
|
||||
import { createRoot } from '@wordpress/element';
|
||||
import NoticeBanner from '@woocommerce/base-components/notice-banner';
|
||||
|
||||
type Context = {
|
||||
woocommerce: {
|
||||
isLoading: boolean;
|
||||
addToCartText: string;
|
||||
productId: number;
|
||||
displayViewCart: boolean;
|
||||
quantityToAdd: number;
|
||||
temporaryNumberOfItems: number;
|
||||
animationStatus: AnimationStatus;
|
||||
};
|
||||
};
|
||||
|
||||
enum AnimationStatus {
|
||||
IDLE = 'IDLE',
|
||||
SLIDE_OUT = 'SLIDE-OUT',
|
||||
SLIDE_IN = 'SLIDE-IN',
|
||||
}
|
||||
|
||||
type State = {
|
||||
woocommerce: {
|
||||
cart: Cart | undefined;
|
||||
inTheCartText: string;
|
||||
};
|
||||
};
|
||||
|
||||
type Store = {
|
||||
state: State;
|
||||
context: Context;
|
||||
selectors: any;
|
||||
ref: HTMLElement;
|
||||
};
|
||||
|
||||
const storeNoticeClass = '.wc-block-store-notices';
|
||||
|
||||
const createNoticeContainer = () => {
|
||||
const noticeContainer = document.createElement( 'div' );
|
||||
noticeContainer.classList.add( storeNoticeClass.replace( '.', '' ) );
|
||||
return noticeContainer;
|
||||
};
|
||||
|
||||
const injectNotice = ( domNode: Element, errorMessage: string ) => {
|
||||
const root = createRoot( domNode );
|
||||
|
||||
root.render(
|
||||
<NoticeBanner status="error" onRemove={ () => root.unmount() }>
|
||||
{ errorMessage }
|
||||
</NoticeBanner>
|
||||
);
|
||||
|
||||
domNode?.scrollIntoView( {
|
||||
behavior: 'smooth',
|
||||
inline: 'nearest',
|
||||
} );
|
||||
};
|
||||
|
||||
// RequestIdleCallback is not available in Safari, so we use setTimeout as an alternative.
|
||||
const callIdleCallback =
|
||||
window.requestIdleCallback || ( ( cb ) => setTimeout( cb, 100 ) );
|
||||
|
||||
const getProductById = ( cartState: Cart | undefined, productId: number ) => {
|
||||
return cartState?.items.find( ( item ) => item.id === productId );
|
||||
};
|
||||
|
||||
const getTextButton = ( {
|
||||
addToCartText,
|
||||
inTheCartText,
|
||||
numberOfItems,
|
||||
}: {
|
||||
addToCartText: string;
|
||||
inTheCartText: string;
|
||||
numberOfItems: number;
|
||||
} ) => {
|
||||
if ( numberOfItems === 0 ) {
|
||||
return addToCartText;
|
||||
}
|
||||
return inTheCartText.replace( '###', numberOfItems.toString() );
|
||||
};
|
||||
|
||||
const productButtonSelectors = {
|
||||
woocommerce: {
|
||||
addToCartText: ( store: Store ) => {
|
||||
const { context, state, selectors } = store;
|
||||
|
||||
// We use the temporary number of items when there's no animation, or the
|
||||
// second part of the animation hasn't started.
|
||||
if (
|
||||
context.woocommerce.animationStatus === AnimationStatus.IDLE ||
|
||||
context.woocommerce.animationStatus ===
|
||||
AnimationStatus.SLIDE_OUT
|
||||
) {
|
||||
return getTextButton( {
|
||||
addToCartText: context.woocommerce.addToCartText,
|
||||
inTheCartText: state.woocommerce.inTheCartText,
|
||||
numberOfItems: context.woocommerce.temporaryNumberOfItems,
|
||||
} );
|
||||
}
|
||||
|
||||
return getTextButton( {
|
||||
addToCartText: context.woocommerce.addToCartText,
|
||||
inTheCartText: state.woocommerce.inTheCartText,
|
||||
numberOfItems:
|
||||
selectors.woocommerce.numberOfItemsInTheCart( store ),
|
||||
} );
|
||||
},
|
||||
displayViewCart: ( store: Store ) => {
|
||||
const { context, selectors } = store;
|
||||
if ( ! context.woocommerce.displayViewCart ) return false;
|
||||
if ( ! selectors.woocommerce.hasCartLoaded( store ) ) {
|
||||
return context.woocommerce.temporaryNumberOfItems > 0;
|
||||
}
|
||||
return selectors.woocommerce.numberOfItemsInTheCart( store ) > 0;
|
||||
},
|
||||
hasCartLoaded: ( { state }: { state: State } ) => {
|
||||
return state.woocommerce.cart !== undefined;
|
||||
},
|
||||
numberOfItemsInTheCart: ( { state, context }: Store ) => {
|
||||
const product = getProductById(
|
||||
state.woocommerce.cart,
|
||||
context.woocommerce.productId
|
||||
);
|
||||
return product?.quantity || 0;
|
||||
},
|
||||
slideOutAnimation: ( { context }: Store ) =>
|
||||
context.woocommerce.animationStatus === AnimationStatus.SLIDE_OUT,
|
||||
slideInAnimation: ( { context }: Store ) =>
|
||||
context.woocommerce.animationStatus === AnimationStatus.SLIDE_IN,
|
||||
},
|
||||
};
|
||||
|
||||
interactivityStore(
|
||||
// @ts-expect-error: Store function isn't typed.
|
||||
{
|
||||
selectors: productButtonSelectors,
|
||||
actions: {
|
||||
woocommerce: {
|
||||
addToCart: async ( store: Store ) => {
|
||||
const { context, selectors, ref } = store;
|
||||
|
||||
if ( ! ref.classList.contains( 'ajax_add_to_cart' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.woocommerce.isLoading = true;
|
||||
|
||||
// Allow 3rd parties to validate and quit early.
|
||||
// https://github.com/woocommerce/woocommerce/blob/154dd236499d8a440edf3cde712511b56baa8e45/plugins/woocommerce/client/legacy/js/frontend/add-to-cart.js/#L74-L77
|
||||
const event = new CustomEvent(
|
||||
'should_send_ajax_request.adding_to_cart',
|
||||
{ detail: [ ref ], cancelable: true }
|
||||
);
|
||||
const shouldSendRequest =
|
||||
document.body.dispatchEvent( event );
|
||||
|
||||
if ( shouldSendRequest === false ) {
|
||||
const ajaxNotSentEvent = new CustomEvent(
|
||||
'ajax_request_not_sent.adding_to_cart',
|
||||
{ detail: [ false, false, ref ] }
|
||||
);
|
||||
document.body.dispatchEvent( ajaxNotSentEvent );
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
await dispatch( storeKey ).addItemToCart(
|
||||
context.woocommerce.productId,
|
||||
context.woocommerce.quantityToAdd
|
||||
);
|
||||
|
||||
// After the cart has been updated, sync the temporary number of
|
||||
// items again.
|
||||
context.woocommerce.temporaryNumberOfItems =
|
||||
selectors.woocommerce.numberOfItemsInTheCart(
|
||||
store
|
||||
);
|
||||
} catch ( error ) {
|
||||
const storeNoticeBlock =
|
||||
document.querySelector( storeNoticeClass );
|
||||
|
||||
if ( ! storeNoticeBlock ) {
|
||||
document
|
||||
.querySelector( '.entry-content' )
|
||||
?.prepend( createNoticeContainer() );
|
||||
}
|
||||
|
||||
const domNode =
|
||||
storeNoticeBlock ??
|
||||
document.querySelector( storeNoticeClass );
|
||||
|
||||
if ( domNode ) {
|
||||
injectNotice( domNode, error.message );
|
||||
}
|
||||
|
||||
// We don't care about errors blocking execution, but will
|
||||
// console.error for troubleshooting.
|
||||
// eslint-disable-next-line no-console
|
||||
console.error( error );
|
||||
} finally {
|
||||
context.woocommerce.displayViewCart = true;
|
||||
context.woocommerce.isLoading = false;
|
||||
}
|
||||
},
|
||||
handleAnimationEnd: (
|
||||
store: Store & { event: AnimationEvent }
|
||||
) => {
|
||||
const { event, context, selectors } = store;
|
||||
if ( event.animationName === 'slideOut' ) {
|
||||
// When the first part of the animation (slide-out) ends, we move
|
||||
// to the second part (slide-in).
|
||||
context.woocommerce.animationStatus =
|
||||
AnimationStatus.SLIDE_IN;
|
||||
} else if ( event.animationName === 'slideIn' ) {
|
||||
// When the second part of the animation ends, we update the
|
||||
// temporary number of items to sync it with the cart and reset the
|
||||
// animation status so it can be triggered again.
|
||||
context.woocommerce.temporaryNumberOfItems =
|
||||
selectors.woocommerce.numberOfItemsInTheCart(
|
||||
store
|
||||
);
|
||||
context.woocommerce.animationStatus =
|
||||
AnimationStatus.IDLE;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
init: {
|
||||
woocommerce: {
|
||||
syncTemporaryNumberOfItemsOnLoad: ( store: Store ) => {
|
||||
const { selectors, context } = store;
|
||||
// If the cart has loaded when we instantiate this element, we sync
|
||||
// the temporary number of items with the number of items in the cart
|
||||
// to avoid triggering the animation. We do this only once, but we
|
||||
// use useLayoutEffect to avoid the useEffect flickering.
|
||||
if ( selectors.woocommerce.hasCartLoaded( store ) ) {
|
||||
context.woocommerce.temporaryNumberOfItems =
|
||||
selectors.woocommerce.numberOfItemsInTheCart(
|
||||
store
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
effects: {
|
||||
woocommerce: {
|
||||
startAnimation: ( store: Store ) => {
|
||||
const { context, selectors } = store;
|
||||
// We start the animation if the cart has loaded, the temporary number
|
||||
// of items is out of sync with the number of items in the cart, the
|
||||
// button is not loading (because that means the user started the
|
||||
// interaction) and the animation hasn't started yet.
|
||||
if (
|
||||
selectors.woocommerce.hasCartLoaded( store ) &&
|
||||
context.woocommerce.temporaryNumberOfItems !==
|
||||
selectors.woocommerce.numberOfItemsInTheCart(
|
||||
store
|
||||
) &&
|
||||
! context.woocommerce.isLoading &&
|
||||
context.woocommerce.animationStatus ===
|
||||
AnimationStatus.IDLE
|
||||
) {
|
||||
context.woocommerce.animationStatus =
|
||||
AnimationStatus.SLIDE_OUT;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
afterLoad: ( store: Store ) => {
|
||||
const { state, selectors } = store;
|
||||
// Subscribe to changes in Cart data.
|
||||
subscribe( () => {
|
||||
const cartData = select( storeKey ).getCartData();
|
||||
const isResolutionFinished =
|
||||
select( storeKey ).hasFinishedResolution( 'getCartData' );
|
||||
if ( isResolutionFinished ) {
|
||||
state.woocommerce.cart = cartData;
|
||||
}
|
||||
}, storeKey );
|
||||
|
||||
// This selector triggers a fetch of the Cart data. It is done in a
|
||||
// `requestIdleCallback` to avoid potential performance issues.
|
||||
callIdleCallback( () => {
|
||||
if ( ! selectors.woocommerce.hasCartLoaded( store ) ) {
|
||||
select( storeKey ).getCartData();
|
||||
}
|
||||
} );
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -1,15 +1,87 @@
|
||||
.wp-block-button.wc-block-components-product-button {
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $gap-small;
|
||||
|
||||
.wp-block-button__link {
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
// Set button font size and padding so it inherits from parent.
|
||||
padding: 0.5em 1em;
|
||||
font-size: 1em;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
&.loading::after {
|
||||
font-family: WooCommerce; /* stylelint-disable-line */
|
||||
content: "\e031";
|
||||
animation: spin 2s linear infinite;
|
||||
margin-left: 0.5em;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
a[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateY(90%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translate(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.align-left {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&.align-right {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.wc-block-components-product-button__button {
|
||||
border-style: none;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
width: 150px;
|
||||
overflow: hidden;
|
||||
|
||||
span {
|
||||
|
||||
&.wc-block-slide-out {
|
||||
animation: slideOut 0.1s linear 1 normal forwards;
|
||||
}
|
||||
&.wc-block-slide-in {
|
||||
animation: slideIn 0.1s linear 1 normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-product-button__button--placeholder {
|
||||
|
||||
@@ -47,6 +47,9 @@ export const blockAttributes: BlockAttributes = {
|
||||
type: 'string',
|
||||
default: 'cover',
|
||||
},
|
||||
aspectRatio: {
|
||||
type: 'string',
|
||||
},
|
||||
};
|
||||
|
||||
export default blockAttributes;
|
||||
|
||||
@@ -49,6 +49,7 @@ interface ImageProps {
|
||||
scale: string;
|
||||
width?: string | undefined;
|
||||
height?: string | undefined;
|
||||
aspectRatio: string | undefined;
|
||||
}
|
||||
|
||||
const Image = ( {
|
||||
@@ -59,6 +60,7 @@ const Image = ( {
|
||||
width,
|
||||
scale,
|
||||
height,
|
||||
aspectRatio,
|
||||
}: ImageProps ): JSX.Element => {
|
||||
const { thumbnail, src, srcset, sizes, alt } = image || {};
|
||||
const imageProps = {
|
||||
@@ -72,6 +74,7 @@ const Image = ( {
|
||||
height,
|
||||
width,
|
||||
objectFit: scale,
|
||||
aspectRatio,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -101,6 +104,7 @@ export const Block = ( props: Props ): JSX.Element | null => {
|
||||
height,
|
||||
width,
|
||||
scale,
|
||||
aspectRatio,
|
||||
...restProps
|
||||
} = props;
|
||||
const styleProps = useStyleProps( props );
|
||||
@@ -120,6 +124,7 @@ export const Block = ( props: Props ): JSX.Element | null => {
|
||||
},
|
||||
styleProps.className
|
||||
) }
|
||||
style={ styleProps.style }
|
||||
>
|
||||
<ImagePlaceholder />
|
||||
</div>
|
||||
@@ -153,6 +158,7 @@ export const Block = ( props: Props ): JSX.Element | null => {
|
||||
},
|
||||
styleProps.className
|
||||
) }
|
||||
style={ styleProps.style }
|
||||
>
|
||||
<ParentComponent { ...( showProductLink && anchorProps ) }>
|
||||
{ !! showSaleBadge && (
|
||||
@@ -169,6 +175,7 @@ export const Block = ( props: Props ): JSX.Element | null => {
|
||||
width={ width }
|
||||
height={ height }
|
||||
scale={ scale }
|
||||
aspectRatio={ aspectRatio }
|
||||
/>
|
||||
</ParentComponent>
|
||||
</div>
|
||||
|
||||
@@ -54,7 +54,7 @@ const Edit = ( {
|
||||
const blockProps = useBlockProps( { style: { width, height } } );
|
||||
const isDescendentOfQueryLoop = Number.isFinite( context.queryId );
|
||||
const isBlockThemeEnabled = getSettingWithCoercion(
|
||||
'is_block_theme_enabled',
|
||||
'isBlockThemeEnabled',
|
||||
false,
|
||||
isBoolean
|
||||
);
|
||||
|
||||
@@ -24,4 +24,6 @@ export interface BlockAttributes {
|
||||
width?: string;
|
||||
// Image scaling method.
|
||||
scale: 'cover' | 'contain' | 'fill';
|
||||
// Aspect ratio of the image.
|
||||
aspectRatio: string;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
"version": "1.0.0",
|
||||
"icon": "info",
|
||||
"title": "Product Details",
|
||||
"description": "Display a product’s description, attributes, and reviews.",
|
||||
"description": "Display a product's description, attributes, and reviews.",
|
||||
"category": "woocommerce",
|
||||
"keywords": [ "WooCommerce" ],
|
||||
"supports": {
|
||||
"align": true
|
||||
"align": true,
|
||||
"spacing": {
|
||||
"margin": true
|
||||
}
|
||||
},
|
||||
"textdomain": "woocommerce",
|
||||
"apiVersion": 2,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
border-radius: 4px 4px 0 0;
|
||||
border-radius: $universal-border-radius $universal-border-radius 0 0;
|
||||
margin: 0;
|
||||
padding: 0.5em 1em;
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "woocommerce/product-rating-counter",
|
||||
"version": "1.0.0",
|
||||
"title": "Product Rating Counter",
|
||||
"description": "Display the review count of a product",
|
||||
"attributes": {
|
||||
"productId": {
|
||||
"type": "number",
|
||||
"default": 0
|
||||
},
|
||||
"isDescendentOfQueryLoop": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"textAlign": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"isDescendentOfSingleProductBlock": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"isDescendentOfSingleProductTemplate": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"usesContext": [ "query", "queryId", "postId" ],
|
||||
"category": "woocommerce",
|
||||
"keywords": [ "WooCommerce" ],
|
||||
"supports": {
|
||||
"align": true
|
||||
},
|
||||
"ancestor": [ "woocommerce/single-product" ],
|
||||
"textdomain": "woocommerce",
|
||||
"apiVersion": 2,
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json"
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import classnames from 'classnames';
|
||||
import {
|
||||
useInnerBlockLayoutContext,
|
||||
useProductDataContext,
|
||||
} from '@woocommerce/shared-context';
|
||||
import { useStyleProps } from '@woocommerce/base-hooks';
|
||||
import { withProductDataContext } from '@woocommerce/shared-hocs';
|
||||
import { isNumber, ProductResponseItem } from '@woocommerce/types';
|
||||
import { Disabled } from '@wordpress/components';
|
||||
|
||||
const getRatingCount = ( product: ProductResponseItem ) => {
|
||||
const count = isNumber( product.review_count )
|
||||
? product.review_count
|
||||
: parseInt( product.review_count, 10 );
|
||||
|
||||
return Number.isFinite( count ) && count > 0 ? count : 0;
|
||||
};
|
||||
|
||||
const ReviewsCount = ( props: { reviews: number } ): JSX.Element => {
|
||||
const { reviews } = props;
|
||||
|
||||
const reviewsCount = reviews
|
||||
? sprintf(
|
||||
/* translators: %s is referring to the total of reviews for a product */
|
||||
_n(
|
||||
'(%s customer review)',
|
||||
'(%s customer reviews)',
|
||||
reviews,
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
reviews
|
||||
)
|
||||
: __( '(X customer reviews)', 'woo-gutenberg-products-block' );
|
||||
|
||||
return (
|
||||
<span className="wc-block-components-product-rating-counter__reviews_count">
|
||||
<Disabled>
|
||||
<a href="/">{ reviewsCount }</a>
|
||||
</Disabled>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
type ProductRatingCounterProps = {
|
||||
className?: string;
|
||||
textAlign?: string;
|
||||
isDescendentOfSingleProductBlock: boolean;
|
||||
isDescendentOfQueryLoop: boolean;
|
||||
postId: number;
|
||||
productId: number;
|
||||
shouldDisplayMockedReviewsWhenProductHasNoReviews: boolean;
|
||||
};
|
||||
|
||||
export const Block = (
|
||||
props: ProductRatingCounterProps
|
||||
): JSX.Element | undefined => {
|
||||
const { textAlign, shouldDisplayMockedReviewsWhenProductHasNoReviews } =
|
||||
props;
|
||||
const styleProps = useStyleProps( props );
|
||||
const { parentClassName } = useInnerBlockLayoutContext();
|
||||
const { product } = useProductDataContext();
|
||||
const reviews = getRatingCount( product );
|
||||
|
||||
const className = classnames(
|
||||
styleProps.className,
|
||||
'wc-block-components-product-rating-counter',
|
||||
{
|
||||
[ `${ parentClassName }__product-rating` ]: parentClassName,
|
||||
[ `has-text-align-${ textAlign }` ]: textAlign,
|
||||
}
|
||||
);
|
||||
|
||||
if ( reviews || shouldDisplayMockedReviewsWhenProductHasNoReviews ) {
|
||||
return (
|
||||
<div className={ className } style={ styleProps.style }>
|
||||
<div className="wc-block-components-product-rating-counter__container">
|
||||
<ReviewsCount reviews={ reviews } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default withProductDataContext( Block );
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
AlignmentToolbar,
|
||||
BlockControls,
|
||||
useBlockProps,
|
||||
} from '@wordpress/block-editor';
|
||||
import type { BlockEditProps } from '@wordpress/blocks';
|
||||
import { useEffect } from '@wordpress/element';
|
||||
import { ProductQueryContext as Context } from '@woocommerce/blocks/product-query/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
import { BlockAttributes } from './types';
|
||||
import { useIsDescendentOfSingleProductBlock } from '../shared/use-is-descendent-of-single-product-block';
|
||||
import { useIsDescendentOfSingleProductTemplate } from '../shared/use-is-descendent-of-single-product-template';
|
||||
|
||||
const Edit = (
|
||||
props: BlockEditProps< BlockAttributes > & { context: Context }
|
||||
): JSX.Element => {
|
||||
const { attributes, setAttributes, context } = props;
|
||||
const blockProps = useBlockProps( {
|
||||
className: 'wp-block-woocommerce-product-rating-counter',
|
||||
} );
|
||||
const blockAttrs = {
|
||||
...attributes,
|
||||
...context,
|
||||
shouldDisplayMockedReviewsWhenProductHasNoReviews: true,
|
||||
};
|
||||
const isDescendentOfQueryLoop = Number.isFinite( context.queryId );
|
||||
const { isDescendentOfSingleProductBlock } =
|
||||
useIsDescendentOfSingleProductBlock( {
|
||||
blockClientId: blockProps?.id,
|
||||
} );
|
||||
let { isDescendentOfSingleProductTemplate } =
|
||||
useIsDescendentOfSingleProductTemplate();
|
||||
|
||||
if ( isDescendentOfQueryLoop || isDescendentOfSingleProductBlock ) {
|
||||
isDescendentOfSingleProductTemplate = false;
|
||||
}
|
||||
|
||||
useEffect( () => {
|
||||
setAttributes( {
|
||||
isDescendentOfQueryLoop,
|
||||
isDescendentOfSingleProductBlock,
|
||||
isDescendentOfSingleProductTemplate,
|
||||
} );
|
||||
}, [
|
||||
setAttributes,
|
||||
isDescendentOfQueryLoop,
|
||||
isDescendentOfSingleProductBlock,
|
||||
isDescendentOfSingleProductTemplate,
|
||||
] );
|
||||
|
||||
return (
|
||||
<>
|
||||
<BlockControls>
|
||||
<AlignmentToolbar
|
||||
value={ attributes.textAlign }
|
||||
onChange={ ( newAlign ) => {
|
||||
setAttributes( { textAlign: newAlign || '' } );
|
||||
} }
|
||||
/>
|
||||
</BlockControls>
|
||||
<div { ...blockProps }>
|
||||
<Block { ...blockAttrs } />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Edit;
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
import { Icon, starFilled } from '@wordpress/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import metadata from './block.json';
|
||||
import edit from './edit';
|
||||
import { supports } from './support';
|
||||
|
||||
registerBlockType( metadata, {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ starFilled }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
supports,
|
||||
edit,
|
||||
} );
|
||||
@@ -0,0 +1,24 @@
|
||||
/* eslint-disable @wordpress/no-unsafe-wp-apis */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
|
||||
|
||||
export const supports = {
|
||||
...( isFeaturePluginBuild() && {
|
||||
color: {
|
||||
text: false,
|
||||
background: false,
|
||||
link: true,
|
||||
},
|
||||
spacing: {
|
||||
margin: true,
|
||||
padding: true,
|
||||
},
|
||||
typography: {
|
||||
fontSize: true,
|
||||
__experimentalSkipSerialization: true,
|
||||
},
|
||||
__experimentalSelector: '.wc-block-components-product-rating-counter',
|
||||
} ),
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface BlockAttributes {
|
||||
productId: number;
|
||||
isDescendentOfQueryLoop: boolean;
|
||||
isDescendentOfSingleProductBlock: boolean;
|
||||
isDescendentOfSingleProductTemplate: boolean;
|
||||
textAlign: string;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "woocommerce/product-rating-stars",
|
||||
"version": "1.0.0",
|
||||
"icon": "info",
|
||||
"title": "Product Rating Stars",
|
||||
"description": "Display the average rating of a product with stars",
|
||||
"attributes": {
|
||||
@@ -32,6 +31,7 @@
|
||||
"supports": {
|
||||
"align": true
|
||||
},
|
||||
"ancestor": [ "woocommerce/single-product" ],
|
||||
"textdomain": "woocommerce",
|
||||
"apiVersion": 2,
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json"
|
||||
|
||||
@@ -50,12 +50,14 @@ const NoRating = ( { parentClassName }: { parentClassName: string } ) => {
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
'wc-block-components-product-rating__norating-container',
|
||||
`${ parentClassName }-product-rating__norating-container`
|
||||
'wc-block-components-product-rating-stars__norating-container',
|
||||
`${ parentClassName }-product-rating-stars__norating-container`
|
||||
) }
|
||||
>
|
||||
<div
|
||||
className={ 'wc-block-components-product-rating__norating' }
|
||||
className={
|
||||
'wc-block-components-product-rating-stars__norating'
|
||||
}
|
||||
role="img"
|
||||
>
|
||||
<span style={ starStyle } />
|
||||
@@ -92,8 +94,8 @@ const Rating = ( props: RatingProps ): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
'wc-block-components-product-rating__stars',
|
||||
`${ parentClassName }__product-rating__stars`
|
||||
'wc-block-components-product-rating-stars__stars',
|
||||
`${ parentClassName }__product-rating-stars__stars`
|
||||
) }
|
||||
role="img"
|
||||
aria-label={ ratingText }
|
||||
@@ -124,7 +126,7 @@ export const Block = ( props: ProductRatingStarsProps ): JSX.Element | null => {
|
||||
|
||||
const className = classnames(
|
||||
styleProps.className,
|
||||
'wc-block-components-product-rating',
|
||||
'wc-block-components-product-rating-stars',
|
||||
{
|
||||
[ `${ parentClassName }__product-rating` ]: parentClassName,
|
||||
[ `has-text-align-${ textAlign }` ]: textAlign,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Icon, starFilled } from '@wordpress/icons';
|
||||
|
||||
export const BLOCK_TITLE: string = __(
|
||||
'Product Rating Stars',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
export const BLOCK_ICON: JSX.Element = (
|
||||
<Icon
|
||||
icon={ starFilled }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
);
|
||||
export const BLOCK_DESCRIPTION: string = __(
|
||||
'Display the average rating of a product with stars',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
@@ -1,36 +1,25 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { BlockConfiguration } from '@wordpress/blocks';
|
||||
import { registerBlockSingleProductTemplate } from '@woocommerce/atomic-utils';
|
||||
import { isExperimentalBuild } from '@woocommerce/block-settings';
|
||||
import { registerBlockType } from '@wordpress/blocks';
|
||||
import { Icon, starFilled } from '@wordpress/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import metadata from './block.json';
|
||||
import edit from './edit';
|
||||
import sharedConfig from '../shared/config';
|
||||
import { supports } from './support';
|
||||
import { BLOCK_ICON } from './constants';
|
||||
|
||||
const blockConfig: BlockConfiguration = {
|
||||
...sharedConfig,
|
||||
ancestor: [
|
||||
'woocommerce/all-products',
|
||||
'woocommerce/single-product',
|
||||
'core/post-template',
|
||||
'woocommerce/product-template',
|
||||
],
|
||||
icon: { src: BLOCK_ICON },
|
||||
registerBlockType( metadata, {
|
||||
icon: {
|
||||
src: (
|
||||
<Icon
|
||||
icon={ starFilled }
|
||||
className="wc-block-editor-components-block-icon"
|
||||
/>
|
||||
),
|
||||
},
|
||||
supports,
|
||||
edit,
|
||||
};
|
||||
|
||||
if ( isExperimentalBuild() ) {
|
||||
registerBlockSingleProductTemplate( {
|
||||
blockName: 'woocommerce/product-rating-stars',
|
||||
blockMetadata: metadata,
|
||||
blockSettings: blockConfig,
|
||||
} );
|
||||
}
|
||||
} );
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.wc-block-components-product-rating {
|
||||
.wc-block-components-product-rating-stars {
|
||||
display: block;
|
||||
line-height: 1;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
|
||||
font-family: star;
|
||||
font-weight: 400;
|
||||
text-align: left;
|
||||
|
||||
&::before {
|
||||
content: "\53\53\53\53\53";
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import { isFeaturePluginBuild } from '@woocommerce/block-settings';
|
||||
import { __experimentalGetSpacingClassesAndStyles } from '@wordpress/block-editor';
|
||||
|
||||
export const supports = {
|
||||
...( isFeaturePluginBuild() && {
|
||||
@@ -23,10 +22,4 @@ export const supports = {
|
||||
},
|
||||
__experimentalSelector: '.wc-block-components-product-rating',
|
||||
} ),
|
||||
...( ! isFeaturePluginBuild() &&
|
||||
typeof __experimentalGetSpacingClassesAndStyles === 'function' && {
|
||||
spacing: {
|
||||
margin: true,
|
||||
},
|
||||
} ),
|
||||
};
|
||||
|
||||
@@ -11,6 +11,11 @@ import { useStyleProps } from '@woocommerce/base-hooks';
|
||||
import { withProductDataContext } from '@woocommerce/shared-hocs';
|
||||
import { isNumber, ProductResponseItem } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
type RatingProps = {
|
||||
reviews: number;
|
||||
rating: number;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.wc-block-components-product-rating {
|
||||
.wc-block-components-product-rating__container {
|
||||
> * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-product-rating__stars {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,10 @@ export const blockAttributes: BlockAttributes = {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
isDescendentOfSingleProductTemplate: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default blockAttributes;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classnames from 'classnames';
|
||||
import Label from '@woocommerce/base-components/label';
|
||||
import { Label } from '@woocommerce/blocks-components';
|
||||
import {
|
||||
useInnerBlockLayoutContext,
|
||||
useProductDataContext,
|
||||
@@ -26,7 +26,10 @@ export const Block = ( props: Props ): JSX.Element | null => {
|
||||
const { parentClassName } = useInnerBlockLayoutContext();
|
||||
const { product } = useProductDataContext();
|
||||
|
||||
if ( ! product.id || ! product.on_sale ) {
|
||||
if (
|
||||
( ! product.id || ! product.on_sale ) &&
|
||||
! props.isDescendentOfSingleProductTemplate
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,13 +10,8 @@ import { useEffect } from '@wordpress/element';
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Block from './block';
|
||||
import withProductSelector from '../shared/with-product-selector';
|
||||
import {
|
||||
BLOCK_TITLE as label,
|
||||
BLOCK_ICON as icon,
|
||||
BLOCK_DESCRIPTION as description,
|
||||
} from './constants';
|
||||
import type { BlockAttributes } from './types';
|
||||
import { useIsDescendentOfSingleProductTemplate } from '../shared/use-is-descendent-of-single-product-template';
|
||||
|
||||
const Edit = ( {
|
||||
attributes,
|
||||
@@ -31,9 +26,20 @@ const Edit = ( {
|
||||
};
|
||||
const isDescendentOfQueryLoop = Number.isFinite( context.queryId );
|
||||
|
||||
const { isDescendentOfSingleProductTemplate } =
|
||||
useIsDescendentOfSingleProductTemplate();
|
||||
|
||||
useEffect(
|
||||
() => setAttributes( { isDescendentOfQueryLoop } ),
|
||||
[ setAttributes, isDescendentOfQueryLoop ]
|
||||
() =>
|
||||
setAttributes( {
|
||||
isDescendentOfQueryLoop,
|
||||
isDescendentOfSingleProductTemplate,
|
||||
} ),
|
||||
[
|
||||
setAttributes,
|
||||
isDescendentOfQueryLoop,
|
||||
isDescendentOfSingleProductTemplate,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -43,4 +49,4 @@ const Edit = ( {
|
||||
);
|
||||
};
|
||||
|
||||
export default withProductSelector( { icon, label, description } )( Edit );
|
||||
export default Edit;
|
||||
|
||||
@@ -32,6 +32,7 @@ const blockConfig: BlockConfiguration = {
|
||||
'woocommerce/single-product',
|
||||
'core/post-template',
|
||||
'woocommerce/product-template',
|
||||
'woocommerce/product-gallery',
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
.wp-block-woocommerce-product-sale-badge {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.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;
|
||||
width: fit-content;
|
||||
border: 1px solid #43454b;
|
||||
border-radius: 3px;
|
||||
border-radius: $universal-border-radius;
|
||||
box-sizing: border-box;
|
||||
color: #43454b;
|
||||
background: #fff;
|
||||
@@ -15,6 +19,16 @@
|
||||
z-index: 9;
|
||||
position: static;
|
||||
|
||||
&--align-left {
|
||||
align-self: auto;
|
||||
}
|
||||
&--align-center {
|
||||
align-self: center;
|
||||
}
|
||||
&--align-right {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
span {
|
||||
color: inherit;
|
||||
background-color: inherit;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { __experimentalGetSpacingClassesAndStyles } from '@wordpress/block-edito
|
||||
|
||||
export const supports = {
|
||||
html: false,
|
||||
align: true,
|
||||
...( isFeaturePluginBuild() && {
|
||||
color: {
|
||||
gradients: true,
|
||||
@@ -31,6 +32,7 @@ export const supports = {
|
||||
width: true,
|
||||
__experimentalSkipSerialization: true,
|
||||
},
|
||||
// @todo: Improve styles support when WordPress 6.4 is released. https://make.wordpress.org/core/2023/07/17/introducing-the-block-selectors-api/
|
||||
...( typeof __experimentalGetSpacingClassesAndStyles === 'function' && {
|
||||
spacing: {
|
||||
margin: true,
|
||||
|
||||
@@ -2,4 +2,5 @@ export interface BlockAttributes {
|
||||
productId: number;
|
||||
align: 'left' | 'center' | 'right';
|
||||
isDescendentOfQueryLoop?: boolean | undefined;
|
||||
isDescendentOfSingleProductTemplate?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { isNumber } from '@woocommerce/types';
|
||||
import { isNumber, isEmpty } from '@woocommerce/types';
|
||||
import {
|
||||
BlockAttributes,
|
||||
BlockConfiguration,
|
||||
@@ -80,19 +80,17 @@ export const registerBlockSingleProductTemplate = ( {
|
||||
|
||||
if ( ! isBlockRegistered ) {
|
||||
if ( isVariationBlock ) {
|
||||
registerBlockVariation( blockName, {
|
||||
...blockSettings,
|
||||
// @ts-expect-error: `ancestor` key is typed in WordPress core
|
||||
ancestor: ! currentTemplateId?.includes( 'single-product' )
|
||||
? blockSettings?.ancestor
|
||||
: undefined,
|
||||
} );
|
||||
// @ts-expect-error: `registerBlockType` is not typed in WordPress core
|
||||
registerBlockVariation( blockName, blockSettings );
|
||||
} else {
|
||||
// @ts-expect-error: `registerBlockType` is typed in WordPress core
|
||||
const ancestor = isEmpty( blockSettings?.ancestor )
|
||||
? [ 'woocommerce/single-product' ]
|
||||
: blockSettings?.ancestor;
|
||||
// @ts-expect-error: `registerBlockType` is not typed in WordPress core
|
||||
registerBlockType( blockMetadata, {
|
||||
...blockSettings,
|
||||
ancestor: ! currentTemplateId?.includes( 'single-product' )
|
||||
? blockSettings?.ancestor
|
||||
? ancestor
|
||||
: undefined,
|
||||
} );
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
import { Button as WPButton } from 'wordpress-components';
|
||||
import type { Button as WPButtonType } from '@wordpress/components';
|
||||
import classNames from 'classnames';
|
||||
import Spinner from '@woocommerce/base-components/spinner';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import Spinner from '../../../../../packages/components/spinner';
|
||||
|
||||
export interface ButtonProps
|
||||
extends Omit< WPButtonType.ButtonProps, 'variant' | 'href' > {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { ValidatedTextInput, isPostcode } from '@woocommerce/blocks-checkout';
|
||||
import {
|
||||
ValidatedTextInput,
|
||||
isPostcode,
|
||||
type ValidatedTextInputHandle,
|
||||
} from '@woocommerce/blocks-checkout';
|
||||
import {
|
||||
BillingCountryInput,
|
||||
ShippingCountryInput,
|
||||
@@ -10,195 +14,111 @@ import {
|
||||
BillingStateInput,
|
||||
ShippingStateInput,
|
||||
} from '@woocommerce/base-components/state-input';
|
||||
import { useEffect, useMemo } from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { withInstanceId } from '@wordpress/compose';
|
||||
import { useEffect, useMemo, useRef } from '@wordpress/element';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
import { useShallowEqual } from '@woocommerce/base-hooks';
|
||||
import {
|
||||
AddressField,
|
||||
AddressFields,
|
||||
AddressType,
|
||||
defaultAddressFields,
|
||||
ShippingAddress,
|
||||
} from '@woocommerce/settings';
|
||||
import { useSelect, useDispatch, dispatch } from '@wordpress/data';
|
||||
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||
import { FieldValidationStatus } from '@woocommerce/types';
|
||||
import { defaultAddressFields } from '@woocommerce/settings';
|
||||
import isShallowEqual from '@wordpress/is-shallow-equal';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
AddressFormProps,
|
||||
FieldType,
|
||||
FieldConfig,
|
||||
AddressFormFields,
|
||||
} from './types';
|
||||
import prepareAddressFields from './prepare-address-fields';
|
||||
import validateShippingCountry from './validate-shipping-country';
|
||||
import customValidationHandler from './custom-validation-handler';
|
||||
|
||||
// If it's the shipping address form and the user starts entering address
|
||||
// values without having set the country first, show an error.
|
||||
const validateShippingCountry = (
|
||||
values: ShippingAddress,
|
||||
setValidationErrors: (
|
||||
errors: Record< string, FieldValidationStatus >
|
||||
) => void,
|
||||
clearValidationError: ( error: string ) => void,
|
||||
hasValidationError: boolean
|
||||
): void => {
|
||||
const validationErrorId = 'shipping_country';
|
||||
if (
|
||||
! hasValidationError &&
|
||||
! values.country &&
|
||||
( values.city || values.state || values.postcode )
|
||||
) {
|
||||
setValidationErrors( {
|
||||
[ validationErrorId ]: {
|
||||
message: __(
|
||||
'Please select a country to calculate rates.',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
hidden: false,
|
||||
},
|
||||
} );
|
||||
}
|
||||
if ( hasValidationError && values.country ) {
|
||||
clearValidationError( validationErrorId );
|
||||
}
|
||||
};
|
||||
|
||||
interface AddressFormProps {
|
||||
// Id for component.
|
||||
id?: string;
|
||||
// Unique id for form.
|
||||
instanceId: string;
|
||||
// Array of fields in form.
|
||||
fields: ( keyof AddressFields )[];
|
||||
// Field configuration for fields in form.
|
||||
fieldConfig?: Record< keyof AddressFields, Partial< AddressField > >;
|
||||
// Function to all for an form onChange event.
|
||||
onChange: ( newValue: ShippingAddress ) => void;
|
||||
// Type of form.
|
||||
type?: AddressType;
|
||||
// Values for fields.
|
||||
values: ShippingAddress;
|
||||
}
|
||||
const defaultFields = Object.keys(
|
||||
defaultAddressFields
|
||||
) as unknown as FieldType[];
|
||||
|
||||
/**
|
||||
* Checkout address form.
|
||||
*/
|
||||
const AddressForm = ( {
|
||||
id = '',
|
||||
fields = Object.keys(
|
||||
defaultAddressFields
|
||||
) as unknown as ( keyof AddressFields )[],
|
||||
fieldConfig = {} as Record< keyof AddressFields, Partial< AddressField > >,
|
||||
instanceId,
|
||||
fields = defaultFields,
|
||||
fieldConfig = {} as FieldConfig,
|
||||
onChange,
|
||||
type = 'shipping',
|
||||
values,
|
||||
}: AddressFormProps ): JSX.Element => {
|
||||
const validationErrorId = 'shipping_country';
|
||||
const { setValidationErrors, clearValidationError } =
|
||||
useDispatch( VALIDATION_STORE_KEY );
|
||||
|
||||
const countryValidationError = useSelect( ( select ) => {
|
||||
const store = select( VALIDATION_STORE_KEY );
|
||||
return store.getValidationError( validationErrorId );
|
||||
} );
|
||||
const instanceId = useInstanceId( AddressForm );
|
||||
|
||||
// Track incoming props.
|
||||
const currentFields = useShallowEqual( fields );
|
||||
const currentFieldConfig = useShallowEqual( fieldConfig );
|
||||
const currentCountry = useShallowEqual( values.country );
|
||||
|
||||
const addressFormFields = useMemo( () => {
|
||||
return prepareAddressFields(
|
||||
// Memoize the address form fields passed in from the parent component.
|
||||
const addressFormFields = useMemo( (): AddressFormFields => {
|
||||
const preparedFields = prepareAddressFields(
|
||||
currentFields,
|
||||
fieldConfig,
|
||||
values.country
|
||||
currentFieldConfig,
|
||||
currentCountry
|
||||
);
|
||||
}, [ currentFields, fieldConfig, values.country ] );
|
||||
return {
|
||||
fields: preparedFields,
|
||||
type,
|
||||
required: preparedFields.filter( ( field ) => field.required ),
|
||||
hidden: preparedFields.filter( ( field ) => field.hidden ),
|
||||
};
|
||||
}, [ currentFields, currentFieldConfig, currentCountry, type ] );
|
||||
|
||||
// Stores refs for rendered fields so we can access them later.
|
||||
const fieldsRef = useRef<
|
||||
Record< string, ValidatedTextInputHandle | null >
|
||||
>( {} );
|
||||
|
||||
// Clear values for hidden fields.
|
||||
useEffect( () => {
|
||||
addressFormFields.forEach( ( field ) => {
|
||||
if ( field.hidden && values[ field.key ] ) {
|
||||
onChange( {
|
||||
...values,
|
||||
[ field.key ]: '',
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}, [ addressFormFields, onChange, values ] );
|
||||
|
||||
// Clear postcode validation error if postcode is not required.
|
||||
useEffect( () => {
|
||||
addressFormFields.forEach( ( field ) => {
|
||||
if ( field.key === 'postcode' && field.required === false ) {
|
||||
const store = dispatch( 'wc/store/validation' );
|
||||
|
||||
if ( type === 'shipping' ) {
|
||||
store.clearValidationError( 'shipping_postcode' );
|
||||
}
|
||||
|
||||
if ( type === 'billing' ) {
|
||||
store.clearValidationError( 'billing_postcode' );
|
||||
}
|
||||
}
|
||||
} );
|
||||
}, [ addressFormFields, type, clearValidationError ] );
|
||||
const newValues = {
|
||||
...values,
|
||||
...Object.fromEntries(
|
||||
addressFormFields.hidden.map( ( field ) => [ field.key, '' ] )
|
||||
),
|
||||
};
|
||||
if ( ! isShallowEqual( values, newValues ) ) {
|
||||
onChange( newValues );
|
||||
}
|
||||
}, [ onChange, addressFormFields, values ] );
|
||||
|
||||
// Maybe validate country when other fields change so user is notified that it's required.
|
||||
useEffect( () => {
|
||||
if ( type === 'shipping' ) {
|
||||
validateShippingCountry(
|
||||
values,
|
||||
setValidationErrors,
|
||||
clearValidationError,
|
||||
!! countryValidationError?.message &&
|
||||
! countryValidationError?.hidden
|
||||
);
|
||||
validateShippingCountry( values );
|
||||
}
|
||||
}, [
|
||||
values,
|
||||
countryValidationError?.message,
|
||||
countryValidationError?.hidden,
|
||||
setValidationErrors,
|
||||
clearValidationError,
|
||||
type,
|
||||
] );
|
||||
}, [ values, type ] );
|
||||
|
||||
id = id || instanceId;
|
||||
// Changing country may change format for postcodes.
|
||||
useEffect( () => {
|
||||
fieldsRef.current?.postcode?.revalidate();
|
||||
}, [ currentCountry ] );
|
||||
|
||||
/**
|
||||
* Custom validation handler for fields with field specific handling.
|
||||
*/
|
||||
const customValidationHandler = (
|
||||
inputObject: HTMLInputElement,
|
||||
field: string,
|
||||
customValues: {
|
||||
country: string;
|
||||
}
|
||||
): boolean => {
|
||||
if (
|
||||
field === 'postcode' &&
|
||||
customValues.country &&
|
||||
! isPostcode( {
|
||||
postcode: inputObject.value,
|
||||
country: customValues.country,
|
||||
} )
|
||||
) {
|
||||
inputObject.setCustomValidity(
|
||||
__(
|
||||
'Please enter a valid postcode',
|
||||
'woo-gutenberg-products-block'
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
id = id || `${ instanceId }`;
|
||||
|
||||
return (
|
||||
<div id={ id } className="wc-block-components-address-form">
|
||||
{ addressFormFields.map( ( field ) => {
|
||||
{ addressFormFields.fields.map( ( field ) => {
|
||||
if ( field.hidden ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create a consistent error ID based on the field key and type
|
||||
const errorId = `${ type }_${ field.key }`;
|
||||
const fieldProps = {
|
||||
id: `${ id }-${ field.key }`,
|
||||
errorId: `${ type }_${ field.key }`,
|
||||
label: field.required ? field.label : field.optionalLabel,
|
||||
autoCapitalize: field.autocapitalize,
|
||||
autoComplete: field.autocomplete,
|
||||
errorMessage: field.errorMessage,
|
||||
required: field.required,
|
||||
className: `wc-block-components-address-form__${ field.key }`,
|
||||
};
|
||||
|
||||
if ( field.key === 'country' ) {
|
||||
const Tag =
|
||||
@@ -208,24 +128,26 @@ const AddressForm = ( {
|
||||
return (
|
||||
<Tag
|
||||
key={ field.key }
|
||||
id={ `${ id }-${ field.key }` }
|
||||
errorId={ errorId }
|
||||
label={
|
||||
field.required
|
||||
? field.label
|
||||
: field.optionalLabel
|
||||
}
|
||||
{ ...fieldProps }
|
||||
value={ values.country }
|
||||
autoComplete={ field.autocomplete }
|
||||
onChange={ ( newValue ) =>
|
||||
onChange( {
|
||||
onChange={ ( newCountry ) => {
|
||||
const newValues = {
|
||||
...values,
|
||||
country: newValue,
|
||||
country: newCountry,
|
||||
state: '',
|
||||
} )
|
||||
}
|
||||
errorMessage={ field.errorMessage }
|
||||
required={ field.required }
|
||||
};
|
||||
// Country will impact postcode too. Do we need to clear it?
|
||||
if (
|
||||
values.postcode &&
|
||||
! isPostcode( {
|
||||
postcode: values.postcode,
|
||||
country: newCountry,
|
||||
} )
|
||||
) {
|
||||
newValues.postcode = '';
|
||||
}
|
||||
onChange( newValues );
|
||||
} }
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -238,24 +160,15 @@ const AddressForm = ( {
|
||||
return (
|
||||
<Tag
|
||||
key={ field.key }
|
||||
id={ `${ id }-${ field.key }` }
|
||||
errorId={ errorId }
|
||||
{ ...fieldProps }
|
||||
country={ values.country }
|
||||
label={
|
||||
field.required
|
||||
? field.label
|
||||
: field.optionalLabel
|
||||
}
|
||||
value={ values.state }
|
||||
autoComplete={ field.autocomplete }
|
||||
onChange={ ( newValue ) =>
|
||||
onChange( {
|
||||
...values,
|
||||
state: newValue,
|
||||
} )
|
||||
}
|
||||
errorMessage={ field.errorMessage }
|
||||
required={ field.required }
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -263,35 +176,30 @@ const AddressForm = ( {
|
||||
return (
|
||||
<ValidatedTextInput
|
||||
key={ field.key }
|
||||
id={ `${ id }-${ field.key }` }
|
||||
errorId={ errorId }
|
||||
className={ `wc-block-components-address-form__${ field.key }` }
|
||||
label={
|
||||
field.required ? field.label : field.optionalLabel
|
||||
ref={ ( el ) =>
|
||||
( fieldsRef.current[ field.key ] = el )
|
||||
}
|
||||
{ ...fieldProps }
|
||||
value={ values[ field.key ] }
|
||||
autoCapitalize={ field.autocapitalize }
|
||||
autoComplete={ field.autocomplete }
|
||||
onChange={ ( newValue: string ) =>
|
||||
onChange( {
|
||||
...values,
|
||||
[ field.key ]:
|
||||
field.key === 'postcode'
|
||||
? newValue.trimStart().toUpperCase()
|
||||
: newValue,
|
||||
[ field.key ]: newValue,
|
||||
} )
|
||||
}
|
||||
customFormatter={ ( value: string ) => {
|
||||
if ( field.key === 'postcode' ) {
|
||||
return value.trimStart().toUpperCase();
|
||||
}
|
||||
return value;
|
||||
} }
|
||||
customValidation={ ( inputObject: HTMLInputElement ) =>
|
||||
field.required || inputObject.value
|
||||
? customValidationHandler(
|
||||
inputObject,
|
||||
field.key,
|
||||
values
|
||||
)
|
||||
: true
|
||||
customValidationHandler(
|
||||
inputObject,
|
||||
field.key,
|
||||
values
|
||||
)
|
||||
}
|
||||
errorMessage={ field.errorMessage }
|
||||
required={ field.required }
|
||||
/>
|
||||
);
|
||||
} ) }
|
||||
@@ -299,4 +207,4 @@ const AddressForm = ( {
|
||||
);
|
||||
};
|
||||
|
||||
export default withInstanceId( AddressForm );
|
||||
export default AddressForm;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { isPostcode } from '@woocommerce/blocks-checkout';
|
||||
|
||||
/**
|
||||
* Custom validation handler for fields with field specific handling.
|
||||
*/
|
||||
const customValidationHandler = (
|
||||
inputObject: HTMLInputElement,
|
||||
field: string,
|
||||
customValues: {
|
||||
country: string;
|
||||
}
|
||||
): boolean => {
|
||||
// Pass validation if the field is not required and is empty.
|
||||
if ( ! inputObject.required && ! inputObject.value ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
field === 'postcode' &&
|
||||
customValues.country &&
|
||||
! isPostcode( {
|
||||
postcode: inputObject.value,
|
||||
country: customValues.country,
|
||||
} )
|
||||
) {
|
||||
inputObject.setCustomValidity(
|
||||
__(
|
||||
'Please enter a valid postcode',
|
||||
'woo-gutenberg-products-block'
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export default customValidationHandler;
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type {
|
||||
AddressField,
|
||||
AddressFields,
|
||||
AddressType,
|
||||
ShippingAddress,
|
||||
KeyedAddressField,
|
||||
} from '@woocommerce/settings';
|
||||
|
||||
export type FieldConfig = Record<
|
||||
keyof AddressFields,
|
||||
Partial< AddressField >
|
||||
>;
|
||||
|
||||
export type FieldType = keyof AddressFields;
|
||||
|
||||
export type AddressFormFields = {
|
||||
fields: KeyedAddressField[];
|
||||
type: AddressType;
|
||||
required: KeyedAddressField[];
|
||||
hidden: KeyedAddressField[];
|
||||
};
|
||||
|
||||
export interface AddressFormProps {
|
||||
// Id for component.
|
||||
id?: string;
|
||||
// Type of form (billing or shipping).
|
||||
type?: AddressType;
|
||||
// Array of fields in form.
|
||||
fields: FieldType[];
|
||||
// Field configuration for fields in form.
|
||||
fieldConfig?: FieldConfig;
|
||||
// Called with the new address data when the address form changes. This is only called when all required fields are filled and there are no validation errors.
|
||||
onChange: ( newValue: ShippingAddress ) => void;
|
||||
// Values for fields.
|
||||
values: ShippingAddress;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { type ShippingAddress } from '@woocommerce/settings';
|
||||
import { select, dispatch } from '@wordpress/data';
|
||||
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||
|
||||
// If it's the shipping address form and the user starts entering address
|
||||
// values without having set the country first, show an error.
|
||||
const validateShippingCountry = ( values: ShippingAddress ): void => {
|
||||
const validationErrorId = 'shipping_country';
|
||||
const hasValidationError =
|
||||
select( VALIDATION_STORE_KEY ).getValidationError( validationErrorId );
|
||||
if (
|
||||
! values.country &&
|
||||
( values.city || values.state || values.postcode )
|
||||
) {
|
||||
if ( hasValidationError ) {
|
||||
dispatch( VALIDATION_STORE_KEY ).showValidationError(
|
||||
validationErrorId
|
||||
);
|
||||
} else {
|
||||
dispatch( VALIDATION_STORE_KEY ).setValidationErrors( {
|
||||
[ validationErrorId ]: {
|
||||
message: __(
|
||||
'Please select your country',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
hidden: false,
|
||||
},
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
if ( hasValidationError && values.country ) {
|
||||
dispatch( VALIDATION_STORE_KEY ).clearValidationError(
|
||||
validationErrorId
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default validateShippingCountry;
|
||||
@@ -1,100 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import Title from '@woocommerce/base-components/title';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
interface StepHeadingProps {
|
||||
title: string;
|
||||
stepHeadingContent?: JSX.Element;
|
||||
}
|
||||
|
||||
const StepHeading = ( { title, stepHeadingContent }: StepHeadingProps ) => (
|
||||
<div className="wc-block-components-checkout-step__heading">
|
||||
<Title
|
||||
aria-hidden="true"
|
||||
className="wc-block-components-checkout-step__title"
|
||||
headingLevel="2"
|
||||
>
|
||||
{ title }
|
||||
</Title>
|
||||
{ !! stepHeadingContent && (
|
||||
<span className="wc-block-components-checkout-step__heading-content">
|
||||
{ stepHeadingContent }
|
||||
</span>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
|
||||
interface FormStepProps {
|
||||
id?: string;
|
||||
className?: string;
|
||||
title?: string;
|
||||
legend?: string;
|
||||
description?: string;
|
||||
children?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
showStepNumber?: boolean;
|
||||
stepHeadingContent?: () => JSX.Element | undefined;
|
||||
}
|
||||
|
||||
const FormStep = ( {
|
||||
id,
|
||||
className,
|
||||
title,
|
||||
legend,
|
||||
description,
|
||||
children,
|
||||
disabled = false,
|
||||
showStepNumber = true,
|
||||
stepHeadingContent = () => undefined,
|
||||
}: FormStepProps ): JSX.Element => {
|
||||
// If the form step doesn't have a legend or title, render a <div> instead
|
||||
// of a <fieldset>.
|
||||
const Element = legend || title ? 'fieldset' : 'div';
|
||||
|
||||
return (
|
||||
<Element
|
||||
className={ classnames(
|
||||
className,
|
||||
'wc-block-components-checkout-step',
|
||||
{
|
||||
'wc-block-components-checkout-step--with-step-number':
|
||||
showStepNumber,
|
||||
'wc-block-components-checkout-step--disabled': disabled,
|
||||
}
|
||||
) }
|
||||
id={ id }
|
||||
disabled={ disabled }
|
||||
>
|
||||
{ !! ( legend || title ) && (
|
||||
<legend className="screen-reader-text">
|
||||
{ legend || title }
|
||||
</legend>
|
||||
) }
|
||||
{ !! title && (
|
||||
<StepHeading
|
||||
title={ title }
|
||||
stepHeadingContent={ stepHeadingContent() }
|
||||
/>
|
||||
) }
|
||||
<div className="wc-block-components-checkout-step__container">
|
||||
{ !! description && (
|
||||
<p className="wc-block-components-checkout-step__description">
|
||||
{ description }
|
||||
</p>
|
||||
) }
|
||||
<div className="wc-block-components-checkout-step__content">
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
</Element>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormStep;
|
||||
@@ -1,128 +0,0 @@
|
||||
.wc-block-components-form {
|
||||
counter-reset: checkout-step;
|
||||
}
|
||||
|
||||
.wc-block-components-form .wc-block-components-checkout-step {
|
||||
position: relative;
|
||||
border: none;
|
||||
padding: 0 0 0 $gap-larger;
|
||||
background: none;
|
||||
margin: 0;
|
||||
|
||||
.is-mobile &,
|
||||
.is-small & {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-checkout-step--disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.wc-block-components-checkout-step__container {
|
||||
position: relative;
|
||||
|
||||
textarea {
|
||||
font-style: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-checkout-step__content > * {
|
||||
margin-bottom: em($gap);
|
||||
}
|
||||
.wc-block-components-checkout-step--with-step-number .wc-block-components-checkout-step__content > :last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: em($gap-large);
|
||||
}
|
||||
|
||||
.wc-block-components-checkout-step__heading {
|
||||
margin: em($gap-small) 0 em($gap);
|
||||
position: relative;
|
||||
|
||||
.wc-block-components-express-payment-continue-rule + .wc-block-components-checkout-step & {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-checkout-step:first-child .wc-block-components-checkout-step__heading {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.wc-block-components-checkout-step__title {
|
||||
margin: 0 $gap-small 0 0;
|
||||
}
|
||||
|
||||
.wc-block-components-checkout-step__heading-content {
|
||||
@include font-size(smaller);
|
||||
|
||||
a {
|
||||
font-weight: bold;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-checkout-step__description {
|
||||
@include font-size(small);
|
||||
line-height: 1.25;
|
||||
margin-bottom: $gap;
|
||||
}
|
||||
|
||||
.wc-block-components-checkout-step--with-step-number {
|
||||
.wc-block-components-checkout-step__title::before {
|
||||
@include reset-box();
|
||||
background: transparent;
|
||||
counter-increment: checkout-step;
|
||||
content: "\00a0" counter(checkout-step) ".";
|
||||
content: "\00a0" counter(checkout-step) "." / "";
|
||||
position: absolute;
|
||||
left: -$gap-large;
|
||||
top: 0;
|
||||
text-align: center;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
|
||||
.is-mobile &,
|
||||
.is-small & {
|
||||
position: static;
|
||||
transform: none;
|
||||
left: auto;
|
||||
top: auto;
|
||||
content: counter(checkout-step) ".\00a0";
|
||||
content: counter(checkout-step) ".\00a0" / "";
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-checkout-step__container::after {
|
||||
content: "";
|
||||
height: 100%;
|
||||
border-left: 1px solid;
|
||||
opacity: 0.3;
|
||||
position: absolute;
|
||||
left: -$gap-large;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.is-mobile &,
|
||||
.is-small & {
|
||||
.wc-block-components-checkout-step__title::before {
|
||||
position: static;
|
||||
transform: none;
|
||||
left: auto;
|
||||
top: auto;
|
||||
content: counter(checkout-step) ".\00a0";
|
||||
content: counter(checkout-step) ".\00a0" / "";
|
||||
}
|
||||
.wc-block-components-checkout-step__container::after {
|
||||
content: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor-styles-wrapper {
|
||||
.wp-block h4.wc-block-components-checkout-step__title {
|
||||
@include font-size(regular);
|
||||
line-height: 24px;
|
||||
margin: 0 $gap-small 0 0;
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FormStep fieldset legend should default to legend prop when title and legend are defined 1`] = `
|
||||
<fieldset
|
||||
className="wc-block-components-checkout-step wc-block-components-checkout-step--with-step-number"
|
||||
disabled={false}
|
||||
>
|
||||
<legend
|
||||
className="screen-reader-text"
|
||||
>
|
||||
Lorem Ipsum 2
|
||||
</legend>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__heading"
|
||||
>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
className="wc-block-components-title wc-block-components-checkout-step__title"
|
||||
>
|
||||
Lorem Ipsum
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__container"
|
||||
>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__content"
|
||||
>
|
||||
Dolor sit amet
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
`;
|
||||
|
||||
exports[`FormStep should apply id and className props 1`] = `
|
||||
<div
|
||||
className="my-classname wc-block-components-checkout-step wc-block-components-checkout-step--with-step-number"
|
||||
disabled={false}
|
||||
id="my-id"
|
||||
>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__container"
|
||||
>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__content"
|
||||
>
|
||||
Dolor sit amet
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`FormStep should remove step number CSS class if prop is false 1`] = `
|
||||
<fieldset
|
||||
className="wc-block-components-checkout-step"
|
||||
disabled={false}
|
||||
>
|
||||
<legend
|
||||
className="screen-reader-text"
|
||||
>
|
||||
Lorem Ipsum
|
||||
</legend>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__heading"
|
||||
>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
className="wc-block-components-title wc-block-components-checkout-step__title"
|
||||
>
|
||||
Lorem Ipsum
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__container"
|
||||
>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__content"
|
||||
>
|
||||
Dolor sit amet
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
`;
|
||||
|
||||
exports[`FormStep should render a div if no title or legend is provided 1`] = `
|
||||
<div
|
||||
className="wc-block-components-checkout-step wc-block-components-checkout-step--with-step-number"
|
||||
disabled={false}
|
||||
>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__container"
|
||||
>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__content"
|
||||
>
|
||||
Dolor sit amet
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`FormStep should render a fieldset if a legend is provided 1`] = `
|
||||
<fieldset
|
||||
className="wc-block-components-checkout-step wc-block-components-checkout-step--with-step-number"
|
||||
disabled={false}
|
||||
>
|
||||
<legend
|
||||
className="screen-reader-text"
|
||||
>
|
||||
Lorem Ipsum 2
|
||||
</legend>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__container"
|
||||
>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__content"
|
||||
>
|
||||
Dolor sit amet
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
`;
|
||||
|
||||
exports[`FormStep should render a fieldset with heading if a title is provided 1`] = `
|
||||
<fieldset
|
||||
className="wc-block-components-checkout-step wc-block-components-checkout-step--with-step-number"
|
||||
disabled={false}
|
||||
>
|
||||
<legend
|
||||
className="screen-reader-text"
|
||||
>
|
||||
Lorem Ipsum
|
||||
</legend>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__heading"
|
||||
>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
className="wc-block-components-title wc-block-components-checkout-step__title"
|
||||
>
|
||||
Lorem Ipsum
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__container"
|
||||
>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__content"
|
||||
>
|
||||
Dolor sit amet
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
`;
|
||||
|
||||
exports[`FormStep should render step description 1`] = `
|
||||
<fieldset
|
||||
className="wc-block-components-checkout-step wc-block-components-checkout-step--with-step-number"
|
||||
disabled={false}
|
||||
>
|
||||
<legend
|
||||
className="screen-reader-text"
|
||||
>
|
||||
Lorem Ipsum
|
||||
</legend>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__heading"
|
||||
>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
className="wc-block-components-title wc-block-components-checkout-step__title"
|
||||
>
|
||||
Lorem Ipsum
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__container"
|
||||
>
|
||||
<p
|
||||
className="wc-block-components-checkout-step__description"
|
||||
>
|
||||
This is the description
|
||||
</p>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__content"
|
||||
>
|
||||
Dolor sit amet
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
`;
|
||||
|
||||
exports[`FormStep should render step heading content 1`] = `
|
||||
<fieldset
|
||||
className="wc-block-components-checkout-step wc-block-components-checkout-step--with-step-number"
|
||||
disabled={false}
|
||||
>
|
||||
<legend
|
||||
className="screen-reader-text"
|
||||
>
|
||||
Lorem Ipsum
|
||||
</legend>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__heading"
|
||||
>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
className="wc-block-components-title wc-block-components-checkout-step__title"
|
||||
>
|
||||
Lorem Ipsum
|
||||
</h2>
|
||||
<span
|
||||
className="wc-block-components-checkout-step__heading-content"
|
||||
>
|
||||
<span>
|
||||
Some context to render next to the heading
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__container"
|
||||
>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__content"
|
||||
>
|
||||
Dolor sit amet
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
`;
|
||||
|
||||
exports[`FormStep should set disabled prop to the fieldset element when disabled is true 1`] = `
|
||||
<fieldset
|
||||
className="wc-block-components-checkout-step wc-block-components-checkout-step--with-step-number wc-block-components-checkout-step--disabled"
|
||||
disabled={true}
|
||||
>
|
||||
<legend
|
||||
className="screen-reader-text"
|
||||
>
|
||||
Lorem Ipsum
|
||||
</legend>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__heading"
|
||||
>
|
||||
<h2
|
||||
aria-hidden="true"
|
||||
className="wc-block-components-title wc-block-components-checkout-step__title"
|
||||
>
|
||||
Lorem Ipsum
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__container"
|
||||
>
|
||||
<div
|
||||
className="wc-block-components-checkout-step__content"
|
||||
>
|
||||
Dolor sit amet
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
`;
|
||||
@@ -1,100 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import TestRenderer from 'react-test-renderer';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import FormStep from '..';
|
||||
|
||||
describe( 'FormStep', () => {
|
||||
test( 'should render a div if no title or legend is provided', () => {
|
||||
const component = TestRenderer.create(
|
||||
<FormStep>Dolor sit amet</FormStep>
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should apply id and className props', () => {
|
||||
const component = TestRenderer.create(
|
||||
<FormStep id="my-id" className="my-classname">
|
||||
Dolor sit amet
|
||||
</FormStep>
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render a fieldset if a legend is provided', () => {
|
||||
const component = TestRenderer.create(
|
||||
<FormStep legend="Lorem Ipsum 2">Dolor sit amet</FormStep>
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render a fieldset with heading if a title is provided', () => {
|
||||
const component = TestRenderer.create(
|
||||
<FormStep title="Lorem Ipsum">Dolor sit amet</FormStep>
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'fieldset legend should default to legend prop when title and legend are defined', () => {
|
||||
const component = TestRenderer.create(
|
||||
<FormStep title="Lorem Ipsum" legend="Lorem Ipsum 2">
|
||||
Dolor sit amet
|
||||
</FormStep>
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should remove step number CSS class if prop is false', () => {
|
||||
const component = TestRenderer.create(
|
||||
<FormStep title="Lorem Ipsum" showStepNumber={ false }>
|
||||
Dolor sit amet
|
||||
</FormStep>
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render step heading content', () => {
|
||||
const component = TestRenderer.create(
|
||||
<FormStep
|
||||
title="Lorem Ipsum"
|
||||
stepHeadingContent={ () => (
|
||||
<span>Some context to render next to the heading</span>
|
||||
) }
|
||||
>
|
||||
Dolor sit amet
|
||||
</FormStep>
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render step description', () => {
|
||||
const component = TestRenderer.create(
|
||||
<FormStep title="Lorem Ipsum" description="This is the description">
|
||||
Dolor sit amet
|
||||
</FormStep>
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should set disabled prop to the fieldset element when disabled is true', () => {
|
||||
const component = TestRenderer.create(
|
||||
<FormStep title="Lorem Ipsum" disabled={ true }>
|
||||
Dolor sit amet
|
||||
</FormStep>
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
} );
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from './address-form';
|
||||
export { default as CartLineItemsTable } from './cart-line-items-table';
|
||||
export { default as FormStep } from './form-step';
|
||||
export { default as OrderSummary } from './order-summary';
|
||||
export { default as PlaceOrderButton } from './place-order-button';
|
||||
export { default as Policies } from './policies';
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { RadioControlOption } from '@woocommerce/base-components/radio-control/types';
|
||||
import {
|
||||
RadioControl,
|
||||
RadioControlOptionType,
|
||||
} from '@woocommerce/blocks-components';
|
||||
import { CartShippingPackageShippingRate } from '@woocommerce/types';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import RadioControl from '../../radio-control';
|
||||
|
||||
interface LocalPickupSelectProps {
|
||||
title?: string | undefined;
|
||||
@@ -17,7 +16,7 @@ interface LocalPickupSelectProps {
|
||||
renderPickupLocation: (
|
||||
location: CartShippingPackageShippingRate,
|
||||
pickupLocationsCount: number
|
||||
) => RadioControlOption;
|
||||
) => RadioControlOptionType;
|
||||
packageCount: number;
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { sprintf, _n } from '@wordpress/i18n';
|
||||
import Label from '@woocommerce/base-components/label';
|
||||
import { Label } from '@woocommerce/blocks-components';
|
||||
import ProductPrice from '@woocommerce/base-components/product-price';
|
||||
import ProductName from '@woocommerce/base-components/product-name';
|
||||
import {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.wc-block-components-product-badge {
|
||||
@include font-size(smaller);
|
||||
border-radius: 2px;
|
||||
border-radius: $universal-border-radius;
|
||||
border: 1px solid;
|
||||
display: inline-block;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { createInterpolateElement } from '@wordpress/element';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
|
||||
import { FormattedMonetaryAmount } from '@woocommerce/blocks-components';
|
||||
import type { Currency } from '@woocommerce/price-format';
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,14 +5,10 @@ import classNames from 'classnames';
|
||||
import { _n, sprintf } from '@wordpress/i18n';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import { Panel } from '@woocommerce/blocks-checkout';
|
||||
import Label from '@woocommerce/base-components/label';
|
||||
import { Label } from '@woocommerce/blocks-components';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
import {
|
||||
useShippingData,
|
||||
useStoreEvents,
|
||||
} from '@woocommerce/base-context/hooks';
|
||||
import { useShippingData } from '@woocommerce/base-context/hooks';
|
||||
import { sanitizeHTML } from '@woocommerce/utils';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
/**
|
||||
@@ -31,8 +27,7 @@ export const ShippingRatesControlPackage = ( {
|
||||
collapsible,
|
||||
showItems,
|
||||
}: PackageProps ): ReactElement => {
|
||||
const { selectShippingRate } = useShippingData();
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
const { selectShippingRate, isSelectingRate } = useShippingData();
|
||||
const multiplePackages =
|
||||
document.querySelectorAll(
|
||||
'.wc-block-components-shipping-rates-control__package'
|
||||
@@ -95,28 +90,32 @@ export const ShippingRatesControlPackage = ( {
|
||||
const onSelectRate = useCallback(
|
||||
( newShippingRateId: string ) => {
|
||||
selectShippingRate( newShippingRateId, packageId );
|
||||
dispatchCheckoutEvent( 'set-selected-shipping-rate', {
|
||||
shippingRateId: newShippingRateId,
|
||||
} );
|
||||
},
|
||||
[ dispatchCheckoutEvent, packageId, selectShippingRate ]
|
||||
[ packageId, selectShippingRate ]
|
||||
);
|
||||
const debouncedOnSelectRate = useDebouncedCallback( onSelectRate, 1000 );
|
||||
const packageRatesProps = {
|
||||
className,
|
||||
noResultsMessage,
|
||||
rates: packageData.shipping_rates,
|
||||
onSelectRate: debouncedOnSelectRate,
|
||||
onSelectRate,
|
||||
selectedRate: packageData.shipping_rates.find(
|
||||
( rate ) => rate.selected
|
||||
),
|
||||
renderOption,
|
||||
disabled: isSelectingRate,
|
||||
};
|
||||
|
||||
if ( shouldBeCollapsible ) {
|
||||
return (
|
||||
<Panel
|
||||
className="wc-block-components-shipping-rates-control__package"
|
||||
className={ classNames(
|
||||
'wc-block-components-shipping-rates-control__package',
|
||||
className,
|
||||
{
|
||||
'wc-block-components-shipping-rates-control__package--disabled':
|
||||
isSelectingRate,
|
||||
}
|
||||
) }
|
||||
// initialOpen remembers only the first value provided to it, so by the
|
||||
// time we know we have several packages, initialOpen would be hardcoded to true.
|
||||
// If we're rendering a panel, we're more likely rendering several
|
||||
@@ -133,7 +132,11 @@ export const ShippingRatesControlPackage = ( {
|
||||
<div
|
||||
className={ classNames(
|
||||
'wc-block-components-shipping-rates-control__package',
|
||||
className
|
||||
className,
|
||||
{
|
||||
'wc-block-components-shipping-rates-control__package--disabled':
|
||||
isSelectingRate,
|
||||
}
|
||||
) }
|
||||
>
|
||||
{ header }
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
import RadioControl, {
|
||||
import {
|
||||
RadioControl,
|
||||
RadioControlOptionLayout,
|
||||
} from '@woocommerce/base-components/radio-control';
|
||||
} from '@woocommerce/blocks-components';
|
||||
import type { CartShippingPackageShippingRate } from '@woocommerce/types';
|
||||
import { usePrevious } from '@woocommerce/base-hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
@@ -20,6 +22,7 @@ interface PackageRates {
|
||||
className?: string;
|
||||
noResultsMessage: JSX.Element;
|
||||
selectedRate: CartShippingPackageShippingRate | undefined;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const PackageRates = ( {
|
||||
@@ -29,34 +32,37 @@ const PackageRates = ( {
|
||||
rates,
|
||||
renderOption = renderPackageRateOption,
|
||||
selectedRate,
|
||||
disabled = false,
|
||||
}: PackageRates ): JSX.Element => {
|
||||
const selectedRateId = selectedRate?.rate_id || '';
|
||||
const previousSelectedRateId = usePrevious( selectedRateId );
|
||||
|
||||
// Store selected rate ID in local state so shipping rates changes are shown in the UI instantly.
|
||||
const [ selectedOption, setSelectedOption ] = useState( selectedRateId );
|
||||
|
||||
// Update the selected option if cart state changes in the data stores.
|
||||
useEffect( () => {
|
||||
const [ selectedOption, setSelectedOption ] = useState( () => {
|
||||
if ( selectedRateId ) {
|
||||
return selectedRateId;
|
||||
}
|
||||
// Default to first rate if no rate is selected.
|
||||
return rates[ 0 ]?.rate_id;
|
||||
} );
|
||||
|
||||
// Update the selected option if cart state changes in the data store.
|
||||
useEffect( () => {
|
||||
if (
|
||||
selectedRateId &&
|
||||
selectedRateId !== previousSelectedRateId &&
|
||||
selectedRateId !== selectedOption
|
||||
) {
|
||||
setSelectedOption( selectedRateId );
|
||||
}
|
||||
}, [ selectedRateId ] );
|
||||
}, [ selectedRateId, selectedOption, previousSelectedRateId ] );
|
||||
|
||||
// Update the selected option if there is no rate selected on mount.
|
||||
// Update the data store when the local selected rate changes.
|
||||
useEffect( () => {
|
||||
// Check the rates to see if any are marked as selected. At least one should be. If no rate is selected, it could be
|
||||
// that the user toggled quickly from local pickup back to shipping.
|
||||
const isRateSelectedInDataStore = rates.some(
|
||||
( { selected } ) => selected
|
||||
);
|
||||
if (
|
||||
( ! selectedOption && rates[ 0 ] ) ||
|
||||
! isRateSelectedInDataStore
|
||||
) {
|
||||
setSelectedOption( rates[ 0 ]?.rate_id );
|
||||
onSelectRate( rates[ 0 ]?.rate_id );
|
||||
if ( selectedOption ) {
|
||||
onSelectRate( selectedOption );
|
||||
}
|
||||
}, [ onSelectRate, rates, selectedOption ] );
|
||||
}, [ onSelectRate, selectedOption ] );
|
||||
|
||||
if ( rates.length === 0 ) {
|
||||
return noResultsMessage;
|
||||
@@ -70,6 +76,7 @@ const PackageRates = ( {
|
||||
setSelectedOption( value );
|
||||
onSelectRate( value );
|
||||
} }
|
||||
disabled={ disabled }
|
||||
selected={ selectedOption }
|
||||
options={ rates.map( renderOption ) }
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
|
||||
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
|
||||
import { FormattedMonetaryAmount } from '@woocommerce/blocks-components';
|
||||
import type { PackageRateOption } from '@woocommerce/types';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { CartShippingPackageShippingRate } from '@woocommerce/types';
|
||||
|
||||
@@ -25,10 +25,6 @@
|
||||
padding-bottom: em($gap-small);
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control {
|
||||
margin-bottom: em($gap-small);
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control,
|
||||
.wc-block-components-radio-control__option-layout {
|
||||
padding-bottom: 0;
|
||||
@@ -44,6 +40,11 @@
|
||||
.wc-block-components-radio-control__description-group {
|
||||
@include font-size(smaller);
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.5;
|
||||
transition: opacity 200ms ease;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-shipping-rates-control__package-items {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import LoadingMask from '@woocommerce/base-components/loading-mask';
|
||||
import { RemovableChip } from '@woocommerce/base-components/chip';
|
||||
import { RemovableChip } from '@woocommerce/blocks-components';
|
||||
import { applyCheckoutFilter, TotalsItem } from '@woocommerce/blocks-checkout';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { createInterpolateElement } from '@wordpress/element';
|
||||
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
|
||||
import { FormattedMonetaryAmount } from '@woocommerce/blocks-components';
|
||||
import { applyCheckoutFilter, TotalsItem } from '@woocommerce/blocks-checkout';
|
||||
import { useStoreCart } from '@woocommerce/base-context/hooks';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
@@ -61,6 +61,7 @@ jest.mock( '@woocommerce/base-context/hooks', () => {
|
||||
} );
|
||||
baseContextHooks.useShippingData.mockReturnValue( {
|
||||
needsShipping: true,
|
||||
selectShippingRate: jest.fn(),
|
||||
shippingRates: [
|
||||
{
|
||||
package_id: 0,
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import { Fragment, useMemo, useState } from '@wordpress/element';
|
||||
import classNames from 'classnames';
|
||||
import { CheckboxControl } from '@woocommerce/blocks-checkout';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
interface CheckboxListOptions {
|
||||
label: React.ReactNode;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface CheckboxListProps {
|
||||
className?: string;
|
||||
isLoading?: boolean;
|
||||
isDisabled?: boolean;
|
||||
limit?: number;
|
||||
checked?: string[];
|
||||
onChange: ( value: string ) => void;
|
||||
options?: CheckboxListOptions[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Component used to show a list of checkboxes in a group.
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {string} props.className CSS class used.
|
||||
* @param {function(string):any} props.onChange Function called when inputs change.
|
||||
* @param {Array} props.options Options for list.
|
||||
* @param {Array} props.checked Which items are checked.
|
||||
* @param {boolean} props.isLoading If loading or not.
|
||||
* @param {boolean} props.isDisabled If inputs are disabled or not.
|
||||
* @param {number} props.limit Whether to limit the number of inputs showing.
|
||||
*/
|
||||
const CheckboxList = ( {
|
||||
className,
|
||||
onChange,
|
||||
options = [],
|
||||
checked = [],
|
||||
isLoading = false,
|
||||
isDisabled = false,
|
||||
limit = 10,
|
||||
}: CheckboxListProps ): JSX.Element => {
|
||||
const [ showExpanded, setShowExpanded ] = useState( false );
|
||||
|
||||
const placeholder = useMemo( () => {
|
||||
return [ ...Array( 5 ) ].map( ( x, i ) => (
|
||||
<li
|
||||
key={ i }
|
||||
style={ {
|
||||
/* stylelint-disable */
|
||||
width: Math.floor( Math.random() * 75 ) + 25 + '%',
|
||||
} }
|
||||
/>
|
||||
) );
|
||||
}, [] );
|
||||
|
||||
const renderedShowMore = useMemo( () => {
|
||||
const optionCount = options.length;
|
||||
const remainingOptionsCount = optionCount - limit;
|
||||
return (
|
||||
! showExpanded && (
|
||||
<li key="show-more" className="show-more">
|
||||
<button
|
||||
onClick={ () => {
|
||||
setShowExpanded( true );
|
||||
} }
|
||||
aria-expanded={ false }
|
||||
aria-label={ sprintf(
|
||||
/* translators: %s is referring the remaining count of options */
|
||||
_n(
|
||||
'Show %s more option',
|
||||
'Show %s more options',
|
||||
remainingOptionsCount,
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
remainingOptionsCount
|
||||
) }
|
||||
>
|
||||
{ sprintf(
|
||||
/* translators: %s number of options to reveal. */
|
||||
_n(
|
||||
'Show %s more',
|
||||
'Show %s more',
|
||||
remainingOptionsCount,
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
remainingOptionsCount
|
||||
) }
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
}, [ options, limit, showExpanded ] );
|
||||
|
||||
const renderedShowLess = useMemo( () => {
|
||||
return (
|
||||
showExpanded && (
|
||||
<li key="show-less" className="show-less">
|
||||
<button
|
||||
onClick={ () => {
|
||||
setShowExpanded( false );
|
||||
} }
|
||||
aria-expanded={ true }
|
||||
aria-label={ __(
|
||||
'Show less options',
|
||||
'woo-gutenberg-products-block'
|
||||
) }
|
||||
>
|
||||
{ __( 'Show less', 'woo-gutenberg-products-block' ) }
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
}, [ showExpanded ] );
|
||||
|
||||
const renderedOptions = useMemo( () => {
|
||||
// Truncate options if > the limit + 5.
|
||||
const optionCount = options.length;
|
||||
const shouldTruncateOptions = optionCount > limit + 5;
|
||||
return (
|
||||
<>
|
||||
{ options.map( ( option, index ) => (
|
||||
<Fragment key={ option.value }>
|
||||
<li
|
||||
{ ...( shouldTruncateOptions &&
|
||||
! showExpanded &&
|
||||
index >= limit && { hidden: true } ) }
|
||||
>
|
||||
<CheckboxControl
|
||||
id={ option.value }
|
||||
className="wc-block-checkbox-list__checkbox"
|
||||
label={ option.label }
|
||||
checked={ checked.includes( option.value ) }
|
||||
onChange={ () => {
|
||||
onChange( option.value );
|
||||
} }
|
||||
disabled={ isDisabled }
|
||||
/>
|
||||
</li>
|
||||
{ shouldTruncateOptions &&
|
||||
index === limit - 1 &&
|
||||
renderedShowMore }
|
||||
</Fragment>
|
||||
) ) }
|
||||
{ shouldTruncateOptions && renderedShowLess }
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
options,
|
||||
onChange,
|
||||
checked,
|
||||
showExpanded,
|
||||
limit,
|
||||
renderedShowLess,
|
||||
renderedShowMore,
|
||||
isDisabled,
|
||||
] );
|
||||
|
||||
const classes = classNames(
|
||||
'wc-block-checkbox-list',
|
||||
'wc-block-components-checkbox-list',
|
||||
{
|
||||
'is-loading': isLoading,
|
||||
},
|
||||
className
|
||||
);
|
||||
|
||||
return (
|
||||
<ul className={ classes }>
|
||||
{ isLoading ? placeholder : renderedOptions }
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckboxList;
|
||||
@@ -1,29 +0,0 @@
|
||||
.editor-styles-wrapper .wc-block-components-checkbox-list,
|
||||
.wc-block-components-checkbox-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none outside;
|
||||
|
||||
li {
|
||||
margin: 0 0 $gap-smallest;
|
||||
padding: 0;
|
||||
list-style: none outside;
|
||||
}
|
||||
|
||||
li.show-more,
|
||||
li.show-less {
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
li {
|
||||
@include placeholder();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classNames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
export interface ChipProps {
|
||||
/**
|
||||
* Text for chip content.
|
||||
*/
|
||||
text: string | JSX.Element;
|
||||
/**
|
||||
* Screenreader text for the content.
|
||||
*/
|
||||
screenReaderText?: string;
|
||||
/**
|
||||
* The element type for the chip. Default 'li'.
|
||||
*/
|
||||
element?: string;
|
||||
/**
|
||||
* CSS class used.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* React children.
|
||||
*/
|
||||
children?: React.ReactNode | React.ReactNode[];
|
||||
/**
|
||||
* Radius size.
|
||||
*/
|
||||
radius?: 'none' | 'small' | 'medium' | 'large';
|
||||
}
|
||||
|
||||
/**
|
||||
* Component used to render a "chip" -- a list item containing some text.
|
||||
*
|
||||
* Each chip defaults to a list element but this can be customized by providing
|
||||
* a wrapperElement.
|
||||
*
|
||||
*/
|
||||
const Chip: React.FC< ChipProps > = ( {
|
||||
text,
|
||||
screenReaderText = '',
|
||||
element = 'li',
|
||||
className = '',
|
||||
radius = 'small',
|
||||
children = null,
|
||||
...props
|
||||
} ) => {
|
||||
const Wrapper = element;
|
||||
const wrapperClassName = classNames(
|
||||
className,
|
||||
'wc-block-components-chip',
|
||||
'wc-block-components-chip--radius-' + radius
|
||||
);
|
||||
|
||||
const showScreenReaderText = Boolean(
|
||||
screenReaderText && screenReaderText !== text
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper className={ wrapperClassName } { ...props }>
|
||||
<span
|
||||
aria-hidden={ showScreenReaderText }
|
||||
className="wc-block-components-chip__text"
|
||||
>
|
||||
{ text }
|
||||
</span>
|
||||
{ showScreenReaderText && (
|
||||
<span className="screen-reader-text">{ screenReaderText }</span>
|
||||
) }
|
||||
{ children }
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
export default Chip;
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as Chip } from './chip';
|
||||
export { default as RemovableChip } from './removable-chip';
|
||||
@@ -1,117 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classNames from 'classnames';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { Icon, closeSmall } from '@wordpress/icons';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Chip, { ChipProps } from './chip';
|
||||
|
||||
export interface RemovableChipProps extends ChipProps {
|
||||
/**
|
||||
* Aria label content.
|
||||
*/
|
||||
ariaLabel?: string;
|
||||
/**
|
||||
* CSS class used.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Whether action is disabled or not.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Function to call when remove event is fired.
|
||||
*/
|
||||
onRemove?: () => void;
|
||||
/**
|
||||
* Whether to expand click area for remove event.
|
||||
*/
|
||||
removeOnAnyClick?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component used to render a "chip" -- an item containing some text with
|
||||
* an X button to remove/dismiss each chip.
|
||||
*
|
||||
* @param {Object} props Incoming props for the component.
|
||||
* @param {string} props.ariaLabel Aria label content.
|
||||
* @param {string} props.className CSS class used.
|
||||
* @param {boolean} props.disabled Whether action is disabled or not.
|
||||
* @param {function():any} props.onRemove Function to call when remove event is fired.
|
||||
* @param {boolean} props.removeOnAnyClick Whether to expand click area for remove event.
|
||||
* @param {string} props.text The text for the chip.
|
||||
* @param {string} props.screenReaderText The screen reader text for the chip.
|
||||
* @param {Object} props.props Rest of props passed into component.
|
||||
*/
|
||||
export const RemovableChip = ( {
|
||||
ariaLabel = '',
|
||||
className = '',
|
||||
disabled = false,
|
||||
onRemove = () => void 0,
|
||||
removeOnAnyClick = false,
|
||||
text,
|
||||
screenReaderText = '',
|
||||
...props
|
||||
}: RemovableChipProps ): JSX.Element => {
|
||||
const RemoveElement = removeOnAnyClick ? 'span' : 'button';
|
||||
|
||||
if ( ! ariaLabel ) {
|
||||
const ariaLabelText =
|
||||
screenReaderText && typeof screenReaderText === 'string'
|
||||
? screenReaderText
|
||||
: text;
|
||||
ariaLabel =
|
||||
typeof ariaLabelText !== 'string'
|
||||
? /* translators: Remove chip. */
|
||||
__( 'Remove', 'woo-gutenberg-products-block' )
|
||||
: sprintf(
|
||||
/* translators: %s text of the chip to remove. */
|
||||
__( 'Remove "%s"', 'woo-gutenberg-products-block' ),
|
||||
ariaLabelText
|
||||
);
|
||||
}
|
||||
|
||||
const clickableElementProps = {
|
||||
'aria-label': ariaLabel,
|
||||
disabled,
|
||||
onClick: onRemove,
|
||||
onKeyDown: ( e: React.KeyboardEvent ) => {
|
||||
if ( e.key === 'Backspace' || e.key === 'Delete' ) {
|
||||
onRemove();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const chipProps = removeOnAnyClick ? clickableElementProps : {};
|
||||
const removeProps = removeOnAnyClick
|
||||
? { 'aria-hidden': true }
|
||||
: clickableElementProps;
|
||||
|
||||
return (
|
||||
<Chip
|
||||
{ ...props }
|
||||
{ ...chipProps }
|
||||
className={ classNames( className, 'is-removable' ) }
|
||||
element={ removeOnAnyClick ? 'button' : props.element }
|
||||
screenReaderText={ screenReaderText }
|
||||
text={ text }
|
||||
>
|
||||
<RemoveElement
|
||||
className="wc-block-components-chip__remove"
|
||||
{ ...removeProps }
|
||||
>
|
||||
<Icon
|
||||
className="wc-block-components-chip__remove-icon"
|
||||
icon={ closeSmall }
|
||||
size={ 16 }
|
||||
/>
|
||||
</RemoveElement>
|
||||
</Chip>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemovableChip;
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Chip, { ChipProps } from '../chip';
|
||||
const availableElements = [ 'li', 'div', 'span' ];
|
||||
const availableRadii = [ 'none', 'small', 'medium', 'large' ];
|
||||
|
||||
export default {
|
||||
title: 'WooCommerce Blocks/@base-components/Chip',
|
||||
component: Chip,
|
||||
argTypes: {
|
||||
element: {
|
||||
control: 'radio',
|
||||
options: availableElements,
|
||||
},
|
||||
className: {
|
||||
control: 'text',
|
||||
},
|
||||
radius: {
|
||||
control: 'radio',
|
||||
options: availableRadii,
|
||||
},
|
||||
},
|
||||
} as Meta< ChipProps >;
|
||||
|
||||
const Template: Story< ChipProps > = ( args ) => <Chip { ...args } />;
|
||||
|
||||
export const Default = Template.bind( {} );
|
||||
Default.args = {
|
||||
element: 'li',
|
||||
text: 'Take me to the casino!',
|
||||
screenReaderText: "I'm a chip, me",
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { RemovableChip, RemovableChipProps } from '../removable-chip';
|
||||
|
||||
const availableElements = [ 'li', 'div', 'span' ];
|
||||
|
||||
export default {
|
||||
title: 'WooCommerce Blocks/@base-components/Chip/RemovableChip',
|
||||
component: RemovableChip,
|
||||
argTypes: {
|
||||
element: {
|
||||
control: 'radio',
|
||||
options: availableElements,
|
||||
},
|
||||
},
|
||||
} as Meta< RemovableChipProps >;
|
||||
|
||||
const Template: Story< RemovableChipProps > = ( args ) => (
|
||||
<RemovableChip { ...args } />
|
||||
);
|
||||
|
||||
export const Default = Template.bind( {} );
|
||||
Default.args = {
|
||||
element: 'li',
|
||||
text: 'Take me to the casino',
|
||||
screenReaderText: "I'm a removable chip, me",
|
||||
};
|
||||
@@ -1,84 +0,0 @@
|
||||
.wc-block-components-chip {
|
||||
@include reset-typography();
|
||||
align-items: center;
|
||||
border: 0;
|
||||
display: inline-flex;
|
||||
padding: em($gap-smallest) 0.5em;
|
||||
margin: 0 0.365em 0.365em 0;
|
||||
border-radius: 0;
|
||||
line-height: 1;
|
||||
max-width: 100%;
|
||||
|
||||
// Chip might be a button, so we need to override theme styles.
|
||||
&,
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
&.wc-block-components-chip--radius-small {
|
||||
border-radius: 3px;
|
||||
}
|
||||
&.wc-block-components-chip--radius-medium {
|
||||
border-radius: 0.433em;
|
||||
}
|
||||
&.wc-block-components-chip--radius-large {
|
||||
border-radius: 2em;
|
||||
padding-left: 0.75em;
|
||||
padding-right: 0.25em;
|
||||
}
|
||||
.wc-block-components-chip__text {
|
||||
flex-grow: 1;
|
||||
@include font-size(small);
|
||||
}
|
||||
&.is-removable .wc-block-components-chip__text {
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
.wc-block-components-chip__remove {
|
||||
background: $gray-200;
|
||||
border: 0;
|
||||
border-radius: 25px;
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
line-height: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wc-block-components-chip__remove-icon {
|
||||
fill: $gray-900;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-twentytwentyone {
|
||||
.wc-block-components-chip,
|
||||
.wc-block-components-chip:active,
|
||||
.wc-block-components-chip:focus,
|
||||
.wc-block-components-chip:hover {
|
||||
background: #fff;
|
||||
button.wc-block-components-chip__remove:not(:hover):not(:active):not(.has-background) {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.wc-block-components-chip:hover > .wc-block-components-chip__remove,
|
||||
button.wc-block-components-chip:focus > .wc-block-components-chip__remove,
|
||||
.wc-block-components-chip__remove:hover,
|
||||
.wc-block-components-chip__remove:focus {
|
||||
background: $gray-600;
|
||||
|
||||
.wc-block-components-chip__remove-icon {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
button.wc-block-components-chip:disabled > .wc-block-components-chip__remove,
|
||||
.wc-block-components-chip__remove:disabled {
|
||||
fill: #fff;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Chip should render children nodes 1`] = `
|
||||
<li
|
||||
className="wc-block-components-chip wc-block-components-chip--radius-small"
|
||||
>
|
||||
<span
|
||||
aria-hidden={false}
|
||||
className="wc-block-components-chip__text"
|
||||
>
|
||||
Test
|
||||
</span>
|
||||
Lorem Ipsum
|
||||
</li>
|
||||
`;
|
||||
|
||||
exports[`Chip should render defined radius 1`] = `
|
||||
<li
|
||||
className="wc-block-components-chip wc-block-components-chip--radius-large"
|
||||
>
|
||||
<span
|
||||
aria-hidden={false}
|
||||
className="wc-block-components-chip__text"
|
||||
>
|
||||
Test
|
||||
</span>
|
||||
</li>
|
||||
`;
|
||||
|
||||
exports[`Chip should render nodes as the text 1`] = `
|
||||
<li
|
||||
className="wc-block-components-chip wc-block-components-chip--radius-small"
|
||||
>
|
||||
<span
|
||||
aria-hidden={false}
|
||||
className="wc-block-components-chip__text"
|
||||
>
|
||||
<h1>
|
||||
Test
|
||||
</h1>
|
||||
</span>
|
||||
</li>
|
||||
`;
|
||||
|
||||
exports[`Chip should render screen reader text 1`] = `
|
||||
<li
|
||||
className="wc-block-components-chip wc-block-components-chip--radius-small"
|
||||
>
|
||||
<span
|
||||
aria-hidden={true}
|
||||
className="wc-block-components-chip__text"
|
||||
>
|
||||
Test
|
||||
</span>
|
||||
<span
|
||||
className="screen-reader-text"
|
||||
>
|
||||
Test 2
|
||||
</span>
|
||||
</li>
|
||||
`;
|
||||
|
||||
exports[`Chip should render text 1`] = `
|
||||
<li
|
||||
className="wc-block-components-chip wc-block-components-chip--radius-small"
|
||||
>
|
||||
<span
|
||||
aria-hidden={false}
|
||||
className="wc-block-components-chip__text"
|
||||
>
|
||||
Test
|
||||
</span>
|
||||
</li>
|
||||
`;
|
||||
|
||||
exports[`Chip with custom wrapper should render a chip made up of a div instead of a li 1`] = `
|
||||
<div
|
||||
className="wc-block-components-chip wc-block-components-chip--radius-small"
|
||||
>
|
||||
<span
|
||||
aria-hidden={false}
|
||||
className="wc-block-components-chip__text"
|
||||
>
|
||||
Test
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RemovableChip should render custom aria label 1`] = `
|
||||
<li
|
||||
className="is-removable wc-block-components-chip wc-block-components-chip--radius-small"
|
||||
>
|
||||
<span
|
||||
aria-hidden={false}
|
||||
className="wc-block-components-chip__text"
|
||||
>
|
||||
<h1>
|
||||
Test
|
||||
</h1>
|
||||
</span>
|
||||
<button
|
||||
aria-label="Aria test"
|
||||
className="wc-block-components-chip__remove"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="wc-block-components-chip__remove-icon"
|
||||
focusable={false}
|
||||
height={16}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
`;
|
||||
|
||||
exports[`RemovableChip should render default aria label if text is a node 1`] = `
|
||||
<li
|
||||
className="is-removable wc-block-components-chip wc-block-components-chip--radius-small"
|
||||
>
|
||||
<span
|
||||
aria-hidden={true}
|
||||
className="wc-block-components-chip__text"
|
||||
>
|
||||
<h1>
|
||||
Test
|
||||
</h1>
|
||||
</span>
|
||||
<span
|
||||
className="screen-reader-text"
|
||||
>
|
||||
Test 2
|
||||
</span>
|
||||
<button
|
||||
aria-label="Remove \\"Test 2\\""
|
||||
className="wc-block-components-chip__remove"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="wc-block-components-chip__remove-icon"
|
||||
focusable={false}
|
||||
height={16}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
`;
|
||||
|
||||
exports[`RemovableChip should render screen reader text aria label 1`] = `
|
||||
<li
|
||||
className="is-removable wc-block-components-chip wc-block-components-chip--radius-small"
|
||||
>
|
||||
<span
|
||||
aria-hidden={true}
|
||||
className="wc-block-components-chip__text"
|
||||
>
|
||||
Test
|
||||
</span>
|
||||
<span
|
||||
className="screen-reader-text"
|
||||
>
|
||||
Test 2
|
||||
</span>
|
||||
<button
|
||||
aria-label="Remove \\"Test 2\\""
|
||||
className="wc-block-components-chip__remove"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="wc-block-components-chip__remove-icon"
|
||||
focusable={false}
|
||||
height={16}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
`;
|
||||
|
||||
exports[`RemovableChip should render text and the remove button 1`] = `
|
||||
<li
|
||||
className="is-removable wc-block-components-chip wc-block-components-chip--radius-small"
|
||||
>
|
||||
<span
|
||||
aria-hidden={false}
|
||||
className="wc-block-components-chip__text"
|
||||
>
|
||||
Test
|
||||
</span>
|
||||
<button
|
||||
aria-label="Remove \\"Test\\""
|
||||
className="wc-block-components-chip__remove"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="wc-block-components-chip__remove-icon"
|
||||
focusable={false}
|
||||
height={16}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
`;
|
||||
|
||||
exports[`RemovableChip should render with disabled remove button 1`] = `
|
||||
<li
|
||||
className="is-removable wc-block-components-chip wc-block-components-chip--radius-small"
|
||||
>
|
||||
<span
|
||||
aria-hidden={false}
|
||||
className="wc-block-components-chip__text"
|
||||
>
|
||||
Test
|
||||
</span>
|
||||
<button
|
||||
aria-label="Remove \\"Test\\""
|
||||
className="wc-block-components-chip__remove"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="wc-block-components-chip__remove-icon"
|
||||
focusable={false}
|
||||
height={16}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
`;
|
||||
|
||||
exports[`RemovableChip with removeOnAnyClick should be a button when removeOnAnyClick is set to true 1`] = `
|
||||
<button
|
||||
aria-label="Remove \\"Test\\""
|
||||
className="is-removable wc-block-components-chip wc-block-components-chip--radius-small"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<span
|
||||
aria-hidden={false}
|
||||
className="wc-block-components-chip__text"
|
||||
>
|
||||
Test
|
||||
</span>
|
||||
<span
|
||||
aria-hidden={true}
|
||||
className="wc-block-components-chip__remove"
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
className="wc-block-components-chip__remove-icon"
|
||||
focusable={false}
|
||||
height={16}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={16}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
`;
|
||||
@@ -1,109 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import TestRenderer from 'react-test-renderer';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Chip, RemovableChip } from '..';
|
||||
|
||||
describe( 'Chip', () => {
|
||||
test( 'should render text', () => {
|
||||
const component = TestRenderer.create( <Chip text="Test" /> );
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render nodes as the text', () => {
|
||||
const component = TestRenderer.create(
|
||||
<Chip text={ <h1>Test</h1> } />
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render defined radius', () => {
|
||||
const component = TestRenderer.create(
|
||||
<Chip text="Test" radius="large" />
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render screen reader text', () => {
|
||||
const component = TestRenderer.create(
|
||||
<Chip text="Test" screenReaderText="Test 2" />
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render children nodes', () => {
|
||||
const component = TestRenderer.create(
|
||||
<Chip text="Test">Lorem Ipsum</Chip>
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
describe( 'with custom wrapper', () => {
|
||||
test( 'should render a chip made up of a div instead of a li', () => {
|
||||
const component = TestRenderer.create(
|
||||
<Chip text="Test" element="div" />
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'RemovableChip', () => {
|
||||
test( 'should render text and the remove button', () => {
|
||||
const component = TestRenderer.create( <RemovableChip text="Test" /> );
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render with disabled remove button', () => {
|
||||
const component = TestRenderer.create(
|
||||
<RemovableChip text="Test" disabled={ true } />
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render custom aria label', () => {
|
||||
const component = TestRenderer.create(
|
||||
<RemovableChip text={ <h1>Test</h1> } ariaLabel="Aria test" />
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render default aria label if text is a node', () => {
|
||||
const component = TestRenderer.create(
|
||||
<RemovableChip text={ <h1>Test</h1> } screenReaderText="Test 2" />
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render screen reader text aria label', () => {
|
||||
const component = TestRenderer.create(
|
||||
<RemovableChip text="Test" screenReaderText="Test 2" />
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
describe( 'with removeOnAnyClick', () => {
|
||||
test( 'should be a button when removeOnAnyClick is set to true', () => {
|
||||
const component = TestRenderer.create(
|
||||
<RemovableChip text="Test" removeOnAnyClick={ true } />
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
.components-base-control__field {
|
||||
@include reset-box();
|
||||
position: relative;
|
||||
}
|
||||
.components-combobox-control__suggestions-container {
|
||||
@include reset-typography();
|
||||
@@ -15,7 +16,8 @@
|
||||
input.components-combobox-control__input {
|
||||
@include reset-typography();
|
||||
@include font-size(regular);
|
||||
|
||||
padding: em($gap + $gap-smaller) em($gap-smaller) em($gap-smaller);
|
||||
line-height: em($gap);
|
||||
box-sizing: border-box;
|
||||
outline: inherit;
|
||||
border: 1px solid $input-border-gray;
|
||||
@@ -24,17 +26,14 @@
|
||||
color: $input-text-active;
|
||||
font-family: inherit;
|
||||
font-weight: normal;
|
||||
height: 3em;
|
||||
letter-spacing: inherit;
|
||||
line-height: 1;
|
||||
padding: em($gap-large) $gap em($gap-smallest);
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: none;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
opacity: initial;
|
||||
border-radius: 4px;
|
||||
border-radius: $universal-border-radius;
|
||||
|
||||
&[aria-expanded="true"],
|
||||
&:focus {
|
||||
@@ -67,12 +66,14 @@
|
||||
background-color: $select-dropdown-light;
|
||||
border: 1px solid $input-border-gray;
|
||||
border-top: 0;
|
||||
margin: 3em 0 0 0;
|
||||
margin: 3em 0 0 -1px;
|
||||
padding: 0;
|
||||
max-height: 300px;
|
||||
min-width: 100%;
|
||||
overflow: auto;
|
||||
color: $input-text-active;
|
||||
border-bottom-left-radius: $universal-border-radius;
|
||||
border-bottom-right-radius: $universal-border-radius;
|
||||
|
||||
.has-dark-controls & {
|
||||
background-color: $select-dropdown-dark;
|
||||
@@ -108,14 +109,16 @@
|
||||
label.components-base-control__label {
|
||||
@include reset-typography();
|
||||
@include font-size(regular);
|
||||
line-height: 1.375; // =22px when font-size is 16px.
|
||||
position: absolute;
|
||||
transform: translateY(0.75em);
|
||||
transform: translateY(em($gap));
|
||||
line-height: 1.25; // =20px when font-size is 16px.
|
||||
left: em($gap-smaller);
|
||||
top: 0;
|
||||
transform-origin: top left;
|
||||
transition: all 200ms ease;
|
||||
color: $gray-700;
|
||||
color: $universal-body-low-emphasis;
|
||||
z-index: 1;
|
||||
margin: 0 0 0 #{$gap + 1px};
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: calc(100% - #{2 * $gap});
|
||||
@@ -130,10 +133,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-combobox-control:has(input:-webkit-autofill) {
|
||||
label {
|
||||
transform: translateY(em($gap-smaller)) scale(0.875);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active,
|
||||
&:focus-within {
|
||||
.wc-block-components-combobox-control label.components-base-control__label {
|
||||
transform: translateY(#{$gap-smallest}) scale(0.75);
|
||||
transform: translateY(em($gap-smaller)) scale(0.875);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export const CountryInput = ( {
|
||||
required = false,
|
||||
errorId,
|
||||
errorMessage = __(
|
||||
'Please select a country.',
|
||||
'Please select a country',
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
}: CountryInputWithCountriesProps ): JSX.Element => {
|
||||
|
||||
@@ -3,5 +3,10 @@
|
||||
@import "node_modules/wordpress-components/src/combobox-control/style";
|
||||
|
||||
.wc-block-components-country-input {
|
||||
margin-top: em($gap-large);
|
||||
margin-top: $gap;
|
||||
|
||||
// Fixes width in the editor.
|
||||
.components-flex {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import { _n, sprintf } from '@wordpress/i18n';
|
||||
import Label from '@woocommerce/base-components/label';
|
||||
import { Label } from '@woocommerce/blocks-components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classNames from 'classnames';
|
||||
import Label from '@woocommerce/base-components/label';
|
||||
import { Label } from '@woocommerce/blocks-components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classNames from 'classnames';
|
||||
import Label from '@woocommerce/base-components/label';
|
||||
import { Label } from '@woocommerce/blocks-components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import NumberFormat from 'react-number-format';
|
||||
import type {
|
||||
NumberFormatValues,
|
||||
NumberFormatProps,
|
||||
} from 'react-number-format';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactElement } from 'react';
|
||||
import type { Currency } from '@woocommerce/types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
interface FormattedMonetaryAmountProps
|
||||
extends Omit< NumberFormatProps, 'onValueChange' > {
|
||||
className?: string;
|
||||
displayType?: NumberFormatProps[ 'displayType' ];
|
||||
allowNegative?: boolean;
|
||||
isAllowed?: ( formattedValue: NumberFormatValues ) => boolean;
|
||||
value: number | string; // Value of money amount.
|
||||
currency: Currency | Record< string, never >; // Currency configuration object.
|
||||
onValueChange?: ( unit: number ) => void; // Function to call when value changes.
|
||||
style?: React.CSSProperties | undefined;
|
||||
renderText?: ( value: string ) => JSX.Element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats currency data into the expected format for NumberFormat.
|
||||
*/
|
||||
const currencyToNumberFormat = (
|
||||
currency: FormattedMonetaryAmountProps[ 'currency' ]
|
||||
) => {
|
||||
return {
|
||||
thousandSeparator: currency?.thousandSeparator,
|
||||
decimalSeparator: currency?.decimalSeparator,
|
||||
fixedDecimalScale: true,
|
||||
prefix: currency?.prefix,
|
||||
suffix: currency?.suffix,
|
||||
isNumericString: true,
|
||||
};
|
||||
};
|
||||
|
||||
type CustomFormattedMonetaryAmountProps = Omit<
|
||||
FormattedMonetaryAmountProps,
|
||||
'currency'
|
||||
> & {
|
||||
currency: Currency | Record< string, never >;
|
||||
};
|
||||
|
||||
/**
|
||||
* FormattedMonetaryAmount component.
|
||||
*
|
||||
* Takes a price and returns a formatted price using the NumberFormat component.
|
||||
*/
|
||||
const FormattedMonetaryAmount = ( {
|
||||
className,
|
||||
value: rawValue,
|
||||
currency,
|
||||
onValueChange,
|
||||
displayType = 'text',
|
||||
...props
|
||||
}: CustomFormattedMonetaryAmountProps ): ReactElement | null => {
|
||||
const value =
|
||||
typeof rawValue === 'string' ? parseInt( rawValue, 10 ) : rawValue;
|
||||
|
||||
if ( ! Number.isFinite( value ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const priceValue = value / 10 ** currency.minorUnit;
|
||||
|
||||
if ( ! Number.isFinite( priceValue ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const classes = classNames(
|
||||
'wc-block-formatted-money-amount',
|
||||
'wc-block-components-formatted-money-amount',
|
||||
className
|
||||
);
|
||||
const decimalScale = props.decimalScale ?? currency?.minorUnit;
|
||||
const numberFormatProps = {
|
||||
...props,
|
||||
...currencyToNumberFormat( currency ),
|
||||
decimalScale,
|
||||
value: undefined,
|
||||
currency: undefined,
|
||||
onValueChange: undefined,
|
||||
};
|
||||
|
||||
// Wrapper for NumberFormat onValueChange which handles subunit conversion.
|
||||
const onValueChangeWrapper = onValueChange
|
||||
? ( values: NumberFormatValues ) => {
|
||||
const minorUnitValue = +values.value * 10 ** currency.minorUnit;
|
||||
onValueChange( minorUnitValue );
|
||||
}
|
||||
: () => void 0;
|
||||
|
||||
return (
|
||||
<NumberFormat
|
||||
className={ classes }
|
||||
displayType={ displayType }
|
||||
{ ...numberFormatProps }
|
||||
value={ priceValue }
|
||||
onValueChange={ onValueChangeWrapper }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormattedMonetaryAmount;
|
||||
@@ -1,3 +0,0 @@
|
||||
.wc-block-components-formatted-money-amount {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
export * from './block-error-boundary';
|
||||
export * from './button';
|
||||
export * from './cart-checkout';
|
||||
export * from './checkbox-list';
|
||||
export * from './chip';
|
||||
export * from './combobox';
|
||||
export * from './country-input';
|
||||
export * from './drawer';
|
||||
@@ -12,7 +10,6 @@ export * from './filter-reset-button';
|
||||
export * from './filter-submit-button';
|
||||
export * from './form';
|
||||
export * from './form-token-field';
|
||||
export * from './formatted-monetary-amount';
|
||||
export * from './label';
|
||||
export * from './load-more-button';
|
||||
export * from './loading-mask';
|
||||
@@ -25,14 +22,11 @@ export * from './product-name';
|
||||
export * from './product-price';
|
||||
export * from './product-rating';
|
||||
export * from './quantity-selector';
|
||||
export * from './radio-control';
|
||||
export * from './radio-control-accordion';
|
||||
export * from './read-more';
|
||||
export * from './reviews';
|
||||
export * from './sidebar-layout';
|
||||
export * from './snackbar-list';
|
||||
export * from './sort-select';
|
||||
export * from './spinner';
|
||||
export * from './state-input';
|
||||
export * from './summary';
|
||||
export * from './tabs';
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Fragment } from '@wordpress/element';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactElement, HTMLProps } from 'react';
|
||||
|
||||
interface LabelProps extends HTMLProps< HTMLElement > {
|
||||
label?: string | undefined;
|
||||
screenReaderLabel?: string | undefined;
|
||||
wrapperElement?: string;
|
||||
wrapperProps?: HTMLProps< HTMLElement >;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component used to render an accessible text given a label and/or a
|
||||
* screenReaderLabel. The wrapper element and wrapper props can also be
|
||||
* specified via props.
|
||||
*
|
||||
*/
|
||||
const Label = ( {
|
||||
label,
|
||||
screenReaderLabel,
|
||||
wrapperElement,
|
||||
wrapperProps = {},
|
||||
}: LabelProps ): ReactElement => {
|
||||
let Wrapper;
|
||||
|
||||
const hasLabel = typeof label !== 'undefined' && label !== null;
|
||||
const hasScreenReaderLabel =
|
||||
typeof screenReaderLabel !== 'undefined' && screenReaderLabel !== null;
|
||||
|
||||
if ( ! hasLabel && hasScreenReaderLabel ) {
|
||||
Wrapper = wrapperElement || 'span';
|
||||
wrapperProps = {
|
||||
...wrapperProps,
|
||||
className: classNames(
|
||||
wrapperProps.className,
|
||||
'screen-reader-text'
|
||||
),
|
||||
};
|
||||
|
||||
return <Wrapper { ...wrapperProps }>{ screenReaderLabel }</Wrapper>;
|
||||
}
|
||||
|
||||
Wrapper = wrapperElement || Fragment;
|
||||
|
||||
if ( hasLabel && hasScreenReaderLabel && label !== screenReaderLabel ) {
|
||||
return (
|
||||
<Wrapper { ...wrapperProps }>
|
||||
<span aria-hidden="true">{ label }</span>
|
||||
<span className="screen-reader-text">
|
||||
{ screenReaderLabel }
|
||||
</span>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return <Wrapper { ...wrapperProps }>{ label }</Wrapper>;
|
||||
};
|
||||
|
||||
export default Label;
|
||||
@@ -1,62 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Label with wrapperElement should render both label and screen reader label 1`] = `
|
||||
<label
|
||||
className="foo-bar"
|
||||
data-foo="bar"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
Lorem
|
||||
</span>
|
||||
<span
|
||||
className="screen-reader-text"
|
||||
>
|
||||
Ipsum
|
||||
</span>
|
||||
</label>
|
||||
`;
|
||||
|
||||
exports[`Label with wrapperElement should render only the label 1`] = `
|
||||
<label
|
||||
className="foo-bar"
|
||||
data-foo="bar"
|
||||
>
|
||||
Lorem
|
||||
</label>
|
||||
`;
|
||||
|
||||
exports[`Label with wrapperElement should render only the screen reader label 1`] = `
|
||||
<label
|
||||
className="foo-bar screen-reader-text"
|
||||
data-foo="bar"
|
||||
>
|
||||
Ipsum
|
||||
</label>
|
||||
`;
|
||||
|
||||
exports[`Label without wrapperElement should render both label and screen reader label 1`] = `
|
||||
Array [
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
Lorem
|
||||
</span>,
|
||||
<span
|
||||
className="screen-reader-text"
|
||||
>
|
||||
Ipsum
|
||||
</span>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Label without wrapperElement should render only the label 1`] = `"Lorem"`;
|
||||
|
||||
exports[`Label without wrapperElement should render only the screen reader label 1`] = `
|
||||
<span
|
||||
className="screen-reader-text"
|
||||
>
|
||||
Ipsum
|
||||
</span>
|
||||
`;
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import TestRenderer from 'react-test-renderer';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Label from '../';
|
||||
|
||||
describe( 'Label', () => {
|
||||
describe( 'without wrapperElement', () => {
|
||||
test( 'should render both label and screen reader label', () => {
|
||||
const component = TestRenderer.create(
|
||||
<Label label="Lorem" screenReaderLabel="Ipsum" />
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render only the label', () => {
|
||||
const component = TestRenderer.create( <Label label="Lorem" /> );
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render only the screen reader label', () => {
|
||||
const component = TestRenderer.create(
|
||||
<Label screenReaderLabel="Ipsum" />
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'with wrapperElement', () => {
|
||||
test( 'should render both label and screen reader label', () => {
|
||||
const component = TestRenderer.create(
|
||||
<Label
|
||||
label="Lorem"
|
||||
screenReaderLabel="Ipsum"
|
||||
wrapperElement="label"
|
||||
wrapperProps={ {
|
||||
className: 'foo-bar',
|
||||
'data-foo': 'bar',
|
||||
} }
|
||||
/>
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render only the label', () => {
|
||||
const component = TestRenderer.create(
|
||||
<Label
|
||||
label="Lorem"
|
||||
wrapperElement="label"
|
||||
wrapperProps={ {
|
||||
className: 'foo-bar',
|
||||
'data-foo': 'bar',
|
||||
} }
|
||||
/>
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render only the screen reader label', () => {
|
||||
const component = TestRenderer.create(
|
||||
<Label
|
||||
screenReaderLabel="Ipsum"
|
||||
wrapperElement="label"
|
||||
wrapperProps={ {
|
||||
className: 'foo-bar',
|
||||
'data-foo': 'bar',
|
||||
} }
|
||||
/>
|
||||
);
|
||||
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
@@ -2,13 +2,13 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import Label from '@woocommerce/base-components/label';
|
||||
import type { MouseEventHandler } from 'react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import Label from '../../../../../packages/components/label'; // Imported like this because importing from the components package loads the data stores unnecessarily - not a problem in the front end but would require a lot of unit test rewrites to prevent breaking tests due to incorrect mocks.
|
||||
|
||||
interface LoadMoreButtonProps {
|
||||
onClick: MouseEventHandler;
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { Spinner } from '@woocommerce/blocks-components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import Spinner from '../spinner';
|
||||
|
||||
interface LoadingMaskProps {
|
||||
children?: React.ReactNode | React.ReactNode[];
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
padding: $gap !important;
|
||||
gap: $gap-small;
|
||||
margin: $gap 0;
|
||||
border-radius: 4px;
|
||||
border-radius: $universal-border-radius;
|
||||
border-color: $gray-800;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
@@ -43,7 +43,7 @@
|
||||
}
|
||||
|
||||
// Legacy notice compatibility.
|
||||
.wc-forward.wp-element-button {
|
||||
.wc-forward {
|
||||
float: right;
|
||||
color: $gray-800 !important;
|
||||
background: transparent;
|
||||
@@ -52,6 +52,8 @@
|
||||
border: 0;
|
||||
appearance: none;
|
||||
opacity: 0.6;
|
||||
text-decoration-line: underline;
|
||||
text-underline-position: under;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import classNames from 'classnames';
|
||||
import Label from '@woocommerce/base-components/label';
|
||||
import { Label } from '@woocommerce/blocks-components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
useLayoutEffect,
|
||||
} from '@wordpress/element';
|
||||
import classnames from 'classnames';
|
||||
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
|
||||
import { FormattedMonetaryAmount } from '@woocommerce/blocks-components';
|
||||
import { Currency, isObject } from '@woocommerce/types';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
|
||||
@@ -169,7 +169,6 @@
|
||||
width: 100%;
|
||||
height: 0;
|
||||
display: block;
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
outline: none !important;
|
||||
position: absolute;
|
||||
@@ -332,11 +331,10 @@
|
||||
}
|
||||
.wc-block-components-price-slider__range-input-progress {
|
||||
--range-color: currentColor;
|
||||
margin: -$border-width;
|
||||
}
|
||||
.wc-block-price-filter__range-input {
|
||||
background: transparent;
|
||||
margin: -$border-width;
|
||||
height: 0;
|
||||
width: calc(100% + #{$border-width * 2});
|
||||
&:hover,
|
||||
&:focus {
|
||||
@@ -351,13 +349,24 @@
|
||||
}
|
||||
}
|
||||
&::-webkit-slider-thumb {
|
||||
margin-top: -9px;
|
||||
background: $white;
|
||||
margin-top: -6px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
&.wc-block-components-price-slider__range-input--max::-moz-range-thumb {
|
||||
transform: translate(2px, 1px);
|
||||
background: $white;
|
||||
transform: translate(2px, 2px);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
&.wc-block-components-price-slider__range-input--min::-moz-range-thumb {
|
||||
transform: translate(-2px, 1px);
|
||||
background: $white;
|
||||
transform: translate(-2px, 2px);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
&::-ms-track {
|
||||
border-color: transparent !important;
|
||||
@@ -366,7 +375,6 @@
|
||||
@include ie11() {
|
||||
.wc-block-components-price-slider__range-input-wrapper {
|
||||
border: 0;
|
||||
height: auto;
|
||||
position: relative;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
|
||||
import { FormattedMonetaryAmount } from '@woocommerce/blocks-components';
|
||||
import classNames from 'classnames';
|
||||
import { formatPrice } from '@woocommerce/price-format';
|
||||
import { createInterpolateElement } from '@wordpress/element';
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
$line-height: 1.618;
|
||||
|
||||
.wc-block-components-product-rating {
|
||||
display: block;
|
||||
line-height: 1;
|
||||
|
||||
span {
|
||||
line-height: $line-height;
|
||||
}
|
||||
|
||||
&__stars {
|
||||
display: inline-block;
|
||||
@@ -8,7 +13,7 @@
|
||||
position: relative;
|
||||
width: 5.3em;
|
||||
height: 1.618em;
|
||||
line-height: 1.618;
|
||||
line-height: $line-height;
|
||||
font-size: 1em;
|
||||
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
|
||||
font-family: star;
|
||||
@@ -20,6 +25,7 @@
|
||||
|
||||
&::before {
|
||||
content: "\53\53\53\53\53";
|
||||
line-height: $line-height;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
@@ -44,6 +50,7 @@
|
||||
right: 0;
|
||||
position: absolute;
|
||||
color: inherit;
|
||||
line-height: $line-height;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
@@ -54,9 +61,13 @@
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: $gap-smaller;
|
||||
> * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&__stars + &__reviews_count {
|
||||
margin-left: $gap-smaller;
|
||||
}
|
||||
|
||||
&__norating-container {
|
||||
@@ -72,7 +83,7 @@
|
||||
position: relative;
|
||||
width: 1.5em;
|
||||
height: 1.618em;
|
||||
line-height: 1.618;
|
||||
line-height: $line-height;
|
||||
font-size: 1em;
|
||||
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
|
||||
font-family: star;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
}
|
||||
|
||||
.wc-block-components-quantity-selector {
|
||||
border-radius: 4px;
|
||||
border-radius: $universal-border-radius;
|
||||
// needed so that buttons fill the container.
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
@@ -21,7 +21,7 @@
|
||||
width: 107px;
|
||||
|
||||
&::after {
|
||||
border-radius: 4px;
|
||||
border-radius: $universal-border-radius;
|
||||
border: 1px solid currentColor;
|
||||
bottom: 0;
|
||||
content: "";
|
||||
@@ -89,12 +89,12 @@
|
||||
}
|
||||
|
||||
> .wc-block-components-quantity-selector__button--minus {
|
||||
border-radius: 4px 0 0 4px;
|
||||
border-radius: $universal-border-radius 0 0 $universal-border-radius;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
> .wc-block-components-quantity-selector__button--plus {
|
||||
border-radius: 0 4px 4px 0;
|
||||
border-radius: 0 $universal-border-radius $universal-border-radius 0;
|
||||
order: 3;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { withInstanceId } from '@wordpress/compose';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import RadioControlOption from '../radio-control/option';
|
||||
|
||||
interface RadioControlAccordionProps {
|
||||
className?: string;
|
||||
instanceId: number;
|
||||
id: string;
|
||||
onChange: ( value: string ) => void;
|
||||
options: Array< {
|
||||
value: string;
|
||||
label: string | JSX.Element;
|
||||
onChange?: ( value: string ) => void;
|
||||
name: string;
|
||||
content: JSX.Element;
|
||||
} >;
|
||||
selected: string | null;
|
||||
}
|
||||
|
||||
const RadioControlAccordion = ( {
|
||||
className,
|
||||
instanceId,
|
||||
id,
|
||||
selected,
|
||||
onChange,
|
||||
options = [],
|
||||
}: RadioControlAccordionProps ): JSX.Element | null => {
|
||||
const radioControlId = id || instanceId;
|
||||
|
||||
if ( ! options.length ) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
'wc-block-components-radio-control',
|
||||
className
|
||||
) }
|
||||
>
|
||||
{ options.map( ( option ) => {
|
||||
const hasOptionContent =
|
||||
typeof option === 'object' && 'content' in option;
|
||||
const checked = option.value === selected;
|
||||
return (
|
||||
<div
|
||||
className="wc-block-components-radio-control-accordion-option"
|
||||
key={ option.value }
|
||||
>
|
||||
<RadioControlOption
|
||||
name={ `radio-control-${ radioControlId }` }
|
||||
checked={ checked }
|
||||
option={ option }
|
||||
onChange={ ( value ) => {
|
||||
onChange( value );
|
||||
if ( typeof option.onChange === 'function' ) {
|
||||
option.onChange( value );
|
||||
}
|
||||
} }
|
||||
/>
|
||||
{ hasOptionContent && checked && (
|
||||
<div
|
||||
className={ classnames(
|
||||
'wc-block-components-radio-control-accordion-content',
|
||||
{
|
||||
'wc-block-components-radio-control-accordion-content-hide':
|
||||
! checked,
|
||||
}
|
||||
) }
|
||||
>
|
||||
{ option.content }
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
} ) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default withInstanceId( RadioControlAccordion );
|
||||
export { RadioControlAccordion };
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
import { useInstanceId } from '@wordpress/compose';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import RadioControlOption from './option';
|
||||
import type { RadioControlProps } from './types';
|
||||
import './style.scss';
|
||||
|
||||
const RadioControl = ( {
|
||||
className = '',
|
||||
id,
|
||||
selected = '',
|
||||
onChange,
|
||||
options = [],
|
||||
}: RadioControlProps ): JSX.Element | null => {
|
||||
const instanceId = useInstanceId( RadioControl );
|
||||
const radioControlId = id || instanceId;
|
||||
|
||||
if ( ! options.length ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
'wc-block-components-radio-control',
|
||||
className
|
||||
) }
|
||||
>
|
||||
{ options.map( ( option ) => (
|
||||
<RadioControlOption
|
||||
key={ `${ radioControlId }-${ option.value }` }
|
||||
name={ `radio-control-${ radioControlId }` }
|
||||
checked={ option.value === selected }
|
||||
option={ option }
|
||||
onChange={ ( value: string ) => {
|
||||
onChange( value );
|
||||
if ( typeof option.onChange === 'function' ) {
|
||||
option.onChange( value );
|
||||
}
|
||||
} }
|
||||
/>
|
||||
) ) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadioControl;
|
||||
export { default as RadioControlOption } from './option';
|
||||
export { default as RadioControlOptionLayout } from './option-layout';
|
||||
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { RadioControlOptionLayout } from './types';
|
||||
|
||||
const OptionLayout = ( {
|
||||
label,
|
||||
secondaryLabel,
|
||||
description,
|
||||
secondaryDescription,
|
||||
id,
|
||||
}: RadioControlOptionLayout ): JSX.Element => {
|
||||
return (
|
||||
<div className="wc-block-components-radio-control__option-layout">
|
||||
<div className="wc-block-components-radio-control__label-group">
|
||||
{ label && (
|
||||
<span
|
||||
id={ id && `${ id }__label` }
|
||||
className="wc-block-components-radio-control__label"
|
||||
>
|
||||
{ label }
|
||||
</span>
|
||||
) }
|
||||
{ secondaryLabel && (
|
||||
<span
|
||||
id={ id && `${ id }__secondary-label` }
|
||||
className="wc-block-components-radio-control__secondary-label"
|
||||
>
|
||||
{ secondaryLabel }
|
||||
</span>
|
||||
) }
|
||||
</div>
|
||||
{ ( description || secondaryDescription ) && (
|
||||
<div className="wc-block-components-radio-control__description-group">
|
||||
{ description && (
|
||||
<span
|
||||
id={ id && `${ id }__description` }
|
||||
className="wc-block-components-radio-control__description"
|
||||
>
|
||||
{ description }
|
||||
</span>
|
||||
) }
|
||||
{ secondaryDescription && (
|
||||
<span
|
||||
id={ id && `${ id }__secondary-description` }
|
||||
className="wc-block-components-radio-control__secondary-description"
|
||||
>
|
||||
{ secondaryDescription }
|
||||
</span>
|
||||
) }
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OptionLayout;
|
||||
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import OptionLayout from './option-layout';
|
||||
import type { RadioControlOptionProps } from './types';
|
||||
|
||||
const Option = ( {
|
||||
checked,
|
||||
name,
|
||||
onChange,
|
||||
option,
|
||||
}: RadioControlOptionProps ): JSX.Element => {
|
||||
const { value, label, description, secondaryLabel, secondaryDescription } =
|
||||
option;
|
||||
const onChangeValue = ( event: React.ChangeEvent< HTMLInputElement > ) =>
|
||||
onChange( event.target.value );
|
||||
|
||||
return (
|
||||
<label
|
||||
className={ classnames(
|
||||
'wc-block-components-radio-control__option',
|
||||
{
|
||||
'wc-block-components-radio-control__option-checked':
|
||||
checked,
|
||||
}
|
||||
) }
|
||||
htmlFor={ `${ name }-${ value }` }
|
||||
>
|
||||
<input
|
||||
id={ `${ name }-${ value }` }
|
||||
className="wc-block-components-radio-control__input"
|
||||
type="radio"
|
||||
name={ name }
|
||||
value={ value }
|
||||
onChange={ onChangeValue }
|
||||
checked={ checked }
|
||||
aria-describedby={ classnames( {
|
||||
[ `${ name }-${ value }__label` ]: label,
|
||||
[ `${ name }-${ value }__secondary-label` ]: secondaryLabel,
|
||||
[ `${ name }-${ value }__description` ]: description,
|
||||
[ `${ name }-${ value }__secondary-description` ]:
|
||||
secondaryDescription,
|
||||
} ) }
|
||||
/>
|
||||
<OptionLayout
|
||||
id={ `${ name }-${ value }` }
|
||||
label={ label }
|
||||
secondaryLabel={ secondaryLabel }
|
||||
description={ description }
|
||||
secondaryDescription={ secondaryDescription }
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default Option;
|
||||
@@ -1,119 +0,0 @@
|
||||
.wc-block-components-radio-control__option {
|
||||
@include reset-color();
|
||||
@include reset-typography();
|
||||
display: block;
|
||||
margin: em($gap) 0;
|
||||
padding: 0 0 0 em($gap-larger);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control__option-layout {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control__option .wc-block-components-radio-control__option-layout {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control__label-group,
|
||||
.wc-block-components-radio-control__description-group {
|
||||
display: table-row;
|
||||
|
||||
> span {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control__secondary-label,
|
||||
.wc-block-components-radio-control__secondary-description {
|
||||
text-align: right;
|
||||
min-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control__label,
|
||||
.wc-block-components-radio-control__secondary-label {
|
||||
// Currently, max() CSS function calls need to be wrapped with unquote.
|
||||
// See: https://github.com/sass/sass/issues/2378#issuecomment-367490840
|
||||
// These values should be the same as the control input height.
|
||||
line-height: string.unquote("max(1.5rem, 24px)");
|
||||
}
|
||||
|
||||
.wc-block-components-radio-control__description,
|
||||
.wc-block-components-radio-control__secondary-description {
|
||||
@include font-size(small);
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
// Extra class for specificity.
|
||||
.wc-block-components-radio-control {
|
||||
.wc-block-components-radio-control__input {
|
||||
appearance: none;
|
||||
background: #fff;
|
||||
border: 2px solid $input-border-gray;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
height: em(24px); // =1.5rem
|
||||
min-height: 24px;
|
||||
min-width: 24px;
|
||||
width: em(24px);
|
||||
// The code belows centers the input vertically.
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translate(0, -45%);
|
||||
margin: inherit;
|
||||
cursor: pointer;
|
||||
|
||||
&:checked::before {
|
||||
background: #000;
|
||||
border-radius: 50%;
|
||||
content: "";
|
||||
display: block;
|
||||
height: em(12px);
|
||||
left: 50%;
|
||||
margin: 0;
|
||||
min-height: 12px;
|
||||
min-width: 12px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: em(12px);
|
||||
}
|
||||
|
||||
.has-dark-controls & {
|
||||
border-color: $controls-border-dark;
|
||||
background-color: $input-background-dark;
|
||||
|
||||
&:checked::before {
|
||||
background: $input-text-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme-twentytwentyone {
|
||||
.wc-block-components-radio-control .wc-block-components-radio-control__input {
|
||||
&:checked {
|
||||
border-width: 2px;
|
||||
|
||||
&::before {
|
||||
background-color: var(--form--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
export interface RadioControlProps {
|
||||
// Class name for control.
|
||||
className?: string;
|
||||
// ID for the control.
|
||||
id?: string;
|
||||
// The selected option. This is a controlled component.
|
||||
selected: string;
|
||||
// Fired when an option is changed.
|
||||
onChange: ( value: string ) => void;
|
||||
// List of radio control options.
|
||||
options: RadioControlOption[];
|
||||
}
|
||||
|
||||
export interface RadioControlOptionProps {
|
||||
checked: boolean;
|
||||
name?: string;
|
||||
onChange: ( value: string ) => void;
|
||||
option: RadioControlOption;
|
||||
}
|
||||
|
||||
interface RadioControlOptionContent {
|
||||
label: string | JSX.Element;
|
||||
description?: string | ReactElement | undefined;
|
||||
secondaryLabel?: string | ReactElement | undefined;
|
||||
secondaryDescription?: string | ReactElement | undefined;
|
||||
}
|
||||
|
||||
export interface RadioControlOption extends RadioControlOptionContent {
|
||||
value: string;
|
||||
onChange?: ( value: string ) => void;
|
||||
}
|
||||
|
||||
export interface RadioControlOptionLayout extends RadioControlOptionContent {
|
||||
id?: string;
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copy-pasted from https://github.com/brankosekulic/trimHtml/blob/master/index.js
|
||||
// the published npm version of this code contains a bug that causes it throw exceptions.
|
||||
export function trimHtml( html, options ) {
|
||||
options = options || {};
|
||||
|
||||
const limit = options.limit || 100,
|
||||
preserveTags =
|
||||
typeof options.preserveTags !== 'undefined'
|
||||
? options.preserveTags
|
||||
: true,
|
||||
wordBreak =
|
||||
typeof options.wordBreak !== 'undefined'
|
||||
? options.wordBreak
|
||||
: false,
|
||||
suffix = options.suffix || '...',
|
||||
moreLink = options.moreLink || '',
|
||||
moreText = options.moreText || '»',
|
||||
preserveWhiteSpace = options.preserveWhiteSpace || false;
|
||||
|
||||
const arr = html
|
||||
.replace( /</g, '\n<' )
|
||||
.replace( />/g, '>\n' )
|
||||
.replace( /\n\n/g, '\n' )
|
||||
.replace( /^\n/g, '' )
|
||||
.replace( /\n$/g, '' )
|
||||
.split( '\n' );
|
||||
|
||||
let sum = 0,
|
||||
row,
|
||||
cut,
|
||||
add,
|
||||
rowCut,
|
||||
tagMatch,
|
||||
tagName,
|
||||
// eslint-disable-next-line prefer-const
|
||||
tagStack = [],
|
||||
more = false;
|
||||
|
||||
for ( let i = 0; i < arr.length; i++ ) {
|
||||
row = arr[ i ];
|
||||
|
||||
// count multiple spaces as one character
|
||||
if ( ! preserveWhiteSpace ) {
|
||||
rowCut = row.replace( /[ ]+/g, ' ' );
|
||||
} else {
|
||||
rowCut = row;
|
||||
}
|
||||
|
||||
if ( ! row.length ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const charArr = getCharArr( rowCut );
|
||||
|
||||
if ( row[ 0 ] !== '<' ) {
|
||||
if ( sum >= limit ) {
|
||||
row = '';
|
||||
} else if ( sum + charArr.length >= limit ) {
|
||||
cut = limit - sum;
|
||||
|
||||
if ( charArr[ cut - 1 ] === ' ' ) {
|
||||
while ( cut ) {
|
||||
cut -= 1;
|
||||
if ( charArr[ cut - 1 ] !== ' ' ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
add = charArr.slice( cut ).indexOf( ' ' );
|
||||
|
||||
// break on halh of word
|
||||
if ( ! wordBreak ) {
|
||||
if ( add !== -1 ) {
|
||||
cut += add;
|
||||
} else {
|
||||
cut = row.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
row = charArr.slice( 0, cut ).join( '' ) + suffix;
|
||||
|
||||
if ( moreLink ) {
|
||||
row +=
|
||||
'<a href="' +
|
||||
moreLink +
|
||||
'" style="display:inline">' +
|
||||
moreText +
|
||||
'</a>';
|
||||
}
|
||||
|
||||
sum = limit;
|
||||
more = true;
|
||||
} else {
|
||||
sum += charArr.length;
|
||||
}
|
||||
} else if ( ! preserveTags ) {
|
||||
row = '';
|
||||
} else if ( sum >= limit ) {
|
||||
tagMatch = row.match( /[a-zA-Z]+/ );
|
||||
tagName = tagMatch ? tagMatch[ 0 ] : '';
|
||||
|
||||
if ( tagName ) {
|
||||
if ( row.substring( 0, 2 ) !== '</' ) {
|
||||
tagStack.push( tagName );
|
||||
row = '';
|
||||
} else {
|
||||
while (
|
||||
tagStack[ tagStack.length - 1 ] !== tagName &&
|
||||
tagStack.length
|
||||
) {
|
||||
tagStack.pop();
|
||||
}
|
||||
|
||||
if ( tagStack.length ) {
|
||||
row = '';
|
||||
}
|
||||
|
||||
tagStack.pop();
|
||||
}
|
||||
} else {
|
||||
row = '';
|
||||
}
|
||||
}
|
||||
|
||||
arr[ i ] = row;
|
||||
}
|
||||
|
||||
return {
|
||||
html: arr.join( '\n' ).replace( /\n/g, '' ),
|
||||
more,
|
||||
};
|
||||
}
|
||||
|
||||
// count symbols like one char
|
||||
function getCharArr( rowCut ) {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let charArr = [],
|
||||
subRow,
|
||||
match,
|
||||
char;
|
||||
|
||||
for ( let i = 0; i < rowCut.length; i++ ) {
|
||||
subRow = rowCut.substring( i );
|
||||
match = subRow.match( /^&[a-z0-9#]+;/ );
|
||||
|
||||
if ( match ) {
|
||||
char = match[ 0 ];
|
||||
charArr.push( char );
|
||||
i += char.length - 1;
|
||||
} else {
|
||||
charArr.push( rowCut[ i ] );
|
||||
}
|
||||
}
|
||||
|
||||
return charArr;
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { trimHtml } from './trim-html';
|
||||
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import trimHtml from 'trim-html';
|
||||
|
||||
type Markers = {
|
||||
end: number;
|
||||
middle: number;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user