UNPKG

@vimeo/iris

Version:
1,393 lines (1,349 loc) 150 kB
'use strict'; var react_esm = require('./react.esm-d9b3c6bd.js'); function _mergeNamespaces(n, m) { m.forEach(function (e) { e && typeof e !== 'string' && !Array.isArray(e) && Object.keys(e).forEach(function (k) { if (k !== 'default' && !(k in n)) { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); }); return Object.freeze(n); } function isElementType(element, tag, props) { if (element.namespaceURI && element.namespaceURI !== 'http://www.w3.org/1999/xhtml') { return false; } tag = Array.isArray(tag) ? tag : [tag]; // tagName is uppercase in HTMLDocument and lowercase in XMLDocument if (!tag.includes(element.tagName.toLowerCase())) { return false; } if (props) { return Object.entries(props).every(([k, v]) => element[k] === v); } return true; } var clickableInputTypes; (function (clickableInputTypes) { clickableInputTypes['button'] = 'button'; clickableInputTypes['color'] = 'color'; clickableInputTypes['file'] = 'file'; clickableInputTypes['image'] = 'image'; clickableInputTypes['reset'] = 'reset'; clickableInputTypes['submit'] = 'submit'; clickableInputTypes['checkbox'] = 'checkbox'; clickableInputTypes['radio'] = 'radio'; })(clickableInputTypes || (clickableInputTypes = {})); function isClickableInput(element) { return isElementType(element, 'button') || isElementType(element, 'input') && element.type in clickableInputTypes; } var helpers = {}; Object.defineProperty(helpers, "__esModule", { value: true }); var TEXT_NODE_1 = helpers.TEXT_NODE = void 0; var checkContainerType_1 = helpers.checkContainerType = checkContainerType; var getDocument_1 = helpers.getDocument = getDocument$1; var getWindowFromNode_1 = helpers.getWindowFromNode = getWindowFromNode$1; var jestFakeTimersAreEnabled_1 = helpers.jestFakeTimersAreEnabled = jestFakeTimersAreEnabled; // Constant node.nodeType for text nodes, see: // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType#Node_type_constants const TEXT_NODE = 3; TEXT_NODE_1 = helpers.TEXT_NODE = TEXT_NODE; function jestFakeTimersAreEnabled() { /* istanbul ignore else */ if (typeof jest !== 'undefined' && jest !== null) { return ( // legacy timers setTimeout._isMockFunction === true || // modern timers Object.prototype.hasOwnProperty.call(setTimeout, 'clock') ); } // istanbul ignore next return false; } function getDocument$1() { /* istanbul ignore if */ if (typeof window === 'undefined') { throw new Error('Could not find default container'); } return window.document; } function getWindowFromNode$1(node) { if (node.defaultView) { // node is document return node.defaultView; } else if (node.ownerDocument && node.ownerDocument.defaultView) { // node is a DOM node return node.ownerDocument.defaultView; } else if (node.window) { // node is window return node.window; } else if (node.ownerDocument && node.ownerDocument.defaultView === null) { throw new Error(`It looks like the window object is not available for the provided node.`); } else if (node.then instanceof Function) { throw new Error(`It looks like you passed a Promise object instead of a DOM node. Did you do something like \`fireEvent.click(screen.findBy...\` when you meant to use a \`getBy\` query \`fireEvent.click(screen.getBy...\`, or await the findBy query \`fireEvent.click(await screen.findBy...\`?`); } else if (Array.isArray(node)) { throw new Error(`It looks like you passed an Array instead of a DOM node. Did you do something like \`fireEvent.click(screen.getAllBy...\` when you meant to use a \`getBy\` query \`fireEvent.click(screen.getBy...\`?`); } else if (typeof node.debug === 'function' && typeof node.logTestingPlaygroundURL === 'function') { throw new Error(`It looks like you passed a \`screen\` object. Did you do something like \`fireEvent.click(screen, ...\` when you meant to use a query, e.g. \`fireEvent.click(screen.getBy..., \`?`); } else { // The user passed something unusual to a calling function throw new Error(`The given node is not an Element, the node type is: ${typeof node}.`); } } function checkContainerType(container) { if (!container || !(typeof container.querySelector === 'function') || !(typeof container.querySelectorAll === 'function')) { throw new TypeError(`Expected container to be an Element, a Document or a DocumentFragment but got ${getTypeName(container)}.`); } function getTypeName(object) { if (typeof object === 'object') { return object === null ? 'null' : object.constructor.name; } return typeof object; } } var named = /*#__PURE__*/_mergeNamespaces({ __proto__: null, get TEXT_NODE () { return TEXT_NODE_1; }, checkContainerType: checkContainerType_1, getDocument: getDocument_1, getWindowFromNode: getWindowFromNode_1, jestFakeTimersAreEnabled: jestFakeTimersAreEnabled_1, 'default': helpers }, [helpers]); const { getWindowFromNode } = named; function getWindow(node) { return getWindowFromNode(node); } // jsdom does not implement Blob.text() function readBlobText(blob, FileReader) { return new Promise((res, rej) => { const fr = new FileReader(); fr.onerror = rej; fr.onabort = rej; fr.onload = () => { res(String(fr.result)); }; fr.readAsText(blob); }); } // FileList can not be created per constructor. function createFileList(window, files) { const list = { ...files, length: files.length, item: index => list[index], [Symbol.iterator]: function* nextFile() { for (let i = 0; i < list.length; i++) { yield list[i]; } } }; list.constructor = window.FileList; // guard for environments without FileList /* istanbul ignore else */ if (window.FileList) { Object.setPrototypeOf(list, window.FileList.prototype); } Object.freeze(list); return list; } function _define_property$8(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } // DataTransfer is not implemented in jsdom. // DataTransfer with FileList is being created by the browser on certain events. class DataTransferItemStub { getAsFile() { return this.file; } getAsString(callback) { if (typeof this.data === 'string') { callback(this.data); } } /* istanbul ignore next */ webkitGetAsEntry() { throw new Error('not implemented'); } constructor(dataOrFile, type) { _define_property$8(this, "kind", void 0); _define_property$8(this, "type", void 0); _define_property$8(this, "file", null); _define_property$8(this, "data", undefined); if (typeof dataOrFile === 'string') { this.kind = 'string'; this.type = String(type); this.data = dataOrFile; } else { this.kind = 'file'; this.type = dataOrFile.type; this.file = dataOrFile; } } } class DataTransferItemListStub extends Array { add(...args) { const item = new DataTransferItemStub(args[0], args[1]); this.push(item); return item; } clear() { this.splice(0, this.length); } remove(index) { this.splice(index, 1); } } function getTypeMatcher(type, exact) { const [group, sub] = type.split('/'); const isGroup = !sub || sub === '*'; return item => { return exact ? item.type === (isGroup ? group : type) : isGroup ? item.type.startsWith(`${group}/`) : item.type === group; }; } function createDataTransferStub(window) { return new class DataTransferStub { getData(format) { var _this_items_find; const match = (_this_items_find = this.items.find(getTypeMatcher(format, true))) !== null && _this_items_find !== void 0 ? _this_items_find : this.items.find(getTypeMatcher(format, false)); let text = ''; match === null || match === void 0 ? void 0 : match.getAsString(t => { text = t; }); return text; } setData(format, data) { const matchIndex = this.items.findIndex(getTypeMatcher(format, true)); const item = new DataTransferItemStub(data, format); if (matchIndex >= 0) { this.items.splice(matchIndex, 1, item); } else { this.items.push(item); } } clearData(format) { if (format) { const matchIndex = this.items.findIndex(getTypeMatcher(format, true)); if (matchIndex >= 0) { this.items.remove(matchIndex); } } else { this.items.clear(); } } get types() { const t = []; if (this.files.length) { t.push('Files'); } this.items.forEach(i => t.push(i.type)); Object.freeze(t); return t; } /* istanbul ignore next */ setDragImage() {} constructor() { _define_property$8(this, "dropEffect", 'none'); _define_property$8(this, "effectAllowed", 'uninitialized'); _define_property$8(this, "items", new DataTransferItemListStub()); _define_property$8(this, "files", createFileList(window, [])); } }(); } function createDataTransfer(window, files = []) { // Use real DataTransfer if available const dt = typeof window.DataTransfer === 'undefined' ? createDataTransferStub(window) : /* istanbul ignore next */new window.DataTransfer(); Object.defineProperty(dt, 'files', { get: () => createFileList(window, files) }); return dt; } function getBlobFromDataTransferItem(window, item) { if (item.kind === 'file') { return item.getAsFile(); } let data = ''; item.getAsString(s => { data = s; }); return new window.Blob([data], { type: item.type }); } // Clipboard is not available in jsdom function _define_property$7(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } // MDN lists string|Blob|Promise<Blob|string> as possible types in ClipboardItemData // lib.dom.d.ts lists only Promise<Blob|string> // https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem/ClipboardItem#syntax function createClipboardItem(window, ...blobs) { const dataMap = Object.fromEntries(blobs.map(b => [typeof b === 'string' ? 'text/plain' : b.type, Promise.resolve(b)])); // use real ClipboardItem if available /* istanbul ignore if */ if (typeof window.ClipboardItem !== 'undefined') { return new window.ClipboardItem(dataMap); } return new class ClipboardItem { get types() { return Array.from(Object.keys(this.data)); } async getType(type) { const value = await this.data[type]; if (!value) { throw new Error(`${type} is not one of the available MIME types on this item.`); } return value instanceof window.Blob ? value : new window.Blob([value], { type }); } constructor(d) { _define_property$7(this, "data", void 0); this.data = d; } }(dataMap); } const ClipboardStubControl = Symbol('Manage ClipboardSub'); function createClipboardStub(window, control) { return Object.assign(new class Clipboard extends window.EventTarget { async read() { return Array.from(this.items); } async readText() { let text = ''; for (const item of this.items) { const type = item.types.includes('text/plain') ? 'text/plain' : item.types.find(t => t.startsWith('text/')); if (type) { text += await item.getType(type).then(b => readBlobText(b, window.FileReader)); } } return text; } async write(data) { this.items = data; } async writeText(text) { this.items = [createClipboardItem(window, text)]; } constructor(...args) { super(...args); _define_property$7(this, "items", []); } }(), { [ClipboardStubControl]: control }); } function isClipboardStub(clipboard) { return !!(clipboard === null || clipboard === void 0 ? void 0 : clipboard[ClipboardStubControl]); } function attachClipboardStubToView(window) { if (isClipboardStub(window.navigator.clipboard)) { return window.navigator.clipboard[ClipboardStubControl]; } const realClipboard = Object.getOwnPropertyDescriptor(window.navigator, 'clipboard'); let stub; const control = { resetClipboardStub: () => { stub = createClipboardStub(window, control); }, detachClipboardStub: () => { /* istanbul ignore if */if (realClipboard) { Object.defineProperty(window.navigator, 'clipboard', realClipboard); } else { Object.defineProperty(window.navigator, 'clipboard', { value: undefined, configurable: true }); } } }; stub = createClipboardStub(window, control); Object.defineProperty(window.navigator, 'clipboard', { get: () => stub, configurable: true }); return stub[ClipboardStubControl]; } function resetClipboardStubOnView(window) { if (isClipboardStub(window.navigator.clipboard)) { window.navigator.clipboard[ClipboardStubControl].resetClipboardStub(); } } function detachClipboardStubFromView(window) { if (isClipboardStub(window.navigator.clipboard)) { window.navigator.clipboard[ClipboardStubControl].detachClipboardStub(); } } async function readDataTransferFromClipboard(document) { const window = document.defaultView; const clipboard = window === null || window === void 0 ? void 0 : window.navigator.clipboard; const items = clipboard && (await clipboard.read()); if (!items) { throw new Error('The Clipboard API is unavailable.'); } const dt = createDataTransfer(window); for (const item of items) { for (const type of item.types) { dt.setData(type, await item.getType(type).then(b => readBlobText(b, window.FileReader))); } } return dt; } async function writeDataTransferToClipboard(document, clipboardData) { const window = getWindow(document); const clipboard = window.navigator.clipboard; const items = []; for (let i = 0; i < clipboardData.items.length; i++) { const dtItem = clipboardData.items[i]; const blob = getBlobFromDataTransferItem(window, dtItem); items.push(createClipboardItem(window, blob)); } const written = clipboard && (await clipboard.write(items).then(() => true, // Can happen with other implementations that e.g. require permissions /* istanbul ignore next */ () => false)); if (!written) { throw new Error('The Clipboard API is unavailable.'); } } const g = globalThis; /* istanbul ignore else */ if (typeof g.afterEach === 'function') { g.afterEach(() => resetClipboardStubOnView(globalThis.window)); } /* istanbul ignore else */ if (typeof g.afterAll === 'function') { g.afterAll(() => detachClipboardStubFromView(globalThis.window)); } //jsdom is not supporting isContentEditable function isContentEditable(element) { return element.hasAttribute('contenteditable') && (element.getAttribute('contenteditable') == 'true' || element.getAttribute('contenteditable') == ''); } /** * If a node is a contenteditable or inside one, return that element. */ function getContentEditable(node) { const element = getElement$1(node); return element && (element.closest('[contenteditable=""]') || element.closest('[contenteditable="true"]')); } function getElement$1(node) { return node.nodeType === 1 ? node : node.parentElement; } function isEditable(element) { return isEditableInputOrTextArea(element) && !element.readOnly || isContentEditable(element); } var editableInputTypes; (function (editableInputTypes) { editableInputTypes['text'] = 'text'; editableInputTypes['date'] = 'date'; editableInputTypes['datetime-local'] = 'datetime-local'; editableInputTypes['email'] = 'email'; editableInputTypes['month'] = 'month'; editableInputTypes['number'] = 'number'; editableInputTypes['password'] = 'password'; editableInputTypes['search'] = 'search'; editableInputTypes['tel'] = 'tel'; editableInputTypes['time'] = 'time'; editableInputTypes['url'] = 'url'; editableInputTypes['week'] = 'week'; })(editableInputTypes || (editableInputTypes = {})); function isEditableInputOrTextArea(element) { return isElementType(element, 'textarea') || isElementType(element, 'input') && element.type in editableInputTypes; } var maxLengthSupportedTypes; (function (maxLengthSupportedTypes) { maxLengthSupportedTypes['email'] = 'email'; maxLengthSupportedTypes['password'] = 'password'; maxLengthSupportedTypes['search'] = 'search'; maxLengthSupportedTypes['telephone'] = 'telephone'; maxLengthSupportedTypes['text'] = 'text'; maxLengthSupportedTypes['url'] = 'url'; })(maxLengthSupportedTypes || (maxLengthSupportedTypes = {})); // can't use .maxLength property because of a jsdom bug: // https://github.com/jsdom/jsdom/issues/2927 function getMaxLength(element) { var _element_getAttribute; const attr = (_element_getAttribute = element.getAttribute('maxlength')) !== null && _element_getAttribute !== void 0 ? _element_getAttribute : ''; return /^\d+$/.test(attr) && Number(attr) >= 0 ? Number(attr) : undefined; } function supportsMaxLength(element) { return isElementType(element, 'textarea') || isElementType(element, 'input') && element.type in maxLengthSupportedTypes; } 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(', '); function isFocusable(element) { return element.matches(FOCUSABLE_SELECTOR); } var bracketDict; (function (bracketDict) { bracketDict['{'] = '}'; bracketDict['['] = ']'; })(bracketDict || (bracketDict = {})); /** * Read the next key definition from user input * * Describe key per `{descriptor}` or `[descriptor]`. * Everything else will be interpreted as a single character as descriptor - e.g. `a`. * Brackets `{` and `[` can be escaped by doubling - e.g. `foo[[bar` translates to `foo[bar`. * A previously pressed key can be released per `{/descriptor}`. * Keeping the key pressed can be written as `{descriptor>}`. * When keeping the key pressed you can choose how long the key is pressed `{descriptor>3}`. * You can then release the key per `{descriptor>3/}` or keep it pressed and continue with the next key. */ function readNextDescriptor(text, context) { let pos = 0; const startBracket = text[pos] in bracketDict ? text[pos] : ''; pos += startBracket.length; const isEscapedChar = new RegExp(`^\\${startBracket}{2}`).test(text); const type = isEscapedChar ? '' : startBracket; return { type, ...(type === '' ? readPrintableChar(text, pos, context) : readTag(text, pos, type, context)) }; } function readPrintableChar(text, pos, context) { const descriptor = text[pos]; assertDescriptor(descriptor, text, pos, context); pos += descriptor.length; return { consumedLength: pos, descriptor, releasePrevious: false, releaseSelf: true, repeat: 1 }; } function readTag(text, pos, startBracket, context) { var _text_slice_match, _text_slice_match1; const releasePreviousModifier = text[pos] === '/' ? '/' : ''; pos += releasePreviousModifier.length; const escapedDescriptor = startBracket === '{' && text[pos] === '\\'; pos += Number(escapedDescriptor); const descriptor = escapedDescriptor ? text[pos] : (_text_slice_match = text.slice(pos).match(startBracket === '{' ? /^\w+|^[^}>/]/ : /^\w+/)) === null || _text_slice_match === void 0 ? void 0 : _text_slice_match[0]; assertDescriptor(descriptor, text, pos, context); pos += descriptor.length; var _text_slice_match_; const repeatModifier = (_text_slice_match_ = (_text_slice_match1 = text.slice(pos).match(/^>\d+/)) === null || _text_slice_match1 === void 0 ? void 0 : _text_slice_match1[0]) !== null && _text_slice_match_ !== void 0 ? _text_slice_match_ : ''; pos += repeatModifier.length; const releaseSelfModifier = text[pos] === '/' || !repeatModifier && text[pos] === '>' ? text[pos] : ''; pos += releaseSelfModifier.length; const expectedEndBracket = bracketDict[startBracket]; const endBracket = text[pos] === expectedEndBracket ? expectedEndBracket : ''; if (!endBracket) { throw new Error(getErrorMessage([!repeatModifier && 'repeat modifier', !releaseSelfModifier && 'release modifier', `"${expectedEndBracket}"`].filter(Boolean).join(' or '), text[pos], text, context)); } pos += endBracket.length; return { consumedLength: pos, descriptor, releasePrevious: !!releasePreviousModifier, repeat: repeatModifier ? Math.max(Number(repeatModifier.substr(1)), 1) : 1, releaseSelf: hasReleaseSelf(releaseSelfModifier, repeatModifier) }; } function assertDescriptor(descriptor, text, pos, context) { if (!descriptor) { throw new Error(getErrorMessage('key descriptor', text[pos], text, context)); } } function hasReleaseSelf(releaseSelfModifier, repeatModifier) { if (releaseSelfModifier) { return releaseSelfModifier === '/'; } if (repeatModifier) { return false; } } function getErrorMessage(expected, found, text, context) { return `Expected ${expected} but found "${found !== null && found !== void 0 ? found : ''}" in "${text}" See ${context === 'pointer' ? `https://testing-library.com/docs/user-event/pointer#pressing-a-button-or-touching-the-screen` : `https://testing-library.com/docs/user-event/keyboard`} for more information about how userEvent parses your input.`; } function cloneEvent(event) { return new event.constructor(event.type, event); } var ApiLevel; (function (ApiLevel) { ApiLevel[ApiLevel["Trigger"] = 2] = "Trigger"; ApiLevel[ApiLevel["Call"] = 1] = "Call"; })(ApiLevel || (ApiLevel = {})); function setLevelRef(instance, level) { instance.levelRefs[level] = {}; } function getLevelRef(instance, level) { return instance.levelRefs[level]; } var PointerEventsCheckLevel; (function (PointerEventsCheckLevel) { PointerEventsCheckLevel[PointerEventsCheckLevel[ /** * Check pointer events on every user interaction that triggers a bunch of events. * E.g. once for releasing a mouse button even though this triggers `pointerup`, `mouseup`, `click`, etc... */ "EachTrigger"] = 4] = "EachTrigger"; PointerEventsCheckLevel[PointerEventsCheckLevel[/** Check each target once per call to pointer (related) API */"EachApiCall"] = 2] = "EachApiCall"; PointerEventsCheckLevel[PointerEventsCheckLevel[/** Check each event target once */"EachTarget"] = 1] = "EachTarget"; PointerEventsCheckLevel[PointerEventsCheckLevel[/** No pointer events check */"Never"] = 0] = "Never"; })(PointerEventsCheckLevel || (PointerEventsCheckLevel = {})); // This should probably just rely on the :disabled pseudo-class, but JSDOM doesn't implement it properly. function isDisabled(element) { for (let el = element; el; el = el.parentElement) { if (isElementType(el, ['button', 'input', 'select', 'textarea', 'optgroup', 'option'])) { if (el.hasAttribute('disabled')) { return true; } } else if (isElementType(el, 'fieldset')) { var _el_querySelector; if (el.hasAttribute('disabled') && !((_el_querySelector = el.querySelector(':scope > legend')) === null || _el_querySelector === void 0 ? void 0 : _el_querySelector.contains(element))) { return true; } } else if (el.tagName.includes('-')) { if (el.constructor.formAssociated && el.hasAttribute('disabled')) { return true; } } } return false; } function getActiveElement(document) { const activeElement = document.activeElement; if (activeElement === null || activeElement === void 0 ? void 0 : activeElement.shadowRoot) { return getActiveElement(activeElement.shadowRoot); } else { // Browser does not yield disabled elements as document.activeElement - jsdom does if (isDisabled(activeElement)) { return document.ownerDocument ? /* istanbul ignore next */document.ownerDocument.body : document.body; } return activeElement; } } function getActiveElementOrBody(document) { var _getActiveElement; return (_getActiveElement = getActiveElement(document)) !== null && _getActiveElement !== void 0 ? _getActiveElement : /* istanbul ignore next */document.body; } function findClosest(element, callback) { let el = element; do { if (callback(el)) { return el; } el = el.parentElement; } while (el && el !== element.ownerDocument.body); return undefined; } /** * Determine if the element has its own selection implementation * and does not interact with the Document Selection API. */ function hasOwnSelection(node) { return isElement$1(node) && isEditableInputOrTextArea(node); } function hasNoSelection(node) { return isElement$1(node) && isClickableInput(node); } function isElement$1(node) { return node.nodeType === 1; } /** * Reset the Document Selection when moving focus into an element * with own selection implementation. */ function updateSelectionOnFocus(element) { const selection = element.ownerDocument.getSelection(); /* istanbul ignore if */ if (!(selection === null || selection === void 0 ? void 0 : selection.focusNode)) { return; } // If the focus moves inside an element with own selection implementation, // the document selection will be this element. // But if the focused element is inside a contenteditable, // 1) a collapsed selection will be retained. // 2) other selections will be replaced by a cursor // 2.a) at the start of the first child if it is a text node // 2.b) at the start of the contenteditable. if (hasOwnSelection(element)) { const contenteditable = getContentEditable(selection.focusNode); if (contenteditable) { if (!selection.isCollapsed) { var _contenteditable_firstChild; const focusNode = ((_contenteditable_firstChild = contenteditable.firstChild) === null || _contenteditable_firstChild === void 0 ? void 0 : _contenteditable_firstChild.nodeType) === 3 ? contenteditable.firstChild : contenteditable; selection.setBaseAndExtent(focusNode, 0, focusNode, 0); } } else { selection.setBaseAndExtent(element, 0, element, 0); } } } const { getConfig: getConfig$2 } = react_esm.named; function wrapEvent(cb, _element) { return getConfig$2().eventWrapper(cb); } /** * Focus closest focusable element. */ function focusElement(element) { const target = findClosest(element, isFocusable); const activeElement = getActiveElement(element.ownerDocument); if ((target !== null && target !== void 0 ? target : element.ownerDocument.body) === activeElement) { return; } else if (target) { wrapEvent(() => target.focus()); } else { wrapEvent(() => activeElement === null || activeElement === void 0 ? void 0 : activeElement.blur()); } updateSelectionOnFocus(target !== null && target !== void 0 ? target : element.ownerDocument.body); } function blurElement(element) { if (!isFocusable(element)) return; const wasActive = getActiveElement(element.ownerDocument) === element; if (!wasActive) return; wrapEvent(() => element.blur()); } const behavior = {}; behavior.click = (event, target, instance) => { const context = target.closest('button,input,label,select,textarea'); const control = context && isElementType(context, 'label') && context.control; if (control) { return () => { if (isFocusable(control)) { focusElement(control); } instance.dispatchEvent(control, cloneEvent(event)); }; } else if (isElementType(target, 'input', { type: 'file' })) { return () => { // blur fires when the file selector pops up blurElement(target); target.dispatchEvent(new (getWindow(target).Event)('fileDialog')); // focus fires after the file selector has been closed focusElement(target); }; } }; const UIValue = Symbol('Displayed value in UI'); const UISelection = Symbol('Displayed selection in UI'); const InitialValue = Symbol('Initial value to compare on blur'); function isUIValue(value) { return typeof value === 'object' && UIValue in value; } function isUISelectionStart(start) { return !!start && typeof start === 'object' && UISelection in start; } function setUIValue(element, value) { if (element[InitialValue] === undefined) { element[InitialValue] = element.value; } element[UIValue] = value; // eslint-disable-next-line no-new-wrappers element.value = Object.assign(new String(value), { [UIValue]: true }); } function getUIValue(element) { return element[UIValue] === undefined ? element.value : String(element[UIValue]); } /** Flag the IDL value as clean. This does not change the value.*/ function setUIValueClean(element) { element[UIValue] = undefined; } function clearInitialValue(element) { element[InitialValue] = undefined; } function getInitialValue(element) { return element[InitialValue]; } function setUISelectionRaw(element, selection) { element[UISelection] = selection; } function setUISelection(element, { focusOffset: focusOffsetParam, anchorOffset: anchorOffsetParam = focusOffsetParam }, mode = 'replace') { const valueLength = getUIValue(element).length; const sanitizeOffset = o => Math.max(0, Math.min(valueLength, o)); const anchorOffset = mode === 'replace' || element[UISelection] === undefined ? sanitizeOffset(anchorOffsetParam) : element[UISelection].anchorOffset; const focusOffset = sanitizeOffset(focusOffsetParam); const startOffset = Math.min(anchorOffset, focusOffset); const endOffset = Math.max(anchorOffset, focusOffset); element[UISelection] = { anchorOffset, focusOffset }; if (element.selectionStart === startOffset && element.selectionEnd === endOffset) { return; } // eslint-disable-next-line no-new-wrappers const startObj = Object.assign(new Number(startOffset), { [UISelection]: true }); try { element.setSelectionRange(startObj, endOffset); } catch { // DOMException for invalid state is expected when calling this // on an element without support for setSelectionRange } } function getUISelection(element) { var _element_selectionStart, _element_selectionEnd, _element_UISelection; const sel = (_element_UISelection = element[UISelection]) !== null && _element_UISelection !== void 0 ? _element_UISelection : { anchorOffset: (_element_selectionStart = element.selectionStart) !== null && _element_selectionStart !== void 0 ? _element_selectionStart : 0, focusOffset: (_element_selectionEnd = element.selectionEnd) !== null && _element_selectionEnd !== void 0 ? _element_selectionEnd : 0 }; return { ...sel, startOffset: Math.min(sel.anchorOffset, sel.focusOffset), endOffset: Math.max(sel.anchorOffset, sel.focusOffset) }; } function hasUISelection(element) { return !!element[UISelection]; } /** Flag the IDL selection as clean. This does not change the selection. */ function setUISelectionClean(element) { element[UISelection] = undefined; } const parseInt = globalThis.parseInt; function buildTimeValue(value) { 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 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')}`; } function isValidDateOrTimeValue(element, value) { const clone = element.cloneNode(); clone.value = value; return clone.value === value; } function getNextCursorPosition(node, offset, direction, inputType) { // The behavior at text node zero offset is inconsistent. // When walking backwards: // Firefox always moves to zero offset and jumps over last offset. // Chrome jumps over zero offset per default but over last offset when Shift is pressed. // The cursor always moves to zero offset if the focus area (contenteditable or body) ends there. // When walking foward both ignore zero offset. // When walking over input elements the cursor moves before or after that element. // When walking over line breaks the cursor moves inside any following text node. if (isTextNode(node) && offset + direction >= 0 && offset + direction <= node.nodeValue.length) { return { node, offset: offset + direction }; } const nextNode = getNextCharacterContentNode(node, offset, direction); if (nextNode) { if (isTextNode(nextNode)) { return { node: nextNode, offset: direction > 0 ? Math.min(1, nextNode.nodeValue.length) : Math.max(nextNode.nodeValue.length - 1, 0) }; } else if (isElementType(nextNode, 'br')) { const nextPlusOne = getNextCharacterContentNode(nextNode, undefined, direction); if (!nextPlusOne) { // The behavior when there is no possible cursor position beyond the line break is inconsistent. // In Chrome outside of contenteditable moving before a leading line break is possible. // A leading line break can still be removed per deleteContentBackward. // A trailing line break on the other hand is not removed by deleteContentForward. if (direction < 0 && inputType === 'deleteContentBackward') { return { node: nextNode.parentNode, offset: getOffset(nextNode) }; } return undefined; } else if (isTextNode(nextPlusOne)) { return { node: nextPlusOne, offset: direction > 0 ? 0 : nextPlusOne.nodeValue.length }; } else if (direction < 0 && isElementType(nextPlusOne, 'br')) { return { node: nextNode.parentNode, offset: getOffset(nextNode) }; } else { return { node: nextPlusOne.parentNode, offset: getOffset(nextPlusOne) + (direction > 0 ? 0 : 1) }; } } else { return { node: nextNode.parentNode, offset: getOffset(nextNode) + (direction > 0 ? 1 : 0) }; } } } function getNextCharacterContentNode(node, offset, direction) { const nextOffset = Number(offset) + (direction < 0 ? -1 : 0); if (offset !== undefined && isElement(node) && nextOffset >= 0 && nextOffset < node.children.length) { node = node.children[nextOffset]; } return walkNodes(node, direction === 1 ? 'next' : 'previous', isTreatedAsCharacterContent); } function isTreatedAsCharacterContent(node) { if (isTextNode(node)) { return true; } if (isElement(node)) { if (isElementType(node, ['input', 'textarea'])) { return node.type !== 'hidden'; } else if (isElementType(node, 'br')) { return true; } } return false; } function getOffset(node) { let i = 0; while (node.previousSibling) { i++; node = node.previousSibling; } return i; } function isElement(node) { return node.nodeType === 1; } function isTextNode(node) { return node.nodeType === 3; } function walkNodes(node, direction, callback) { for (;;) { var _node_ownerDocument; const sibling = node[`${direction}Sibling`]; if (sibling) { node = getDescendant(sibling, direction === 'next' ? 'first' : 'last'); if (callback(node)) { return node; } } else if (node.parentNode && (!isElement(node.parentNode) || !isContentEditable(node.parentNode) && node.parentNode !== ((_node_ownerDocument = node.ownerDocument) === null || _node_ownerDocument === void 0 ? void 0 : _node_ownerDocument.body))) { node = node.parentNode; } else { break; } } } function getDescendant(node, direction) { while (node.hasChildNodes()) { node = node[`${direction}Child`]; } return node; } const TrackChanges = Symbol('Track programmatic changes for React workaround'); // When the input event happens in the browser, React executes all event handlers // and if they change state of a controlled value, nothing happens. // But when we trigger the event handlers in test environment with React@17, // the changes are rolled back before the state update is applied. // This results in a reset cursor. // There might be a better way to work around if we figure out // why the batched update is executed differently in our test environment. function isReact17Element(element) { return Object.getOwnPropertyNames(element).some(k => k.startsWith('__react')) && getWindow(element).REACT_VERSION === 17; } function startTrackValue(element) { if (!isReact17Element(element)) { return; } element[TrackChanges] = { previousValue: String(element.value), tracked: [] }; } function trackOrSetValue(element, v) { var _element_TrackChanges_tracked, _element_TrackChanges; (_element_TrackChanges = element[TrackChanges]) === null || _element_TrackChanges === void 0 ? void 0 : (_element_TrackChanges_tracked = _element_TrackChanges.tracked) === null || _element_TrackChanges_tracked === void 0 ? void 0 : _element_TrackChanges_tracked.push(v); if (!element[TrackChanges]) { setUIValueClean(element); setUISelection(element, { focusOffset: v.length }); } } function commitValueAfterInput(element, cursorOffset) { var _changes_tracked; const changes = element[TrackChanges]; element[TrackChanges] = undefined; if (!(changes === null || changes === void 0 ? void 0 : (_changes_tracked = changes.tracked) === null || _changes_tracked === void 0 ? void 0 : _changes_tracked.length)) { return; } const isJustReactStateUpdate = changes.tracked.length === 2 && changes.tracked[0] === changes.previousValue && changes.tracked[1] === element.value; if (!isJustReactStateUpdate) { setUIValueClean(element); } if (hasUISelection(element)) { setUISelection(element, { focusOffset: isJustReactStateUpdate ? cursorOffset : element.value.length }); } } /** * Determine which selection logic and selection ranges to consider. */ function getTargetTypeAndSelection(node) { const element = getElement(node); if (element && hasOwnSelection(element)) { return { type: 'input', selection: getUISelection(element) }; } const selection = element === null || element === void 0 ? void 0 : element.ownerDocument.getSelection(); // It is possible to extend a single-range selection into a contenteditable. // This results in the range acting like a range outside of contenteditable. const isCE = getContentEditable(node) && (selection === null || selection === void 0 ? void 0 : selection.anchorNode) && getContentEditable(selection.anchorNode); return { type: isCE ? 'contenteditable' : 'default', selection }; } function getElement(node) { return node.nodeType === 1 ? node : node.parentElement; } /** * Get the range that would be overwritten by input. */ function getInputRange(focusNode) { const typeAndSelection = getTargetTypeAndSelection(focusNode); if (typeAndSelection.type === 'input') { return typeAndSelection.selection; } else if (typeAndSelection.type === 'contenteditable') { var _typeAndSelection_selection; // Multi-range on contenteditable edits the first selection instead of the last return (_typeAndSelection_selection = typeAndSelection.selection) === null || _typeAndSelection_selection === void 0 ? void 0 : _typeAndSelection_selection.getRangeAt(0); } } /** * Set the selection */ function setSelection({ focusNode, focusOffset, anchorNode = focusNode, anchorOffset = focusOffset }) { var _anchorNode_ownerDocument_getSelection, _anchorNode_ownerDocument; const typeAndSelection = getTargetTypeAndSelection(focusNode); if (typeAndSelection.type === 'input') { return setUISelection(focusNode, { anchorOffset, focusOffset }); } (_anchorNode_ownerDocument = anchorNode.ownerDocument) === null || _anchorNode_ownerDocument === void 0 ? void 0 : (_anchorNode_ownerDocument_getSelection = _anchorNode_ownerDocument.getSelection()) === null || _anchorNode_ownerDocument_getSelection === void 0 ? void 0 : _anchorNode_ownerDocument_getSelection.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset); } function isDateOrTime(element) { return isElementType(element, 'input') && ['date', 'time'].includes(element.type); } function input(instance, element, data, inputType = 'insertText') { const inputRange = getInputRange(element); /* istanbul ignore if */ if (!inputRange) { return; } // There is no `beforeinput` event on `date` and `time` input if (!isDateOrTime(element)) { const unprevented = instance.dispatchUIEvent(element, 'beforeinput', { inputType, data }); if (!unprevented) { return; } } if ('startContainer' in inputRange) { editContenteditable(instance, element, inputRange, data, inputType); } else { editInputElement(instance, element, inputRange, data, inputType); } } function editContenteditable(instance, element, inputRange, data, inputType) { let del = false; if (!inputRange.collapsed) { del = true; inputRange.deleteContents(); } else if (['deleteContentBackward', 'deleteContentForward'].includes(inputType)) { const nextPosition = getNextCursorPosition(inputRange.startContainer, inputRange.startOffset, inputType === 'deleteContentBackward' ? -1 : 1, inputType); if (nextPosition) { del = true; const delRange = inputRange.cloneRange(); if (delRange.comparePoint(nextPosition.node, nextPosition.offset) < 0) { delRange.setStart(nextPosition.node, nextPosition.offset); } else { delRange.setEnd(nextPosition.node, nextPosition.offset); } delRange.deleteContents(); } } if (data) { if (inputRange.endContainer.nodeType === 3) { const offset = inputRange.endOffset; inputRange.endContainer.insertData(offset, data); inputRange.setStart(inputRange.endContainer, offset + data.length); inputRange.setEnd(inputRange.endContainer, offset + data.length); } else { const text = element.ownerDocument.createTextNode(data); inputRange.insertNode(text); inputRange.setStart(text, data.length); inputRange.setEnd(text, data.length); } } if (del || data) { instance.dispatchUIEvent(element, 'input', { inputType }); } } function editInputElement(instance, element, inputRange, data, inputType) { let dataToInsert = data; if (supportsMaxLength(element)) { const maxLength = getMaxLength(element); if (maxLength !== undefined && data.length > 0) { const spaceUntilMaxLength = maxLength - element.value.length; if (spaceUntilMaxLength > 0) { dataToInsert = data.substring(0, spaceUntilMaxLength); } else { return; } } } const { newValue, newOffset, oldValue } = calculateNewValue(dataToInsert, element, inputRange, inputType); if (newValue === oldValue && newOffset === inputRange.startOffset && newOffset === inputRange.endOffset) { return; } if (isElementType(element, 'input', { type: 'number' }) && !isValidNumberInput(newValue)) { return; } setUIValue(element, newValue); setSelection({ focusNode: element, anchorOffset: newOffset, focusOffset: newOffset }); if (isDateOrTime(element)) { if (isValidDateOrTimeValue(element, newValue)) { commitInput(instance, element, newOffset, {}); instance.dispatchUIEvent(element, 'change'); clearInitialValue(element); } } else { commitInput(instance, element, newOffset, { data, inputType }); } } function calculateNewValue(inputData, node, { startOffset, endOffset }, inputType) { const value = getUIValue(node); const prologEnd = Math.max(0, startOffset === endOffset && inputType === 'deleteContentBackward' ? startOffset - 1 : startOffset); const prolog = value.substring(0, prologEnd); const epilogStart = Math.min(value.length, startOffset === endOffset && inputType === 'deleteContentForward' ? startOffset + 1 : endOffset); const epilog = value.substring(epilogStart, value.length); let newValue = `${prolog}${inputData}${epilog}`; let newOffset = prologEnd + inputData.length; if (isElementType(node, 'input', { type: 'time' })) { const builtValue = buildTimeValue(newValue); if (builtValue !== '' && isValidDateOrTimeValue(node, builtValue)) { newValue = builtValue; newOffset = builtValue.length; } } return { oldValue: value, newValue, newOffset }; } function commitInput(instance, element, newOffset, inputInit) { instance.dispatchUIEvent(element, 'input', inputInit); commitValueAfterInput(element, newOffset); } function isValidNumberInput(value) { var _value_match, _value_match1; // the browser allows some invalid input but not others // it allows up to two '-' at any place before any 'e' or one directly following 'e' // it allows one '.' at any place before e const valueParts = value.split('e', 2); return !(/[^\d.\-e]/.test(value) || Number((_value_match = value.match(/-/g)) === null || _value_match === void 0 ? void 0 : _value_match.length) > 2 || Number((_value_match1 = value.match(/\./g)) === null || _value_match1 === void 0 ? void 0 : _value_match1.length) > 1 || valueParts[1] && !/^-?\d*$/.test(valueParts[1])); } behavior.cut = (event, target, instance) => { return () => { if (isEditable(target)) { input(instance, target, '', 'deleteByCut'); } }; }; function getValueOrTextContent(element) { // istanbul ignore if if (!element) { return null; } if (isContentEditable(element)) { return element.textContent; } return getUIValue(element); } function isVisible(element) { const window = getWindow(element); for (let el = element; el === null || el === void 0 ? void 0 : el.ownerDocument; el = el.parentElement) { const { display, visibility } = window.getComputedStyle(el); if (display === 'none') { return false; } if (visibility === 'hidden') { return false; } } return true; } function getTabDestination(activeElement, shift) { const document = activeElement.ownerDocument; const focusableElements = document.querySelectorAll(FOCUSABLE_SELECTOR); const enabledElements = Array.from(focusableElements).filter(el => el === activeElement || !(Number(el.getAttribute('tabindex')) < 0 || isDisabled(el))); // tabindex has no effect if the active element has negative tabindex if (Number(activeElement.getAttribute('tabindex')) >= 0) { enabledElements.sort((a, b) => { const i = Number(a.getAttribute('tabindex')); const j = Number(b.getAttribute('tabindex')); if (i === j) { return 0; } else if (i === 0) { return 1; } else if (j === 0) { return -1; } return i - j; }); } const checkedRadio = {}; let prunedElements = [document.body]; const activeRadioGroup = isElementType(activeElement, 'input', { type: 'radio' }) ? activeElement.name : undefined; enabledElements.forEach(currentElement => { const el = currentElement; // For radio groups keep only the active radio // If there is no active radio, keep only the checked radio // If there is no checked radio, treat like everything else if (isElementType(el, 'input', { type: 'radio' }) && el.name) { // If the active element is part of the group, add only that if (el === activeElement) { prunedElements.push(el); return; } else if (el.name === activeRadioGroup) { return; } // If we stumble upon a checked radio, remove the others if (el.checked) { prunedElements = prunedElements.filter(e => !isElementType(e, 'input', { type: 'radio', name: el.name })); prunedElements.push(el); checkedRadio[el.name] = el; return; } // If we already found the checked one, skip if (typeof checkedRadio[el.name] !== 'undefined') { return; } } prunedElements.push(el); }); for (let index = prunedElements.findIndex(el => el === activeElement);;) { index += shift ? -1 : 1; // loop at overflow if (index === prunedElements.length) { index = 0; } else if (index === -1) { index = prunedElements.length - 1; } if (prunedElements[index] === activeElement || prunedElements[index] === document.body || isVisible(prunedElements[index])) { return prunedElements[index]; } } } /** * Move the selection */ function moveSelection(node, direction) { // TODO: implement shift if (hasOwnSelection(node)) { const selection = getUISelection(node); setSelection({ focusNode: node, focusOffset: selection.startOffset === selection.endOffset ? selection.focusOffset + direction : direction < 0 ? selection.startOffset : selection.endOffset }); } else { const selection = node.ownerDocument.getSe