UNPKG

@wordpress/components

Version:
120 lines (117 loc) 4.28 kB
/* eslint-disable jsdoc/require-param */ /** * WordPress dependencies */ import { useEvent } from '@wordpress/compose'; import { useLayoutEffect } from '@wordpress/element'; /** * Internal dependencies */ import type { ElementOffsetRect } from '../element-rect'; import { useOnValueUpdate } from './use-on-value-update'; /** * A utility used to animate something in a container component based on the "offset * rect" (position relative to the container and size) of a subelement. For example, * this is useful to render an indicator for the selected option of a component, and * to animate it when the selected option changes. * * Takes in a container element and the up-to-date "offset rect" of the target * subelement, obtained with `useTrackElementOffsetRect`. Then it does the following: * * - Adds CSS variables with rect information to the container, so that the indicator * can be rendered and animated with them. These are kept up-to-date, enabling CSS * transitions on change. * - Sets an attribute (`data-subelement-animated` by default) when the tracked * element changes, so that the target (e.g. the indicator) can be animated to its * new size and position. * - Removes the attribute when the animation is done. * * The need for the attribute is due to the fact that the rect might update in * situations other than when the tracked element changes, e.g. the tracked element * might be resized. In such cases, there is no need to animate the indicator, and * the change in size or position of the indicator needs to be reflected immediately. */ export function useAnimatedOffsetRect( /** * The container element. */ container: HTMLElement | undefined, /** * The rect of the tracked element. */ rect: ElementOffsetRect, { prefix = 'subelement', dataAttribute = `${ prefix }-animated`, transitionEndFilter = () => true, roundRect = false, }: { /** * The prefix used for the CSS variables, e.g. if `prefix` is `selected`, the * CSS variables will be `--selected-top`, `--selected-left`, etc. * @default 'subelement' */ prefix?: string; /** * The name of the data attribute used to indicate that the animation is in * progress. The `data-` prefix is added automatically. * * For example, if `dataAttribute` is `indicator-animated`, the attribute will * be `data-indicator-animated`. * @default `${ prefix }-animated` */ dataAttribute?: string; /** * A function that is called with the transition event and returns a boolean * indicating whether the animation should be stopped. The default is a function * that always returns `true`. * * For example, if the animated element is the `::before` pseudo-element, the * function can be written as `( event ) => event.pseudoElement === '::before'`. * @default () => true */ transitionEndFilter?: ( event: TransitionEvent ) => boolean; /** * Whether the `rect` measurements should be rounded down when applied * to the CSS variables. This can be useful to avoid blurry animations or * to avoid subpixel rendering issues. * @default false */ roundRect?: boolean; } = {} ) { const setProperties = useEvent( () => { ( Object.keys( rect ) as Array< keyof typeof rect > ).forEach( ( property ) => property !== 'element' && container?.style.setProperty( `--${ prefix }-${ property }`, String( roundRect ? Math.floor( rect[ property ] ) : rect[ property ] ) ) ); } ); useLayoutEffect( () => { setProperties(); }, [ rect, setProperties ] ); useOnValueUpdate( rect.element, ( { previousValue } ) => { // Only enable the animation when moving from one element to another. if ( rect.element && previousValue ) { container?.setAttribute( `data-${ dataAttribute }`, '' ); } } ); useLayoutEffect( () => { function onTransitionEnd( event: TransitionEvent ) { if ( transitionEndFilter( event ) ) { container?.removeAttribute( `data-${ dataAttribute }` ); } } container?.addEventListener( 'transitionend', onTransitionEnd ); return () => container?.removeEventListener( 'transitionend', onTransitionEnd ); }, [ dataAttribute, container, transitionEndFilter ] ); } /* eslint-enable jsdoc/require-param */