1 | ;
|
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 | */
|
17 | Object.defineProperty(exports, "__esModule", { value: true });
|
18 | exports.AsyncControllableInput = void 0;
|
19 | var tslib_1 = require("tslib");
|
20 | var React = tslib_1.__importStar(require("react"));
|
21 | var 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 | */
|
31 | var 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));
|
117 | exports.AsyncControllableInput = AsyncControllableInput;
|
118 | //# sourceMappingURL=asyncControllableInput.js.map |
\ | No newline at end of file |