utils.js 9.26 KB
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.isFocusable = isFocusable;
exports.isClickable = isClickable;
exports.getMouseEventOptions = getMouseEventOptions;
exports.isLabelWithInternallyDisabledControl = isLabelWithInternallyDisabledControl;
exports.getActiveElement = getActiveElement;
exports.calculateNewValue = calculateNewValue;
exports.setSelectionRangeIfNecessary = setSelectionRangeIfNecessary;
exports.eventWrapper = eventWrapper;
exports.isValidDateValue = isValidDateValue;
exports.isValidInputTimeValue = isValidInputTimeValue;
exports.buildTimeValue = buildTimeValue;
exports.getValue = getValue;
exports.getSelectionRange = getSelectionRange;
exports.isContentEditable = isContentEditable;
exports.FOCUSABLE_SELECTOR = void 0;

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

function isMousePressEvent(event) {
  return event === 'mousedown' || event === 'mouseup' || event === 'click' || event === 'dblclick';
}

function invert(map) {
  const res = {};

  for (const key of Object.keys(map)) {
    res[map[key]] = key;
  }

  return res;
} // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons


const BUTTONS_TO_NAMES = {
  0: 'none',
  1: 'primary',
  2: 'secondary',
  4: 'auxiliary'
};
const NAMES_TO_BUTTONS = invert(BUTTONS_TO_NAMES); // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button

const BUTTON_TO_NAMES = {
  0: 'primary',
  1: 'auxiliary',
  2: 'secondary'
};
const NAMES_TO_BUTTON = invert(BUTTON_TO_NAMES);

function convertMouseButtons(event, init, property, mapping) {
  if (!isMousePressEvent(event)) {
    return 0;
  }

  if (init[property] != null) {
    return init[property];
  }

  if (init.buttons != null) {
    // not sure how to test this. Feel free to try and add a test if you want.
    // istanbul ignore next
    return mapping[BUTTONS_TO_NAMES[init.buttons]] || 0;
  }

  if (init.button != null) {
    // not sure how to test this. Feel free to try and add a test if you want.
    // istanbul ignore next
    return mapping[BUTTON_TO_NAMES[init.button]] || 0;
  }

  return property != 'button' && isMousePressEvent(event) ? 1 : 0;
}

function getMouseEventOptions(event, init, clickCount = 0) {
  init = init || {};
  return { ...init,
    // https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail
    detail: event === 'mousedown' || event === 'mouseup' || event === 'click' ? 1 + clickCount : clickCount,
    buttons: convertMouseButtons(event, init, 'buttons', NAMES_TO_BUTTONS),
    button: convertMouseButtons(event, init, 'button', NAMES_TO_BUTTON)
  };
} // Absolutely NO events fire on label elements that contain their control
// if that control is disabled. NUTS!
// no joke. There are NO events for: <label><input disabled /><label>


function isLabelWithInternallyDisabledControl(element) {
  var _element$control;

  return element.tagName === 'LABEL' && ((_element$control = element.control) == null ? void 0 : _element$control.disabled) && element.contains(element.control);
}

function getActiveElement(document) {
  const activeElement = document.activeElement;

  if (activeElement != null && activeElement.shadowRoot) {
    return getActiveElement(activeElement.shadowRoot);
  } else {
    return activeElement;
  }
}

function supportsMaxLength(element) {
  if (element.tagName === 'TEXTAREA') return true;

  if (element.tagName === 'INPUT') {
    const type = element.getAttribute('type'); // Missing value default is "text"

    if (!type) return true; // https://html.spec.whatwg.org/multipage/input.html#concept-input-apply

    if (type.match(/email|password|search|telephone|text|url/)) return true;
  }

  return false;
}

function getSelectionRange(element) {
  if (isContentEditable(element)) {
    const range = element.ownerDocument.getSelection().getRangeAt(0);
    return {
      selectionStart: range.startOffset,
      selectionEnd: range.endOffset
    };
  }

  return {
    selectionStart: element.selectionStart,
    selectionEnd: element.selectionEnd
  };
} //jsdom is not supporting isContentEditable


function isContentEditable(element) {
  return element.hasAttribute('contenteditable') && (element.getAttribute('contenteditable') == 'true' || element.getAttribute('contenteditable') == '');
}

function getValue(element) {
  if (isContentEditable(element)) {
    return element.textContent;
  }

  return element.value;
}

function calculateNewValue(newEntry, element) {
  var _element$getAttribute;

  const {
    selectionStart,
    selectionEnd
  } = getSelectionRange(element);
  const value = getValue(element); // can't use .maxLength property because of a jsdom bug:
  // https://github.com/jsdom/jsdom/issues/2927

  const maxLength = Number((_element$getAttribute = element.getAttribute('maxlength')) != null ? _element$getAttribute : -1);
  let newValue, newSelectionStart;

  if (selectionStart === null) {
    // at the end of an input type that does not support selection ranges
    // https://github.com/testing-library/user-event/issues/316#issuecomment-639744793
    newValue = value + newEntry;
  } else if (selectionStart === selectionEnd) {
    if (selectionStart === 0) {
      // at the beginning of the input
      newValue = newEntry + value;
    } else if (selectionStart === value.length) {
      // at the end of the input
      newValue = value + newEntry;
    } else {
      // in the middle of the input
      newValue = value.slice(0, selectionStart) + newEntry + value.slice(selectionEnd);
    }

    newSelectionStart = selectionStart + newEntry.length;
  } else {
    // we have something selected
    const firstPart = value.slice(0, selectionStart) + newEntry;
    newValue = firstPart + value.slice(selectionEnd);
    newSelectionStart = firstPart.length;
  }

  if (element.type === 'date' && !isValidDateValue(element, newValue)) {
    newValue = value;
  }

  if (element.type === 'time' && !isValidInputTimeValue(element, newValue)) {
    if (isValidInputTimeValue(element, newEntry)) {
      newValue = newEntry;
    } else {
      newValue = value;
    }
  }

  if (!supportsMaxLength(element) || maxLength < 0) {
    return {
      newValue,
      newSelectionStart
    };
  } else {
    return {
      newValue: newValue.slice(0, maxLength),
      newSelectionStart: newSelectionStart > maxLength ? maxLength : newSelectionStart
    };
  }
}

function setSelectionRangeIfNecessary(element, newSelectionStart, newSelectionEnd) {
  const {
    selectionStart,
    selectionEnd
  } = getSelectionRange(element);

  if (!isContentEditable(element) && (!element.setSelectionRange || selectionStart === null)) {
    // cannot set selection
    return;
  }

  if (selectionStart !== newSelectionStart || selectionEnd !== newSelectionStart) {
    if (isContentEditable(element)) {
      const range = element.ownerDocument.createRange();
      range.selectNodeContents(element);
      range.setStart(element.firstChild, newSelectionStart);
      range.setEnd(element.firstChild, newSelectionEnd);
      element.ownerDocument.getSelection().removeAllRanges();
      element.ownerDocument.getSelection().addRange(range);
    } else {
      element.setSelectionRange(newSelectionStart, newSelectionEnd);
    }
  }
}

const FOCUSABLE_SELECTOR = ['input:not([type=hidden]):not([disabled])', 'button:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', '[contenteditable=""]', '[contenteditable="true"]', 'a[href]', '[tabindex]:not([disabled])'].join(', ');
exports.FOCUSABLE_SELECTOR = FOCUSABLE_SELECTOR;

function isFocusable(element) {
  return !isLabelWithInternallyDisabledControl(element) && (element == null ? void 0 : element.matches(FOCUSABLE_SELECTOR));
}

const CLICKABLE_INPUT_TYPES = ['button', 'color', 'file', 'image', 'reset', 'submit'];

function isClickable(element) {
  return element.tagName === 'BUTTON' || element instanceof element.ownerDocument.defaultView.HTMLInputElement && CLICKABLE_INPUT_TYPES.includes(element.type);
}

function eventWrapper(cb) {
  let result;
  (0, _dom.getConfig)().eventWrapper(() => {
    result = cb();
  });
  return result;
}

function isValidDateValue(element, value) {
  if (element.type !== 'date') return false;
  const clone = element.cloneNode();
  clone.value = value;
  return clone.value === value;
}

function buildTimeValue(value) {
  function build(onlyDigitsValue, index) {
    const hours = onlyDigitsValue.slice(0, index);
    const validHours = Math.min(parseInt(hours, 10), 23);
    const minuteCharacters = onlyDigitsValue.slice(index);
    const parsedMinutes = parseInt(minuteCharacters, 10);
    const validMinutes = Math.min(parsedMinutes, 59);
    return `${validHours.toString().padStart(2, '0')}:${validMinutes.toString().padStart(2, '0')}`;
  }

  const onlyDigitsValue = value.replace(/\D/g, '');

  if (onlyDigitsValue.length < 2) {
    return value;
  }

  const firstDigit = parseInt(onlyDigitsValue[0], 10);
  const secondDigit = parseInt(onlyDigitsValue[1], 10);

  if (firstDigit >= 3 || firstDigit === 2 && secondDigit >= 4) {
    let index;

    if (firstDigit >= 3) {
      index = 1;
    } else {
      index = 2;
    }

    return build(onlyDigitsValue, index);
  }

  if (value.length === 2) {
    return value;
  }

  return build(onlyDigitsValue, 2);
}

function isValidInputTimeValue(element, timeValue) {
  if (element.type !== 'time') return false;
  const clone = element.cloneNode();
  clone.value = timeValue;
  return clone.value === timeValue;
}