/** * TextInput.tsx * * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT license. * * Web-specific implementation of the cross-platform TextInput abstraction. */ import * as PropTypes from 'prop-types'; import * as React from 'react'; import { FocusArbitratorProvider } from '../common/utils/AutoFocusHelper'; import { Types } from '../common/Interfaces'; import { applyFocusableComponentMixin } from './utils/FocusManager'; import { isEmpty } from './utils/lodashMini'; import Styles from './Styles'; export interface TextInputState { inputValue?: string; autoResize?: boolean; } const _isMac = (typeof navigator !== 'undefined') && (typeof navigator.platform === 'string') && (navigator.platform.indexOf('Mac') >= 0); // Cast to any to allow merging of web and RX styles const _styles = { defaultStyle: { position: 'relative', display: 'flex', flexDirection: 'row', flexBasis: 'auto', flexGrow: 0, flexShrink: 0, overflowX: 'hidden', overflowY: 'auto', alignItems: 'stretch' } as any, formStyle: { display: 'flex', flex: 1 } as any }; export interface TextInputContext { focusArbitrator?: FocusArbitratorProvider; } interface TextInputPlaceholderCacheItem { refCounter: number; styleElement: HTMLStyleElement; } class TextInputPlaceholderSupport { private static _cachedStyles: { [color: string]: TextInputPlaceholderCacheItem } = {}; static getClassName(color: string): string { const key = this._colorKey(color); return `reactxp-placeholder-${key}`; } static addRef(color: string) { if (typeof document === undefined) { return; } const cache = this._cachedStyles; const key = this._colorKey(color); if (cache.hasOwnProperty(key)) { cache[key].refCounter++; } else { const className = this.getClassName(color); const style = document.createElement('style'); style.type = 'text/css'; style.textContent = this._getStyle(className, color); document.head.appendChild(style); cache[key] = { refCounter: 1, styleElement: style }; } } static removeRef(color: string) { const cache = this._cachedStyles; const key = this._colorKey(color); if (cache.hasOwnProperty(key)) { const item = cache[key]; if (--item.refCounter < 1) { const styleElement = item.styleElement; if (styleElement.parentNode) { styleElement.parentNode.removeChild(styleElement); } delete cache[key]; } } } private static _colorKey(color: string): string { return color.toLowerCase() .replace(/(,|\.|#)/g, '_') .replace(/[^a-z0-9_]/g, ''); } private static _getStyle(className: string, placeholderColor: string): string { const selectors = [ '::placeholder', // Modern browsers '::-webkit-input-placeholder', // Webkit '::-moz-placeholder', // Firefox 19+ ':-moz-placeholder', // Firefox 18- ':-ms-input-placeholder' // IE 10+ ]; return selectors .map(pseudoSelector => `.${className}${pseudoSelector} {\n` + ` opacity: 1;\n` + ` color: ${placeholderColor};\n` + `}` ).join('\n'); } } export class TextInput extends React.Component { static contextTypes: React.ValidationMap = { focusArbitrator: PropTypes.object }; context!: TextInputContext; private _mountedComponent: HTMLInputElement | HTMLTextAreaElement | null = null; private _selectionStart = 0; private _selectionEnd = 0; private _isFocused = false; private _ariaLiveEnabled = false; constructor(props: Types.TextInputProps, context?: TextInputContext) { super(props, context); this.state = { inputValue: props.value !== undefined ? props.value : (props.defaultValue || ''), autoResize: TextInput._shouldAutoResize(props) }; } UNSAFE_componentWillReceiveProps(nextProps: Types.TextInputProps) { const nextState: Partial = {}; if (nextProps.value !== undefined && nextProps.value !== this.state.inputValue) { nextState.inputValue = nextProps.value; } if (nextProps.style !== this.props.style || nextProps.multiline !== this.props.multiline) { const fixedHeight = TextInput._shouldAutoResize(nextProps); if (this.state.autoResize !== fixedHeight) { nextState.autoResize = fixedHeight; } } if (nextProps.placeholderTextColor !== this.props.placeholderTextColor) { if (nextProps.placeholderTextColor) { TextInputPlaceholderSupport.addRef(nextProps.placeholderTextColor); } if (this.props.placeholderTextColor) { TextInputPlaceholderSupport.removeRef(this.props.placeholderTextColor); } } if (!isEmpty(nextState)) { this.setState(nextState, () => { // Resize as needed after state is set if (this._mountedComponent instanceof HTMLTextAreaElement) { TextInput._updateScrollPositions(this._mountedComponent, !!this.state.autoResize); } }); } } componentDidMount() { if (this.props.placeholderTextColor) { TextInputPlaceholderSupport.addRef(this.props.placeholderTextColor); } if (this.props.autoFocus) { this.requestFocus(); } } componentWillUnmount() { if (this.props.placeholderTextColor) { TextInputPlaceholderSupport.removeRef(this.props.placeholderTextColor); } } render() { const combinedStyles = Styles.combine([_styles.defaultStyle, this.props.style]); // Always hide the outline. combinedStyles.outline = 'none'; combinedStyles.resize = 'none'; // Set the border to zero width if not otherwise specified. if (combinedStyles.borderWidth === undefined) { combinedStyles.borderWidth = 0; } // By default, the control is editable. const editable = (this.props.editable !== undefined ? this.props.editable : true); const spellCheck = (this.props.spellCheck !== undefined ? this.props.spellCheck : this.props.autoCorrect); const className = this.props.placeholderTextColor !== undefined ? TextInputPlaceholderSupport.getClassName(this.props.placeholderTextColor) : undefined; // Use a textarea for multi-line and a regular input for single-line. if (this.props.multiline) { return (