UNPKG

@gravityforms/components

Version:

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

250 lines (226 loc) 7.79 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 Heading * @description The heading component with optional editability and a copy-to-clipboard button. * When editable is true, the heading 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 {JSX.Element} props.children React element children (ignored when editable). * @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 heading. * @param {string|number|Array|object} props.spacing The spacing for the component. * @param {string} props.tagName The tag used for the heading, from `h1` to `h6`. * @param {string} props.type The type of the heading, one of `regular` or `boxed`. * @param {string} props.weight The font weight for the heading. * @param {boolean} props.editable Whether the heading 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|null} ref Ref to the component. * * @return {JSX.Element} The heading component. * * @example * // With copy button inline, non-editable * <Heading tagName="h2" content="Copy Me" showCopyButton /> * * // Controlled usage with copy button inline * const [heading, setHeading] = useState("Editable Heading"); * <Heading tagName="h2" content={heading} editable onChange={setHeading} showCopyButton /> * * // Self-managed form usage with copy button inline * <form> * <Heading tagName="h2" content="Editable Heading" editable useHiddenInput name="heading" id="heading-1" showCopyButton /> * <button type="submit">Submit</button> * </form> */ const Heading = forwardRef( ( { children = null, content = '', copyButtonAttributes = {}, copyButtonClasses = [], customAttributes = {}, customClasses = [], editable = false, id = '', name = '', onBlur = () => {}, onChange = () => {}, onFocus = () => {}, placeholder = '', showCopyButton = false, size = 'display-3xl', spacing = '', tagName = 'h1', type = 'regular', useHiddenInput = false, weight = 'semibold', }, ref ) => { const internalRef = useRef( null ); const combinedRef = ref || internalRef; const hiddenInputRef = useRef( null ); const headingAttributes = { className: classnames( { 'gform-heading': true, 'gform-heading__has-copy': showCopyButton, [ `gform-typography--size-${ size }` ]: true, [ `gform-typography--weight-${ weight }` ]: true, [ `gform-heading--${ type }` ]: true, ...spacerClasses( spacing ), }, customClasses ), ref: combinedRef, ...customAttributes, }; const handleCopy = () => { const textToCopy = editable ? combinedRef.current.textContent : content; clipboard( textToCopy ); }; const copyButtonProps = { customClasses: classnames( { 'gform-heading__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-heading__wrapper': true, }, [] ), }; if ( editable ) { headingAttributes.contentEditable = true; headingAttributes.placeholder = placeholder; headingAttributes.tabIndex = 0; headingAttributes.role = 'textbox'; if ( ! useHiddenInput ) { if ( name ) { headingAttributes.name = name; } if ( id ) { headingAttributes.id = id; } } if ( ! onChange && ! useHiddenInput ) { console.warn( 'Heading component: when editable is true and useHiddenInput is false, onChange prop is recommended for controlled behavior.' ); } } 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 ( editable ) { return ( <> <ConditionalWrapper condition={ showCopyButton } wrapper={ ( ch ) => <div { ...editableWithCopyWrapperProps }>{ ch }</div> } > <Container { ...headingAttributes } /> { showCopyButton && copyButton } </ConditionalWrapper> { useHiddenInput && ( <input type="hidden" ref={ hiddenInputRef } name={ name } id={ id } defaultValue={ content } /> ) } </> ); } return ( <Container { ...headingAttributes }> { content } { children } { showCopyButton && copyButton } </Container> ); } ); Heading.propTypes = { children: PropTypes.oneOfType( [ PropTypes.arrayOf( PropTypes.node ), PropTypes.node, ] ), content: PropTypes.string, 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, type: PropTypes.string, useHiddenInput: PropTypes.bool, weight: PropTypes.string, }; Heading.displayName = 'Heading'; export default Heading;