import { React, PropTypes, classnames } from '@gravityforms/libraries';
import { spacerClasses } from '@gravityforms/utils';
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.
*
* @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 heading.
* @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 heading.
* @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 {object|null} ref Ref to the component.
*
* @return {JSX.Element} The text component.
*
* @example
* // Controlled usage
* const [text, setText] = useState("Hello world");
* <Text content={text} editable onChange={setText} />
*
* // Self-managed form usage
* <form>
* <Text content="Hello world" editable useHiddenInput name="text-field" id="text-1" />
* <button type="submit">Submit</button>
* </form>
*/
const Text = forwardRef( ( {
asHtml = false,
children = null,
color = 'port',
content = '',
customAttributes = {},
customClasses = [],
editable = false,
id = '',
name = '',
onChange = () => {},
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 ); // Ref for hidden input
const componentProps = {
className: classnames( {
'gform-text': true,
[ `gform-text--color-${ color }` ]: true,
[ `gform-typography--size-${ size }` ]: true,
[ `gform-typography--weight-${ weight }` ]: true,
...spacerClasses( spacing ),
}, customClasses ),
ref: combinedRef,
...customAttributes,
};
if ( editable ) {
componentProps.contentEditable = true;
componentProps.tabIndex = 0;
componentProps.role = 'textbox';
// Only apply name/id to container if not using hidden input
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 ( 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 ( effectiveAsHtml ) {
return <Container { ...componentProps } />;
}
if ( editable ) {
return (
<>
<Container { ...componentProps } />
{ useHiddenInput && (
<input
type="hidden"
ref={ hiddenInputRef }
name={ name }
id={ id }
defaultValue={ content }
/>
) }
</>
);
}
return (
<Container { ...componentProps }>
{ content }
{ children }
</Container>
);
} );
Text.propTypes = {
asHtml: PropTypes.bool,
children: PropTypes.oneOfType( [
PropTypes.arrayOf( PropTypes.node ),
PropTypes.node,
] ),
color: PropTypes.string,
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,
useHiddenInput: PropTypes.bool,
weight: PropTypes.string,
};
Text.displayName = 'Text';
export default Text;