tabbable
Version:
Returns an array of all tabbable DOM nodes within a containing node.
231 lines (180 loc) • 7.14 kB
JavaScript
/*!
* tabbable 5.2.0
* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
*/
;
Object.defineProperty(exports, '__esModule', { value: true });
var candidateSelectors = ['input', 'select', 'textarea', 'a[href]', 'button', '[tabindex]', 'audio[controls]', 'video[controls]', '[contenteditable]:not([contenteditable="false"])', 'details>summary:first-of-type', 'details'];
var candidateSelector = /* #__PURE__ */candidateSelectors.join(',');
var matches = typeof Element === 'undefined' ? function () {} : Element.prototype.matches || Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
var getCandidates = function getCandidates(el, includeContainer, filter) {
var candidates = Array.prototype.slice.apply(el.querySelectorAll(candidateSelector));
if (includeContainer && matches.call(el, candidateSelector)) {
candidates.unshift(el);
}
candidates = candidates.filter(filter);
return candidates;
};
var isContentEditable = function isContentEditable(node) {
return node.contentEditable === 'true';
};
var getTabindex = function getTabindex(node) {
var tabindexAttr = parseInt(node.getAttribute('tabindex'), 10);
if (!isNaN(tabindexAttr)) {
return tabindexAttr;
} // Browsers do not return `tabIndex` correctly for contentEditable nodes;
// so if they don't have a tabindex attribute specifically set, assume it's 0.
if (isContentEditable(node)) {
return 0;
} // in Chrome, <details/>, <audio controls/> and <video controls/> elements get a default
// `tabIndex` of -1 when the 'tabindex' attribute isn't specified in the DOM,
// yet they are still part of the regular tab order; in FF, they get a default
// `tabIndex` of 0; since Chrome still puts those elements in the regular tab
// order, consider their tab index to be 0.
if ((node.nodeName === 'AUDIO' || node.nodeName === 'VIDEO' || node.nodeName === 'DETAILS') && node.getAttribute('tabindex') === null) {
return 0;
}
return node.tabIndex;
};
var sortOrderedTabbables = function sortOrderedTabbables(a, b) {
return a.tabIndex === b.tabIndex ? a.documentOrder - b.documentOrder : a.tabIndex - b.tabIndex;
};
var isInput = function isInput(node) {
return node.tagName === 'INPUT';
};
var isHiddenInput = function isHiddenInput(node) {
return isInput(node) && node.type === 'hidden';
};
var isDetailsWithSummary = function isDetailsWithSummary(node) {
var r = node.tagName === 'DETAILS' && Array.prototype.slice.apply(node.children).some(function (child) {
return child.tagName === 'SUMMARY';
});
return r;
};
var getCheckedRadio = function getCheckedRadio(nodes, form) {
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].checked && nodes[i].form === form) {
return nodes[i];
}
}
};
var isTabbableRadio = function isTabbableRadio(node) {
if (!node.name) {
return true;
}
var radioScope = node.form || node.ownerDocument;
var queryRadios = function queryRadios(name) {
return radioScope.querySelectorAll('input[type="radio"][name="' + name + '"]');
};
var radioSet;
if (typeof window !== 'undefined' && typeof window.CSS !== 'undefined' && typeof window.CSS.escape === 'function') {
radioSet = queryRadios(window.CSS.escape(node.name));
} else {
try {
radioSet = queryRadios(node.name);
} catch (err) {
// eslint-disable-next-line no-console
console.error('Looks like you have a radio button with a name attribute containing invalid CSS selector characters and need the CSS.escape polyfill: %s', err.message);
return false;
}
}
var checked = getCheckedRadio(radioSet, node.form);
return !checked || checked === node;
};
var isRadio = function isRadio(node) {
return isInput(node) && node.type === 'radio';
};
var isNonTabbableRadio = function isNonTabbableRadio(node) {
return isRadio(node) && !isTabbableRadio(node);
};
var isHidden = function isHidden(node, displayCheck) {
if (getComputedStyle(node).visibility === 'hidden') {
return true;
}
var isDirectSummary = matches.call(node, 'details>summary:first-of-type');
var nodeUnderDetails = isDirectSummary ? node.parentElement : node;
if (matches.call(nodeUnderDetails, 'details:not([open]) *')) {
return true;
}
if (!displayCheck || displayCheck === 'full') {
while (node) {
if (getComputedStyle(node).display === 'none') {
return true;
}
node = node.parentElement;
}
} else if (displayCheck === 'non-zero-area') {
var _node$getBoundingClie = node.getBoundingClientRect(),
width = _node$getBoundingClie.width,
height = _node$getBoundingClie.height;
return width === 0 && height === 0;
}
return false;
};
var isNodeMatchingSelectorFocusable = function isNodeMatchingSelectorFocusable(options, node) {
if (node.disabled || isHiddenInput(node) || isHidden(node, options.displayCheck) ||
/* For a details element with a summary, the summary element gets the focused */
isDetailsWithSummary(node)) {
return false;
}
return true;
};
var isNodeMatchingSelectorTabbable = function isNodeMatchingSelectorTabbable(options, node) {
if (!isNodeMatchingSelectorFocusable(options, node) || isNonTabbableRadio(node) || getTabindex(node) < 0) {
return false;
}
return true;
};
var tabbable = function tabbable(el, options) {
options = options || {};
var regularTabbables = [];
var orderedTabbables = [];
var candidates = getCandidates(el, options.includeContainer, isNodeMatchingSelectorTabbable.bind(null, options));
candidates.forEach(function (candidate, i) {
var candidateTabindex = getTabindex(candidate);
if (candidateTabindex === 0) {
regularTabbables.push(candidate);
} else {
orderedTabbables.push({
documentOrder: i,
tabIndex: candidateTabindex,
node: candidate
});
}
});
var tabbableNodes = orderedTabbables.sort(sortOrderedTabbables).map(function (a) {
return a.node;
}).concat(regularTabbables);
return tabbableNodes;
};
var focusable = function focusable(el, options) {
options = options || {};
var candidates = getCandidates(el, options.includeContainer, isNodeMatchingSelectorFocusable.bind(null, options));
return candidates;
};
var isTabbable = function isTabbable(node, options) {
options = options || {};
if (!node) {
throw new Error('No node provided');
}
if (matches.call(node, candidateSelector) === false) {
return false;
}
return isNodeMatchingSelectorTabbable(options, node);
};
var focusableCandidateSelector = /* #__PURE__ */candidateSelectors.concat('iframe').join(',');
var isFocusable = function isFocusable(node, options) {
options = options || {};
if (!node) {
throw new Error('No node provided');
}
if (matches.call(node, focusableCandidateSelector) === false) {
return false;
}
return isNodeMatchingSelectorFocusable(options, node);
};
exports.focusable = focusable;
exports.isFocusable = isFocusable;
exports.isTabbable = isTabbable;
exports.tabbable = tabbable;
//# sourceMappingURL=index.js.map