fireInputEvent.js 3.94 KB
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.fireInputEvent = fireInputEvent;

var _dom = require("@testing-library/dom");

var _utils = require("../../utils");

function fireInputEvent(element, {
  newValue,
  newSelectionStart,
  eventOverrides
}) {
  // apply the changes before firing the input event, so that input handlers can access the altered dom and selection
  if ((0, _utils.isContentEditable)(element)) {
    applyNative(element, 'textContent', newValue);
  } else
    /* istanbul ignore else */
    if ((0, _utils.isElementType)(element, ['input', 'textarea'])) {
      applyNative(element, 'value', newValue);
    } else {
      // TODO: properly type guard
      throw new Error('Invalid Element');
    }

  setSelectionRangeAfterInput(element, newSelectionStart);

  _dom.fireEvent.input(element, { ...eventOverrides
  });

  setSelectionRangeAfterInputHandler(element, newValue, newSelectionStart);
}

function setSelectionRangeAfterInput(element, newSelectionStart) {
  (0, _utils.setSelectionRange)(element, newSelectionStart, newSelectionStart);
}

function setSelectionRangeAfterInputHandler(element, newValue, newSelectionStart) {
  const value = (0, _utils.getValue)(element); // don't apply this workaround on elements that don't necessarily report the visible value - e.g. number
  // TODO: this could probably be only applied when there is keyboardState.carryValue

  const isUnreliableValue = value === '' && (0, _utils.hasUnreliableEmptyValue)(element);

  if (!isUnreliableValue && value === newValue) {
    const {
      selectionStart
    } = (0, _utils.getSelectionRange)(element);

    if (selectionStart === value.length) {
      // The value was changed as expected, but the cursor was moved to the end
      // TODO: this could probably be only applied when we work around a framework setter on the element in applyNative
      (0, _utils.setSelectionRange)(element, newSelectionStart, newSelectionStart);
    }
  }
}

const initial = Symbol('initial input value/textContent');
const onBlur = Symbol('onBlur');

/**
 * React tracks the changes on element properties.
 * This workaround tries to alter the DOM element without React noticing,
 * so that it later picks up the change.
 *
 * @see https://github.com/facebook/react/blob/148f8e497c7d37a3c7ab99f01dec2692427272b1/packages/react-dom/src/client/inputValueTracking.js#L51-L104
 */
function applyNative(element, propName, propValue) {
  const descriptor = Object.getOwnPropertyDescriptor(element, propName);
  const nativeDescriptor = Object.getOwnPropertyDescriptor(element.constructor.prototype, propName);

  if (descriptor && nativeDescriptor) {
    Object.defineProperty(element, propName, nativeDescriptor);
  } // Keep track of the initial value to determine if a change event should be dispatched.
  // CONSTRAINT: We can not determine what happened between focus event and our first API call.


  if (element[initial] === undefined) {
    element[initial] = String(element[propName]);
  }

  element[propName] = propValue; // Add an event listener for the blur event to the capture phase on the window.
  // CONSTRAINT: Currently there is no cross-platform solution to unshift the event handler stack.
  // Our change event might occur after other event handlers on the blur event have been processed.

  if (!element[onBlur]) {
    var _element$ownerDocumen;

    (_element$ownerDocumen = element.ownerDocument.defaultView) == null ? void 0 : _element$ownerDocumen.addEventListener('blur', element[onBlur] = () => {
      const initV = element[initial]; // eslint-disable-next-line @typescript-eslint/no-dynamic-delete

      delete element[onBlur]; // eslint-disable-next-line @typescript-eslint/no-dynamic-delete

      delete element[initial];

      if (String(element[propName]) !== initV) {
        _dom.fireEvent.change(element);
      }
    }, {
      capture: true,
      once: true
    });
  }

  if (descriptor) {
    Object.defineProperty(element, propName, descriptor);
  }
}