UNPKG

@gravityforms/components

Version:

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

271 lines (247 loc) 8.81 kB
import { React, PropTypes, classnames } from '@gravityforms/libraries'; import { ConditionalWrapper } from '@gravityforms/react-utils'; import { spacerClasses, clipboard } from '@gravityforms/utils'; import Button from '../Button'; const { forwardRef, useEffect, useRef } = React; /** * @module Text * @description Wraps html text with some preset style options in a configurable tag container. * When editable is true, the text becomes editable on focus, with accessibility and onChange support. * Optionally, a hidden input can store the value for form submission, and a button can copy content to the clipboard inline. * * @since 1.1.15 * * @param {object} props Component props. * @param {boolean} props.asHtml Whether or not to accept HTML in the content (ignored when editable). * @param {JSX.Element} props.children React element children (ignored when editable). * @param {string} props.color The text color. * @param {string} props.content The text content. * @param {object} props.customAttributes Custom attributes for the component. * @param {string|Array|object} props.customClasses Custom classes for the component. * @param {string} props.size The font size for the text. * @param {string|number|Array|object} props.spacing The spacing for the component. * @param {string} props.tagName The tag used for the container element. * @param {string} props.weight The font weight for the text. * @param {boolean} props.editable Whether the text is editable on focus (default false). * @param {function} props.onChange Handler for text changes when editable. * @param {string} props.name Optional name attribute when editable. * @param {string} props.id Optional id attribute when editable. * @param {boolean} props.useHiddenInput Whether to include a hidden input for form submission (default false). * @param {boolean} props.showCopyButton Whether to show a button to copy the content to the clipboard (default false). * @param {object} props.copyButtonAttributes Custom attributes for the copy button. * @param {string|Array|object} props.copyButtonClasses Custom classes for the copy button. * @param {function} props.onFocus Handler for focus events when editable (default empty function). * @param {function} props.onBlur Handler for blur events when editable (default empty function). * @param {string} props.placeholder Placeholder text when editable and empty (default empty string). * @param {object|null} ref Ref to the component. * * @return {JSX.Element} The text component. * * @example * // With copy button inline, non-editable * <Text content="Copy Me" showCopyButton /> * * // Controlled usage with copy button inline * const [text, setText] = useState("Hello world"); * <Text content={text} editable onChange={setText} showCopyButton /> * * // Self-managed form usage with copy button inline * <form> * <Text content="Hello world" editable useHiddenInput name="text-field" id="text-1" showCopyButton /> * <button type="submit">Submit</button> * </form> */ const Text = forwardRef( ( { asHtml = false, children = null, color = 'port', content = '', copyButtonAttributes = {}, copyButtonClasses = [], customAttributes = {}, customClasses = [], editable = false, id = '', name = '', onBlur = () => {}, onChange = () => {}, onFocus = () => {}, placeholder = '', showCopyButton = false, size = 'text-md', spacing = '', tagName = 'div', useHiddenInput = false, weight = 'regular', }, ref ) => { const effectiveAsHtml = asHtml && ! editable; const internalRef = useRef( null ); const combinedRef = ref || internalRef; const hiddenInputRef = useRef( null ); const componentProps = { className: classnames( { 'gform-text': true, 'gform-text__has-copy': showCopyButton, [ `gform-text--color-${ color }` ]: true, [ `gform-typography--size-${ size }` ]: true, [ `gform-typography--weight-${ weight }` ]: true, ...spacerClasses( spacing ), }, customClasses ), ref: combinedRef, ...customAttributes, }; const handleCopy = () => { const textToCopy = editable ? combinedRef.current.textContent : content; clipboard( textToCopy ); }; const copyButtonProps = { customClasses: classnames( { 'gform-text__copy-button': true, [ `gform-typography--size-${ size }` ]: true, }, copyButtonClasses ), icon: 'copy-alt', iconPrefix: 'gravity-component-icon', onClick: handleCopy, type: 'unstyled', ...copyButtonAttributes, }; const editableWithCopyWrapperProps = { className: classnames( { 'gform-text__wrapper': true, }, [] ), }; if ( editable ) { componentProps.contentEditable = true; componentProps.placeholder = placeholder; componentProps.tabIndex = 0; componentProps.role = 'textbox'; if ( ! useHiddenInput ) { if ( name ) { componentProps.name = name; } if ( id ) { componentProps.id = id; } } if ( ! onChange && ! useHiddenInput ) { console.warn( 'Text component: when editable is true and useHiddenInput is false, onChange prop is recommended for controlled behavior.' ); } } if ( effectiveAsHtml ) { componentProps.dangerouslySetInnerHTML = { __html: content }; } useEffect( () => { if ( editable && combinedRef.current ) { const element = combinedRef.current; if ( element.textContent !== content ) { element.textContent = content; } const handleInput = () => { const newText = element.textContent; if ( newText.trim() === '' ) { element.textContent = content; } if ( onChange ) { onChange( newText ); } if ( useHiddenInput && hiddenInputRef.current ) { hiddenInputRef.current.value = newText; } }; const handleBlur = () => { if ( element.textContent.trim() === '' ) { element.textContent = content; } onBlur( element.textContent ); }; const handleFocus = () => { onFocus( element.textContent ); }; element.addEventListener( 'input', handleInput ); element.addEventListener( 'blur', handleBlur ); element.addEventListener( 'focus', handleFocus ); return () => { element.removeEventListener( 'input', handleInput ); element.removeEventListener( 'blur', handleBlur ); element.removeEventListener( 'focus', handleFocus ); }; } }, [ editable, content, onChange, useHiddenInput, combinedRef, onBlur, onFocus ] ); const Container = tagName; const copyButton = showCopyButton ? ( <Button { ...copyButtonProps } /> ) : null; if ( effectiveAsHtml ) { return <Container { ...componentProps } />; } if ( editable ) { return ( <> <ConditionalWrapper condition={ showCopyButton } wrapper={ ( ch ) => <div { ...editableWithCopyWrapperProps }>{ ch }</div> } > <Container { ...componentProps } /> { showCopyButton && copyButton } </ConditionalWrapper> { useHiddenInput && ( <input type="hidden" ref={ hiddenInputRef } name={ name } id={ id } defaultValue={ content } /> ) } </> ); } return ( <Container { ...componentProps }> { content } { children } { showCopyButton && copyButton } </Container> ); } ); Text.propTypes = { asHtml: PropTypes.bool, children: PropTypes.oneOfType( [ PropTypes.arrayOf( PropTypes.node ), PropTypes.node, ] ), color: PropTypes.string, content: PropTypes.string, copyButtonAttributes: PropTypes.object, copyButtonClasses: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array, PropTypes.object, ] ), customAttributes: PropTypes.object, customClasses: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array, PropTypes.object, ] ), editable: PropTypes.bool, id: PropTypes.string, name: PropTypes.string, onBlur: PropTypes.func, onChange: PropTypes.func, onFocus: PropTypes.func, placeholder: PropTypes.string, showCopyButton: PropTypes.bool, size: PropTypes.string, spacing: PropTypes.oneOfType( [ PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object, ] ), tagName: PropTypes.string, useHiddenInput: PropTypes.bool, weight: PropTypes.string, }; Text.displayName = 'Text'; export default Text;