UNPKG

wix-style-react

Version:
482 lines (400 loc) • 13.5 kB
import React from 'react'; import PropTypes from 'prop-types'; import deprecationLog from '../utils/deprecationLog'; import StatusIndicator from '../StatusIndicator'; import debounce from 'lodash/debounce'; import isNaN from 'lodash/isNaN'; import { st, classes } from './InputArea.st.css'; import { dataAttr, dataHooks } from './constants'; import { filterObject } from '../utils/filterObject'; import { FontUpgradeContext } from '../FontUpgrade/context'; /** * General inputArea container */ class InputArea extends React.PureComponent { _computedStyle = null; state = { focus: false, counter: (this.props.value || this.props.defaultValue || '').length, computedRows: this.props.minRowsAutoGrow, }; // For autoGrow prop min rows is 2 so the textarea does not look like an input static MIN_ROWS = 2; constructor(props) { super(props); if (props.size === 'normal') { deprecationLog( '<InputArea/> - change prop size="normal" to size="medium"', ); } } // For testing purposes only _getDataAttr = () => { const { size, status, disabled, resizable, forceHover, forceFocus, } = this.props; return filterObject( { [dataAttr.SIZE]: size, [dataAttr.STATUS]: !!status && !disabled, [dataAttr.DISABLED]: !!disabled, [dataAttr.RESIZABLE]: !!resizable && !disabled, [dataAttr.HOVER]: !!forceHover, [dataAttr.FOCUS]: !!(forceFocus || this.state.focus), }, (key, value) => !!value, ); }; 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, value, defaultValue, autoGrow, hasCounter, } = this.props; if (autoGrow && prevProps.minRowsAutoGrow !== minRowsAutoGrow) { 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, } = 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 ( <FontUpgradeContext.Consumer> {({ active: isMadefor }) => ( <div data-hook={dataHook} className={st( classes.root, { isMadefor, disabled, size, status, hasFocus: forceFocus || this.state.focus, forceHover, resizable, readOnly, }, className, )} {...this._getDataAttr()} > {/* Input Area */} <div className={classes.inputArea}> {typeof children === 'function' ? ( children({ rows: rowsAttr, ref: ref => (this.textArea = ref), onFocus: this._onFocus, onBlur: this._onBlur, onKeyDown: this._onKeyDown, onInput: onInput, }) ) : ( <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} {...ariaAttribute} readOnly={readOnly} /> )} {/* Counter */} {hasCounter && maxLength && ( <span className={classes.counter} data-hook="counter"> {this.state.counter}/{maxLength} </span> )} </div> {/* Status Indicator */} <div className={classes.status}> {!!status && !disabled && ( <StatusIndicator dataHook={dataHooks.tooltip} status={status} message={statusMessage} tooltipPlacement={tooltipPlacement} /> )} </div> </div> )} </FontUpgradeContext.Consumer> ); } focus = () => { this.textArea && this.textArea.focus(); }; blur = () => { this.textArea && this.textArea.blur(); }; select = () => { this.textArea && this.textArea.select(); }; _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); } }; _onBlur = e => { this.setState({ focus: false }); this.props.onBlur && this.props.onBlur(e); }; _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(); } }; _onChange = e => { this.props.onChange && this.props.onChange(e); }; _onInput = () => { this._calculateComputedRows(); }; _calculateComputedRows = () => { const { minRowsAutoGrow } = this.props; this.setState({ computedRows: 1 }, () => { const rowsCount = this._getRowsCount(); const computedRows = Math.max(minRowsAutoGrow, rowsCount); this.setState({ computedRows, }); }); }; _updateComputedStyle = debounce( () => { this._computedStyle = window.getComputedStyle(this.textArea); }, 500, { leading: true }, ); _getComputedStyle = () => { this._updateComputedStyle(); return this._computedStyle; }; _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() * fontSize; } else { lineHeightValue = lineHeight; } return Math.floor(this.textArea.scrollHeight / lineHeightValue); }; _getDefaultLineHeight = () => { if (!this._defaultLineHeight) { 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, 10) / parseInt(fontSize, 10); tempElement.parentNode.removeChild(tempElement); } return this._defaultLineHeight; }; } InputArea.displayName = 'InputArea'; InputArea.defaultProps = { minRowsAutoGrow: InputArea.MIN_ROWS, size: 'medium', }; InputArea.propTypes = { /** Applies a data-hook HTML attribute that can be used in the tests. */ dataHook: PropTypes.string, /** Specifies a CSS class name to be appended to the component’s root element. */ className: PropTypes.string, /** Specifies custom textarea render function */ children: PropTypes.func, /** Associate a control with the regions that it controls. */ ariaControls: PropTypes.string, /** Associate a region with its descriptions. Similar to aria-controls but instead associating descriptions to the region and description identifiers are separated with a space. */ ariaDescribedby: PropTypes.string, /** Define a string that labels the current element in case where a text label is not visible on the screen. */ ariaLabel: PropTypes.string, /** Focus the element on mount (standard React input autoFocus). */ autoFocus: PropTypes.bool, /** Select the entire text of the element on focus (standard React input autoSelect). */ autoSelect: PropTypes.bool, /** Controls the size of the input. */ size: PropTypes.oneOf(['small', 'medium']), /** Sets a default value for those who want to use this component un-controlled. */ defaultValue: PropTypes.string, /** Specifies whether input should be disabled. */ disabled: PropTypes.bool, /** Specifies the status of a field. */ status: PropTypes.oneOf(['error', 'warning', 'loading']), /** Defines the message to display on status icon hover. If not given or empty there will be no tooltip. */ statusMessage: PropTypes.node, /** USED FOR TESTING. Forces focus state on the input. */ forceFocus: PropTypes.bool, /** USED FOR TESTING. Forces hover state on the input. */ forceHover: PropTypes.bool, /** Specifies whether character count is enabled. */ hasCounter: PropTypes.bool, /** Assigns an unique identifier for the root element. */ id: PropTypes.string, /** Reference element data when a form is submitted. */ name: PropTypes.string, /** Sets the maximum height of an area in pixels. */ maxHeight: PropTypes.string, /** Defines the maximum text length in number of characters. */ maxLength: PropTypes.number, /** Sets the minimum height of an area in pixels. */ minHeight: PropTypes.string, /** Defines a standard input onBlur callback */ onBlur: PropTypes.func, /** Defines a standard input onChange callback. */ onChange: PropTypes.func, /** Defines a callback handler that is called when user presses enter. */ onEnterPressed: PropTypes.func, /** Defines a callback handler that is called when user presses escape. */ onEscapePressed: PropTypes.func, /** Defines a standard input onFocus callback. */ onFocus: PropTypes.func, /** Defines a standard input onKeyDown callback. */ onKeyDown: PropTypes.func, /** Defines a standard input onKeyUp callback. */ onKeyUp: PropTypes.func, /** Sets a placeholder message to display. */ placeholder: PropTypes.string, /** Specifies whether input is read only. */ readOnly: PropTypes.bool, /** Specifies whether area can be manually resized by the user. */ resizable: PropTypes.bool, /** Sets initial height of an area to fit a specified number of rows. */ rows: PropTypes.number, /** Specifies whether area should grow and shrink according to user input. */ autoGrow: PropTypes.bool, /** Sets the minimum amount of rows the component can have in `autoGrow` mode */ minRowsAutoGrow: PropTypes.number, /** Indicates that element can be focused and where it participates in sequential keyboard navigation. */ tabIndex: PropTypes.number, /** Controls placement of a status tooltip. */ tooltipPlacement: PropTypes.string, /** Defines input value. */ value: PropTypes.string, /** Specifies whether the input area is a mandatory field. */ required: PropTypes.bool, }; export default InputArea;