wix-style-react
Version:
299 lines (255 loc) • 8.46 kB
JavaScript
import PropTypes from 'prop-types';
import React from 'react';
import InfoIcon from '../InfoIcon';
import Text, { SIZES, SKINS, WEIGHTS } from '../Text';
import { dataHooks } from './constants';
import { st, classes } from './FormField.st.css';
import { TooltipCommonProps } from '../common/PropTypes/TooltipCommon';
import { WixStyleReactContext } from '../WixStyleReactProvider/context';
const PLACEMENT = {
top: 'top',
right: 'right',
left: 'left',
};
const ALIGN = {
middle: 'middle',
top: 'top',
};
const asterisk = (
<div
data-hook={dataHooks.asterisk}
className={classes.asterisk}
children="*"
/>
);
const charactersLeft = lengthLeft => {
const colorProps =
lengthLeft >= 0 ? { light: true, secondary: true } : { skin: SKINS.error };
return (
<Text
size={SIZES.small}
weight={WEIGHTS.normal}
{...colorProps}
dataHook={dataHooks.counter}
children={lengthLeft}
/>
);
};
class FormField extends React.Component {
static displayName = 'FormField';
static propTypes = {
/** when function, it receives object with:
* * `setCharactersLeft` - function accepts a number and will display it on top right of `FormField` component
*
* Note that alternatively you can also use `charCount` prop to display character count
* instead of using the render function method.
*/
/** Accept any kind of component as a child element. A child should be a form element like an Input, InputArea, Dropdown or RichTextArea. */
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
/** Applies a data-hook HTML attribute that can be used in tests. */
dataHook: PropTypes.string,
/** Adds a string used to match text labels with FormField children items. E.g.
*
* ```js
* <FormField id="myFormField" label="Hello">
* <Input id="myFormField"/>
* </FormField>
* ```
*/
id: PropTypes.string,
/** Displays a passed info message in a tooltip. Default value is a text string, but it can also be overridden with any other component. */
infoContent: PropTypes.node,
/** Allows control over the tooltip style and behaviour by passed tooltip properties. Check <Tooltip/> API for a full list. */
infoTooltipProps: PropTypes.shape(TooltipCommonProps),
/** Sets a field label. It’s default value is a text string, but it can be overridden with any other component. */
label: PropTypes.node,
/** Controls the label alignment */
labelAlignment: PropTypes.oneOf([ALIGN.middle, ALIGN.top]),
/** Controls the label placement */
labelPlacement: PropTypes.oneOf([
PLACEMENT.top,
PLACEMENT.right,
PLACEMENT.left,
]),
/** Controls label size */
labelSize: PropTypes.oneOf(['tiny', 'small', 'medium']),
/** Marks a field as mandatory with an asterisk (*) at the end of a label. */
required: PropTypes.bool,
/** Defines whether or not the content (children container) grows when there's space available. Otherwise, it only uses the necessary space. */
stretchContent: PropTypes.bool,
/** Adds a custom element at the end of the label row (it overrides the charCount in case it's provided). */
suffix: PropTypes.node,
/** Sets the maximum length for the field value. Character count is displayed in the top right corner of a component. */
charCount: PropTypes.number,
/** Sets the id of the label */
labelId: PropTypes.string,
};
static defaultProps = {
required: false,
stretchContent: true,
labelSize: 'medium',
labelPlacement: PLACEMENT.top,
labelAlignment: ALIGN.middle,
};
state = {
lengthLeft: undefined,
};
childrenRenderPropInterface = {
setCharactersLeft: lengthLeft => this.setState({ lengthLeft }),
};
_renderChildren() {
const { children } = this.props;
if (typeof children === 'function') {
return children(this.childrenRenderPropInterface);
}
return children;
}
_hasCharCounter = () =>
this.props.charCount !== undefined ||
typeof this.state.lengthLeft === 'number';
_renderCharCounter = () => {
if (!this._hasCharCounter()) {
return;
}
const { charCount } = this.props;
return charactersLeft(
charCount !== undefined ? charCount : this.state.lengthLeft,
);
};
_renderInfoIcon = ({ labelSize }) => {
const { infoContent, infoTooltipProps } = this.props;
const iconSize = labelSize === 'tiny' ? 'small' : labelSize;
return (
infoContent && (
<InfoIcon
dataHook={dataHooks.infoIcon}
className={classes.infoIcon}
content={infoContent}
tooltipProps={infoTooltipProps}
size={iconSize}
/>
)
);
};
_renderLabelIndicators = ({ labelSize }) => {
const { required, suffix } = this.props;
return (
<div
data-hook={dataHooks.labelIndicators}
className={st(classes.labelIndicators, {
inlineWithSuffix: Boolean(suffix || this._hasCharCounter()),
})}
>
{this._renderLabel({ trimLongText: false, labelSize })}
{required && asterisk}
{this._renderInfoIcon({ labelSize })}
</div>
);
};
_renderSuffix = () => {
const { suffix } = this.props;
return (
(suffix || this._hasCharCounter()) && (
<div data-hook={dataHooks.suffix} className={classes.suffix}>
{suffix ? suffix : this._renderCharCounter()}
</div>
)
);
};
_hasInlineLabel = (label, labelPlacement) =>
label &&
(labelPlacement === PLACEMENT.left || labelPlacement === PLACEMENT.right);
_renderLabel = ({ trimLongText, labelSize }) => {
const { label, id, labelId } = this.props;
const weight = labelSize === 'tiny' ? 'normal' : undefined;
return (
<Text
size={labelSize}
weight={weight}
htmlFor={id}
id={labelId}
tagName={'label'}
dataHook={dataHooks.label}
ellipsis={trimLongText}
style={{ display: 'block' }} // allows the label to middle vertically
secondary
>
{label}
</Text>
);
};
render() {
const {
label,
labelPlacement,
labelAlignment,
required,
infoContent,
dataHook,
children,
classNames,
stretchContent,
} = this.props;
let { labelSize } = this.props;
const rootStyles = label
? {
labelPlacement,
labelAlignment,
stretchContent,
required,
minLabelHeight: !children,
}
: {
stretchContent,
required,
minLabelHeight: !children,
};
return (
<WixStyleReactContext.Consumer>
{({ reducedSpacingAndImprovedLayout }) => {
if (reducedSpacingAndImprovedLayout && labelSize === 'medium') {
labelSize = 'small';
}
return (
<div
data-hook={dataHook}
className={st(
classes.root,
{ ...rootStyles, ...(label ? { labelSize } : {}) },
classNames,
)}
>
{label && labelPlacement === PLACEMENT.top && (
<div className={classes.label}>
{this._renderLabel({ trimLongText: true, labelSize })}
{required && asterisk}
{this._renderInfoIcon({ labelSize })}
{this._renderSuffix()}
</div>
)}
{children && (
<div
data-hook={dataHooks.children}
className={st(classes.children, {
childrenWithInlineLabel:
!label || this._hasInlineLabel(label, labelPlacement),
})}
>
{(!label || labelPlacement !== PLACEMENT.top) &&
this._renderSuffix()}
{this._renderChildren()}
</div>
)}
{!label &&
(required || infoContent) &&
this._renderLabelIndicators({ labelSize })}
{this._hasInlineLabel(label, labelPlacement) &&
this._renderLabelIndicators({ labelSize })}
</div>
);
}}
</WixStyleReactContext.Consumer>
);
}
}
export default FormField;