UNPKG

@gravityforms/components

Version:

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

179 lines (163 loc) 5.69 kB
import { React, PropTypes, classnames } from '@gravityforms/libraries'; import { spacerClasses } from '@gravityforms/utils'; const { forwardRef, useEffect, useRef } = React; /** * @module Heading * @description The heading component with optional editability. * 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. * * @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 {object|null} ref Ref to the component. * * @return {JSX.Element} The heading component. * * @example * // Controlled usage * const [heading, setHeading] = useState("Editable Heading"); * <Heading tagName="h2" content={heading} editable onChange={setHeading} /> * * // Self-managed form usage * <form> * <Heading tagName="h2" content="Editable Heading" editable useHiddenInput name="heading" id="heading-1" /> * <button type="submit">Submit</button> * </form> */ const Heading = forwardRef( ( { children = null, content = '', customAttributes = {}, customClasses = [], editable = false, id = '', name = '', onChange = () => {}, 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 ); // Ref for hidden input const headingAttributes = { className: classnames( { 'gform-heading': true, 'gform-text': true, // Maintains compatibility with text styles [ `gform-typography--size-${ size }` ]: true, [ `gform-typography--weight-${ weight }` ]: true, [ `gform-heading--${ type }` ]: true, ...spacerClasses( spacing ), }, customClasses ), ref: combinedRef, ...customAttributes, }; if ( editable ) { headingAttributes.contentEditable = true; headingAttributes.tabIndex = 0; headingAttributes.role = 'textbox'; // Apply name/id to container only if not using hidden input 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; // Set initial content only if it differs if ( element.textContent !== content ) { element.textContent = content; } const handleInput = () => { const newText = element.textContent; if ( onChange ) { onChange( newText ); } if ( useHiddenInput && hiddenInputRef.current ) { hiddenInputRef.current.value = newText; // Sync hidden input } }; element.addEventListener( 'input', handleInput ); return () => element.removeEventListener( 'input', handleInput ); } }, [ editable, content, onChange, useHiddenInput, combinedRef ] ); const Container = tagName; if ( editable ) { return ( <> <Container { ...headingAttributes } /> { useHiddenInput && ( <input type="hidden" ref={ hiddenInputRef } name={ name } id={ id } defaultValue={ content } /> ) } </> ); } return ( <Container { ...headingAttributes }> { content } { children } </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, onChange: PropTypes.func, 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;