-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Changes from all commits
d4a2d69
9aa932a
676dc67
85b1deb
5444d21
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
}; | ||
|
||
/** | ||
* 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's where we render one |
||
{ 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; |
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%; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
|
@@ -68,6 +69,7 @@ export default function Editor() { | |
isListViewOpen, | ||
showIconLabels, | ||
showBlockBreadcrumbs, | ||
globalStylesId, | ||
} = useSelect( ( select ) => { | ||
const { | ||
getEditedPostContext, | ||
|
@@ -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. | ||
|
@@ -99,6 +109,8 @@ export default function Editor() { | |
'core/edit-site', | ||
'showBlockBreadcrumbs' | ||
), | ||
globalStylesId: _globalStylesId, | ||
globalStylesRecord, | ||
}; | ||
}, [] ); | ||
const { setEditedPostContext } = useDispatch( editSiteStore ); | ||
|
@@ -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 />; | ||
} | ||
|
@@ -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 } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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={ | ||
|
There was a problem hiding this comment.
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.