UNPKG

@fluentui/react

Version:

Reusable React components for building web experiences.

344 lines 16.8 kB
import { __assign, __extends } from "tslib"; import * as React from 'react'; import { Async, getDocument, getNativeProps, initializeComponentRef, inputProperties, isIE11, KeyCodes, } from '../../Utilities'; import { WindowContext } from '@fluentui/react-window-provider'; var SELECTION_FORWARD = 'forward'; var SELECTION_BACKWARD = 'backward'; /** * {@docCategory Autofill} */ export var Autofill = /** @class */ (function (_super) { __extends(Autofill, _super); function Autofill(props) { var _this = _super.call(this, props) || this; _this._inputElement = React.createRef(); _this._autoFillEnabled = true; // Composition events are used when the character/text requires several keystrokes to be completed. // Some examples of this are mobile text input and languages like Japanese or Arabic. // Find out more at https://developer.mozilla.org/en-US/docs/Web/Events/compositionstart _this._onCompositionStart = function (ev) { _this.setState({ isComposing: true }); _this._autoFillEnabled = false; }; // Composition events are used when the character/text requires several keystrokes to be completed. // Some examples of this are mobile text input and languages like Japanese or Arabic. // Find out more at https://developer.mozilla.org/en-US/docs/Web/Events/compositionstart _this._onCompositionUpdate = function () { if (isIE11()) { _this._updateValue(_this._getCurrentInputValue(), true); } }; // Composition events are used when the character/text requires several keystrokes to be completed. // Some examples of this are mobile text input and languages like Japanese or Arabic. // Find out more at https://developer.mozilla.org/en-US/docs/Web/Events/compositionstart _this._onCompositionEnd = function (ev) { var inputValue = _this._getCurrentInputValue(); _this._tryEnableAutofill(inputValue, _this.value, false, true); _this.setState({ isComposing: false }); // Due to timing, this needs to be async, otherwise no text will be selected. _this._async.setTimeout(function () { // it's technically possible that the value of isComposing is reset during this timeout, // so explicitly trigger this with composing=true here, since it is supposed to be the // update for composition end _this._updateValue(_this._getCurrentInputValue(), false); }, 0); }; _this._onClick = function () { if (_this.value && _this.value !== '' && _this._autoFillEnabled) { _this._autoFillEnabled = false; } }; _this._onKeyDown = function (ev) { if (_this.props.onKeyDown) { _this.props.onKeyDown(ev); } // If the event is actively being composed, then don't alert autofill. // Right now typing does not have isComposing, once that has been fixed any should be removed. if (!ev.nativeEvent.isComposing) { // eslint-disable-next-line deprecation/deprecation switch (ev.which) { case KeyCodes.backspace: _this._autoFillEnabled = false; break; case KeyCodes.left: case KeyCodes.right: if (_this._autoFillEnabled) { _this.setState(function (prev) { return ({ inputValue: _this.props.suggestedDisplayValue || prev.inputValue, }); }); _this._autoFillEnabled = false; } break; default: if (!_this._autoFillEnabled) { // eslint-disable-next-line deprecation/deprecation if (_this.props.enableAutofillOnKeyPress.indexOf(ev.which) !== -1) { _this._autoFillEnabled = true; } } break; } } }; _this._onInputChanged = function (ev) { var value = _this._getCurrentInputValue(ev); if (!_this.state.isComposing) { _this._tryEnableAutofill(value, _this.value, ev.nativeEvent.isComposing); } // If it is not IE11 and currently composing, update the value if (!(isIE11() && _this.state.isComposing)) { var nativeEventComposing = ev.nativeEvent.isComposing; var isComposing = nativeEventComposing === undefined ? _this.state.isComposing : nativeEventComposing; _this._updateValue(value, isComposing); } }; _this._onChanged = function () { // Swallow this event, we don't care about it // We must provide it because React PropTypes marks it as required, but onInput serves the correct purpose return; }; /** * Updates the current input value as well as getting a new display value. * @param newValue - The new value from the input */ _this._updateValue = function (newValue, composing) { var _a; // Only proceed if the value is nonempty and is different from the old value // This is to work around the fact that, in IE 11, inputs with a placeholder fire an onInput event on focus if (!newValue && newValue === _this.value) { return; } // eslint-disable-next-line deprecation/deprecation var onInputChange = (_a = _this.props, _a.onInputChange), onInputValueChange = _a.onInputValueChange; if (onInputChange) { newValue = (onInputChange === null || onInputChange === void 0 ? void 0 : onInputChange(newValue, composing)) || ''; } _this.setState({ inputValue: newValue }, function () { return onInputValueChange === null || onInputValueChange === void 0 ? void 0 : onInputValueChange(newValue, composing); }); }; initializeComponentRef(_this); _this._async = new Async(_this); _this.state = { inputValue: props.defaultVisibleValue || '', isComposing: false, }; return _this; } Autofill.getDerivedStateFromProps = function (props, state) { // eslint-disable-next-line deprecation/deprecation if (props.updateValueInWillReceiveProps) { // eslint-disable-next-line deprecation/deprecation var updatedInputValue = props.updateValueInWillReceiveProps(); // Don't update if we have a null value or the value isn't changing // the value should still update if an empty string is passed in if (updatedInputValue !== null && updatedInputValue !== state.inputValue && !state.isComposing) { return __assign(__assign({}, state), { inputValue: updatedInputValue }); } } return null; }; Object.defineProperty(Autofill.prototype, "cursorLocation", { get: function () { if (this._inputElement.current) { var inputElement = this._inputElement.current; if (inputElement.selectionDirection !== SELECTION_FORWARD) { return inputElement.selectionEnd; } else { return inputElement.selectionStart; } } else { return -1; } }, enumerable: false, configurable: true }); Object.defineProperty(Autofill.prototype, "isValueSelected", { get: function () { return Boolean(this.inputElement && this.inputElement.selectionStart !== this.inputElement.selectionEnd); }, enumerable: false, configurable: true }); Object.defineProperty(Autofill.prototype, "value", { get: function () { return this._getControlledValue() || this.state.inputValue || ''; }, enumerable: false, configurable: true }); Object.defineProperty(Autofill.prototype, "selectionStart", { get: function () { return this._inputElement.current ? this._inputElement.current.selectionStart : -1; }, enumerable: false, configurable: true }); Object.defineProperty(Autofill.prototype, "selectionEnd", { get: function () { return this._inputElement.current ? this._inputElement.current.selectionEnd : -1; }, enumerable: false, configurable: true }); Object.defineProperty(Autofill.prototype, "inputElement", { get: function () { return this._inputElement.current; }, enumerable: false, configurable: true }); Autofill.prototype.componentDidUpdate = function (_, _1, cursor) { var _a; var _b; var suggestedDisplayValue = (_a = this.props, _a.suggestedDisplayValue), shouldSelectFullInputValueInComponentDidUpdate = _a.shouldSelectFullInputValueInComponentDidUpdate, preventValueSelection = _a.preventValueSelection; var differenceIndex = 0; if (preventValueSelection) { return; } var document = ((_b = this.context) === null || _b === void 0 ? void 0 : _b.window.document) || getDocument(this._inputElement.current); var isFocused = this._inputElement.current && this._inputElement.current === (document === null || document === void 0 ? void 0 : document.activeElement); if (isFocused && this._autoFillEnabled && this.value && suggestedDisplayValue && _doesTextStartWith(suggestedDisplayValue, this.value)) { var shouldSelectFullRange = false; if (shouldSelectFullInputValueInComponentDidUpdate) { shouldSelectFullRange = shouldSelectFullInputValueInComponentDidUpdate(); } if (shouldSelectFullRange) { this._inputElement.current.setSelectionRange(0, suggestedDisplayValue.length, SELECTION_BACKWARD); } else { while (differenceIndex < this.value.length && this.value[differenceIndex].toLocaleLowerCase() === suggestedDisplayValue[differenceIndex].toLocaleLowerCase()) { differenceIndex++; } if (differenceIndex > 0) { this._inputElement.current.setSelectionRange(differenceIndex, suggestedDisplayValue.length, SELECTION_BACKWARD); } } } else if (this._inputElement.current) { if (cursor !== null && !this._autoFillEnabled && !this.state.isComposing) { this._inputElement.current.setSelectionRange(cursor.start, cursor.end, cursor.dir); } } }; Autofill.prototype.componentWillUnmount = function () { this._async.dispose(); }; Autofill.prototype.render = function () { var nativeProps = getNativeProps(this.props, inputProperties); var style = __assign(__assign({}, this.props.style), { fontFamily: 'inherit' }); return (React.createElement("input", __assign({ autoCapitalize: "off", autoComplete: "off", "aria-autocomplete": 'both' }, nativeProps, { style: style, ref: this._inputElement, value: this._getDisplayValue(), onCompositionStart: this._onCompositionStart, onCompositionUpdate: this._onCompositionUpdate, onCompositionEnd: this._onCompositionEnd, // TODO (Fabric 8?) - switch to calling only onChange. See notes in TextField._onInputChange. onChange: this._onChanged, onInput: this._onInputChanged, onKeyDown: this._onKeyDown, onClick: this.props.onClick ? this.props.onClick : this._onClick, "data-lpignore": true }))); }; Autofill.prototype.focus = function () { this._inputElement.current && this._inputElement.current.focus(); }; Autofill.prototype.clear = function () { this._autoFillEnabled = true; this._updateValue('', false); this._inputElement.current && this._inputElement.current.setSelectionRange(0, 0); }; Autofill.prototype.getSnapshotBeforeUpdate = function () { var _a, _b; var inel = this._inputElement.current; if (inel && inel.selectionStart !== this.value.length) { return { start: (_a = inel.selectionStart) !== null && _a !== void 0 ? _a : inel.value.length, end: (_b = inel.selectionEnd) !== null && _b !== void 0 ? _b : inel.value.length, dir: inel.selectionDirection || 'backward' || 'none', }; } return null; }; Autofill.prototype._getCurrentInputValue = function (ev) { if (ev && ev.target && ev.target.value) { return ev.target.value; } else if (this.inputElement && this.inputElement.value) { return this.inputElement.value; } else { return ''; } }; /** * Attempts to enable autofill. Whether or not autofill is enabled depends on the input value, * whether or not any text is selected, and only if the new input value is longer than the old input value. * Autofill should never be set to true if the value is composing. Once compositionEnd is called, then * it should be completed. * See https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent for more information on composition. * @param newValue - new input value * @param oldValue - old input value * @param isComposing - if true then the text is actively being composed and it has not completed. * @param isComposed - if the text is a composed text value. */ Autofill.prototype._tryEnableAutofill = function (newValue, oldValue, isComposing, isComposed) { if (!isComposing && newValue && this._inputElement.current && this._inputElement.current.selectionStart === newValue.length && !this._autoFillEnabled && (newValue.length > oldValue.length || isComposed)) { this._autoFillEnabled = true; } }; Autofill.prototype._getDisplayValue = function () { if (this._autoFillEnabled) { return _getDisplayValue(this.value, this.props.suggestedDisplayValue); } return this.value; }; Autofill.prototype._getControlledValue = function () { var value = this.props.value; if (value === undefined || typeof value === 'string') { return value; } // eslint-disable-next-line no-console console.warn("props.value of Autofill should be a string, but it is ".concat(value, " with type of ").concat(typeof value)); return value.toString(); }; Autofill.defaultProps = { enableAutofillOnKeyPress: [KeyCodes.down, KeyCodes.up], }; // need to check WindowContext to get the provided document Autofill.contextType = WindowContext; return Autofill; }(React.Component)); /** * Returns a string that should be used as the display value. * It evaluates this based on whether or not the suggested value starts with the input value * and whether or not autofill is enabled. * @param inputValue - the value that the input currently has. * @param suggestedDisplayValue - the possible full value */ function _getDisplayValue(inputValue, suggestedDisplayValue) { var displayValue = inputValue; if (suggestedDisplayValue && inputValue && _doesTextStartWith(suggestedDisplayValue, displayValue)) { displayValue = suggestedDisplayValue; } return displayValue; } function _doesTextStartWith(text, startWith) { if (!text || !startWith) { return false; } if (process.env.NODE_ENV !== 'production') { for (var _i = 0, _a = [text, startWith]; _i < _a.length; _i++) { var val = _a[_i]; if (typeof val !== 'string') { throw new Error("".concat(Autofill.name // eslint-disable-next-line @fluentui/max-len , " received non-string value \"").concat(val, "\" of type ").concat(typeof val, " from either input's value or suggestedDisplayValue")); } } } return text.toLocaleLowerCase().indexOf(startWith.toLocaleLowerCase()) === 0; } //# sourceMappingURL=Autofill.js.map