[Experimental] Add styling controls (#52598)

* Add alignment controls

* Add changefile(s) from automation for the following project(s): woocommerce-blocks

* Fix justication and orientation layouts

* Move styling settings to inner blocks

* Add e2e tests

* Fix chips not displaying correctly on first load

* Fix security error

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Roy Ho 2024-11-11 13:44:03 -08:00 committed by GitHub
parent d43b3af43f
commit dd38a6ed2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 285 additions and 48 deletions

View File

@ -14,7 +14,29 @@
"woocommerce/product-filters" "woocommerce/product-filters"
], ],
"supports": { "supports": {
"interactivity": true "interactivity": true,
"__experimentalBorder": {
"color": true,
"radius": true,
"style": true,
"width": true,
"__experimentalDefaultControls": {
"color": false,
"radius": false,
"style": false,
"width": false
}
},
"spacing": {
"margin": true,
"padding": true,
"blockGap": false,
"__experimentalDefaultControls": {
"margin": false,
"padding": false,
"blockGap": false
}
}
}, },
"usesContext": [ "usesContext": [
"filterParams" "filterParams"

View File

@ -18,7 +18,8 @@ import { EditProps } from './types';
import { filtersPreview } from './constants'; import { filtersPreview } from './constants';
const Edit = ( props: EditProps ) => { const Edit = ( props: EditProps ) => {
const { clearButton } = props.attributes; const { attributes } = props;
const { clearButton } = attributes;
const { children, ...innerBlocksProps } = useInnerBlocksProps( const { children, ...innerBlocksProps } = useInnerBlocksProps(
useBlockProps(), useBlockProps(),

View File

@ -13,7 +13,17 @@
"ancestor": [ "ancestor": [
"woocommerce/product-filter-active" "woocommerce/product-filter-active"
], ],
"supports": {}, "supports": {
"layout": {
"allowSwitching": false,
"allowInheriting": false,
"allowJustification": false,
"allowVerticalAlignment": false,
"default": {
"type": "flex"
}
}
},
"usesContext": [ "usesContext": [
"queryId", "queryId",
"filterData" "filterData"

View File

@ -3,11 +3,15 @@
*/ */
import { __, sprintf } from '@wordpress/i18n'; import { __, sprintf } from '@wordpress/i18n';
import clsx from 'clsx'; import clsx from 'clsx';
import { Icon, closeSmall } from '@wordpress/icons'; import { Icon, closeSmall, arrowRight, arrowDown } from '@wordpress/icons';
import { Label } from '@woocommerce/blocks-components'; import { Label } from '@woocommerce/blocks-components';
import { ToolbarGroup, ToolbarButton } from '@wordpress/components';
import { getBlockSupport } from '@wordpress/blocks';
import { import {
InspectorControls, InspectorControls,
useBlockProps, useBlockProps,
useInnerBlocksProps,
BlockControls,
withColors, withColors,
// @ts-expect-error - no types. // @ts-expect-error - no types.
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis // eslint-disable-next-line @wordpress/no-unsafe-wp-apis
@ -26,6 +30,7 @@ import { getColorClasses, getColorVars } from './utils';
const Edit = ( props: EditProps ): JSX.Element => { const Edit = ( props: EditProps ): JSX.Element => {
const colorGradientSettings = useMultipleOriginColorsAndGradients(); const colorGradientSettings = useMultipleOriginColorsAndGradients();
const { const {
name,
context, context,
clientId, clientId,
attributes, attributes,
@ -37,11 +42,16 @@ const Edit = ( props: EditProps ): JSX.Element => {
chipBorder, chipBorder,
setChipBorder, setChipBorder,
} = props; } = props;
const { customChipText, customChipBackground, customChipBorder } = const { customChipText, customChipBackground, customChipBorder, layout } =
attributes; attributes;
const { filterData } = context; const { filterData } = context;
const { items } = filterData; const { items } = filterData;
// Extract attributes from block layout
const layoutBlockSupport = getBlockSupport( name, 'layout' );
const defaultBlockLayout = layoutBlockSupport?.default;
const usedLayout = layout || defaultBlockLayout || {};
const blockProps = useBlockProps( { const blockProps = useBlockProps( {
className: clsx( 'wc-block-product-filter-removable-chips', { className: clsx( 'wc-block-product-filter-removable-chips', {
...getColorClasses( attributes ), ...getColorClasses( attributes ),
@ -49,6 +59,7 @@ const Edit = ( props: EditProps ): JSX.Element => {
style: getColorVars( attributes ), style: getColorVars( attributes ),
} ); } );
const innerBlocksProps = useInnerBlocksProps( blockProps, {} );
const removeText = ( label: string ): string => { const removeText = ( label: string ): string => {
return sprintf( return sprintf(
/* translators: %s attribute value used in the filter. For example: yellow, green, small, large. */ /* translators: %s attribute value used in the filter. For example: yellow, green, small, large. */
@ -58,33 +69,64 @@ const Edit = ( props: EditProps ): JSX.Element => {
}; };
return ( return (
<> <div { ...innerBlocksProps }>
<div { ...blockProps }> <BlockControls>
<ul className="wc-block-product-filter-removable-chips__items"> <ToolbarGroup>
{ items?.map( ( item, index ) => ( <ToolbarButton
<li icon={ arrowRight }
key={ index } label={ __( 'Horizontal', 'woocommerce' ) }
className="wc-block-product-filter-removable-chips__item" onClick={ () =>
> setAttributes( {
<span className="wc-block-product-filter-removable-chips__label"> layout: {
{ item.type + ': ' + item.label } ...usedLayout,
</span> orientation: 'horizontal',
<button className="wc-block-product-filter-removable-chips__remove"> },
<Icon } )
className="wc-block-product-filter-removable-chips__remove-icon" }
icon={ closeSmall } isPressed={
size={ 25 } usedLayout.orientation === 'horizontal' ||
/> ! usedLayout.orientation
<Label }
screenReaderLabel={ removeText( />
item.type + ': ' + item.label <ToolbarButton
) } icon={ arrowDown }
/> label={ __( 'Vertical', 'woocommerce' ) }
</button> onClick={ () =>
</li> setAttributes( {
) ) } layout: {
</ul> ...usedLayout,
</div> orientation: 'vertical',
},
} )
}
isPressed={ usedLayout.orientation === 'vertical' }
/>
</ToolbarGroup>
</BlockControls>
<ul className="wc-block-product-filter-removable-chips__items">
{ items?.map( ( item, index ) => (
<li
key={ index }
className="wc-block-product-filter-removable-chips__item"
>
<span className="wc-block-product-filter-removable-chips__label">
{ item.type + ': ' + item.label }
</span>
<button className="wc-block-product-filter-removable-chips__remove">
<Icon
className="wc-block-product-filter-removable-chips__remove-icon"
icon={ closeSmall }
size={ 25 }
/>
<Label
screenReaderLabel={ removeText(
item.type + ': ' + item.label
) }
/>
</button>
</li>
) ) }
</ul>
<InspectorControls group="color"> <InspectorControls group="color">
{ colorGradientSettings.hasColorsOrGradients && ( { colorGradientSettings.hasColorsOrGradients && (
<ColorGradientSettingsDropdown <ColorGradientSettingsDropdown
@ -147,7 +189,7 @@ const Edit = ( props: EditProps ): JSX.Element => {
/> />
) } ) }
</InspectorControls> </InspectorControls>
</> </div>
); );
}; };

View File

@ -1,5 +1,29 @@
.is-vertical {
.wc-block-product-filter-removable-chips__items {
flex-direction: column;
}
}
.is-content-justification-center {
.wc-block-product-filter-removable-chips__items {
justify-content: center;
}
}
.is-content-justification-right {
.wc-block-product-filter-removable-chips__items {
justify-content: flex-end;
}
}
.is-content-justification-space-between {
.wc-block-product-filter-removable-chips__items {
justify-content: space-between;
}
}
.wc-block-product-filter-removable-chips__items { .wc-block-product-filter-removable-chips__items {
display: flex; display: inline-flex;
gap: 4px; gap: 4px;
flex-wrap: wrap; flex-wrap: wrap;
list-style: none; list-style: none;
@ -16,13 +40,14 @@
font-size: 0.875em; font-size: 0.875em;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
.wc-block-product-filter-removable-chips__remove { .wc-block-product-filter-removable-chips__remove {
background: none; background: none;
border: none; border: none;
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
margin: 0 0 0 5px; margin: 0;
display: flex; display: flex;
align-items: center; align-items: center;
fill: var(--wc-product-filter-removable-chips-text, currentColor); fill: var(--wc-product-filter-removable-chips-text, currentColor);

View File

@ -23,6 +23,9 @@ export type BlockAttributes = {
customChipBackground?: string; customChipBackground?: string;
chipBorder?: string; chipBorder?: string;
customChipBorder?: string; customChipBorder?: string;
layout: {
orientation: string;
};
}; };
export type EditProps = BlockEditProps< BlockAttributes > & { export type EditProps = BlockEditProps< BlockAttributes > & {
@ -34,4 +37,5 @@ export type EditProps = BlockEditProps< BlockAttributes > & {
setChipBackground: ( value: string ) => void; setChipBackground: ( value: string ) => void;
chipBorder: Color; chipBorder: Color;
setChipBorder: ( value: string ) => void; setChipBorder: ( value: string ) => void;
name: string;
}; };

View File

@ -0,0 +1,127 @@
/**
* External dependencies
*/
import { test as base, expect } from '@woocommerce/e2e-utils';
/**
* Internal dependencies
*/
import { ProductFiltersPage } from './product-filters.page';
const blockData = {
name: 'woocommerce/product-filter-active',
selectors: {
frontend: {},
editor: {
settings: {},
label: 'Block: Active (Experimental)',
innerBlocks: {
chips: {
label: 'Block: Chips',
},
},
},
},
slug: 'archive-product',
};
const test = base.extend< { pageObject: ProductFiltersPage } >( {
pageObject: async ( { page, editor, frontendUtils }, use ) => {
const pageObject = new ProductFiltersPage( {
page,
editor,
frontendUtils,
} );
await use( pageObject );
},
} );
test.describe( `${ blockData.name }`, () => {
test.beforeEach( async ( { admin, requestUtils } ) => {
await requestUtils.setFeatureFlag( 'experimental-blocks', true );
await admin.visitSiteEditor( {
postId: `woocommerce/woocommerce//${ blockData.slug }`,
postType: 'wp_template',
canvas: 'edit',
} );
} );
test( 'should display the correct inspector layout controls', async ( {
editor,
pageObject,
} ) => {
await pageObject.addProductFiltersBlock( { cleanContent: true } );
const activeBlock = editor.canvas.getByLabel(
blockData.selectors.editor.label
);
await expect( activeBlock ).toBeVisible();
await activeBlock.click();
const chipsBlock = editor.canvas.getByLabel(
blockData.selectors.editor.innerBlocks.chips.label
);
await expect( chipsBlock ).toBeVisible();
await chipsBlock.click();
await editor.openDocumentSettingsSidebar();
await expect( editor.page.getByText( 'Justification' ) ).toBeVisible();
await expect( editor.page.getByText( 'Orientation' ) ).toBeVisible();
} );
test( 'should add correct layout CSS class when modifying layout settings', async ( {
editor,
pageObject,
} ) => {
await pageObject.addProductFiltersBlock( { cleanContent: true } );
const activeBlock = editor.canvas.getByLabel(
blockData.selectors.editor.label
);
await expect( activeBlock ).toBeVisible();
await activeBlock.click();
const chipsBlock = editor.canvas.getByLabel(
blockData.selectors.editor.innerBlocks.chips.label
);
await expect( chipsBlock ).toBeVisible();
await chipsBlock.click();
await editor.openDocumentSettingsSidebar();
await editor.page.getByLabel( 'Space between items' ).click();
await expect( chipsBlock ).toHaveClass(
/is-content-justification-space-between/
);
await editor.page.getByLabel( 'Justify items right' ).click();
await expect( chipsBlock ).toHaveClass(
/is-content-justification-right/
);
await editor.page.getByLabel( 'Justify items center' ).click();
await expect( chipsBlock ).toHaveClass(
/is-content-justification-center/
);
await editor.page.getByLabel( 'Justify items left' ).click();
await expect( chipsBlock ).toHaveClass(
/is-content-justification-left/
);
await editor.page.getByRole( 'button', { name: 'Horizontal' } ).click();
await expect( chipsBlock ).toHaveClass( /is-horizontal/ );
await editor.page.getByRole( 'button', { name: 'Vertical' } ).click();
await expect( chipsBlock ).toHaveClass( /is-vertical/ );
} );
} );

View File

@ -0,0 +1,4 @@
Significance: patch
Type: tweak
Comment: Adds additional styling settings to active filter block

View File

@ -24,13 +24,13 @@ final class ProductFilterRemovableChips extends AbstractBlock {
* @return string Rendered block type output. * @return string Rendered block type output.
*/ */
protected function render( $attributes, $content, $block ) { protected function render( $attributes, $content, $block ) {
if ( empty( $block->context['filterData'] ) || empty( $block->context['filterData']['items'] ) ) { $filters = array();
return '';
if ( ! empty( $block->context['filterData'] ) && ! empty( $block->context['filterData']['items'] ) ) {
$filters = $block->context['filterData']['items'];
} }
$style = ''; $style = '';
$context = $block->context['filterData'];
$filters = $context['items'] ?? array();
$tags = new \WP_HTML_Tag_Processor( $content ); $tags = new \WP_HTML_Tag_Processor( $content );
if ( $tags->next_tag( array( 'class_name' => 'wc-block-product-filter-removable-chips' ) ) ) { if ( $tags->next_tag( array( 'class_name' => 'wc-block-product-filter-removable-chips' ) ) ) {
@ -38,19 +38,21 @@ final class ProductFilterRemovableChips extends AbstractBlock {
$style = $tags->get_attribute( 'style' ); $style = $tags->get_attribute( 'style' );
} }
$wrapper_attributes = get_block_wrapper_attributes( $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-key' => wp_unique_prefixed_id( $this->get_full_block_name() ),
'data-wc-key' => wp_unique_prefixed_id( $this->get_full_block_name() ), 'class' => esc_attr( $classes ),
'class' => esc_attr( $classes ), 'style' => esc_attr( $style ),
'style' => esc_attr( $style ),
)
); );
if ( empty( $filters ) ) {
$wrapper_attributes['hidden'] = true;
}
ob_start(); ob_start();
?> ?>
<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>> <div <?php echo get_block_wrapper_attributes( $wrapper_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<?php if ( ! empty( $filters ) ) : ?> <?php if ( ! empty( $filters ) ) : ?>
<ul class="wc-block-product-filter-removable-chips__items"> <ul class="wc-block-product-filter-removable-chips__items">
<?php foreach ( $filters as $filter ) : ?> <?php foreach ( $filters as $filter ) : ?>