From 1b580988482cb47968368e92cb4fd303f97c231a Mon Sep 17 00:00:00 2001 From: Tung Du Date: Wed, 18 Sep 2024 13:16:07 +0700 Subject: [PATCH] [Experimental] Product Filters Chips style and new interactivity API implementation (#51393) --- .../{frontend.tsx => frontend.ts} | 114 +++++--- .../inner-blocks/active-filters/frontend.ts | 17 +- .../inner-blocks/attribute-filter/frontend.ts | 136 ++++------ .../inner-blocks/checkbox-list/edit.tsx | 23 +- .../inner-blocks/checkbox-list/index.tsx | 2 + .../inner-blocks/checkbox-list/save.tsx | 35 +++ .../inner-blocks/checkbox-list/style.scss | 20 +- .../inner-blocks/checkbox-list/utils.ts | 66 +++++ .../inner-blocks/chips/block.json | 42 ++- .../inner-blocks/chips/edit.tsx | 255 +++++++++++++++++- .../inner-blocks/chips/editor.scss | 6 + .../inner-blocks/chips/frontend.ts | 46 ++++ .../inner-blocks/chips/index.tsx | 2 + .../inner-blocks/chips/save.tsx | 35 +++ .../inner-blocks/chips/style.scss | 56 +++- .../inner-blocks/chips/types.ts | 37 ++- .../inner-blocks/chips/utils.ts | 91 +++++++ ...style-and-new-interactitity-implementation | 5 + .../Blocks/BlockTypes/ProductFilterActive.php | 6 - .../BlockTypes/ProductFilterAttribute.php | 39 +-- .../BlockTypes/ProductFilterCheckboxList.php | 67 ++--- .../Blocks/BlockTypes/ProductFilterChips.php | 86 ++++++ .../src/Blocks/BlockTypes/ProductFilters.php | 44 +++ 23 files changed, 987 insertions(+), 243 deletions(-) rename plugins/woocommerce-blocks/assets/js/blocks/product-filters/{frontend.tsx => frontend.ts} (64%) create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/save.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/utils.ts create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/editor.scss create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/frontend.ts create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/save.tsx create mode 100644 plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/utils.ts create mode 100644 plugins/woocommerce/changelog/add-chips-style-and-new-interactitity-implementation diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.ts similarity index 64% rename from plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.tsx rename to plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.ts index 4b3085a291e..380a3e79ebc 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/frontend.ts @@ -2,47 +2,12 @@ * External dependencies */ import { - getContext as getContextFn, + getContext, store, navigate as navigateFn, } from '@woocommerce/interactivity'; import { getSetting } from '@woocommerce/settings'; -export interface ProductFiltersContext { - isDialogOpen: boolean; - hasPageWithWordPressAdminBar: boolean; -} - -const getContext = ( ns?: string ) => - getContextFn< ProductFiltersContext >( ns ); - -store( 'woocommerce/product-filters', { - state: { - isDialogOpen: () => { - const context = getContext(); - return context.isDialogOpen; - }, - }, - actions: { - openDialog: () => { - const context = getContext(); - document.body.classList.add( 'wc-modal--open' ); - context.hasPageWithWordPressAdminBar = Boolean( - document.getElementById( 'wpadminbar' ) - ); - - context.isDialogOpen = true; - }, - closeDialog: () => { - const context = getContext(); - document.body.classList.remove( 'wc-modal--open' ); - - context.isDialogOpen = false; - }, - }, - callbacks: {}, -} ); - const isBlockTheme = getSetting< boolean >( 'isBlockTheme' ); const isProductArchive = getSetting< boolean >( 'isProductArchive' ); const needsRefresh = getSetting< boolean >( @@ -50,6 +15,28 @@ const needsRefresh = getSetting< boolean >( false ); +function isParamsEqual( + obj1: Record< string, string >, + obj2: Record< string, string > +): boolean { + const keys1 = Object.keys( obj1 ); + const keys2 = Object.keys( obj2 ); + + // First check if both objects have the same number of keys + if ( keys1.length !== keys2.length ) { + return false; + } + + // Check if all keys and values are the same + for ( const key of keys1 ) { + if ( obj1[ key ] !== obj2[ key ] ) { + return false; + } + } + + return true; +} + export function navigate( href: string, options = {} ) { /** * We may need to reset the current page when changing filters. @@ -79,3 +66,58 @@ export function navigate( href: string, options = {} ) { } return navigateFn( href, options ); } + +export interface ProductFiltersContext { + isDialogOpen: boolean; + hasPageWithWordPressAdminBar: boolean; + params: Record< string, string >; + originalParams: Record< string, string >; +} + +store( 'woocommerce/product-filters', { + state: { + isDialogOpen: () => { + const context = getContext< ProductFiltersContext >(); + return context.isDialogOpen; + }, + }, + actions: { + openDialog: () => { + const context = getContext< ProductFiltersContext >(); + document.body.classList.add( 'wc-modal--open' ); + context.hasPageWithWordPressAdminBar = Boolean( + document.getElementById( 'wpadminbar' ) + ); + + context.isDialogOpen = true; + }, + closeDialog: () => { + const context = getContext< ProductFiltersContext >(); + document.body.classList.remove( 'wc-modal--open' ); + + context.isDialogOpen = false; + }, + }, + callbacks: { + maybeNavigate: () => { + const { params, originalParams } = + getContext< ProductFiltersContext >(); + + if ( isParamsEqual( params, originalParams ) ) { + return; + } + + const url = new URL( window.location.href ); + const { searchParams } = url; + + for ( const key in originalParams ) { + searchParams.delete( key, originalParams[ key ] ); + } + + for ( const key in params ) { + searchParams.set( key, params[ key ] ); + } + navigate( url.href ); + }, + }, +} ); 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 69cab9093e1..3b2c41c766c 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,22 +6,15 @@ import { store, getContext } from '@woocommerce/interactivity'; /** * Internal dependencies */ -import { navigate } from '../../frontend'; - -type ActiveFiltersContext = { - queryId: number; - params: string[]; -}; +import { ProductFiltersContext } from '../../frontend'; store( 'woocommerce/product-filter-active', { actions: { clearAll: () => { - const { params } = getContext< ActiveFiltersContext >(); - const url = new URL( window.location.href ); - const { searchParams } = url; - - params.forEach( ( param ) => searchParams.delete( param ) ); - navigate( url.href ); + const productFiltersContext = getContext< ProductFiltersContext >( + 'woocommerce/product-filters' + ); + productFiltersContext.params = {}; }, }, } ); 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 6170ececffb..4416accc8df 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 @@ -1,14 +1,12 @@ /** * External dependencies */ -import { store, getContext } from '@woocommerce/interactivity'; -import { DropdownContext } from '@woocommerce/interactivity-components/dropdown'; -import { HTMLElementEvent } from '@woocommerce/types'; +import { store, getContext, getElement } from '@woocommerce/interactivity'; /** * Internal dependencies */ -import { navigate } from '../../frontend'; +import { ProductFiltersContext } from '../../frontend'; type AttributeFilterContext = { attributeSlug: string; @@ -16,102 +14,72 @@ type AttributeFilterContext = { selectType: 'single' | 'multiple'; }; -interface ActiveAttributeFilterContext extends AttributeFilterContext { - value: string; -} - -function nonNullable< T >( value: T ): value is NonNullable< T > { - return value !== null && value !== undefined; -} - -function getUrl( - selectedTerms: string[], - slug: string, - queryType: 'or' | 'and' -) { - const url = new URL( window.location.href ); - const { searchParams } = url; - - if ( selectedTerms.length > 0 ) { - searchParams.set( `filter_${ slug }`, selectedTerms.join( ',' ) ); - searchParams.set( `query_type_${ slug }`, queryType ); - } else { - searchParams.delete( `filter_${ slug }` ); - searchParams.delete( `query_type_${ slug }` ); - } - - return url.href; -} - -function getSelectedTermsFromUrl( slug: string ) { - const url = new URL( window.location.href ); - return ( url.searchParams.get( `filter_${ slug }` ) || '' ) - .split( ',' ) - .filter( Boolean ); -} - store( 'woocommerce/product-filter-attribute', { actions: { - navigate: () => { - const dropdownContext = getContext< DropdownContext >( - 'woocommerce/interactivity-dropdown' - ); - const context = getContext< AttributeFilterContext >(); - const filters = dropdownContext.selectedItems - .map( ( item ) => item.value ) - .filter( nonNullable ); + toggleFilter: () => { + const { ref } = getElement(); + const targetAttribute = + ref.getAttribute( 'data-attribute-value' ) ?? 'value'; + const termSlug = ref.getAttribute( targetAttribute ); - navigate( - getUrl( filters, context.attributeSlug, context.queryType ) - ); - }, - updateProducts: ( event: HTMLElementEvent< HTMLInputElement > ) => { - if ( ! event.target.value ) return; + if ( ! termSlug ) return; - const context = getContext< AttributeFilterContext >(); - - let selectedTerms = getSelectedTermsFromUrl( - context.attributeSlug + const { attributeSlug, queryType } = + getContext< AttributeFilterContext >(); + const productFiltersContext = getContext< ProductFiltersContext >( + 'woocommerce/product-filters' ); if ( - event.target.checked && - ! selectedTerms.includes( event.target.value ) + ! ( + `filter_${ attributeSlug }` in productFiltersContext.params + ) ) { - if ( context.selectType === 'multiple' ) - selectedTerms.push( event.target.value ); - if ( context.selectType === 'single' ) - selectedTerms = [ event.target.value ]; - } else { - selectedTerms = selectedTerms.filter( - ( value ) => value !== event.target.value - ); + productFiltersContext.params = { + ...productFiltersContext.params, + [ `filter_${ attributeSlug }` ]: termSlug, + [ `query_type_${ attributeSlug }` ]: queryType, + }; + return; } - navigate( - getUrl( - selectedTerms, - context.attributeSlug, - context.queryType - ) - ); - }, - removeFilter: () => { - const { attributeSlug, queryType, value } = - getContext< ActiveAttributeFilterContext >(); + const selectedTerms = + productFiltersContext.params[ + `filter_${ attributeSlug }` + ].split( ',' ); + if ( selectedTerms.includes( termSlug ) ) { + const remainingSelectedTerms = selectedTerms.filter( + ( term ) => term !== termSlug + ); + if ( remainingSelectedTerms.length > 0 ) { + productFiltersContext.params[ + `filter_${ attributeSlug }` + ] = remainingSelectedTerms.join( ',' ); + } else { + const updatedParams = productFiltersContext.params; - let selectedTerms = getSelectedTermsFromUrl( attributeSlug ); + delete updatedParams[ `filter_${ attributeSlug }` ]; + delete updatedParams[ `query_type_${ attributeSlug }` ]; - selectedTerms = selectedTerms.filter( ( item ) => item !== value ); - - navigate( getUrl( selectedTerms, attributeSlug, queryType ) ); + productFiltersContext.params = updatedParams; + } + } else { + productFiltersContext.params[ `filter_${ attributeSlug }` ] = + selectedTerms.concat( termSlug ).join( ',' ); + } }, clearFilters: () => { - const { attributeSlug, queryType } = - getContext< ActiveAttributeFilterContext >(); + const { attributeSlug } = getContext< AttributeFilterContext >(); + const productFiltersContext = getContext< ProductFiltersContext >( + 'woocommerce/product-filters' + ); + const updatedParams = productFiltersContext.params; - navigate( getUrl( [], attributeSlug, queryType ) ); + delete updatedParams[ `filter_${ attributeSlug }` ]; + delete updatedParams[ `query_type_${ attributeSlug }` ]; + + productFiltersContext.params = updatedParams; }, }, } ); 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 index 3e4ea3b73c4..0d290cf8bfa 100644 --- 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 @@ -24,6 +24,7 @@ import { import './style.scss'; import './editor.scss'; import { EditProps } from './types'; +import { getColorClasses, getColorVars } from './utils'; const Edit = ( props: EditProps ): JSX.Element => { const { @@ -51,21 +52,9 @@ const Edit = ( props: EditProps ): JSX.Element => { 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, + ...getColorClasses( attributes ), } ), - 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, - }, + style: getColorVars( attributes ), } ); const loadingState = useMemo( () => { @@ -131,9 +120,9 @@ const Edit = ( props: EditProps ): JSX.Element => { ) ) } { ! isLoading && isLongList && ( - - { __( 'Show more…', 'woocommerce' ) } - + ) } 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 index d87769fa657..75205c4b208 100644 --- 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 @@ -11,10 +11,12 @@ import { registerBlockType } from '@wordpress/blocks'; import metadata from './block.json'; import Edit from './edit'; import './style.scss'; +import Save from './save'; if ( isExperimentalBlocksEnabled() ) { registerBlockType( metadata, { edit: Edit, icon: productFilterOptions, + save: Save, } ); } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/save.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/save.tsx new file mode 100644 index 00000000000..e470a5c0f49 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/save.tsx @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import { useBlockProps } from '@wordpress/block-editor'; +import clsx from 'clsx'; + +/** + * Internal dependencies + */ +import { BlockAttributes } from './types'; +import { getColorClasses, getColorVars } from './utils'; + +const Save = ( { + attributes, + style, +}: { + attributes: BlockAttributes; + style: Record< string, string >; +} ) => { + const blockProps = useBlockProps.save( { + className: clsx( + 'wc-block-product-filter-checkbox-list', + attributes.className, + getColorClasses( attributes ) + ), + style: { + ...style, + ...getColorVars( attributes ), + }, + } ); + + return
; +}; + +export default Save; 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 index ece2e52bc7f..8e0691d3af1 100644 --- 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 @@ -4,11 +4,6 @@ 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; @@ -34,6 +29,7 @@ width: 1em; height: 1em; border-radius: 2px; + pointer-events: none; .has-option-element-color & { display: none; @@ -51,6 +47,7 @@ margin: 0; width: 1em; background: var(--wc-product-filter-checkbox-list-option-element, transparent); + cursor: pointer; } .wc-block-product-filter-checkbox-list__input:checked + .wc-block-product-filter-checkbox-list__mark { @@ -75,12 +72,15 @@ color: var(--wc-product-filter-checkbox-list-option-element-selected, currentColor); } +:where(.wc-block-product-filter-checkbox-list__text) { + font-size: 0.875em; +} + :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; + appearance: none; + background: transparent; + border: none; + padding: 0; } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/utils.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/utils.ts new file mode 100644 index 00000000000..df761d0aed6 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/checkbox-list/utils.ts @@ -0,0 +1,66 @@ +/** + * Internal dependencies + */ +import { BlockAttributes } from './types'; + +function getCSSVar( slug: string | undefined, value: string | undefined ) { + if ( slug ) { + return `var(--wp--preset--color--${ slug })`; + } + return value || ''; +} + +export function getColorVars( attributes: BlockAttributes ) { + const { + optionElement, + optionElementBorder, + optionElementSelected, + customOptionElement, + customOptionElementBorder, + customOptionElementSelected, + } = attributes; + + const vars: Record< string, string > = { + '--wc-product-filter-checkbox-list-option-element': getCSSVar( + optionElement, + customOptionElement + ), + '--wc-product-filter-checkbox-list-option-element-border': getCSSVar( + optionElementBorder, + customOptionElementBorder + ), + '--wc-product-filter-checkbox-list-option-element-selected': getCSSVar( + optionElementSelected, + customOptionElementSelected + ), + }; + + return Object.keys( vars ).reduce( + ( acc: Record< string, string >, key ) => { + if ( vars[ key ] ) { + acc[ key ] = vars[ key ]; + } + return acc; + }, + {} + ); +} + +export function getColorClasses( attributes: BlockAttributes ) { + const { + optionElement, + optionElementBorder, + optionElementSelected, + customOptionElement, + customOptionElementBorder, + customOptionElementSelected, + } = attributes; + + return { + 'has-option-element-color': optionElement || customOptionElement, + 'has-option-element-border-color': + optionElementBorder || customOptionElementBorder, + 'has-option-element-selected-color': + optionElementSelected || customOptionElementSelected, + }; +} 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 index 44e26c25279..7ef9d364b96 100644 --- 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 @@ -15,8 +15,44 @@ ], "supports": {}, "usesContext": [ - "filterData", - "isParentSelected" + "filterData" ], - "attributes": {} + "attributes": { + "chipText":{ + "type": "string" + }, + "customChipText":{ + "type": "string" + }, + "chipBackground":{ + "type": "string" + }, + "customChipBackground":{ + "type": "string" + }, + "chipBorder":{ + "type": "string" + }, + "customChipBorder":{ + "type": "string" + }, + "selectedChipText":{ + "type": "string" + }, + "customSelectedChipText":{ + "type": "string" + }, + "selectedChipBackground":{ + "type": "string" + }, + "customSelectedChipBackground":{ + "type": "string" + }, + "selectedChipBorder":{ + "type": "string" + }, + "customSelectedChipBorder":{ + "type": "string" + } + } } 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 index fcefe04c0ab..b2782b3c323 100644 --- 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 @@ -1,15 +1,260 @@ /** * External dependencies */ -import { useBlockProps } from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; +import { useMemo } from '@wordpress/element'; +import clsx from 'clsx'; +import { + InspectorControls, + useBlockProps, + withColors, + // @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 { EditProps } from './types'; +import './editor.scss'; +import { getColorClasses, getColorVars } from './utils'; -const Edit = () => { - return
These are chips.
; +const Edit = ( props: EditProps ): JSX.Element => { + const colorGradientSettings = useMultipleOriginColorsAndGradients(); + const { + context, + clientId, + attributes, + setAttributes, + chipText, + setChipText, + chipBackground, + setChipBackground, + chipBorder, + setChipBorder, + selectedChipText, + setSelectedChipText, + selectedChipBackground, + setSelectedChipBackground, + selectedChipBorder, + setSelectedChipBorder, + } = props; + const { + customChipText, + customChipBackground, + customChipBorder, + customSelectedChipText, + customSelectedChipBackground, + customSelectedChipBorder, + } = attributes; + const { filterData } = context; + const { isLoading, items } = filterData; + + const blockProps = useBlockProps( { + className: clsx( 'wc-block-product-filter-chips', { + 'is-loading': isLoading, + ...getColorClasses( attributes ), + } ), + style: getColorVars( attributes ), + } ); + + const loadingState = useMemo( () => { + return [ ...Array( 10 ) ].map( ( _, 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 ) => ( +
+ + { item.label } + +
+ ) ) } +
+ { ! isLoading && isLongList && ( + + ) } +
+ + { colorGradientSettings.hasColorsOrGradients && ( + { + setChipText( colorValue ); + setAttributes( { + customChipText: colorValue, + } ); + }, + resetAllFilter: () => { + setChipText( '' ); + setAttributes( { + customChipText: '', + } ); + }, + }, + { + label: __( + 'Unselected Chip Border', + 'woocommerce' + ), + colorValue: + chipBorder.color || customChipBorder, + onColorChange: ( colorValue: string ) => { + setChipBorder( colorValue ); + setAttributes( { + customChipBorder: colorValue, + } ); + }, + resetAllFilter: () => { + setChipBorder( '' ); + setAttributes( { + customChipBorder: '', + } ); + }, + }, + { + label: __( + 'Unselected Chip Background', + 'woocommerce' + ), + colorValue: + chipBackground.color || + customChipBackground, + onColorChange: ( colorValue: string ) => { + setChipBackground( colorValue ); + setAttributes( { + customChipBackground: colorValue, + } ); + }, + resetAllFilter: () => { + setChipBackground( '' ); + setAttributes( { + customChipBackground: '', + } ); + }, + }, + { + label: __( + 'Selected Chip Text', + 'woocommerce' + ), + colorValue: + selectedChipText.color || + customSelectedChipText, + onColorChange: ( colorValue: string ) => { + setSelectedChipText( colorValue ); + setAttributes( { + customSelectedChipText: colorValue, + } ); + }, + resetAllFilter: () => { + setSelectedChipText( '' ); + setAttributes( { + customSelectedChipText: '', + } ); + }, + }, + { + label: __( + 'Selected Chip Border', + 'woocommerce' + ), + colorValue: + selectedChipBorder.color || + customSelectedChipBorder, + onColorChange: ( colorValue: string ) => { + setSelectedChipBorder( colorValue ); + setAttributes( { + customSelectedChipBorder: colorValue, + } ); + }, + resetAllFilter: () => { + setSelectedChipBorder( '' ); + setAttributes( { + customSelectedChipBorder: '', + } ); + }, + }, + { + label: __( + 'Selected Chip Background', + 'woocommerce' + ), + colorValue: + selectedChipBackground.color || + customSelectedChipBackground, + onColorChange: ( colorValue: string ) => { + setSelectedChipBackground( colorValue ); + setAttributes( { + customSelectedChipBackground: + colorValue, + } ); + }, + resetAllFilter: () => { + setSelectedChipBackground( '' ); + setAttributes( { + customSelectedChipBackground: '', + } ); + }, + }, + ] } + panelId={ clientId } + { ...colorGradientSettings } + /> + ) } + + + ); }; -export default Edit; +export default withColors( { + chipText: 'chip-text', + chipBorder: 'chip-border', + chipBackground: 'chip-background', + selectedChipText: 'selected-chip-text', + selectedChipBorder: 'selected-chip-border', + selectedChipBackground: 'selected-chip-background', +} )( Edit ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/editor.scss b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/editor.scss new file mode 100644 index 00000000000..ec741894be7 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/editor.scss @@ -0,0 +1,6 @@ +.wc-block-product-filter-chips.is-loading { + .wc-block-product-filter-chips__item { + @include placeholder(); + margin: 5px 0; + } +} diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/frontend.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/frontend.ts new file mode 100644 index 00000000000..45a5de9f317 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/frontend.ts @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import { getElement, getContext, store } from '@woocommerce/interactivity'; + +/** + * Internal dependencies + */ + +export type ChipsContext = { + items: { + id: string; + label: string; + value: string; + checked: boolean; + }[]; + showAll: boolean; +}; + +store( 'woocommerce/product-filter-chips', { + actions: { + showAllItems: () => { + const context = getContext< ChipsContext >(); + context.showAll = true; + }, + + selectItem: () => { + const { ref } = getElement(); + const value = ref.getAttribute( 'value' ); + + if ( ! value ) return; + + const context = getContext< ChipsContext >(); + + 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/chips/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/index.tsx index d87769fa657..df088f6f16f 100644 --- 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 @@ -10,11 +10,13 @@ import { registerBlockType } from '@wordpress/blocks'; */ import metadata from './block.json'; import Edit from './edit'; +import Save from './save'; import './style.scss'; if ( isExperimentalBlocksEnabled() ) { registerBlockType( metadata, { edit: Edit, icon: productFilterOptions, + save: Save, } ); } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/save.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/save.tsx new file mode 100644 index 00000000000..5a5f9a3d110 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/save.tsx @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import { useBlockProps } from '@wordpress/block-editor'; +import clsx from 'clsx'; + +/** + * Internal dependencies + */ +import { BlockAttributes } from './types'; +import { getColorClasses, getColorVars } from './utils'; + +const Save = ( { + attributes, + style, +}: { + attributes: BlockAttributes; + style: Record< string, string >; +} ) => { + const blockProps = useBlockProps.save( { + className: clsx( + 'wc-block-product-filter-chips', + attributes.className, + getColorClasses( attributes ) + ), + style: { + ...style, + ...getColorVars( attributes ), + }, + } ); + + return
; +}; + +export default Save; 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 index a8af7fda118..a26f333241b 100644 --- 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 @@ -1,3 +1,55 @@ -:where(.wc-block-product-filter-chips) { - // WIP +:where(.wc-block-product-filter-chips__items) { + display: flex; + flex-wrap: wrap; + gap: $gap-smallest; +} + +:where(.wc-block-product-filter-chips__item) { + border: 1px solid color-mix(in srgb, currentColor 20%, transparent); + padding: $gap-smallest $gap-smaller; + appearance: none; + background: transparent; + border-radius: 2px; + font-size: 0.875em; + cursor: pointer; + + .has-chip-text & { + color: var(--wc-product-filter-chips-text); + } + .has-chip-background & { + background: var(--wc-product-filter-chips-background); + } + .has-chip-border & { + border-color: var(--wc-product-filter-chips-border); + } +} + +:where(.wc-block-product-filter-chips__item[aria-checked="true"]) { + background: currentColor; + + .has-selected-chip-text & { + color: var(--wc-product-filter-chips-selected-text); + } + .has-selected-chip-background & { + background: var(--wc-product-filter-chips-selected-background); + } + .has-selected-chip-border & { + border-color: var(--wc-product-filter-chips-selected-border); + } +} + +:where( +.wc-block-product-filter-chips:not(.has-selected-chip-text) +.wc-block-product-filter-chips__item[aria-checked="true"] +> .wc-block-product-filter-chips__label +) { + filter: invert(100%); +} + +:where(.wc-block-product-filter-chips__show-more) { + text-decoration: underline; + appearance: none; + background: transparent; + border: none; + padding: 0; } diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/types.ts index 0a68e80edca..096019cb760 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/types.ts +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/types.ts @@ -6,9 +6,44 @@ import { BlockEditProps } from '@wordpress/blocks'; /** * Internal dependencies */ +import { FilterBlockContext } from '../../types'; + +export type Color = { + slug?: string; + name?: string; + class?: string; + color: string; +}; export type BlockAttributes = { className: string; + chipText?: string; + customChipText?: string; + chipBackground?: string; + customChipBackground?: string; + chipBorder?: string; + customChipBorder?: string; + selectedChipText?: string; + customSelectedChipText?: string; + selectedChipBackground?: string; + customSelectedChipBackground?: string; + selectedChipBorder?: string; + customSelectedChipBorder?: string; }; -export type EditProps = BlockEditProps< BlockAttributes >; +export type EditProps = BlockEditProps< BlockAttributes > & { + style: Record< string, string >; + context: FilterBlockContext; + chipText: Color; + setChipText: ( value: string ) => void; + chipBackground: Color; + setChipBackground: ( value: string ) => void; + chipBorder: Color; + setChipBorder: ( value: string ) => void; + selectedChipText: Color; + setSelectedChipText: ( value: string ) => void; + selectedChipBackground: Color; + setSelectedChipBackground: ( value: string ) => void; + selectedChipBorder: Color; + setSelectedChipBorder: ( value: string ) => void; +}; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/utils.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/utils.ts new file mode 100644 index 00000000000..0d9d462600e --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/blocks/product-filters/inner-blocks/chips/utils.ts @@ -0,0 +1,91 @@ +/** + * Internal dependencies + */ +import { BlockAttributes } from './types'; + +function getCSSVar( slug: string | undefined, value: string | undefined ) { + if ( slug ) { + return `var(--wp--preset--color--${ slug })`; + } + return value || ''; +} + +export function getColorVars( attributes: BlockAttributes ) { + const { + chipText, + chipBackground, + chipBorder, + selectedChipText, + selectedChipBackground, + selectedChipBorder, + customChipText, + customChipBackground, + customChipBorder, + customSelectedChipText, + customSelectedChipBackground, + customSelectedChipBorder, + } = attributes; + + const vars: Record< string, string > = { + '--wc-product-filter-chips-text': getCSSVar( chipText, customChipText ), + '--wc-product-filter-chips-background': getCSSVar( + chipBackground, + customChipBackground + ), + '--wc-product-filter-chips-border': getCSSVar( + chipBorder, + customChipBorder + ), + '--wc-product-filter-chips-selected-text': getCSSVar( + selectedChipText, + customSelectedChipText + ), + '--wc-product-filter-chips-selected-background': getCSSVar( + selectedChipBackground, + customSelectedChipBackground + ), + '--wc-product-filter-chips-selected-border': getCSSVar( + selectedChipBorder, + customSelectedChipBorder + ), + }; + + return Object.keys( vars ).reduce( + ( acc: Record< string, string >, key ) => { + if ( vars[ key ] ) { + acc[ key ] = vars[ key ]; + } + return acc; + }, + {} + ); +} + +export function getColorClasses( attributes: BlockAttributes ) { + const { + chipText, + chipBackground, + chipBorder, + selectedChipText, + selectedChipBackground, + selectedChipBorder, + customChipText, + customChipBackground, + customChipBorder, + customSelectedChipText, + customSelectedChipBackground, + customSelectedChipBorder, + } = attributes; + + return { + 'has-chip-text-color': chipText || customChipText, + 'has-chip-background-color': chipBackground || customChipBackground, + 'has-chip-border-color': chipBorder || customChipBorder, + 'has-selected-chip-text-color': + selectedChipText || customSelectedChipText, + 'has-selected-chip-background-color': + selectedChipBackground || customSelectedChipBackground, + 'has-selected-chip-border-color': + selectedChipBorder || customSelectedChipBorder, + }; +} diff --git a/plugins/woocommerce/changelog/add-chips-style-and-new-interactitity-implementation b/plugins/woocommerce/changelog/add-chips-style-and-new-interactitity-implementation new file mode 100644 index 00000000000..e9c85e6063a --- /dev/null +++ b/plugins/woocommerce/changelog/add-chips-style-and-new-interactitity-implementation @@ -0,0 +1,5 @@ +Significance: patch +Type: add +Comment: [Experimental] Product Filters Chips style and new interactivity API implementation + + diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php index 11469663107..e8d24554024 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterActive.php @@ -48,15 +48,9 @@ final class ProductFilterActive extends AbstractBlock { */ $active_filters = apply_filters( 'collection_active_filters_data', array(), $this->get_filter_query_params( $query_id ) ); - $context = array( - 'queryId' => $query_id, - 'params' => array_keys( $this->get_filter_query_params( $query_id ) ), - ); - $wrapper_attributes = 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-wc-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), ) ); diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php index 0a5327f9ce0..0ef49f27988 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php @@ -130,10 +130,10 @@ final class ProductFilterAttribute extends AbstractBlock { return array( 'title' => $term_object->name, 'attributes' => array( - 'data-wc-on--click' => "$action_namespace::actions.removeFilter", + 'value' => $term, + 'data-wc-on--click' => "$action_namespace::actions.toggleFilter", 'data-wc-context' => "$action_namespace::" . wp_json_encode( array( - 'value' => $term, 'attributeSlug' => $product_attribute, 'queryType' => get_query_var( "query_type_{$product_attribute}" ), ), @@ -228,8 +228,8 @@ final class ProductFilterAttribute extends AbstractBlock { ); $filter_context = array( - 'on_change' => "{$this->get_full_block_name()}::actions.updateProducts", - 'items' => $filtered_options, + 'action' => "{$this->get_full_block_name()}::actions.toggleFilter", + 'items' => $filtered_options, ); foreach ( $block->parsed_block['innerBlocks'] as $inner_block ) { @@ -395,22 +395,25 @@ final class ProductFilterAttribute extends AbstractBlock { '
- -
- -

{{attribute_label}}

- + +
+ +

{{attribute_label}}

+ - - -
- -
- -
- + + +
+ +
+ +
+ + + +
+ -
', diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php index a4a299d9ff8..3c0fa38eddb 100644 --- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php +++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterCheckboxList.php @@ -27,29 +27,16 @@ final class ProductFilterCheckboxList extends AbstractBlock { $context = $block->context['filterData']; $items = $context['items'] ?? array(); $checkbox_list_context = array( 'items' => $items ); - $on_change = $context['on_change'] ?? ''; + $action = $context['action'] ?? ''; $namespace = wp_json_encode( array( 'namespace' => 'woocommerce/product-filter-checkbox-list' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ); + $classes = ''; + $style = ''; - $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]} );"; - } - } - ); + $tags = new \WP_HTML_Tag_Processor( $content ); + if ( $tags->next_tag( array( 'class_name' => 'wc-block-product-filter-checkbox-list' ) ) ) { + $classes = $tags->get_attribute( 'class' ); + $style = $tags->get_attribute( 'style' ); + } $checked_items = array_filter( $items, @@ -64,7 +51,7 @@ final class ProductFilterCheckboxList extends AbstractBlock { $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 ) ), + 'class' => esc_attr( $classes ), 'style' => esc_attr( $style ), ); @@ -84,8 +71,9 @@ final class ProductFilterCheckboxList extends AbstractBlock { if ( ! $item['selected'] ) : if ( $count >= $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" + data-wc-bind--hidden="!context.showAll" + hidden @@ -104,7 +92,7 @@ final class ProductFilterCheckboxList extends AbstractBlock { aria-invalid="false" aria-label="" data-wc-on--change--select-item="actions.selectCheckboxItem" - data-wc-on--change--parent-action="" + data-wc-on--change--parent-action="" value="" > @@ -120,36 +108,17 @@ final class ProductFilterCheckboxList extends AbstractBlock { $show_initially ) : ?> - + +
context['filterData']; + $items = $context['items'] ?? array(); + $checkbox_list_context = array( 'items' => $items ); + $action = $context['action'] ?? ''; + $namespace = wp_json_encode( array( 'namespace' => 'woocommerce/product-filter-chips' ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ); + + $tags = new \WP_HTML_Tag_Processor( $content ); + if ( $tags->next_tag( array( 'class_name' => 'wc-block-product-filter-chips' ) ) ) { + $classes = $tags->get_attribute( 'class' ); + $style = $tags->get_attribute( 'style' ); + } + + $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' => esc_attr( $classes ), + 'style' => esc_attr( $style ), + ); + + ob_start(); + ?> +
> +
+ + + + +
+ $show_initially ) : ?> + + +
+ false, 'hasPageWithWordPressAdminBar' => false, + 'params' => $this->get_filter_query_params( 0 ), + 'originalParams' => $this->get_filter_query_params( 0 ), ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ); $tags->set_attribute( 'data-wc-navigation-id', $this->generate_navigation_id( $block ) ); + $tags->set_attribute( 'data-wc-watch', 'callbacks.maybeNavigate' ); if ( 'always' === $attributes['overlay'] || @@ -171,4 +174,45 @@ class ProductFilters extends AbstractBlock { md5( wp_json_encode( $block->parsed_block['innerBlocks'] ) ) ); } + + /** + * Parse the filter parameters from the URL. + * For now we only get the global query params from the URL. In the future, + * we should get the query params based on $query_id. + * + * @param int $query_id Query ID. + * @return array Parsed filter params. + */ + private function get_filter_query_params( $query_id ) { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; + + $parsed_url = wp_parse_url( esc_url_raw( $request_uri ) ); + + if ( empty( $parsed_url['query'] ) ) { + return array(); + } + + parse_str( $parsed_url['query'], $url_query_params ); + + /** + * Filters the active filter data provided by filter blocks. + * + * @since 11.7.0 + * + * @param array $filter_param_keys The active filters data + * @param array $url_param_keys The query param parsed from the URL. + * + * @return array Active filters params. + */ + $filter_param_keys = array_unique( apply_filters( 'collection_filter_query_param_keys', array(), array_keys( $url_query_params ) ) ); + + return array_filter( + $url_query_params, + function ( $key ) use ( $filter_param_keys ) { + return in_array( $key, $filter_param_keys, true ); + }, + ARRAY_FILTER_USE_KEY + ); + } }