components_cross-fade.js

import { React, PropTypes, classnames } from '@gravityforms/libraries';
import useId from '../hooks/use-id';

const { Children, useEffect, useRef, useState } = React;

/**
 * @module CrossFade
 * @description A component that cross fades the children.
 *
 * @since 4.0.3
 *
 * @param {object}              props                        Component props.
 * @param {number}              props.activeIndex            The active index.
 * @param {JSX.Element}         props.children               The children to cross fade.
 * @param {object}              props.childWrapperAttributes Custom attributes for the child wrapper.
 * @param {string|Array|object} props.childWrapperClasses    Custom classes for the child wrapper.
 * @param {object}              props.customAttributes       Custom attributes for the component.
 * @param {string|Array|object} props.customClasses          Custom classes for the component.
 * @param {number}              props.duration               The duration of the cross fade.
 * @param {string}              props.id                     The ID of the component.
 *
 * @return {JSX.Element} The CrossFade component.
 */
const CrossFade = ( {
	activeIndex = 0,
	children,
	childWrapperAttributes = {},
	childWrapperClasses = [],
	customAttributes = {},
	customClasses = [],
	duration = 250,
	id = '',
} ) => {
	const refs = useRef( [] );
	const [ renderIndex, setRenderIndex ] = useState( activeIndex );
	const [ prevRenderIndex, setPrevRenderIndex ] = useState( null );
	const crossFadeId = useId( id );

	// Initialize the refs array with null values
	if ( ! refs.current || refs.current.length !== Children.count( children ) ) {
		refs.current = Array.from( { length: Children.count( children ) }, () => null );
	}

	useEffect( () => {
		setRenderIndex( activeIndex );
		setTimeout( () => setPrevRenderIndex( activeIndex ), duration );
	}, [ activeIndex, duration ] );

	const componentProps = {
		className: classnames( customClasses ),
		id: crossFadeId,
		...customAttributes,
		style: {
			position: 'relative',
			...( customAttributes.style || {} ),
		},
	};

	return (
		<div { ...componentProps }>
			{ Children.map( children, ( child, index ) => {
				const shouldRender = [ renderIndex, prevRenderIndex ].includes( index );
				const childId = `${ crossFadeId }-${ index }`;
				const style = {
					transition: `opacity ${ duration }ms`,
				};
				if ( index === renderIndex ) {
					style.opacity = 1;
				} else if ( index === prevRenderIndex ) {
					style.position = 'absolute';
					style.top = '0';
					style.left = '0';
					style.width = '100%';
					style.opacity = 0;
				}
				const childProps = {
					className: classnames( childWrapperClasses ),
					id: childId,
					style,
					...childWrapperAttributes,
				};

				return (
					<div
						{ ...childProps }
						key={ childId }
						ref={ ( el ) => refs.current[ index ] = el }
					>
						{ shouldRender && child }
					</div>
				);
			} ) }
		</div>
	);
};

CrossFade.propTypes = {
	activeIndex: PropTypes.number,
	children: PropTypes.node,
	childWrapperAttributes: PropTypes.object,
	childWrapperClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	duration: PropTypes.number,
	id: PropTypes.string,
};

export default CrossFade;