Skip to content

Commit

Permalink
ToggleGroupControl: clean up animation logic (#65808)
Browse files Browse the repository at this point in the history
* Clean up and misc improvements.

* Add default to `transitionEndFilter`.

* Rename `attribute` to `dataAttribute` and improve docs.

* Update snapshot tests.

Co-authored-by: DaniGuardiola <[email protected]>
Co-authored-by: ciampo <[email protected]>
  • Loading branch information
3 people authored Oct 3, 2024
1 parent d660f37 commit 8e3146f
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] =
}
@media not ( prefers-reduced-motion ) {
.emotion-8.is-animation-enabled::before {
.emotion-8[data-indicator-animated]::before {
transition-property: transform,border-radius;
transition-duration: 0.2s;
transition-timing-function: ease-out;
Expand Down Expand Up @@ -426,7 +426,7 @@ exports[`ToggleGroupControl controlled should render correctly with text options
}
@media not ( prefers-reduced-motion ) {
.emotion-8.is-animation-enabled::before {
.emotion-8[data-indicator-animated]::before {
transition-property: transform,border-radius;
transition-duration: 0.2s;
transition-timing-function: ease-out;
Expand Down Expand Up @@ -695,7 +695,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`]
}
@media not ( prefers-reduced-motion ) {
.emotion-8.is-animation-enabled::before {
.emotion-8[data-indicator-animated]::before {
transition-property: transform,border-radius;
transition-duration: 0.2s;
transition-timing-function: ease-out;
Expand Down Expand Up @@ -1054,7 +1054,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with text optio
}
@media not ( prefers-reduced-motion ) {
.emotion-8.is-animation-enabled::before {
.emotion-8[data-indicator-animated]::before {
transition-property: transform,border-radius;
transition-duration: 0.2s;
transition-timing-function: ease-out;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,43 +20,79 @@ import { VisualLabelWrapper } from './styles';
import * as styles from './styles';
import { ToggleGroupControlAsRadioGroup } from './as-radio-group';
import { ToggleGroupControlAsButtonGroup } from './as-button-group';
import type { ElementOffsetRect } from '../../utils/element-rect';
import { useTrackElementOffsetRect } from '../../utils/element-rect';
import { useOnValueUpdate } from '../../utils/hooks/use-on-value-update';
import { useEvent, useMergeRefs } from '@wordpress/compose';

/**
* A utility used to animate something (e.g. an indicator for the selected option
* of a component).
* 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.
*
* It works by tracking the position and size (i.e., the "rect") of a given subelement,
* typically the one that corresponds to the selected option, relative to its offset
* parent. Then it:
* Takes in a container element and the up-to-date "offset rect" of the target
* subelement, obtained with `useTrackElementOffsetRect`. Then it does the following:
*
* - Keeps CSS variables with that information in the parent, so that the animation
* can be implemented with them.
* - Adds a `is-animation-enabled` CSS class when the element changes, so that the
* target (e.g. the indicator) can be animated to its new position.
* - Removes the `is-animation-enabled` class when the animation is done.
* - 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.
*/
function useSubelementAnimation(
subelement?: HTMLElement | null,
function useAnimatedOffsetRect(
/**
* The container element.
*/
container: HTMLElement | undefined,
/**
* The rect of the tracked element.
*/
rect: ElementOffsetRect,
{
parent = subelement?.offsetParent as HTMLElement | null | undefined,
prefix = 'subelement',
transitionEndFilter,
dataAttribute = `${ prefix }-animated`,
transitionEndFilter = () => true,
}: {
parent?: HTMLElement | null | undefined;
/**
* 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;
} = {}
) {
const rect = useTrackElementOffsetRect( subelement );

const setProperties = useEvent( () => {
( Object.keys( rect ) as Array< keyof typeof rect > ).forEach(
( property ) =>
property !== 'element' &&
parent?.style.setProperty(
container?.style.setProperty(
`--${ prefix }-${ property }`,
String( rect[ property ] )
)
Expand All @@ -68,19 +104,19 @@ function useSubelementAnimation(
useOnValueUpdate( rect.element, ( { previousValue } ) => {
// Only enable the animation when moving from one element to another.
if ( rect.element && previousValue ) {
parent?.classList.add( 'is-animation-enabled' );
container?.setAttribute( `data-${ dataAttribute }`, '' );
}
} );
useLayoutEffect( () => {
function onTransitionEnd( event: TransitionEvent ) {
if ( transitionEndFilter?.( event ) ?? true ) {
parent?.classList.remove( 'is-animation-enabled' );
if ( transitionEndFilter( event ) ) {
container?.removeAttribute( `data-${ dataAttribute }` );
}
}
parent?.addEventListener( 'transitionend', onTransitionEnd );
container?.addEventListener( 'transitionend', onTransitionEnd );
return () =>
parent?.removeEventListener( 'transitionend', onTransitionEnd );
}, [ parent, transitionEndFilter ] );
container?.removeEventListener( 'transitionend', onTransitionEnd );
}, [ dataAttribute, container, transitionEndFilter ] );
}

function UnconnectedToggleGroupControl(
Expand Down Expand Up @@ -110,9 +146,12 @@ function UnconnectedToggleGroupControl(
const [ selectedElement, setSelectedElement ] = useState< HTMLElement >();
const [ controlElement, setControlElement ] = useState< HTMLElement >();
const refs = useMergeRefs( [ setControlElement, forwardedRef ] );
useSubelementAnimation( value ? selectedElement : undefined, {
parent: controlElement,
const selectedRect = useTrackElementOffsetRect(
value ? selectedElement : undefined
);
useAnimatedOffsetRect( controlElement, selectedRect, {
prefix: 'selected',
dataAttribute: 'indicator-animated',
transitionEndFilter: ( event ) => event.pseudoElement === '::before',
} );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const toggleGroupControl = ( {
${ ! isDeselectable && enclosingBorders( isBlock ) }
@media not ( prefers-reduced-motion ) {
&.is-animation-enabled::before {
&[data-indicator-animated]::before {
transition-property: transform, border-radius;
transition-duration: 0.2s;
transition-timing-function: ease-out;
Expand Down

0 comments on commit 8e3146f

Please sign in to comment.