Skip to content

Commit c665d0b

Browse files
0ddfell0wWilcoFiers
authored andcommitted
fix(commons/dom): fix isFocusable functions by checking screenreader (#658)
visibility and enabledness Adds tests for disabled/AT-hidden elements with tabindex fixes #647
1 parent 9e8a310 commit c665d0b

File tree

3 files changed

+149
-7
lines changed

3 files changed

+149
-7
lines changed

lib/commons/dom/is-focusable.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
/* global dom */
22
/* jshint maxcomplexity: 20 */
3+
4+
/**
5+
* Determines if focusing has been disabled on an element.
6+
* @param {HTMLElement} el The HTMLElement
7+
* @return {Boolean} Whether focusing has been disabled on an element.
8+
*/
9+
function focusDisabled(el) {
10+
return el.disabled ||
11+
(!dom.isVisible(el, true) && el.nodeName.toUpperCase() !== 'AREA');
12+
}
13+
314
/**
415
* Determines if an element is focusable
516
* @method isFocusable
@@ -11,8 +22,9 @@
1122

1223
dom.isFocusable = function (el) {
1324
'use strict';
14-
15-
if (dom.isNativelyFocusable(el)) {
25+
if (focusDisabled(el)) {
26+
return false;
27+
} else if (dom.isNativelyFocusable(el)) {
1628
return true;
1729
}
1830
// check if the tabindex is specified and a parseable number
@@ -36,9 +48,8 @@ dom.isFocusable = function (el) {
3648
dom.isNativelyFocusable = function(el) {
3749
'use strict';
3850

39-
if (!el ||
40-
el.disabled ||
41-
(!dom.isVisible(el) && el.nodeName.toUpperCase() !== 'AREA')) {
51+
if (!el || focusDisabled(el)) {
52+
4253
return false;
4354
}
4455

test/commons/dom/is-focusable.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,30 @@ describe('dom.isFocusable', function () {
5656

5757
});
5858

59+
it('should return false for hidden inputs with tabindex', function () {
60+
fixture.innerHTML = '<input type="hidden" tabindex="1" id="target">';
61+
var el = document.getElementById('target');
62+
63+
assert.isFalse(axe.commons.dom.isFocusable(el));
64+
65+
});
66+
67+
it('should return false for hidden buttons with tabindex', function () {
68+
fixture.innerHTML = '<button style="visibility:hidden" tabindex="0" id="target"></button>';
69+
var el = document.getElementById('target');
70+
71+
assert.isFalse(axe.commons.dom.isFocusable(el));
72+
73+
});
74+
75+
it('should return false for disabled buttons with tabindex', function () {
76+
fixture.innerHTML = '<button tabindex="0" id="target" disabled></button>';
77+
var el = document.getElementById('target');
78+
79+
assert.isFalse(axe.commons.dom.isFocusable(el));
80+
81+
});
82+
5983
it('should return false for non-visible elements', function () {
6084
fixture.innerHTML = '<input type="text" id="target" style="display: none">';
6185
var el = document.getElementById('target');

test/commons/dom/is-natively-focusable.js

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,21 @@ describe('dom.isNativelyFocusable', function () {
33

44
var fixture = document.getElementById('fixture');
55

6+
function hideByClipping (el) {
7+
el.style.cssText = 'position: absolute !important;' +
8+
' clip: rect(0px 0px 0px 0px); /* IE6, IE7 */' +
9+
' clip: rect(0px, 0px, 0px, 0px);';
10+
}
11+
12+
function hideByMovingOffScreen (el) {
13+
el.style.cssText = 'position:absolute;' +
14+
' left:-10000px;' +
15+
' top:auto;' +
16+
' width:1px;' +
17+
' height:1px;' +
18+
' overflow:hidden;';
19+
}
20+
621
afterEach(function () {
722
document.getElementById('fixture').innerHTML = '';
823
});
@@ -72,14 +87,106 @@ describe('dom.isNativelyFocusable', function () {
7287

7388
});
7489

75-
it('should return false for non-visible elements', function () {
76-
fixture.innerHTML = '<input type="text" id="target" style="display: none">';
90+
it('should return false for elements hidden with display:none', function () {
91+
fixture.innerHTML = '<button id="target" style="display: none">button</button>';
92+
var el = document.getElementById('target');
93+
94+
assert.isFalse(axe.commons.dom.isNativelyFocusable(el));
95+
96+
});
97+
98+
it('should return false for elements hidden with visibility:hidden', function () {
99+
fixture.innerHTML = '<button id="target" style="visibility: hidden">button</button>';
100+
var el = document.getElementById('target');
101+
102+
assert.isFalse(axe.commons.dom.isNativelyFocusable(el));
103+
104+
});
105+
106+
it('should return true for clipped elements', function () {
107+
fixture.innerHTML = '<button id="target">button</button>';
108+
var el = document.getElementById('target');
109+
hideByClipping(el);
110+
111+
assert.isTrue(axe.commons.dom.isNativelyFocusable(el));
112+
113+
});
114+
115+
it('should return true for elements positioned off screen', function () {
116+
fixture.innerHTML = '<button id="target">button</button>';
117+
var el = document.getElementById('target');
118+
hideByMovingOffScreen(el);
119+
120+
assert.isTrue(axe.commons.dom.isNativelyFocusable(el));
121+
122+
});
123+
124+
it('should return false for elements hidden with display:none on an ancestor', function () {
125+
fixture.innerHTML = '<div id="parent" style="display:none"><button id="target">button</button></div>';
126+
var el = document.getElementById('target');
127+
128+
assert.isFalse(axe.commons.dom.isNativelyFocusable(el));
129+
130+
});
131+
132+
it('should return false for elements hidden with visibility:hidden on an ancestor', function () {
133+
fixture.innerHTML = '<div id="parent" style="visibility: hidden"><button id="target">button</button></div>';
77134
var el = document.getElementById('target');
78135

79136
assert.isFalse(axe.commons.dom.isNativelyFocusable(el));
80137

81138
});
82139

140+
it('should return true for elements with a clipped ancestor', function () {
141+
fixture.innerHTML = '<div id="parent"><button id="target">button</button></div>';
142+
hideByClipping(document.getElementById('parent'));
143+
var el = document.getElementById('target');
144+
145+
assert.isTrue(axe.commons.dom.isNativelyFocusable(el));
146+
147+
});
148+
149+
it('should return true for elements off-screened by an ancestor', function () {
150+
fixture.innerHTML = '<div id="parent"><button id="target">button</button></div>';
151+
hideByMovingOffScreen(document.getElementById('parent'));
152+
var el = document.getElementById('target');
153+
154+
assert.isTrue(axe.commons.dom.isNativelyFocusable(el));
155+
156+
});
157+
158+
it('should return false for hidden inputs with tabindex', function () {
159+
fixture.innerHTML = '<input type="hidden" tabindex="1" id="target">';
160+
var el = document.getElementById('target');
161+
162+
assert.isFalse(axe.commons.dom.isFocusable(el));
163+
164+
});
165+
166+
it('should return false for disabled inputs with tabindex', function () {
167+
fixture.innerHTML = '<input tabindex="1" id="target" disabled>';
168+
var el = document.getElementById('target');
169+
170+
assert.isFalse(axe.commons.dom.isFocusable(el));
171+
172+
});
173+
174+
it('should return false for hidden buttons with tabindex', function () {
175+
fixture.innerHTML = '<button style="visibility:hidden" tabindex="0" id="target"></button>';
176+
var el = document.getElementById('target');
177+
178+
assert.isFalse(axe.commons.dom.isFocusable(el));
179+
180+
});
181+
182+
it('should return false for disabled buttons with tabindex', function () {
183+
fixture.innerHTML = '<button tabindex="0" id="target" disabled></button>';
184+
var el = document.getElementById('target');
185+
186+
assert.isFalse(axe.commons.dom.isFocusable(el));
187+
188+
});
189+
83190
it('should return true for an anchor with an href', function () {
84191
fixture.innerHTML = '<a href="something.html" id="target"></a>';
85192
var el = document.getElementById('target');

0 commit comments

Comments
 (0)