UNPKG

@wix/design-system

Version:

@wix/design-system

267 lines 12.3 kB
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