UNPKG

6.63 kBJavaScriptView Raw
1"use strict";
2/* !
3 * Copyright 2020 Palantir Technologies, Inc. All rights reserved.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17Object.defineProperty(exports, "__esModule", { value: true });
18exports.AsyncControllableInput = void 0;
19var tslib_1 = require("tslib");
20var React = tslib_1.__importStar(require("react"));
21var common_1 = require("../../common");
22/**
23 * A stateful wrapper around the low-level <input> component which works around a
24 * [React bug](https://github.com/facebook/react/issues/3926). This bug is reproduced when an input
25 * receives CompositionEvents (for example, through IME composition) and has its value prop updated
26 * asychronously. This might happen if a component chooses to do async validation of a value
27 * returned by the input's `onChange` callback.
28 *
29 * Note: this component does not apply any Blueprint-specific styling.
30 */
31var AsyncControllableInput = /** @class */ (function (_super) {
32 tslib_1.__extends(AsyncControllableInput, _super);
33 function AsyncControllableInput() {
34 var _this = _super !== null && _super.apply(this, arguments) || this;
35 _this.state = {
36 hasPendingUpdate: false,
37 isComposing: false,
38 nextValue: _this.props.value,
39 value: _this.props.value,
40 };
41 _this.cancelPendingCompositionEnd = null;
42 _this.handleCompositionStart = function (e) {
43 var _a, _b, _c;
44 (_a = _this.cancelPendingCompositionEnd) === null || _a === void 0 ? void 0 : _a.call(_this);
45 _this.setState({ isComposing: true });
46 (_c = (_b = _this.props).onCompositionStart) === null || _c === void 0 ? void 0 : _c.call(_b, e);
47 };
48 _this.handleCompositionEnd = function (e) {
49 var _a, _b;
50 // In some non-latin languages, a keystroke can end a composition event and immediately afterwards start another.
51 // This can lead to unexpected characters showing up in the text input. In order to circumvent this problem, we
52 // use a timeout which creates a delay which merges the two composition events, creating a more natural and predictable UX.
53 // `this.state.nextValue` will become "locked" (it cannot be overwritten by the `value` prop) until a delay (10ms) has
54 // passed without a new composition event starting.
55 _this.cancelPendingCompositionEnd = _this.setTimeout(function () { return _this.setState({ isComposing: false }); }, AsyncControllableInput.COMPOSITION_END_DELAY);
56 (_b = (_a = _this.props).onCompositionEnd) === null || _b === void 0 ? void 0 : _b.call(_a, e);
57 };
58 _this.handleChange = function (e) {
59 var _a, _b;
60 var value = e.target.value;
61 _this.setState({ nextValue: value });
62 (_b = (_a = _this.props).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, e);
63 };
64 return _this;
65 }
66 AsyncControllableInput.getDerivedStateFromProps = function (nextProps, nextState) {
67 if (nextState.isComposing || nextProps.value === undefined) {
68 // don't derive anything from props if:
69 // - in uncontrolled mode, OR
70 // - currently composing, since we'll do that after composition ends
71 return null;
72 }
73 var userTriggeredUpdate = nextState.nextValue !== nextState.value;
74 if (userTriggeredUpdate) {
75 if (nextProps.value === nextState.nextValue) {
76 // parent has processed and accepted our update
77 if (nextState.hasPendingUpdate) {
78 return { value: nextProps.value, hasPendingUpdate: false };
79 }
80 else {
81 return { value: nextState.nextValue };
82 }
83 }
84 else {
85 if (nextProps.value === nextState.value) {
86 // we have sent the update to our parent, but it has not been processed yet. just wait.
87 // DO NOT set nextValue here, since that will temporarily render a potentially stale controlled value,
88 // causing the cursor to jump once the new value is accepted
89 return { hasPendingUpdate: true };
90 }
91 // accept controlled update overriding user action
92 return { value: nextProps.value, nextValue: nextProps.value, hasPendingUpdate: false };
93 }
94 }
95 else {
96 // accept controlled update, could be confirming or denying user action
97 return { value: nextProps.value, nextValue: nextProps.value, hasPendingUpdate: false };
98 }
99 };
100 AsyncControllableInput.prototype.render = function () {
101 var _a = this.state, isComposing = _a.isComposing, hasPendingUpdate = _a.hasPendingUpdate, value = _a.value, nextValue = _a.nextValue;
102 var _b = this.props, inputRef = _b.inputRef, restProps = tslib_1.__rest(_b, ["inputRef"]);
103 return (React.createElement("input", tslib_1.__assign({}, restProps, { ref: inputRef,
104 // render the pending value even if it is not confirmed by a parent's async controlled update
105 // so that the cursor does not jump to the end of input as reported in
106 // https://github.com/palantir/blueprint/issues/4298
107 value: isComposing || hasPendingUpdate ? nextValue : value, onCompositionStart: this.handleCompositionStart, onCompositionEnd: this.handleCompositionEnd, onChange: this.handleChange })));
108 };
109 AsyncControllableInput.displayName = "".concat(common_1.DISPLAYNAME_PREFIX, ".AsyncControllableInput");
110 /**
111 * The amount of time (in milliseconds) which the input will wait after a compositionEnd event before
112 * unlocking its state value for external updates via props. See `handleCompositionEnd` for more details.
113 */
114 AsyncControllableInput.COMPOSITION_END_DELAY = 10;
115 return AsyncControllableInput;
116}(common_1.AbstractPureComponent2));
117exports.AsyncControllableInput = AsyncControllableInput;
118//# sourceMappingURL=asyncControllableInput.js.map
\No newline at end of file