elements_Button_index.js

import { React, classnames, PropTypes } from '@gravityforms/libraries';
import Icon from '../Icon';
import RingLoader from '../../modules/Loaders/RingLoader';
import { spacerClasses } from '@gravityforms/utils';

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

const buttonSizeToTypographyMap = {
	'size-height-s': 'size-text-xs',
	'size-height-m': 'size-text-sm',
	'size-height-l': 'size-text-sm',
	'size-height-xl': 'size-text-sm',
	'size-height-xxl': 'size-text-md',
};

/**
 * @module Button
 * @description A highly configurable button component.
 *
 * @since 1.1.15
 *
 * @param {object}                     props                    Component props.
 * @param {boolean}                    props.active             Whether the button is active or not.
 * @param {string}                     props.activeText         The active text when the button is active.
 * @param {string}                     props.activeType         The active type, currently supports `loader`.
 * @param {string}                     props.ariaLabel          The aria-label text for the button.
 * @param {JSX.Element|null}           props.children           React element children.
 * @param {boolean}                    props.circular           Whether the button is a circular shape or not.
 * @param {object}                     props.customAttributes   Custom attributes for the component.
 * @param {string|Array|object}        props.customClasses      Custom classes for the component.
 * @param {boolean}                    props.disabled           Whether the button is disabled or not.
 * @param {boolean}                    props.disableWhileActive Whether to disable the button while active.
 * @param {string}                     props.icon               Icon name if using an icon button.
 * @param {object}                     props.iconAttributes     Custom attributes for the icon.
 * @param {string}                     props.iconPosition       Icon position if using one, `leading` or `trailing`.
 * @param {string}                     props.iconPrefix         The prefix for the icon library to be used.
 * @param {string}                     props.label              The label for the button, or the text displayed when inactive.
 * @param {object}                     props.loaderProps        All valid options for the loader component if loader button is active.
 * @param {boolean}                    props.lockSize           If interactive, whether to lock the width of the button when transitioning states.
 * @param {Function}                   props.onClick            On click handler for the button.
 * @param {string}                     props.size               Size of the button.
 * @param {string|number|Array|object} props.spacing            The spacing for the component, as a string, number, array, or object.
 * @param {string}                     props.type               The button type.
 * @param {string}                     props.width              The button width, `auto` or `full`.
 * @param {object|null}                ref                      Ref to the component.
 *
 * @return {JSX.Element} The button component.
 *
 * @example
 * import Button from '@gravityforms/components/react/admin/elements/Button';
 *
 * return (
 *     <Button onClick={ () => {} } size="size-height-xl" type="white">
 *         { 'Click me' }
 *     </Button>
 * );
 *
 */
const Button = forwardRef( ( {
	active = false,
	activeText = '',
	activeType = '',
	ariaLabel = '',
	children = null,
	circular = false,
	customAttributes = {},
	customClasses = [],
	disabled = false,
	disableWhileActive = true,
	icon = '',
	iconAttributes = {},
	iconPosition = '',
	iconPrefix = 'gform-icon',
	label = '',
	loaderProps = {
		customClasses: 'gform-button__loader', // additional classes for the loader element.
		lineWeight: 2, // line weight of the loader.
		size: 16, // size of the loader, decimal int values.
	},
	lockSize = false,
	onClick = () => {},
	size = 'size-r',
	spacing = '',
	type = 'primary-new',
	width = 'auto',
}, ref ) => {
	const typeIsIcon = [ 'icon-white', 'icon-grey' ].includes( type );
	const [ observer, setObserver ] = useState( null );
	const [ buttonSize, setButtonSize ] = useState( { width: 'auto', height: 'auto' } );
	const buttonRef = useRef();

	const setRefs = ( node ) => {
		buttonRef.current = node;
		if ( typeof ref === 'function' ) {
			ref( node );
		} else if ( ref ) {
			ref.current = node;
		}
	};

	useEffect( () => {
		if ( buttonRef.current && lockSize ) {
			// we use an observer as buttons may be hidden on initial render. We need to get dims when they become visible.
			const newObserver = new IntersectionObserver(
				( entries ) => {
					entries.forEach( ( entry ) => {
						if ( entry.isIntersecting ) {
							setButtonSize( {
								width: buttonRef.current.offsetWidth,
								height: buttonRef.current.offsetHeight,
							} );
							newObserver.disconnect();
						}
					} );
				},
				{ threshold: 0.1 }
			);

			newObserver.observe( buttonRef.current );
			setObserver( newObserver );
		}
		return () => {
			if ( observer ) {
				observer.disconnect();
			}
		};
	}, [ buttonRef, lockSize ] );

	const attributes = {
		className: classnames( {
			'gform-button': true,
			[ `gform-button--${ size }` ]: true,
			[ `gform-button--${ type }` ]: true,
			[ `gform-button--width-${ width }` ]: ! typeIsIcon,
			'gform-button--circular': ! typeIsIcon && circular,
			'gform-button--activated': active,
			[ `gform-button--active-type-${ activeType }` ]: activeType,
			'gform-button--loader-after': 'loader' === activeType,
			'gform-button--icon-leading': ! typeIsIcon && icon && iconPosition === 'leading',
			'gform-button--icon-trailing': ! typeIsIcon && icon && iconPosition === 'trailing',
			...spacerClasses( spacing ),
		}, customClasses ),
		onClick,
		disabled: disabled || ( disableWhileActive && active ),
		ref: setRefs,
		style: active && lockSize ? { width: `${ buttonSize.width }px`, height: `${ buttonSize.height }px` } : {},
		...customAttributes,
	};

	if ( ariaLabel ) {
		attributes[ 'aria-label' ] = ariaLabel;
	}

	const iconProps = {
		...iconAttributes,
		customClasses: classnames(
			[ 'gform-button__icon' ],
			( iconAttributes.customClasses || [] ),
		),
		icon,
		iconPrefix,
	};

	const getIconButtonContent = () => {
		const textHiddenClassName = classnames( {
			'gform-button__text': true,
			'gform-visually-hidden': true,
		} );

		return (
			<>
				<Icon { ...iconProps } />
				{ label && <span className={ textHiddenClassName }>{ label }</span> }
			</>
		);
	};

	const getButtonContent = () => {
		const textSize = buttonSizeToTypographyMap[ size ];
		const textInactiveClassName = classnames( {
			'gform-button__text': true,
			'gform-button__text--inactive': true,
			[ `gform-typography--${ textSize }` ]: 0 === size.indexOf( 'size-height-' ),
			'gform-visually-hidden': typeIsIcon,
		} );
		const textActiveClassName = classnames( {
			'gform-button__text': true,
			'gform-button__text--active': true,
			[ `gform-typography--${ textSize }` ]: 0 === size.indexOf( 'size-height-' ),
		} );
		const showActiveText = activeText && active;

		return (
			<>
				{ icon && ( ! label || iconPosition === 'leading' ) && <Icon { ...iconProps } /> }
				{ label && ! showActiveText && <span className={ textInactiveClassName }>{ label }</span> }
				{ showActiveText && <span className={ textActiveClassName }>{ activeText }</span> }
				{ icon && iconPosition === 'trailing' && <Icon { ...iconProps } /> }
				{ 'loader' === activeType && active && <RingLoader { ...loaderProps } /> }
				{ children }
			</>
		);
	};

	return (
		<button { ...attributes }>
			{ ( typeIsIcon && icon && getIconButtonContent() ) || getButtonContent() }
		</button>
	);
} );

Button.propTypes = {
	active: PropTypes.bool,
	activeText: PropTypes.string,
	activeType: PropTypes.oneOf( [ 'loader' ] ),
	children: PropTypes.oneOfType( [
		PropTypes.arrayOf( PropTypes.node ),
		PropTypes.node,
	] ),
	circular: PropTypes.bool,
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	disabled: PropTypes.bool,
	disableWhileActive: PropTypes.bool,
	icon: PropTypes.string,
	iconAttributes: PropTypes.object,
	iconPosition: PropTypes.oneOf( [ 'leading', 'trailing' ] ),
	iconPrefix: PropTypes.string,
	label: PropTypes.string,
	loaderProps: PropTypes.object,
	lockSize: PropTypes.bool,
	onClick: PropTypes.func,
	size: PropTypes.string,
	spacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	type: PropTypes.string,
	width: PropTypes.string,
};

Button.displayName = 'Button';

export default Button;