@wix/design-system
Version:
@wix/design-system
267 lines • 12.3 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import StatusIndicator from '../StatusIndicator';
import { STATUS } from '../StatusIndicator/StatusIndicator.constants';
import clamp from 'lodash/clamp';
import debounce from 'lodash/debounce';
import isNaN from 'lodash/isNaN';
import { st, classes } from './InputArea.st.css.js';
import { dataAttr, dataHooks } from './constants';
import { filterObject } from '../utils/filterObject';
import { StatusContext, getStatusFromContext, getAriaAttributesFromContext, } from '../FormField/StatusContext';
import semanticClassNames from './InputArea.semanticClassNames';
/**
* General inputArea container
*/
class InputArea extends React.PureComponent {
constructor() {
super(...arguments);
this.textArea = null;
this._computedStyle = null;
this.state = {
focus: false,
counter: (this.props.value || this.props.defaultValue || '').length,
computedRows: this.props.minRowsAutoGrow,
};
// For testing purposes only
this._getDataAttr = ({ statusContext }) => {
const { size, status, disabled, resizable, forceHover, forceFocus } = this.props;
return filterObject({
[dataAttr.SIZE]: size,
[dataAttr.STATUS]: !disabled
? getStatusFromContext(statusContext, status)
: null,
[dataAttr.DISABLED]: !!disabled,
[dataAttr.RESIZABLE]: !!resizable && !disabled,
[dataAttr.HOVER]: !!forceHover,
[dataAttr.FOCUS]: !!(forceFocus || this.state.focus),
}, (_, value) => !!value);
};
this.focus = () => {
this.textArea?.focus();
};
this.blur = () => {
this.textArea?.blur();
};
this.select = () => {
this.textArea?.select();
};
this.calculateComputedRows = () => {
const { minRowsAutoGrow, maxRowsAutoGrow } = this.props;
this.setState({ computedRows: 1 }, () => {
const rowsCount = this._getRowsCount();
const computedRows = clamp(rowsCount, minRowsAutoGrow || InputArea.MIN_ROWS, maxRowsAutoGrow || InputArea.MAX_ROWS);
this.setState({
computedRows,
});
});
};
this._onFocus = (e) => {
this.setState({ focus: true });
this.props.onFocus && this.props.onFocus(e);
if (this.props.autoSelect) {
// Set timeout is needed here since onFocus is called before react
// gets the reference for the input (specifically when autoFocus
// is on. So setTimeout ensures we have the ref.input needed in select)
setTimeout(() => this.select(), 0);
}
};
this._onBlur = (e) => {
this.setState({ focus: false });
this.props.onBlur && this.props.onBlur(e);
};
this._onKeyDown = (e) => {
this.props.onKeyDown && this.props.onKeyDown(e);
if (e.key === 'Enter') {
this.props.onEnterPressed && this.props.onEnterPressed(e);
}
else if (e.key === 'Escape') {
this.props.onEscapePressed && this.props.onEscapePressed();
}
};
this._onChange = (e) => {
this.props.onChange && this.props.onChange(e);
};
this._onInput = () => {
this.calculateComputedRows();
};
this._updateComputedStyle = debounce(() => {
if (this.textArea) {
this._computedStyle = window.getComputedStyle(this.textArea);
}
}, 500, { leading: true });
this._getComputedStyle = () => {
this._updateComputedStyle();
return this._computedStyle;
};
this._getRowsCount = () => {
const computedStyle = this._getComputedStyle();
const fontSize = parseInt(computedStyle?.getPropertyValue('font-size') ?? '', 10);
const lineHeight = parseInt(computedStyle?.getPropertyValue('line-height') ?? '', 10);
let lineHeightValue;
if (isNaN(lineHeight)) {
if (isNaN(fontSize)) {
return InputArea.MIN_ROWS;
}
lineHeightValue = this._getDefaultLineHeight() ?? 0 * fontSize;
}
else {
lineHeightValue = lineHeight;
}
return Math.floor((this.textArea?.scrollHeight ?? 0) / lineHeightValue);
};
this._getDefaultLineHeight = () => {
if (!this._defaultLineHeight && this.textArea) {
const { parentNode } = this.textArea;
const computedStyles = this._getComputedStyle();
const fontFamily = computedStyles?.getPropertyValue('font-family');
const fontSize = computedStyles?.getPropertyValue('font-size');
const tempElement = document.createElement('span');
const defaultStyles = 'position:absolute;display:inline;border:0;margin:0;padding:0;line-height:normal;';
tempElement.setAttribute('style', `${defaultStyles}font-family:${fontFamily};font-size:${fontSize};`);
tempElement.innerText = 'M';
parentNode?.appendChild(tempElement);
this._defaultLineHeight =
parseInt(tempElement.clientHeight.toString(), 10) /
parseInt(fontSize ?? '0', 10);
tempElement.parentNode?.removeChild(tempElement);
}
return this._defaultLineHeight;
};
}
componentDidMount() {
const { autoFocus, autoGrow, value } = this.props;
autoFocus && this._onFocus();
if (autoGrow) {
this.calculateComputedRows();
}
/*
* autoFocus doesn't automatically selects text like focus do.
* Therefore we set the selection range, but in order to support prior implementation we set the start position as the end in order to place the cursor there.
*/
if (autoFocus && !!value) {
this.textArea?.setSelectionRange(value.length, value.length);
}
}
componentDidUpdate(prevProps) {
const { minRowsAutoGrow, maxRowsAutoGrow, value, defaultValue, autoGrow, hasCounter, } = this.props;
if (autoGrow &&
(prevProps.minRowsAutoGrow !== minRowsAutoGrow ||
prevProps.maxRowsAutoGrow !== maxRowsAutoGrow)) {
this.calculateComputedRows();
}
if (hasCounter && prevProps.value !== value) {
this.setState({
counter: (value || defaultValue || '').length,
});
}
}
componentWillUnmount() {
this._updateComputedStyle.cancel();
}
render() {
const { dataHook, className, autoFocus, defaultValue, disabled, forceFocus, forceHover, id, name, onKeyUp, placeholder, readOnly, tabIndex, rows, autoGrow, value, required, minHeight, maxHeight, maxLength, resizable, hasCounter, size, tooltipPlacement, status, statusMessage, children, onCompositionStart, onCompositionEnd, dir, } = this.props;
const inlineStyle = {};
const rowsAttr = rows
? rows
: autoGrow
? this.state.computedRows
: undefined;
const onInput = !rows && autoGrow ? this._onInput : undefined;
if (minHeight) {
inlineStyle.minHeight = minHeight;
}
if (maxHeight) {
inlineStyle.maxHeight = maxHeight;
}
const ariaAttribute = {};
Object.keys(this.props)
.filter(key => key.startsWith('aria'))
.map(key => (ariaAttribute['aria-' + key.substr(4).toLowerCase()] = this.props[key]));
return (React.createElement(StatusContext.Consumer, null, statusContext => {
const finalStatus = getStatusFromContext(statusContext, status);
return (React.createElement("div", { "data-hook": dataHook, className: st(classes.root, {
disabled,
size,
status: finalStatus,
hasFocus: forceFocus || this.state.focus,
forceHover,
resizable,
readOnly,
}, className), ...this._getDataAttr({ statusContext }) },
React.createElement("div", { className: st(classes.inputArea, semanticClassNames.inputContainer) },
typeof children === 'function' ? (children({
className: '',
rows: rowsAttr || 0,
ref: ref => (this.textArea = ref),
onFocus: (e) => this._onFocus(e),
onBlur: (e) => this._onBlur(e),
onKeyDown: (e) => this._onKeyDown(e),
onInput,
dir,
})) : (React.createElement("textarea", { rows: rowsAttr, maxLength: maxLength, ref: ref => (this.textArea = ref), id: id, name: name, style: inlineStyle, defaultValue: defaultValue, disabled: disabled, value: value, required: required, onFocus: this._onFocus, onBlur: this._onBlur, onKeyDown: this._onKeyDown, onChange: this._onChange, onInput: onInput, placeholder: placeholder, tabIndex: tabIndex, autoFocus: autoFocus, onKeyUp: onKeyUp, ...getAriaAttributesFromContext(statusContext), ...ariaAttribute, readOnly: readOnly, onCompositionStart: onCompositionStart, onCompositionEnd: onCompositionEnd, dir: dir })),
hasCounter && maxLength && (React.createElement("span", { className: classes.counter, "data-hook": "counter" },
this.state.counter,
"/",
maxLength))),
React.createElement("div", { className: classes.status }, (!!status || finalStatus === STATUS.LOADING) && !disabled && (React.createElement(StatusIndicator, { dataHook: dataHooks.tooltip, status: finalStatus ?? undefined, message: statusMessage, tooltipProps: {
placement: tooltipPlacement,
} })))));
}));
}
}
// For autoGrow prop min rows is 2 so the textarea does not look like an input
InputArea.MIN_ROWS = 2;
// For autoGrow prop max rows is Infinity to keep backwards compatibility
InputArea.MAX_ROWS = Number.POSITIVE_INFINITY;
InputArea.displayName = 'InputArea';
InputArea.defaultProps = {
minRowsAutoGrow: InputArea.MIN_ROWS,
maxRowsAutoGrow: InputArea.MAX_ROWS,
size: 'medium',
};
InputArea.propTypes = {
dataHook: PropTypes.string,
className: PropTypes.string,
children: PropTypes.func,
ariaControls: PropTypes.string,
ariaDescribedby: PropTypes.string,
ariaLabel: PropTypes.string,
autoFocus: PropTypes.bool,
size: PropTypes.oneOf(['small', 'medium']),
defaultValue: PropTypes.string,
disabled: PropTypes.bool,
status: PropTypes.oneOf(['error', 'warning', 'loading']),
statusMessage: PropTypes.node,
forceFocus: PropTypes.bool,
forceHover: PropTypes.bool,
hasCounter: PropTypes.bool,
id: PropTypes.string,
name: PropTypes.string,
maxHeight: PropTypes.string,
maxLength: PropTypes.number,
minHeight: PropTypes.string,
onBlur: PropTypes.func,
onChange: PropTypes.func,
onEnterPressed: PropTypes.func,
onEscapePressed: PropTypes.func,
onFocus: PropTypes.func,
onKeyDown: PropTypes.func,
onKeyUp: PropTypes.func,
onCompositionStart: PropTypes.func,
onCompositionEnd: PropTypes.func,
placeholder: PropTypes.string,
readOnly: PropTypes.bool,
resizable: PropTypes.bool,
rows: PropTypes.number,
autoGrow: PropTypes.bool,
minRowsAutoGrow: PropTypes.number,
maxRowsAutoGrow: PropTypes.number,
tabIndex: PropTypes.number,
tooltipPlacement: PropTypes.string,
value: PropTypes.string,
required: PropTypes.bool,
dir: PropTypes.oneOf(['ltr', 'rtl', 'auto']),
};
export default InputArea;
//# sourceMappingURL=InputArea.js.map