From c434e8434564c8648ed8ba103869a59a6d7d4c45 Mon Sep 17 00:00:00 2001 From: Conlan Craft Date: Wed, 7 Dec 2016 14:53:57 -0600 Subject: [PATCH] [fixed] Respect overflow css property when determining whether or not a tabbable node is hidden --- specs/Modal.helpers.spec.js | 110 ++++++++++++++++++++++++++++++++++++ specs/Modal.spec.js | 1 - specs/index.js | 2 + src/helpers/tabbable.js | 17 ++++-- 4 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 specs/Modal.helpers.spec.js diff --git a/specs/Modal.helpers.spec.js b/specs/Modal.helpers.spec.js new file mode 100644 index 00000000..50931619 --- /dev/null +++ b/specs/Modal.helpers.spec.js @@ -0,0 +1,110 @@ +/* eslint-env mocha */ +import "should"; +import tabbable from "../src/helpers/tabbable"; +import "sinon"; + +export default () => { + describe("tabbable", () => { + describe("without tabbable descendents", () => { + it("returns an empty array", () => { + const elem = document.createElement("div"); + tabbable(elem).should.deepEqual([]); + }); + }); + + describe("with tabbable descendents", () => { + let elem; + beforeEach(() => { + elem = document.createElement("div"); + document.body.appendChild(elem); + }); + + afterEach(() => { + document.body.removeChild(elem); + }); + + it("includes descendent tabbable inputs", () => { + const input = document.createElement("input"); + elem.appendChild(input); + tabbable(elem).should.containEql(input); + }); + + it("includes tabbable non-input elements", () => { + const div = document.createElement("div"); + div.tabIndex = 1; + elem.appendChild(div); + tabbable(elem).should.containEql(div); + }); + + it("includes links with an href", () => { + const a = document.createElement("a"); + a.href = "foobar"; + a.innerHTML = "link"; + elem.appendChild(a); + tabbable(elem).should.containEql(a); + }); + + it("excludes links without an href or a tabindex", () => { + const a = document.createElement("a"); + elem.appendChild(a); + tabbable(elem).should.not.containEql(a); + }); + + it("excludes descendent inputs if they are not tabbable", () => { + const input = document.createElement("input"); + input.tabIndex = -1; + elem.appendChild(input); + tabbable(elem).should.not.containEql(input); + }); + + it("excludes descendent inputs if they are disabled", () => { + const input = document.createElement("input"); + input.disabled = true; + elem.appendChild(input); + tabbable(elem).should.not.containEql(input); + }); + + it("excludes descendent inputs if they are not displayed", () => { + const input = document.createElement("input"); + input.style.display = "none"; + elem.appendChild(input); + tabbable(elem).should.not.containEql(input); + }); + + it("excludes descendent inputs with 0 width and height", () => { + const input = document.createElement("input"); + input.style.width = "0"; + input.style.height = "0"; + input.style.border = "0"; + input.style.padding = "0"; + elem.appendChild(input); + tabbable(elem).should.not.containEql(input); + }); + + it("excludes descendents with hidden parents", () => { + const input = document.createElement("input"); + elem.style.display = "none"; + elem.appendChild(input); + tabbable(elem).should.not.containEql(input); + }); + + it("excludes inputs with parents that have zero width and height", () => { + const input = document.createElement("input"); + elem.style.width = "0"; + elem.style.height = "0"; + elem.style.overflow = "hidden"; + elem.appendChild(input); + tabbable(elem).should.not.containEql(input); + }); + + it("includes inputs visible because of overflow == visible", () => { + const input = document.createElement("input"); + elem.style.width = "0"; + elem.style.height = "0"; + elem.style.overflow = "visible"; + elem.appendChild(input); + tabbable(elem).should.containEql(input); + }); + }); + }); +}; diff --git a/specs/Modal.spec.js b/specs/Modal.spec.js index 6aed4586..22ef069f 100644 --- a/specs/Modal.spec.js +++ b/specs/Modal.spec.js @@ -1,5 +1,4 @@ /* eslint-env mocha */ -import "should"; import should from "should"; import React, { Component } from "react"; import ReactDOM from "react-dom"; diff --git a/specs/index.js b/specs/index.js index f640547c..b9e03b91 100644 --- a/specs/index.js +++ b/specs/index.js @@ -3,7 +3,9 @@ import ModalState from "./Modal.spec"; import ModalEvents from "./Modal.events.spec"; import ModalStyle from "./Modal.style.spec"; +import ModalHelpers from "./Modal.helpers.spec"; describe("State", ModalState); describe("Style", ModalStyle); describe("Events", ModalEvents); +describe("Helpers", ModalHelpers); diff --git a/src/helpers/tabbable.js b/src/helpers/tabbable.js index dc3c50b4..e1aa3936 100644 --- a/src/helpers/tabbable.js +++ b/src/helpers/tabbable.js @@ -12,17 +12,24 @@ const tabbableNode = /input|select|textarea|button|object/; -function hidden(el) { - return ( - (el.offsetWidth <= 0 && el.offsetHeight <= 0) || el.style.display === "none" - ); +function hidesContents(element) { + const zeroSize = element.offsetWidth <= 0 && element.offsetHeight <= 0; + + // If the node is empty, this is good enough + if (zeroSize && !element.innerHTML) return true; + + // Otherwise we need to check some styles + const style = window.getComputedStyle(element); + return zeroSize + ? style.getPropertyValue("overflow") !== "visible" + : style.getPropertyValue("display") == "none"; } function visible(element) { let parentElement = element; while (parentElement) { if (parentElement === document.body) break; - if (hidden(parentElement)) return false; + if (hidesContents(parentElement)) return false; parentElement = parentElement.parentNode; } return true;