From 93053f39ed0da7f3497613661e83d991f53fd09d Mon Sep 17 00:00:00 2001 From: Tung Du Date: Sat, 14 Sep 2024 00:04:06 +0700 Subject: [PATCH] [Experimental] Product Filters: New and improved blocks structure (#51096) --- .../components/initial-disabled/index.tsx | 26 ++ .../components/initial-disabled/style.scss | 17 + .../components/notice/index.tsx | 28 ++ .../components/notice/style.scss | 17 + .../js/blocks/product-filters/constants.ts | 6 + .../assets/js/blocks/product-filters/edit.tsx | 43 +-- .../js/blocks/product-filters/frontend.tsx | 48 ++- .../inner-blocks/active-filters/block.json | 9 +- .../inner-blocks/active-filters/frontend.ts | 2 +- .../inner-blocks/active-filters/index.tsx | 4 +- .../inner-blocks/attribute-filter/block.json | 14 +- .../components/attribute-dropdown.tsx | 29 -- .../components/attribute-select-controls.tsx | 86 ----- .../components/checkbox-list-editor.tsx | 68 ---- .../components/placeholder.tsx | 55 --- .../attribute-filter/constants.ts | 92 +++-- .../inner-blocks/attribute-filter/edit.tsx | 266 +++++++++------ .../inner-blocks/attribute-filter/editor.scss | 8 - .../inner-blocks/attribute-filter/frontend.ts | 9 +- .../inner-blocks/attribute-filter/index.tsx | 29 +- .../{components => }/inspector.tsx | 129 +++---- .../inner-blocks/attribute-filter/save.tsx | 12 + .../inner-blocks/checkbox-list/block.json | 50 +++ .../inner-blocks/checkbox-list/edit.tsx | 223 ++++++++++++ .../inner-blocks/checkbox-list/editor.scss | 10 + .../inner-blocks/checkbox-list/frontend.ts | 43 +++ .../inner-blocks/checkbox-list/index.tsx | 20 ++ .../inner-blocks/checkbox-list/style.scss | 86 +++++ .../inner-blocks/checkbox-list/types.ts | 36 ++ .../inner-blocks/chips/block.json | 22 ++ .../inner-blocks/chips/edit.tsx | 15 + .../inner-blocks/chips/index.tsx | 20 ++ .../inner-blocks/chips/style.scss | 3 + .../{product-filter => chips}/types.ts | 6 +- .../inner-blocks/clear-button/frontend.ts | 7 +- .../inner-blocks/price-filter/frontend.ts | 2 +- .../product-filter/block-variations.tsx | 129 ------- .../inner-blocks/product-filter/block.json | 47 --- .../product-filter/components/warning.tsx | 36 -- .../inner-blocks/product-filter/constants.ts | 8 - .../inner-blocks/product-filter/edit.tsx | 113 ------- .../inner-blocks/product-filter/editor.scss | 5 - .../inner-blocks/product-filter/frontend.ts | 42 --- .../inner-blocks/product-filter/index.tsx | 73 ---- .../inner-blocks/product-filter/save.tsx | 8 - .../inner-blocks/product-filter/utils.ts | 18 - .../inner-blocks/rating-filter/frontend.ts | 2 +- .../inner-blocks/stock-filter/frontend.ts | 2 +- .../assets/js/blocks/product-filters/types.ts | 22 +- .../assets/js/blocks/product-filters/utils.ts | 35 ++ .../assets/js/icons/index.js | 1 + .../assets/js/icons/library/check-mark.tsx | 24 ++ .../woocommerce-blocks/bin/webpack-entries.js | 12 +- ..._archive-product_active-filters.handlebars | 9 +- ...rchive-product_attribute-filter.handlebars | 24 +- .../active-filters.block_theme.spec.ts | 99 ------ .../attribute-filter.block_theme.spec.ts | 316 ------------------ .../filter-blocks/basic.block_theme.spec.ts | 54 --- ...-overlay-template-part.block_theme.spec.ts | 18 - ...active-filter-frontend.block_theme.spec.ts | 115 +++++++ ...tribute-filter-editor.block_theme.spec.ts} | 12 +- ...ribute-filter-frontend.block_theme.spec.ts | 171 ++++++++++ ...price-filter-frontend.block_theme.spec.ts} | 2 +- ...-filters-template-part.block_theme.spec.ts | 16 +- .../product-filters.block_theme.spec.ts | 87 +---- ...ating-filter-frontend.block_theme.spec.ts} | 2 +- ...stock-status-frontend.block_theme.spec.ts} | 2 +- .../try-new-improved-filter-blocks-structure | 5 + .../src/Blocks/BlockTypes/ProductFilter.php | 157 --------- .../Blocks/BlockTypes/ProductFilterActive.php | 1 - .../BlockTypes/ProductFilterAttribute.php | 212 +++++------- .../BlockTypes/ProductFilterCheckboxList.php | 155 +++++++++ .../Blocks/BlockTypes/ProductFilterChips.php | 17 + .../BlockTypes/ProductFilterClearButton.php | 4 +- .../src/Blocks/BlockTypes/ProductFilters.php | 55 ++- .../src/Blocks/BlockTypesController.php | 3 +- .../templates/parts/product-filters.html | 97 ++---- 77 files changed, 1763 insertions(+), 1987 deletions(-) create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/components/initial-disabled/index.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/components/initial-disabled/style.scss create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/components/notice/index.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/components/notice/style.scss delete mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/attribute-dropdown.tsx delete mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/attribute-select-controls.tsx delete mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/checkbox-list-editor.tsx delete mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/placeholder.tsx delete mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/editor.scss rename plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/{components => }/inspector.tsx (68%) create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/save.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/block.json create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/edit.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/editor.scss create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/frontend.ts create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/index.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/style.scss create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/types.ts create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/index.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss rename plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/{product-filter => chips}/types.ts (59%) delete mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block-variations.tsx delete mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block.json delete mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/components/warning.tsx delete mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/constants.ts delete mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/edit.tsx delete mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/editor.scss delete mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/frontend.ts delete mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/index.tsx delete mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/save.tsx delete mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/utils.ts create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/utils.ts create mode 100644 plugins/woocommerce-blocks/assets/js/icons/library/check-mark.tsx delete mode 100644 plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/active-filters.block_theme.spec.ts delete mode 100644 plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/attribute-filter.block_theme.spec.ts delete mode 100644 plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/basic.block_theme.spec.ts create mode 100644 plugins/woocommerce-blocks/tests/e2e/tests/product-filters/active-filter-frontend.block_theme.spec.ts rename plugins/woocommerce-blocks/tests/e2e/tests/product-filters/{attribute-filter.block_theme.spec.ts => attribute-filter-editor.block_theme.spec.ts} (91%) create mode 100644 plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-frontend.block_theme.spec.ts rename plugins/woocommerce-blocks/tests/e2e/tests/{filter-blocks/price-filter.block_theme.spec.ts => product-filters/price-filter-frontend.block_theme.spec.ts} (99%) rename plugins/woocommerce-blocks/tests/e2e/tests/{filter-blocks/rating-filter.block_theme.spec.ts => product-filters/rating-filter-frontend.block_theme.spec.ts} (97%) rename plugins/woocommerce-blocks/tests/e2e/tests/{filter-blocks/stock-status.block_theme.spec.ts => product-filters/stock-status-frontend.block_theme.spec.ts} (98%) create mode 100644 plugins/woocommerce/changelog/try-new-improved-filter-blocks-structure delete mode 100644 plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php create mode 100644 plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php create mode 100644 plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterChips.php diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/components/initial-disabled/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/components/initial-disabled/index.tsx new file mode 100644 index 00000000000..81ebaa2368e --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/components/initial-disabled/index.tsx @@ -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 => ( +
+
+ { children } +
+); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/components/initial-disabled/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/components/initial-disabled/style.scss new file mode 100644 index 00000000000..9c18798a936 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/components/initial-disabled/style.scss @@ -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; + } + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/components/notice/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/components/notice/index.tsx new file mode 100644 index 00000000000..f8cc250fc10 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/components/notice/index.tsx @@ -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 } ) => ( +
+ +
+ { children } +
+
+); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/components/notice/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/components/notice/style.scss new file mode 100644 index 00000000000..97985ee80e9 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/components/notice/style.scss @@ -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; + } + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/constants.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/constants.ts index 12220a2bf0c..5374588fd53 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/constants.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/constants.ts @@ -3,3 +3,9 @@ export const BlockOverlayAttribute = { MOBILE: 'mobile', ALWAYS: 'always', } as const; + +export const EXCLUDED_BLOCKS = [ + 'woocommerce/product-filter-attribute', + 'woocommerce/product-collection', + 'core/query', +]; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/edit.tsx index 09d6521991e..0d8c773e3cd 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/edit.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { getSetting } from '@woocommerce/settings'; -import { AttributeSetting } from '@woocommerce/types'; import { InnerBlocks, InspectorControls, @@ -37,10 +36,6 @@ import './editor.scss'; import { type BlockAttributes } from './types'; import { BlockOverlayAttribute } from './constants'; -const defaultAttribute = getSetting< AttributeSetting >( - 'defaultProductFilterAttribute' -); - const TEMPLATE: InnerBlockTemplate[] = [ [ 'core/heading', @@ -50,42 +45,8 @@ const TEMPLATE: InnerBlockTemplate[] = [ content: __( 'Filters', 'woocommerce' ), }, ], - [ - 'woocommerce/product-filter', - { - 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' ), - }, - ], + [ 'woocommerce/product-filter-active' ], + [ 'woocommerce/product-filter-attribute' ], [ 'core/buttons', { layout: { type: 'flex' } }, diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.tsx index dab9ef6c6c9..4b3085a291e 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.tsx @@ -1,7 +1,12 @@ /** * 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 { isDialogOpen: boolean; @@ -11,7 +16,7 @@ export interface ProductFiltersContext { const getContext = ( ns?: string ) => getContextFn< ProductFiltersContext >( ns ); -const productFilters = { +store( 'woocommerce/product-filters', { state: { isDialogOpen: () => { const context = getContext(); @@ -36,8 +41,41 @@ const productFilters = { }, }, 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 ); +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/block.json index 6d002248202..2754afa721a 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/block.json +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "name": "woocommerce/product-filter-active", "version": "1.0.0", - "title": "Filter Options", + "title": "Active (Experimental)", "description": "Display the currently active filters.", "category": "woocommerce", "keywords": [ @@ -11,11 +11,10 @@ "textdomain": "woocommerce", "apiVersion": 3, "ancestor": [ - "woocommerce/product-filter" + "woocommerce/product-filters" ], "supports": { "interactivity": true, - "inserter": false, "color": { "text": true, "background": false @@ -27,7 +26,7 @@ "attributes": { "displayStyle": { "type": "string", - "default": "list" + "default": "chips" } } -} \ No newline at end of file +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/frontend.ts index 0b462692f15..69cab9093e1 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/frontend.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/frontend.ts @@ -6,7 +6,7 @@ import { store, getContext } from '@woocommerce/interactivity'; /** * Internal dependencies */ -import { navigate } from '../product-filter/frontend'; +import { navigate } from '../../frontend'; type ActiveFiltersContext = { queryId: number; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/index.tsx index c1285586709..198cc46ce78 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/active-filters/index.tsx @@ -3,7 +3,7 @@ */ import { registerBlockType } from '@wordpress/blocks'; import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings'; -import { productFilterOptions } from '@woocommerce/icons'; +import { productFilterActive } from '@woocommerce/icons'; /** * Internal dependencies @@ -14,7 +14,7 @@ import './style.scss'; if ( isExperimentalBlocksEnabled() ) { registerBlockType( metadata, { - icon: productFilterOptions, + icon: productFilterActive, edit: Edit, } ); } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/block.json index 2cc947376f2..fa9d859c2ab 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/block.json +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "name": "woocommerce/product-filter-attribute", "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.", "category": "woocommerce", "keywords": [ @@ -11,11 +11,10 @@ "textdomain": "woocommerce", "apiVersion": 3, "ancestor": [ - "woocommerce/product-filter" + "woocommerce/product-filters" ], "supports": { "interactivity": true, - "inserter": false, "color": { "text": true, "background": false, @@ -78,7 +77,7 @@ }, "displayStyle": { "type": "string", - "default": "list" + "default": "woocommerce/product-filter-checkbox-list" }, "selectType": { "type": "string", @@ -100,5 +99,10 @@ "type": "boolean", "default":true } + }, + "example": { + "attributes": { + "isPreview": true + } } -} \ No newline at end of file +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/attribute-dropdown.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/attribute-dropdown.tsx deleted file mode 100644 index 933ab2e111a..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/attribute-dropdown.tsx +++ /dev/null @@ -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 ( -
- - -
- ); -}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/attribute-select-controls.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/attribute-select-controls.tsx deleted file mode 100644 index 8055d2a2dce..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/attribute-select-controls.tsx +++ /dev/null @@ -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 ( - id === attributeId ) } - onChange={ onChange } - messages={ messages } - isSingle - isCompact={ isCompact } - /> - ); -}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/checkbox-list-editor.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/checkbox-list-editor.tsx deleted file mode 100644 index c06e5ae30bb..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/checkbox-list-editor.tsx +++ /dev/null @@ -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 ( -
-
    - { ( isLongList ? items.slice( 0, threshold ) : items ).map( - ( item, index ) => ( -
  • - -
  • - ) - ) } -
- { isLongList && ( - - { __( 'Show more…', 'woocommerce' ) } - - ) } -
- ); -}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/placeholder.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/placeholder.tsx deleted file mode 100644 index bee0d130c28..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/placeholder.tsx +++ /dev/null @@ -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; -} ) => ( - } - label={ __( 'Filter by Attribute', 'woocommerce' ) } - instructions={ __( - 'Enable customers to filter the product grid by selecting one or more attributes, such as color.', - 'woocommerce' - ) } - > - { children } - -); - -export const NoAttributesPlaceholder = () => ( - -

- { __( - "Attributes are needed for filtering your products. You haven't created any attributes yet.", - 'woocommerce' - ) } -

- - -
-); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/constants.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/constants.ts index 8c47d8c30e4..e535dfebc16 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/constants.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/constants.ts @@ -5,49 +5,71 @@ import { __ } from '@wordpress/i18n'; export const attributeOptionsPreview = [ { - id: 23, - name: __( 'Blue', 'woocommerce' ), - slug: 'blue', - attr_slug: 'blue', - description: '', - parent: 0, - count: 4, + label: __( 'Blue', 'woocommerce' ), + value: 'blue', + rawData: { + id: 23, + name: __( 'Blue', 'woocommerce' ), + slug: 'blue', + attr_slug: 'blue', + description: '', + parent: 0, + count: 4, + }, }, { - id: 29, - name: __( 'Gray', 'woocommerce' ), - slug: 'gray', - attr_slug: 'gray', - description: '', - parent: 0, - count: 3, + label: __( 'Gray', 'woocommerce' ), + value: 'gray', + selected: true, + rawData: { + id: 29, + name: __( 'Gray', 'woocommerce' ), + slug: 'gray', + attr_slug: 'gray', + description: '', + parent: 0, + count: 3, + }, }, { - id: 24, - name: __( 'Green', 'woocommerce' ), - slug: 'green', - attr_slug: 'green', - description: '', - parent: 0, - count: 3, + label: __( 'Green', 'woocommerce' ), + value: 'green', + rawData: { + id: 24, + name: __( 'Green', 'woocommerce' ), + slug: 'green', + attr_slug: 'green', + description: '', + parent: 0, + count: 3, + }, }, { - id: 25, - name: __( 'Red', 'woocommerce' ), - slug: 'red', - attr_slug: 'red', - description: '', - parent: 0, - count: 4, + label: __( 'Red', 'woocommerce' ), + value: 'red', + selected: true, + rawData: { + id: 25, + name: __( 'Red', 'woocommerce' ), + slug: 'red', + attr_slug: 'red', + description: '', + parent: 0, + count: 4, + }, }, { - id: 30, - name: __( 'Yellow', 'woocommerce' ), - slug: 'yellow', - attr_slug: 'yellow', - description: '', - parent: 0, - count: 1, + label: __( 'Yellow', 'woocommerce' ), + value: 'yellow', + rawData: { + id: 30, + name: __( 'Yellow', 'woocommerce' ), + slug: 'yellow', + attr_slug: 'yellow', + description: '', + parent: 0, + count: 1, + }, }, ]; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx index 9fb37b004b1..5f709d0089d 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/edit.tsx @@ -5,29 +5,34 @@ import { useCollection, useCollectionData, } from '@woocommerce/base-context/hooks'; -import { getSetting } from '@woocommerce/settings'; import { AttributeSetting, AttributeTerm, objectHasProp, } from '@woocommerce/types'; -import { useBlockProps } from '@wordpress/block-editor'; -import { Disabled, Notice, withSpokenMessages } from '@wordpress/components'; -import { useEffect, useState, useMemo } from '@wordpress/element'; +import { + useBlockProps, + useInnerBlocksProps, + BlockContextProvider, +} from '@wordpress/block-editor'; +import { withSpokenMessages } from '@wordpress/components'; +import { useEffect, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { getSetting } from '@woocommerce/settings'; /** * Internal dependencies */ -import { AttributeDropdown } from './components/attribute-dropdown'; -import { Preview as CheckboxListPreview } from './components/checkbox-list-editor'; -import { Inspector } from './components/inspector'; -import { NoAttributesPlaceholder } from './components/placeholder'; +import { Inspector } from './inspector'; import { attributeOptionsPreview } from './constants'; import './style.scss'; import { EditProps, isAttributeCounts } from './types'; 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', [] ); @@ -47,8 +52,10 @@ const Edit = ( props: EditProps ) => { const attributeObject = getAttributeFromId( attributeId ); const [ attributeOptions, setAttributeOptions ] = useState< - AttributeTerm[] + FilterOptionItem[] >( [] ); + const [ isOptionsLoading, setIsOptionsLoading ] = + useState< boolean >( true ); const { results: attributeTerms, isLoading: isTermsLoading } = useCollection< AttributeTerm >( { @@ -59,7 +66,7 @@ const Edit = ( props: EditProps ) => { query: { orderby: 'menu_order', hide_empty: hideEmpty }, } ); - const { results: filteredCounts, isLoading: isCountsLoading } = + const { results: filteredCounts, isLoading: isFilterCountsLoading } = useCollectionData( { queryAttribute: { taxonomy: attributeObject?.taxonomy || '', @@ -69,90 +76,137 @@ const Edit = ( props: EditProps ) => { isEditor: true, } ); - const isLoading = isTermsLoading || isCountsLoading; - useEffect( () => { + if ( isTermsLoading || isFilterCountsLoading ) return; + const termIdHasProducts = objectHasProp( filteredCounts, 'attribute_counts' ) && isAttributeCounts( filteredCounts.attribute_counts ) ? filteredCounts.attribute_counts.map( ( term ) => term.term ) : []; - if ( termIdHasProducts.length === 0 && hideEmpty ) - return setAttributeOptions( [] ); + if ( termIdHasProducts.length === 0 && hideEmpty ) { + setAttributeOptions( [] ); + } else { + setAttributeOptions( + attributeTerms + .filter( ( term ) => { + if ( hideEmpty ) + return termIdHasProducts.includes( term.id ); + return true; + } ) + .sort( ( a, b ) => { + switch ( sortOrder ) { + case 'name-asc': + return a.name > b.name ? 1 : -1; + case 'name-desc': + return a.name < b.name ? 1 : -1; + case 'count-asc': + return a.count > b.count ? 1 : -1; + case 'count-desc': + default: + return a.count < b.count ? 1 : -1; + } + } ) + .map( ( term, index ) => ( { + label: showCounts + ? `${ term.name } (${ term.count })` + : term.name, + value: term.id.toString(), + selected: index === 1, + rawData: term, + } ) ) + ); + } - setAttributeOptions( - attributeTerms - .filter( ( term ) => { - if ( hideEmpty ) - return termIdHasProducts.includes( term.id ); - return true; - } ) - .sort( ( a, b ) => { - switch ( sortOrder ) { - case 'name-asc': - return a.name > b.name ? 1 : -1; - case 'name-desc': - return a.name < b.name ? 1 : -1; - case 'count-asc': - return a.count > b.count ? 1 : -1; - case 'count-desc': - default: - return a.count < b.count ? 1 : -1; - } - } ) - ); - }, [ attributeTerms, filteredCounts, sortOrder, hideEmpty ] ); + setIsOptionsLoading( false ); + }, [ + showCounts, + attributeTerms, + filteredCounts, + sortOrder, + hideEmpty, + isTermsLoading, + isFilterCountsLoading, + ] ); - const Wrapper = ( { children }: { children: React.ReactNode } ) => ( -
- - { children } -
+ 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 loadingState = useMemo( () => { - return [ ...Array( 5 ) ].map( ( x, i ) => ( -
  • -   -
  • - ) ); - }, [] ); + const isLoading = + isTermsLoading || isFilterCountsLoading || isOptionsLoading; - if ( isPreview ) { - return ( - - - { - if ( showCounts ) - return `${ term.name } (${ term.count })`; - return term.name; - } ) } - /> - - - ); - } - - // Block rendering starts. if ( Object.keys( ATTRIBUTES ).length === 0 ) return ( - - - +
    + + +

    + { __( + "Attributes are needed for filtering your products. You haven't created any attributes yet.", + 'woocommerce' + ) } +

    +
    +
    ); if ( ! attributeId || ! attributeObject ) return ( - - +
    + +

    { __( 'Please select an attribute to use this filter!', @@ -160,22 +214,14 @@ const Edit = ( props: EditProps ) => { ) }

    - +
    ); - if ( isLoading ) + if ( ! isLoading && attributeTerms.length === 0 ) return ( - -
      - { loadingState } -
    -
    - ); - - if ( attributeTerms.length === 0 ) - return ( - - +
    + +

    { __( 'There are no products with the selected attributes.', @@ -183,30 +229,28 @@ const Edit = ( props: EditProps ) => { ) }

    - +
    ); return ( - - - { displayStyle === 'dropdown' ? ( - - ) : ( - { - if ( showCounts ) - return `${ term.name } (${ term.count })`; - return term.name; - } ) } - /> - ) } - - +
    + + + + { children } + + +
    ); }; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/editor.scss deleted file mode 100644 index 0580af4584f..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/editor.scss +++ /dev/null @@ -1,8 +0,0 @@ -.wp-block-woocommerce-product-filter-attribute__loading { - padding: 0; - - li { - @include placeholder(); - margin: 5px 0; - } -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/frontend.ts index 0f9eb935586..6170ececffb 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/frontend.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/frontend.ts @@ -8,7 +8,7 @@ import { HTMLElementEvent } from '@woocommerce/types'; /** * Internal dependencies */ -import { navigate } from '../product-filter/frontend'; +import { navigate } from '../../frontend'; type AttributeFilterContext = { attributeSlug: string; @@ -106,5 +106,12 @@ store( 'woocommerce/product-filter-attribute', { navigate( getUrl( selectedTerms, attributeSlug, queryType ) ); }, + + clearFilters: () => { + const { attributeSlug, queryType } = + getContext< ActiveAttributeFilterContext >(); + + navigate( getUrl( [], attributeSlug, queryType ) ); + }, }, } ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/index.tsx index 6c1c49b3fb7..66a1c806d0b 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/index.tsx @@ -2,18 +2,21 @@ * External dependencies */ import { isExperimentalBlocksEnabled } from '@woocommerce/block-settings'; -import { productFilterOptions } from '@woocommerce/icons'; +import { productFilterAttribute } from '@woocommerce/icons'; import { getSetting } from '@woocommerce/settings'; -import { registerBlockType } from '@wordpress/blocks'; import { AttributeSetting } from '@woocommerce/types'; +import { registerBlockType } from '@wordpress/blocks'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ import metadata from './block.json'; import Edit from './edit'; +import Save from './save'; import './style.scss'; +const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] ); if ( isExperimentalBlocksEnabled() ) { const defaultAttribute = getSetting< AttributeSetting >( 'defaultProductFilterAttribute' @@ -21,7 +24,7 @@ if ( isExperimentalBlocksEnabled() ) { registerBlockType( metadata, { edit: Edit, - icon: productFilterOptions, + icon: productFilterAttribute, attributes: { ...metadata.attributes, attributeId: { @@ -29,5 +32,25 @@ if ( isExperimentalBlocksEnabled() ) { 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, + }; + } ), } ); } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/inspector.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/inspector.tsx similarity index 68% rename from plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/inspector.tsx rename to plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/inspector.tsx index 21b62ac0857..d2efdccbc07 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/components/inspector.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/inspector.tsx @@ -5,8 +5,9 @@ import { getSetting } from '@woocommerce/settings'; import { AttributeSetting } from '@woocommerce/types'; import { InspectorControls } from '@wordpress/block-editor'; import { dispatch, useSelect } from '@wordpress/data'; -import { createInterpolateElement } from '@wordpress/element'; +import { createInterpolateElement, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { Block, getBlockTypes, createBlock } from '@wordpress/blocks'; import { ComboboxControl, PanelBody, @@ -23,12 +24,15 @@ import { /** * Internal dependencies */ -import { sortOrderOptions } from '../constants'; -import { BlockAttributes, EditProps } from '../types'; -import { getAttributeFromId } from '../utils'; +import { sortOrderOptions } from './constants'; +import { BlockAttributes, EditProps } from './types'; +import { getAttributeFromId } from './utils'; +import { getInnerBlockByName } from '../../utils'; const ATTRIBUTES = getSetting< AttributeSetting[] >( 'attributes', [] ); +let displayStyleOptions: Block[] = []; + export const Inspector = ( { clientId, attributes, @@ -43,47 +47,29 @@ export const Inspector = ( { hideEmpty, clearButton, } = attributes; - const { updateBlockAttributes } = dispatch( 'core/block-editor' ); - const { productFilterWrapperBlockId, productFilterWrapperHeadingBlockId } = - useSelect( - ( select ) => { - if ( ! clientId ) - return { - productFilterWrapperBlockId: undefined, - productFilterWrapperHeadingBlockId: undefined, - }; + const { updateBlockAttributes, insertBlock, replaceBlock } = + dispatch( 'core/block-editor' ); + const filterBlock = useSelect( + ( select ) => { + return select( 'core/block-editor' ).getBlock( clientId ); + }, + [ clientId ] + ); + const [ displayStyleBlocksAttributes, setDisplayStyleBlocksAttributes ] = + useState< Record< string, unknown > >( {} ); - const { getBlockParentsByBlockName, getBlock } = - select( 'core/block-editor' ); + const filterHeadingBlock = getInnerBlockByName( + filterBlock, + 'core/heading' + ); - 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 ] + if ( displayStyleOptions.length === 0 ) { + displayStyleOptions = getBlockTypes().filter( ( blockType ) => + blockType.ancestor?.includes( + 'woocommerce/product-filter-attribute' + ) ); + } return ( <> @@ -102,17 +88,9 @@ export const Inspector = ( { } ); const attributeObject = getAttributeFromId( numericId ); - if ( productFilterWrapperBlockId ) { + if ( filterHeadingBlock ) { updateBlockAttributes( - productFilterWrapperBlockId, - { - attributeId: numericId, - } - ); - } - if ( productFilterWrapperHeadingBlockId ) { - updateBlockAttributes( - productFilterWrapperHeadingBlockId, + filterHeadingBlock.clientId, { content: attributeObject?.label ?? @@ -188,17 +166,46 @@ export const Inspector = ( { value={ displayStyle } onChange={ ( 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%' } } > - - + { displayStyleOptions.map( ( blockType ) => ( + + ) ) } { + const blockProps = useBlockProps.save(); + const innerBlocksProps = useInnerBlocksProps.save( blockProps ); + return
    ; +}; + +export default Save; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/block.json new file mode 100644 index 00000000000..75be211eb62 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/block.json @@ -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": "" + } + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/edit.tsx new file mode 100644 index 00000000000..3e4ea3b73c4 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/edit.tsx @@ -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 ) => ( +
  • +   +
  • + ) ); + }, [] ); + + if ( ! items ) { + return <>; + } + + const threshold = 15; + const isLongList = items.length > threshold; + + return ( + <> +
    +
      + { isLoading && loadingState } + { ! isLoading && + ( isLongList + ? items.slice( 0, threshold ) + : items + ).map( ( item, index ) => ( +
    • + +
    • + ) ) } +
    + { ! isLoading && isLongList && ( + + { __( 'Show more…', 'woocommerce' ) } + + ) } +
    + + { colorGradientSettings.hasColorsOrGradients && ( + { + 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 } + /> + ) } + + + ); +}; + +export default withColors( { + optionElementBorder: 'option-element-border', + optionElementSelected: 'option-element-border', + optionElement: 'option-element', +} )( Edit ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/editor.scss new file mode 100644 index 00000000000..dbca49525b7 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/editor.scss @@ -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; + } + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/frontend.ts new file mode 100644 index 00000000000..f88d26cf9e2 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/frontend.ts @@ -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; + } ); + }, + }, +} ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/index.tsx new file mode 100644 index 00000000000..d87769fa657 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/index.tsx @@ -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, + } ); +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/style.scss new file mode 100644 index 00000000000..ece2e52bc7f --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/style.scss @@ -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; +} + diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/types.ts new file mode 100644 index 00000000000..ef10e7f6bd4 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/types.ts @@ -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; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json new file mode 100644 index 00000000000..44e26c25279 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/block.json @@ -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": {} +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx new file mode 100644 index 00000000000..fcefe04c0ab --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/edit.tsx @@ -0,0 +1,15 @@ +/** + * External dependencies + */ +import { useBlockProps } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import './style.scss'; + +const Edit = () => { + return
    These are chips.
    ; +}; + +export default Edit; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/index.tsx new file mode 100644 index 00000000000..d87769fa657 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/index.tsx @@ -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, + } ); +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss new file mode 100644 index 00000000000..a8af7fda118 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/style.scss @@ -0,0 +1,3 @@ +:where(.wc-block-product-filter-chips) { + // WIP +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/types.ts similarity index 59% rename from plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/types.ts rename to plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/types.ts index 726784edfd3..0a68e80edca 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/types.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/types.ts @@ -6,13 +6,9 @@ import { BlockEditProps } from '@wordpress/blocks'; /** * Internal dependencies */ -import { BLOCK_NAME_MAP } from './constants'; - -export type FilterType = keyof typeof BLOCK_NAME_MAP; export type BlockAttributes = { - filterType: FilterType; - heading: string; + className: string; }; export type EditProps = BlockEditProps< BlockAttributes >; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/clear-button/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/clear-button/frontend.ts index 0bfc299b660..7fccd55836c 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/clear-button/frontend.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/clear-button/frontend.ts @@ -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 */ @@ -6,7 +11,7 @@ import { store, getContext } from '@woocommerce/interactivity'; /** * Internal dependencies */ -import { navigate } from '../product-filter/frontend'; +import { navigate } from '../../frontend'; const getQueryParams = ( e: Event ) => { const filterNavContainer = ( e.target as HTMLElement )?.closest( diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/frontend.ts index 844e3c364f6..482e3e51117 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/frontend.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/frontend.ts @@ -9,7 +9,7 @@ import { debounce } from '@woocommerce/base-utils'; /** * Internal dependencies */ -import { navigate } from '../product-filter/frontend'; +import { navigate } from '../../frontend'; import type { PriceFilterContext, PriceFilterStore } from './types'; const getUrl = ( context: PriceFilterContext ) => { diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block-variations.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block-variations.tsx deleted file mode 100644 index 7d33338bea6..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block-variations.tsx +++ /dev/null @@ -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; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block.json b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block.json deleted file mode 100644 index 673300a288c..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/block.json +++ /dev/null @@ -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" -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/components/warning.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/components/warning.tsx deleted file mode 100644 index 33b16eaf5cf..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/components/warning.tsx +++ /dev/null @@ -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 ( - - { __( - 'The widget area containing Collection Filters block needs to be placed on a product archive page for filters to function properly.', - 'woocommerce' - ) } - - ); - } - - const isSiteEditor = getSetting< boolean >( 'isSiteEditor' ); - if ( ! isSiteEditor ) { - return ( - - { __( - 'When added to a post or page, Collection Filters block needs to be nested inside a Product Collection block to function properly.', - 'woocommerce' - ) } - - ); - } - - return null; -}; - -export default Warning; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/constants.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/constants.ts deleted file mode 100644 index ba212b495f2..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/constants.ts +++ /dev/null @@ -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', -}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/edit.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/edit.tsx deleted file mode 100644 index 9706f037f9f..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/edit.tsx +++ /dev/null @@ -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 ( - - ); -}; - -export default Edit; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/editor.scss deleted file mode 100644 index b739f84f377..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/editor.scss +++ /dev/null @@ -1,5 +0,0 @@ -.wp-block-woocommerce-collection-filters { - .components-notice { - margin: 0; - } -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/frontend.ts deleted file mode 100644 index 13a489f89c0..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/frontend.ts +++ /dev/null @@ -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 ); -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/index.tsx deleted file mode 100644 index 850ac787f5d..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/index.tsx +++ /dev/null @@ -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: ( - - ), - }, - 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 - ); - }, - }, - ], - }, - } ); -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/save.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/save.tsx deleted file mode 100644 index 992874cb932..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/save.tsx +++ /dev/null @@ -1,8 +0,0 @@ -/** - * External dependencies - */ -import { InnerBlocks } from '@wordpress/block-editor'; - -export default function save() { - return ; -} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/utils.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/utils.ts deleted file mode 100644 index 10a27961150..00000000000 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/product-filter/utils.ts +++ /dev/null @@ -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 ) ); -}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/frontend.ts index 3bd9f961704..8d865131b33 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/frontend.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/frontend.ts @@ -8,7 +8,7 @@ import { DropdownContext } from '@woocommerce/interactivity-components/dropdown' /** * Internal dependencies */ -import { navigate } from '../product-filter/frontend'; +import { navigate } from '../../frontend'; function getUrl( filters: Array< string | null > ) { filters = filters.filter( Boolean ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/stock-filter/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/stock-filter/frontend.ts index 9833ff753c1..f0037014671 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/stock-filter/frontend.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/stock-filter/frontend.ts @@ -9,7 +9,7 @@ import { CheckboxListContext } from '@woocommerce/interactivity-components/check /** * Internal dependencies */ -import { navigate } from '../product-filter/frontend'; +import { navigate } from '../../frontend'; const getUrl = ( activeFilters: string ) => { const url = new URL( window.location.href ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/types.ts index f7ad8b247e9..0709a1490b8 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/types.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/types.ts @@ -7,8 +7,8 @@ export type BlockOverlayAttributeOptions = ( typeof BlockOverlayAttribute )[ keyof typeof BlockOverlayAttribute ]; export interface BlockAttributes { - productId?: string; setAttributes: ( attributes: ProductFiltersBlockAttributes ) => void; + productId?: string; overlay: BlockOverlayAttributeOptions; overlayIcon: | 'filter-icon-1' @@ -18,3 +18,23 @@ export interface BlockAttributes { overlayButtonStyle: 'label-icon' | 'label' | 'icon'; 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; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/utils.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/utils.ts new file mode 100644 index 00000000000..d21e622fc3e --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/utils.ts @@ -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; +}; diff --git a/plugins/woocommerce-blocks/assets/js/icons/index.js b/plugins/woocommerce-blocks/assets/js/icons/index.js index 446bbadcebb..6cfa7e5f667 100644 --- a/plugins/woocommerce-blocks/assets/js/icons/index.js +++ b/plugins/woocommerce-blocks/assets/js/icons/index.js @@ -4,6 +4,7 @@ export { default as bagAlt } from './library/bag-alt'; export { default as barcode } from './library/barcode'; export { default as cart } from './library/cart'; 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 closeSquareShadow } from './library/close-square-shadow'; export { default as customerAccount } from './library/customer-account'; diff --git a/plugins/woocommerce-blocks/assets/js/icons/library/check-mark.tsx b/plugins/woocommerce-blocks/assets/js/icons/library/check-mark.tsx new file mode 100644 index 00000000000..cd7e6b1bffa --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/icons/library/check-mark.tsx @@ -0,0 +1,24 @@ +/** + * External dependencies + */ +import { IconProps } from '@wordpress/icons/build-types/icon'; +import { Path, SVG } from '@wordpress/primitives'; + +const CheckMark = ( props: IconProps ) => ( + + + +); + +export default CheckMark; diff --git a/plugins/woocommerce-blocks/bin/webpack-entries.js b/plugins/woocommerce-blocks/bin/webpack-entries.js index 11a84832ef5..aeeb511c7eb 100644 --- a/plugins/woocommerce-blocks/bin/webpack-entries.js +++ b/plugins/woocommerce-blocks/bin/webpack-entries.js @@ -88,10 +88,6 @@ const blocks = { 'product-filters': { isExperimental: true, }, - 'product-filter': { - isExperimental: true, - customDir: 'product-filters/inner-blocks/product-filter', - }, 'product-filters-overlay': { isExperimental: true, customDir: 'product-filters/inner-blocks/overlay', @@ -124,6 +120,14 @@ const blocks = { customDir: 'product-filters/inner-blocks/clear-button', 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': { customDir: 'order-confirmation/summary', }, diff --git a/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_active-filters.handlebars b/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_active-filters.handlebars index f99258d7b35..1434a83d574 100644 --- a/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_active-filters.handlebars +++ b/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_active-filters.handlebars @@ -1,11 +1,12 @@ - - -

    Active filters

    - + +
    {{#> wp-block blockName='woocommerce/product-filter-active' attributes=attributes }} {{/ wp-block }} +
    + +
    diff --git a/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_attribute-filter.handlebars b/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_attribute-filter.handlebars index 1ae53f9c3d8..805634b68a7 100644 --- a/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_attribute-filter.handlebars +++ b/plugins/woocommerce-blocks/tests/e2e/content-templates/template_archive-product_attribute-filter.handlebars @@ -1,18 +1,26 @@ - - -

    Filter by Attribute

    - + +
    +{{#> wp-block blockName='woocommerce/product-filter-attribute' attributes=attributes }} +
    +
    +

    Attribute

    + + + -
    + - - +
    + -{{#> wp-block blockName='woocommerce/product-filter-attribute' attributes=attributes }} +
    {{/ wp-block }} +
    + +
    diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/active-filters.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/active-filters.block_theme.spec.ts deleted file mode 100644 index b4c78fd0ab1..00000000000 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/active-filters.block_theme.spec.ts +++ /dev/null @@ -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(); - } ); - } ); -} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/attribute-filter.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/attribute-filter.block_theme.spec.ts deleted file mode 100644 index d39c14ff018..00000000000 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/attribute-filter.block_theme.spec.ts +++ /dev/null @@ -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(); - } ); - } ); -} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/basic.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/basic.block_theme.spec.ts deleted file mode 100644 index db9f8dd5b10..00000000000 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/basic.block_theme.spec.ts +++ /dev/null @@ -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(); - } - } ); -} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts index 4dae7c49c84..97ae2631985 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters-overlay/product-filters-overlay-template-part.block_theme.spec.ts @@ -105,10 +105,6 @@ test.describe( 'Filters Overlay Template Part', () => { templatePartData.selectors.editor.blocks.activeFilters .blockLabel ) - .getByLabel( - templatePartData.selectors.editor.blocks.filterOptions - .blockLabel - ) .click(); await editor.openDocumentSettingsSidebar(); @@ -201,10 +197,6 @@ test.describe( 'Filters Overlay Template Part', () => { templatePartData.selectors.editor.blocks.activeFilters .blockLabel ) - .getByLabel( - templatePartData.selectors.editor.blocks.filterOptions - .blockLabel - ) .click(); await editor.openDocumentSettingsSidebar(); @@ -221,12 +213,6 @@ test.describe( 'Filters Overlay Template Part', () => { 'OverlayNeverMobileAlways' ); await layoutSettings.getByLabel( 'Never' ).click(); - await editor.page - .getByRole( 'link', { - name: templatePartData.selectors.editor.blocks - .productFiltersOverlayNavigation.title, - } ) - .click(); await editor.saveSiteEditorEntities( { isOnlyCurrentEntityDirty: true, @@ -271,10 +257,6 @@ test.describe( 'Filters Overlay Template Part', () => { templatePartData.selectors.editor.blocks.activeFilters .blockLabel ) - .getByLabel( - templatePartData.selectors.editor.blocks.filterOptions - .blockLabel - ) .click(); await editor.openDocumentSettingsSidebar(); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/active-filter-frontend.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/active-filter-frontend.block_theme.spec.ts new file mode 100644 index 00000000000..341a57ee21e --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/active-filter-frontend.block_theme.spec.ts @@ -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(); + } ); +} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-editor.block_theme.spec.ts similarity index 91% rename from plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter.block_theme.spec.ts rename to plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-editor.block_theme.spec.ts index 54220f04b92..77f9b13f090 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-editor.block_theme.spec.ts @@ -48,9 +48,7 @@ test.describe( `${ blockData.name }`, () => { } ) => { await pageObject.addProductFiltersBlock( { cleanContent: true } ); - const block = editor.canvas - .getByLabel( 'Block: Color (Experimental)' ) - .getByLabel( 'Block: Filter Options' ); + const block = editor.canvas.getByLabel( 'Block: Color (Experimental)' ); await expect( block ).toBeVisible(); @@ -82,9 +80,7 @@ test.describe( `${ blockData.name }`, () => { } ) => { await pageObject.addProductFiltersBlock( { cleanContent: true } ); - const block = editor.canvas - .getByLabel( 'Block: Color (Experimental)' ) - .getByLabel( 'Block: Filter Options' ); + const block = editor.canvas.getByLabel( 'Block: Color (Experimental)' ); await expect( block ).toBeVisible(); @@ -112,9 +108,7 @@ test.describe( `${ blockData.name }`, () => { } ) => { await pageObject.addProductFiltersBlock( { cleanContent: true } ); - const block = editor.canvas - .getByLabel( 'Block: Color (Experimental)' ) - .getByLabel( 'Block: Filter Options' ); + const block = editor.canvas.getByLabel( 'Block: Color (Experimental)' ); await editor.openDocumentSettingsSidebar(); await block.click(); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-frontend.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-frontend.block_theme.spec.ts new file mode 100644 index 00000000000..8f79924b291 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/attribute-filter-frontend.block_theme.spec.ts @@ -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 ] + ); + } + } ); + } ); +} ); diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/price-filter.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/price-filter-frontend.block_theme.spec.ts similarity index 99% rename from plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/price-filter.block_theme.spec.ts rename to plugins/woocommerce-blocks/tests/e2e/tests/product-filters/price-filter-frontend.block_theme.spec.ts index 796fd4af488..191c16f53f4 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/price-filter.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/price-filter-frontend.block_theme.spec.ts @@ -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.beforeEach( async ( { requestUtils, templateCompiler } ) => { await requestUtils.activatePlugin( diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters-template-part.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters-template-part.block_theme.spec.ts index d065baf73dc..3d16f0c9e89 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters-template-part.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters-template-part.block_theme.spec.ts @@ -52,13 +52,7 @@ test.describe( 'Product Filters Template Part', () => { const block = editor.canvas.getByLabel( `Block: ${ blockData.name }` ); await expect( block ).toBeVisible(); - const searchTerms = [ - 'Status (Experimental)', - 'Price (Experimental)', - 'Rating (Experimental)', - 'Attribute (Experimental)', - 'Active (Experimental)', - ]; + const searchTerms = [ 'Color (Experimental)', 'Active (Experimental)' ]; for ( const filter of searchTerms ) { await editor.selectBlocks( blockData.selectors.editor.block ); @@ -78,13 +72,7 @@ test.describe( 'Product Filters Template Part', () => { await searchResult.click(); - let _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'; - } + const _locator = `[aria-label="Block: ${ filter }"]`; await expect( editor.canvas.locator( _locator ) ).toHaveCount( 2 ); } diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts index 887dcf60e20..e6838d7c37d 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/product-filters.block_theme.spec.ts @@ -68,53 +68,17 @@ test.describe( `${ blockData.name }`, () => { ); await expect( block ).toBeVisible(); - const activeHeading = block.getByText( 'Active', { exact: true } ); - const activeFilterBlock = block - .getByLabel( 'Block: Filter Options' ) - .and( - editor.canvas.locator( - '[data-type="woocommerce/product-filter-active"]' - ) - ); - await expect( activeHeading ).toBeVisible(); + const activeFilterBlock = block.getByLabel( + 'Block: Active (Experimental)' + ); 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', { exact: true, } ); - const colorFilterBlock = block - .getByLabel( 'Block: Filter Options' ) - .and( - editor.canvas.locator( - '[data-type="woocommerce/product-filter-attribute"]' - ) - ); + const colorFilterBlock = block.getByLabel( + 'Block: Color (Experimental)' + ); const expectedColorFilterOptions = [ 'Blue', 'Green', @@ -122,27 +86,11 @@ test.describe( `${ blockData.name }`, () => { 'Red', 'Yellow', ]; - const colorFilterOptions = ( - await colorFilterBlock.allInnerTexts() - )[ 0 ].split( '\n' ); await expect( colorHeading ).toBeVisible(); await expect( colorFilterBlock ).toBeVisible(); - expect( colorFilterOptions ).toEqual( - expect.arrayContaining( expectedColorFilterOptions ) - ); - - 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(); + for ( const option of expectedColorFilterOptions ) { + await expect( colorFilterBlock ).toContainText( option ); + } } ); 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(); - const productFilterPriceBlockListItem = listView.getByText( - 'Price (Experimental)' - ); - await expect( productFilterPriceBlockListItem ).toBeVisible(); - - const productFilterStatusBlockListItem = listView.getByText( - 'Status (Experimental)' - ); - await expect( productFilterStatusBlockListItem ).toBeVisible(); - const productFilterAttributeBlockListItem = listView.getByText( 'Color (Experimental)' // it must select the attribute with the highest product count ); await expect( productFilterAttributeBlockListItem ).toBeVisible(); - - const productFilterRatingBlockListItem = listView.getByText( - 'Rating (Experimental)' - ); - await expect( productFilterRatingBlockListItem ).toBeVisible(); } ); test( 'should display the correct inspector style controls', async ( { @@ -370,7 +303,7 @@ test.describe( `${ blockData.name }`, () => { ).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, pageObject, } ) => { diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/rating-filter.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/rating-filter-frontend.block_theme.spec.ts similarity index 97% rename from plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/rating-filter.block_theme.spec.ts rename to plugins/woocommerce-blocks/tests/e2e/tests/product-filters/rating-filter-frontend.block_theme.spec.ts index 279fd391567..272aeb4966f 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/rating-filter.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/rating-filter-frontend.block_theme.spec.ts @@ -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.beforeEach( async ( { requestUtils, templateCompiler } ) => { await requestUtils.activatePlugin( diff --git a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/stock-status.block_theme.spec.ts b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/stock-status-frontend.block_theme.spec.ts similarity index 98% rename from plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/stock-status.block_theme.spec.ts rename to plugins/woocommerce-blocks/tests/e2e/tests/product-filters/stock-status-frontend.block_theme.spec.ts index c465257eea2..4012ea324af 100644 --- a/plugins/woocommerce-blocks/tests/e2e/tests/filter-blocks/stock-status.block_theme.spec.ts +++ b/plugins/woocommerce-blocks/tests/e2e/tests/product-filters/stock-status-frontend.block_theme.spec.ts @@ -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.beforeEach( async ( { requestUtils, templateCompiler } ) => { await requestUtils.activatePlugin( diff --git a/plugins/woocommerce/changelog/try-new-improved-filter-blocks-structure b/plugins/woocommerce/changelog/try-new-improved-filter-blocks-structure new file mode 100644 index 00000000000..61232016454 --- /dev/null +++ b/plugins/woocommerce/changelog/try-new-improved-filter-blocks-structure @@ -0,0 +1,5 @@ +Significance: patch +Type: add +Comment: Filter blocks: new and improve filter blocks structure. + + diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php deleted file mode 100644 index 6cfc9a1465a..00000000000 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilter.php +++ /dev/null @@ -1,157 +0,0 @@ -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( - '', - get_block_wrapper_attributes( $attributes_data ), - $content - ); - } - } - - return sprintf( - '', - 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'] ) ) - ); - } -} diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php index 679227f3a09..11469663107 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php @@ -57,7 +57,6 @@ final class ProductFilterActive extends AbstractBlock { 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( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), - 'data-has-filter' => empty( $active_filters ) ? 'no' : 'yes', ) ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php index d997799c772..0a5327f9ce0 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php @@ -1,10 +1,10 @@ get_default_product_attribute(); - $attributes['attributeId'] = $default_product_attribute->attribute_id; + protected function render( $block_attributes, $content, $block ) { + if ( empty( $block_attributes['attributeId'] ) ) { + $default_product_attribute = $this->get_default_product_attribute(); + $block_attributes['attributeId'] = $default_product_attribute->attribute_id; } // 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 ''; } - $product_attribute = wc_get_attribute( $attributes['attributeId'] ); - $attribute_counts = $this->get_attribute_counts( $block, $product_attribute->slug, $attributes['queryType'] ); + $product_attribute = wc_get_attribute( $block_attributes['attributeId'] ); + $attribute_counts = $this->get_attribute_counts( $block, $product_attribute->slug, $block_attributes['queryType'] ); if ( empty( $attribute_counts ) ) { return sprintf( @@ -181,7 +182,6 @@ final class ProductFilterAttribute extends AbstractBlock { get_block_wrapper_attributes( 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-has-filter' => 'no', ) ), ); @@ -202,118 +202,56 @@ final class ProductFilterAttribute extends AbstractBlock { ); $attribute_options = array_map( - function ( $term ) use ( $attribute_counts, $selected_terms ) { + function ( $term ) use ( $block_attributes, $attribute_counts, $selected_terms ) { $term = (array) $term; $term['count'] = $attribute_counts[ $term['term_id'] ]; $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 ); $filtered_options = array_filter( $attribute_options, - function ( $option ) { - return $option['count'] > 0; + function ( $option ) use ( $block_attributes ) { + $hide_empty = $block_attributes['hideEmpty'] ?? true; + if ( $hide_empty ) { + return $option['rawData']['count'] > 0; + } + return true; } ); - $filter_content = 'dropdown' === $attributes['displayStyle'] ? - $this->render_attribute_dropdown( $filtered_options, $attributes ) : - $this->render_attribute_checkbox_list( $filtered_options, $attributes ); + $filter_context = array( + 'on_change' => "{$this->get_full_block_name()}::actions.updateProducts", + 'items' => $filtered_options, + ); + + foreach ( $block->parsed_block['innerBlocks'] as $inner_block ) { + $content .= ( new \WP_Block( $inner_block, array( 'filterData' => $filter_context ) ) )->render(); + } $context = array( - 'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ), - 'queryType' => $attributes['queryType'], - 'selectType' => 'multiple', + 'attributeSlug' => str_replace( 'pa_', '', $product_attribute->slug ), + 'queryType' => $block_attributes['queryType'], + 'selectType' => 'multiple', + 'hasSelectedFilters' => count( $selected_terms ) > 0, ); return sprintf( - '
    %2$s%3$s
    ', + '
    %2$s
    ', get_block_wrapper_attributes( array( '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-has-filter' => 'yes', ) ), - $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", - ) + $content ); } @@ -380,7 +318,16 @@ final class ProductFilterAttribute extends AbstractBlock { $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; } @@ -428,10 +375,9 @@ final class ProductFilterAttribute extends AbstractBlock { if ( $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; } @@ -447,32 +393,26 @@ final class ProductFilterAttribute extends AbstractBlock { 'inserter' => false, 'content' => strtr( ' - - -
    - -

    {{attribute_label}}

    - + +
    + +
    + +

    {{attribute_label}}

    + - - -
    - -
    - Clear -
    - -
    - - + + +
    + +
    + +
    + + +
    - - - - + ', array( '{{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; + } } diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php new file mode 100644 index 00000000000..a4a299d9ff8 --- /dev/null +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php @@ -0,0 +1,155 @@ +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(); + ?> +
    > +
      + + +
    • = $remaining_initial_unchecked ) : + ?> + class="wc-block-product-filter-checkbox-list__item hidden" + data-wc-class--hidden="!context.showAll" + + + + + class="wc-block-product-filter-checkbox-list__item" + > + +
    • + +
    + $show_initially ) : ?> + + + + +
    + '!context.hasSelectedFilter', + 'data-wc-bind--hidden' => '!context.hasSelectedFilters', ) ); $p = new \WP_HTML_Tag_Processor( $content ); 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' ); $p->set_attribute( 'style', 'outline:none;' . $style ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php index 9c149ef6b3c..0137f32abf4 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php @@ -23,6 +23,23 @@ class ProductFilters extends AbstractBlock { 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. * @@ -116,12 +133,10 @@ class ProductFilters extends AbstractBlock { * @return string Rendered block type output. */ protected function render( $attributes, $content, $block ) { - $html = $content; - $p = new \WP_HTML_Tag_Processor( $html ); - - if ( $p->next_tag() ) { - $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( + $tags = new \WP_HTML_Tag_Processor( $content ); + 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 ) ); + $tags->set_attribute( 'data-wc-context', wp_json_encode( array( @@ -131,13 +146,29 @@ class ProductFilters extends AbstractBlock { 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() ); + } + + return $tags->get_updated_html(); } + } - $dialog_html = $this->render_dialog(); - - $html = $this->inject_dialog( $html, $dialog_html ); - - return $html; + /** + * 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-filters-%s', + md5( wp_json_encode( $block->parsed_block['innerBlocks'] ) ) + ); } } diff --git a/plugins/woocommerce/src/Blocks/BlockTypesController.php b/plugins/woocommerce/src/Blocks/BlockTypesController.php index 059152fd7fc..03e87ecbbad 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypesController.php +++ b/plugins/woocommerce/src/Blocks/BlockTypesController.php @@ -404,7 +404,6 @@ final class BlockTypesController { // Update plugins/woocommerce-blocks/docs/internal-developers/blocks/feature-flags-and-experimental-interfaces.md // when modifying this list. if ( Features::is_enabled( 'experimental-blocks' ) ) { - $block_types[] = 'ProductFilter'; $block_types[] = 'ProductFilters'; $block_types[] = 'ProductFiltersOverlay'; $block_types[] = 'ProductFiltersOverlayNavigation'; @@ -414,6 +413,8 @@ final class BlockTypesController { $block_types[] = 'ProductFilterRating'; $block_types[] = 'ProductFilterActive'; $block_types[] = 'ProductFilterClearButton'; + $block_types[] = 'ProductFilterCheckboxList'; + $block_types[] = 'ProductFilterChips'; $block_types[] = 'OrderConfirmation\CreateAccount'; } diff --git a/plugins/woocommerce/templates/parts/product-filters.html b/plugins/woocommerce/templates/parts/product-filters.html index 9e699ccd71d..a4e82272829 100644 --- a/plugins/woocommerce/templates/parts/product-filters.html +++ b/plugins/woocommerce/templates/parts/product-filters.html @@ -1,79 +1,28 @@ - -
    - +
    + +

    Filters

    + -
    -

    Filters

    - + - - -

    Active

    - + - - - - - -
    -

    Price

    - - - - -
    - -
    - -
    - - - - - - - -
    -

    Status

    - - - - -
    - -
    - -
    - - - - - - - - - -
    -

    Rating

    - - - - -
    - -
    - -
    - - - - - - -
    - -
    -
    + +
    + +
    + Apply +
    + +
    + +