[Experimental] Product Filters Chips style and new interactivity API implementation (#51393)
This commit is contained in:
parent
ec29880e3e
commit
1b58098848
|
@ -2,47 +2,12 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
getContext as getContextFn,
|
getContext,
|
||||||
store,
|
store,
|
||||||
navigate as navigateFn,
|
navigate as navigateFn,
|
||||||
} from '@woocommerce/interactivity';
|
} from '@woocommerce/interactivity';
|
||||||
import { getSetting } from '@woocommerce/settings';
|
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 isBlockTheme = getSetting< boolean >( 'isBlockTheme' );
|
||||||
const isProductArchive = getSetting< boolean >( 'isProductArchive' );
|
const isProductArchive = getSetting< boolean >( 'isProductArchive' );
|
||||||
const needsRefresh = getSetting< boolean >(
|
const needsRefresh = getSetting< boolean >(
|
||||||
|
@ -50,6 +15,28 @@ const needsRefresh = getSetting< boolean >(
|
||||||
false
|
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 = {} ) {
|
export function navigate( href: string, options = {} ) {
|
||||||
/**
|
/**
|
||||||
* We may need to reset the current page when changing filters.
|
* 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 );
|
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 );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} );
|
|
@ -6,22 +6,15 @@ import { store, getContext } from '@woocommerce/interactivity';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { navigate } from '../../frontend';
|
import { ProductFiltersContext } from '../../frontend';
|
||||||
|
|
||||||
type ActiveFiltersContext = {
|
|
||||||
queryId: number;
|
|
||||||
params: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
store( 'woocommerce/product-filter-active', {
|
store( 'woocommerce/product-filter-active', {
|
||||||
actions: {
|
actions: {
|
||||||
clearAll: () => {
|
clearAll: () => {
|
||||||
const { params } = getContext< ActiveFiltersContext >();
|
const productFiltersContext = getContext< ProductFiltersContext >(
|
||||||
const url = new URL( window.location.href );
|
'woocommerce/product-filters'
|
||||||
const { searchParams } = url;
|
);
|
||||||
|
productFiltersContext.params = {};
|
||||||
params.forEach( ( param ) => searchParams.delete( param ) );
|
|
||||||
navigate( url.href );
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { store, getContext } from '@woocommerce/interactivity';
|
import { store, getContext, getElement } from '@woocommerce/interactivity';
|
||||||
import { DropdownContext } from '@woocommerce/interactivity-components/dropdown';
|
|
||||||
import { HTMLElementEvent } from '@woocommerce/types';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { navigate } from '../../frontend';
|
import { ProductFiltersContext } from '../../frontend';
|
||||||
|
|
||||||
type AttributeFilterContext = {
|
type AttributeFilterContext = {
|
||||||
attributeSlug: string;
|
attributeSlug: string;
|
||||||
|
@ -16,102 +14,72 @@ type AttributeFilterContext = {
|
||||||
selectType: 'single' | 'multiple';
|
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', {
|
store( 'woocommerce/product-filter-attribute', {
|
||||||
actions: {
|
actions: {
|
||||||
navigate: () => {
|
toggleFilter: () => {
|
||||||
const dropdownContext = getContext< DropdownContext >(
|
const { ref } = getElement();
|
||||||
'woocommerce/interactivity-dropdown'
|
const targetAttribute =
|
||||||
);
|
ref.getAttribute( 'data-attribute-value' ) ?? 'value';
|
||||||
const context = getContext< AttributeFilterContext >();
|
const termSlug = ref.getAttribute( targetAttribute );
|
||||||
const filters = dropdownContext.selectedItems
|
|
||||||
.map( ( item ) => item.value )
|
|
||||||
.filter( nonNullable );
|
|
||||||
|
|
||||||
navigate(
|
if ( ! termSlug ) return;
|
||||||
getUrl( filters, context.attributeSlug, context.queryType )
|
|
||||||
);
|
|
||||||
},
|
|
||||||
updateProducts: ( event: HTMLElementEvent< HTMLInputElement > ) => {
|
|
||||||
if ( ! event.target.value ) return;
|
|
||||||
|
|
||||||
const context = getContext< AttributeFilterContext >();
|
const { attributeSlug, queryType } =
|
||||||
|
getContext< AttributeFilterContext >();
|
||||||
let selectedTerms = getSelectedTermsFromUrl(
|
const productFiltersContext = getContext< ProductFiltersContext >(
|
||||||
context.attributeSlug
|
'woocommerce/product-filters'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
event.target.checked &&
|
! (
|
||||||
! selectedTerms.includes( event.target.value )
|
`filter_${ attributeSlug }` in productFiltersContext.params
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
if ( context.selectType === 'multiple' )
|
productFiltersContext.params = {
|
||||||
selectedTerms.push( event.target.value );
|
...productFiltersContext.params,
|
||||||
if ( context.selectType === 'single' )
|
[ `filter_${ attributeSlug }` ]: termSlug,
|
||||||
selectedTerms = [ event.target.value ];
|
[ `query_type_${ attributeSlug }` ]: queryType,
|
||||||
} else {
|
};
|
||||||
selectedTerms = selectedTerms.filter(
|
return;
|
||||||
( value ) => value !== event.target.value
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(
|
const selectedTerms =
|
||||||
getUrl(
|
productFiltersContext.params[
|
||||||
selectedTerms,
|
`filter_${ attributeSlug }`
|
||||||
context.attributeSlug,
|
].split( ',' );
|
||||||
context.queryType
|
if ( selectedTerms.includes( termSlug ) ) {
|
||||||
)
|
const remainingSelectedTerms = selectedTerms.filter(
|
||||||
|
( term ) => term !== termSlug
|
||||||
);
|
);
|
||||||
},
|
if ( remainingSelectedTerms.length > 0 ) {
|
||||||
removeFilter: () => {
|
productFiltersContext.params[
|
||||||
const { attributeSlug, queryType, value } =
|
`filter_${ attributeSlug }`
|
||||||
getContext< ActiveAttributeFilterContext >();
|
] = 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 );
|
productFiltersContext.params = updatedParams;
|
||||||
|
}
|
||||||
navigate( getUrl( selectedTerms, attributeSlug, queryType ) );
|
} else {
|
||||||
|
productFiltersContext.params[ `filter_${ attributeSlug }` ] =
|
||||||
|
selectedTerms.concat( termSlug ).join( ',' );
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
clearFilters: () => {
|
clearFilters: () => {
|
||||||
const { attributeSlug, queryType } =
|
const { attributeSlug } = getContext< AttributeFilterContext >();
|
||||||
getContext< ActiveAttributeFilterContext >();
|
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;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
import './editor.scss';
|
import './editor.scss';
|
||||||
import { EditProps } from './types';
|
import { EditProps } from './types';
|
||||||
|
import { getColorClasses, getColorVars } from './utils';
|
||||||
|
|
||||||
const Edit = ( props: EditProps ): JSX.Element => {
|
const Edit = ( props: EditProps ): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
|
@ -51,21 +52,9 @@ const Edit = ( props: EditProps ): JSX.Element => {
|
||||||
const blockProps = useBlockProps( {
|
const blockProps = useBlockProps( {
|
||||||
className: clsx( 'wc-block-product-filter-checkbox-list', {
|
className: clsx( 'wc-block-product-filter-checkbox-list', {
|
||||||
'is-loading': isLoading,
|
'is-loading': isLoading,
|
||||||
'has-option-element-border-color':
|
...getColorClasses( attributes ),
|
||||||
optionElementBorder.color || customOptionElementBorder,
|
|
||||||
'has-option-element-selected-color':
|
|
||||||
optionElementSelected.color || customOptionElementSelected,
|
|
||||||
'has-option-element-color':
|
|
||||||
optionElement.color || customOptionElement,
|
|
||||||
} ),
|
} ),
|
||||||
style: {
|
style: getColorVars( attributes ),
|
||||||
'--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( () => {
|
const loadingState = useMemo( () => {
|
||||||
|
@ -131,9 +120,9 @@ const Edit = ( props: EditProps ): JSX.Element => {
|
||||||
) ) }
|
) ) }
|
||||||
</ul>
|
</ul>
|
||||||
{ ! isLoading && isLongList && (
|
{ ! isLoading && isLongList && (
|
||||||
<span className="wc-block-product-filter-checkbox-list__show-more">
|
<button className="wc-block-product-filter-checkbox-list__show-more">
|
||||||
<small>{ __( 'Show more…', 'woocommerce' ) }</small>
|
{ __( 'Show more…', 'woocommerce' ) }
|
||||||
</span>
|
</button>
|
||||||
) }
|
) }
|
||||||
</div>
|
</div>
|
||||||
<InspectorControls group="color">
|
<InspectorControls group="color">
|
||||||
|
|
|
@ -11,10 +11,12 @@ import { registerBlockType } from '@wordpress/blocks';
|
||||||
import metadata from './block.json';
|
import metadata from './block.json';
|
||||||
import Edit from './edit';
|
import Edit from './edit';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
import Save from './save';
|
||||||
|
|
||||||
if ( isExperimentalBlocksEnabled() ) {
|
if ( isExperimentalBlocksEnabled() ) {
|
||||||
registerBlockType( metadata, {
|
registerBlockType( metadata, {
|
||||||
edit: Edit,
|
edit: Edit,
|
||||||
icon: productFilterOptions,
|
icon: productFilterOptions,
|
||||||
|
save: Save,
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 <div { ...blockProps } />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Save;
|
|
@ -4,11 +4,6 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wc-block-product-filter-checkbox-list__item.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
:where(.wc-block-product-filter-checkbox-list__label) {
|
:where(.wc-block-product-filter-checkbox-list__label) {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -34,6 +29,7 @@
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
.has-option-element-color & {
|
.has-option-element-color & {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -51,6 +47,7 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
background: var(--wc-product-filter-checkbox-list-option-element, transparent);
|
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 {
|
.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);
|
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) {
|
:where(.wc-block-product-filter-checkbox-list__show-more) {
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
appearance: none;
|
||||||
|
background: transparent;
|
||||||
.wc-block-product-filter-checkbox-list__show-more.hidden {
|
border: none;
|
||||||
display: none;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -15,8 +15,44 @@
|
||||||
],
|
],
|
||||||
"supports": {},
|
"supports": {},
|
||||||
"usesContext": [
|
"usesContext": [
|
||||||
"filterData",
|
"filterData"
|
||||||
"isParentSelected"
|
|
||||||
],
|
],
|
||||||
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,260 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* 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
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import './style.scss';
|
import { EditProps } from './types';
|
||||||
|
import './editor.scss';
|
||||||
|
import { getColorClasses, getColorVars } from './utils';
|
||||||
|
|
||||||
const Edit = () => {
|
const Edit = ( props: EditProps ): JSX.Element => {
|
||||||
return <div { ...useBlockProps() }>These are chips.</div>;
|
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 ) => (
|
||||||
|
<div
|
||||||
|
className="wc-block-product-filter-chips__item"
|
||||||
|
key={ i }
|
||||||
|
style={ {
|
||||||
|
/* stylelint-disable */
|
||||||
|
width: Math.floor( Math.random() * ( 100 - 25 ) ) + '%',
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
) );
|
||||||
|
}, [] );
|
||||||
|
|
||||||
|
if ( ! items ) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const threshold = 15;
|
||||||
|
const isLongList = items.length > threshold;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div { ...blockProps }>
|
||||||
|
<div className="wc-block-product-filter-chips__items">
|
||||||
|
{ isLoading && loadingState }
|
||||||
|
{ ! isLoading &&
|
||||||
|
( isLongList
|
||||||
|
? items.slice( 0, threshold )
|
||||||
|
: items
|
||||||
|
).map( ( item, index ) => (
|
||||||
|
<div
|
||||||
|
key={ index }
|
||||||
|
className="wc-block-product-filter-chips__item"
|
||||||
|
aria-checked={ !! item.selected }
|
||||||
|
>
|
||||||
|
<span className="wc-block-product-filter-chips__label">
|
||||||
|
{ item.label }
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) ) }
|
||||||
|
</div>
|
||||||
|
{ ! isLoading && isLongList && (
|
||||||
|
<button className="wc-block-product-filter-chips__show-more">
|
||||||
|
{ __( 'Show more…', 'woocommerce' ) }
|
||||||
|
</button>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
<InspectorControls group="color">
|
||||||
|
{ colorGradientSettings.hasColorsOrGradients && (
|
||||||
|
<ColorGradientSettingsDropdown
|
||||||
|
__experimentalIsRenderedInSidebar
|
||||||
|
settings={ [
|
||||||
|
{
|
||||||
|
label: __(
|
||||||
|
'Unselected Chip Text',
|
||||||
|
'woocommerce'
|
||||||
|
),
|
||||||
|
colorValue: chipText.color || customChipText,
|
||||||
|
onColorChange: ( colorValue: string ) => {
|
||||||
|
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 }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
</InspectorControls>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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 );
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
.wc-block-product-filter-chips.is-loading {
|
||||||
|
.wc-block-product-filter-chips__item {
|
||||||
|
@include placeholder();
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} );
|
|
@ -10,11 +10,13 @@ import { registerBlockType } from '@wordpress/blocks';
|
||||||
*/
|
*/
|
||||||
import metadata from './block.json';
|
import metadata from './block.json';
|
||||||
import Edit from './edit';
|
import Edit from './edit';
|
||||||
|
import Save from './save';
|
||||||
import './style.scss';
|
import './style.scss';
|
||||||
|
|
||||||
if ( isExperimentalBlocksEnabled() ) {
|
if ( isExperimentalBlocksEnabled() ) {
|
||||||
registerBlockType( metadata, {
|
registerBlockType( metadata, {
|
||||||
edit: Edit,
|
edit: Edit,
|
||||||
icon: productFilterOptions,
|
icon: productFilterOptions,
|
||||||
|
save: Save,
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 <div { ...blockProps } />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Save;
|
|
@ -1,3 +1,55 @@
|
||||||
:where(.wc-block-product-filter-chips) {
|
:where(.wc-block-product-filter-chips__items) {
|
||||||
// WIP
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,44 @@ import { BlockEditProps } from '@wordpress/blocks';
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
import { FilterBlockContext } from '../../types';
|
||||||
|
|
||||||
|
export type Color = {
|
||||||
|
slug?: string;
|
||||||
|
name?: string;
|
||||||
|
class?: string;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type BlockAttributes = {
|
export type BlockAttributes = {
|
||||||
className: string;
|
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;
|
||||||
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
Significance: patch
|
||||||
|
Type: add
|
||||||
|
Comment: [Experimental] Product Filters Chips style and new interactivity API implementation
|
||||||
|
|
||||||
|
|
|
@ -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 ) );
|
$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(
|
$wrapper_attributes = get_block_wrapper_attributes(
|
||||||
array(
|
array(
|
||||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
|
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
|
||||||
'data-wc-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ),
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -130,10 +130,10 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
return array(
|
return array(
|
||||||
'title' => $term_object->name,
|
'title' => $term_object->name,
|
||||||
'attributes' => array(
|
'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(
|
'data-wc-context' => "$action_namespace::" . wp_json_encode(
|
||||||
array(
|
array(
|
||||||
'value' => $term,
|
|
||||||
'attributeSlug' => $product_attribute,
|
'attributeSlug' => $product_attribute,
|
||||||
'queryType' => get_query_var( "query_type_{$product_attribute}" ),
|
'queryType' => get_query_var( "query_type_{$product_attribute}" ),
|
||||||
),
|
),
|
||||||
|
@ -228,7 +228,7 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
);
|
);
|
||||||
|
|
||||||
$filter_context = array(
|
$filter_context = array(
|
||||||
'on_change' => "{$this->get_full_block_name()}::actions.updateProducts",
|
'action' => "{$this->get_full_block_name()}::actions.toggleFilter",
|
||||||
'items' => $filtered_options,
|
'items' => $filtered_options,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -410,7 +410,10 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||||
<!-- /wp:woocommerce/product-filter-clear-button --></div>
|
<!-- /wp:woocommerce/product-filter-clear-button --></div>
|
||||||
<!-- /wp:group -->
|
<!-- /wp:group -->
|
||||||
|
|
||||||
<!-- wp:woocommerce/product-filter-checkbox-list {"lock":{"remove":true},"className":"wp-block-woocommerce-product-filter-checkbox-list"} /-->
|
<!-- wp:woocommerce/product-filter-checkbox-list {"lock":{"remove":true}} -->
|
||||||
|
<div class="wp-block-woocommerce-product-filter-checkbox-list wc-block-product-filter-checkbox-list"></div>
|
||||||
|
<!-- /wp:woocommerce/product-filter-checkbox-list -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- /wp:woocommerce/product-filter-attribute -->
|
<!-- /wp:woocommerce/product-filter-attribute -->
|
||||||
',
|
',
|
||||||
|
|
|
@ -27,29 +27,16 @@ final class ProductFilterCheckboxList extends AbstractBlock {
|
||||||
$context = $block->context['filterData'];
|
$context = $block->context['filterData'];
|
||||||
$items = $context['items'] ?? array();
|
$items = $context['items'] ?? array();
|
||||||
$checkbox_list_context = array( 'items' => $items );
|
$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 );
|
$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(
|
$tags = new \WP_HTML_Tag_Processor( $content );
|
||||||
'has-option-element-border-color' => $this->get_color_attribute_value( 'optionElementBorder', $attributes ),
|
if ( $tags->next_tag( array( 'class_name' => 'wc-block-product-filter-checkbox-list' ) ) ) {
|
||||||
'has-option-element-selected-color' => $this->get_color_attribute_value( 'optionElementSelected', $attributes ),
|
$classes = $tags->get_attribute( 'class' );
|
||||||
'has-option-element-color' => $this->get_color_attribute_value( 'optionElement', $attributes ),
|
$style = $tags->get_attribute( 'style' );
|
||||||
);
|
|
||||||
$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(
|
$checked_items = array_filter(
|
||||||
$items,
|
$items,
|
||||||
|
@ -64,7 +51,7 @@ final class ProductFilterCheckboxList extends AbstractBlock {
|
||||||
$wrapper_attributes = array(
|
$wrapper_attributes = array(
|
||||||
'data-wc-interactive' => esc_attr( $namespace ),
|
'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 ),
|
'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 ),
|
'style' => esc_attr( $style ),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -84,8 +71,9 @@ final class ProductFilterCheckboxList extends AbstractBlock {
|
||||||
if ( ! $item['selected'] ) :
|
if ( ! $item['selected'] ) :
|
||||||
if ( $count >= $remaining_initial_unchecked ) :
|
if ( $count >= $remaining_initial_unchecked ) :
|
||||||
?>
|
?>
|
||||||
class="wc-block-product-filter-checkbox-list__item hidden"
|
class="wc-block-product-filter-checkbox-list__item"
|
||||||
data-wc-class--hidden="!context.showAll"
|
data-wc-bind--hidden="!context.showAll"
|
||||||
|
hidden
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<?php ++$count; ?>
|
<?php ++$count; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
@ -104,7 +92,7 @@ final class ProductFilterCheckboxList extends AbstractBlock {
|
||||||
aria-invalid="false"
|
aria-invalid="false"
|
||||||
aria-label="<?php echo esc_attr( $i18n_label ); ?>"
|
aria-label="<?php echo esc_attr( $i18n_label ); ?>"
|
||||||
data-wc-on--change--select-item="actions.selectCheckboxItem"
|
data-wc-on--change--select-item="actions.selectCheckboxItem"
|
||||||
data-wc-on--change--parent-action="<?php echo esc_attr( $on_change ); ?>"
|
data-wc-on--change--parent-action="<?php echo esc_attr( $action ); ?>"
|
||||||
value="<?php echo esc_attr( $item['value'] ); ?>"
|
value="<?php echo esc_attr( $item['value'] ); ?>"
|
||||||
<?php checked( $item['selected'], 1 ); ?>
|
<?php checked( $item['selected'], 1 ); ?>
|
||||||
>
|
>
|
||||||
|
@ -120,36 +108,17 @@ final class ProductFilterCheckboxList extends AbstractBlock {
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
</ul>
|
</ul>
|
||||||
<?php if ( count( $items ) > $show_initially ) : ?>
|
<?php if ( count( $items ) > $show_initially ) : ?>
|
||||||
<span
|
<button
|
||||||
role="button"
|
|
||||||
class="wc-block-product-filter-checkbox-list__show-more"
|
class="wc-block-product-filter-checkbox-list__show-more"
|
||||||
data-wc-class--hidden="context.showAll"
|
data-wc-bind--hidden="context.showAll"
|
||||||
data-wc-on--click="actions.showAllItems"
|
data-wc-on--click="actions.showAllItems"
|
||||||
|
hidden
|
||||||
>
|
>
|
||||||
<small role="presentation"><?php echo esc_html__( 'Show more...', 'woocommerce' ); ?></small>
|
<?php echo esc_html__( 'Show more...', 'woocommerce' ); ?>
|
||||||
</span>
|
</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
return ob_get_clean();
|
return ob_get_clean();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the color value from the color attributes.
|
|
||||||
*
|
|
||||||
* @param string $key The key of the color attribute.
|
|
||||||
* @param array $attributes The block attributes.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function get_color_attribute_value( $key, $attributes ) {
|
|
||||||
if ( $attributes[ $key ] ) {
|
|
||||||
return $attributes[ $key ];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( $attributes[ 'custom' . ucfirst( $key ) ] ) {
|
|
||||||
return $attributes[ 'custom' . ucfirst( $key ) ];
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,4 +14,90 @@ final class ProductFilterChips extends AbstractBlock {
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $block_name = 'product-filter-chips';
|
protected $block_name = 'product-filter-chips';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 ) {
|
||||||
|
$classes = '';
|
||||||
|
$style = '';
|
||||||
|
$context = $block->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();
|
||||||
|
?>
|
||||||
|
<div <?php echo get_block_wrapper_attributes( $wrapper_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
|
||||||
|
<div class="wc-block-product-filter-chips__items" aria-label="<?php echo esc_attr__( 'Filter Options', 'woocommerce' ); ?>">
|
||||||
|
<?php foreach ( $items as $item ) { ?>
|
||||||
|
<?php $item['id'] = $item['id'] ?? uniqid( 'chips-' ); ?>
|
||||||
|
<button
|
||||||
|
data-wc-key="<?php echo esc_attr( $item['id'] ); ?>"
|
||||||
|
<?php
|
||||||
|
if ( ! $item['selected'] ) :
|
||||||
|
if ( $count >= $remaining_initial_unchecked ) :
|
||||||
|
?>
|
||||||
|
class="wc-block-product-filter-chips__item"
|
||||||
|
data-wc-bind--hidden="!context.showAll"
|
||||||
|
hidden
|
||||||
|
<?php else : ?>
|
||||||
|
<?php ++$count; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
class="wc-block-product-filter-chips__item"
|
||||||
|
data-wc-on--click--select-item="actions.selectItem"
|
||||||
|
data-wc-on--click--parent-action="<?php echo esc_attr( $action ); ?>"
|
||||||
|
value="<?php echo esc_attr( $item['value'] ); ?>"
|
||||||
|
aria-checked="<?php echo $item['selected'] ? 'true' : 'false'; ?>"
|
||||||
|
>
|
||||||
|
<span class="wc-block-product-filter-chips__label">
|
||||||
|
<?php echo wp_kses_post( $item['label'] ); ?>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
<?php if ( count( $items ) > $show_initially ) : ?>
|
||||||
|
<button
|
||||||
|
class="wc-block-product-filter-chips__show-more"
|
||||||
|
data-wc-bind--hidden="context.showAll"
|
||||||
|
data-wc-on--click="actions.showAllItems"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<?php echo esc_html__( 'Show more...', 'woocommerce' ); ?>
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,11 +142,14 @@ class ProductFilters extends AbstractBlock {
|
||||||
array(
|
array(
|
||||||
'isDialogOpen' => false,
|
'isDialogOpen' => false,
|
||||||
'hasPageWithWordPressAdminBar' => 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
|
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-navigation-id', $this->generate_navigation_id( $block ) );
|
||||||
|
$tags->set_attribute( 'data-wc-watch', 'callbacks.maybeNavigate' );
|
||||||
|
|
||||||
if (
|
if (
|
||||||
'always' === $attributes['overlay'] ||
|
'always' === $attributes['overlay'] ||
|
||||||
|
@ -171,4 +174,45 @@ class ProductFilters extends AbstractBlock {
|
||||||
md5( wp_json_encode( $block->parsed_block['innerBlocks'] ) )
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue