Modify components empty state (#47487)

* Create EmptyState component

* Create getEmptyStateSequentialNames util

* Use EmptyState component in Custom fields

* Use EmptyState in AttributeControl

* Use EmptyState component in VariationItems

* Remove not used references

* Accept empty strings

* Add tests

* Add changelog

* Improve rows opacity

* Add i18n to Attributre

* Fix lint
This commit is contained in:
Fernando Marichal 2024-05-21 10:07:38 -03:00 committed by GitHub
parent 9213073782
commit bf7204f119
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 240 additions and 346 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Modify components empty state #47487

View File

@ -28,7 +28,7 @@ import { VariableProductTour } from './variable-product-tour';
import { TRACKS_SOURCE } from '../../../constants';
import { handlePrompt } from '../../../utils/handle-prompt';
import { ProductEditorBlockEditProps } from '../../../types';
import { EmptyState } from './empty-state';
import { EmptyState } from '../../../components/empty-state';
export function Edit( {
attributes,
@ -183,7 +183,15 @@ export function Edit( {
: '';
if ( ! hasVariationOptions ) {
return <EmptyState />;
return (
<EmptyState
names={ [
__( 'Variation', 'woocommerce' ),
__( 'Colors', 'woocommerce' ),
__( 'Sizes', 'woocommerce' ),
] }
/>
);
}
return (

View File

@ -1,5 +1,3 @@
@import "empty-state/style.scss";
.wp-block-woocommerce-product-variation-items-field {
@media ( min-width: #{ ($break-medium) } ) {
min-height: 420px;

View File

@ -1,54 +0,0 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
export function EmptyState(
props: React.DetailedHTMLProps<
React.HTMLAttributes< HTMLDivElement >,
HTMLDivElement
>
) {
return (
<div
{ ...props }
role="none"
className="wp-block-woocommerce-product-variation-items-field__empty-state"
>
<div className="wp-block-woocommerce-product-variation-items-field__empty-state-row">
<div>{ __( 'Variation', 'woocommerce' ) }</div>
<div>
<div className="wp-block-woocommerce-product-variation-items-field__empty-state-name" />
</div>
<div>
<div className="wp-block-woocommerce-product-variation-items-field__empty-state-actions" />
</div>
</div>
<div className="wp-block-woocommerce-product-variation-items-field__empty-state-row">
<div>{ __( 'Colors', 'woocommerce' ) }</div>
<div>
<div className="wp-block-woocommerce-product-variation-items-field__empty-state-name" />
</div>
<div>
<div className="wp-block-woocommerce-product-variation-items-field__empty-state-actions" />
</div>
</div>
<div className="wp-block-woocommerce-product-variation-items-field__empty-state-row">
<div>{ __( 'Sizes', 'woocommerce' ) }</div>
<div>
<div className="wp-block-woocommerce-product-variation-items-field__empty-state-name" />
</div>
<div>
<div className="wp-block-woocommerce-product-variation-items-field__empty-state-actions" />
</div>
</div>
</div>
);
}

View File

@ -1,59 +0,0 @@
.wp-block-woocommerce-product-variation-items-field__empty-state {
@mixin skeleton {
@include placeholder();
background-color: $gray-200;
border-radius: $grid-unit-05;
width: $grid-unit-30;
height: $grid-unit;
}
border: 1px dashed $gray-400;
padding: 0 $grid-unit-30;
border-radius: 2px;
&-row {
display: grid;
grid-template-columns: 1.5fr 1fr 0.5fr;
height: $grid-unit-80;
align-items: center;
border-top: 1px solid $gray-100;
&:first-child {
border-top: none;
.wp-block-woocommerce-product-variation-items-field__empty-state-name {
width: 140px;
}
}
&:nth-child(2) {
opacity: 0.7;
.wp-block-woocommerce-product-variation-items-field__empty-state-name {
width: 75px;
}
}
&:nth-child(3) {
opacity: 0.5;
.wp-block-woocommerce-product-variation-items-field__empty-state-name {
width: 114px;
}
}
:last-child {
display: flex;
justify-content: flex-end;
}
}
&-name {
@include skeleton();
}
&-actions {
@include skeleton();
width: $grid-unit-60;
}
}

View File

@ -33,9 +33,10 @@ import { AttributeListItem } from '../attribute-list-item';
import { NewAttributeModal } from './new-attribute-modal';
import { RemoveConfirmationModal } from '../remove-confirmation-modal';
import { TRACKS_SOURCE } from '../../constants';
import { AttributeEmptyStateSkeleton } from './attribute-empty-state-skeleton';
import { EmptyState } from '../empty-state';
import { SectionActions } from '../block-slot-fill';
import { AttributeControlProps } from './types';
import { getEmptyStateSequentialNames } from '../../utils';
export const AttributeControl: React.FC< AttributeControlProps > = ( {
value,
@ -215,7 +216,14 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
} );
}
return <AttributeEmptyStateSkeleton />;
return (
<EmptyState
names={ getEmptyStateSequentialNames(
__( 'Attribute', 'woocommerce' ),
3
) }
/>
);
}
function renderSectionActions() {

View File

@ -1,49 +0,0 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import classNames from 'classnames';
export function AttributeEmptyStateSkeleton() {
return (
<div className="woocommerce-product-page-attribute-skeleton">
{ Array( 3 )
.fill( 0 )
.map( ( _, index ) => (
<div
key={ index }
className="woocommerce-product-page-attribute-skeleton__row"
>
<div
className={ classNames(
'woocommerce-product-page-attribute-skeleton__item'
) }
>
<div
className={ classNames(
`woocommerce-product-page-attribute-skeleton__name${ index }`,
`woocommerce-product-page-attribute-skeleton__row${ index }`
) }
></div>
</div>
<div className="woocommerce-product-page-attribute-skeleton__item">
<div
className={ classNames(
`woocommerce-product-page-attribute-skeleton__value${ index }`,
`woocommerce-product-page-attribute-skeleton__row${ index }`
) }
></div>
</div>
<div className="woocommerce-product-page-attribute-skeleton__last-item">
<div
className={ classNames(
'woocommerce-product-page-attribute-skeleton__buttons',
`woocommerce-product-page-attribute-skeleton__row${ index }`
) }
></div>
</div>
</div>
) ) }
</div>
);
}

View File

@ -1,61 +0,0 @@
@mixin attributeSkeleton {
border-radius: $grid-unit-05;
min-width: $grid-unit-20;
min-height: $grid-unit-20;
color: transparent;
}
.woocommerce-product-page-attribute-skeleton {
border: 1px dashed $gray-200;
padding: 0 $gap-large 0 $gap-large;
&__row {
display: grid;
grid-template-columns: 45% auto 90px;
padding-bottom: $gap-large;
padding-top: $gap-large;
border-bottom: 1px solid $gray-200;
}
&__row:last-child {
border-bottom: none;
}
&__last-item {
justify-self: end;
}
&__row0 {
background-color: $gray-200;
}
&__row1 {
background-color: #e9e9e9;
}
&__row2 {
background-color: #efefef;
}
&__name0 {
@include attributeSkeleton();
width: 40%;
}
&__value0 {
@include attributeSkeleton();
width: 40%;
}
&__name1 {
@include attributeSkeleton();
width: 30%;
}
&__value1 {
@include attributeSkeleton();
width: 30%;
}
&__name2 {
@include attributeSkeleton();
width: 35%;
}
&__value2 {
@include attributeSkeleton();
width: 35%;
}
&__buttons {
@include attributeSkeleton();
width: 50px;
}
}

View File

@ -15,9 +15,10 @@ import { TRACKS_SOURCE } from '../../constants';
import { useCustomFields } from '../../hooks/use-custom-fields';
import { CreateModal } from './create-modal';
import { EditModal } from './edit-modal';
import { EmptyState } from './empty-state';
import { EmptyState } from '../empty-state';
import type { Metadata } from '../../types';
import type { CustomFieldsProps } from './types';
import { getEmptyStateSequentialNames } from '../../utils';
export function CustomFields( {
className,
@ -106,7 +107,12 @@ export function CustomFields( {
) }
{ customFields.length === 0 ? (
<EmptyState />
<EmptyState
names={ getEmptyStateSequentialNames(
__( 'Custom field', 'woocommerce' ),
3
) }
/>
) : (
<table
{ ...props }

View File

@ -1,54 +0,0 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
export function EmptyState(
props: React.DetailedHTMLProps<
React.HTMLAttributes< HTMLDivElement >,
HTMLDivElement
>
) {
return (
<div
{ ...props }
role="none"
className="woocommerce-product-custom-fields__empty-state"
>
<div className="woocommerce-product-custom-fields__empty-state-row">
<div>{ __( 'Custom field 1', 'woocommerce' ) }</div>
<div>
<div className="woocommerce-product-custom-fields__empty-state-name" />
</div>
<div>
<div className="woocommerce-product-custom-fields__empty-state-actions" />
</div>
</div>
<div className="woocommerce-product-custom-fields__empty-state-row">
<div>{ __( 'Custom field 2', 'woocommerce' ) }</div>
<div>
<div className="woocommerce-product-custom-fields__empty-state-name" />
</div>
<div>
<div className="woocommerce-product-custom-fields__empty-state-actions" />
</div>
</div>
<div className="woocommerce-product-custom-fields__empty-state-row">
<div>{ __( 'Custom field 3', 'woocommerce' ) }</div>
<div>
<div className="woocommerce-product-custom-fields__empty-state-name" />
</div>
<div>
<div className="woocommerce-product-custom-fields__empty-state-actions" />
</div>
</div>
</div>
);
}

View File

@ -1 +0,0 @@
export * from './empty-state';

View File

@ -1,58 +0,0 @@
.woocommerce-product-custom-fields__empty-state {
@mixin skeleton {
background-color: $gray-200;
border-radius: $grid-unit-05;
width: $grid-unit-30;
height: $grid-unit;
}
border: 1px dashed $gray-400;
padding: 0 $grid-unit-30;
border-radius: 2px;
&-row {
display: grid;
grid-template-columns: 1.5fr 1fr 0.5fr;
height: $grid-unit-80;
align-items: center;
border-top: 1px solid $gray-100;
&:first-child {
border-top: none;
.woocommerce-product-custom-fields__empty-state-name {
width: 140px;
}
}
&:nth-child(2) {
opacity: 0.7;
.woocommerce-product-custom-fields__empty-state-name {
width: 75px;
}
}
&:nth-child(3) {
opacity: 0.5;
.woocommerce-product-custom-fields__empty-state-name {
width: 114px;
}
}
:last-child {
display: flex;
justify-content: flex-end;
}
}
&-name {
@include skeleton();
}
&-actions {
@include skeleton();
width: $grid-unit-60;
}
}

View File

@ -1,6 +1,5 @@
@import "./create-modal/style.scss";
@import "./edit-modal/style.scss";
@import "./empty-state/style.scss";
.woocommerce-product-custom-fields {
&__table {

View File

@ -0,0 +1,40 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
type EmptyStateProps = React.DetailedHTMLProps<
React.HTMLAttributes< HTMLDivElement >,
HTMLDivElement
> & {
names: string[];
};
export function EmptyState( { names = [], ...props }: EmptyStateProps ) {
return (
<div
{ ...props }
role="none"
className="woocommerce-product-empty-state"
>
{ names.map( ( name ) => (
<div
key={ name }
className="woocommerce-product-empty-state__row"
>
{ name === '' ? (
<div className="woocommerce-product-empty-state__name" />
) : (
<div>{ name }</div>
) }
<div>
<div className="woocommerce-product-empty-state__value" />
</div>
<div>
<div className="woocommerce-product-empty-state__actions" />
</div>
</div>
) ) }
</div>
);
}

View File

@ -0,0 +1,86 @@
.woocommerce-product-empty-state {
@mixin skeleton {
background-color: $gray-200;
border-radius: $grid-unit-05;
width: $grid-unit-30;
height: $grid-unit;
}
border: 1px dashed $gray-400;
padding: 0 $grid-unit-30;
border-radius: 2px;
&__row {
display: grid;
grid-template-columns: 1.5fr 1fr 0.5fr;
height: $grid-unit-80;
align-items: center;
// Apply border-top to all rows except the first one
&:not( :first-child ) {
border-top: 1px solid $gray-100;
}
&:nth-of-type( 3n + 1 ) {
.woocommerce-product-empty-state__name {
width: 85px;
}
.woocommerce-product-empty-state__value {
width: 140px;
}
}
&:nth-of-type( 3n + 2 ) {
.woocommerce-product-empty-state__name {
width: 120px;
}
.woocommerce-product-empty-state__value {
width: 75px;
}
}
&:nth-of-type( 3n + 3 ) {
.woocommerce-product-empty-state__name {
width: 100px;
}
.woocommerce-product-empty-state__value {
width: 114px;
}
}
// Decreasing opacity based on position
@for $i from 1 through 100 {
&:nth-of-type( #{ $i } ) {
@if $i == 1 {
opacity: 1;
} @else if $i == 2 {
opacity: 0.7;
} @else if $i == 3 {
opacity: 0.5;
} @else {
opacity: calc(0.5 - 0.04 * ($i - 3));
}
}
}
:last-child {
display: flex;
justify-content: flex-end;
}
}
&__name {
@include skeleton();
}
&__value {
@include skeleton();
}
&__actions {
@include skeleton();
width: $grid-unit-60;
}
}

View File

@ -0,0 +1,64 @@
/**
* External dependencies
*/
import { render } from '@testing-library/react';
import React from 'react';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { EmptyState } from '../empty-state';
describe( 'EmptyState', () => {
it( 'should render empty name rows when names are empty', () => {
const { container } = render( <EmptyState names={ [ '', '', '' ] } /> );
const rows = container.querySelectorAll(
'.woocommerce-product-empty-state__row'
);
expect( rows.length ).toBe( 3 );
rows.forEach( ( row ) => {
expect(
row.querySelector( '.woocommerce-product-empty-state__name' )
).toBeInTheDocument();
expect(
row.querySelector( '.woocommerce-product-empty-state__value' )
).toBeInTheDocument();
expect(
row.querySelector( '.woocommerce-product-empty-state__actions' )
).toBeInTheDocument();
} );
} );
it( 'should render names when provided', () => {
const names = [ 'Name 1', 'Name 2', 'Name 3' ];
const { container, queryByText } = render(
<EmptyState names={ names } />
);
const rows = container.querySelectorAll(
'.woocommerce-product-empty-state__row'
);
expect( rows.length ).toBe( 3 );
names.forEach( ( name ) => {
expect( queryByText( name ) ).toBeInTheDocument();
} );
} );
it( 'should render the correct number of rows based on the provided array', () => {
const testCases = [
{ names: [ 'Name 1', 'Name 2' ], expectedLength: 2 },
{
names: [ 'Name 1', 'Name 2', 'Name 3', 'Name 4' ],
expectedLength: 4,
},
];
testCases.forEach( ( { names, expectedLength } ) => {
const { container } = render( <EmptyState names={ names } /> );
const rows = container.querySelectorAll(
'.woocommerce-product-empty-state__row'
);
expect( rows.length ).toBe( expectedLength );
} );
} );
} );

View File

@ -36,7 +36,6 @@
@import "components/manage-download-limits-modal/style.scss";
@import "components/label/style.scss";
@import "components/modal-editor-welcome-guide/style.scss";
@import "components/attribute-control/attribute-skeleton.scss";
@import "components/checkbox-control/style.scss";
@import "components/add-products-modal/style.scss";
@import "components/advice-card/style.scss";
@ -52,6 +51,7 @@
@import "components/text-control/style.scss";
@import "components/attribute-combobox-field/styles.scss";
@import "components/number-control/style.scss";
@import "components/empty-state/style.scss";
/* Field Blocks */

View File

@ -0,0 +1,15 @@
/**
* Generates an array of sequentially numbered strings in the format "string number".
*
* @param name The base string to be used.
* @param number The number of times the string should be repeated with an incremented number.
* @return An array of formatted strings.
*/
export function getEmptyStateSequentialNames(
name: string,
number: number
): string[] {
return Array( number )
.fill( 0 )
.map( ( _, index ) => `${ name } ${ index + 1 }` );
}

View File

@ -15,6 +15,7 @@ import {
getProductStockStatusClass,
} from './get-product-stock-status';
import { getProductTitle } from './get-product-title';
import { getEmptyStateSequentialNames } from './get-empty-state-names';
import {
getProductVariationTitle,
getTruncatedProductVariationTitle,
@ -42,6 +43,7 @@ export {
getCheckboxTracks,
getCurrencySymbolProps,
getDerivedProductType,
getEmptyStateSequentialNames,
getHeaderTitle,
getPermalinkParts,
getProductStatus,