Skip to content

Commit

Permalink
Add some really dodgy animation code
Browse files Browse the repository at this point in the history
  • Loading branch information
talldan committed Dec 20, 2022
1 parent 5e90d55 commit a60d775
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useMergeRefs, useRefEffect } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
import {
createPortal,
forwardRef,
useContext,
useEffect,
useMemo,
Expand All @@ -34,19 +35,25 @@ import { useBlockAlignmentZoneContext } from './zone-context';
/**
* A component that displays block alignment guidelines.
*
* @param {Object} root0
* @param {?string[]} root0.allowedAlignments An optional array of alignments names. By default, the alignment support will be derived from the
* 'focused' block's block supports, but some blocks (image) have an ad-hoc alignment implementation.
* @param {string|null} root0.layoutClientId The client id of the block that provides the layout.
* @param {string} root0.focusedClientId The client id of the block to show the alignment guides for.
* @param {?string} root0.highlightedAlignment The alignment name to show the label of.
* @param {Object} props
* @param {?string[]} props.allowedAlignments An optional array of alignments names. By default, the alignment support will be derived from the
* 'focused' block's block supports, but some blocks (image) have an ad-hoc alignment implementation.
* @param {import('react').ReactElement} props.children
* @param {string|null} props.layoutClientId The client id of the block that provides the layout.
* @param {string} props.focusedClientId The client id of the block to show the alignment guides for.
* @param {?string} props.highlightedAlignment The alignment name to show the label of.
* @param {import('react').Ref} ref
*/
export default function BlockAlignmentVisualizer( {
allowedAlignments,
layoutClientId,
focusedClientId,
highlightedAlignment,
} ) {
function BlockAlignmentVisualizer(
{
allowedAlignments,
children,
layoutClientId,
focusedClientId,
highlightedAlignment,
},
ref
) {
const { focusedBlockName, layoutBlockName, layoutBlockAttributes } =
useSelect(
( select ) => {
Expand Down Expand Up @@ -206,6 +213,7 @@ export default function BlockAlignmentVisualizer( {

return (
<Popover
ref={ ref }
anchor={ popoverAnchor }
placement="top-start"
className="block-editor__alignment-visualizer"
Expand Down Expand Up @@ -288,10 +296,13 @@ export default function BlockAlignmentVisualizer( {
</div>
</Iframe>
</div>
{ children }
</Popover>
);
}

export default forwardRef( BlockAlignmentVisualizer );

function BlockAlignmentVisualizerZone( {
alignment,
justification,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '@wordpress/components';
import { throttle } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
import { useState } from '@wordpress/element';
import { useEffect, useRef, useState } from '@wordpress/element';
import { isRTL } from '@wordpress/i18n';

/**
Expand Down Expand Up @@ -145,14 +145,33 @@ function ResizableAlignmentControls( {
const [ isAlignmentVisualizerVisible, setIsAlignmentVisualizerVisible ] =
useState( false );
const [ snappedAlignment, setSnappedAlignment ] = useState( null );
const [ showNaturalContent, setShowNaturalContent ] = useState( true );
const alignmentZones = useBlockAlignmentZoneContext();
const resizableBoxRef = useRef();
const alignmentVisualizerRef = useRef();

const rootClientId = useSelect(
( select ) =>
select( blockEditorStore ).getBlockRootClientId( clientId ),
[ clientId ]
);

useEffect( () => {
let timeoutId = null;
if ( ! snappedAlignment ) {
// Show the 'natural' content only after the animation has completed.
// This is not a great way to do it, but suffices for demoing the animation.
timeoutId = setTimeout( () => setShowNaturalContent( true ), 90 );
}

return () => {
if ( timeoutId ) {
clearTimeout( timeoutId );
timeoutId = null;
}
};
}, [ isAlignmentVisualizerVisible, snappedAlignment ] );

return (
<>
<AnimatePresence>
Expand All @@ -164,21 +183,27 @@ function ResizableAlignmentControls( {
transition={ { duration: 0.15 } }
>
<BlockAlignmentVisualizer
ref={ alignmentVisualizerRef }
layoutClientId={ rootClientId }
focusedClientId={ clientId }
allowedAlignments={ allowedAlignments }
highlightedAlignment={ snappedAlignment }
/>
>
<SnappedContent
alignmentVisualizerRef={
alignmentVisualizerRef
}
alignmentZone={ alignmentZones.get(
snappedAlignment
) }
resizableBoxRef={ resizableBoxRef }
>
{ children }
</SnappedContent>
</BlockAlignmentVisualizer>
</motion.div>
) }
</AnimatePresence>
{ isAlignmentVisualizerVisible && (
<SnappedContent
alignmentZone={ alignmentZones.get( snappedAlignment ) }
>
{ children }
</SnappedContent>
) }
<ResizableBox
size={ size }
showHandle={ showHandle }
Expand All @@ -202,6 +227,10 @@ function ResizableAlignmentControls( {
}
} }
onResize={ ( event, resizeDirection, resizableElement ) => {
if ( resizableBoxRef.current !== resizableElement ) {
resizableBoxRef.current = resizableElement;
}

// Detect if snapping is happening.
const newSnappedAlignment = throttledDetectSnapping(
resizableElement,
Expand All @@ -210,6 +239,7 @@ function ResizableAlignmentControls( {
);
if ( newSnappedAlignment !== snappedAlignment ) {
setSnappedAlignment( newSnappedAlignment );
setShowNaturalContent( false );
}
} }
onResizeStop={ ( ...resizeArgs ) => {
Expand All @@ -220,12 +250,13 @@ function ResizableAlignmentControls( {
}
setIsAlignmentVisualizerVisible( false );
setSnappedAlignment( null );
setShowNaturalContent( true );
} }
resizeRatio={ currentAlignment === 'center' ? 2 : 1 }
>
<div
style={ {
visibility: snappedAlignment ? 'hidden' : 'visible',
visibility: showNaturalContent ? 'visible' : 'hidden',
width: '100%',
} }
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,52 +1,106 @@
/**
* WordPress dependencies
*/
import { Popover } from '@wordpress/components';
import { __unstableMotion as motion } from '@wordpress/components';
import { useLayoutEffect, useState } from '@wordpress/element';

export default function SnappedContent( { alignmentZone, children } ) {
const [ snapStyle, setSnapStyle ] = useState( null );
function getOffsetRect( element, offsetElement ) {
const rect = element.getBoundingClientRect();
const offsetRect = offsetElement.getBoundingClientRect();
// const frame = ownerDocument?.defaultView?.frameElement;
// const frameRect = frame?.getBoundingClientRect();

return new window.DOMRect(
rect.x - ( offsetRect?.x ?? 0 ),
rect.y - ( offsetRect?.y ?? 0 ),
rect.width,
rect.height
);
}

export default function SnappedContent( {
alignmentVisualizerRef,
alignmentZone,
children,
resizableBoxRef,
} ) {
const [ animationProps, setAnimationProps ] = useState( {} );
const [ anchor, setAnchor ] = useState( null );

useLayoutEffect( () => {
if ( ! alignmentZone ) {
return setSnapStyle( { visibility: 'hidden' } );
if ( ! resizableBoxRef.current || ! alignmentVisualizerRef.current ) {
return;
}

const ownerDocument = alignmentZone.ownerDocument;
const defaultView = ownerDocument.defaultView;
const resizableBoxRect = getOffsetRect(
resizableBoxRef.current,
alignmentVisualizerRef.current
);

function update() {
const rect = alignmentZone.getBoundingClientRect();
if ( ! alignmentZone ) {
// When unsnapping, animate the image back to the resizable box's position,
// and then set it as 'hidden'.
setAnimationProps( {
animate: {
x: resizableBoxRect.x,
y: 0,
width: resizableBoxRect.width,
height: resizableBoxRect.height,
transitionEnd: {
visibility: 'hidden',
},
},
transition: { duration: 0.1 },
} );
return;
}

setSnapStyle( {
position: 'absolute',
width: rect.width,
height: 'auto',
if ( ! anchor ) {
setAnchor( {
getBoundingClientRect: () =>
alignmentZone.parentElement.getBoundingClientRect(),
ownerDocument: alignmentZone.ownerDocument,
} );
}

const resizeObserver = defaultView.ResizeObserver
? new defaultView.ResizeObserver( update )
: undefined;
resizeObserver?.observe( alignmentZone );
const alignmentZoneRect = alignmentZone.getBoundingClientRect();
const aspectRatio = resizableBoxRect.width / resizableBoxRect.height;

return () => {
resizeObserver?.disconnect();
};
// When snapping, first move the image immediately to the resizable box's current position,
// making it visible.
setAnimationProps( {
animate: {
visibility: 'visible',
x: resizableBoxRect.x,
y: 0,
width: resizableBoxRect.width,
height: resizableBoxRect.height,
},
transition: { duration: 0 },
} );

// Then using `requestAnimationFrame` to defer the animation, animate to the alignment zone's
// position.
window.requestAnimationFrame( () => {
setAnimationProps( {
animate: {
visibility: 'visible',
x: alignmentZoneRect.x,
y: 0,
width: alignmentZoneRect.width,
height: alignmentZoneRect.width / aspectRatio,
},
transition: { duration: 0.1 },
} );
} );
}, [ alignmentZone ] );

return (
<Popover
anchor={ alignmentZone }
placement="top-start"
animate={ false }
focusOnMount={ false }
flip={ false }
resize={ false }
variant="unstyled"
__unstableInline
<motion.div
style={ { position: 'absolute', visibility: 'hidden' } }
{ ...animationProps }
>
<div style={ snapStyle }>{ children }</div>
</Popover>
{ children }
</motion.div>
);
}

1 comment on commit a60d775

@github-actions
Copy link

Choose a reason for hiding this comment

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

Flaky tests detected.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/3738798924
📝 Reported issues:

Please sign in to comment.