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;