utils.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.isFocusable = isFocusable;
  6. exports.isClickableInput = isClickableInput;
  7. exports.getMouseEventOptions = getMouseEventOptions;
  8. exports.isLabelWithInternallyDisabledControl = isLabelWithInternallyDisabledControl;
  9. exports.getActiveElement = getActiveElement;
  10. exports.calculateNewValue = calculateNewValue;
  11. exports.setSelectionRangeIfNecessary = setSelectionRangeIfNecessary;
  12. exports.eventWrapper = eventWrapper;
  13. exports.isValidDateValue = isValidDateValue;
  14. exports.isValidInputTimeValue = isValidInputTimeValue;
  15. exports.buildTimeValue = buildTimeValue;
  16. exports.getValue = getValue;
  17. exports.getSelectionRange = getSelectionRange;
  18. exports.isContentEditable = isContentEditable;
  19. exports.isInstanceOfElement = isInstanceOfElement;
  20. exports.isVisible = isVisible;
  21. exports.FOCUSABLE_SELECTOR = void 0;
  22. var _dom = require("@testing-library/dom");
  23. var _helpers = require("@testing-library/dom/dist/helpers");
  24. // isInstanceOfElement can be removed once the peerDependency for @testing-library/dom is bumped to a version that includes https://github.com/testing-library/dom-testing-library/pull/885
  25. /**
  26. * Check if an element is of a given type.
  27. *
  28. * @param {Element} element The element to test
  29. * @param {string} elementType Constructor name. E.g. 'HTMLSelectElement'
  30. */
  31. function isInstanceOfElement(element, elementType) {
  32. try {
  33. const window = (0, _helpers.getWindowFromNode)(element); // Window usually has the element constructors as properties but is not required to do so per specs
  34. if (typeof window[elementType] === 'function') {
  35. return element instanceof window[elementType];
  36. }
  37. } catch (e) {// The document might not be associated with a window
  38. } // Fall back to the constructor name as workaround for test environments that
  39. // a) not associate the document with a window
  40. // b) not provide the constructor as property of window
  41. if (/^HTML(\w+)Element$/.test(element.constructor.name)) {
  42. return element.constructor.name === elementType;
  43. } // The user passed some node that is not created in a browser-like environment
  44. throw new Error(`Unable to verify if element is instance of ${elementType}. Please file an issue describing your test environment: https://github.com/testing-library/dom-testing-library/issues/new`);
  45. }
  46. function isMousePressEvent(event) {
  47. return event === 'mousedown' || event === 'mouseup' || event === 'click' || event === 'dblclick';
  48. }
  49. function invert(map) {
  50. const res = {};
  51. for (const key of Object.keys(map)) {
  52. res[map[key]] = key;
  53. }
  54. return res;
  55. } // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
  56. const BUTTONS_TO_NAMES = {
  57. 0: 'none',
  58. 1: 'primary',
  59. 2: 'secondary',
  60. 4: 'auxiliary'
  61. };
  62. const NAMES_TO_BUTTONS = invert(BUTTONS_TO_NAMES); // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
  63. const BUTTON_TO_NAMES = {
  64. 0: 'primary',
  65. 1: 'auxiliary',
  66. 2: 'secondary'
  67. };
  68. const NAMES_TO_BUTTON = invert(BUTTON_TO_NAMES);
  69. function convertMouseButtons(event, init, property, mapping) {
  70. if (!isMousePressEvent(event)) {
  71. return 0;
  72. }
  73. if (init[property] != null) {
  74. return init[property];
  75. }
  76. if (init.buttons != null) {
  77. // not sure how to test this. Feel free to try and add a test if you want.
  78. // istanbul ignore next
  79. return mapping[BUTTONS_TO_NAMES[init.buttons]] || 0;
  80. }
  81. if (init.button != null) {
  82. // not sure how to test this. Feel free to try and add a test if you want.
  83. // istanbul ignore next
  84. return mapping[BUTTON_TO_NAMES[init.button]] || 0;
  85. }
  86. return property != 'button' && isMousePressEvent(event) ? 1 : 0;
  87. }
  88. function getMouseEventOptions(event, init, clickCount = 0) {
  89. init = init || {};
  90. return { ...init,
  91. // https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail
  92. detail: event === 'mousedown' || event === 'mouseup' || event === 'click' ? 1 + clickCount : clickCount,
  93. buttons: convertMouseButtons(event, init, 'buttons', NAMES_TO_BUTTONS),
  94. button: convertMouseButtons(event, init, 'button', NAMES_TO_BUTTON)
  95. };
  96. } // Absolutely NO events fire on label elements that contain their control
  97. // if that control is disabled. NUTS!
  98. // no joke. There are NO events for: <label><input disabled /><label>
  99. function isLabelWithInternallyDisabledControl(element) {
  100. var _element$control;
  101. return element.tagName === 'LABEL' && ((_element$control = element.control) == null ? void 0 : _element$control.disabled) && element.contains(element.control);
  102. }
  103. function getActiveElement(document) {
  104. const activeElement = document.activeElement;
  105. if (activeElement != null && activeElement.shadowRoot) {
  106. return getActiveElement(activeElement.shadowRoot);
  107. } else {
  108. return activeElement;
  109. }
  110. }
  111. function supportsMaxLength(element) {
  112. if (element.tagName === 'TEXTAREA') return true;
  113. if (element.tagName === 'INPUT') {
  114. const type = element.getAttribute('type'); // Missing value default is "text"
  115. if (!type) return true; // https://html.spec.whatwg.org/multipage/input.html#concept-input-apply
  116. if (type.match(/email|password|search|telephone|text|url/)) return true;
  117. }
  118. return false;
  119. }
  120. function getSelectionRange(element) {
  121. if (isContentEditable(element)) {
  122. const range = element.ownerDocument.getSelection().getRangeAt(0);
  123. return {
  124. selectionStart: range.startOffset,
  125. selectionEnd: range.endOffset
  126. };
  127. }
  128. return {
  129. selectionStart: element.selectionStart,
  130. selectionEnd: element.selectionEnd
  131. };
  132. } //jsdom is not supporting isContentEditable
  133. function isContentEditable(element) {
  134. return element.hasAttribute('contenteditable') && (element.getAttribute('contenteditable') == 'true' || element.getAttribute('contenteditable') == '');
  135. }
  136. function getValue(element) {
  137. if (isContentEditable(element)) {
  138. return element.textContent;
  139. }
  140. return element.value;
  141. }
  142. function calculateNewValue(newEntry, element) {
  143. var _element$getAttribute;
  144. const {
  145. selectionStart,
  146. selectionEnd
  147. } = getSelectionRange(element);
  148. const value = getValue(element); // can't use .maxLength property because of a jsdom bug:
  149. // https://github.com/jsdom/jsdom/issues/2927
  150. const maxLength = Number((_element$getAttribute = element.getAttribute('maxlength')) != null ? _element$getAttribute : -1);
  151. let newValue, newSelectionStart;
  152. if (selectionStart === null) {
  153. // at the end of an input type that does not support selection ranges
  154. // https://github.com/testing-library/user-event/issues/316#issuecomment-639744793
  155. newValue = value + newEntry;
  156. } else if (selectionStart === selectionEnd) {
  157. if (selectionStart === 0) {
  158. // at the beginning of the input
  159. newValue = newEntry + value;
  160. } else if (selectionStart === value.length) {
  161. // at the end of the input
  162. newValue = value + newEntry;
  163. } else {
  164. // in the middle of the input
  165. newValue = value.slice(0, selectionStart) + newEntry + value.slice(selectionEnd);
  166. }
  167. newSelectionStart = selectionStart + newEntry.length;
  168. } else {
  169. // we have something selected
  170. const firstPart = value.slice(0, selectionStart) + newEntry;
  171. newValue = firstPart + value.slice(selectionEnd);
  172. newSelectionStart = firstPart.length;
  173. }
  174. if (element.type === 'date' && !isValidDateValue(element, newValue)) {
  175. newValue = value;
  176. }
  177. if (element.type === 'time' && !isValidInputTimeValue(element, newValue)) {
  178. if (isValidInputTimeValue(element, newEntry)) {
  179. newValue = newEntry;
  180. } else {
  181. newValue = value;
  182. }
  183. }
  184. if (!supportsMaxLength(element) || maxLength < 0) {
  185. return {
  186. newValue,
  187. newSelectionStart
  188. };
  189. } else {
  190. return {
  191. newValue: newValue.slice(0, maxLength),
  192. newSelectionStart: newSelectionStart > maxLength ? maxLength : newSelectionStart
  193. };
  194. }
  195. }
  196. function setSelectionRangeIfNecessary(element, newSelectionStart, newSelectionEnd) {
  197. const {
  198. selectionStart,
  199. selectionEnd
  200. } = getSelectionRange(element);
  201. if (!isContentEditable(element) && (!element.setSelectionRange || selectionStart === null)) {
  202. // cannot set selection
  203. return;
  204. }
  205. if (selectionStart !== newSelectionStart || selectionEnd !== newSelectionStart) {
  206. if (isContentEditable(element)) {
  207. const range = element.ownerDocument.createRange();
  208. range.selectNodeContents(element);
  209. range.setStart(element.firstChild, newSelectionStart);
  210. range.setEnd(element.firstChild, newSelectionEnd);
  211. element.ownerDocument.getSelection().removeAllRanges();
  212. element.ownerDocument.getSelection().addRange(range);
  213. } else {
  214. element.setSelectionRange(newSelectionStart, newSelectionEnd);
  215. }
  216. }
  217. }
  218. 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(', ');
  219. exports.FOCUSABLE_SELECTOR = FOCUSABLE_SELECTOR;
  220. function isFocusable(element) {
  221. return !isLabelWithInternallyDisabledControl(element) && (element == null ? void 0 : element.matches(FOCUSABLE_SELECTOR));
  222. }
  223. const CLICKABLE_INPUT_TYPES = ['button', 'color', 'file', 'image', 'reset', 'submit'];
  224. function isClickableInput(element) {
  225. return element.tagName === 'BUTTON' || isInstanceOfElement(element, 'HTMLInputElement') && CLICKABLE_INPUT_TYPES.includes(element.type);
  226. }
  227. function isVisible(element) {
  228. const getComputedStyle = (0, _helpers.getWindowFromNode)(element).getComputedStyle;
  229. for (; element && element.ownerDocument; element = element.parentNode) {
  230. const display = getComputedStyle(element).display;
  231. if (display === 'none') {
  232. return false;
  233. }
  234. }
  235. return true;
  236. }
  237. function eventWrapper(cb) {
  238. let result;
  239. (0, _dom.getConfig)().eventWrapper(() => {
  240. result = cb();
  241. });
  242. return result;
  243. }
  244. function isValidDateValue(element, value) {
  245. if (element.type !== 'date') return false;
  246. const clone = element.cloneNode();
  247. clone.value = value;
  248. return clone.value === value;
  249. }
  250. function buildTimeValue(value) {
  251. function build(onlyDigitsValue, index) {
  252. const hours = onlyDigitsValue.slice(0, index);
  253. const validHours = Math.min(parseInt(hours, 10), 23);
  254. const minuteCharacters = onlyDigitsValue.slice(index);
  255. const parsedMinutes = parseInt(minuteCharacters, 10);
  256. const validMinutes = Math.min(parsedMinutes, 59);
  257. return `${validHours.toString().padStart(2, '0')}:${validMinutes.toString().padStart(2, '0')}`;
  258. }
  259. const onlyDigitsValue = value.replace(/\D/g, '');
  260. if (onlyDigitsValue.length < 2) {
  261. return value;
  262. }
  263. const firstDigit = parseInt(onlyDigitsValue[0], 10);
  264. const secondDigit = parseInt(onlyDigitsValue[1], 10);
  265. if (firstDigit >= 3 || firstDigit === 2 && secondDigit >= 4) {
  266. let index;
  267. if (firstDigit >= 3) {
  268. index = 1;
  269. } else {
  270. index = 2;
  271. }
  272. return build(onlyDigitsValue, index);
  273. }
  274. if (value.length === 2) {
  275. return value;
  276. }
  277. return build(onlyDigitsValue, 2);
  278. }
  279. function isValidInputTimeValue(element, timeValue) {
  280. if (element.type !== 'time') return false;
  281. const clone = element.cloneNode();
  282. clone.value = timeValue;
  283. return clone.value === timeValue;
  284. }