Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Site Editor: Improve loading experience (v2) #47612

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/block-editor/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export { default as __experimentalLinkControlSearchResults } from './link-contro
export { default as __experimentalLinkControlSearchItem } from './link-control/search-item';
export { default as LineHeightControl } from './line-height-control';
export { default as __experimentalListView } from './list-view';
export { default as __experimentalLoadingScreen } from './loading-screen';
export { default as MediaReplaceFlow } from './media-replace-flow';
export { default as MediaPlaceholder } from './media-placeholder';
export { default as MediaUpload } from './media-upload';
Expand Down
145 changes: 145 additions & 0 deletions packages/block-editor/src/components/loading-screen/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* External dependencies
*/
import classNames from 'classnames';

/**
* WordPress dependencies
*/
import {
createContext,
Suspense,
useEffect,
useState,
} from '@wordpress/element';
import { Modal, Spinner } from '@wordpress/components';
import { useSuspenseSelect } from '@wordpress/data';

const LoadingScreenContext = createContext( false );

/**
* Component that declares a data dependency.
* Will suspend if data has not resolved.
*
* @param {Object} props Component props
* @param {import('@wordpress/data').StoreDescriptor} props.store Data store descriptor
* @param {string} props.selector Selector name
* @param {Array} props.args Optional arguments to pass to the selector
*/
const SuspenseDataDependency = ( { store, selector, args = [] } ) => {
useSuspenseSelect(
( select ) => select( store )[ selector ]( ...args ),
[]
);

return null;
};
Comment on lines +29 to +36
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the component that we render for each declared data dependency. It will suspend when the data has not been resolved yet.


/**
* Component that will render a loading screen if dependencies have not resolved,
* or its children if all dependencies have resolved.
*
* @param {Object} props Component props
* @param {Array} props.dataDependencies Array of dependencies
* @param {string} props.children Component children
* @param {string?} props.overlayClassName Additional overlay classname
*/
const SuspenseWithLoadingScreen = ( {
dataDependencies,
children,
overlayClassName,
} ) => {
const [ loaded, setLoaded ] = useState( false );

const finishedLoading = () => {
if ( ! loaded ) {
setLoaded( true );
}
};

return (
<LoadingScreenContext.Provider value={ loaded }>
{ loaded ? (
<>
<LoadingScreen
overlayClassName={ overlayClassName }
autoClose
/>
{ children }
</>
) : (
<Suspense
fallback={
<LoadingScreen
onUnmount={ finishedLoading }
overlayClassName={ overlayClassName }
/>
}
>
{ dataDependencies.map(
( { store, selector, args }, depindex ) => (
<SuspenseDataDependency
key={ `suspense-dep-${ depindex }-${ store.name }-${ selector }` }
store={ store }
selector={ selector }
args={ args }
/>
)
) }
Comment on lines +79 to +88
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's where we render one SuspenseDataDependency per data dependency.

{ children }
</Suspense>
) }
</LoadingScreenContext.Provider>
);
};

/**
* Renders a loading screen.
* Supports automatic closing with the `autoClose` prop.
*
* @param {Object} props Component props
* @param {Function?} props.onUnmount Optional callback to call on unmount.
* @param {boolean} props.autoClose Whether to automatically close.
* @param {string?} props.overlayClassName Additional overlay classname
*/
const LoadingScreen = ( { onUnmount, autoClose, overlayClassName } ) => {
const [ visible, setVisible ] = useState( true );

useEffect( () => {
if ( autoClose ) {
setTimeout( () => {
setVisible( false );
}, 2000 );
}

return () => {
if ( onUnmount ) {
onUnmount();
}
};
} );

if ( ! visible ) {
return null;
}

return (
<Modal
isFullScreen
isDismissible={ false }
onRequestClose={ () => {} }
__experimentalHideHeader
className="block-editor-loading-screen-modal"
overlayClassName={ classNames(
'block-editor-loading-screen-modal-overlay',
overlayClassName
) }
>
<div className="block-editor-loading-screen-wrapper">
<Spinner />
</div>
</Modal>
);
};

export default SuspenseWithLoadingScreen;
24 changes: 24 additions & 0 deletions packages/block-editor/src/components/loading-screen/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.block-editor-loading-screen-modal-overlay {
backdrop-filter: none;
background-color: transparent;
padding-top: $header-height;
}

.block-editor-loading-screen-modal-overlay.is-canvas-view {
padding-top: 0;
}

.block-editor-loading-screen-modal.is-full-screen {
box-shadow: 0 0 0 transparent;
width: 100%;
max-width: 100%;
max-height: 100%;
min-height: 100%;
}

.block-editor-loading-screen-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
1 change: 1 addition & 0 deletions packages/block-editor/src/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
@import "./components/justify-content-control/style.scss";
@import "./components/link-control/style.scss";
@import "./components/list-view/style.scss";
@import "./components/loading-screen/style.scss";
@import "./components/media-replace-flow/style.scss";
@import "./components/multi-selection-inspector/style.scss";
@import "./components/responsive-block-control/style.scss";
Expand Down
107 changes: 85 additions & 22 deletions packages/edit-site/src/components/editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
import { useMemo } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import { Notice } from '@wordpress/components';
import { EntityProvider } from '@wordpress/core-data';
import { EntityProvider, store as coreStore } from '@wordpress/core-data';
import { store as preferencesStore } from '@wordpress/preferences';
import {
BlockContextProvider,
BlockBreadcrumb,
__experimentalLoadingScreen as LoadingScreen,
store as blockEditorStore,
} from '@wordpress/block-editor';
import {
Expand Down Expand Up @@ -68,6 +69,7 @@ export default function Editor() {
isListViewOpen,
showIconLabels,
showBlockBreadcrumbs,
globalStylesId,
} = useSelect( ( select ) => {
const {
getEditedPostContext,
Expand All @@ -76,8 +78,16 @@ export default function Editor() {
isInserterOpened,
isListViewOpened,
} = unlock( select( editSiteStore ) );
const {
__experimentalGetCurrentGlobalStylesId,
getEditedEntityRecord,
} = select( coreStore );
const { __unstableGetEditorMode } = select( blockEditorStore );
const { getActiveComplementaryArea } = select( interfaceStore );
const _globalStylesId = __experimentalGetCurrentGlobalStylesId();
const globalStylesRecord = _globalStylesId
? getEditedEntityRecord( 'root', 'globalStyles', _globalStylesId )
: undefined;

// The currently selected entity to display.
// Typically template or template part in the site editor.
Expand All @@ -99,6 +109,8 @@ export default function Editor() {
'core/edit-site',
'showBlockBreadcrumbs'
),
globalStylesId: _globalStylesId,
globalStylesRecord,
};
}, [] );
const { setEditedPostContext } = useDispatch( editSiteStore );
Expand Down Expand Up @@ -152,6 +164,48 @@ export default function Editor() {
// action in <URlQueryController> from double-announcing.
useTitle( hasLoadedPost && title );

const contentDependencies = [
// Current post entity,
{
store: coreStore,
selector: 'getEntityRecord',
args: [ 'postType', 'postType', editedPostId ],
},
// Global styles entity ID
{
store: coreStore,
selector: '__experimentalGetCurrentGlobalStylesId',
},
// Global styles entity
globalStylesId && {
store: coreStore,
selector: 'getEditedEntityRecord',
args: [ 'root', 'globalStyles', globalStylesId ],
},
// Menus
{
store: coreStore,
selector: 'getEntityRecords',
args: [ 'root', 'menu', { per_page: -1, context: 'edit' } ],
},
// Pages
{
store: coreStore,
selector: 'getEntityRecords',
args: [
'postType',
'page',
{
parent: 0,
order: 'asc',
orderby: 'id',
per_page: -1,
context: 'view',
},
],
},
].filter( Boolean );

if ( ! hasLoadedPost ) {
return <CanvasSpinner />;
}
Expand All @@ -174,27 +228,36 @@ export default function Editor() {
notices={ isEditMode && <EditorSnackbars /> }
content={
<>
<GlobalStylesRenderer />
{ isEditMode && <EditorNotices /> }
{ showVisualEditor && editedPost && (
<BlockEditor />
) }
{ editorMode === 'text' &&
editedPost &&
isEditMode && <CodeEditor /> }
{ hasLoadedPost && ! editedPost && (
<Notice
status="warning"
isDismissible={ false }
>
{ __(
"You attempted to edit an item that doesn't exist. Perhaps it was deleted?"
) }
</Notice>
) }
{ isEditMode && (
<KeyboardShortcutsEditMode />
) }
<LoadingScreen
dataDependencies={ contentDependencies }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's how we pass declared dependencies to the suspense boundary.

Technically, this gives us the flexibility to have multiple suspense boundaries that have different data dependencies.

overlayClassName={
isViewMode
? 'is-canvas-view'
: undefined
}
>
<GlobalStylesRenderer />
{ isEditMode && <EditorNotices /> }
{ showVisualEditor && editedPost && (
<BlockEditor />
) }
{ editorMode === 'text' &&
editedPost &&
isEditMode && <CodeEditor /> }
{ hasLoadedPost && ! editedPost && (
<Notice
status="warning"
isDismissible={ false }
>
{ __(
"You attempted to edit an item that doesn't exist. Perhaps it was deleted?"
) }
</Notice>
) }
{ isEditMode && (
<KeyboardShortcutsEditMode />
) }
</LoadingScreen>
</>
}
secondarySidebar={
Expand Down