@blueprintjs/core
Version:
Core styles & components
106 lines • 6.04 kB
JavaScript
/* !
* (c) Copyright 2023 Palantir Technologies Inc. All rights reserved.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.useAsyncControllableValue = exports.ASYNC_CONTROLLABLE_VALUE_COMPOSITION_END_DELAY = void 0;
const tslib_1 = require("tslib");
const React = tslib_1.__importStar(require("react"));
/**
* The amount of time (in milliseconds) which the input will wait after a compositionEnd event before
* unlocking its state value for external updates via props. See `handleCompositionEnd` for more details.
*/
exports.ASYNC_CONTROLLABLE_VALUE_COMPOSITION_END_DELAY = 10;
/**
* A hook to workaround the following [React bug](https://github.com/facebook/react/issues/3926).
* This bug is reproduced when an input receives CompositionEvents
* (for example, through IME composition) and has its value prop updated asychronously.
* This might happen if a component chooses to do async validation of a value
* returned by the input's `onChange` callback.
*/
function useAsyncControllableValue(props) {
const { onCompositionStart, onCompositionEnd, value: propValue, onChange } = props;
// The source of truth for the input value. This is not updated during IME composition.
// It may be updated by a parent component.
const [value, setValue] = React.useState(propValue);
// The latest input value, which updates during IME composition.
const [nextValue, setNextValue] = React.useState(propValue);
// Whether we are in the middle of a composition event.
const [isComposing, setIsComposing] = React.useState(false);
// Whether there is a pending update we are expecting from a parent component.
const [hasPendingUpdate, setHasPendingUpdate] = React.useState(false);
const cancelPendingCompositionEnd = React.useRef();
const handleCompositionStart = React.useCallback(event => {
var _a;
(_a = cancelPendingCompositionEnd.current) === null || _a === void 0 ? void 0 : _a.call(cancelPendingCompositionEnd);
setIsComposing(true);
onCompositionStart === null || onCompositionStart === void 0 ? void 0 : onCompositionStart(event);
}, [onCompositionStart]);
// creates a timeout which will set `isComposing` to false after a delay
// returns a function which will cancel the timeout if called before it fires
const createOnCancelPendingCompositionEnd = React.useCallback(() => {
const timeoutId = window.setTimeout(() => setIsComposing(false), exports.ASYNC_CONTROLLABLE_VALUE_COMPOSITION_END_DELAY);
return () => window.clearTimeout(timeoutId);
}, []);
const handleCompositionEnd = React.useCallback(event => {
// In some non-latin languages, a keystroke can end a composition event and immediately afterwards start another.
// This can lead to unexpected characters showing up in the text input. In order to circumvent this problem, we
// use a timeout which creates a delay which merges the two composition events, creating a more natural and predictable UX.
// `this.state.nextValue` will become "locked" (it cannot be overwritten by the `value` prop) until a delay (10ms) has
// passed without a new composition event starting.
cancelPendingCompositionEnd.current = createOnCancelPendingCompositionEnd();
onCompositionEnd === null || onCompositionEnd === void 0 ? void 0 : onCompositionEnd(event);
}, [createOnCancelPendingCompositionEnd, onCompositionEnd]);
const handleChange = React.useCallback(event => {
const { value: targetValue } = event.target;
setNextValue(targetValue);
onChange === null || onChange === void 0 ? void 0 : onChange(event);
}, [onChange]);
// don't derive anything from props if:
// - in uncontrolled mode, OR
// - currently composing, since we'll do that after composition ends
const shouldDeriveFromProps = !(isComposing || propValue === undefined);
if (shouldDeriveFromProps) {
const userTriggeredUpdate = nextValue !== value;
if (userTriggeredUpdate && propValue === nextValue) {
// parent has processed and accepted our update
setValue(propValue);
setHasPendingUpdate(false);
}
else if (userTriggeredUpdate && propValue === value) {
// we have sent the update to our parent, but it has not been processed yet. just wait.
// DO NOT set nextValue here, since that will temporarily render a potentially stale controlled value,
// causing the cursor to jump once the new value is accepted
if (!hasPendingUpdate) {
// make sure to setState only when necessary to avoid infinite loops
setHasPendingUpdate(true);
}
}
else if (userTriggeredUpdate && propValue !== value) {
// accept controlled update overriding user action
setValue(propValue);
setNextValue(propValue);
setHasPendingUpdate(false);
}
else if (!userTriggeredUpdate) {
// accept controlled update, could be confirming or denying user action
if (value !== propValue || hasPendingUpdate) {
// make sure to setState only when necessary to avoid infinite loops
setValue(propValue);
setNextValue(propValue);
setHasPendingUpdate(false);
}
}
}
return {
onChange: handleChange,
onCompositionEnd: handleCompositionEnd,
onCompositionStart: handleCompositionStart,
// render the pending value even if it is not confirmed by a parent's async controlled update
// so that the cursor does not jump to the end of input as reported in
// https://github.com/palantir/blueprint/issues/4298
value: isComposing || hasPendingUpdate ? nextValue : value,
};
}
exports.useAsyncControllableValue = useAsyncControllableValue;
//# sourceMappingURL=useAsyncControllableValue.js.map
;