UNPKG

@gravityforms/components

Version:

UI components for use in Gravity Forms development. Both React and vanilla js flavors.

251 lines (233 loc) 8.75 kB
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;