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 the frame animation #60363

Merged
merged 4 commits into from Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/unit-test.yml
Expand Up @@ -158,7 +158,6 @@ jobs:
- name: Docker debug information
run: |
docker -v
docker-compose -v
Copy link
Member

Choose a reason for hiding this comment

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

Is this temporary to check what's causing the failures?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this is what was causing the failures actually. I guess these days it's docker compose and not docker-compose and Github probably updated docker or something so the PHP tests started failing, I expect that they'll fail elsewhere as well.

Copy link
Member

Choose a reason for hiding this comment

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

Shall we merge this so we fix the tests in other PRs as well?


- name: General debug information
run: |
Expand Down
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/edit-site/package.json
Expand Up @@ -27,6 +27,7 @@
"react-native": "src/index",
"dependencies": {
"@babel/runtime": "^7.16.0",
"@react-spring/web": "^9.4.5",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This adds a new dependency to the site editor but I think it's worth it.

"@wordpress/a11y": "file:../a11y",
"@wordpress/api-fetch": "file:../api-fetch",
"@wordpress/blob": "file:../blob",
Expand Down
122 changes: 122 additions & 0 deletions packages/edit-site/src/components/layout/animation.js
@@ -0,0 +1,122 @@
/**
* External dependencies
*/
import { Controller } from '@react-spring/web';

/**
* WordPress dependencies
*/
import { useLayoutEffect, useMemo, useRef } from '@wordpress/element';

function getAbsolutePosition( element ) {
return {
top: element.offsetTop,
left: element.offsetLeft,
};
}

const ANIMATION_DURATION = 300;

/**
* Hook used to compute the styles required to move a div into a new position.
*
* The way this animation works is the following:
* - It first renders the element as if there was no animation.
* - It takes a snapshot of the position of the block to use it
* as a destination point for the animation.
* - It restores the element to the previous position using a CSS transform
* - It uses the "resetAnimation" flag to reset the animation
* from the beginning in order to animate to the new destination point.
*
* @param {Object} $1 Options
* @param {*} $1.triggerAnimationOnChange Variable used to trigger the animation if it changes.
*/
function useMovingAnimation( { triggerAnimationOnChange } ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Potentially this can be a generic hook in the compose package but I think the recent perf optimization that we've made to the block animation made it less likely to be shared. Maybe it's fine for it to be duplicated. (cc @ellatrix )

The other difference with the block animation is that this one animates the "width" and "height" as well as the position.

const ref = useRef();

// Whenever the trigger changes, we need to take a snapshot of the current
// position of the block to use it as a destination point for the animation.
const { previous, prevRect } = useMemo(
() => ( {
previous: ref.current && getAbsolutePosition( ref.current ),
prevRect: ref.current && ref.current.getBoundingClientRect(),
} ),
// eslint-disable-next-line react-hooks/exhaustive-deps
[ triggerAnimationOnChange ]
);

useLayoutEffect( () => {
if ( ! previous || ! ref.current ) {
return;
}

// We disable the animation if the user has a preference for reduced
// motion.
const disableAnimation = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;

if ( disableAnimation ) {
return;
}

const controller = new Controller( {
x: 0,
y: 0,
width: prevRect.width,
height: prevRect.height,
config: { duration: ANIMATION_DURATION },
onChange( { value } ) {
if ( ! ref.current ) {
return;
}
let { x, y, width, height } = value;
x = Math.round( x );
y = Math.round( y );
width = Math.round( width );
height = Math.round( height );
const finishedMoving = x === 0 && y === 0;
ref.current.style.transformOrigin = 'center center';
ref.current.style.transform = finishedMoving
? null // Set to `null` to explicitly remove the transform.
: `translate3d(${ x }px,${ y }px,0)`;
ref.current.style.width = finishedMoving
? null
: `${ width }px`;
ref.current.style.height = finishedMoving
? null
: `${ height }px`;
},
} );

ref.current.style.transform = undefined;
const destination = ref.current.getBoundingClientRect();

const x = Math.round( prevRect.left - destination.left );
const y = Math.round( prevRect.top - destination.top );
const width = destination.width;
const height = destination.height;

controller.start( {
x: 0,
y: 0,
width,
height,
from: { x, y, width: prevRect.width, height: prevRect.height },
} );

return () => {
controller.stop();
controller.set( {
x: 0,
y: 0,
width: prevRect.width,
height: prevRect.height,
} );
};
}, [ previous, prevRect ] );

return ref;
}

export default useMovingAnimation;
39 changes: 12 additions & 27 deletions packages/edit-site/src/components/layout/index.js
Expand Up @@ -51,12 +51,13 @@ import { useCommonCommands } from '../../hooks/commands/use-common-commands';
import { useEditModeCommands } from '../../hooks/commands/use-edit-mode-commands';
import { useIsSiteEditorLoading } from './hooks';
import useLayoutAreas from './router';
import useMovingAnimation from './animation';

const { useCommands } = unlock( coreCommandsPrivateApis );
const { useCommandContext } = unlock( commandsPrivateApis );
const { useGlobalStyle } = unlock( blockEditorPrivateApis );

const ANIMATION_DURATION = 0.5;
const ANIMATION_DURATION = 0.3;

export default function Layout() {
// This ensures the edited entity id and type are initialized properly.
Expand Down Expand Up @@ -114,7 +115,10 @@ export default function Layout() {
const isEditorLoading = useIsSiteEditorLoading();
const [ isResizableFrameOversized, setIsResizableFrameOversized ] =
useState( false );
const { areas, widths } = useLayoutAreas();
const { key: routeKey, areas, widths } = useLayoutAreas();
const animationRef = useMovingAnimation( {
triggerAnimationOnChange: canvasMode + '__' + routeKey,
} );

// This determines which animation variant should apply to the header.
// There is also a `isDistractionFreeHovering` state that gets priority
Expand Down Expand Up @@ -239,7 +243,9 @@ export default function Layout() {
} }
transition={ {
type: 'tween',
duration: disableMotion ? 0 : 0.2,
duration: disableMotion
? 0
: ANIMATION_DURATION,
ease: 'easeOut',
} }
>
Expand Down Expand Up @@ -315,36 +321,15 @@ export default function Layout() {
<div className="edit-site-layout__canvas-container">
{ canvasResizer }
{ !! canvasSize.width && (
<motion.div
whileHover={
canvasMode === 'view'
? {
scale: 1.005,
transition: {
duration: disableMotion
? 0
: 0.5,
ease: 'easeOut',
},
}
: {}
}
initial={ false }
layout="position"
<div
className={ classnames(
'edit-site-layout__canvas',
{
'is-right-aligned':
isResizableFrameOversized,
}
) }
transition={ {
type: 'tween',
duration: disableMotion
? 0
: ANIMATION_DURATION,
ease: 'easeOut',
} }
ref={ animationRef }
>
<ErrorBoundary>
<ResizableFrame
Expand Down Expand Up @@ -373,7 +358,7 @@ export default function Layout() {
{ areas.preview }
</ResizableFrame>
</ErrorBoundary>
</motion.div>
</div>
) }
</div>
) }
Expand Down
6 changes: 6 additions & 0 deletions packages/edit-site/src/components/layout/router.js
Expand Up @@ -33,6 +33,7 @@ export default function useLayoutAreas() {
if ( path === '/page' ) {
const isListLayout = layout === 'list' || ! layout;
return {
key: 'pages-list',
areas: {
content: <PagePages />,
preview: isListLayout && (
Expand Down Expand Up @@ -62,6 +63,7 @@ export default function useLayoutAreas() {
// Regular other post types
if ( postType && postId ) {
return {
key: 'page',
areas: {
preview: <Editor isLoading={ isSiteEditorLoading } />,
mobile:
Expand All @@ -76,6 +78,7 @@ export default function useLayoutAreas() {
if ( path === '/wp_template' ) {
const isListLayout = isCustom !== 'true' && layout === 'list';
return {
key: 'templates-list',
areas: {
content: (
<PageTemplatesTemplateParts
Expand All @@ -101,6 +104,7 @@ export default function useLayoutAreas() {
if ( path === '/wp_template_part/all' ) {
const isListLayout = isCustom !== 'true' && layout === 'list';
return {
key: 'template-parts',
areas: {
content: (
<PageTemplatesTemplateParts
Expand All @@ -125,6 +129,7 @@ export default function useLayoutAreas() {
// Patterns
if ( path === '/patterns' ) {
return {
key: 'patterns',
areas: {
content: <PagePatterns />,
mobile: <PagePatterns />,
Expand All @@ -134,6 +139,7 @@ export default function useLayoutAreas() {

// Fallback shows the home page preview
return {
key: 'default',
areas: {
preview: <Editor isLoading={ isSiteEditorLoading } />,
mobile:
Expand Down
2 changes: 2 additions & 0 deletions packages/edit-site/src/components/layout/style.scss
Expand Up @@ -93,6 +93,8 @@
position: relative;
flex-grow: 1;
z-index: z-index(".edit-site-layout__canvas-container");
// When animating the frame size can exceed its container size.
overflow: visible;

&.is-resizing::after {
// This covers the whole content which ensures mouse up triggers
Expand Down