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:
parent
61a68131bd
commit
c4c56f3fc8
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add new VariationSwitcherFooter component for switching variations on edit variation page.
|
|
@ -98,7 +98,7 @@ export function BlockEditor( {
|
|||
onChange( blockInstances, {} );
|
||||
|
||||
updateEditorSettings( _settings ?? {} );
|
||||
}, [ productType ] );
|
||||
}, [ productType, productId ] );
|
||||
|
||||
const editedProduct: Product = useSelect( ( select ) =>
|
||||
select( 'core' ).getEditedEntityRecord(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from './footer';
|
|
@ -1,3 +0,0 @@
|
|||
.woocommerce-product-footer {
|
||||
width: 100%;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './variation-switcher-footer';
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 */
|
||||
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 )
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Move product page footer from editor to product page, and update useIsScrolled hook.
|
Loading…
Reference in New Issue