modules_SnackBar_index.js

import { React, PropTypes, classnames } from '@gravityforms/libraries';
import { spacerClasses } from '@gravityforms/utils';
import Button from '../../elements/Button';
import Icon from '../../elements/Icon';

const { forwardRef, useEffect, useState, createContext, useContext } = React;

const SnackbarSettingsContext = createContext( {} );
const SnackbarContext = createContext( null );

/**
 * @module SnackBar
 * @description Renders a SnackBar module.
 *
 * @since 3.4.0
 *
 * @param {object}                     props                  Component props.
 * @param {JSX.Element}                props.children         React element children.
 * @param {object}                     props.customAttributes Custom attributes for the component
 * @param {string|Array|object}        props.customClasses    Custom classes for the component.
 * @param {number}                     props.delay            The delay for the component.
 * @param {string}                     props.id               The id for the component.
 * @param {string}                     props.message          The message for the component.
 * @param {Function}                   props.onDismiss        Callback function to run when the component is dismissed.
 * @param {string|number|Array|object} props.spacing          The spacing for the component, as a string, number, array, or object.
 * @param {string}                     props.theme            The theme for the component.
 * @param {string}                     props.type             The type for the component.
 * @param {object|null}                ref                    Ref to the component.
 *
 * @return {JSX.Element} The icon component.
 *
 * @example
 * import SnackBar from '@gravityforms/components/react/admin/modules/SnackBar';
 *
 * return <SnackBar delay={ 2000 } message="Success saving settings!" />;
 *
 */
const SnackBar = forwardRef( ( props, ref ) => {
	const globalSettings = useContext( SnackbarSettingsContext );
	const mergedProps = { ...globalSettings, ...props };

	const {
		ariaLive = 'polite',
		children = null,
		closeButtonAttributes = {},
		closeButtonClasses = [],
		customAttributes = {},
		customClasses = [],
		delay = 5000,
		errorIconAttributes = {},
		errorIconClasses = [],
		id = '',
		interactive = false,
		message = '',
		onDismiss = () => {},
		spacing = '',
		successIconAttributes = {},
		successIconClasses = [],
		theme = 'cosmos',
		type = 'success',
		...otherProps
	} = mergedProps;

	const [ isVisible, setIsVisible ] = useState( false );

	useEffect( () => {
		setTimeout( () => {
			setIsVisible( true );
		}, 50 );
	}, [] );
	useEffect( () => {
		if ( interactive ) {
			return;
		}
		const timer = setTimeout( () => {
			setIsVisible( false );
		}, delay );

		const timerForRemove = setTimeout( () => {
			onDismiss( id );
		}, ( delay + 100 ) );

		return () => {
			clearTimeout( timer );
			clearTimeout( timerForRemove );
		};
	}, [ interactive, delay, id, onDismiss ] );

	const componentProps = {
		className: classnames( {
			'gform-snackbar': true,
			'gform-snackbar--react': true,
			'gform-snackbar--interactive': interactive,
			'gform-snackbar--visible': isVisible,
			[ `gform-snackbar--theme-${ theme }` ]: true,
			[ `gform-snackbar--type-${ type }` ]: true,
			...spacerClasses( spacing ),
		}, customClasses ),
		style: {
			...customAttributes.style,
			'--gform-snackbar-animation-delay': `${ isVisible ? 0 : delay }ms`,
		},
		'aria-live': ariaLive,
		ref,
		...customAttributes,
	};
	const closeButtonProps = {
		customClasses: classnames( {
			'gform-snackbar__close': true,
		}, closeButtonClasses ),
		iconPosition: 'leading',
		type: 'unstyled',
		...closeButtonAttributes,
	};
	const errorIconProps = {
		customClasses: classnames( {
			'gform-snackbar__type-icon': true,
			'gform-snackbar__type-icon--error': true,
		}, errorIconClasses ),
		icon: 'delete',
		iconPrefix: 'gform-icon',
		spacing: [ 0, 3, 0, 0 ],
		...errorIconAttributes,
	};

	const successIconProps = {
		customClasses: classnames( {
			'gform-snackbar__type-icon': true,
			'gform-snackbar__type-icon--success': true,
		}, successIconClasses ),
		icon: 'check',
		iconPrefix: 'gform-icon',
		spacing: [ 0, 3, 0, 0 ],
		...successIconAttributes,
	};

	return (
		<div { ...componentProps } { ...otherProps }>
			{ type === 'error' && <Icon { ...errorIconProps } /> }
			{ type === 'success' && <Icon { ...successIconProps } /> }
			{ message }
			{ children }
			{ interactive && <Button { ...closeButtonProps } onClick={ () => {
				onDismiss( id );
				setIsVisible( false );
			} } /> }
		</div>
	);
} );

export const SnackbarProvider = ( { children, defaultSettings = {} } ) => {
	const [ messages, setMessages ] = useState( [] );

	const addMessage = ( message, type = 'success', additionalProps = {} ) => {
		// Merge the global defaults with the individual message settings
		const settings = { ...defaultSettings, type, ...additionalProps };

		// Generate a unique ID for the message
		const id = Math.random().toString( 36 ).substring( 7 );

		setMessages( ( prevMessages ) => [ ...prevMessages, { id, message, ...settings } ] );
	};

	const removeMessage = ( id ) => {
		setMessages( ( prevMessages ) => prevMessages.filter( ( message ) => message.id !== id ) );
	};

	return (
		<SnackbarSettingsContext.Provider value={ defaultSettings }>
			<SnackbarContext.Provider value={ addMessage }>
				{ children }
				{ messages.map( ( { id, message, type, ...otherProps }, index ) => (
					<SnackBar
						customAttributes={ { style: { '--gform-snackbar-index': index } } }
						id={ id }
						key={ id }
						message={ message }
						onDismiss={ removeMessage }
						type={ type }
						{ ...otherProps }
					/>
				) ) }
			</SnackbarContext.Provider>
		</SnackbarSettingsContext.Provider>
	);
};

export const useSnackbar = () => {
	return useContext( SnackbarContext );
};

SnackBar.propTypes = {
	children: PropTypes.oneOfType( [
		PropTypes.arrayOf( PropTypes.node ),
		PropTypes.node,
	] ),
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	delay: PropTypes.number,
	id: PropTypes.string,
	message: PropTypes.string,
	onDismiss: PropTypes.func,
	spacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	theme: PropTypes.string,
	type: PropTypes.string,
};

SnackBar.displayName = 'SnackBar';

export default SnackBar;