Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

tabbable

Package Overview
Dependencies
Maintainers
1
Versions
47
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

tabbable - npm Package Compare versions

Comparing version 2.0.0 to 3.0.0

4

CHANGELOG.md
# Changelog
## 3.0.0
- Add `[contenteditable]` elements.
## 2.0.0

@@ -4,0 +8,0 @@

136

index.js

@@ -10,4 +10,7 @@ var candidateSelectors = [

'video[controls]',
'[contenteditable]:not([contenteditable="false"])',
];
var matches = Element.prototype.matches || Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
module.exports = function tabbable(el, options) {

@@ -17,16 +20,13 @@ options = options || {};

var elementDocument = el.ownerDocument || el;
var basicTabbables = [];
var regularTabbables = [];
var orderedTabbables = [];
var isHiddenByCss = createIsHiddenByCssChecker(elementDocument);
var untouchabilityChecker = new UntouchabilityChecker(elementDocument);
var candidates = el.querySelectorAll(candidateSelectors.join(','));
if (options.includeContainer) {
var matches = Element.prototype.matches || Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
if (
candidateSelectors.some(function(candidateSelector) {
return matches.call(el, candidateSelector);
})
) {
var containerIsCandidate = candidateSelectors.some(function(selector) {
return matches.call(el, selector);
});
if (containerIsCandidate) {
candidates = Array.prototype.slice.apply(candidates);

@@ -37,14 +37,13 @@ candidates.unshift(el);

var i, candidate, candidateTabindexAttr, candidateTabindex;
var i, candidate, candidateTabindex;
for (i = 0; i < candidates.length; i++) {
candidate = candidates[i];
candidateTabindexAttr = parseInt(candidate.getAttribute('tabindex'), 10)
candidateTabindex = isNaN(candidateTabindexAttr) ? candidate.tabIndex : candidateTabindexAttr;
candidateTabindex = getTabindex(candidate);
if (
candidateTabindex < 0
|| candidate.disabled
|| isHiddenInput(candidate)
|| isNonTabbableRadio(candidate)
|| candidate.disabled
|| isHiddenByCss(candidate, elementDocument)
|| untouchabilityChecker.isUntouchable(candidate)
) {

@@ -55,3 +54,3 @@ continue;

if (candidateTabindex === 0) {
basicTabbables.push(candidate);
regularTabbables.push(candidate);
} else {

@@ -68,53 +67,15 @@ orderedTabbables.push({

.sort(sortOrderedTabbables)
.map(function(a) {
return a.node
});
.map(function(a) { return a.node })
.concat(regularTabbables);
Array.prototype.push.apply(tabbableNodes, basicTabbables);
return tabbableNodes;
}
function createIsHiddenByCssChecker(elementDocument) {
// Node cache must be refreshed on every check, in case
// the content of the element has changed. The cache contains tuples
// mapping nodes to their boolean result.
var isHiddenByCssCache = [];
// getComputedStyle accurately reflects `visiblity: "hidden"`
// in context but not `display: "none"`, so we need to recursively check parents.
function hasDisplayNone(node, nodeComputedStyle) {
if (node === elementDocument.documentElement) return false;
// Search for a cached result.
var cached = find(isHiddenByCssCache, function(item) {
return item === node;
});
if (cached) return cached[1];
nodeComputedStyle = nodeComputedStyle || elementDocument.defaultView.getComputedStyle(node);
var result = false;
if (nodeComputedStyle.display === 'none') {
result = true;
} else if (node.parentNode) {
result = hasDisplayNone(node.parentNode);
}
isHiddenByCssCache.push([node, result]);
return result;
}
return function isHiddenByCss(innerNode) {
if (innerNode === elementDocument.documentElement) return false;
var computedStyle = elementDocument.defaultView.getComputedStyle(innerNode);
if (hasDisplayNone(innerNode, computedStyle)) return true;
return computedStyle.visibility === 'hidden';
}
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;
return node.tabIndex;
}

@@ -126,3 +87,3 @@

// Array.prototype.find not available in IE
// Array.prototype.find not available in IE.
function find(list, predicate) {

@@ -134,2 +95,6 @@ for (var i = 0, length = list.length; i < length; i++) {

function isContentEditable(node) {
return node.contentEditable === "true";
}
function isInput(node) {

@@ -161,2 +126,4 @@ return node.tagName === 'INPUT';

if (!node.name) return true;
// This won't account for the edge case where you have radio groups with the same
// in separate forms on the same page.
var radioSet = node.ownerDocument.querySelectorAll('input[type="radio"][name="' + node.name + '"]');

@@ -166,1 +133,44 @@ var checked = getCheckedRadio(radioSet);

}
// An element is "untouchable" if *it or one of its ancestors* has
// `visibility: hidden` or `display: none`.
function UntouchabilityChecker(elementDocument) {
this.doc = elementDocument;
// Node cache must be refreshed on every check, in case
// the content of the element has changed. The cache contains tuples
// mapping nodes to their boolean result.
this.cache = [];
}
// getComputedStyle accurately reflects `visibility: hidden` of ancestors
// but not `display: none`, so we need to recursively check parents.
UntouchabilityChecker.prototype.hasDisplayNone = function hasDisplayNone(node, nodeComputedStyle) {
if (node === this.doc.documentElement) return false;
// Search for a cached result.
var cached = find(this.cache, function(item) {
return item === node;
});
if (cached) return cached[1];
nodeComputedStyle = nodeComputedStyle || this.doc.defaultView.getComputedStyle(node);
var result = false;
if (nodeComputedStyle.display === 'none') {
result = true;
} else if (node.parentNode) {
result = this.hasDisplayNone(node.parentNode);
}
this.cache.push([node, result]);
return result;
}
UntouchabilityChecker.prototype.isUntouchable = function isUntouchable(node) {
if (node === this.doc.documentElement) return false;
var computedStyle = this.doc.defaultView.getComputedStyle(node);
if (this.hasDisplayNone(node, computedStyle)) return true;
return computedStyle.visibility === 'hidden';
}
{
"name": "tabbable",
"version": "2.0.0",
"version": "3.0.0",
"description": "Returns an array of all tabbable DOM nodes within a containing node.",

@@ -5,0 +5,0 @@ "main": "index.js",

@@ -5,14 +5,15 @@ # tabbable

Returns an array of all\* tabbable DOM nodes within a containing node, in their actual tab order (cf. [Sequential focus navigation and the tabindex attribute](http://www.w3.org/TR/html5/editing.html#sequential-focus-navigation-and-the-tabindex-attribute)).
Returns an array of all\* tabbable DOM nodes within a containing node.
<small>\* "all" has some necessary caveats, which you'll learn about by reading below.</small>
(\* "all" has some necessary caveats, which you'll learn about by reading below.)
The array of tabbable nodes should include the following:
- `<button>`s,
- `<input>`s,
- `<select>`s,
- `<textarea>`s,
- `<a>`s and `<area>`s with `href` attributes,
- `<audio>`s and `<videos>`s with `controls` attributes,
- `<button>`s
- `<input>`s
- `<select>`s
- `<textarea>`s
- `<a>`s with `href` or `xlink:href` attributes
- `<audio>`s and `<videos>`s with `controls` attributes
- `[contenteditable]` elements
- anything with a non-negative `tabindex`

@@ -25,5 +26,5 @@

- either the node itself *or an ancestor of it* is hidden via `display: none` or `visibility: hidden`
- it's an `<input type="radio">` and a different radio in its group is `checked`.
- it's an `<input type="radio">` and a different radio in its group is `checked`
**If you think a node should be included in your array of tabbables *but it's not*, all you need to do is add `tabindex="0"` to deliberately include it.** This will also result in more consistent cross-browser behavior. For information about why your special node might *not* be included, see ["More details"](#more-details), below.
**If you think a node should be included in your array of tabbables *but it's not*, all you need to do is add `tabindex="0"` to deliberately include it.** (Or if it is in your array but you don't want it, you can add `tabindex="-1" to deliberately exclude it.) This will also result in more consistent cross-browser behavior. For information about why your special node might *not* be included, see ["More details"](#more-details), below.

@@ -80,8 +81,8 @@ ## Goals

- **Tabbable tries to identify elements that are reliably tabbable across (not dead) browsers.** Browsers are stupidly inconsistent in their behavior, though — especially for edge-case elements like `<object>` and `<iframe>` — so this means *some* elements that you *can* tab to in *some* browsers will be left out of the results. (To learn more about that stupid inconsistency, see this [amazing table](https://allyjs.io/data-tables/focusable.html)). To provide better consistency across browsers and ensure the elements you *want* in your tabbables list show up there, **try adding `tabindex="0"` to edge-case elements that Tabbable ignores**.
- (As an example of the above:) Although browsers allow tabbing into elements marked `contenteditable`, outstanding bugs in the `tabIndex` API prevents Tabbable from registering them. If you have `contenteditable` elements that you need included in the array, you'll have to additionally specify `tabindex="0"`. (See [issue #7](https://github.com/davidtheclark/tabbable/issues/7).)
- Although Tabbable tries to deal with positive tabindexes, **you should not use positive tabindexes**. Accessibility experts seem to be in (rare) unanimous and clear consent about this: rely on the order of elements in the document.
- (Exemplifying the above ^^:) **The tabbability of `<iframe>`s, `<embed>`s, `<object>`s, and `<svg>`s is [inconsistent across browsers](https://allyjs.io/data-tables/focusable.html), so if you need an accurate read on one of these elements you should give it a `tabindex`. (You'll also need to pay attention to the `focusable` attribute on SVGs in IE & Edge.)
- **Radio groups have some edge cases, which you can avoid by always having a `checked` one in each group** (and that is what you should usually do anyway). If there is no `checked` radio in the radio group, *all* of the radios will be considered tabbable. (Some browsers do this, otherwise don't — there's not consistency.)
- If you're thinking, "Why not just use the right `querySelectorAll`?", you *may* be on to something ... but, as with most "just" statements, you're probably not. For example, a simple `querySelectorAll` approach will not figure out whether an element is *hidden*, and therefore not actually tabbable. (That said, if you do think Tabbable can be simplified or otherwise improved, I'd love to hear your idea.)
- jQuery UI's `:tabbable` selector ignores elements with height and width of `0`. I'm not sure why — because I've found that I can still tab to those elements. So I kept them in. Only elements hidden with `display: none` or `visibility: hidden` are left out.
- **Radio groups have some edge cases, which you can avoid by always having a `checked` one in each group** (and that is what you should usually do anyway). If there is no `checked` radio in the radio group, *all* of the radios will be considered tabbable. (Some browsers do this, otherwise don't — there's not consistency.)
- Although Tabbable tries to deal with positive tabindexes, **you should not use positive tabindexes**. Accessibility experts seem to be in (rare) unanimous and clear consent about this: rely on the order of elements in the document.
***Feedback and contributions more than welcome!***
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc