UNPKG

7.99 kBJavaScriptView Raw
1"use strict";
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.isFocusable = isFocusable;
7exports.isClickable = isClickable;
8exports.getMouseEventOptions = getMouseEventOptions;
9exports.isLabelWithInternallyDisabledControl = isLabelWithInternallyDisabledControl;
10exports.getActiveElement = getActiveElement;
11exports.calculateNewValue = calculateNewValue;
12exports.setSelectionRangeIfNecessary = setSelectionRangeIfNecessary;
13exports.eventWrapper = eventWrapper;
14exports.isValidDateValue = isValidDateValue;
15exports.getValue = getValue;
16exports.getSelectionRange = getSelectionRange;
17exports.isContentEditable = isContentEditable;
18exports.FOCUSABLE_SELECTOR = void 0;
19
20var _dom = require("@testing-library/dom");
21
22function isMousePressEvent(event) {
23 return event === 'mousedown' || event === 'mouseup' || event === 'click' || event === 'dblclick';
24}
25
26function invert(map) {
27 const res = {};
28
29 for (const key of Object.keys(map)) {
30 res[map[key]] = key;
31 }
32
33 return res;
34} // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
35
36
37const BUTTONS_TO_NAMES = {
38 0: 'none',
39 1: 'primary',
40 2: 'secondary',
41 4: 'auxiliary'
42};
43const NAMES_TO_BUTTONS = invert(BUTTONS_TO_NAMES); // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
44
45const BUTTON_TO_NAMES = {
46 0: 'primary',
47 1: 'auxiliary',
48 2: 'secondary'
49};
50const NAMES_TO_BUTTON = invert(BUTTON_TO_NAMES);
51
52function convertMouseButtons(event, init, property, mapping) {
53 if (!isMousePressEvent(event)) {
54 return 0;
55 }
56
57 if (init[property] != null) {
58 return init[property];
59 }
60
61 if (init.buttons != null) {
62 // not sure how to test this. Feel free to try and add a test if you want.
63 // istanbul ignore next
64 return mapping[BUTTONS_TO_NAMES[init.buttons]] || 0;
65 }
66
67 if (init.button != null) {
68 // not sure how to test this. Feel free to try and add a test if you want.
69 // istanbul ignore next
70 return mapping[BUTTON_TO_NAMES[init.button]] || 0;
71 }
72
73 return property != 'button' && isMousePressEvent(event) ? 1 : 0;
74}
75
76function getMouseEventOptions(event, init, clickCount = 0) {
77 init = init || {};
78 return { ...init,
79 // https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail
80 detail: event === 'mousedown' || event === 'mouseup' || event === 'click' ? 1 + clickCount : clickCount,
81 buttons: convertMouseButtons(event, init, 'buttons', NAMES_TO_BUTTONS),
82 button: convertMouseButtons(event, init, 'button', NAMES_TO_BUTTON)
83 };
84} // Absolutely NO events fire on label elements that contain their control
85// if that control is disabled. NUTS!
86// no joke. There are NO events for: <label><input disabled /><label>
87
88
89function isLabelWithInternallyDisabledControl(element) {
90 var _element$control;
91
92 return element.tagName === 'LABEL' && ((_element$control = element.control) == null ? void 0 : _element$control.disabled) && element.contains(element.control);
93}
94
95function getActiveElement(document) {
96 const activeElement = document.activeElement;
97
98 if (activeElement == null ? void 0 : activeElement.shadowRoot) {
99 return getActiveElement(activeElement.shadowRoot);
100 } else {
101 return activeElement;
102 }
103}
104
105function supportsMaxLength(element) {
106 if (element.tagName === 'TEXTAREA') return true;
107
108 if (element.tagName === 'INPUT') {
109 const type = element.getAttribute('type'); // Missing value default is "text"
110
111 if (!type) return true; // https://html.spec.whatwg.org/multipage/input.html#concept-input-apply
112
113 if (type.match(/email|password|search|telephone|text|url/)) return true;
114 }
115
116 return false;
117}
118
119function getSelectionRange(element) {
120 if (isContentEditable(element)) {
121 const range = element.ownerDocument.getSelection().getRangeAt(0);
122 return {
123 selectionStart: range.startOffset,
124 selectionEnd: range.endOffset
125 };
126 }
127
128 return {
129 selectionStart: element.selectionStart,
130 selectionEnd: element.selectionEnd
131 };
132} //jsdom is not supporting isContentEditable
133
134
135function isContentEditable(element) {
136 return element.hasAttribute('contenteditable') && (element.getAttribute('contenteditable') == 'true' || element.getAttribute('contenteditable') == '');
137}
138
139function getValue(element) {
140 if (isContentEditable(element)) {
141 return element.textContent;
142 }
143
144 return element.value;
145}
146
147function calculateNewValue(newEntry, element) {
148 var _element$getAttribute;
149
150 const {
151 selectionStart,
152 selectionEnd
153 } = getSelectionRange(element);
154 const value = getValue(element); // can't use .maxLength property because of a jsdom bug:
155 // https://github.com/jsdom/jsdom/issues/2927
156
157 const maxLength = Number((_element$getAttribute = element.getAttribute('maxlength')) != null ? _element$getAttribute : -1);
158 let newValue, newSelectionStart;
159
160 if (selectionStart === null) {
161 // at the end of an input type that does not support selection ranges
162 // https://github.com/testing-library/user-event/issues/316#issuecomment-639744793
163 newValue = value + newEntry;
164 } else if (selectionStart === selectionEnd) {
165 if (selectionStart === 0) {
166 // at the beginning of the input
167 newValue = newEntry + value;
168 } else if (selectionStart === value.length) {
169 // at the end of the input
170 newValue = value + newEntry;
171 } else {
172 // in the middle of the input
173 newValue = value.slice(0, selectionStart) + newEntry + value.slice(selectionEnd);
174 }
175
176 newSelectionStart = selectionStart + newEntry.length;
177 } else {
178 // we have something selected
179 const firstPart = value.slice(0, selectionStart) + newEntry;
180 newValue = firstPart + value.slice(selectionEnd);
181 newSelectionStart = firstPart.length;
182 }
183
184 if (element.type === 'date' && !isValidDateValue(element, newValue)) {
185 newValue = value;
186 }
187
188 if (!supportsMaxLength(element) || maxLength < 0) {
189 return {
190 newValue,
191 newSelectionStart
192 };
193 } else {
194 return {
195 newValue: newValue.slice(0, maxLength),
196 newSelectionStart: newSelectionStart > maxLength ? maxLength : newSelectionStart
197 };
198 }
199}
200
201function setSelectionRangeIfNecessary(element, newSelectionStart, newSelectionEnd) {
202 const {
203 selectionStart,
204 selectionEnd
205 } = getSelectionRange(element);
206
207 if (!isContentEditable(element) && (!element.setSelectionRange || selectionStart === null)) {
208 // cannot set selection
209 return;
210 }
211
212 if (selectionStart !== newSelectionStart || selectionEnd !== newSelectionStart) {
213 if (isContentEditable(element)) {
214 const range = element.ownerDocument.createRange();
215 range.selectNodeContents(element);
216 range.setStart(element.firstChild, newSelectionStart);
217 range.setEnd(element.firstChild, newSelectionEnd);
218 element.ownerDocument.getSelection().removeAllRanges();
219 element.ownerDocument.getSelection().addRange(range);
220 } else {
221 element.setSelectionRange(newSelectionStart, newSelectionEnd);
222 }
223 }
224}
225
226const FOCUSABLE_SELECTOR = ['input:not([disabled])', 'button:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', '[contenteditable=""]', '[contenteditable="true"]', 'a[href]', '[tabindex]:not([disabled])'].join(', ');
227exports.FOCUSABLE_SELECTOR = FOCUSABLE_SELECTOR;
228
229function isFocusable(element) {
230 return !isLabelWithInternallyDisabledControl(element) && (element == null ? void 0 : element.matches(FOCUSABLE_SELECTOR));
231}
232
233const CLICKABLE_INPUT_TYPES = ['button', 'color', 'file', 'image', 'reset', 'submit'];
234
235function isClickable(element) {
236 return element.tagName === 'BUTTON' || element instanceof element.ownerDocument.defaultView.HTMLInputElement && CLICKABLE_INPUT_TYPES.includes(element.type);
237}
238
239function eventWrapper(cb) {
240 let result;
241 (0, _dom.getConfig)().eventWrapper(() => {
242 result = cb();
243 });
244 return result;
245}
246
247function isValidDateValue(element, value) {
248 if (element.type !== 'date') return false;
249 const clone = element.cloneNode();
250 clone.value = value;
251 return clone.value === value;
252}
\No newline at end of file