UNPKG

5.91 kBJavaScriptView Raw
1/* !
2 * (c) Copyright 2023 Palantir Technologies Inc. All rights reserved.
3 */
4import * as React from "react";
5/**
6 * The amount of time (in milliseconds) which the input will wait after a compositionEnd event before
7 * unlocking its state value for external updates via props. See `handleCompositionEnd` for more details.
8 */
9export var ASYNC_CONTROLLABLE_VALUE_COMPOSITION_END_DELAY = 10;
10/*
11 * A hook to workaround the following [React bug](https://github.com/facebook/react/issues/3926).
12 * This bug is reproduced when an input receives CompositionEvents
13 * (for example, through IME composition) and has its value prop updated asychronously.
14 * This might happen if a component chooses to do async validation of a value
15 * returned by the input's `onChange` callback.
16 */
17export function useAsyncControllableValue(props) {
18 var onCompositionStart = props.onCompositionStart, onCompositionEnd = props.onCompositionEnd, propValue = props.value, onChange = props.onChange;
19 // The source of truth for the input value. This is not updated during IME composition.
20 // It may be updated by a parent component.
21 var _a = React.useState(propValue), value = _a[0], setValue = _a[1];
22 // The latest input value, which updates during IME composition.
23 var _b = React.useState(propValue), nextValue = _b[0], setNextValue = _b[1];
24 // Whether we are in the middle of a composition event.
25 var _c = React.useState(false), isComposing = _c[0], setIsComposing = _c[1];
26 // Whether there is a pending update we are expecting from a parent component.
27 var _d = React.useState(false), hasPendingUpdate = _d[0], setHasPendingUpdate = _d[1];
28 var cancelPendingCompositionEnd = React.useRef();
29 var handleCompositionStart = React.useCallback(function (event) {
30 var _a;
31 (_a = cancelPendingCompositionEnd.current) === null || _a === void 0 ? void 0 : _a.call(cancelPendingCompositionEnd);
32 setIsComposing(true);
33 onCompositionStart === null || onCompositionStart === void 0 ? void 0 : onCompositionStart(event);
34 }, [onCompositionStart]);
35 // creates a timeout which will set `isComposing` to false after a delay
36 // returns a function which will cancel the timeout if called before it fires
37 var createOnCancelPendingCompositionEnd = React.useCallback(function () {
38 var timeoutId = window.setTimeout(function () { return setIsComposing(false); }, ASYNC_CONTROLLABLE_VALUE_COMPOSITION_END_DELAY);
39 return function () { return window.clearTimeout(timeoutId); };
40 }, []);
41 var handleCompositionEnd = React.useCallback(function (event) {
42 // In some non-latin languages, a keystroke can end a composition event and immediately afterwards start another.
43 // This can lead to unexpected characters showing up in the text input. In order to circumvent this problem, we
44 // use a timeout which creates a delay which merges the two composition events, creating a more natural and predictable UX.
45 // `this.state.nextValue` will become "locked" (it cannot be overwritten by the `value` prop) until a delay (10ms) has
46 // passed without a new composition event starting.
47 cancelPendingCompositionEnd.current = createOnCancelPendingCompositionEnd();
48 onCompositionEnd === null || onCompositionEnd === void 0 ? void 0 : onCompositionEnd(event);
49 }, [createOnCancelPendingCompositionEnd, onCompositionEnd]);
50 var handleChange = React.useCallback(function (event) {
51 var targetValue = event.target.value;
52 setNextValue(targetValue);
53 onChange === null || onChange === void 0 ? void 0 : onChange(event);
54 }, [onChange]);
55 // don't derive anything from props if:
56 // - in uncontrolled mode, OR
57 // - currently composing, since we'll do that after composition ends
58 var shouldDeriveFromProps = !(isComposing || propValue === undefined);
59 if (shouldDeriveFromProps) {
60 var userTriggeredUpdate = nextValue !== value;
61 if (userTriggeredUpdate && propValue === nextValue) {
62 // parent has processed and accepted our update
63 setValue(propValue);
64 setHasPendingUpdate(false);
65 }
66 else if (userTriggeredUpdate && propValue === value) {
67 // we have sent the update to our parent, but it has not been processed yet. just wait.
68 // DO NOT set nextValue here, since that will temporarily render a potentially stale controlled value,
69 // causing the cursor to jump once the new value is accepted
70 if (!hasPendingUpdate) {
71 // make sure to setState only when necessary to avoid infinite loops
72 setHasPendingUpdate(true);
73 }
74 }
75 else if (userTriggeredUpdate && propValue !== value) {
76 // accept controlled update overriding user action
77 setValue(propValue);
78 setNextValue(propValue);
79 setHasPendingUpdate(false);
80 }
81 else if (!userTriggeredUpdate) {
82 // accept controlled update, could be confirming or denying user action
83 if (value !== propValue || hasPendingUpdate) {
84 // make sure to setState only when necessary to avoid infinite loops
85 setValue(propValue);
86 setNextValue(propValue);
87 setHasPendingUpdate(false);
88 }
89 }
90 }
91 return {
92 onChange: handleChange,
93 onCompositionEnd: handleCompositionEnd,
94 onCompositionStart: handleCompositionStart,
95 // render the pending value even if it is not confirmed by a parent's async controlled update
96 // so that the cursor does not jump to the end of input as reported in
97 // https://github.com/palantir/blueprint/issues/4298
98 value: isComposing || hasPendingUpdate ? nextValue : value,
99 };
100}
101//# sourceMappingURL=useAsyncControllableValue.js.map
\No newline at end of file