[Product Editor] Fix blank editor flash when loading product (#43840)

* EditorLoadingContext

* Use EditorLoadingContext

* Remove fallbacks

* Make sure metadata exists before using it

* Add header loading state

* Do not return skeleton

* Use EditorLoadingContext

* Update editor loading state

* Remove ProductPageSkeleton

* Remove unused import

* Remove unused import

* Handle undefined variationId and parentId in VariationSwitcherFooter

* Remove ProductPageSkeleton

* Include productId in determination of whether editor is loading

* Handle variation loading

* Fix rebase merge conflict mistakes

* Fix layout margins

* Show welcome tour and feedback bar after editor has loaded

* Changelogs

* Make loading context experimental
This commit is contained in:
Matt Sherman 2024-02-14 09:57:17 -05:00 committed by GitHub
parent 2432b3b22e
commit d279466eb3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 310 additions and 303 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Product Editor loading state now shows until form is displayed. No more blank flash of white.

View File

@ -46,6 +46,7 @@ import { ModalEditor } from '../modal-editor';
import { ProductEditorSettings } from '../editor';
import { BlockEditorProps } from './types';
import { ProductTemplate } from '../../types';
import { LoadingState } from './loading-state';
function getLayoutTemplateId(
productTemplate: ProductTemplate | undefined,
@ -62,11 +63,13 @@ function getLayoutTemplateId(
// Fallback to simple product if no layout template is set.
return 'simple-product';
}
export function BlockEditor( {
context,
settings: _settings,
postType,
productId,
setIsEditorLoading,
}: BlockEditorProps ) {
useConfirmUnsavedProductChanges( postType );
@ -142,8 +145,14 @@ export function BlockEditor( {
const { updateEditorSettings } = useDispatch( 'core/editor' );
const isEditorLoading =
! layoutTemplate ||
// variations don't have a product template
( postType !== 'product_variation' && ! productTemplate ) ||
productId === -1;
useLayoutEffect( () => {
if ( ! layoutTemplate ) {
if ( isEditorLoading ) {
return;
}
@ -159,6 +168,8 @@ export function BlockEditor( {
productTemplate,
} as Partial< ProductEditorSettings > );
setIsEditorLoading( isEditorLoading );
// We don't need to include onChange or updateEditorSettings in the dependencies,
// since we get new instances of them on every render, which would cause an infinite loop.
//
@ -176,10 +187,6 @@ export function BlockEditor( {
const { closeModalEditor } = useDispatch( productEditorUiStore );
if ( ! blocks ) {
return null;
}
if ( isModalEditorOpen ) {
return (
<ModalEditor
@ -204,7 +211,11 @@ export function BlockEditor( {
<BlockEditorKeyboardShortcuts.Register />
<BlockTools>
<ObserveTyping>
{ isEditorLoading ? (
<LoadingState />
) : (
<BlockList className="woocommerce-product-block-editor__block-list" />
) }
</ObserveTyping>
</BlockTools>
{ /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ }

View File

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

View File

@ -0,0 +1,61 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
export function LoadingState() {
return (
<div
className="woocommerce-product-block-editor__block-list block-editor-block-list__layout is-root-container is-loading"
aria-hidden="true"
>
<div className="wp-block-woocommerce-product-tab">
<div className="wp-block-woocommerce-product-section">
<div className="wp-block-woocommerce-product-section__heading-title-wrapper">
<div className="wp-block-woocommerce-product-section__heading-title" />
</div>
<div className="wp-block-woocommerce-product-section__content wp-block-woocommerce-product-section-header__content--block-gap-unit-30">
<div className="block-editor-block-list__block">
<div className="woocommerce-product-form-label__label" />
<div className="woocommerce-product-form-input" />
</div>
<div className="block-editor-block-list__block">
<div className="woocommerce-product-form-label__label" />
<div className="woocommerce-product-form-textarea" />
</div>
<div className="block-editor-block-list__block">
<div className="woocommerce-product-form-label__label" />
<div className="woocommerce-product-form-textarea" />
</div>
</div>
</div>
<div className="wp-block-woocommerce-product-section">
<div className="wp-block-woocommerce-product-section__heading-title-wrapper">
<div className="wp-block-woocommerce-product-section__heading-title" />
</div>
<div className="wp-block-woocommerce-product-section__content wp-block-woocommerce-product-section__content--block-gap-unit-30">
<div className="block-editor-block-list__block">
<div className="woocommerce-product-form-label__label" />
<div className="woocommerce-product-form-input" />
</div>
<div className="block-editor-block-list__block">
<div className="woocommerce-product-form-label__label" />
<div className="woocommerce-product-form-textarea" />
</div>
<div className="block-editor-block-list__block">
<div className="woocommerce-product-form-label__label" />
<div className="woocommerce-product-form-textarea" />
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -160,6 +160,44 @@
}
}
.woocommerce-product-block-editor {
.block-editor-block-list__layout.is-root-container.is-loading {
.wp-block-woocommerce-product-section__heading-title {
@include placeholder();
background-color: $gray-200;
border-radius: $grid-unit-05;
width: 300px;
height: 28px;
margin-bottom: $gap-smaller;
}
.woocommerce-product-form-label__label {
@include placeholder();
background-color: $gray-200;
border-radius: $grid-unit-05;
width: $grid-unit-80;
height: 28px;
margin-bottom: $gap-smaller;
}
.woocommerce-product-form-input {
@include placeholder();
background-color: $gray-200;
border-radius: $grid-unit-05;
width: 100%;
height: 36px;
}
.woocommerce-product-form-textarea {
@include placeholder();
background-color: $gray-200;
border-radius: $grid-unit-05;
width: 100%;
height: 108px;
}
}
}
.wp-admin.woocommerce-feature-enabled-product-block-editor {
.components-modal {
&__frame {

View File

@ -13,4 +13,5 @@ export type BlockEditorProps = {
postType: string;
productId: number;
settings?: ProductEditorSettings;
setIsEditorLoading: ( isEditorLoading: boolean ) => void;
};

View File

@ -31,6 +31,7 @@ import { InterfaceSkeleton } from '@wordpress/interface';
*/
import { Header } from '../header';
import { BlockEditor } from '../block-editor';
import { EditorLoadingContext } from '../../contexts/editor-loading-context';
import { ValidationProvider } from '../../contexts/validation-context';
import { EditorProps } from './types';
import { store as productEditorUiStore } from '../../store/product-editor-ui';
@ -41,10 +42,13 @@ export function Editor( {
productType = 'product',
settings,
}: EditorProps ) {
const [ isEditorLoading, setIsEditorLoading ] = useState( true );
const [ selectedTab, setSelectedTab ] = useState< string | null >( null );
const updatedLayoutContext = useExtendLayout( 'product-block-editor' );
const productId = product?.id || -1;
// Check if the prepublish sidebar is open from the store.
const isPrepublishPanelOpen = useSelect( ( select ) => {
return select( productEditorUiStore ).isPrepublishPanelOpen();
@ -56,10 +60,13 @@ export function Editor( {
<EntityProvider
kind="postType"
type={ productType }
id={ product.id }
id={ productId }
>
<ShortcutProvider>
<ValidationProvider initialValue={ product }>
<EditorLoadingContext.Provider
value={ isEditorLoading }
>
<InterfaceSkeleton
header={
<Header
@ -72,12 +79,15 @@ export function Editor( {
<BlockEditor
settings={ settings }
postType={ productType }
productId={ product.id }
productId={ productId }
context={ {
selectedTab,
postType: productType,
postId: product.id,
postId: productId,
} }
setIsEditorLoading={
setIsEditorLoading
}
/>
</>
}
@ -85,11 +95,12 @@ export function Editor( {
isPrepublishPanelOpen && (
<PrepublishPanel
productType={ productType }
productId={ product.id }
productId={ productId }
/>
)
}
/>
</EditorLoadingContext.Provider>
<Popover.Slot />
</ValidationProvider>
</ShortcutProvider>

View File

@ -29,7 +29,7 @@ export type ProductEditorSettings = Partial<
};
export type EditorProps = {
product: Pick< Product, 'id' | 'type' >;
product?: Pick< Product, 'id' | 'type' > | null;
productType?: string;
settings?: ProductEditorSettings;
};

View File

@ -4,7 +4,7 @@
import { WooHeaderItem, useAdminSidebarWidth } from '@woocommerce/admin-layout';
import { useEntityProp } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import { createElement, useEffect } from '@wordpress/element';
import { createElement, useContext, useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Button, Tooltip } from '@wordpress/components';
import { chevronLeft, group, Icon } from '@wordpress/icons';
@ -18,11 +18,13 @@ import { PinnedItems } from '@wordpress/interface';
/**
* Internal dependencies
*/
import { EditorLoadingContext } from '../../contexts/editor-loading-context';
import { getHeaderTitle } from '../../utils';
import { MoreMenu } from './more-menu';
import { PreviewButton } from './preview-button';
import { SaveDraftButton } from './save-draft-button';
import { PublishButton } from './publish-button';
import { LoadingState } from './loading-state';
import { PrepublishButton } from '../prepublish-panel';
import { Tabs } from '../tabs';
import { HEADER_PINNED_ITEMS_SCOPE, TRACKS_SOURCE } from '../../constants';
@ -41,6 +43,8 @@ export function Header( {
onTabSelect,
productType = 'product',
}: HeaderProps ) {
const isEditorLoading = useContext( EditorLoadingContext );
const [ productId ] = useEntityProp< number >(
'postType',
productType,
@ -75,8 +79,8 @@ export function Header( {
} );
}, [ sidebarWidth ] );
if ( ! productId ) {
return null;
if ( isEditorLoading ) {
return <LoadingState />;
}
const isVariation = lastPersistedProduct?.parent_id > 0;

View File

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

View File

@ -0,0 +1,34 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
export function LoadingState() {
return (
<div
className="woocommerce-product-header is-loading"
aria-hidden="true"
>
<div className="woocommerce-product-header__inner">
<div />
<div className="woocommerce-product-header__title" />
<div className="woocommerce-product-header__actions">
<div className="woocommerce-product-header__action" />
<div className="woocommerce-product-header__action" />
<div className="woocommerce-product-header__action" />
<div className="woocommerce-product-header__action" />
</div>
</div>
<div className="woocommerce-product-tabs">
{ Array( 7 )
.fill( 0 )
.map( ( _, index ) => (
<div key={ index } className="components-button" />
) ) }
</div>
</div>
);
}

View File

@ -70,6 +70,51 @@
}
}
.woocommerce-product-header.is-loading {
.woocommerce-product-header__title {
@include placeholder();
background-color: $gray-200;
border-radius: $grid-unit-05;
min-width: 300px;
min-height: $grid-unit-30;
}
.woocommerce-product-header__actions {
.woocommerce-product-header__action {
@include placeholder();
background-color: $gray-200;
border-radius: $grid-unit-05;
height: $grid-unit-30;
&:nth-child(n) {
min-width: $grid-unit-80;
}
&:first-child {
min-width: calc(10 * $grid-unit);
}
&:last-child {
min-width: $icon-size;
}
}
}
.woocommerce-product-tabs {
height: 46px;
align-items: flex-start;
.components-button {
@include placeholder();
background-color: $gray-200;
border-radius: $grid-unit-05;
height: $grid-unit-20;
min-width: $grid-unit-80;
}
}
}
.woocommerce-product-header__more-menu {
.components-popover__content {
min-width: auto;

View File

@ -48,8 +48,6 @@ export {
export { Checkbox as __experimentalCheckboxControl } from './checkbox-control';
export { NumberControl as __experimentalNumberControl } from './number-control';
export * from './product-page-skeleton';
export * from './modal-editor-welcome-guide';
export {

View File

@ -1 +0,0 @@
export * from './product-page-skeleton';

View File

@ -1,63 +0,0 @@
/**
* External dependencies
*/
import { createElement } from '@wordpress/element';
export function ProductPageSkeleton() {
return (
<div className="woocommerce-product-page-skeleton" aria-hidden="true">
<div className="woocommerce-product-page-skeleton__header">
<div className="woocommerce-product-page-skeleton__header-row">
<div />
<div className="woocommerce-product-page-skeleton__header-title" />
<div className="woocommerce-product-page-skeleton__header-actions">
<div className="woocommerce-product-page-skeleton__header-actions-other" />
<div className="woocommerce-product-page-skeleton__header-actions-main" />
<div className="woocommerce-product-page-skeleton__header-actions-config" />
</div>
</div>
<div className="woocommerce-product-page-skeleton__header-row">
<div className="woocommerce-product-page-skeleton__tabs">
{ Array( 7 )
.fill( 0 )
.map( ( _, index ) => (
<div
key={ index }
className="woocommerce-product-page-skeleton__tab-item"
/>
) ) }
</div>
</div>
</div>
<div className="woocommerce-product-page-skeleton__body">
<div className="woocommerce-product-page-skeleton__body-tabs-content">
<div className="woocommerce-product-page-skeleton__block-title" />
<div className="woocommerce-product-page-skeleton__block-label" />
<div className="woocommerce-product-page-skeleton__block-input" />
<div className="woocommerce-product-page-skeleton__block-label" />
<div className="woocommerce-product-page-skeleton__block-textarea" />
<div className="woocommerce-product-page-skeleton__block-label" />
<div className="woocommerce-product-page-skeleton__block-textarea" />
<div className="woocommerce-product-page-skeleton__block-separator" />
<div className="woocommerce-product-page-skeleton__block-title" />
<div className="woocommerce-product-page-skeleton__block-label" />
<div className="woocommerce-product-page-skeleton__block-input" />
<div className="woocommerce-product-page-skeleton__block-label" />
<div className="woocommerce-product-page-skeleton__block-textarea" />
<div className="woocommerce-product-page-skeleton__block-label" />
<div className="woocommerce-product-page-skeleton__block-textarea" />
</div>
</div>
</div>
);
}

View File

@ -1,156 +0,0 @@
@mixin skeleton {
@include placeholder();
background-color: $gray-200;
border-radius: $grid-unit-05;
min-width: $grid-unit-20;
min-height: $grid-unit-20;
}
.woocommerce-product-page-skeleton {
height: calc(100vh - 46px);
overflow: hidden;
@include breakpoint(">782px") {
height: calc(100vh - $grid-unit-40);
}
&__header {
border-bottom: 1px solid $gray-300;
}
&__header-row {
&:first-child {
display: flex;
align-items: center;
height: 60px;
padding: 0 $grid-unit-40;
@include breakpoint(">782px") {
display: grid;
grid-template-columns: 1fr auto 1fr;
grid-gap: $grid-unit-20;
padding: 0 $grid-unit-20;
}
}
&:last-child {
overflow: hidden;
@include breakpoint(">782px") {
display: flex;
justify-content: center;
}
}
}
&__header-title {
@include skeleton();
width: 100%;
height: $grid-unit-30;
@include breakpoint(">782px") {
width: 450px;
}
}
&__header-actions {
display: none;
@include breakpoint(">782px") {
display: flex;
align-items: center;
justify-content: flex-end;
gap: $grid-unit;
}
}
&__header-actions-other,
&__header-actions-main,
&__header-actions-config {
@include skeleton();
height: $grid-unit-30;
}
&__header-actions-other {
width: $grid-unit-60;
}
&__header-actions-main {
width: $grid-unit-80;
}
&__header-actions-config {
width: $grid-unit-30;
}
&__tabs {
height: 46px;
width: fit-content;
display: flex;
align-items: flex-start;
justify-content: center;
gap: $grid-unit-30;
overflow-x: auto;
margin: 0 $grid-unit-40;
}
&__tab-item {
@include skeleton();
width: $grid-unit-80;
height: $grid-unit-20;
margin-top: $grid-unit;
flex-shrink: 0;
}
&__body-tabs-content {
padding-top: $grid-unit-80;
padding-left: $grid-unit-40;
padding-right: $grid-unit-40;
@include breakpoint(">782px") {
padding-left: 0;
padding-right: 0;
max-width: 650px;
margin-left: auto;
margin-right: auto;
}
}
&__block-title {
@include skeleton();
width: 100%;
height: 28px;
margin-bottom: $grid-unit-40;
@include breakpoint(">782px") {
width: 450px;
}
}
&__block-label {
@include skeleton();
width: $grid-unit-80;
margin-bottom: $grid-unit;
}
&__block-input {
@include skeleton();
width: 100%;
height: 36px;
margin-bottom: $grid-unit-30;
}
&__block-textarea {
@include skeleton();
width: 100%;
height: 126px;
margin-bottom: $grid-unit-30;
}
&__block-separator {
border-bottom: 1px solid $gray-300;
width: 100%;
height: 0;
margin: $grid-unit-80 0;
}
}

View File

@ -17,8 +17,8 @@ import { useVariationSwitcher } from '../../hooks/use-variation-switcher';
export type VariationSwitcherProps = {
parentProductType?: string;
variationId: number;
parentId: number;
variationId?: number;
parentId?: number;
};
export function VariationSwitcherFooter( {

View File

@ -0,0 +1,6 @@
/**
* External dependencies
*/
import { createContext } from '@wordpress/element';
export const EditorLoadingContext = createContext( false );

View File

@ -34,7 +34,8 @@ function useProductEntityProp< T >(
);
const metadataItem = useMemo(
() => metadata.find( ( item ) => item.key === metaKey ),
() =>
metadata ? metadata.find( ( item ) => item.key === metaKey ) : null,
[ metadata, metaKey ]
);

View File

@ -31,10 +31,15 @@ export * from './store/product-editor-ui';
* Hooks
*/
export * from './hooks';
export { PostTypeContext } from './contexts/post-type-context';
export { useValidation, useValidations } from './contexts/validation-context';
export * from './contexts/validation-context/types';
/**
* Contexts
*/
export { EditorLoadingContext as __experimentalEditorLoadingContext } from './contexts/editor-loading-context';
export { PostTypeContext } from './contexts/post-type-context';
// Init the store
registerProductEditorUiStore();

View File

@ -35,7 +35,6 @@
@import "components/remove-confirmation-modal/style.scss";
@import "components/manage-download-limits-modal/style.scss";
@import "components/label/style.scss";
@import "components/product-page-skeleton/styles.scss";
@import "components/modal-editor-welcome-guide/style.scss";
@import "components/attribute-control/attribute-skeleton.scss";
@import "components/checkbox-control/style.scss";

View File

@ -16,7 +16,6 @@ import {
isWCAdmin,
} from '@woocommerce/navigation';
import { Spinner } from '@woocommerce/components';
import { ProductPageSkeleton } from '@woocommerce/product-editor';
/**
* Internal dependencies
@ -208,7 +207,6 @@ export const getPages = () => {
if ( isFeatureEnabled( 'product_block_editor' ) ) {
const productPage = {
container: ProductPage,
fallback: ProductPageSkeleton,
layout: {
header: false,
},
@ -274,7 +272,6 @@ export const getPages = () => {
if ( window.wcAdminFeatures[ 'product-variation-management' ] ) {
pages.push( {
container: ProductVariationPage,
fallback: ProductPageSkeleton,
layout: {
header: false,
},

View File

@ -10,10 +10,10 @@ import {
TRACKS_SOURCE,
__experimentalProductMVPCESFooter as FeedbackBar,
__experimentalProductMVPFeedbackModalContainer as ProductMVPFeedbackModalContainer,
ProductPageSkeleton,
__experimentalEditorLoadingContext as EditorLoadingContext,
} from '@woocommerce/product-editor';
import { recordEvent } from '@woocommerce/tracks';
import { useEffect } from '@wordpress/element';
import { useContext, useEffect } from '@wordpress/element';
import { registerPlugin, unregisterPlugin } from '@wordpress/plugins';
import { useParams } from 'react-router-dom';
import { WooFooterItem } from '@woocommerce/admin-layout';
@ -36,18 +36,42 @@ export default function ProductPage() {
const product = useProductEntityRecord( productId );
useEffect( () => {
registerPlugin( 'wc-admin-more-menu', {
registerPlugin( 'wc-admin-product-editor', {
// @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated.
scope: 'woocommerce-product-block-editor',
render: () => (
render: () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const isEditorLoading = useContext( EditorLoadingContext );
if ( isEditorLoading ) {
return null;
}
return (
<>
<WooProductMoreMenuItem>
{ ( { onClose }: { onClose: () => void } ) => (
<MoreMenuFill onClose={ onClose } />
) }
</WooProductMoreMenuItem>
<WooFooterItem>
<>
<FeedbackBar productType="product" />
<ProductMVPFeedbackModalContainer
productId={
productId
? parseInt( productId, 10 )
: undefined
}
/>
</>
),
</WooFooterItem>
<BlockEditorTourWrapper />
</>
);
},
} );
const unregisterBlocks = initBlocks();
@ -56,7 +80,7 @@ export default function ProductPage() {
unregisterPlugin( 'wc-admin-more-menu' );
unregisterBlocks();
};
}, [] );
}, [ productId ] );
useEffect(
function trackViewEvents() {
@ -74,25 +98,12 @@ export default function ProductPage() {
[ productId ]
);
if ( ! product?.id ) {
return <ProductPageSkeleton />;
}
return (
<>
<Editor
product={ product }
settings={ productBlockEditorSettings || {} }
/>
<WooFooterItem>
<>
<FeedbackBar productType="product" />
<ProductMVPFeedbackModalContainer
productId={ product.id }
/>
</>
</WooFooterItem>
<BlockEditorTourWrapper />
</>
);
}

View File

@ -10,7 +10,6 @@ import {
TRACKS_SOURCE,
__experimentalVariationSwitcherFooter as VariationSwitcherFooter,
__experimentalProductMVPFeedbackModalContainer as ProductMVPFeedbackModalContainer,
ProductPageSkeleton,
} from '@woocommerce/product-editor';
import { recordEvent } from '@woocommerce/tracks';
import { useEffect } from '@wordpress/element';
@ -80,10 +79,6 @@ export default function ProductPage() {
[ productId ]
);
if ( ! variation?.id ) {
return <ProductPageSkeleton />;
}
return (
<>
<Editor
@ -94,12 +89,12 @@ export default function ProductPage() {
<WooFooterItem order={ 0 }>
<>
<VariationSwitcherFooter
parentId={ variation.parent_id }
variationId={ variation.id }
parentId={ variation?.parent_id }
variationId={ variation?.id }
/>
<ProductMVPFeedbackModalContainer
productId={ variation.parent_id }
productId={ variation?.parent_id }
/>
</>
</WooFooterItem>

View File

@ -0,0 +1,4 @@
Significance: patch
Type: fix
Product Editor loading state now shows until form is displayed. No more blank flash of white.