Add variation switcher in footer for variation page (#40713)

* Add variation switcher in footer for variation page

* Add productId to make sure template is updated correctly

* Don't show bar when less then 2 variations.

* Add class for when page is scrolled to bottom

* Add changelogs

* Add tracks

* Fix lint error

* Fix css lint issues
This commit is contained in:
louwie17 2023-10-12 11:56:48 -03:00 committed by GitHub
parent 61a68131bd
commit c4c56f3fc8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 361 additions and 47 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add new VariationSwitcherFooter component for switching variations on edit variation page.

View File

@ -98,7 +98,7 @@ export function BlockEditor( {
onChange( blockInstances, {} );
updateEditorSettings( _settings ?? {} );
}, [ productType ] );
}, [ productType, productId ] );
const editedProduct: Product = useSelect( ( select ) =>
select( 'core' ).getEditedEntityRecord(

View File

@ -35,7 +35,6 @@ import { FullscreenMode, InterfaceSkeleton } from '@wordpress/interface';
/**
* Internal dependencies
*/
import { Footer } from '../footer';
import { Header } from '../header';
import { BlockEditor } from '../block-editor';
import { ValidationProvider } from '../../contexts/validation-context';
@ -101,14 +100,6 @@ export function Editor( {
</ValidationProvider>
</SlotFillProvider>
</ShortcutProvider>
{ /* We put Footer here instead of in InterfaceSkeleton because Footer uses
WooFooterItem to actually render in the WooFooterItem.Slot defined by
WooCommerce Admin. And, we need to put it outside of the SlotFillProvider
we create in this component. */ }
<Footer
productId={ product.id }
productType={ product.type }
/>
</EntityProvider>
</StrictMode>
</LayoutContextProvider>

View File

@ -1,27 +0,0 @@
/**
* External dependencies
*/
import { createElement, Fragment } from '@wordpress/element';
import { WooFooterItem } from '@woocommerce/admin-layout';
/**
* Internal dependencies
*/
import { FeedbackBar } from '../feedback-bar';
import { ProductMVPFeedbackModalContainer } from '../product-mvp-feedback-modal-container';
export type FooterProps = {
productType: string;
productId: number;
};
export function Footer( { productType, productId }: FooterProps ) {
return (
<WooFooterItem>
<>
<FeedbackBar productType={ productType } />
<ProductMVPFeedbackModalContainer productId={ productId } />
</>
</WooFooterItem>
);
}

View File

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

View File

@ -1,3 +0,0 @@
.woocommerce-product-footer {
width: 100%;
}

View File

@ -31,3 +31,4 @@ export {
export { AttributeControl as __experimentalAttributeControl } from './attribute-control';
export { Attributes as __experimentalAttributes } from './attributes';
export * from './add-new-shipping-class-modal';
export { VariationSwitcherFooter as __experimentalVariationSwitcherFooter } from './variation-switcher-footer';

View File

@ -0,0 +1 @@
export * from './variation-switcher-footer';

View File

@ -0,0 +1,43 @@
/**
* External dependencies
*/
import { createElement, Fragment } from '@wordpress/element';
import { arrowLeft, arrowRight, Icon } from '@wordpress/icons';
import { Button } from '@wordpress/components';
export function SwitcherLoadingPlaceholder( {
position,
}: {
position: 'left' | 'right';
} ) {
return (
<Button
data-testid="woocommerce-product-variation-switcher-footer-placeholder"
className="woocommerce-product-variation-switcher-footer__button is-placeholder"
disabled={ true }
>
{ position === 'left' && (
<>
<Icon
icon={ arrowLeft }
size={ 16 }
className="woocommerce-product-variation-switcher-footer__arrow"
/>
<div className="woocommerce-product-variation-switcher-footer__product-image" />
</>
) }
<div className="woocommerce-product-variation-switcher-footer__item-label" />
{ position === 'right' && (
<>
<div className="woocommerce-product-variation-switcher-footer__product-image" />
<Icon
icon={ arrowRight }
size={ 16 }
className="woocommerce-product-variation-switcher-footer__arrow"
/>
</>
) }
</Button>
);
}

View File

@ -0,0 +1,46 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
export function VariationImagePlaceholder( {
className,
}: {
className?: string;
} ) {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={ className }
style={ {
borderRadius: '3px',
background: 'var(--wp-gray-gray-0, #F6F7F7)',
} }
>
<g clipPath="url(#clip0_7819_478402)">
<path
d="M22.9869 7.07134C19.8275 7.07134 17.9977 7.62377 16.9543 8.85386C17.6297 6.47326 18.8776 4.68605 21.1572 3.06738C18.4558 3.41616 16.8877 4.44261 16.1282 5.91147C15.3688 4.44378 13.8007 3.41733 11.0993 3.06738C13.3789 4.68605 14.6268 6.47209 15.3021 8.85386C14.2587 7.62377 12.429 7.07134 9.26953 7.07134C13.0354 8.83982 14.4761 10.263 15.7029 12.8133H16.5524C17.7792 10.263 19.2211 8.83982 22.9858 7.07134H22.9869Z"
fill="#BBBBBB"
/>
<path
d="M18.6464 34.8438C19.8718 34.3897 24.3648 33.3925 24.3648 29.0402C24.3648 26.4551 22.0269 24.5052 19.3727 24.0527C20.5536 23.6669 22.278 21.9488 22.278 20.0211C22.278 17.5377 20.0307 16.5469 18.1934 16.1626C18.1934 15.5957 20.2357 12.6709 20.2357 12.6709H11.7932C11.7932 12.6709 13.8355 15.5957 13.8355 16.1626C11.9966 16.5485 9.75086 17.5377 9.75086 20.0211C9.75086 21.9488 11.4753 23.6669 12.6562 24.0527C10.0004 24.5068 7.66406 26.4567 7.66406 29.0402C7.66406 33.3941 12.1571 34.3913 13.3825 34.8438C10.4772 35.1154 9.02453 36.2491 8.79885 37.1558H23.2316C23.0043 36.2491 21.5517 35.1154 18.648 34.8438H18.6464Z"
fill="#DDDDDD"
/>
</g>
<defs>
<clipPath id="clip0_7819_478402">
<rect
width="34.0881"
height="25.9994"
fill="white"
transform="translate(-1)"
/>
</clipPath>
</defs>
</svg>
);
}

View File

@ -0,0 +1,39 @@
.woocommerce-product-variation-switcher-footer {
max-width: 650px;
padding: 16px 0;
margin: 0 auto;
display: flex;
justify-content: space-between;
&__button {
height: 32px;
gap: $gap-small;
&.is-placeholder {
.woocommerce-product-variation-switcher-footer__arrow {
@include placeholder();
background-color: transparent;
color: inherit;
}
.woocommerce-product-variation-switcher-footer__item-label {
@include placeholder();
background-color: $gray-200;
width: $gap-largest;
}
.woocommerce-product-variation-switcher-footer__product-image {
@include placeholder();
background-color: $gray-200;
width: 32px;
height: 32px;
}
}
}
&__product-image {
max-height: 32px;
max-width: 32px;
}
}

View File

@ -0,0 +1,163 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { createElement } from '@wordpress/element';
import { arrowLeft, arrowRight, Icon } from '@wordpress/icons';
import { useSelect } from '@wordpress/data';
import { Product, ProductVariation } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { getNewPath, navigateTo } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { SwitcherLoadingPlaceholder } from './switcher-loading-placeholder';
import { VariationImagePlaceholder } from './variation-image-placeholder';
export type VariationSwitcherProps = {
productType?: string;
variationId: number;
parentId: number;
};
function getVariationName( variation: ProductVariation ): string {
return variation.attributes.map( ( attr ) => attr.option ).join( ', ' );
}
export function VariationSwitcherFooter( {
variationId,
parentId,
}: VariationSwitcherProps ) {
const {
previousVariation,
nextVariation,
numberOfVariations,
...variationIndexes
} = useSelect(
( select ) => {
const { getEntityRecord } = select( 'core' );
const parentProduct = getEntityRecord< Product >(
'postType',
'product',
parentId
);
if ( parentProduct && parentProduct.variations ) {
const activeVariationIndex =
parentProduct.variations.indexOf( variationId );
const previousVariationIndex =
activeVariationIndex > 0
? activeVariationIndex - 1
: parentProduct.variations.length - 1;
const nextVariationIndex =
activeVariationIndex !== parentProduct.variations.length - 1
? activeVariationIndex + 1
: 0;
const previousVariationId =
parentProduct.variations[ previousVariationIndex ];
const nextVariationId =
parentProduct.variations[ nextVariationIndex ];
return {
activeVariationIndex,
nextVariationIndex,
previousVariationIndex,
numberOfVariations: parentProduct.variations.length,
previousVariation: getEntityRecord< ProductVariation >(
'postType',
'product_variation',
previousVariationId
),
nextVariation: getEntityRecord< ProductVariation >(
'postType',
'product_variation',
nextVariationId
),
};
}
return {};
},
[ variationId, parentId ]
);
function onPrevious() {
recordEvent( 'product_variation_switch_previous', {
variation_length: numberOfVariations,
variation_id: previousVariation?.id,
variation_index: variationIndexes.activeVariationIndex,
previous_variation_index: variationIndexes.previousVariationIndex,
} );
navigateTo( {
url: getNewPath(
{},
`/product/${ parentId }/variation/${ previousVariation?.id }`
),
} );
}
function onNext() {
recordEvent( 'product_variation_switch_next', {
variation_length: numberOfVariations,
variation_id: nextVariation?.id,
variation_index: variationIndexes.activeVariationIndex,
next_variation_index: variationIndexes.nextVariationIndex,
} );
navigateTo( {
url: getNewPath(
{},
`/product/${ parentId }/variation/${ nextVariation?.id }`
),
} );
}
if ( ! numberOfVariations || numberOfVariations < 2 ) {
return null;
}
return (
<div className="woocommerce-product-variation-switcher-footer">
{ previousVariation ? (
<Button
className="woocommerce-product-variation-switcher-footer__button"
label={ __( 'Previous', 'woocommerce' ) }
onClick={ onPrevious }
>
<Icon icon={ arrowLeft } size={ 16 } />
{ previousVariation.image ? (
<img
alt={ previousVariation.image.alt || '' }
src={ previousVariation.image.src }
className="woocommerce-product-variation-switcher-footer__product-image"
/>
) : (
<VariationImagePlaceholder className="woocommerce-product-variation-switcher-footer__product-image" />
) }
{ getVariationName( previousVariation ) }
</Button>
) : (
<SwitcherLoadingPlaceholder position="left" />
) }
{ nextVariation ? (
<Button
className="woocommerce-product-variation-switcher-footer__button"
label={ __( 'Next', 'woocommerce' ) }
onClick={ onNext }
>
{ getVariationName( nextVariation ) }
{ nextVariation.image ? (
<img
alt={ nextVariation.image.alt || '' }
src={ nextVariation.image.src }
className="woocommerce-product-variation-switcher-footer__product-image"
/>
) : (
<VariationImagePlaceholder className="woocommerce-product-variation-switcher-footer__product-image" />
) }
<Icon icon={ arrowRight } size={ 16 } />
</Button>
) : (
<SwitcherLoadingPlaceholder position="right" />
) }
</div>
);
}

View File

@ -8,7 +8,6 @@
@import "components/tabs/style.scss";
@import "components/product-section-layout/style.scss";
@import "components/header/style.scss";
@import "components/footer/style.scss";
@import "components/add-new-shipping-class-modal/style.scss";
/* Components */
@ -32,6 +31,7 @@
@import "components/variations-table/styles.scss";
@import "components/tags-field/create-tag-modal.scss";
@import "components/tags-field/style.scss";
@import "components/variation-switcher-footer//variation-switcher-footer.scss";
/* Field Blocks */

View File

@ -29,7 +29,7 @@ export const Header = ( { sections, isEmbedded = false, query } ) => {
const activeSetupList = useActiveSetupTasklist();
const siteTitle = getSetting( 'siteTitle', '' );
const pageTitle = sections.slice( -1 )[ 0 ];
const isScrolled = useIsScrolled();
const { isScrolled } = useIsScrolled();
let debounceTimer = null;
const className = classnames( 'woocommerce-layout__header', {

View File

@ -3,12 +3,18 @@
*/
import { useEffect, useRef, useState } from '@wordpress/element';
function isAtBottom() {
return window.innerHeight + window.scrollY >= document.body.scrollHeight;
}
export default function useIsScrolled() {
const [ isScrolled, setIsScrolled ] = useState( false );
const [ atBottom, setAtBottom ] = useState( isAtBottom() );
const rafHandle = useRef( null );
useEffect( () => {
const updateIsScrolled = () => {
setIsScrolled( window.pageYOffset > 20 );
setAtBottom( isAtBottom() );
};
const scrollListener = () => {
@ -18,11 +24,18 @@ export default function useIsScrolled() {
window.addEventListener( 'scroll', scrollListener );
window.addEventListener( 'resize', scrollListener );
return () => {
window.removeEventListener( 'scroll', scrollListener );
window.removeEventListener( 'resize', scrollListener );
window.cancelAnimationFrame( rafHandle.current );
};
}, [] );
return isScrolled;
return {
isScrolled,
atBottom,
atTop: ! isScrolled,
};
}

View File

@ -1,6 +1,7 @@
.woocommerce-layout__footer {
background: $studio-white;
border-top: 1px solid $gray-200;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.05);
box-sizing: border-box;
padding: 0;
position: fixed;

View File

@ -3,21 +3,28 @@
*/
import { WC_FOOTER_SLOT_NAME, WooFooterItem } from '@woocommerce/admin-layout';
import { useSlot } from '@woocommerce/experimental';
import classNames from 'classnames';
/**
* Internal dependencies
*/
import './footer.scss';
import useIsScrolled from '~/hooks/useIsScrolled';
export const Footer: React.FC = () => {
const slot = useSlot( WC_FOOTER_SLOT_NAME );
const hasFills = Boolean( slot?.fills?.length );
const { atBottom } = useIsScrolled();
if ( ! hasFills ) {
return null;
}
return (
<div className="woocommerce-layout__footer">
<div
className={ classNames( 'woocommerce-layout__footer', {
'at-bottom': atBottom,
} ) }
>
<WooFooterItem.Slot />
</div>
);

View File

@ -20,7 +20,7 @@ import useIsScrolled from '../../../hooks/useIsScrolled';
const Header = () => {
const siteTitle = getSetting( 'siteTitle', '' );
const homeUrl = getSetting( 'homeUrl', '' );
const isScrolled = useIsScrolled();
const { isScrolled } = useIsScrolled();
const [ isFolded, setIsFolded ] = useState(
document.body.classList.contains( false )
);

View File

@ -1,5 +1,6 @@
.woocommerce-admin-page__add-product,
.woocommerce-admin-page__product_product-id {
.woocommerce-admin-page__product_product-id,
.woocommerce-admin-page__product_product-id_variation_variation-id {
.woocommerce-store-alerts {
display: none;
}
@ -61,10 +62,20 @@
}
}
.woocommerce-admin-page__product_product-id_variation_variation-id {
.interface-interface-skeleton__content {
overflow: hidden;
}
}
html.interface-interface-skeleton__html-container {
@include breakpoint( '<782px' ) {
position: inherit;
}
.woocommerce-layout__footer.at-bottom {
border-top: none;
box-shadow: none;
}
}
body {

View File

@ -7,11 +7,14 @@ import {
ProductEditorSettings,
productApiFetchMiddleware,
TRACKS_SOURCE,
__experimentalProductMVPCESFooter as FeedbackBar,
__experimentalProductMVPFeedbackModalContainer as ProductMVPFeedbackModalContainer,
} from '@woocommerce/product-editor';
import { recordEvent } from '@woocommerce/tracks';
import { Spinner } from '@wordpress/components';
import { useEffect } from '@wordpress/element';
import { useParams } from 'react-router-dom';
import { WooFooterItem } from '@woocommerce/admin-layout';
/**
* Internal dependencies
@ -60,6 +63,14 @@ export default function ProductPage() {
product={ product }
settings={ productBlockEditorSettings || {} }
/>
<WooFooterItem>
<>
<FeedbackBar productType="product" />
<ProductMVPFeedbackModalContainer
productId={ product.id }
/>
</>
</WooFooterItem>
<BlockEditorTourWrapper />
</>
);

View File

@ -7,10 +7,12 @@ import {
ProductEditorSettings,
productApiFetchMiddleware,
TRACKS_SOURCE,
__experimentalVariationSwitcherFooter as VariationSwitcherFooter,
} from '@woocommerce/product-editor';
import { recordEvent } from '@woocommerce/tracks';
import { Spinner } from '@wordpress/components';
import { useEffect } from '@wordpress/element';
import { WooFooterItem } from '@woocommerce/admin-layout';
import { useParams } from 'react-router-dom';
/**
@ -60,6 +62,14 @@ export default function ProductPage() {
productType="product_variation"
settings={ productBlockEditorSettings || {} }
/>
{ productId && variationId && (
<WooFooterItem order={ 0 }>
<VariationSwitcherFooter
parentId={ parseInt( productId, 10 ) }
variationId={ parseInt( variationId, 10 ) }
/>
</WooFooterItem>
) }
</>
);
}

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Move product page footer from editor to product page, and update useIsScrolled hook.