[Experimental] Product Filters: New and improved blocks structure (#51096)
This commit is contained in:
parent
3854f710df
commit
93053f39ed
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reason for using this component instead of the core/disabled component is
|
||||||
|
* that the Disabled component disrupts the focus on inner blocks. For example,
|
||||||
|
* when a heading block is nested inside, the text cursor, which indicates the
|
||||||
|
* editable area, isn't visible when focused on the heading block.
|
||||||
|
*
|
||||||
|
* This component only uses CSS to control the selected behavior of inner
|
||||||
|
* blocks, which fixes the abovementioned issues. However, being a static
|
||||||
|
* component comes with a limitation: this component is meant to be placed
|
||||||
|
* directly inside the block wrapper element that holds block props.
|
||||||
|
*/
|
||||||
|
export const InitialDisabled = ( {
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
} ): JSX.Element => (
|
||||||
|
<div className="wc-block-product-filter-components-initial-disabled">
|
||||||
|
<div className="wc-block-product-filter-components-initial-disabled-overlay" />
|
||||||
|
{ children }
|
||||||
|
</div>
|
||||||
|
);
|
|
@ -0,0 +1,17 @@
|
||||||
|
.wc-block-product-filter-components-initial-disabled {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.wc-block-product-filter-components-initial-disabled-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.is-selected > &,
|
||||||
|
.has-child-selected > & {
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { Icon } from '@wordpress/components';
|
||||||
|
import { info } from '@wordpress/icons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom notice component is designed specifically for new filter blocks. We
|
||||||
|
* are not reusing the existing components because we have a new design for the
|
||||||
|
* filter blocks notice. We want users to utilize the sidebar for attribute
|
||||||
|
* settings, so we are keeping the new notice minimal."
|
||||||
|
*/
|
||||||
|
export const Notice = ( { children }: { children: React.ReactNode } ) => (
|
||||||
|
<div className="wc-block-product-filter-components-notice">
|
||||||
|
<Icon
|
||||||
|
className="wc-block-product-filter-components-notice__icon"
|
||||||
|
icon={ info }
|
||||||
|
/>
|
||||||
|
<div className="wc-block-product-filter-components-notice__content">
|
||||||
|
{ children }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
|
@ -0,0 +1,17 @@
|
||||||
|
.wc-block-product-filter-components-notice {
|
||||||
|
display: flex;
|
||||||
|
padding: $gap;
|
||||||
|
gap: $gap-smaller;
|
||||||
|
border: 1px solid;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
fill: $alert-red;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
> * {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,3 +3,9 @@ export const BlockOverlayAttribute = {
|
||||||
MOBILE: 'mobile',
|
MOBILE: 'mobile',
|
||||||
ALWAYS: 'always',
|
ALWAYS: 'always',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const EXCLUDED_BLOCKS = [
|
||||||
|
'woocommerce/product-filter-attribute',
|
||||||
|
'woocommerce/product-collection',
|
||||||
|
'core/query',
|
||||||
|
];
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { getSetting } from '@woocommerce/settings';
|
import { getSetting } from '@woocommerce/settings';
|
||||||
import { AttributeSetting } from '@woocommerce/types';
|
|
||||||
import {
|
import {
|
||||||
InnerBlocks,
|
InnerBlocks,
|
||||||
InspectorControls,
|
InspectorControls,
|
||||||
|
@ -37,10 +36,6 @@ import './editor.scss';
|
||||||
import { type BlockAttributes } from './types';
|
import { type BlockAttributes } from './types';
|
||||||
import { BlockOverlayAttribute } from './constants';
|
import { BlockOverlayAttribute } from './constants';
|
||||||
|
|
||||||
const defaultAttribute = getSetting< AttributeSetting >(
|
|
||||||
'defaultProductFilterAttribute'
|
|
||||||
);
|
|
||||||
|
|
||||||
const TEMPLATE: InnerBlockTemplate[] = [
|
const TEMPLATE: InnerBlockTemplate[] = [
|
||||||
[
|
[
|
||||||
'core/heading',
|
'core/heading',
|
||||||
|
@ -50,42 +45,8 @@ const TEMPLATE: InnerBlockTemplate[] = [
|
||||||
content: __( 'Filters', 'woocommerce' ),
|
content: __( 'Filters', 'woocommerce' ),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[ 'woocommerce/product-filter-active' ],
|
||||||
'woocommerce/product-filter',
|
[ 'woocommerce/product-filter-attribute' ],
|
||||||
{
|
|
||||||
filterType: 'active-filters',
|
|
||||||
heading: __( 'Active', 'woocommerce' ),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'woocommerce/product-filter',
|
|
||||||
{
|
|
||||||
filterType: 'price-filter',
|
|
||||||
heading: __( 'Price', 'woocommerce' ),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'woocommerce/product-filter',
|
|
||||||
{
|
|
||||||
filterType: 'stock-filter',
|
|
||||||
heading: __( 'Status', 'woocommerce' ),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'woocommerce/product-filter',
|
|
||||||
{
|
|
||||||
filterType: 'attribute-filter',
|
|
||||||
heading: defaultAttribute.attribute_label,
|
|
||||||
attributeId: parseInt( defaultAttribute.attribute_id, 10 ),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'woocommerce/product-filter',
|
|
||||||
{
|
|
||||||
filterType: 'rating-filter',
|
|
||||||
heading: __( 'Rating', 'woocommerce' ),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
'core/buttons',
|
'core/buttons',
|
||||||
{ layout: { type: 'flex' } },
|
{ layout: { type: 'flex' } },
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { getContext as getContextFn, store } from '@woocommerce/interactivity';
|
import {
|
||||||
|
getContext as getContextFn,
|
||||||
|
store,
|
||||||
|
navigate as navigateFn,
|
||||||
|
} from '@woocommerce/interactivity';
|
||||||
|
import { getSetting } from '@woocommerce/settings';
|
||||||
|
|
||||||
export interface ProductFiltersContext {
|
export interface ProductFiltersContext {
|
||||||
isDialogOpen: boolean;
|
isDialogOpen: boolean;
|
||||||
|
@ -11,7 +16,7 @@ export interface ProductFiltersContext {
|
||||||
const getContext = ( ns?: string ) =>
|
const getContext = ( ns?: string ) =>
|
||||||
getContextFn< ProductFiltersContext >( ns );
|
getContextFn< ProductFiltersContext >( ns );
|
||||||
|
|
||||||
const productFilters = {
|
store( 'woocommerce/product-filters', {
|
||||||
state: {
|
state: {
|
||||||
isDialogOpen: () => {
|
isDialogOpen: () => {
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
|
@ -36,8 +41,41 @@ const productFilters = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
callbacks: {},
|
callbacks: {},
|
||||||
};
|
} );
|
||||||
|
|
||||||
store( 'woocommerce/product-filters', productFilters );
|
const isBlockTheme = getSetting< boolean >( 'isBlockTheme' );
|
||||||
|
const isProductArchive = getSetting< boolean >( 'isProductArchive' );
|
||||||
|
const needsRefresh = getSetting< boolean >(
|
||||||
|
'needsRefreshForInteractivityAPI',
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
export type ProductFilters = typeof productFilters;
|
export function navigate( href: string, options = {} ) {
|
||||||
|
/**
|
||||||
|
* We may need to reset the current page when changing filters.
|
||||||
|
* This is because the current page may not exist for this set
|
||||||
|
* of filters and will 404 when the user navigates to it.
|
||||||
|
*
|
||||||
|
* There are different pagination formats to consider, as documented here:
|
||||||
|
* https://github.com/WordPress/gutenberg/blob/317eb8f14c8e1b81bf56972cca2694be250580e3/packages/block-library/src/query-pagination-numbers/index.php#L22-L85
|
||||||
|
*/
|
||||||
|
const url = new URL( href );
|
||||||
|
// When pretty permalinks are enabled, the page number may be in the path name.
|
||||||
|
url.pathname = url.pathname.replace( /\/page\/[0-9]+/i, '' );
|
||||||
|
// When plain permalinks are enabled, the page number may be in the "paged" query parameter.
|
||||||
|
url.searchParams.delete( 'paged' );
|
||||||
|
// On posts and pages the page number will be in a query parameter that
|
||||||
|
// identifies which block we are paginating.
|
||||||
|
url.searchParams.forEach( ( _, key ) => {
|
||||||
|
if ( key.match( /^query(?:-[0-9]+)?-page$/ ) ) {
|
||||||
|
url.searchParams.delete( key );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
// Make sure to update the href with the changes.
|
||||||
|
href = url.href;
|
||||||
|
|
||||||
|
if ( needsRefresh || ( ! isBlockTheme && isProductArchive ) ) {
|
||||||
|
return ( window.location.href = href );
|
||||||
|
}
|
||||||
|
return navigateFn( href, options );
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||||
"name": "woocommerce/product-filter-active",
|
"name": "woocommerce/product-filter-active",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"title": "Filter Options",
|
"title": "Active (Experimental)",
|
||||||
"description": "Display the currently active filters.",
|
"description": "Display the currently active filters.",
|
||||||
"category": "woocommerce",
|
"category": "woocommerce",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
@ -11,11 +11,10 @@
|
||||||
"textdomain": "woocommerce",
|
"textdomain": "woocommerce",
|
||||||
"apiVersion": 3,
|
"apiVersion": 3,
|
||||||
"ancestor": [
|
"ancestor": [
|
||||||
"woocommerce/product-filter"
|
"woocommerce/product-filters"
|
||||||
],
|
],
|
||||||
"supports": {
|
"supports": {
|
||||||
"interactivity": true,
|
"interactivity": true,
|
||||||
"inserter": false,
|
|
||||||
"color": {
|
"color": {
|
||||||
"text": true,
|
"text": true,
|
||||||
"background": false
|
"background": false
|
||||||
|
@ -27,7 +26,7 @@
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"displayStyle": {
|
"displayStyle": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "list"
|
"default": "chips"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,7 +6,7 @@ import { store, getContext } from '@woocommerce/interactivity';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { navigate } from '../product-filter/frontend';
|
import { navigate } from '../../frontend';
|
||||||
|
|
||||||
type ActiveFiltersContext = {
|
type ActiveFiltersContext = {
|
||||||
queryId: number;
|
queryId: number;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
import { registerBlockType } from '@wordpress/blocks';
|
import { registerBlockType } from '@wordpress/blocks';
|
||||||
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
|
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
|
||||||
import { productFilterOptions } from '@woocommerce/icons';
|
import { productFilterActive } from '@woocommerce/icons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -14,7 +14,7 @@ import './style.scss';
|
||||||
|
|
||||||
if ( isExperimentalBlocksEnabled() ) {
|
if ( isExperimentalBlocksEnabled() ) {
|
||||||
registerBlockType( metadata, {
|
registerBlockType( metadata, {
|
||||||
icon: productFilterOptions,
|
icon: productFilterActive,
|
||||||
edit: Edit,
|
edit: Edit,
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||||
"name": "woocommerce/product-filter-attribute",
|
"name": "woocommerce/product-filter-attribute",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"title": "Filter Options",
|
"title": "Attribute (Experimental)",
|
||||||
"description": "Enable customers to filter the product grid by selecting one or more attributes, such as color.",
|
"description": "Enable customers to filter the product grid by selecting one or more attributes, such as color.",
|
||||||
"category": "woocommerce",
|
"category": "woocommerce",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
@ -11,11 +11,10 @@
|
||||||
"textdomain": "woocommerce",
|
"textdomain": "woocommerce",
|
||||||
"apiVersion": 3,
|
"apiVersion": 3,
|
||||||
"ancestor": [
|
"ancestor": [
|
||||||
"woocommerce/product-filter"
|
"woocommerce/product-filters"
|
||||||
],
|
],
|
||||||
"supports": {
|
"supports": {
|
||||||
"interactivity": true,
|
"interactivity": true,
|
||||||
"inserter": false,
|
|
||||||
"color": {
|
"color": {
|
||||||
"text": true,
|
"text": true,
|
||||||
"background": false,
|
"background": false,
|
||||||
|
@ -78,7 +77,7 @@
|
||||||
},
|
},
|
||||||
"displayStyle": {
|
"displayStyle": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "list"
|
"default": "woocommerce/product-filter-checkbox-list"
|
||||||
},
|
},
|
||||||
"selectType": {
|
"selectType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -100,5 +99,10 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default":true
|
"default":true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"attributes": {
|
||||||
|
"isPreview": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,29 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { __, sprintf } from '@wordpress/i18n';
|
|
||||||
import { Icon, chevronDown } from '@wordpress/icons';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal dependencies
|
|
||||||
*/
|
|
||||||
import { PreviewDropdown } from '../../components/preview-dropdown';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AttributeDropdown = ( { label }: Props ) => {
|
|
||||||
return (
|
|
||||||
<div className="wc-block-attribute-filter style-dropdown">
|
|
||||||
<PreviewDropdown
|
|
||||||
placeholder={ sprintf(
|
|
||||||
/* translators: %s attribute name. */
|
|
||||||
__( 'Select %s', 'woocommerce' ),
|
|
||||||
label
|
|
||||||
) }
|
|
||||||
/>
|
|
||||||
<Icon icon={ chevronDown } size={ 30 } />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,86 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { sort } from 'fast-sort';
|
|
||||||
import { __, sprintf, _n } from '@wordpress/i18n';
|
|
||||||
import { SearchListControl } from '@woocommerce/editor-components/search-list-control';
|
|
||||||
import { getSetting } from '@woocommerce/settings';
|
|
||||||
import { SearchListItem } from '@woocommerce/editor-components/search-list-control/types';
|
|
||||||
import { AttributeSetting } from '@woocommerce/types';
|
|
||||||
|
|
||||||
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
|
|
||||||
|
|
||||||
type AttributeSelectControlsProps = {
|
|
||||||
isCompact: boolean;
|
|
||||||
setAttributeId: ( id: number ) => void;
|
|
||||||
attributeId: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AttributeSelectControls = ( {
|
|
||||||
isCompact,
|
|
||||||
setAttributeId,
|
|
||||||
attributeId,
|
|
||||||
}: AttributeSelectControlsProps ) => {
|
|
||||||
const messages = {
|
|
||||||
clear: __( 'Clear selected attribute', 'woocommerce' ),
|
|
||||||
list: __( 'Product Attributes', 'woocommerce' ),
|
|
||||||
noItems: __(
|
|
||||||
"Your store doesn't have any product attributes.",
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
search: __( 'Search for a product attribute:', 'woocommerce' ),
|
|
||||||
selected: ( n: number ) =>
|
|
||||||
sprintf(
|
|
||||||
/* translators: %d is the number of attributes selected. */
|
|
||||||
_n(
|
|
||||||
'%d attribute selected',
|
|
||||||
'%d attributes selected',
|
|
||||||
n,
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
n
|
|
||||||
),
|
|
||||||
updated: __(
|
|
||||||
'Product attribute search results updated.',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const list = sort(
|
|
||||||
ATTRIBUTES.map( ( item ) => {
|
|
||||||
return {
|
|
||||||
id: parseInt( item.attribute_id, 10 ),
|
|
||||||
name: item.attribute_label,
|
|
||||||
};
|
|
||||||
} )
|
|
||||||
).asc( 'name' );
|
|
||||||
|
|
||||||
const onChange = ( selected: SearchListItem[] ) => {
|
|
||||||
if ( ! selected || ! selected.length ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedId = selected[ 0 ].id;
|
|
||||||
const productAttribute = ATTRIBUTES.find(
|
|
||||||
( attribute ) => attribute.attribute_id === selectedId.toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( ! productAttribute || attributeId === selectedId ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setAttributeId( selectedId as number );
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SearchListControl
|
|
||||||
className="woocommerce-product-attributes"
|
|
||||||
list={ list }
|
|
||||||
selected={ list.filter( ( { id } ) => id === attributeId ) }
|
|
||||||
onChange={ onChange }
|
|
||||||
messages={ messages }
|
|
||||||
isSingle
|
|
||||||
isCompact={ isCompact }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,68 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ideally, this component should belong to packages/interactivity-components.
|
|
||||||
* But we haven't export it as a packages so we place it here temporary.
|
|
||||||
*/
|
|
||||||
export const Preview = ( { items }: { items: string[] } ) => {
|
|
||||||
const threshold = 15;
|
|
||||||
const isLongList = items.length > threshold;
|
|
||||||
return (
|
|
||||||
<div className="wc-block-interactivity-components-checkbox-list">
|
|
||||||
<ul className="wc-block-interactivity-components-checkbox-list__list">
|
|
||||||
{ ( isLongList ? items.slice( 0, threshold ) : items ).map(
|
|
||||||
( item, index ) => (
|
|
||||||
<li
|
|
||||||
key={ index }
|
|
||||||
className="wc-block-interactivity-components-checkbox-list__item"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
htmlFor={ `interactive-checkbox-${ index }` }
|
|
||||||
className=" wc-block-interactivity-components-checkbox-list__label"
|
|
||||||
>
|
|
||||||
<span className="wc-block-interactive-components-checkbox-list__input-wrapper">
|
|
||||||
<span className="wc-block-interactivity-components-checkbox-list__input-wrapper">
|
|
||||||
<input
|
|
||||||
name={ `interactive-checkbox-${ index }` }
|
|
||||||
type="checkbox"
|
|
||||||
className="wc-block-interactivity-components-checkbox-list__input"
|
|
||||||
// Harded coded some checked items for styling purpose.
|
|
||||||
defaultChecked={ [
|
|
||||||
1, 3, 4,
|
|
||||||
].includes( index ) }
|
|
||||||
/>
|
|
||||||
<svg
|
|
||||||
className="wc-block-interactivity-components-checkbox-list__mark"
|
|
||||||
viewBox="0 0 10 8"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M9.25 1.19922L3.75 6.69922L1 3.94922"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span className="wc-block-interactivity-components-checkbox-list__text">
|
|
||||||
{ item }
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
) }
|
|
||||||
</ul>
|
|
||||||
{ isLongList && (
|
|
||||||
<span className="wc-block-interactivity-components-checkbox-list__show-more">
|
|
||||||
<small>{ __( 'Show more…', 'woocommerce' ) }</small>
|
|
||||||
</span>
|
|
||||||
) }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,55 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
import { Icon, category, external } from '@wordpress/icons';
|
|
||||||
import { getAdminLink } from '@woocommerce/settings';
|
|
||||||
import { Placeholder, Button } from '@wordpress/components';
|
|
||||||
|
|
||||||
export const AttributesPlaceholder = ( {
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
} ) => (
|
|
||||||
<Placeholder
|
|
||||||
className="wc-block-attribute-filter"
|
|
||||||
icon={ <Icon icon={ category } /> }
|
|
||||||
label={ __( 'Filter by Attribute', 'woocommerce' ) }
|
|
||||||
instructions={ __(
|
|
||||||
'Enable customers to filter the product grid by selecting one or more attributes, such as color.',
|
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
>
|
|
||||||
{ children }
|
|
||||||
</Placeholder>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const NoAttributesPlaceholder = () => (
|
|
||||||
<AttributesPlaceholder>
|
|
||||||
<p>
|
|
||||||
{ __(
|
|
||||||
"Attributes are needed for filtering your products. You haven't created any attributes yet.",
|
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
className="wc-block-attribute-filter__add-attribute-button"
|
|
||||||
variant="secondary"
|
|
||||||
href={ getAdminLink(
|
|
||||||
'edit.php?post_type=product&page=product_attributes'
|
|
||||||
) }
|
|
||||||
target="_top"
|
|
||||||
>
|
|
||||||
{ __( 'Add new attribute', 'woocommerce' ) + ' ' }
|
|
||||||
<Icon icon={ external } />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="wc-block-attribute-filter__read_more_button"
|
|
||||||
variant="tertiary"
|
|
||||||
href="https://woocommerce.com/document/managing-product-taxonomies/"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{ __( 'Learn more', 'woocommerce' ) }
|
|
||||||
</Button>
|
|
||||||
</AttributesPlaceholder>
|
|
||||||
);
|
|
|
@ -5,6 +5,9 @@ import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
export const attributeOptionsPreview = [
|
export const attributeOptionsPreview = [
|
||||||
{
|
{
|
||||||
|
label: __( 'Blue', 'woocommerce' ),
|
||||||
|
value: 'blue',
|
||||||
|
rawData: {
|
||||||
id: 23,
|
id: 23,
|
||||||
name: __( 'Blue', 'woocommerce' ),
|
name: __( 'Blue', 'woocommerce' ),
|
||||||
slug: 'blue',
|
slug: 'blue',
|
||||||
|
@ -13,7 +16,12 @@ export const attributeOptionsPreview = [
|
||||||
parent: 0,
|
parent: 0,
|
||||||
count: 4,
|
count: 4,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
|
label: __( 'Gray', 'woocommerce' ),
|
||||||
|
value: 'gray',
|
||||||
|
selected: true,
|
||||||
|
rawData: {
|
||||||
id: 29,
|
id: 29,
|
||||||
name: __( 'Gray', 'woocommerce' ),
|
name: __( 'Gray', 'woocommerce' ),
|
||||||
slug: 'gray',
|
slug: 'gray',
|
||||||
|
@ -22,7 +30,11 @@ export const attributeOptionsPreview = [
|
||||||
parent: 0,
|
parent: 0,
|
||||||
count: 3,
|
count: 3,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
|
label: __( 'Green', 'woocommerce' ),
|
||||||
|
value: 'green',
|
||||||
|
rawData: {
|
||||||
id: 24,
|
id: 24,
|
||||||
name: __( 'Green', 'woocommerce' ),
|
name: __( 'Green', 'woocommerce' ),
|
||||||
slug: 'green',
|
slug: 'green',
|
||||||
|
@ -31,7 +43,12 @@ export const attributeOptionsPreview = [
|
||||||
parent: 0,
|
parent: 0,
|
||||||
count: 3,
|
count: 3,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
|
label: __( 'Red', 'woocommerce' ),
|
||||||
|
value: 'red',
|
||||||
|
selected: true,
|
||||||
|
rawData: {
|
||||||
id: 25,
|
id: 25,
|
||||||
name: __( 'Red', 'woocommerce' ),
|
name: __( 'Red', 'woocommerce' ),
|
||||||
slug: 'red',
|
slug: 'red',
|
||||||
|
@ -40,7 +57,11 @@ export const attributeOptionsPreview = [
|
||||||
parent: 0,
|
parent: 0,
|
||||||
count: 4,
|
count: 4,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
|
label: __( 'Yellow', 'woocommerce' ),
|
||||||
|
value: 'yellow',
|
||||||
|
rawData: {
|
||||||
id: 30,
|
id: 30,
|
||||||
name: __( 'Yellow', 'woocommerce' ),
|
name: __( 'Yellow', 'woocommerce' ),
|
||||||
slug: 'yellow',
|
slug: 'yellow',
|
||||||
|
@ -49,6 +70,7 @@ export const attributeOptionsPreview = [
|
||||||
parent: 0,
|
parent: 0,
|
||||||
count: 1,
|
count: 1,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const sortOrders = {
|
export const sortOrders = {
|
||||||
|
|
|
@ -5,29 +5,34 @@ import {
|
||||||
useCollection,
|
useCollection,
|
||||||
useCollectionData,
|
useCollectionData,
|
||||||
} from '@woocommerce/base-context/hooks';
|
} from '@woocommerce/base-context/hooks';
|
||||||
import { getSetting } from '@woocommerce/settings';
|
|
||||||
import {
|
import {
|
||||||
AttributeSetting,
|
AttributeSetting,
|
||||||
AttributeTerm,
|
AttributeTerm,
|
||||||
objectHasProp,
|
objectHasProp,
|
||||||
} from '@woocommerce/types';
|
} from '@woocommerce/types';
|
||||||
import { useBlockProps } from '@wordpress/block-editor';
|
import {
|
||||||
import { Disabled, Notice, withSpokenMessages } from '@wordpress/components';
|
useBlockProps,
|
||||||
import { useEffect, useState, useMemo } from '@wordpress/element';
|
useInnerBlocksProps,
|
||||||
|
BlockContextProvider,
|
||||||
|
} from '@wordpress/block-editor';
|
||||||
|
import { withSpokenMessages } from '@wordpress/components';
|
||||||
|
import { useEffect, useState } from '@wordpress/element';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { getSetting } from '@woocommerce/settings';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { AttributeDropdown } from './components/attribute-dropdown';
|
import { Inspector } from './inspector';
|
||||||
import { Preview as CheckboxListPreview } from './components/checkbox-list-editor';
|
|
||||||
import { Inspector } from './components/inspector';
|
|
||||||
import { NoAttributesPlaceholder } from './components/placeholder';
|
|
||||||
import { attributeOptionsPreview } from './constants';
|
import { attributeOptionsPreview } from './constants';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
import { EditProps, isAttributeCounts } from './types';
|
import { EditProps, isAttributeCounts } from './types';
|
||||||
import { getAttributeFromId } from './utils';
|
import { getAttributeFromId } from './utils';
|
||||||
import './editor.scss';
|
import { getAllowedBlocks } from '../../utils';
|
||||||
|
import { EXCLUDED_BLOCKS } from '../../constants';
|
||||||
|
import { FilterOptionItem } from '../../types';
|
||||||
|
import { InitialDisabled } from '../../components/initial-disabled';
|
||||||
|
import { Notice } from '../../components/notice';
|
||||||
|
|
||||||
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
|
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
|
||||||
|
|
||||||
|
@ -47,8 +52,10 @@ const Edit = ( props: EditProps ) => {
|
||||||
const attributeObject = getAttributeFromId( attributeId );
|
const attributeObject = getAttributeFromId( attributeId );
|
||||||
|
|
||||||
const [ attributeOptions, setAttributeOptions ] = useState<
|
const [ attributeOptions, setAttributeOptions ] = useState<
|
||||||
AttributeTerm[]
|
FilterOptionItem[]
|
||||||
>( [] );
|
>( [] );
|
||||||
|
const [ isOptionsLoading, setIsOptionsLoading ] =
|
||||||
|
useState< boolean >( true );
|
||||||
|
|
||||||
const { results: attributeTerms, isLoading: isTermsLoading } =
|
const { results: attributeTerms, isLoading: isTermsLoading } =
|
||||||
useCollection< AttributeTerm >( {
|
useCollection< AttributeTerm >( {
|
||||||
|
@ -59,7 +66,7 @@ const Edit = ( props: EditProps ) => {
|
||||||
query: { orderby: 'menu_order', hide_empty: hideEmpty },
|
query: { orderby: 'menu_order', hide_empty: hideEmpty },
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const { results: filteredCounts, isLoading: isCountsLoading } =
|
const { results: filteredCounts, isLoading: isFilterCountsLoading } =
|
||||||
useCollectionData( {
|
useCollectionData( {
|
||||||
queryAttribute: {
|
queryAttribute: {
|
||||||
taxonomy: attributeObject?.taxonomy || '',
|
taxonomy: attributeObject?.taxonomy || '',
|
||||||
|
@ -69,18 +76,18 @@ const Edit = ( props: EditProps ) => {
|
||||||
isEditor: true,
|
isEditor: true,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const isLoading = isTermsLoading || isCountsLoading;
|
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
|
if ( isTermsLoading || isFilterCountsLoading ) return;
|
||||||
|
|
||||||
const termIdHasProducts =
|
const termIdHasProducts =
|
||||||
objectHasProp( filteredCounts, 'attribute_counts' ) &&
|
objectHasProp( filteredCounts, 'attribute_counts' ) &&
|
||||||
isAttributeCounts( filteredCounts.attribute_counts )
|
isAttributeCounts( filteredCounts.attribute_counts )
|
||||||
? filteredCounts.attribute_counts.map( ( term ) => term.term )
|
? filteredCounts.attribute_counts.map( ( term ) => term.term )
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
if ( termIdHasProducts.length === 0 && hideEmpty )
|
if ( termIdHasProducts.length === 0 && hideEmpty ) {
|
||||||
return setAttributeOptions( [] );
|
setAttributeOptions( [] );
|
||||||
|
} else {
|
||||||
setAttributeOptions(
|
setAttributeOptions(
|
||||||
attributeTerms
|
attributeTerms
|
||||||
.filter( ( term ) => {
|
.filter( ( term ) => {
|
||||||
|
@ -101,58 +108,105 @@ const Edit = ( props: EditProps ) => {
|
||||||
return a.count < b.count ? 1 : -1;
|
return a.count < b.count ? 1 : -1;
|
||||||
}
|
}
|
||||||
} )
|
} )
|
||||||
);
|
.map( ( term, index ) => ( {
|
||||||
}, [ attributeTerms, filteredCounts, sortOrder, hideEmpty ] );
|
label: showCounts
|
||||||
|
? `${ term.name } (${ term.count })`
|
||||||
const Wrapper = ( { children }: { children: React.ReactNode } ) => (
|
: term.name,
|
||||||
<div { ...useBlockProps() }>
|
value: term.id.toString(),
|
||||||
<Inspector { ...props } />
|
selected: index === 1,
|
||||||
{ children }
|
rawData: term,
|
||||||
</div>
|
} ) )
|
||||||
);
|
|
||||||
|
|
||||||
const loadingState = useMemo( () => {
|
|
||||||
return [ ...Array( 5 ) ].map( ( x, i ) => (
|
|
||||||
<li
|
|
||||||
key={ i }
|
|
||||||
style={ {
|
|
||||||
/* stylelint-disable */
|
|
||||||
width: Math.floor( Math.random() * ( 100 - 25 ) ) + '%',
|
|
||||||
} }
|
|
||||||
>
|
|
||||||
|
|
||||||
</li>
|
|
||||||
) );
|
|
||||||
}, [] );
|
|
||||||
|
|
||||||
if ( isPreview ) {
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<Disabled>
|
|
||||||
<CheckboxListPreview
|
|
||||||
items={ attributeOptionsPreview.map( ( term ) => {
|
|
||||||
if ( showCounts )
|
|
||||||
return `${ term.name } (${ term.count })`;
|
|
||||||
return term.name;
|
|
||||||
} ) }
|
|
||||||
/>
|
|
||||||
</Disabled>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block rendering starts.
|
setIsOptionsLoading( false );
|
||||||
|
}, [
|
||||||
|
showCounts,
|
||||||
|
attributeTerms,
|
||||||
|
filteredCounts,
|
||||||
|
sortOrder,
|
||||||
|
hideEmpty,
|
||||||
|
isTermsLoading,
|
||||||
|
isFilterCountsLoading,
|
||||||
|
] );
|
||||||
|
|
||||||
|
const { children, ...innerBlocksProps } = useInnerBlocksProps(
|
||||||
|
useBlockProps(),
|
||||||
|
{
|
||||||
|
allowedBlocks: getAllowedBlocks( EXCLUDED_BLOCKS ),
|
||||||
|
template: [
|
||||||
|
[
|
||||||
|
'core/group',
|
||||||
|
{
|
||||||
|
layout: {
|
||||||
|
type: 'flex',
|
||||||
|
flexWrap: 'nowrap',
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
name: __( 'Header', 'woocommerce' ),
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
spacing: {
|
||||||
|
blockGap: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'core/heading',
|
||||||
|
{
|
||||||
|
level: 3,
|
||||||
|
content:
|
||||||
|
attributeObject?.label ||
|
||||||
|
__( 'Attribute', 'woocommerce' ),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'woocommerce/product-filter-clear-button',
|
||||||
|
{
|
||||||
|
lock: {
|
||||||
|
remove: true,
|
||||||
|
move: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
displayStyle,
|
||||||
|
{
|
||||||
|
lock: {
|
||||||
|
remove: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
isTermsLoading || isFilterCountsLoading || isOptionsLoading;
|
||||||
|
|
||||||
if ( Object.keys( ATTRIBUTES ).length === 0 )
|
if ( Object.keys( ATTRIBUTES ).length === 0 )
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<div { ...innerBlocksProps }>
|
||||||
<NoAttributesPlaceholder />
|
<Inspector { ...props } />
|
||||||
</Wrapper>
|
<Notice>
|
||||||
|
<p>
|
||||||
|
{ __(
|
||||||
|
"Attributes are needed for filtering your products. You haven't created any attributes yet.",
|
||||||
|
'woocommerce'
|
||||||
|
) }
|
||||||
|
</p>
|
||||||
|
</Notice>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( ! attributeId || ! attributeObject )
|
if ( ! attributeId || ! attributeObject )
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<div { ...innerBlocksProps }>
|
||||||
<Notice status="warning" isDismissible={ false }>
|
<Inspector { ...props } />
|
||||||
|
<Notice>
|
||||||
<p>
|
<p>
|
||||||
{ __(
|
{ __(
|
||||||
'Please select an attribute to use this filter!',
|
'Please select an attribute to use this filter!',
|
||||||
|
@ -160,22 +214,14 @@ const Edit = ( props: EditProps ) => {
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
</Notice>
|
</Notice>
|
||||||
</Wrapper>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( isLoading )
|
if ( ! isLoading && attributeTerms.length === 0 )
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<div { ...innerBlocksProps }>
|
||||||
<ul className="is-loading wp-block-woocommerce-product-filter-attribute__loading">
|
<Inspector { ...props } />
|
||||||
{ loadingState }
|
<Notice>
|
||||||
</ul>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( attributeTerms.length === 0 )
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<Notice status="warning" isDismissible={ false }>
|
|
||||||
<p>
|
<p>
|
||||||
{ __(
|
{ __(
|
||||||
'There are no products with the selected attributes.',
|
'There are no products with the selected attributes.',
|
||||||
|
@ -183,30 +229,28 @@ const Edit = ( props: EditProps ) => {
|
||||||
) }
|
) }
|
||||||
</p>
|
</p>
|
||||||
</Notice>
|
</Notice>
|
||||||
</Wrapper>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<div { ...innerBlocksProps }>
|
||||||
<Disabled>
|
<Inspector { ...props } />
|
||||||
{ displayStyle === 'dropdown' ? (
|
<InitialDisabled>
|
||||||
<AttributeDropdown
|
<BlockContextProvider
|
||||||
label={
|
value={ {
|
||||||
attributeObject.label ||
|
filterData: {
|
||||||
__( 'attribute', 'woocommerce' )
|
items:
|
||||||
}
|
attributeOptions.length === 0 && isPreview
|
||||||
/>
|
? attributeOptionsPreview
|
||||||
) : (
|
: attributeOptions,
|
||||||
<CheckboxListPreview
|
isLoading,
|
||||||
items={ attributeOptions.map( ( term ) => {
|
},
|
||||||
if ( showCounts )
|
} }
|
||||||
return `${ term.name } (${ term.count })`;
|
>
|
||||||
return term.name;
|
{ children }
|
||||||
} ) }
|
</BlockContextProvider>
|
||||||
/>
|
</InitialDisabled>
|
||||||
) }
|
</div>
|
||||||
</Disabled>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
.wp-block-woocommerce-product-filter-attribute__loading {
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
li {
|
|
||||||
@include placeholder();
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,7 +8,7 @@ import { HTMLElementEvent } from '@woocommerce/types';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { navigate } from '../product-filter/frontend';
|
import { navigate } from '../../frontend';
|
||||||
|
|
||||||
type AttributeFilterContext = {
|
type AttributeFilterContext = {
|
||||||
attributeSlug: string;
|
attributeSlug: string;
|
||||||
|
@ -106,5 +106,12 @@ store( 'woocommerce/product-filter-attribute', {
|
||||||
|
|
||||||
navigate( getUrl( selectedTerms, attributeSlug, queryType ) );
|
navigate( getUrl( selectedTerms, attributeSlug, queryType ) );
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearFilters: () => {
|
||||||
|
const { attributeSlug, queryType } =
|
||||||
|
getContext< ActiveAttributeFilterContext >();
|
||||||
|
|
||||||
|
navigate( getUrl( [], attributeSlug, queryType ) );
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -2,18 +2,21 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
|
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
|
||||||
import { productFilterOptions } from '@woocommerce/icons';
|
import { productFilterAttribute } from '@woocommerce/icons';
|
||||||
import { getSetting } from '@woocommerce/settings';
|
import { getSetting } from '@woocommerce/settings';
|
||||||
import { registerBlockType } from '@wordpress/blocks';
|
|
||||||
import { AttributeSetting } from '@woocommerce/types';
|
import { AttributeSetting } from '@woocommerce/types';
|
||||||
|
import { registerBlockType } from '@wordpress/blocks';
|
||||||
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import metadata from './block.json';
|
import metadata from './block.json';
|
||||||
import Edit from './edit';
|
import Edit from './edit';
|
||||||
|
import Save from './save';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
|
||||||
|
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
|
||||||
if ( isExperimentalBlocksEnabled() ) {
|
if ( isExperimentalBlocksEnabled() ) {
|
||||||
const defaultAttribute = getSetting< AttributeSetting >(
|
const defaultAttribute = getSetting< AttributeSetting >(
|
||||||
'defaultProductFilterAttribute'
|
'defaultProductFilterAttribute'
|
||||||
|
@ -21,7 +24,7 @@ if ( isExperimentalBlocksEnabled() ) {
|
||||||
|
|
||||||
registerBlockType( metadata, {
|
registerBlockType( metadata, {
|
||||||
edit: Edit,
|
edit: Edit,
|
||||||
icon: productFilterOptions,
|
icon: productFilterAttribute,
|
||||||
attributes: {
|
attributes: {
|
||||||
...metadata.attributes,
|
...metadata.attributes,
|
||||||
attributeId: {
|
attributeId: {
|
||||||
|
@ -29,5 +32,25 @@ if ( isExperimentalBlocksEnabled() ) {
|
||||||
default: parseInt( defaultAttribute.attribute_id, 10 ),
|
default: parseInt( defaultAttribute.attribute_id, 10 ),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
save: Save,
|
||||||
|
variations: ATTRIBUTES.map( ( attribute, index ) => {
|
||||||
|
return {
|
||||||
|
name: `product-filter-attribute-${ attribute.attribute_name }`,
|
||||||
|
title: `${ attribute.attribute_label } (Experimental)`,
|
||||||
|
description: sprintf(
|
||||||
|
// translators: %s is the attribute label.
|
||||||
|
__(
|
||||||
|
`Enable customers to filter the product collection by selecting one or more %s attributes.`,
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
attribute.attribute_label
|
||||||
|
),
|
||||||
|
attributes: {
|
||||||
|
attributeId: parseInt( attribute.attribute_id, 10 ),
|
||||||
|
},
|
||||||
|
isActive: [ 'attributeId' ],
|
||||||
|
isDefault: index === 0,
|
||||||
|
};
|
||||||
|
} ),
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,9 @@ import { getSetting } from '@woocommerce/settings';
|
||||||
import { AttributeSetting } from '@woocommerce/types';
|
import { AttributeSetting } from '@woocommerce/types';
|
||||||
import { InspectorControls } from '@wordpress/block-editor';
|
import { InspectorControls } from '@wordpress/block-editor';
|
||||||
import { dispatch, useSelect } from '@wordpress/data';
|
import { dispatch, useSelect } from '@wordpress/data';
|
||||||
import { createInterpolateElement } from '@wordpress/element';
|
import { createInterpolateElement, useState } from '@wordpress/element';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { Block, getBlockTypes, createBlock } from '@wordpress/blocks';
|
||||||
import {
|
import {
|
||||||
ComboboxControl,
|
ComboboxControl,
|
||||||
PanelBody,
|
PanelBody,
|
||||||
|
@ -23,12 +24,15 @@ import {
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { sortOrderOptions } from '../constants';
|
import { sortOrderOptions } from './constants';
|
||||||
import { BlockAttributes, EditProps } from '../types';
|
import { BlockAttributes, EditProps } from './types';
|
||||||
import { getAttributeFromId } from '../utils';
|
import { getAttributeFromId } from './utils';
|
||||||
|
import { getInnerBlockByName } from '../../utils';
|
||||||
|
|
||||||
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
|
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
|
||||||
|
|
||||||
|
let displayStyleOptions: Block[] = [];
|
||||||
|
|
||||||
export const Inspector = ( {
|
export const Inspector = ( {
|
||||||
clientId,
|
clientId,
|
||||||
attributes,
|
attributes,
|
||||||
|
@ -43,47 +47,29 @@ export const Inspector = ( {
|
||||||
hideEmpty,
|
hideEmpty,
|
||||||
clearButton,
|
clearButton,
|
||||||
} = attributes;
|
} = attributes;
|
||||||
const { updateBlockAttributes } = dispatch( 'core/block-editor' );
|
const { updateBlockAttributes, insertBlock, replaceBlock } =
|
||||||
const { productFilterWrapperBlockId, productFilterWrapperHeadingBlockId } =
|
dispatch( 'core/block-editor' );
|
||||||
useSelect(
|
const filterBlock = useSelect(
|
||||||
( select ) => {
|
( select ) => {
|
||||||
if ( ! clientId )
|
return select( 'core/block-editor' ).getBlock( clientId );
|
||||||
return {
|
|
||||||
productFilterWrapperBlockId: undefined,
|
|
||||||
productFilterWrapperHeadingBlockId: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { getBlockParentsByBlockName, getBlock } =
|
|
||||||
select( 'core/block-editor' );
|
|
||||||
|
|
||||||
const parentBlocksByBlockName = getBlockParentsByBlockName(
|
|
||||||
clientId,
|
|
||||||
'woocommerce/product-filter'
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( parentBlocksByBlockName.length === 0 )
|
|
||||||
return {
|
|
||||||
productFilterWrapperBlockId: undefined,
|
|
||||||
productFilterWrapperHeadingBlockId: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const parentBlockId = parentBlocksByBlockName[ 0 ];
|
|
||||||
|
|
||||||
const parentBlock = getBlock( parentBlockId );
|
|
||||||
const headerGroupBlock = parentBlock?.innerBlocks.find(
|
|
||||||
( block ) => block.name === 'core/group'
|
|
||||||
);
|
|
||||||
const headingBlock = headerGroupBlock?.innerBlocks.find(
|
|
||||||
( block ) => block.name === 'core/heading'
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
productFilterWrapperBlockId: parentBlockId,
|
|
||||||
productFilterWrapperHeadingBlockId: headingBlock?.clientId,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
[ clientId ]
|
[ clientId ]
|
||||||
);
|
);
|
||||||
|
const [ displayStyleBlocksAttributes, setDisplayStyleBlocksAttributes ] =
|
||||||
|
useState< Record< string, unknown > >( {} );
|
||||||
|
|
||||||
|
const filterHeadingBlock = getInnerBlockByName(
|
||||||
|
filterBlock,
|
||||||
|
'core/heading'
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( displayStyleOptions.length === 0 ) {
|
||||||
|
displayStyleOptions = getBlockTypes().filter( ( blockType ) =>
|
||||||
|
blockType.ancestor?.includes(
|
||||||
|
'woocommerce/product-filter-attribute'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -102,17 +88,9 @@ export const Inspector = ( {
|
||||||
} );
|
} );
|
||||||
const attributeObject =
|
const attributeObject =
|
||||||
getAttributeFromId( numericId );
|
getAttributeFromId( numericId );
|
||||||
if ( productFilterWrapperBlockId ) {
|
if ( filterHeadingBlock ) {
|
||||||
updateBlockAttributes(
|
updateBlockAttributes(
|
||||||
productFilterWrapperBlockId,
|
filterHeadingBlock.clientId,
|
||||||
{
|
|
||||||
attributeId: numericId,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if ( productFilterWrapperHeadingBlockId ) {
|
|
||||||
updateBlockAttributes(
|
|
||||||
productFilterWrapperHeadingBlockId,
|
|
||||||
{
|
{
|
||||||
content:
|
content:
|
||||||
attributeObject?.label ??
|
attributeObject?.label ??
|
||||||
|
@ -188,17 +166,46 @@ export const Inspector = ( {
|
||||||
value={ displayStyle }
|
value={ displayStyle }
|
||||||
onChange={ (
|
onChange={ (
|
||||||
value: BlockAttributes[ 'displayStyle' ]
|
value: BlockAttributes[ 'displayStyle' ]
|
||||||
) => setAttributes( { displayStyle: value } ) }
|
) => {
|
||||||
|
if ( ! filterBlock ) return;
|
||||||
|
const currentStyleBlock = getInnerBlockByName(
|
||||||
|
filterBlock,
|
||||||
|
displayStyle
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( currentStyleBlock ) {
|
||||||
|
setDisplayStyleBlocksAttributes( {
|
||||||
|
...displayStyleBlocksAttributes,
|
||||||
|
[ displayStyle ]:
|
||||||
|
currentStyleBlock.attributes,
|
||||||
|
} );
|
||||||
|
replaceBlock(
|
||||||
|
currentStyleBlock.clientId,
|
||||||
|
createBlock(
|
||||||
|
value,
|
||||||
|
displayStyleBlocksAttributes[ value ] ||
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
insertBlock(
|
||||||
|
createBlock( value ),
|
||||||
|
filterBlock.innerBlocks.length,
|
||||||
|
filterBlock.clientId,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setAttributes( { displayStyle: value } );
|
||||||
|
} }
|
||||||
style={ { width: '100%' } }
|
style={ { width: '100%' } }
|
||||||
>
|
>
|
||||||
|
{ displayStyleOptions.map( ( blockType ) => (
|
||||||
<ToggleGroupControlOption
|
<ToggleGroupControlOption
|
||||||
label={ __( 'List', 'woocommerce' ) }
|
key={ blockType.name }
|
||||||
value="list"
|
label={ blockType.title }
|
||||||
/>
|
value={ blockType.name }
|
||||||
<ToggleGroupControlOption
|
|
||||||
label={ __( 'Chips', 'woocommerce' ) }
|
|
||||||
value="chips"
|
|
||||||
/>
|
/>
|
||||||
|
) ) }
|
||||||
</ToggleGroupControl>
|
</ToggleGroupControl>
|
||||||
<ToggleControl
|
<ToggleControl
|
||||||
label={ __( 'Product counts', 'woocommerce' ) }
|
label={ __( 'Product counts', 'woocommerce' ) }
|
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor';
|
||||||
|
|
||||||
|
const Save = () => {
|
||||||
|
const blockProps = useBlockProps.save();
|
||||||
|
const innerBlocksProps = useInnerBlocksProps.save( blockProps );
|
||||||
|
return <div { ...innerBlocksProps } />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Save;
|
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||||
|
"name": "woocommerce/product-filter-checkbox-list",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "List",
|
||||||
|
"description": "Display a list of filter options.",
|
||||||
|
"category": "woocommerce",
|
||||||
|
"keywords": [
|
||||||
|
"WooCommerce"
|
||||||
|
],
|
||||||
|
"textdomain": "woocommerce",
|
||||||
|
"apiVersion": 3,
|
||||||
|
"ancestor": [
|
||||||
|
"woocommerce/product-filter-attribute"
|
||||||
|
],
|
||||||
|
"supports": {
|
||||||
|
"color": {
|
||||||
|
"enableContrastChecker": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"usesContext": [
|
||||||
|
"filterData"
|
||||||
|
],
|
||||||
|
"attributes": {
|
||||||
|
"optionElementBorder": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"customOptionElementBorder": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"optionElementSelected": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"customOptionElementSelected": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"optionElement": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"customOptionElement": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,223 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { Icon } from '@wordpress/components';
|
||||||
|
import { checkMark } from '@woocommerce/icons';
|
||||||
|
import { useMemo } from '@wordpress/element';
|
||||||
|
import {
|
||||||
|
useBlockProps,
|
||||||
|
withColors,
|
||||||
|
InspectorControls,
|
||||||
|
// @ts-expect-error - no types.
|
||||||
|
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||||
|
__experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown,
|
||||||
|
// @ts-expect-error - no types.
|
||||||
|
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||||
|
__experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients,
|
||||||
|
} from '@wordpress/block-editor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './style.scss';
|
||||||
|
import './editor.scss';
|
||||||
|
import { EditProps } from './types';
|
||||||
|
|
||||||
|
const Edit = ( props: EditProps ): JSX.Element => {
|
||||||
|
const {
|
||||||
|
clientId,
|
||||||
|
context,
|
||||||
|
attributes,
|
||||||
|
setAttributes,
|
||||||
|
optionElementBorder,
|
||||||
|
setOptionElementBorder,
|
||||||
|
optionElementSelected,
|
||||||
|
setOptionElementSelected,
|
||||||
|
optionElement,
|
||||||
|
setOptionElement,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
customOptionElementBorder,
|
||||||
|
customOptionElementSelected,
|
||||||
|
customOptionElement,
|
||||||
|
} = attributes;
|
||||||
|
const { filterData } = context;
|
||||||
|
const { isLoading, items } = filterData;
|
||||||
|
|
||||||
|
const colorGradientSettings = useMultipleOriginColorsAndGradients();
|
||||||
|
const blockProps = useBlockProps( {
|
||||||
|
className: clsx( 'wc-block-product-filter-checkbox-list', {
|
||||||
|
'is-loading': isLoading,
|
||||||
|
'has-option-element-border-color':
|
||||||
|
optionElementBorder.color || customOptionElementBorder,
|
||||||
|
'has-option-element-selected-color':
|
||||||
|
optionElementSelected.color || customOptionElementSelected,
|
||||||
|
'has-option-element-color':
|
||||||
|
optionElement.color || customOptionElement,
|
||||||
|
} ),
|
||||||
|
style: {
|
||||||
|
'--wc-product-filter-checkbox-list-option-element-border':
|
||||||
|
optionElementBorder.color || customOptionElementBorder,
|
||||||
|
'--wc-product-filter-checkbox-list-option-element-selected':
|
||||||
|
optionElementSelected.color || customOptionElementSelected,
|
||||||
|
'--wc-product-filter-checkbox-list-option-element':
|
||||||
|
optionElement.color || customOptionElement,
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
const loadingState = useMemo( () => {
|
||||||
|
return [ ...Array( 5 ) ].map( ( x, i ) => (
|
||||||
|
<li
|
||||||
|
key={ i }
|
||||||
|
style={ {
|
||||||
|
/* stylelint-disable */
|
||||||
|
width: Math.floor( Math.random() * ( 100 - 25 ) ) + '%',
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
) );
|
||||||
|
}, [] );
|
||||||
|
|
||||||
|
if ( ! items ) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const threshold = 15;
|
||||||
|
const isLongList = items.length > threshold;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div { ...blockProps }>
|
||||||
|
<ul className="wc-block-product-filter-checkbox-list__list">
|
||||||
|
{ isLoading && loadingState }
|
||||||
|
{ ! isLoading &&
|
||||||
|
( isLongList
|
||||||
|
? items.slice( 0, threshold )
|
||||||
|
: items
|
||||||
|
).map( ( item, index ) => (
|
||||||
|
<li
|
||||||
|
key={ index }
|
||||||
|
className="wc-block-product-filter-checkbox-list__item"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
htmlFor={ `interactive-checkbox-${ index }` }
|
||||||
|
className=" wc-block-product-filter-checkbox-list__label"
|
||||||
|
>
|
||||||
|
<span className="wc-block-interactive-components-checkbox-list__input-wrapper">
|
||||||
|
<span className="wc-block-product-filter-checkbox-list__input-wrapper">
|
||||||
|
<input
|
||||||
|
name={ `interactive-checkbox-${ index }` }
|
||||||
|
type="checkbox"
|
||||||
|
className="wc-block-product-filter-checkbox-list__input"
|
||||||
|
defaultChecked={
|
||||||
|
!! item.selected
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
className="wc-block-product-filter-checkbox-list__mark"
|
||||||
|
icon={ checkMark }
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="wc-block-product-filter-checkbox-list__text">
|
||||||
|
{ item.label }
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
) ) }
|
||||||
|
</ul>
|
||||||
|
{ ! isLoading && isLongList && (
|
||||||
|
<span className="wc-block-product-filter-checkbox-list__show-more">
|
||||||
|
<small>{ __( 'Show more…', 'woocommerce' ) }</small>
|
||||||
|
</span>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
<InspectorControls group="color">
|
||||||
|
{ colorGradientSettings.hasColorsOrGradients && (
|
||||||
|
<ColorGradientSettingsDropdown
|
||||||
|
__experimentalIsRenderedInSidebar
|
||||||
|
settings={ [
|
||||||
|
{
|
||||||
|
label: __(
|
||||||
|
'Option Element Border',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
colorValue:
|
||||||
|
optionElementBorder.color ||
|
||||||
|
customOptionElementBorder,
|
||||||
|
isShownByDefault: true,
|
||||||
|
enableAlpha: true,
|
||||||
|
onColorChange: ( colorValue: string ) => {
|
||||||
|
setOptionElementBorder( colorValue );
|
||||||
|
setAttributes( {
|
||||||
|
customOptionElementBorder: colorValue,
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
resetAllFilter: () => {
|
||||||
|
setOptionElementBorder( '' );
|
||||||
|
setAttributes( {
|
||||||
|
customOptionElementBorder: '',
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __(
|
||||||
|
'Option Element (Selected)',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
colorValue:
|
||||||
|
optionElementSelected.color ||
|
||||||
|
customOptionElementSelected,
|
||||||
|
isShownByDefault: true,
|
||||||
|
enableAlpha: true,
|
||||||
|
onColorChange: ( colorValue: string ) => {
|
||||||
|
setOptionElementSelected( colorValue );
|
||||||
|
setAttributes( {
|
||||||
|
customOptionElementSelected: colorValue,
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
resetAllFilter: () => {
|
||||||
|
setOptionElementSelected( '' );
|
||||||
|
setAttributes( {
|
||||||
|
customOptionElementSelected: '',
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __( 'Option Element', 'woocommerce' ),
|
||||||
|
colorValue:
|
||||||
|
optionElement.color || customOptionElement,
|
||||||
|
isShownByDefault: true,
|
||||||
|
enableAlpha: true,
|
||||||
|
onColorChange: ( colorValue: string ) => {
|
||||||
|
setOptionElement( colorValue );
|
||||||
|
setAttributes( {
|
||||||
|
customOptionElement: colorValue,
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
resetAllFilter: () => {
|
||||||
|
setOptionElement( '' );
|
||||||
|
setAttributes( {
|
||||||
|
customOptionElement: '',
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] }
|
||||||
|
panelId={ clientId }
|
||||||
|
{ ...colorGradientSettings }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
</InspectorControls>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withColors( {
|
||||||
|
optionElementBorder: 'option-element-border',
|
||||||
|
optionElementSelected: 'option-element-border',
|
||||||
|
optionElement: 'option-element',
|
||||||
|
} )( Edit );
|
|
@ -0,0 +1,10 @@
|
||||||
|
.wc-block-product-filter-checkbox-list.is-loading {
|
||||||
|
.wc-block-product-filter-checkbox-list__list {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
@include placeholder();
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { getContext, store } from '@woocommerce/interactivity';
|
||||||
|
import { HTMLElementEvent } from '@woocommerce/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type CheckboxListContext = {
|
||||||
|
items: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
checked: boolean;
|
||||||
|
}[];
|
||||||
|
showAll: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
store( 'woocommerce/product-filter-checkbox-list', {
|
||||||
|
actions: {
|
||||||
|
showAllItems: () => {
|
||||||
|
const context = getContext< CheckboxListContext >();
|
||||||
|
context.showAll = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
selectCheckboxItem: ( event: HTMLElementEvent< HTMLInputElement > ) => {
|
||||||
|
const context = getContext< CheckboxListContext >();
|
||||||
|
const value = event.target.value;
|
||||||
|
|
||||||
|
context.items = context.items.map( ( item ) => {
|
||||||
|
if ( item.value.toString() === value ) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
checked: ! item.checked,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} );
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
|
||||||
|
import { productFilterOptions } from '@woocommerce/icons';
|
||||||
|
import { registerBlockType } from '@wordpress/blocks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import metadata from './block.json';
|
||||||
|
import Edit from './edit';
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
|
if ( isExperimentalBlocksEnabled() ) {
|
||||||
|
registerBlockType( metadata, {
|
||||||
|
edit: Edit,
|
||||||
|
icon: productFilterOptions,
|
||||||
|
} );
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
:where(.wc-block-product-filter-checkbox-list__list) {
|
||||||
|
list-style: none outside;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-block-product-filter-checkbox-list__item.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:where(.wc-block-product-filter-checkbox-list__label) {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-block-product-filter-checkbox-list__item .wc-block-product-filter-checkbox-list__label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(.wc-block-product-filter-checkbox-list__input-wrapper) {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-block-product-filter-checkbox-list__input-wrapper::before {
|
||||||
|
content: "";
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
background: currentColor;
|
||||||
|
opacity: 0.1;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
.has-option-element-color & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(.wc-block-product-filter-checkbox-list__input) {
|
||||||
|
appearance: none;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--wc-product-filter-checkbox-list-option-element-border, transparent);
|
||||||
|
color: inherit;
|
||||||
|
display: block;
|
||||||
|
font-size: inherit;
|
||||||
|
height: 1em;
|
||||||
|
margin: 0;
|
||||||
|
width: 1em;
|
||||||
|
background: var(--wc-product-filter-checkbox-list-option-element, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-block-product-filter-checkbox-list__input:checked + .wc-block-product-filter-checkbox-list__mark {
|
||||||
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-block-product-filter-checkbox-list__input:focus {
|
||||||
|
outline-width: 1px;
|
||||||
|
outline-color: var(--wc-product-filter-checkbox-list-option-element-border, currentColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(.wc-block-product-filter-checkbox-list__mark) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: none;
|
||||||
|
height: 1em;
|
||||||
|
left: 0;
|
||||||
|
padding: 0.2em;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 1em;
|
||||||
|
color: var(--wc-product-filter-checkbox-list-option-element-selected, currentColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(.wc-block-product-filter-checkbox-list__show-more) {
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-block-product-filter-checkbox-list__show-more.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { BlockEditProps } from '@wordpress/blocks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { FilterBlockContext } from '../../types';
|
||||||
|
|
||||||
|
export type Color = {
|
||||||
|
slug: string;
|
||||||
|
class: string;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BlockAttributes = {
|
||||||
|
className: string;
|
||||||
|
optionElementBorder: string;
|
||||||
|
customOptionElementBorder: string;
|
||||||
|
optionElementSelected: string;
|
||||||
|
customOptionElementSelected: string;
|
||||||
|
optionElement: string;
|
||||||
|
customOptionElement: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EditProps = BlockEditProps< BlockAttributes > & {
|
||||||
|
context: FilterBlockContext;
|
||||||
|
optionElementBorder: Color;
|
||||||
|
setOptionElementBorder: ( value: string ) => void;
|
||||||
|
optionElementSelected: Color;
|
||||||
|
setOptionElementSelected: ( value: string ) => void;
|
||||||
|
optionElement: Color;
|
||||||
|
setOptionElement: ( value: string ) => void;
|
||||||
|
};
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||||
|
"name": "woocommerce/product-filter-chips",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "Chips",
|
||||||
|
"description": "Display filter options as chips.",
|
||||||
|
"category": "woocommerce",
|
||||||
|
"keywords": [
|
||||||
|
"WooCommerce"
|
||||||
|
],
|
||||||
|
"textdomain": "woocommerce",
|
||||||
|
"apiVersion": 3,
|
||||||
|
"ancestor": [
|
||||||
|
"woocommerce/product-filter-attribute"
|
||||||
|
],
|
||||||
|
"supports": {},
|
||||||
|
"usesContext": [
|
||||||
|
"filterData",
|
||||||
|
"isParentSelected"
|
||||||
|
],
|
||||||
|
"attributes": {}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { useBlockProps } from '@wordpress/block-editor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
|
const Edit = () => {
|
||||||
|
return <div { ...useBlockProps() }>These are chips.</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Edit;
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
|
||||||
|
import { productFilterOptions } from '@woocommerce/icons';
|
||||||
|
import { registerBlockType } from '@wordpress/blocks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import metadata from './block.json';
|
||||||
|
import Edit from './edit';
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
|
if ( isExperimentalBlocksEnabled() ) {
|
||||||
|
registerBlockType( metadata, {
|
||||||
|
edit: Edit,
|
||||||
|
icon: productFilterOptions,
|
||||||
|
} );
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
:where(.wc-block-product-filter-chips) {
|
||||||
|
// WIP
|
||||||
|
}
|
|
@ -6,13 +6,9 @@ import { BlockEditProps } from '@wordpress/blocks';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { BLOCK_NAME_MAP } from './constants';
|
|
||||||
|
|
||||||
export type FilterType = keyof typeof BLOCK_NAME_MAP;
|
|
||||||
|
|
||||||
export type BlockAttributes = {
|
export type BlockAttributes = {
|
||||||
filterType: FilterType;
|
className: string;
|
||||||
heading: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EditProps = BlockEditProps< BlockAttributes >;
|
export type EditProps = BlockEditProps< BlockAttributes >;
|
|
@ -1,3 +1,8 @@
|
||||||
|
/**
|
||||||
|
* Logic in this file is unused and should be moved to product-fitlers block.
|
||||||
|
*
|
||||||
|
* @see https://github.com/woocommerce/woocommerce/issues/50868
|
||||||
|
*/
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
|
@ -6,7 +11,7 @@ import { store, getContext } from '@woocommerce/interactivity';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { navigate } from '../product-filter/frontend';
|
import { navigate } from '../../frontend';
|
||||||
|
|
||||||
const getQueryParams = ( e: Event ) => {
|
const getQueryParams = ( e: Event ) => {
|
||||||
const filterNavContainer = ( e.target as HTMLElement )?.closest(
|
const filterNavContainer = ( e.target as HTMLElement )?.closest(
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { debounce } from '@woocommerce/base-utils';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { navigate } from '../product-filter/frontend';
|
import { navigate } from '../../frontend';
|
||||||
import type { PriceFilterContext, PriceFilterStore } from './types';
|
import type { PriceFilterContext, PriceFilterStore } from './types';
|
||||||
|
|
||||||
const getUrl = ( context: PriceFilterContext ) => {
|
const getUrl = ( context: PriceFilterContext ) => {
|
||||||
|
|
|
@ -1,129 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { __, sprintf } from '@wordpress/i18n';
|
|
||||||
import { BlockVariation } from '@wordpress/blocks';
|
|
||||||
import {
|
|
||||||
productFilterActive,
|
|
||||||
productFilterAttribute,
|
|
||||||
productFilterPrice,
|
|
||||||
productFilterRating,
|
|
||||||
productFilterStockStatus,
|
|
||||||
} from '@woocommerce/icons';
|
|
||||||
import { getSetting } from '@woocommerce/settings';
|
|
||||||
import { AttributeSetting, objectHasProp } from '@woocommerce/types';
|
|
||||||
|
|
||||||
const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] );
|
|
||||||
|
|
||||||
const variations: BlockVariation[] = [
|
|
||||||
{
|
|
||||||
name: 'product-filter-active',
|
|
||||||
title: __( 'Active (Experimental)', 'woocommerce' ),
|
|
||||||
description: __(
|
|
||||||
'Display the currently active filters.',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
attributes: {
|
|
||||||
heading: __( 'Active filters', 'woocommerce' ),
|
|
||||||
filterType: 'active-filters',
|
|
||||||
},
|
|
||||||
icon: productFilterActive,
|
|
||||||
isDefault: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'product-filter-price',
|
|
||||||
title: __( 'Price (Experimental)', 'woocommerce' ),
|
|
||||||
description: __(
|
|
||||||
'Enable customers to filter the product collection by choosing a price range.',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
attributes: {
|
|
||||||
filterType: 'price-filter',
|
|
||||||
heading: __( 'Price', 'woocommerce' ),
|
|
||||||
},
|
|
||||||
icon: productFilterPrice,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'product-filter-stock-status',
|
|
||||||
title: __( 'Status (Experimental)', 'woocommerce' ),
|
|
||||||
description: __(
|
|
||||||
'Enable customers to filter the product collection by stock status.',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
attributes: {
|
|
||||||
filterType: 'stock-filter',
|
|
||||||
heading: __( 'Status', 'woocommerce' ),
|
|
||||||
},
|
|
||||||
icon: productFilterStockStatus,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'product-filter-rating',
|
|
||||||
title: __( 'Rating (Experimental)', 'woocommerce' ),
|
|
||||||
description: __(
|
|
||||||
'Enable customers to filter the product collection by rating.',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
attributes: {
|
|
||||||
filterType: 'rating-filter',
|
|
||||||
heading: __( 'Rating', 'woocommerce' ),
|
|
||||||
},
|
|
||||||
icon: productFilterRating,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
ATTRIBUTES.forEach( ( attribute ) => {
|
|
||||||
variations.push( {
|
|
||||||
name: `product-filter-attribute-${ attribute.attribute_name }`,
|
|
||||||
title: `${ attribute.attribute_label } (Experimental)`,
|
|
||||||
description: sprintf(
|
|
||||||
// translators: %s is the attribute label.
|
|
||||||
__(
|
|
||||||
`Enable customers to filter the product collection by selecting one or more %s attributes.`,
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
attribute.attribute_label
|
|
||||||
),
|
|
||||||
attributes: {
|
|
||||||
filterType: 'attribute-filter',
|
|
||||||
heading: attribute.attribute_label,
|
|
||||||
attributeId: parseInt( attribute.attribute_id, 10 ),
|
|
||||||
},
|
|
||||||
icon: productFilterAttribute,
|
|
||||||
// Can be `isActive: [ 'filterType', 'attributeId' ]`, but the API is available from 6.6.
|
|
||||||
isActive: ( blockAttributes, variationAttributes ) => {
|
|
||||||
return (
|
|
||||||
blockAttributes.filterType === variationAttributes.filterType &&
|
|
||||||
blockAttributes.attributeId === variationAttributes.attributeId
|
|
||||||
);
|
|
||||||
},
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
variations.push( {
|
|
||||||
name: 'product-filter-attribute',
|
|
||||||
title: __( 'Attribute (Experimental)', 'woocommerce' ),
|
|
||||||
description: __(
|
|
||||||
'Enable customers to filter the product collection by selecting one or more attributes, such as color.',
|
|
||||||
'woocommerce'
|
|
||||||
),
|
|
||||||
attributes: {
|
|
||||||
filterType: 'attribute-filter',
|
|
||||||
heading: __( 'Attribute', 'woocommerce' ),
|
|
||||||
attributeId: 0,
|
|
||||||
},
|
|
||||||
icon: productFilterAttribute,
|
|
||||||
} );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add `isActive` function to all Product Filter block variations.
|
|
||||||
* `isActive` function is used to find a variation match from a created
|
|
||||||
* Block by providing its attributes.
|
|
||||||
*/
|
|
||||||
variations.forEach( ( variation ) => {
|
|
||||||
if ( ! objectHasProp( variation, 'isActive' ) ) {
|
|
||||||
// @ts-expect-error: `isActive` is currently typed wrong in `@wordpress/blocks`.
|
|
||||||
variation.isActive = [ 'filterType' ];
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
export const blockVariations = variations;
|
|
|
@ -1,47 +0,0 @@
|
||||||
{
|
|
||||||
"name": "woocommerce/product-filter",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"title": "Product Filter (Experimental)",
|
|
||||||
"description": "A block that adds product filters to the product collection.",
|
|
||||||
"category": "woocommerce",
|
|
||||||
"keywords": [
|
|
||||||
"WooCommerce",
|
|
||||||
"Filters"
|
|
||||||
],
|
|
||||||
"textdomain": "woocommerce",
|
|
||||||
"supports": {
|
|
||||||
"html": false,
|
|
||||||
"reusable": false,
|
|
||||||
"inserter": true
|
|
||||||
},
|
|
||||||
"ancestor": [
|
|
||||||
"woocommerce/product-filters"
|
|
||||||
],
|
|
||||||
"usesContext": [
|
|
||||||
"query",
|
|
||||||
"queryId"
|
|
||||||
],
|
|
||||||
"attributes": {
|
|
||||||
"filterType": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"heading": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"isPreview": {
|
|
||||||
"type": "boolean",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"attributeId": {
|
|
||||||
"type": "number",
|
|
||||||
"default": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"example": {
|
|
||||||
"attributes": {
|
|
||||||
"isPreview": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apiVersion": 3,
|
|
||||||
"$schema": "https://schemas.wp.org/trunk/block.json"
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { getSetting } from '@woocommerce/settings';
|
|
||||||
import { Notice } from '@wordpress/components';
|
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
|
|
||||||
const Warning = () => {
|
|
||||||
const isWidgetEditor = getSetting< boolean >( 'isWidgetEditor' );
|
|
||||||
if ( isWidgetEditor ) {
|
|
||||||
return (
|
|
||||||
<Notice status="info" isDismissible={ false }>
|
|
||||||
{ __(
|
|
||||||
'The widget area containing Collection Filters block needs to be placed on a product archive page for filters to function properly.',
|
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
</Notice>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSiteEditor = getSetting< boolean >( 'isSiteEditor' );
|
|
||||||
if ( ! isSiteEditor ) {
|
|
||||||
return (
|
|
||||||
<Notice status="warning" isDismissible={ false }>
|
|
||||||
{ __(
|
|
||||||
'When added to a post or page, Collection Filters block needs to be nested inside a Product Collection block to function properly.',
|
|
||||||
'woocommerce'
|
|
||||||
) }
|
|
||||||
</Notice>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Warning;
|
|
|
@ -1,8 +0,0 @@
|
||||||
export const BLOCK_NAME_MAP = {
|
|
||||||
'active-filters': 'woocommerce/product-filter-active',
|
|
||||||
'price-filter': 'woocommerce/product-filter-price',
|
|
||||||
'stock-filter': 'woocommerce/product-filter-stock-status',
|
|
||||||
'rating-filter': 'woocommerce/product-filter-rating',
|
|
||||||
'attribute-filter': 'woocommerce/product-filter-attribute',
|
|
||||||
'clear-button': 'woocommerce/product-filter-clear-button',
|
|
||||||
};
|
|
|
@ -1,113 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
|
|
||||||
import { BlockEditProps } from '@wordpress/blocks';
|
|
||||||
import { useSelect } from '@wordpress/data';
|
|
||||||
import { __ } from '@wordpress/i18n';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal dependencies
|
|
||||||
*/
|
|
||||||
import Warning from './components/warning';
|
|
||||||
import './editor.scss';
|
|
||||||
import { getAllowedBlocks } from './utils';
|
|
||||||
import { BLOCK_NAME_MAP } from './constants';
|
|
||||||
import type { FilterType } from './types';
|
|
||||||
|
|
||||||
const Edit = ( {
|
|
||||||
attributes,
|
|
||||||
clientId,
|
|
||||||
}: BlockEditProps< {
|
|
||||||
heading: string;
|
|
||||||
filterType: FilterType;
|
|
||||||
isPreview: boolean;
|
|
||||||
attributeId: number | undefined;
|
|
||||||
} > ) => {
|
|
||||||
const blockProps = useBlockProps();
|
|
||||||
|
|
||||||
const isNested = useSelect( ( select ) => {
|
|
||||||
const { getBlockParentsByBlockName } = select( 'core/block-editor' );
|
|
||||||
return !! getBlockParentsByBlockName(
|
|
||||||
clientId,
|
|
||||||
'woocommerce/product-collection'
|
|
||||||
).length;
|
|
||||||
} );
|
|
||||||
|
|
||||||
return (
|
|
||||||
<nav { ...blockProps }>
|
|
||||||
{ ! isNested && <Warning /> }
|
|
||||||
<InnerBlocks
|
|
||||||
allowedBlocks={ getAllowedBlocks( [
|
|
||||||
...Object.values( BLOCK_NAME_MAP ),
|
|
||||||
'woocommerce/product-filter',
|
|
||||||
'woocommerce/filter-wrapper',
|
|
||||||
'woocommerce/product-collection',
|
|
||||||
'core/query',
|
|
||||||
] ) }
|
|
||||||
template={ [
|
|
||||||
/**
|
|
||||||
* We want to hide the clear filter button for active filters block
|
|
||||||
* as it has its own "clear all" button.
|
|
||||||
*/
|
|
||||||
attributes.filterType === 'active-filters'
|
|
||||||
? [
|
|
||||||
'core/heading',
|
|
||||||
{ level: 3, content: attributes.heading || '' },
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
'core/group',
|
|
||||||
{
|
|
||||||
layout: {
|
|
||||||
type: 'flex',
|
|
||||||
flexWrap: 'nowrap',
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
name: __( 'Header', 'woocommerce' ),
|
|
||||||
},
|
|
||||||
style: {
|
|
||||||
spacing: {
|
|
||||||
blockGap: '0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[
|
|
||||||
[
|
|
||||||
'core/heading',
|
|
||||||
{
|
|
||||||
level: 3,
|
|
||||||
content: attributes.heading || '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'woocommerce/product-filter-clear-button',
|
|
||||||
{
|
|
||||||
lock: {
|
|
||||||
remove: true,
|
|
||||||
move: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
BLOCK_NAME_MAP[ attributes.filterType ],
|
|
||||||
{
|
|
||||||
lock: {
|
|
||||||
remove: true,
|
|
||||||
},
|
|
||||||
isPreview: attributes.isPreview,
|
|
||||||
attributeId:
|
|
||||||
attributes.filterType === 'attribute-filter' &&
|
|
||||||
attributes.attributeId
|
|
||||||
? attributes.attributeId
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
] }
|
|
||||||
/>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Edit;
|
|
|
@ -1,5 +0,0 @@
|
||||||
.wp-block-woocommerce-collection-filters {
|
|
||||||
.components-notice {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { navigate as navigateFn } from '@woocommerce/interactivity';
|
|
||||||
import { getSetting } from '@woocommerce/settings';
|
|
||||||
|
|
||||||
const isBlockTheme = getSetting< boolean >( 'isBlockTheme' );
|
|
||||||
const isProductArchive = getSetting< boolean >( 'isProductArchive' );
|
|
||||||
const needsRefresh = getSetting< boolean >(
|
|
||||||
'needsRefreshForInteractivityAPI',
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
export function navigate( href: string, options = {} ) {
|
|
||||||
/**
|
|
||||||
* We may need to reset the current page when changing filters.
|
|
||||||
* This is because the current page may not exist for this set
|
|
||||||
* of filters and will 404 when the user navigates to it.
|
|
||||||
*
|
|
||||||
* There are different pagination formats to consider, as documented here:
|
|
||||||
* https://github.com/WordPress/gutenberg/blob/317eb8f14c8e1b81bf56972cca2694be250580e3/packages/block-library/src/query-pagination-numbers/index.php#L22-L85
|
|
||||||
*/
|
|
||||||
const url = new URL( href );
|
|
||||||
// When pretty permalinks are enabled, the page number may be in the path name.
|
|
||||||
url.pathname = url.pathname.replace( /\/page\/[0-9]+/i, '' );
|
|
||||||
// When plain permalinks are enabled, the page number may be in the "paged" query parameter.
|
|
||||||
url.searchParams.delete( 'paged' );
|
|
||||||
// On posts and pages the page number will be in a query parameter that
|
|
||||||
// identifies which block we are paginating.
|
|
||||||
url.searchParams.forEach( ( _, key ) => {
|
|
||||||
if ( key.match( /^query(?:-[0-9]+)?-page$/ ) ) {
|
|
||||||
url.searchParams.delete( key );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
// Make sure to update the href with the changes.
|
|
||||||
href = url.href;
|
|
||||||
|
|
||||||
if ( needsRefresh || ( ! isBlockTheme && isProductArchive ) ) {
|
|
||||||
return ( window.location.href = href );
|
|
||||||
}
|
|
||||||
return navigateFn( href, options );
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
BlockInstance,
|
|
||||||
createBlock,
|
|
||||||
registerBlockType,
|
|
||||||
} from '@wordpress/blocks';
|
|
||||||
import { Icon, more } from '@wordpress/icons';
|
|
||||||
import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings';
|
|
||||||
/**
|
|
||||||
* Internal dependencies
|
|
||||||
*/
|
|
||||||
import metadata from './block.json';
|
|
||||||
import edit from './edit';
|
|
||||||
import save from './save';
|
|
||||||
import { BLOCK_NAME_MAP } from './constants';
|
|
||||||
import { BlockAttributes } from './types';
|
|
||||||
import { blockVariations } from './block-variations';
|
|
||||||
|
|
||||||
if ( isExperimentalBlocksEnabled() ) {
|
|
||||||
registerBlockType( metadata, {
|
|
||||||
icon: {
|
|
||||||
src: (
|
|
||||||
<Icon
|
|
||||||
icon={ more }
|
|
||||||
className="wc-block-editor-components-block-icon"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
edit,
|
|
||||||
save,
|
|
||||||
variations: blockVariations,
|
|
||||||
transforms: {
|
|
||||||
from: [
|
|
||||||
{
|
|
||||||
type: 'block',
|
|
||||||
blocks: [ 'woocommerce/filter-wrapper' ],
|
|
||||||
transform: (
|
|
||||||
attributes: BlockAttributes,
|
|
||||||
innerBlocks: BlockInstance[]
|
|
||||||
) => {
|
|
||||||
const newInnerBlocks: BlockInstance[] = [];
|
|
||||||
// Loop through inner blocks to preserve the block order.
|
|
||||||
innerBlocks.forEach( ( block ) => {
|
|
||||||
if (
|
|
||||||
block.name ===
|
|
||||||
`woocommerce/${ attributes.filterType }`
|
|
||||||
) {
|
|
||||||
newInnerBlocks.push(
|
|
||||||
createBlock(
|
|
||||||
BLOCK_NAME_MAP[ attributes.filterType ],
|
|
||||||
block.attributes
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( block.name === 'core/heading' ) {
|
|
||||||
newInnerBlocks.push( block );
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
return createBlock(
|
|
||||||
'woocommerce/product-filter',
|
|
||||||
attributes,
|
|
||||||
newInnerBlocks
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
} );
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { InnerBlocks } from '@wordpress/block-editor';
|
|
||||||
|
|
||||||
export default function save() {
|
|
||||||
return <InnerBlocks.Content />;
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { getBlockTypes } from '@wordpress/blocks';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of allowed block names excluding the disallowedBlocks array.
|
|
||||||
*
|
|
||||||
* @param disallowedBlocks Array of block names to disallow.
|
|
||||||
* @return Array of allowed block names.
|
|
||||||
*/
|
|
||||||
export const getAllowedBlocks = ( disallowedBlocks: string[] ) => {
|
|
||||||
const allBlocks = getBlockTypes();
|
|
||||||
|
|
||||||
return allBlocks
|
|
||||||
.map( ( block ) => block.name )
|
|
||||||
.filter( ( name ) => ! disallowedBlocks.includes( name ) );
|
|
||||||
};
|
|
|
@ -8,7 +8,7 @@ import { DropdownContext } from '@woocommerce/interactivity-components/dropdown'
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { navigate } from '../product-filter/frontend';
|
import { navigate } from '../../frontend';
|
||||||
|
|
||||||
function getUrl( filters: Array< string | null > ) {
|
function getUrl( filters: Array< string | null > ) {
|
||||||
filters = filters.filter( Boolean );
|
filters = filters.filter( Boolean );
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { CheckboxListContext } from '@woocommerce/interactivity-components/check
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { navigate } from '../product-filter/frontend';
|
import { navigate } from '../../frontend';
|
||||||
|
|
||||||
const getUrl = ( activeFilters: string ) => {
|
const getUrl = ( activeFilters: string ) => {
|
||||||
const url = new URL( window.location.href );
|
const url = new URL( window.location.href );
|
||||||
|
|
|
@ -7,8 +7,8 @@ export type BlockOverlayAttributeOptions =
|
||||||
( typeof BlockOverlayAttribute )[ keyof typeof BlockOverlayAttribute ];
|
( typeof BlockOverlayAttribute )[ keyof typeof BlockOverlayAttribute ];
|
||||||
|
|
||||||
export interface BlockAttributes {
|
export interface BlockAttributes {
|
||||||
productId?: string;
|
|
||||||
setAttributes: ( attributes: ProductFiltersBlockAttributes ) => void;
|
setAttributes: ( attributes: ProductFiltersBlockAttributes ) => void;
|
||||||
|
productId?: string;
|
||||||
overlay: BlockOverlayAttributeOptions;
|
overlay: BlockOverlayAttributeOptions;
|
||||||
overlayIcon:
|
overlayIcon:
|
||||||
| 'filter-icon-1'
|
| 'filter-icon-1'
|
||||||
|
@ -18,3 +18,23 @@ export interface BlockAttributes {
|
||||||
overlayButtonStyle: 'label-icon' | 'label' | 'icon';
|
overlayButtonStyle: 'label-icon' | 'label' | 'icon';
|
||||||
overlayIconSize?: number;
|
overlayIconSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FilterOptionItem = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
selected?: boolean;
|
||||||
|
rawData?: Record< string, unknown >;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FilterBlockContext = {
|
||||||
|
filterData: {
|
||||||
|
isLoading: boolean;
|
||||||
|
items?: FilterOptionItem[];
|
||||||
|
range?: {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
isParentSelected: boolean;
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { BlockInstance, getBlockTypes } from '@wordpress/blocks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of allowed block names excluding the disallowedBlocks array.
|
||||||
|
*
|
||||||
|
* @param disallowedBlocks Array of block names to disallow.
|
||||||
|
* @return Array of allowed block names.
|
||||||
|
*/
|
||||||
|
export const getAllowedBlocks = ( disallowedBlocks: string[] ) => {
|
||||||
|
const allBlocks = getBlockTypes();
|
||||||
|
|
||||||
|
return allBlocks
|
||||||
|
.map( ( block ) => block.name )
|
||||||
|
.filter( ( name ) => ! disallowedBlocks.includes( name ) );
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getInnerBlockByName = (
|
||||||
|
block: BlockInstance | null,
|
||||||
|
name: string
|
||||||
|
): BlockInstance | null => {
|
||||||
|
if ( ! block ) return null;
|
||||||
|
|
||||||
|
if ( block.innerBlocks.length === 0 ) return null;
|
||||||
|
|
||||||
|
for ( const innerBlock of block.innerBlocks ) {
|
||||||
|
if ( innerBlock.name === name ) return innerBlock;
|
||||||
|
const innerInnerBlock = getInnerBlockByName( innerBlock, name );
|
||||||
|
if ( innerInnerBlock ) return innerInnerBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
|
@ -4,6 +4,7 @@ export { default as bagAlt } from './library/bag-alt';
|
||||||
export { default as barcode } from './library/barcode';
|
export { default as barcode } from './library/barcode';
|
||||||
export { default as cart } from './library/cart';
|
export { default as cart } from './library/cart';
|
||||||
export { default as cartOutline } from './library/cart-outline';
|
export { default as cartOutline } from './library/cart-outline';
|
||||||
|
export { default as checkMark } from './library/check-mark';
|
||||||
export { default as checkPayment } from './library/check-payment';
|
export { default as checkPayment } from './library/check-payment';
|
||||||
export { default as closeSquareShadow } from './library/close-square-shadow';
|
export { default as closeSquareShadow } from './library/close-square-shadow';
|
||||||
export { default as customerAccount } from './library/customer-account';
|
export { default as customerAccount } from './library/customer-account';
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { IconProps } from '@wordpress/icons/build-types/icon';
|
||||||
|
import { Path, SVG } from '@wordpress/primitives';
|
||||||
|
|
||||||
|
const CheckMark = ( props: IconProps ) => (
|
||||||
|
<SVG
|
||||||
|
viewBox="0 0 10 8"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{ ...props }
|
||||||
|
>
|
||||||
|
<Path
|
||||||
|
d="M9.25 1.19922L3.75 6.69922L1 3.94922"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</SVG>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default CheckMark;
|
|
@ -88,10 +88,6 @@ const blocks = {
|
||||||
'product-filters': {
|
'product-filters': {
|
||||||
isExperimental: true,
|
isExperimental: true,
|
||||||
},
|
},
|
||||||
'product-filter': {
|
|
||||||
isExperimental: true,
|
|
||||||
customDir: 'product-filters/inner-blocks/product-filter',
|
|
||||||
},
|
|
||||||
'product-filters-overlay': {
|
'product-filters-overlay': {
|
||||||
isExperimental: true,
|
isExperimental: true,
|
||||||
customDir: 'product-filters/inner-blocks/overlay',
|
customDir: 'product-filters/inner-blocks/overlay',
|
||||||
|
@ -124,6 +120,14 @@ const blocks = {
|
||||||
customDir: 'product-filters/inner-blocks/clear-button',
|
customDir: 'product-filters/inner-blocks/clear-button',
|
||||||
isExperimental: true,
|
isExperimental: true,
|
||||||
},
|
},
|
||||||
|
'product-filter-checkbox-list': {
|
||||||
|
customDir: 'product-filters/inner-blocks/checkbox-list',
|
||||||
|
isExperimental: true,
|
||||||
|
},
|
||||||
|
'product-filter-chips': {
|
||||||
|
customDir: 'product-filters/inner-blocks/chips',
|
||||||
|
isExperimental: true,
|
||||||
|
},
|
||||||
'order-confirmation-summary': {
|
'order-confirmation-summary': {
|
||||||
customDir: 'order-confirmation/summary',
|
customDir: 'order-confirmation/summary',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
<!-- wp:woocommerce/product-filter {"filterType":"active-filters","heading":"Active filters"} -->
|
<!-- wp:woocommerce/product-filters -->
|
||||||
<!-- wp:heading {"level":3} -->
|
<div class="wp-block-woocommerce-product-filters wc-block-product-filters">
|
||||||
<h3 class='wp-block-heading'>Active filters</h3>
|
|
||||||
<!-- /wp:heading -->
|
|
||||||
|
|
||||||
{{#> wp-block blockName='woocommerce/product-filter-active' attributes=attributes }}
|
{{#> wp-block blockName='woocommerce/product-filter-active' attributes=attributes }}
|
||||||
{{/ wp-block }}
|
{{/ wp-block }}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- /wp:woocommerce/product-filters -->
|
||||||
|
|
||||||
<!-- wp:woocommerce/product-collection {"query":{"perPage":9,"pages":0,"offset":0,"postType":"product","order":"asc","orderBy":"title","search":"","exclude":[],"inherit":true,"taxQuery":[],"isProductCollectionBlock":true,"featured":false,"woocommerceOnSale":false,"woocommerceStockStatus":["instock","outofstock","onbackorder"],"woocommerceAttributes":[],"woocommerceHandPickedProducts":[]},"tagName":"div","displayLayout":{"type":"flex","columns":3,"shrinkColumns":true},"queryContextIncludes":["collection"]} -->
|
<!-- wp:woocommerce/product-collection {"query":{"perPage":9,"pages":0,"offset":0,"postType":"product","order":"asc","orderBy":"title","search":"","exclude":[],"inherit":true,"taxQuery":[],"isProductCollectionBlock":true,"featured":false,"woocommerceOnSale":false,"woocommerceStockStatus":["instock","outofstock","onbackorder"],"woocommerceAttributes":[],"woocommerceHandPickedProducts":[]},"tagName":"div","displayLayout":{"type":"flex","columns":3,"shrinkColumns":true},"queryContextIncludes":["collection"]} -->
|
||||||
<div class='wp-block-woocommerce-product-collection'>
|
<div class='wp-block-woocommerce-product-collection'>
|
||||||
<!-- wp:woocommerce/product-template -->
|
<!-- wp:woocommerce/product-template -->
|
||||||
|
|
|
@ -1,18 +1,26 @@
|
||||||
<!-- wp:woocommerce/product-filter {"filterType":"attribute-filter","heading":"Filter by Attribute"} -->
|
<!-- wp:woocommerce/product-filters -->
|
||||||
<!-- wp:heading {"level":3} -->
|
<div class="wp-block-woocommerce-product-filters wc-block-product-filters">
|
||||||
<h3 class="wp-block-heading">Filter by Attribute</h3>
|
{{#> wp-block blockName='woocommerce/product-filter-attribute' attributes=attributes }}
|
||||||
<!-- wp:woocommerce/product-filter-clear-button {"lock":{"remove":true,"move":false}} -->
|
<div class="wp-block-woocommerce-product-filter-attribute"><!-- wp:group {"metadata":{"name":"Header"},"style":{"spacing":{"blockGap":"0"}},"layout":{"type":"flex","flexWrap":"nowrap"}} -->
|
||||||
|
<div class="wp-block-group"><!-- wp:heading {"level":3} -->
|
||||||
|
<h3 class="wp-block-heading">Attribute</h3>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
|
<!-- wp:woocommerce/product-filter-clear-button {"lock":{"remove":true}} -->
|
||||||
<!-- wp:buttons {"layout":{"type":"flex"}} -->
|
<!-- wp:buttons {"layout":{"type":"flex"}} -->
|
||||||
<div class="wp-block-buttons"><!-- wp:button {"style":{"border":{"width":"0px","style":"none"},"typography":{"textDecoration":"underline"},"outline":"none","fontSize":"medium"},"className":"wc-block-product-filter-clear-button is-style-outline"} -->
|
<div class="wp-block-buttons"><!-- wp:button {"className":"wc-block-product-filter-clear-button is-style-outline","style":{"border":{"width":"0px","style":"none"},"typography":{"textDecoration":"underline"},"outline":"none","fontSize":"medium"}} -->
|
||||||
<div class="wp-block-button wc-block-product-filter-clear-button is-style-outline" style="text-decoration:underline"><a class="wp-block-button__link wp-element-button" style="border-style:none;border-width:0px">Clear</a></div>
|
<div class="wp-block-button wc-block-product-filter-clear-button is-style-outline" style="text-decoration:underline"><a class="wp-block-button__link wp-element-button" style="border-style:none;border-width:0px">Clear</a></div>
|
||||||
<!-- /wp:button --></div>
|
<!-- /wp:button --></div>
|
||||||
<!-- /wp:buttons -->
|
<!-- /wp:buttons -->
|
||||||
<!-- /wp:woocommerce/product-filter-clear-button -->
|
<!-- /wp:woocommerce/product-filter-clear-button --></div>
|
||||||
<!-- /wp:heading -->
|
<!-- /wp:group -->
|
||||||
|
|
||||||
{{#> wp-block blockName='woocommerce/product-filter-attribute' attributes=attributes }}
|
<!-- wp:woocommerce/product-filter-checkbox-list {"lock":{"remove":true},"className":"wp-block-woocommerce-product-filter-checkbox-list"} /--></div>
|
||||||
{{/ wp-block }}
|
{{/ wp-block }}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- /wp:woocommerce/product-filters -->
|
||||||
|
|
||||||
<!-- wp:woocommerce/product-collection {"query":{"perPage":9,"pages":0,"offset":0,"postType":"product","order":"asc","orderBy":"title","search":"","exclude":[],"inherit":true,"taxQuery":[],"isProductCollectionBlock":true,"featured":false,"woocommerceOnSale":false,"woocommerceStockStatus":["instock","outofstock","onbackorder"],"woocommerceAttributes":[],"woocommerceHandPickedProducts":[]},"tagName":"div","displayLayout":{"type":"flex","columns":3,"shrinkColumns":true},"queryContextIncludes":["collection"]} -->
|
<!-- wp:woocommerce/product-collection {"query":{"perPage":9,"pages":0,"offset":0,"postType":"product","order":"asc","orderBy":"title","search":"","exclude":[],"inherit":true,"taxQuery":[],"isProductCollectionBlock":true,"featured":false,"woocommerceOnSale":false,"woocommerceStockStatus":["instock","outofstock","onbackorder"],"woocommerceAttributes":[],"woocommerceHandPickedProducts":[]},"tagName":"div","displayLayout":{"type":"flex","columns":3,"shrinkColumns":true},"queryContextIncludes":["collection"]} -->
|
||||||
<div class="wp-block-woocommerce-product-collection">
|
<div class="wp-block-woocommerce-product-collection">
|
||||||
<!-- wp:woocommerce/product-template -->
|
<!-- wp:woocommerce/product-template -->
|
||||||
|
|
|
@ -1,99 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { TemplateCompiler, test as base, expect } from '@woocommerce/e2e-utils';
|
|
||||||
|
|
||||||
const test = base.extend< { templateCompiler: TemplateCompiler } >( {
|
|
||||||
templateCompiler: async ( { requestUtils }, use ) => {
|
|
||||||
const compiler = await requestUtils.createTemplateFromFile(
|
|
||||||
'archive-product_active-filters'
|
|
||||||
);
|
|
||||||
await use( compiler );
|
|
||||||
},
|
|
||||||
} );
|
|
||||||
|
|
||||||
test.describe( 'Product Filter: Active Filters Block', () => {
|
|
||||||
test.describe( 'frontend', () => {
|
|
||||||
test.beforeEach( async ( { requestUtils } ) => {
|
|
||||||
await requestUtils.activatePlugin(
|
|
||||||
'woocommerce-blocks-test-enable-experimental-features'
|
|
||||||
);
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'Without any filters selected, only a wrapper block is rendered', async ( {
|
|
||||||
page,
|
|
||||||
templateCompiler,
|
|
||||||
} ) => {
|
|
||||||
await templateCompiler.compile();
|
|
||||||
|
|
||||||
await page.goto( '/shop' );
|
|
||||||
|
|
||||||
const locator = page.locator(
|
|
||||||
'.wp-block-woocommerce-product-filter'
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect( locator ).toHaveCount( 1 );
|
|
||||||
|
|
||||||
const html = await locator.innerHTML();
|
|
||||||
expect( html.trim() ).toBe( '' );
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'With rating filters applied it shows the correct active filters', async ( {
|
|
||||||
page,
|
|
||||||
templateCompiler,
|
|
||||||
} ) => {
|
|
||||||
await templateCompiler.compile();
|
|
||||||
|
|
||||||
await page.goto( `${ '/shop' }?rating_filter=1,2,5` );
|
|
||||||
|
|
||||||
await expect( page.getByText( 'Rating:' ) ).toBeVisible();
|
|
||||||
await expect( page.getByText( 'Rated 1 out of 5' ) ).toBeVisible();
|
|
||||||
await expect( page.getByText( 'Rated 2 out of 5' ) ).toBeVisible();
|
|
||||||
await expect( page.getByText( 'Rated 5 out of 5' ) ).toBeVisible();
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'With stock filters applied it shows the correct active filters', async ( {
|
|
||||||
page,
|
|
||||||
templateCompiler,
|
|
||||||
} ) => {
|
|
||||||
await templateCompiler.compile();
|
|
||||||
|
|
||||||
await page.goto(
|
|
||||||
`${ '/shop' }?filter_stock_status=instock,onbackorder`
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect( page.getByText( 'Stock Status:' ) ).toBeVisible();
|
|
||||||
await expect( page.getByText( 'In stock' ) ).toBeVisible();
|
|
||||||
await expect( page.getByText( 'On backorder' ) ).toBeVisible();
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'With attribute filters applied it shows the correct active filters', async ( {
|
|
||||||
page,
|
|
||||||
templateCompiler,
|
|
||||||
} ) => {
|
|
||||||
await templateCompiler.compile();
|
|
||||||
|
|
||||||
await page.goto(
|
|
||||||
`${ '/shop' }?filter_color=blue,gray&query_type_color=or`
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect( page.getByText( 'Color:' ) ).toBeVisible();
|
|
||||||
await expect( page.getByText( 'Blue' ) ).toBeVisible();
|
|
||||||
await expect( page.getByText( 'Gray' ) ).toBeVisible();
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'With price filters applied it shows the correct active filters', async ( {
|
|
||||||
page,
|
|
||||||
templateCompiler,
|
|
||||||
} ) => {
|
|
||||||
await templateCompiler.compile();
|
|
||||||
|
|
||||||
await page.goto( `${ '/shop' }?min_price=17&max_price=71` );
|
|
||||||
|
|
||||||
await expect( page.getByText( 'Price:' ) ).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
page.getByText( 'Between $17 and $71' )
|
|
||||||
).toBeVisible();
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
} );
|
|
|
@ -1,316 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { TemplateCompiler, test as base, expect } from '@woocommerce/e2e-utils';
|
|
||||||
|
|
||||||
const COLOR_ATTRIBUTE_VALUES = [ 'Blue', 'Gray', 'Green', 'Red', 'Yellow' ];
|
|
||||||
const COLOR_ATTRIBUTES_WITH_COUNTS = [
|
|
||||||
'Blue (4)',
|
|
||||||
'Gray (2)',
|
|
||||||
'Green (3)',
|
|
||||||
'Red (4)',
|
|
||||||
'Yellow (1)',
|
|
||||||
];
|
|
||||||
|
|
||||||
const test = base.extend< { templateCompiler: TemplateCompiler } >( {
|
|
||||||
templateCompiler: async ( { requestUtils }, use ) => {
|
|
||||||
const compiler = await requestUtils.createTemplateFromFile(
|
|
||||||
'archive-product_attribute-filter'
|
|
||||||
);
|
|
||||||
await use( compiler );
|
|
||||||
},
|
|
||||||
} );
|
|
||||||
|
|
||||||
test.describe( 'Product Filter: Attribute Block', () => {
|
|
||||||
test.describe( 'With default display style', () => {
|
|
||||||
test.beforeEach( async ( { requestUtils, templateCompiler } ) => {
|
|
||||||
await requestUtils.activatePlugin(
|
|
||||||
'woocommerce-blocks-test-enable-experimental-features'
|
|
||||||
);
|
|
||||||
await templateCompiler.compile( {
|
|
||||||
attributes: {
|
|
||||||
attributeId: 1,
|
|
||||||
},
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'clear button is not shown on initial page load', async ( {
|
|
||||||
page,
|
|
||||||
} ) => {
|
|
||||||
await page.goto( '/shop' );
|
|
||||||
|
|
||||||
const button = page.getByRole( 'button', { name: 'Clear' } );
|
|
||||||
|
|
||||||
await expect( button ).toBeHidden();
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'renders a checkbox list with the available attribute filters', async ( {
|
|
||||||
page,
|
|
||||||
} ) => {
|
|
||||||
await page.goto( '/shop' );
|
|
||||||
|
|
||||||
const attributes = page.locator(
|
|
||||||
'.wc-block-interactivity-components-checkbox-list__label'
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect( attributes ).toHaveCount( 5 );
|
|
||||||
|
|
||||||
for ( let i = 0; i < COLOR_ATTRIBUTE_VALUES.length; i++ ) {
|
|
||||||
await expect( attributes.nth( i ) ).toHaveText(
|
|
||||||
COLOR_ATTRIBUTE_VALUES[ i ]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'filters the list of products by selecting an attribute', async ( {
|
|
||||||
page,
|
|
||||||
} ) => {
|
|
||||||
await page.goto( '/shop' );
|
|
||||||
|
|
||||||
const grayCheckbox = page.getByText( 'Gray' );
|
|
||||||
await grayCheckbox.click();
|
|
||||||
|
|
||||||
// wait for navigation
|
|
||||||
await page.waitForURL( /.*filter_color=gray.*/ );
|
|
||||||
|
|
||||||
const products = page.locator( '.wc-block-product' );
|
|
||||||
|
|
||||||
await expect( products ).toHaveCount( 2 );
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'clear button appears after a filter is applied', async ( {
|
|
||||||
page,
|
|
||||||
} ) => {
|
|
||||||
await page.goto( '/shop' );
|
|
||||||
|
|
||||||
const grayCheckbox = page.getByText( 'Gray' );
|
|
||||||
await grayCheckbox.click();
|
|
||||||
|
|
||||||
// wait for navigation
|
|
||||||
await page.waitForURL( /.*filter_color=gray.*/ );
|
|
||||||
|
|
||||||
const button = page.getByRole( 'button', { name: 'Clear' } );
|
|
||||||
|
|
||||||
await expect( button ).toBeVisible();
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'clear button hides after deselecting all filters', async ( {
|
|
||||||
page,
|
|
||||||
} ) => {
|
|
||||||
await page.goto( '/shop' );
|
|
||||||
|
|
||||||
const grayCheckbox = page.getByText( 'Gray' );
|
|
||||||
await grayCheckbox.click();
|
|
||||||
|
|
||||||
// wait for navigation
|
|
||||||
await page.waitForURL( /.*filter_color=gray.*/ );
|
|
||||||
|
|
||||||
await grayCheckbox.click();
|
|
||||||
|
|
||||||
const button = page.getByRole( 'button', { name: 'Clear' } );
|
|
||||||
|
|
||||||
await expect( button ).toBeHidden();
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'filters are cleared after clear button is clicked', async ( {
|
|
||||||
page,
|
|
||||||
} ) => {
|
|
||||||
await page.goto( '/shop' );
|
|
||||||
|
|
||||||
const grayCheckbox = page.getByText( 'Gray' );
|
|
||||||
await grayCheckbox.click();
|
|
||||||
|
|
||||||
// wait for navigation
|
|
||||||
await page.waitForURL( /.*filter_color=gray.*/ );
|
|
||||||
|
|
||||||
const button = page.getByRole( 'button', { name: 'Clear' } );
|
|
||||||
|
|
||||||
await button.click();
|
|
||||||
|
|
||||||
COLOR_ATTRIBUTE_VALUES.map( async ( color ) => {
|
|
||||||
const element = page.locator(
|
|
||||||
`input[value="${ color.toLowerCase() }"]`
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect( element ).not.toBeChecked();
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
test.describe( 'With show counts enabled', () => {
|
|
||||||
test.beforeEach( async ( { requestUtils, templateCompiler } ) => {
|
|
||||||
await requestUtils.activatePlugin(
|
|
||||||
'woocommerce-blocks-test-enable-experimental-features'
|
|
||||||
);
|
|
||||||
await templateCompiler.compile( {
|
|
||||||
attributes: {
|
|
||||||
attributeId: 1,
|
|
||||||
showCounts: true,
|
|
||||||
},
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'Renders checkboxes with associated product counts', async ( {
|
|
||||||
page,
|
|
||||||
} ) => {
|
|
||||||
await page.goto( '/shop' );
|
|
||||||
|
|
||||||
const attributes = page.locator(
|
|
||||||
'.wc-block-interactivity-components-checkbox-list__label'
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect( attributes ).toHaveCount( 5 );
|
|
||||||
|
|
||||||
for ( let i = 0; i < COLOR_ATTRIBUTES_WITH_COUNTS.length; i++ ) {
|
|
||||||
await expect( attributes.nth( i ) ).toHaveText(
|
|
||||||
COLOR_ATTRIBUTES_WITH_COUNTS[ i ]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
test.describe( "With display style 'dropdown'", () => {
|
|
||||||
test.beforeEach( async ( { requestUtils, templateCompiler } ) => {
|
|
||||||
await requestUtils.activatePlugin(
|
|
||||||
'woocommerce-blocks-test-enable-experimental-features'
|
|
||||||
);
|
|
||||||
await templateCompiler.compile( {
|
|
||||||
attributes: {
|
|
||||||
attributeId: 1,
|
|
||||||
displayStyle: 'dropdown',
|
|
||||||
},
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'clear button is not shown on initial page load', async ( {
|
|
||||||
page,
|
|
||||||
} ) => {
|
|
||||||
await page.goto( '/shop' );
|
|
||||||
|
|
||||||
const button = page.getByRole( 'button', { name: 'Clear' } );
|
|
||||||
|
|
||||||
await expect( button ).toBeHidden();
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'renders a dropdown list with the available attribute filters', async ( {
|
|
||||||
page,
|
|
||||||
} ) => {
|
|
||||||
await page.goto( '/shop' );
|
|
||||||
|
|
||||||
const dropdownLocator = page.locator(
|
|
||||||
'.wc-interactivity-dropdown'
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect( dropdownLocator ).toBeVisible();
|
|
||||||
await dropdownLocator.click();
|
|
||||||
|
|
||||||
for ( let i = 0; i < COLOR_ATTRIBUTE_VALUES.length; i++ ) {
|
|
||||||
await expect(
|
|
||||||
dropdownLocator.getByText( COLOR_ATTRIBUTE_VALUES[ i ] )
|
|
||||||
).toBeVisible();
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'Clicking a dropdown option should filter the displayed products', async ( {
|
|
||||||
page,
|
|
||||||
} ) => {
|
|
||||||
await page.goto( '/shop' );
|
|
||||||
|
|
||||||
const dropdownLocator = page.locator(
|
|
||||||
'.wc-interactivity-dropdown'
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect( dropdownLocator ).toBeVisible();
|
|
||||||
await dropdownLocator.click();
|
|
||||||
|
|
||||||
const yellowOption = page.getByText( 'Yellow' );
|
|
||||||
await yellowOption.click();
|
|
||||||
|
|
||||||
// wait for navigation
|
|
||||||
await page.waitForURL( /.*filter_color=yellow.*/ );
|
|
||||||
|
|
||||||
const products = page.locator( '.wc-block-product' );
|
|
||||||
|
|
||||||
await expect( products ).toHaveCount( 1 );
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'clear button appears after a filter is applied', async ( {
|
|
||||||
page,
|
|
||||||
} ) => {
|
|
||||||
await page.goto( '/shop' );
|
|
||||||
|
|
||||||
const dropdownLocator = page.locator(
|
|
||||||
'.wc-interactivity-dropdown'
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect( dropdownLocator ).toBeVisible();
|
|
||||||
await dropdownLocator.click();
|
|
||||||
|
|
||||||
const yellowOption = page.getByText( 'Yellow' );
|
|
||||||
await yellowOption.click();
|
|
||||||
|
|
||||||
// wait for navigation
|
|
||||||
await page.waitForURL( /.*filter_color=yellow.*/ );
|
|
||||||
|
|
||||||
const button = page.getByRole( 'button', { name: 'Clear' } );
|
|
||||||
|
|
||||||
await expect( button ).toBeVisible();
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'clear button hides after deselecting all filters', async ( {
|
|
||||||
page,
|
|
||||||
} ) => {
|
|
||||||
await page.goto( '/shop' );
|
|
||||||
|
|
||||||
const dropdownLocator = page.locator(
|
|
||||||
'.wc-interactivity-dropdown'
|
|
||||||
);
|
|
||||||
|
|
||||||
await dropdownLocator.click();
|
|
||||||
|
|
||||||
const yellowOption = page.getByText( 'Yellow' );
|
|
||||||
await yellowOption.click();
|
|
||||||
|
|
||||||
// wait for navigation
|
|
||||||
await page.waitForURL( /.*filter_color=yellow.*/ );
|
|
||||||
|
|
||||||
await dropdownLocator.click();
|
|
||||||
|
|
||||||
const button = page.getByRole( 'button', { name: 'Clear' } );
|
|
||||||
|
|
||||||
const removeFilter = page.locator(
|
|
||||||
'.wc-interactivity-dropdown__badge-remove'
|
|
||||||
);
|
|
||||||
|
|
||||||
await removeFilter.click();
|
|
||||||
|
|
||||||
await expect( button ).toBeHidden();
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'filters are cleared after clear button is clicked', async ( {
|
|
||||||
page,
|
|
||||||
} ) => {
|
|
||||||
await page.goto( '/shop' );
|
|
||||||
|
|
||||||
const dropdownLocator = page.locator(
|
|
||||||
'.wc-interactivity-dropdown'
|
|
||||||
);
|
|
||||||
|
|
||||||
await dropdownLocator.click();
|
|
||||||
|
|
||||||
const yellowOption = page.getByText( 'Yellow' );
|
|
||||||
await yellowOption.click();
|
|
||||||
|
|
||||||
// wait for navigation
|
|
||||||
await page.waitForURL( /.*filter_color=yellow.*/ );
|
|
||||||
|
|
||||||
const button = page.getByRole( 'button', { name: 'Clear' } );
|
|
||||||
|
|
||||||
await button.click();
|
|
||||||
|
|
||||||
const placeholder = page.getByText( 'Select Color' );
|
|
||||||
|
|
||||||
await expect( placeholder ).toBeVisible();
|
|
||||||
} );
|
|
||||||
} );
|
|
||||||
} );
|
|
|
@ -1,54 +0,0 @@
|
||||||
/**
|
|
||||||
* External dependencies
|
|
||||||
*/
|
|
||||||
import { test, expect } from '@woocommerce/e2e-utils';
|
|
||||||
|
|
||||||
const filterBlocks = [
|
|
||||||
{
|
|
||||||
name: 'woocommerce/product-filter-price',
|
|
||||||
title: 'Product Filter: Price (Experimental)',
|
|
||||||
heading: 'Filter by Price',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'woocommerce/product-filter-stock-status',
|
|
||||||
title: 'Product Filter: Stock Status (Experimental)',
|
|
||||||
heading: 'Filter by Stock Status',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'woocommerce/product-filter-rating',
|
|
||||||
title: 'Product Filter: Rating (Experimental)',
|
|
||||||
heading: 'Filter by Rating',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'woocommerce/product-filter-attribute',
|
|
||||||
title: 'Product Filter: Attribute (Experimental)',
|
|
||||||
heading: 'Filter by Attribute',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'woocommerce/product-filter-active',
|
|
||||||
title: 'Product Filter: Active Filters (Experimental)',
|
|
||||||
heading: 'Active Filters',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
test.describe( 'Filter blocks registration', () => {
|
|
||||||
test.beforeEach( async ( { admin } ) => {
|
|
||||||
await admin.createNewPost();
|
|
||||||
} );
|
|
||||||
|
|
||||||
test( 'Variations cannot be inserted through the inserter.', async ( {
|
|
||||||
page,
|
|
||||||
editor,
|
|
||||||
} ) => {
|
|
||||||
for ( const block of filterBlocks ) {
|
|
||||||
await editor.openGlobalBlockInserter();
|
|
||||||
await page.getByPlaceholder( 'Search' ).fill( block.title );
|
|
||||||
const filterBlock = page.getByRole( 'option', {
|
|
||||||
name: block.title,
|
|
||||||
exact: true,
|
|
||||||
} );
|
|
||||||
|
|
||||||
await expect( filterBlock ).toBeHidden();
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
} );
|
|
|
@ -105,10 +105,6 @@ test.describe( 'Filters Overlay Template Part', () => {
|
||||||
templatePartData.selectors.editor.blocks.activeFilters
|
templatePartData.selectors.editor.blocks.activeFilters
|
||||||
.blockLabel
|
.blockLabel
|
||||||
)
|
)
|
||||||
.getByLabel(
|
|
||||||
templatePartData.selectors.editor.blocks.filterOptions
|
|
||||||
.blockLabel
|
|
||||||
)
|
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await editor.openDocumentSettingsSidebar();
|
await editor.openDocumentSettingsSidebar();
|
||||||
|
@ -201,10 +197,6 @@ test.describe( 'Filters Overlay Template Part', () => {
|
||||||
templatePartData.selectors.editor.blocks.activeFilters
|
templatePartData.selectors.editor.blocks.activeFilters
|
||||||
.blockLabel
|
.blockLabel
|
||||||
)
|
)
|
||||||
.getByLabel(
|
|
||||||
templatePartData.selectors.editor.blocks.filterOptions
|
|
||||||
.blockLabel
|
|
||||||
)
|
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await editor.openDocumentSettingsSidebar();
|
await editor.openDocumentSettingsSidebar();
|
||||||
|
@ -221,12 +213,6 @@ test.describe( 'Filters Overlay Template Part', () => {
|
||||||
'OverlayNeverMobileAlways'
|
'OverlayNeverMobileAlways'
|
||||||
);
|
);
|
||||||
await layoutSettings.getByLabel( 'Never' ).click();
|
await layoutSettings.getByLabel( 'Never' ).click();
|
||||||
await editor.page
|
|
||||||
.getByRole( 'link', {
|
|
||||||
name: templatePartData.selectors.editor.blocks
|
|
||||||
.productFiltersOverlayNavigation.title,
|
|
||||||
} )
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await editor.saveSiteEditorEntities( {
|
await editor.saveSiteEditorEntities( {
|
||||||
isOnlyCurrentEntityDirty: true,
|
isOnlyCurrentEntityDirty: true,
|
||||||
|
@ -271,10 +257,6 @@ test.describe( 'Filters Overlay Template Part', () => {
|
||||||
templatePartData.selectors.editor.blocks.activeFilters
|
templatePartData.selectors.editor.blocks.activeFilters
|
||||||
.blockLabel
|
.blockLabel
|
||||||
)
|
)
|
||||||
.getByLabel(
|
|
||||||
templatePartData.selectors.editor.blocks.filterOptions
|
|
||||||
.blockLabel
|
|
||||||
)
|
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await editor.openDocumentSettingsSidebar();
|
await editor.openDocumentSettingsSidebar();
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { TemplateCompiler, test as base, expect } from '@woocommerce/e2e-utils';
|
||||||
|
|
||||||
|
const test = base.extend< { templateCompiler: TemplateCompiler } >( {
|
||||||
|
templateCompiler: async ( { requestUtils }, use ) => {
|
||||||
|
const compiler = await requestUtils.createTemplateFromFile(
|
||||||
|
'archive-product_active-filters'
|
||||||
|
);
|
||||||
|
await use( compiler );
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
test.describe( 'woocommerce/product-filter-active - Frontend', () => {
|
||||||
|
test.beforeEach( async ( { requestUtils } ) => {
|
||||||
|
await requestUtils.activatePlugin(
|
||||||
|
'woocommerce-blocks-test-enable-experimental-features'
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'Without any filters selected, only a wrapper block is rendered', async ( {
|
||||||
|
page,
|
||||||
|
templateCompiler,
|
||||||
|
} ) => {
|
||||||
|
await templateCompiler.compile( {
|
||||||
|
attributes: {
|
||||||
|
displayStyle: 'list',
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
await page.goto( '/shop' );
|
||||||
|
|
||||||
|
const locator = page.locator(
|
||||||
|
'.wp-block-woocommerce-product-filter-active'
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect( locator ).toHaveCount( 1 );
|
||||||
|
|
||||||
|
const html = await locator.innerHTML();
|
||||||
|
expect( html.trim() ).toBe( '' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'With rating filters applied it shows the correct active filters', async ( {
|
||||||
|
page,
|
||||||
|
templateCompiler,
|
||||||
|
} ) => {
|
||||||
|
await templateCompiler.compile( {
|
||||||
|
attributes: {
|
||||||
|
displayStyle: 'list',
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
await page.goto( `${ '/shop' }?rating_filter=1,2,5` );
|
||||||
|
|
||||||
|
await expect( page.getByText( 'Rating:' ) ).toBeVisible();
|
||||||
|
await expect( page.getByText( 'Rated 1 out of 5' ) ).toBeVisible();
|
||||||
|
await expect( page.getByText( 'Rated 2 out of 5' ) ).toBeVisible();
|
||||||
|
await expect( page.getByText( 'Rated 5 out of 5' ) ).toBeVisible();
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'With stock filters applied it shows the correct active filters', async ( {
|
||||||
|
page,
|
||||||
|
templateCompiler,
|
||||||
|
} ) => {
|
||||||
|
await templateCompiler.compile( {
|
||||||
|
attributes: {
|
||||||
|
displayStyle: 'list',
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
await page.goto(
|
||||||
|
`${ '/shop' }?filter_stock_status=instock,onbackorder`
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect( page.getByText( 'Stock Status:' ) ).toBeVisible();
|
||||||
|
await expect( page.getByText( 'In stock' ) ).toBeVisible();
|
||||||
|
await expect( page.getByText( 'On backorder' ) ).toBeVisible();
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'With attribute filters applied it shows the correct active filters', async ( {
|
||||||
|
page,
|
||||||
|
templateCompiler,
|
||||||
|
} ) => {
|
||||||
|
await templateCompiler.compile( {
|
||||||
|
attributes: {
|
||||||
|
displayStyle: 'list',
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
await page.goto(
|
||||||
|
`${ '/shop' }?filter_color=blue,gray&query_type_color=or`
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect( page.getByText( 'Color:' ) ).toBeVisible();
|
||||||
|
await expect( page.getByText( 'Blue' ) ).toBeVisible();
|
||||||
|
await expect( page.getByText( 'Gray' ) ).toBeVisible();
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'With price filters applied it shows the correct active filters', async ( {
|
||||||
|
page,
|
||||||
|
templateCompiler,
|
||||||
|
} ) => {
|
||||||
|
await templateCompiler.compile( {
|
||||||
|
attributes: {
|
||||||
|
displayStyle: 'list',
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
await page.goto( `${ '/shop' }?min_price=17&max_price=71` );
|
||||||
|
|
||||||
|
await expect( page.getByText( 'Price:' ) ).toBeVisible();
|
||||||
|
await expect( page.getByText( 'Between $17 and $71' ) ).toBeVisible();
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -48,9 +48,7 @@ test.describe( `${ blockData.name }`, () => {
|
||||||
} ) => {
|
} ) => {
|
||||||
await pageObject.addProductFiltersBlock( { cleanContent: true } );
|
await pageObject.addProductFiltersBlock( { cleanContent: true } );
|
||||||
|
|
||||||
const block = editor.canvas
|
const block = editor.canvas.getByLabel( 'Block: Color (Experimental)' );
|
||||||
.getByLabel( 'Block: Color (Experimental)' )
|
|
||||||
.getByLabel( 'Block: Filter Options' );
|
|
||||||
|
|
||||||
await expect( block ).toBeVisible();
|
await expect( block ).toBeVisible();
|
||||||
|
|
||||||
|
@ -82,9 +80,7 @@ test.describe( `${ blockData.name }`, () => {
|
||||||
} ) => {
|
} ) => {
|
||||||
await pageObject.addProductFiltersBlock( { cleanContent: true } );
|
await pageObject.addProductFiltersBlock( { cleanContent: true } );
|
||||||
|
|
||||||
const block = editor.canvas
|
const block = editor.canvas.getByLabel( 'Block: Color (Experimental)' );
|
||||||
.getByLabel( 'Block: Color (Experimental)' )
|
|
||||||
.getByLabel( 'Block: Filter Options' );
|
|
||||||
|
|
||||||
await expect( block ).toBeVisible();
|
await expect( block ).toBeVisible();
|
||||||
|
|
||||||
|
@ -112,9 +108,7 @@ test.describe( `${ blockData.name }`, () => {
|
||||||
} ) => {
|
} ) => {
|
||||||
await pageObject.addProductFiltersBlock( { cleanContent: true } );
|
await pageObject.addProductFiltersBlock( { cleanContent: true } );
|
||||||
|
|
||||||
const block = editor.canvas
|
const block = editor.canvas.getByLabel( 'Block: Color (Experimental)' );
|
||||||
.getByLabel( 'Block: Color (Experimental)' )
|
|
||||||
.getByLabel( 'Block: Filter Options' );
|
|
||||||
|
|
||||||
await editor.openDocumentSettingsSidebar();
|
await editor.openDocumentSettingsSidebar();
|
||||||
await block.click();
|
await block.click();
|
|
@ -0,0 +1,171 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { TemplateCompiler, test as base, expect } from '@woocommerce/e2e-utils';
|
||||||
|
|
||||||
|
const COLOR_ATTRIBUTE_VALUES = [ 'Blue', 'Gray', 'Green', 'Red', 'Yellow' ];
|
||||||
|
const COLOR_ATTRIBUTES_WITH_COUNTS = [
|
||||||
|
'Blue (4)',
|
||||||
|
'Gray (2)',
|
||||||
|
'Green (3)',
|
||||||
|
'Red (4)',
|
||||||
|
'Yellow (1)',
|
||||||
|
];
|
||||||
|
|
||||||
|
const test = base.extend< { templateCompiler: TemplateCompiler } >( {
|
||||||
|
templateCompiler: async ( { requestUtils }, use ) => {
|
||||||
|
const compiler = await requestUtils.createTemplateFromFile(
|
||||||
|
'archive-product_attribute-filter'
|
||||||
|
);
|
||||||
|
await use( compiler );
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
test.describe( 'woocommerce/product-filter-attribute - Frontend', () => {
|
||||||
|
test.describe( 'With default display style', () => {
|
||||||
|
test.beforeEach( async ( { requestUtils, templateCompiler } ) => {
|
||||||
|
await requestUtils.activatePlugin(
|
||||||
|
'woocommerce-blocks-test-enable-experimental-features'
|
||||||
|
);
|
||||||
|
await templateCompiler.compile( {
|
||||||
|
attributes: {
|
||||||
|
attributeId: 1,
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'clear button is not shown on initial page load', async ( {
|
||||||
|
page,
|
||||||
|
} ) => {
|
||||||
|
await page.goto( '/shop' );
|
||||||
|
|
||||||
|
const button = page.getByRole( 'button', { name: 'Clear' } );
|
||||||
|
|
||||||
|
await expect( button ).toBeHidden();
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'renders a checkbox list with the available attribute filters', async ( {
|
||||||
|
page,
|
||||||
|
} ) => {
|
||||||
|
await page.goto( '/shop' );
|
||||||
|
|
||||||
|
const listItems = page
|
||||||
|
.getByLabel( 'Filter Options' )
|
||||||
|
.getByRole( 'listitem' );
|
||||||
|
|
||||||
|
await expect( listItems ).toHaveCount( 5 );
|
||||||
|
|
||||||
|
for ( let i = 0; i < COLOR_ATTRIBUTE_VALUES.length; i++ ) {
|
||||||
|
await expect( listItems.nth( i ) ).toHaveText(
|
||||||
|
COLOR_ATTRIBUTE_VALUES[ i ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'filters the list of products by selecting an attribute', async ( {
|
||||||
|
page,
|
||||||
|
} ) => {
|
||||||
|
await page.goto( '/shop' );
|
||||||
|
|
||||||
|
const grayCheckbox = page.getByText( 'Gray' );
|
||||||
|
await grayCheckbox.click();
|
||||||
|
|
||||||
|
// wait for navigation
|
||||||
|
await page.waitForURL( /.*filter_color=gray.*/ );
|
||||||
|
|
||||||
|
const products = page.locator( '.wc-block-product' );
|
||||||
|
|
||||||
|
await expect( products ).toHaveCount( 2 );
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'clear button appears after a filter is applied', async ( {
|
||||||
|
page,
|
||||||
|
} ) => {
|
||||||
|
await page.goto( '/shop' );
|
||||||
|
|
||||||
|
const grayCheckbox = page.getByText( 'Gray' );
|
||||||
|
await grayCheckbox.click();
|
||||||
|
|
||||||
|
// wait for navigation
|
||||||
|
await page.waitForURL( /.*filter_color=gray.*/ );
|
||||||
|
|
||||||
|
const button = page.getByRole( 'button', { name: 'Clear' } );
|
||||||
|
|
||||||
|
await expect( button ).toBeVisible();
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'clear button hides after deselecting all filters', async ( {
|
||||||
|
page,
|
||||||
|
} ) => {
|
||||||
|
await page.goto( '/shop' );
|
||||||
|
|
||||||
|
const grayCheckbox = page.getByText( 'Gray' );
|
||||||
|
await grayCheckbox.click();
|
||||||
|
|
||||||
|
// wait for navigation
|
||||||
|
await page.waitForURL( /.*filter_color=gray.*/ );
|
||||||
|
|
||||||
|
await grayCheckbox.click();
|
||||||
|
|
||||||
|
const button = page.getByRole( 'button', { name: 'Clear' } );
|
||||||
|
|
||||||
|
await expect( button ).toBeHidden();
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'filters are cleared after clear button is clicked', async ( {
|
||||||
|
page,
|
||||||
|
} ) => {
|
||||||
|
await page.goto( '/shop' );
|
||||||
|
|
||||||
|
const grayCheckbox = page.getByText( 'Gray' );
|
||||||
|
await grayCheckbox.click();
|
||||||
|
|
||||||
|
// wait for navigation
|
||||||
|
await page.waitForURL( /.*filter_color=gray.*/ );
|
||||||
|
|
||||||
|
const button = page.getByRole( 'button', { name: 'Clear' } );
|
||||||
|
|
||||||
|
await button.click();
|
||||||
|
|
||||||
|
COLOR_ATTRIBUTE_VALUES.map( async ( color ) => {
|
||||||
|
const element = page.locator(
|
||||||
|
`input[value="${ color.toLowerCase() }"]`
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect( element ).not.toBeChecked();
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
test.describe( 'With show counts enabled', () => {
|
||||||
|
test.beforeEach( async ( { requestUtils, templateCompiler } ) => {
|
||||||
|
await requestUtils.activatePlugin(
|
||||||
|
'woocommerce-blocks-test-enable-experimental-features'
|
||||||
|
);
|
||||||
|
await templateCompiler.compile( {
|
||||||
|
attributes: {
|
||||||
|
attributeId: 1,
|
||||||
|
showCounts: true,
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
test( 'Renders checkboxes with associated product counts', async ( {
|
||||||
|
page,
|
||||||
|
} ) => {
|
||||||
|
await page.goto( '/shop' );
|
||||||
|
|
||||||
|
const listItems = page
|
||||||
|
.getByLabel( 'Filter Options' )
|
||||||
|
.getByRole( 'listitem' );
|
||||||
|
|
||||||
|
await expect( listItems ).toHaveCount( 5 );
|
||||||
|
|
||||||
|
for ( let i = 0; i < COLOR_ATTRIBUTES_WITH_COUNTS.length; i++ ) {
|
||||||
|
await expect( listItems.nth( i ) ).toHaveText(
|
||||||
|
COLOR_ATTRIBUTES_WITH_COUNTS[ i ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -12,7 +12,7 @@ const test = base.extend< { templateCompiler: TemplateCompiler } >( {
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
||||||
test.describe( 'Product Filter: Price Filter Block', () => {
|
test.describe.skip( 'Product Filter: Price Filter Block', () => {
|
||||||
test.describe( 'frontend', () => {
|
test.describe( 'frontend', () => {
|
||||||
test.beforeEach( async ( { requestUtils, templateCompiler } ) => {
|
test.beforeEach( async ( { requestUtils, templateCompiler } ) => {
|
||||||
await requestUtils.activatePlugin(
|
await requestUtils.activatePlugin(
|
|
@ -52,13 +52,7 @@ test.describe( 'Product Filters Template Part', () => {
|
||||||
const block = editor.canvas.getByLabel( `Block: ${ blockData.name }` );
|
const block = editor.canvas.getByLabel( `Block: ${ blockData.name }` );
|
||||||
await expect( block ).toBeVisible();
|
await expect( block ).toBeVisible();
|
||||||
|
|
||||||
const searchTerms = [
|
const searchTerms = [ 'Color (Experimental)', 'Active (Experimental)' ];
|
||||||
'Status (Experimental)',
|
|
||||||
'Price (Experimental)',
|
|
||||||
'Rating (Experimental)',
|
|
||||||
'Attribute (Experimental)',
|
|
||||||
'Active (Experimental)',
|
|
||||||
];
|
|
||||||
|
|
||||||
for ( const filter of searchTerms ) {
|
for ( const filter of searchTerms ) {
|
||||||
await editor.selectBlocks( blockData.selectors.editor.block );
|
await editor.selectBlocks( blockData.selectors.editor.block );
|
||||||
|
@ -78,13 +72,7 @@ test.describe( 'Product Filters Template Part', () => {
|
||||||
|
|
||||||
await searchResult.click();
|
await searchResult.click();
|
||||||
|
|
||||||
let _locator = `[aria-label="Block: ${ filter }"]`;
|
const _locator = `[aria-label="Block: ${ filter }"]`;
|
||||||
|
|
||||||
// We need to treat the attributes filter different because
|
|
||||||
// the variation of the block label depends on the product attribute.
|
|
||||||
if ( filter === 'Attribute (Experimental)' ) {
|
|
||||||
_locator = '.wp-block-woocommerce-product-filter-attribute';
|
|
||||||
}
|
|
||||||
|
|
||||||
await expect( editor.canvas.locator( _locator ) ).toHaveCount( 2 );
|
await expect( editor.canvas.locator( _locator ) ).toHaveCount( 2 );
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,52 +68,16 @@ test.describe( `${ blockData.name }`, () => {
|
||||||
);
|
);
|
||||||
await expect( block ).toBeVisible();
|
await expect( block ).toBeVisible();
|
||||||
|
|
||||||
const activeHeading = block.getByText( 'Active', { exact: true } );
|
const activeFilterBlock = block.getByLabel(
|
||||||
const activeFilterBlock = block
|
'Block: Active (Experimental)'
|
||||||
.getByLabel( 'Block: Filter Options' )
|
|
||||||
.and(
|
|
||||||
editor.canvas.locator(
|
|
||||||
'[data-type="woocommerce/product-filter-active"]'
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
await expect( activeHeading ).toBeVisible();
|
|
||||||
await expect( activeFilterBlock ).toBeVisible();
|
await expect( activeFilterBlock ).toBeVisible();
|
||||||
|
|
||||||
const priceHeading = block.getByText( 'Price', {
|
|
||||||
exact: true,
|
|
||||||
} );
|
|
||||||
const priceFilterBlock = block
|
|
||||||
.getByLabel( 'Block: Filter Options' )
|
|
||||||
.and(
|
|
||||||
editor.canvas.locator(
|
|
||||||
'[data-type="woocommerce/product-filter-price"]'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
await expect( priceHeading ).toBeVisible();
|
|
||||||
await expect( priceFilterBlock ).toBeVisible();
|
|
||||||
|
|
||||||
const statusHeading = block.getByText( 'Status', {
|
|
||||||
exact: true,
|
|
||||||
} );
|
|
||||||
const statusFilterBlock = block
|
|
||||||
.getByLabel( 'Block: Filter Options' )
|
|
||||||
.and(
|
|
||||||
editor.canvas.locator(
|
|
||||||
'[data-type="woocommerce/product-filter-stock-status"]'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
await expect( statusHeading ).toBeVisible();
|
|
||||||
await expect( statusFilterBlock ).toBeVisible();
|
|
||||||
|
|
||||||
const colorHeading = block.getByText( 'Color', {
|
const colorHeading = block.getByText( 'Color', {
|
||||||
exact: true,
|
exact: true,
|
||||||
} );
|
} );
|
||||||
const colorFilterBlock = block
|
const colorFilterBlock = block.getByLabel(
|
||||||
.getByLabel( 'Block: Filter Options' )
|
'Block: Color (Experimental)'
|
||||||
.and(
|
|
||||||
editor.canvas.locator(
|
|
||||||
'[data-type="woocommerce/product-filter-attribute"]'
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
const expectedColorFilterOptions = [
|
const expectedColorFilterOptions = [
|
||||||
'Blue',
|
'Blue',
|
||||||
|
@ -122,27 +86,11 @@ test.describe( `${ blockData.name }`, () => {
|
||||||
'Red',
|
'Red',
|
||||||
'Yellow',
|
'Yellow',
|
||||||
];
|
];
|
||||||
const colorFilterOptions = (
|
|
||||||
await colorFilterBlock.allInnerTexts()
|
|
||||||
)[ 0 ].split( '\n' );
|
|
||||||
await expect( colorHeading ).toBeVisible();
|
await expect( colorHeading ).toBeVisible();
|
||||||
await expect( colorFilterBlock ).toBeVisible();
|
await expect( colorFilterBlock ).toBeVisible();
|
||||||
expect( colorFilterOptions ).toEqual(
|
for ( const option of expectedColorFilterOptions ) {
|
||||||
expect.arrayContaining( expectedColorFilterOptions )
|
await expect( colorFilterBlock ).toContainText( option );
|
||||||
);
|
}
|
||||||
|
|
||||||
const ratingHeading = block.getByText( 'Rating', {
|
|
||||||
exact: true,
|
|
||||||
} );
|
|
||||||
const ratingFilterBlock = block
|
|
||||||
.getByLabel( 'Block: Filter Options' )
|
|
||||||
.and(
|
|
||||||
editor.canvas.locator(
|
|
||||||
'[data-type="woocommerce/product-filter-rating"]'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
await expect( ratingHeading ).toBeVisible();
|
|
||||||
await expect( ratingFilterBlock ).toBeVisible();
|
|
||||||
} );
|
} );
|
||||||
|
|
||||||
test( 'should contain the correct inner block names in the list view', async ( {
|
test( 'should contain the correct inner block names in the list view', async ( {
|
||||||
|
@ -181,25 +129,10 @@ test.describe( `${ blockData.name }`, () => {
|
||||||
);
|
);
|
||||||
await expect( productFilterActiveBlocksListItem ).toBeVisible();
|
await expect( productFilterActiveBlocksListItem ).toBeVisible();
|
||||||
|
|
||||||
const productFilterPriceBlockListItem = listView.getByText(
|
|
||||||
'Price (Experimental)'
|
|
||||||
);
|
|
||||||
await expect( productFilterPriceBlockListItem ).toBeVisible();
|
|
||||||
|
|
||||||
const productFilterStatusBlockListItem = listView.getByText(
|
|
||||||
'Status (Experimental)'
|
|
||||||
);
|
|
||||||
await expect( productFilterStatusBlockListItem ).toBeVisible();
|
|
||||||
|
|
||||||
const productFilterAttributeBlockListItem = listView.getByText(
|
const productFilterAttributeBlockListItem = listView.getByText(
|
||||||
'Color (Experimental)' // it must select the attribute with the highest product count
|
'Color (Experimental)' // it must select the attribute with the highest product count
|
||||||
);
|
);
|
||||||
await expect( productFilterAttributeBlockListItem ).toBeVisible();
|
await expect( productFilterAttributeBlockListItem ).toBeVisible();
|
||||||
|
|
||||||
const productFilterRatingBlockListItem = listView.getByText(
|
|
||||||
'Rating (Experimental)'
|
|
||||||
);
|
|
||||||
await expect( productFilterRatingBlockListItem ).toBeVisible();
|
|
||||||
} );
|
} );
|
||||||
|
|
||||||
test( 'should display the correct inspector style controls', async ( {
|
test( 'should display the correct inspector style controls', async ( {
|
||||||
|
@ -370,7 +303,7 @@ test.describe( `${ blockData.name }`, () => {
|
||||||
).toHaveCSS( 'align-items', 'center' );
|
).toHaveCSS( 'align-items', 'center' );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
test( 'Layout > Orientation: changing option should update the preview', async ( {
|
test.skip( 'Layout > Orientation: changing option should update the preview', async ( {
|
||||||
editor,
|
editor,
|
||||||
pageObject,
|
pageObject,
|
||||||
} ) => {
|
} ) => {
|
||||||
|
|
|
@ -12,7 +12,7 @@ const test = base.extend< { templateCompiler: TemplateCompiler } >( {
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
||||||
test.describe( 'Product Filter: Rating Filter Block', () => {
|
test.describe.skip( 'Product Filter: Rating Filter Block', () => {
|
||||||
test.describe( 'frontend', () => {
|
test.describe( 'frontend', () => {
|
||||||
test.beforeEach( async ( { requestUtils, templateCompiler } ) => {
|
test.beforeEach( async ( { requestUtils, templateCompiler } ) => {
|
||||||
await requestUtils.activatePlugin(
|
await requestUtils.activatePlugin(
|
|
@ -12,7 +12,7 @@ const test = base.extend< { templateCompiler: TemplateCompiler } >( {
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
||||||
test.describe( 'Product Filter: Stock Status Block', () => {
|
test.describe.skip( 'Product Filter: Stock Status Block', () => {
|
||||||
test.describe( 'With default display style', () => {
|
test.describe( 'With default display style', () => {
|
||||||
test.beforeEach( async ( { requestUtils, templateCompiler } ) => {
|
test.beforeEach( async ( { requestUtils, templateCompiler } ) => {
|
||||||
await requestUtils.activatePlugin(
|
await requestUtils.activatePlugin(
|
|
@ -0,0 +1,5 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: add
|
||||||
|
Comment: Filter blocks: new and improve filter blocks structure.
|
||||||
|
|
||||||
|
|
|
@ -1,157 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
|
||||||
|
|
||||||
use Automattic\WooCommerce\Blocks\QueryFilters;
|
|
||||||
use Automattic\WooCommerce\Blocks\Package;
|
|
||||||
use WP_HTML_Tag_Processor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Product Filter Block.
|
|
||||||
*/
|
|
||||||
final class ProductFilter extends AbstractBlock {
|
|
||||||
/**
|
|
||||||
* Block name.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $block_name = 'product-filter';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the frontend style handle for this block type.
|
|
||||||
*
|
|
||||||
* @return null
|
|
||||||
*/
|
|
||||||
protected function get_block_type_style() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the frontend script handle for this block type.
|
|
||||||
*
|
|
||||||
* @see $this->register_block_type()
|
|
||||||
* @param string $key Data to get, or default to everything.
|
|
||||||
* @return array|string|null
|
|
||||||
*/
|
|
||||||
protected function get_block_type_script( $key = null ) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extra data passed through from server to client for block.
|
|
||||||
*
|
|
||||||
* @param array $attributes Any attributes that currently are available from the block.
|
|
||||||
* Note, this will be empty in the editor context when the block is
|
|
||||||
* not in the post content on editor load.
|
|
||||||
*/
|
|
||||||
protected function enqueue_data( array $attributes = [] ) {
|
|
||||||
global $pagenow;
|
|
||||||
parent::enqueue_data( $attributes );
|
|
||||||
|
|
||||||
$this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme() );
|
|
||||||
$this->asset_data_registry->add( 'isProductArchive', is_shop() || is_product_taxonomy() );
|
|
||||||
$this->asset_data_registry->add( 'isSiteEditor', 'site-editor.php' === $pagenow );
|
|
||||||
$this->asset_data_registry->add( 'isWidgetEditor', 'widgets.php' === $pagenow || 'customize.php' === $pagenow );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check array for checked item.
|
|
||||||
*
|
|
||||||
* @param array $items Items to check.
|
|
||||||
*/
|
|
||||||
private function hasSelectedFilter( $items ) {
|
|
||||||
foreach ( $items as $key => $value ) {
|
|
||||||
if ( 'checked' === $key && true === $value ) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( is_array( $value ) && $this->hasSelectedFilter( $value ) ) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the block.
|
|
||||||
*
|
|
||||||
* @param array $attributes Block attributes.
|
|
||||||
* @param string $content Block content.
|
|
||||||
* @param WP_Block $block Block instance.
|
|
||||||
* @return string Rendered block type output.
|
|
||||||
*/
|
|
||||||
protected function render( $attributes, $content, $block ) {
|
|
||||||
if ( is_admin() ) {
|
|
||||||
return $content;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tags = new WP_HTML_Tag_Processor( $content );
|
|
||||||
$has_selected_filter = false;
|
|
||||||
|
|
||||||
while ( $tags->next_tag( 'div' ) ) {
|
|
||||||
$items = $tags->get_attribute( 'data-wc-context' ) ? json_decode( $tags->get_attribute( 'data-wc-context' ), true ) : null;
|
|
||||||
|
|
||||||
// For checked box filters.
|
|
||||||
if ( $items && array_key_exists( 'items', $items ) ) {
|
|
||||||
$has_selected_filter = $this->hasSelectedFilter( $items['items'] );
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For price range filter.
|
|
||||||
if ( $items && array_key_exists( 'minPrice', $items ) ) {
|
|
||||||
if ( $items['minPrice'] > $items['minRange'] || $items['maxPrice'] < $items['maxRange'] ) {
|
|
||||||
$has_selected_filter = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For dropdown filters.
|
|
||||||
if ( $items && array_key_exists( 'selectedItems', $items ) ) {
|
|
||||||
if ( count( $items['selectedItems'] ) > 0 ) {
|
|
||||||
$has_selected_filter = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$attributes_data = array(
|
|
||||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
|
|
||||||
'data-wc-context' => wp_json_encode( array( 'hasSelectedFilter' => $has_selected_filter ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
|
|
||||||
'class' => 'wc-block-product-filters',
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( ! isset( $block->context['queryId'] ) ) {
|
|
||||||
$attributes_data['data-wc-navigation-id'] = $this->generate_navigation_id( $block );
|
|
||||||
}
|
|
||||||
|
|
||||||
$tags = new WP_HTML_Tag_Processor( $content );
|
|
||||||
|
|
||||||
while ( $tags->next_tag( 'div' ) ) {
|
|
||||||
if ( 'yes' === $tags->get_attribute( 'data-has-filter' ) ) {
|
|
||||||
return sprintf(
|
|
||||||
'<nav %1$s>%2$s</nav>',
|
|
||||||
get_block_wrapper_attributes( $attributes_data ),
|
|
||||||
$content
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sprintf(
|
|
||||||
'<nav %1$s></nav>',
|
|
||||||
get_block_wrapper_attributes( $attributes_data ),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a unique navigation ID for the block.
|
|
||||||
*
|
|
||||||
* @param mixed $block - Block instance.
|
|
||||||
* @return string - Unique navigation ID.
|
|
||||||
*/
|
|
||||||
private function generate_navigation_id( $block ) {
|
|
||||||
return sprintf(
|
|
||||||
'wc-product-filter-%s',
|
|
||||||
md5( wp_json_encode( $block->parsed_block['innerBlocks'] ) )
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -57,7 +57,6 @@ final class ProductFilterActive extends AbstractBlock {
|
||||||
array(
|
array(
|
||||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
|
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
|
||||||
'data-wc-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
|
'data-wc-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
|
||||||
'data-has-filter' => empty( $active_filters ) ? 'no' : 'yes',
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
declare( strict_types = 1 );
|
||||||
|
|
||||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||||
|
|
||||||
use Automattic\WooCommerce\Blocks\QueryFilters;
|
use Automattic\WooCommerce\Blocks\QueryFilters;
|
||||||
use Automattic\WooCommerce\Blocks\Package;
|
use Automattic\WooCommerce\Blocks\Package;
|
||||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\Dropdown;
|
|
||||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\CheckboxList;
|
|
||||||
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
|
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,6 +26,7 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
* - Register the block with WordPress.
|
* - Register the block with WordPress.
|
||||||
*/
|
*/
|
||||||
protected function initialize() {
|
protected function initialize() {
|
||||||
|
add_filter( 'block_type_metadata_settings', array( $this, 'add_block_type_metadata_settings' ), 10, 2 );
|
||||||
parent::initialize();
|
parent::initialize();
|
||||||
|
|
||||||
add_filter( 'collection_filter_query_param_keys', array( $this, 'get_filter_query_param_keys' ), 10, 2 );
|
add_filter( 'collection_filter_query_param_keys', array( $this, 'get_filter_query_param_keys' ), 10, 2 );
|
||||||
|
@ -156,24 +157,24 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
/**
|
/**
|
||||||
* Render the block.
|
* Render the block.
|
||||||
*
|
*
|
||||||
* @param array $attributes Block attributes.
|
* @param array $block_attributes Block attributes.
|
||||||
* @param string $content Block content.
|
* @param string $content Block content.
|
||||||
* @param WP_Block $block Block instance.
|
* @param WP_Block $block Block instance.
|
||||||
* @return string Rendered block type output.
|
* @return string Rendered block type output.
|
||||||
*/
|
*/
|
||||||
protected function render( $attributes, $content, $block ) {
|
protected function render( $block_attributes, $content, $block ) {
|
||||||
if ( empty( $attributes['attributeId'] ) ) {
|
if ( empty( $block_attributes['attributeId'] ) ) {
|
||||||
$default_product_attribute = $this->get_default_product_attribute();
|
$default_product_attribute = $this->get_default_product_attribute();
|
||||||
$attributes['attributeId'] = $default_product_attribute->attribute_id;
|
$block_attributes['attributeId'] = $default_product_attribute->attribute_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't render if its admin, or ajax in progress.
|
// don't render if its admin, or ajax in progress.
|
||||||
if ( is_admin() || wp_doing_ajax() || empty( $attributes['attributeId'] ) ) {
|
if ( is_admin() || wp_doing_ajax() || empty( $block_attributes['attributeId'] ) ) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$product_attribute = wc_get_attribute( $attributes['attributeId'] );
|
$product_attribute = wc_get_attribute( $block_attributes['attributeId'] );
|
||||||
$attribute_counts = $this->get_attribute_counts( $block, $product_attribute->slug, $attributes['queryType'] );
|
$attribute_counts = $this->get_attribute_counts( $block, $product_attribute->slug, $block_attributes['queryType'] );
|
||||||
|
|
||||||
if ( empty( $attribute_counts ) ) {
|
if ( empty( $attribute_counts ) ) {
|
||||||
return sprintf(
|
return sprintf(
|
||||||
|
@ -181,7 +182,6 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
get_block_wrapper_attributes(
|
get_block_wrapper_attributes(
|
||||||
array(
|
array(
|
||||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
|
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
|
||||||
'data-has-filter' => 'no',
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -202,118 +202,56 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
);
|
);
|
||||||
|
|
||||||
$attribute_options = array_map(
|
$attribute_options = array_map(
|
||||||
function ( $term ) use ( $attribute_counts, $selected_terms ) {
|
function ( $term ) use ( $block_attributes, $attribute_counts, $selected_terms ) {
|
||||||
$term = (array) $term;
|
$term = (array) $term;
|
||||||
$term['count'] = $attribute_counts[ $term['term_id'] ];
|
$term['count'] = $attribute_counts[ $term['term_id'] ];
|
||||||
$term['selected'] = in_array( $term['slug'], $selected_terms, true );
|
$term['selected'] = in_array( $term['slug'], $selected_terms, true );
|
||||||
return $term;
|
return array(
|
||||||
|
'label' => $block_attributes['showCounts'] ? sprintf( '%1$s (%2$d)', $term['name'], $term['count'] ) : $term['name'],
|
||||||
|
'value' => $term['slug'],
|
||||||
|
'selected' => $term['selected'],
|
||||||
|
'rawData' => $term,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
$attribute_terms
|
$attribute_terms
|
||||||
);
|
);
|
||||||
|
|
||||||
$filtered_options = array_filter(
|
$filtered_options = array_filter(
|
||||||
$attribute_options,
|
$attribute_options,
|
||||||
function ( $option ) {
|
function ( $option ) use ( $block_attributes ) {
|
||||||
return $option['count'] > 0;
|
$hide_empty = $block_attributes['hideEmpty'] ?? true;
|
||||||
|
if ( $hide_empty ) {
|
||||||
|
return $option['rawData']['count'] > 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
$filter_content = 'dropdown' === $attributes['displayStyle'] ?
|
$filter_context = array(
|
||||||
$this->render_attribute_dropdown( $filtered_options, $attributes ) :
|
'on_change' => "{$this->get_full_block_name()}::actions.updateProducts",
|
||||||
$this->render_attribute_checkbox_list( $filtered_options, $attributes );
|
'items' => $filtered_options,
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $block->parsed_block['innerBlocks'] as $inner_block ) {
|
||||||
|
$content .= ( new \WP_Block( $inner_block, array( 'filterData' => $filter_context ) ) )->render();
|
||||||
|
}
|
||||||
|
|
||||||
$context = array(
|
$context = array(
|
||||||
'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ),
|
'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ),
|
||||||
'queryType' => $attributes['queryType'],
|
'queryType' => $block_attributes['queryType'],
|
||||||
'selectType' => 'multiple',
|
'selectType' => 'multiple',
|
||||||
|
'hasSelectedFilters' => count( $selected_terms ) > 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
return sprintf(
|
return sprintf(
|
||||||
'<div %1$s>%2$s%3$s</div>',
|
'<div %1$s>%2$s</div>',
|
||||||
get_block_wrapper_attributes(
|
get_block_wrapper_attributes(
|
||||||
array(
|
array(
|
||||||
'data-wc-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
|
'data-wc-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
|
||||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
|
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
|
||||||
'data-has-filter' => 'yes',
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
$content,
|
$content
|
||||||
$filter_content
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the dropdown.
|
|
||||||
*
|
|
||||||
* @param array $options Data to render the dropdown.
|
|
||||||
* @param bool $attributes Block attributes.
|
|
||||||
*/
|
|
||||||
private function render_attribute_dropdown( $options, $attributes ) {
|
|
||||||
if ( empty( $options ) ) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$list_items = array();
|
|
||||||
$selected_items = array();
|
|
||||||
|
|
||||||
$product_attribute = wc_get_attribute( $attributes['attributeId'] );
|
|
||||||
|
|
||||||
foreach ( $options as $option ) {
|
|
||||||
$item = array(
|
|
||||||
'label' => $attributes['showCounts'] ? sprintf( '%1$s (%2$d)', $option['name'], $option['count'] ) : $option['name'],
|
|
||||||
'value' => $option['slug'],
|
|
||||||
);
|
|
||||||
|
|
||||||
$list_items[] = $item;
|
|
||||||
|
|
||||||
if ( $option['selected'] ) {
|
|
||||||
$selected_items[] = $item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Dropdown::render(
|
|
||||||
array(
|
|
||||||
'items' => $list_items,
|
|
||||||
'action' => "{$this->get_full_block_name()}::actions.navigate",
|
|
||||||
'selected_items' => $selected_items,
|
|
||||||
'select_type' => 'multiple',
|
|
||||||
// translators: %s is a product attribute name.
|
|
||||||
'placeholder' => sprintf( __( 'Select %s', 'woocommerce' ), $product_attribute->name ),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the attribute filter checkbox list.
|
|
||||||
*
|
|
||||||
* @param mixed $options Attribute filter options to render in the checkbox list.
|
|
||||||
* @param mixed $attributes Block attributes.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function render_attribute_checkbox_list( $options, $attributes ) {
|
|
||||||
if ( empty( $options ) ) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$show_counts = $attributes['showCounts'] ?? false;
|
|
||||||
|
|
||||||
$list_options = array_map(
|
|
||||||
function ( $option ) use ( $show_counts ) {
|
|
||||||
return array(
|
|
||||||
'id' => $option['slug'] . '-' . $option['term_id'],
|
|
||||||
'checked' => $option['selected'],
|
|
||||||
'label' => $show_counts ? sprintf( '%1$s (%2$d)', $option['name'], $option['count'] ) : $option['name'],
|
|
||||||
'value' => $option['slug'],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
$options
|
|
||||||
);
|
|
||||||
|
|
||||||
return CheckboxList::render(
|
|
||||||
array(
|
|
||||||
'items' => $list_options,
|
|
||||||
'on_change' => "{$this->get_full_block_name()}::actions.updateProducts",
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -380,7 +318,16 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
|
|
||||||
$cached = get_transient( 'wc_block_product_filter_attribute_default_attribute' );
|
$cached = get_transient( 'wc_block_product_filter_attribute_default_attribute' );
|
||||||
|
|
||||||
if ( $cached ) {
|
if (
|
||||||
|
$cached &&
|
||||||
|
isset( $cached->attribute_id ) &&
|
||||||
|
isset( $cached->attribute_name ) &&
|
||||||
|
isset( $cached->attribute_label ) &&
|
||||||
|
isset( $cached->attribute_type ) &&
|
||||||
|
isset( $cached->attribute_orderby ) &&
|
||||||
|
isset( $cached->attribute_public ) &&
|
||||||
|
'0' !== $cached->attribute_id
|
||||||
|
) {
|
||||||
return $cached;
|
return $cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -428,10 +375,9 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
|
|
||||||
if ( $attribute_id ) {
|
if ( $attribute_id ) {
|
||||||
$default_attribute = $attributes[ $attribute_id ];
|
$default_attribute = $attributes[ $attribute_id ];
|
||||||
|
set_transient( 'wc_block_product_filter_attribute_default_attribute', $default_attribute, DAY_IN_SECONDS );
|
||||||
}
|
}
|
||||||
|
|
||||||
set_transient( 'wc_block_product_filter_attribute_default_attribute', $default_attribute );
|
|
||||||
|
|
||||||
return $default_attribute;
|
return $default_attribute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,32 +393,26 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
'inserter' => false,
|
'inserter' => false,
|
||||||
'content' => strtr(
|
'content' => strtr(
|
||||||
'
|
'
|
||||||
<!-- wp:woocommerce/product-filter {"filterType":"attribute-filter","attributeId":{{attribute_id}}} -->
|
<!-- wp:woocommerce/product-filter-attribute {"attributeId":{{attribute_id}}} -->
|
||||||
<!-- wp:group {"metadata":{"name":"Header"},"style":{"spacing":{"blockGap":"0"}},"layout":{"type":"flex","flexWrap":"nowrap"}} -->
|
<div class="wp-block-woocommerce-product-filter-attribute">
|
||||||
<div class="wp-block-group">
|
<!-- wp:group {"metadata":{"name":"Header"},"style":{"spacing":{"blockGap":"0"}},"layout":{"type":"flex","flexWrap":"nowrap"}} -->
|
||||||
|
<div class="wp-block-group">
|
||||||
<!-- wp:heading {"level":3} -->
|
<!-- wp:heading {"level":3} -->
|
||||||
<h3 class="wp-block-heading">{{attribute_label}}</h3>
|
<h3 class="wp-block-heading">{{attribute_label}}</h3>
|
||||||
<!-- /wp:heading -->
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
<!-- wp:woocommerce/product-filter-clear-button {"lock":{"remove":true,"move":false}} -->
|
<!-- wp:woocommerce/product-filter-clear-button {"lock":{"remove":true}} -->
|
||||||
<!-- wp:buttons {"layout":{"type":"flex"}} -->
|
<!-- wp:buttons {"layout":{"type":"flex"}} -->
|
||||||
<div class="wp-block-buttons">
|
<div class="wp-block-buttons"><!-- wp:button {"className":"wc-block-product-filter-clear-button is-style-outline","style":{"border":{"width":"0px","style":"none"},"typography":{"textDecoration":"underline"},"outline":"none","fontSize":"medium"}} -->
|
||||||
<!-- wp:button {"className":"wc-block-product-filter-clear-button is-style-outline","style":{"border":{"width":"0px","style":"none"},"typography":{"textDecoration":"underline"},"outline":"none","fontSize":"medium"}} -->
|
<div class="wp-block-button wc-block-product-filter-clear-button is-style-outline" style="text-decoration:underline"><a class="wp-block-button__link wp-element-button" style="border-style:none;border-width:0px">Clear</a></div>
|
||||||
<div
|
<!-- /wp:button --></div>
|
||||||
class="wp-block-button wc-block-product-filter-clear-button is-style-outline"
|
|
||||||
style="text-decoration: underline"
|
|
||||||
>
|
|
||||||
<a class="wp-block-button__link wp-element-button" style="border-style: none; border-width: 0px">Clear</a>
|
|
||||||
</div>
|
|
||||||
<!-- /wp:button -->
|
|
||||||
</div>
|
|
||||||
<!-- /wp:buttons -->
|
<!-- /wp:buttons -->
|
||||||
<!-- /wp:woocommerce/product-filter-clear-button -->
|
<!-- /wp:woocommerce/product-filter-clear-button --></div>
|
||||||
</div>
|
<!-- /wp:group -->
|
||||||
<!-- /wp:group -->
|
|
||||||
|
|
||||||
<!-- wp:woocommerce/product-filter-attribute {"attributeId":{{attribute_id}},"lock":{"remove":true}} /-->
|
<!-- wp:woocommerce/product-filter-checkbox-list {"lock":{"remove":true},"className":"wp-block-woocommerce-product-filter-checkbox-list"} /-->
|
||||||
<!-- /wp:woocommerce/product-filter -->
|
</div>
|
||||||
|
<!-- /wp:woocommerce/product-filter-attribute -->
|
||||||
',
|
',
|
||||||
array(
|
array(
|
||||||
'{{attribute_id}}' => intval( $default_attribute->attribute_id ),
|
'{{attribute_id}}' => intval( $default_attribute->attribute_id ),
|
||||||
|
@ -482,4 +422,18 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skip default rendering routine for inner blocks.
|
||||||
|
*
|
||||||
|
* @param array $settings Array of determined settings for registering a block type.
|
||||||
|
* @param array $metadata Metadata provided for registering a block type.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function add_block_type_metadata_settings( $settings, $metadata ) {
|
||||||
|
if ( ! empty( $metadata['name'] ) && "woocommerce/{$this->block_name}" === $metadata['name'] ) {
|
||||||
|
$settings['skip_inner_blocks'] = true;
|
||||||
|
}
|
||||||
|
return $settings;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
<?php
|
||||||
|
declare( strict_types = 1 );
|
||||||
|
|
||||||
|
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Product Filter: Checkbox List Block.
|
||||||
|
*/
|
||||||
|
final class ProductFilterCheckboxList extends AbstractBlock {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block name.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $block_name = 'product-filter-checkbox-list';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the block.
|
||||||
|
*
|
||||||
|
* @param array $attributes Block attributes.
|
||||||
|
* @param string $content Block content.
|
||||||
|
* @param WP_Block $block Block instance.
|
||||||
|
* @return string Rendered block type output.
|
||||||
|
*/
|
||||||
|
protected function render( $attributes, $content, $block ) {
|
||||||
|
$context = $block->context['filterData'];
|
||||||
|
$items = $context['items'] ?? array();
|
||||||
|
$checkbox_list_context = array( 'items' => $items );
|
||||||
|
$on_change = $context['on_change'] ?? '';
|
||||||
|
$namespace = wp_json_encode( array( 'namespace' => 'woocommerce/product-filter-checkbox-list' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP );
|
||||||
|
|
||||||
|
$classes = array(
|
||||||
|
'has-option-element-border-color' => $this->get_color_attribute_value( 'optionElementBorder', $attributes ),
|
||||||
|
'has-option-element-selected-color' => $this->get_color_attribute_value( 'optionElementSelected', $attributes ),
|
||||||
|
'has-option-element-color' => $this->get_color_attribute_value( 'optionElement', $attributes ),
|
||||||
|
);
|
||||||
|
$classes = array_filter( $classes );
|
||||||
|
|
||||||
|
$styles = array(
|
||||||
|
'--wc-product-filter-checkbox-list-option-element-border' => $this->get_color_attribute_value( 'optionElementBorder', $attributes ),
|
||||||
|
'--wc-product-filter-checkbox-list-option-element-selected' => $this->get_color_attribute_value( 'optionElementSelected', $attributes ),
|
||||||
|
'--wc-product-filter-checkbox-list-option-element' => $this->get_color_attribute_value( 'optionElement', $attributes ),
|
||||||
|
);
|
||||||
|
$style = array_reduce(
|
||||||
|
array_keys( $styles ),
|
||||||
|
function ( $acc, $key ) use ( $styles ) {
|
||||||
|
if ( $styles[ $key ] ) {
|
||||||
|
return $acc . "{$key}: var( --wp--preset--color--{$styles[$key]} );";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$checked_items = array_filter(
|
||||||
|
$items,
|
||||||
|
function ( $item ) {
|
||||||
|
return $item['selected'];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
$show_initially = $context['show_initially'] ?? 15;
|
||||||
|
$remaining_initial_unchecked = count( $checked_items ) > $show_initially ? count( $checked_items ) : $show_initially - count( $checked_items );
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
$wrapper_attributes = array(
|
||||||
|
'data-wc-interactive' => esc_attr( $namespace ),
|
||||||
|
'data-wc-context' => wp_json_encode( $checkbox_list_context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
|
||||||
|
'class' => implode( ' ', array_keys( $classes ) ),
|
||||||
|
'style' => esc_attr( $style ),
|
||||||
|
);
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div <?php echo get_block_wrapper_attributes( $wrapper_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
|
||||||
|
<ul class="wc-block-product-filter-checkbox-list__list" aria-label="<?php echo esc_attr__( 'Filter Options', 'woocommerce' ); ?>">
|
||||||
|
<?php foreach ( $items as $item ) { ?>
|
||||||
|
<?php
|
||||||
|
$item['id'] = $item['id'] ?? uniqid( 'checkbox-' );
|
||||||
|
// translators: %s: checkbox label.
|
||||||
|
$i18n_label = sprintf( __( 'Checkbox: %s', 'woocommerce' ), $item['aria_label'] ?? '' );
|
||||||
|
?>
|
||||||
|
<li
|
||||||
|
data-wc-key="<?php echo esc_attr( $item['id'] ); ?>"
|
||||||
|
<?php
|
||||||
|
if ( ! $item['selected'] ) :
|
||||||
|
if ( $count >= $remaining_initial_unchecked ) :
|
||||||
|
?>
|
||||||
|
class="wc-block-product-filter-checkbox-list__item hidden"
|
||||||
|
data-wc-class--hidden="!context.showAll"
|
||||||
|
<?php else : ?>
|
||||||
|
<?php ++$count; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
class="wc-block-product-filter-checkbox-list__item"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="wc-block-product-filter-checkbox-list__label"
|
||||||
|
for="<?php echo esc_attr( $item['id'] ); ?>"
|
||||||
|
>
|
||||||
|
<span class="wc-block-product-filter-checkbox-list__input-wrapper">
|
||||||
|
<input
|
||||||
|
id="<?php echo esc_attr( $item['id'] ); ?>"
|
||||||
|
class="wc-block-product-filter-checkbox-list__input"
|
||||||
|
type="checkbox"
|
||||||
|
aria-invalid="false"
|
||||||
|
aria-label="<?php echo esc_attr( $i18n_label ); ?>"
|
||||||
|
data-wc-on--change--select-item="actions.selectCheckboxItem"
|
||||||
|
data-wc-on--change--parent-action="<?php echo esc_attr( $on_change ); ?>"
|
||||||
|
value="<?php echo esc_attr( $item['value'] ); ?>"
|
||||||
|
<?php checked( $item['selected'], 1 ); ?>
|
||||||
|
>
|
||||||
|
<svg class="wc-block-product-filter-checkbox-list__mark" viewBox="0 0 10 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9.25 1.19922L3.75 6.69922L1 3.94922" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="wc-block-product-filter-checkbox-list__text">
|
||||||
|
<?php echo wp_kses_post( $item['label'] ); ?>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<?php } ?>
|
||||||
|
</ul>
|
||||||
|
<?php if ( count( $items ) > $show_initially ) : ?>
|
||||||
|
<span
|
||||||
|
role="button"
|
||||||
|
class="wc-block-product-filter-checkbox-list__show-more"
|
||||||
|
data-wc-class--hidden="context.showAll"
|
||||||
|
data-wc-on--click="actions.showAllItems"
|
||||||
|
>
|
||||||
|
<small role="presentation"><?php echo esc_html__( 'Show more...', 'woocommerce' ); ?></small>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the color value from the color attributes.
|
||||||
|
*
|
||||||
|
* @param string $key The key of the color attribute.
|
||||||
|
* @param array $attributes The block attributes.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function get_color_attribute_value( $key, $attributes ) {
|
||||||
|
if ( $attributes[ $key ] ) {
|
||||||
|
return $attributes[ $key ];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $attributes[ 'custom' . ucfirst( $key ) ] ) {
|
||||||
|
return $attributes[ 'custom' . ucfirst( $key ) ];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
declare( strict_types = 1 );
|
||||||
|
|
||||||
|
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Product Filter: Chips Block.
|
||||||
|
*/
|
||||||
|
final class ProductFilterChips extends AbstractBlock {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block name.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $block_name = 'product-filter-chips';
|
||||||
|
}
|
|
@ -38,14 +38,14 @@ final class ProductFilterClearButton extends AbstractBlock {
|
||||||
|
|
||||||
$wrapper_attributes = get_block_wrapper_attributes(
|
$wrapper_attributes = get_block_wrapper_attributes(
|
||||||
array(
|
array(
|
||||||
'data-wc-bind--hidden' => '!context.hasSelectedFilter',
|
'data-wc-bind--hidden' => '!context.hasSelectedFilters',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
$p = new \WP_HTML_Tag_Processor( $content );
|
$p = new \WP_HTML_Tag_Processor( $content );
|
||||||
|
|
||||||
if ( $p->next_tag( array( 'class_name' => 'wp-block-button__link' ) ) ) {
|
if ( $p->next_tag( array( 'class_name' => 'wp-block-button__link' ) ) ) {
|
||||||
$p->set_attribute( 'data-wc-on--click', 'actions.clear' );
|
$p->set_attribute( 'data-wc-on--click', 'actions.clearFilters' );
|
||||||
|
|
||||||
$style = $p->get_attribute( 'style' );
|
$style = $p->get_attribute( 'style' );
|
||||||
$p->set_attribute( 'style', 'outline:none;' . $style );
|
$p->set_attribute( 'style', 'outline:none;' . $style );
|
||||||
|
|
|
@ -23,6 +23,23 @@ class ProductFilters extends AbstractBlock {
|
||||||
return array( 'postId' );
|
return array( 'postId' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra data passed through from server to client for block.
|
||||||
|
*
|
||||||
|
* @param array $attributes Any attributes that currently are available from the block.
|
||||||
|
* Note, this will be empty in the editor context when the block is
|
||||||
|
* not in the post content on editor load.
|
||||||
|
*/
|
||||||
|
protected function enqueue_data( array $attributes = array() ) {
|
||||||
|
global $pagenow;
|
||||||
|
parent::enqueue_data( $attributes );
|
||||||
|
|
||||||
|
$this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme() );
|
||||||
|
$this->asset_data_registry->add( 'isProductArchive', is_shop() || is_product_taxonomy() );
|
||||||
|
$this->asset_data_registry->add( 'isSiteEditor', 'site-editor.php' === $pagenow );
|
||||||
|
$this->asset_data_registry->add( 'isWidgetEditor', 'widgets.php' === $pagenow || 'customize.php' === $pagenow );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the dialog content.
|
* Return the dialog content.
|
||||||
*
|
*
|
||||||
|
@ -116,12 +133,10 @@ class ProductFilters extends AbstractBlock {
|
||||||
* @return string Rendered block type output.
|
* @return string Rendered block type output.
|
||||||
*/
|
*/
|
||||||
protected function render( $attributes, $content, $block ) {
|
protected function render( $attributes, $content, $block ) {
|
||||||
$html = $content;
|
$tags = new \WP_HTML_Tag_Processor( $content );
|
||||||
$p = new \WP_HTML_Tag_Processor( $html );
|
if ( $tags->next_tag() ) {
|
||||||
|
$tags->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/' . $this->block_name ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
|
||||||
if ( $p->next_tag() ) {
|
$tags->set_attribute(
|
||||||
$p->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-filters' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) );
|
|
||||||
$p->set_attribute(
|
|
||||||
'data-wc-context',
|
'data-wc-context',
|
||||||
wp_json_encode(
|
wp_json_encode(
|
||||||
array(
|
array(
|
||||||
|
@ -131,13 +146,29 @@ class ProductFilters extends AbstractBlock {
|
||||||
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
|
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$html = $p->get_updated_html();
|
$tags->set_attribute( 'data-wc-navigation-id', $this->generate_navigation_id( $block ) );
|
||||||
|
|
||||||
|
if (
|
||||||
|
'always' === $attributes['overlay'] ||
|
||||||
|
( 'mobile' === $attributes['overlay'] && wp_is_mobile() )
|
||||||
|
) {
|
||||||
|
return $this->inject_dialog( $tags->get_updated_html(), $this->render_dialog() );
|
||||||
}
|
}
|
||||||
|
|
||||||
$dialog_html = $this->render_dialog();
|
return $tags->get_updated_html();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$html = $this->inject_dialog( $html, $dialog_html );
|
/**
|
||||||
|
* Generate a unique navigation ID for the block.
|
||||||
return $html;
|
*
|
||||||
|
* @param mixed $block - Block instance.
|
||||||
|
* @return string - Unique navigation ID.
|
||||||
|
*/
|
||||||
|
private function generate_navigation_id( $block ) {
|
||||||
|
return sprintf(
|
||||||
|
'wc-product-filters-%s',
|
||||||
|
md5( wp_json_encode( $block->parsed_block['innerBlocks'] ) )
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -404,7 +404,6 @@ final class BlockTypesController {
|
||||||
// Update plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md
|
// Update plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md
|
||||||
// when modifying this list.
|
// when modifying this list.
|
||||||
if ( Features::is_enabled( 'experimental-blocks' ) ) {
|
if ( Features::is_enabled( 'experimental-blocks' ) ) {
|
||||||
$block_types[] = 'ProductFilter';
|
|
||||||
$block_types[] = 'ProductFilters';
|
$block_types[] = 'ProductFilters';
|
||||||
$block_types[] = 'ProductFiltersOverlay';
|
$block_types[] = 'ProductFiltersOverlay';
|
||||||
$block_types[] = 'ProductFiltersOverlayNavigation';
|
$block_types[] = 'ProductFiltersOverlayNavigation';
|
||||||
|
@ -414,6 +413,8 @@ final class BlockTypesController {
|
||||||
$block_types[] = 'ProductFilterRating';
|
$block_types[] = 'ProductFilterRating';
|
||||||
$block_types[] = 'ProductFilterActive';
|
$block_types[] = 'ProductFilterActive';
|
||||||
$block_types[] = 'ProductFilterClearButton';
|
$block_types[] = 'ProductFilterClearButton';
|
||||||
|
$block_types[] = 'ProductFilterCheckboxList';
|
||||||
|
$block_types[] = 'ProductFilterChips';
|
||||||
$block_types[] = 'OrderConfirmation\CreateAccount';
|
$block_types[] = 'OrderConfirmation\CreateAccount';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,79 +1,28 @@
|
||||||
<!-- wp:woocommerce/product-filters -->
|
<!-- wp:woocommerce/product-filters -->
|
||||||
<!-- wp:woocommerce/product-filters-overlay-navigation {"align":"left","triggerType":"open-overlay","lock":{"move":true,"remove":true}} -->
|
<div class="wp-block-woocommerce-product-filters wc-block-product-filters">
|
||||||
<div class="wp-block-woocommerce-product-filters-overlay-navigation alignleft wc-block-product-filters-overlay-navigation"></div>
|
<!-- wp:heading {"level":2} -->
|
||||||
<!-- /wp:woocommerce/product-filters-overlay-navigation -->
|
<h2 class="wp-block-heading">Filters</h2>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
<div class="wp-block-woocommerce-product-filters wc-block-product-filters"><!-- wp:heading {"level":3,"style":{"typography":{"fontSize":"24px"}}} -->
|
<!-- wp:woocommerce/product-filter-active {"displayStyle":"chips"} /-->
|
||||||
<h3 class="wp-block-heading" style="font-size:24px">Filters</h3>
|
|
||||||
<!-- /wp:heading -->
|
|
||||||
|
|
||||||
<!-- wp:woocommerce/product-filter {"filterType":"active-filters","heading":"Active"} -->
|
<!-- wp:pattern {"slug":"woocommerce/default-attribute-filter"} /-->
|
||||||
<!-- wp:heading {"level":3} -->
|
|
||||||
<h3 class="wp-block-heading">Active</h3>
|
|
||||||
<!-- /wp:heading -->
|
|
||||||
|
|
||||||
<!-- wp:woocommerce/product-filter-active {"lock":{"remove":true}} /-->
|
<!-- wp:buttons {"layout":{"type":"flex"}} -->
|
||||||
<!-- /wp:woocommerce/product-filter -->
|
<div class="wp-block-buttons">
|
||||||
|
<!-- wp:button {"className":"wc-block-product-filters__apply-button","style":{"border":{"width":"0px","style":"none"},"typography":{"textDecoration":"none"},"outline":"none","fontSize":"medium"}} -->
|
||||||
<!-- wp:woocommerce/product-filter {"filterType":"price-filter","heading":"Price"} -->
|
<div
|
||||||
<!-- wp:group {"metadata":{"name":"Header"},"style":{"spacing":{"blockGap":"0"}},"layout":{"type":"flex","flexWrap":"nowrap"}} -->
|
class="wp-block-button wc-block-product-filters__apply-button"
|
||||||
<div class="wp-block-group"><!-- wp:heading {"level":3} -->
|
style="text-decoration: none"
|
||||||
<h3 class="wp-block-heading">Price</h3>
|
>
|
||||||
<!-- /wp:heading -->
|
<a
|
||||||
|
class="wp-block-button__link wp-element-button"
|
||||||
<!-- wp:woocommerce/product-filter-clear-button {"lock":{"remove":true,"move":false}} -->
|
style="border-style: none; border-width: 0px"
|
||||||
<!-- wp:buttons {"layout":{"type":"flex"}} -->
|
>Apply</a
|
||||||
<div class="wp-block-buttons"><!-- wp:button {"className":"wc-block-product-filter-clear-button is-style-outline","style":{"border":{"width":"0px","style":"none"},"typography":{"textDecoration":"underline"},"outline":"none","fontSize":"medium"}} -->
|
>
|
||||||
<div class="wp-block-button wc-block-product-filter-clear-button is-style-outline" style="text-decoration:underline"><a class="wp-block-button__link wp-element-button" style="border-style:none;border-width:0px">Clear</a></div>
|
</div>
|
||||||
<!-- /wp:button --></div>
|
<!-- /wp:button -->
|
||||||
<!-- /wp:buttons -->
|
</div>
|
||||||
<!-- /wp:woocommerce/product-filter-clear-button --></div>
|
<!-- /wp:buttons -->
|
||||||
<!-- /wp:group -->
|
</div>
|
||||||
|
|
||||||
<!-- wp:woocommerce/product-filter-price {"lock":{"remove":true}} /-->
|
|
||||||
<!-- /wp:woocommerce/product-filter -->
|
|
||||||
|
|
||||||
<!-- wp:woocommerce/product-filter {"filterType":"stock-filter","heading":"Status"} -->
|
|
||||||
<!-- wp:group {"metadata":{"name":"Header"},"style":{"spacing":{"blockGap":"0"}},"layout":{"type":"flex","flexWrap":"nowrap"}} -->
|
|
||||||
<div class="wp-block-group"><!-- wp:heading {"level":3} -->
|
|
||||||
<h3 class="wp-block-heading">Status</h3>
|
|
||||||
<!-- /wp:heading -->
|
|
||||||
|
|
||||||
<!-- wp:woocommerce/product-filter-clear-button {"lock":{"remove":true,"move":false}} -->
|
|
||||||
<!-- wp:buttons {"layout":{"type":"flex"}} -->
|
|
||||||
<div class="wp-block-buttons"><!-- wp:button {"className":"wc-block-product-filter-clear-button is-style-outline","style":{"border":{"width":"0px","style":"none"},"typography":{"textDecoration":"underline"},"outline":"none","fontSize":"medium"}} -->
|
|
||||||
<div class="wp-block-button wc-block-product-filter-clear-button is-style-outline" style="text-decoration:underline"><a class="wp-block-button__link wp-element-button" style="border-style:none;border-width:0px">Clear</a></div>
|
|
||||||
<!-- /wp:button --></div>
|
|
||||||
<!-- /wp:buttons -->
|
|
||||||
<!-- /wp:woocommerce/product-filter-clear-button --></div>
|
|
||||||
<!-- /wp:group -->
|
|
||||||
|
|
||||||
<!-- wp:woocommerce/product-filter-stock-status {"lock":{"remove":true}} /-->
|
|
||||||
<!-- /wp:woocommerce/product-filter -->
|
|
||||||
|
|
||||||
<!-- wp:pattern {"slug":"woocommerce/default-attribute-filter"} /-->
|
|
||||||
|
|
||||||
<!-- wp:woocommerce/product-filter {"filterType":"rating-filter","heading":"Rating"} -->
|
|
||||||
<!-- wp:group {"metadata":{"name":"Header"},"style":{"spacing":{"blockGap":"0"}},"layout":{"type":"flex","flexWrap":"nowrap"}} -->
|
|
||||||
<div class="wp-block-group"><!-- wp:heading {"level":3} -->
|
|
||||||
<h3 class="wp-block-heading">Rating</h3>
|
|
||||||
<!-- /wp:heading -->
|
|
||||||
|
|
||||||
<!-- wp:woocommerce/product-filter-clear-button {"lock":{"remove":true,"move":false}} -->
|
|
||||||
<!-- wp:buttons {"layout":{"type":"flex"}} -->
|
|
||||||
<div class="wp-block-buttons"><!-- wp:button {"className":"wc-block-product-filter-clear-button is-style-outline","style":{"border":{"width":"0px","style":"none"},"typography":{"textDecoration":"underline"},"outline":"none","fontSize":"medium"}} -->
|
|
||||||
<div class="wp-block-button wc-block-product-filter-clear-button is-style-outline" style="text-decoration:underline"><a class="wp-block-button__link wp-element-button" style="border-style:none;border-width:0px">Clear</a></div>
|
|
||||||
<!-- /wp:button --></div>
|
|
||||||
<!-- /wp:buttons -->
|
|
||||||
<!-- /wp:woocommerce/product-filter-clear-button --></div>
|
|
||||||
<!-- /wp:group -->
|
|
||||||
|
|
||||||
<!-- wp:woocommerce/product-filter-rating {"lock":{"remove":true}} /-->
|
|
||||||
<!-- /wp:woocommerce/product-filter -->
|
|
||||||
|
|
||||||
<!-- wp:buttons {"layout":{"type":"flex"}} -->
|
|
||||||
<div class="wp-block-buttons"><!-- wp:button {"className":"wc-block-product-filters__apply-button","style":{"border":{"width":"0px","style":"none"},"typography":{"textDecoration":"none"},"outline":"none","fontSize":"medium"}} -->
|
|
||||||
<div class="wp-block-button wc-block-product-filters__apply-button" style="text-decoration:none"><a class="wp-block-button__link wp-element-button" style="border-style:none;border-width:0px">Apply</a></div>
|
|
||||||
<!-- /wp:button --></div>
|
|
||||||
<!-- /wp:buttons --></div>
|
|
||||||
<!-- /wp:woocommerce/product-filters -->
|
<!-- /wp:woocommerce/product-filters -->
|
||||||
|
|
Loading…
Reference in New Issue