Skip to content

Commit 8a4b69d

Browse files
jelbournkara
authored andcommitted
feat(a11y): initial interactivity checker (#1288)
* feat(a11y): initial interactivity checker * <div> is actually tabbable in IE11 and Edge. Go figure.
1 parent 6cade28 commit 8a4b69d

File tree

2 files changed

+417
-0
lines changed

2 files changed

+417
-0
lines changed
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import {InteractivityChecker} from './interactivity-checker';
2+
3+
describe('InteractivityChecker', () => {
4+
let testContainerElement: HTMLElement;
5+
let checker: InteractivityChecker;
6+
7+
beforeEach(() => {
8+
testContainerElement = document.createElement('div');
9+
document.body.appendChild(testContainerElement);
10+
11+
checker = new InteractivityChecker();
12+
});
13+
14+
afterEach(() => {
15+
document.body.removeChild(testContainerElement);
16+
testContainerElement.innerHTML = '';
17+
});
18+
19+
describe('isDisabled', () => {
20+
it('should return true for disabled elements', () => {
21+
let elements = createElements('input', 'textarea', 'select', 'button', 'md-checkbox');
22+
elements.forEach(el => el.setAttribute('disabled', ''));
23+
appendElements(elements);
24+
25+
elements.forEach(el => {
26+
expect(checker.isDisabled(el))
27+
.toBe(true, `Expected <${el.nodeName} disabled> to be disabled`);
28+
});
29+
});
30+
31+
it('should return false for elements without disabled', () => {
32+
let elements = createElements('input', 'textarea', 'select', 'button', 'md-checkbox');
33+
appendElements(elements);
34+
35+
elements.forEach(el => {
36+
expect(checker.isDisabled(el))
37+
.toBe(false, `Expected <${el.nodeName}> not to be disabled`);
38+
});
39+
});
40+
});
41+
42+
describe('isVisible', () => {
43+
it('should return false for a `display: none` element', () => {
44+
testContainerElement.innerHTML =
45+
`<input style="display: none;">`;
46+
let input = testContainerElement.querySelector('input') as HTMLElement;
47+
48+
expect(checker.isVisible(input))
49+
.toBe(false, 'Expected element with `display: none` to not be visible');
50+
});
51+
52+
it('should return false for the child of a `display: none` element', () => {
53+
testContainerElement.innerHTML =
54+
`<div style="display: none;">
55+
<input>
56+
</div>`;
57+
let input = testContainerElement.querySelector('input') as HTMLElement;
58+
59+
expect(checker.isVisible(input))
60+
.toBe(false, 'Expected element with `display: none` parent to not be visible');
61+
});
62+
63+
it('should return false for a `visibility: hidden` element', () => {
64+
testContainerElement.innerHTML =
65+
`<input style="visibility: hidden;">`;
66+
let input = testContainerElement.querySelector('input') as HTMLElement;
67+
68+
expect(checker.isVisible(input))
69+
.toBe(false, 'Expected element with `visibility: hidden` to not be visible');
70+
});
71+
72+
it('should return false for the child of a `visibility: hidden` element', () => {
73+
testContainerElement.innerHTML =
74+
`<div style="visibility: hidden;">
75+
<input>
76+
</div>`;
77+
let input = testContainerElement.querySelector('input') as HTMLElement;
78+
79+
expect(checker.isVisible(input))
80+
.toBe(false, 'Expected element with `visibility: hidden` parent to not be visible');
81+
});
82+
83+
it('should return true for an element with `visibility: hidden` ancestor and *closer* ' +
84+
'`visibility: visible` ancestor', () => {
85+
testContainerElement.innerHTML =
86+
`<div style="visibility: hidden;">
87+
<div style="visibility: visible;">
88+
<input>
89+
</div>
90+
</div>`;
91+
let input = testContainerElement.querySelector('input') as HTMLElement;
92+
93+
expect(checker.isVisible(input))
94+
.toBe(true, 'Expected element with `visibility: hidden` ancestor and closer ' +
95+
'`visibility: visible` ancestor to be visible');
96+
});
97+
98+
it('should return true for an element without visibility modifiers', () => {
99+
let input = document.createElement('input');
100+
testContainerElement.appendChild(input);
101+
102+
expect(checker.isVisible(input))
103+
.toBe(true, 'Expected element without visibility modifiers to be visible');
104+
});
105+
});
106+
107+
describe('isFocusable', () => {
108+
it('should return true for native form controls', () => {
109+
let elements = createElements('input', 'textarea', 'select', 'button');
110+
appendElements(elements);
111+
112+
elements.forEach(el => {
113+
expect(checker.isFocusable(el)).toBe(true, `Expected <${el.nodeName}> to be focusable`);
114+
});
115+
});
116+
117+
it('should return true for an anchor with an href', () => {
118+
let anchor = document.createElement('a');
119+
anchor.href = 'google.com';
120+
testContainerElement.appendChild(anchor);
121+
122+
expect(checker.isFocusable(anchor)).toBe(true, `Expected <a> with href to be focusable`);
123+
});
124+
125+
it('should return false for an anchor without an href', () => {
126+
let anchor = document.createElement('a');
127+
testContainerElement.appendChild(anchor);
128+
129+
expect(checker.isFocusable(anchor))
130+
.toBe(false, `Expected <a> without href not to be focusable`);
131+
});
132+
133+
it('should return false for disabled form controls', () => {
134+
let elements = createElements('input', 'textarea', 'select', 'button');
135+
elements.forEach(el => el.setAttribute('disabled', ''));
136+
appendElements(elements);
137+
138+
elements.forEach(el => {
139+
expect(checker.isFocusable(el))
140+
.toBe(false, `Expected <${el.nodeName} disabled> not to be focusable`);
141+
});
142+
});
143+
144+
it('should return false for a `display: none` element', () => {
145+
testContainerElement.innerHTML =
146+
`<input style="display: none;">`;
147+
let input = testContainerElement.querySelector('input') as HTMLElement;
148+
149+
expect(checker.isFocusable(input))
150+
.toBe(false, 'Expected element with `display: none` to not be visible');
151+
});
152+
153+
it('should return false for the child of a `display: none` element', () => {
154+
testContainerElement.innerHTML =
155+
`<div style="display: none;">
156+
<input>
157+
</div>`;
158+
let input = testContainerElement.querySelector('input') as HTMLElement;
159+
160+
expect(checker.isFocusable(input))
161+
.toBe(false, 'Expected element with `display: none` parent to not be visible');
162+
});
163+
164+
it('should return false for a `visibility: hidden` element', () => {
165+
testContainerElement.innerHTML =
166+
`<input style="visibility: hidden;">`;
167+
let input = testContainerElement.querySelector('input') as HTMLElement;
168+
169+
expect(checker.isFocusable(input))
170+
.toBe(false, 'Expected element with `visibility: hidden` not to be focusable');
171+
});
172+
173+
it('should return false for the child of a `visibility: hidden` element', () => {
174+
testContainerElement.innerHTML =
175+
`<div style="visibility: hidden;">
176+
<input>
177+
</div>`;
178+
let input = testContainerElement.querySelector('input') as HTMLElement;
179+
180+
expect(checker.isFocusable(input))
181+
.toBe(false, 'Expected element with `visibility: hidden` parent not to be focusable');
182+
});
183+
184+
it('should return true for an element with `visibility: hidden` ancestor and *closer* ' +
185+
'`visibility: visible` ancestor', () => {
186+
testContainerElement.innerHTML =
187+
`<div style="visibility: hidden;">
188+
<div style="visibility: visible;">
189+
<input>
190+
</div>
191+
</div>`;
192+
let input = testContainerElement.querySelector('input') as HTMLElement;
193+
194+
expect(checker.isFocusable(input))
195+
.toBe(true, 'Expected element with `visibility: hidden` ancestor and closer ' +
196+
'`visibility: visible` ancestor to be focusable');
197+
});
198+
199+
it('should return false for an element with an empty tabindex', () => {
200+
let element = document.createElement('div');
201+
element.setAttribute('tabindex', '');
202+
testContainerElement.appendChild(element);
203+
204+
expect(checker.isFocusable(element))
205+
.toBe(false, `Expected element with tabindex="" not to be focusable`);
206+
});
207+
208+
it('should return false for an element with a non-numeric tabindex', () => {
209+
let element = document.createElement('div');
210+
element.setAttribute('tabindex', 'abba');
211+
testContainerElement.appendChild(element);
212+
213+
expect(checker.isFocusable(element))
214+
.toBe(false, `Expected element with non-numeric tabindex not to be focusable`);
215+
});
216+
217+
it('should return true for an element with contenteditable', () => {
218+
let element = document.createElement('div');
219+
element.setAttribute('contenteditable', '');
220+
testContainerElement.appendChild(element);
221+
222+
expect(checker.isFocusable(element))
223+
.toBe(true, `Expected element with contenteditable to be focusable`);
224+
});
225+
226+
227+
it('should return false for inert div and span', () => {
228+
let elements = createElements('div', 'span');
229+
appendElements(elements);
230+
231+
elements.forEach(el => {
232+
expect(checker.isFocusable(el))
233+
.toBe(false, `Expected <${el.nodeName}> not to be focusable`);
234+
});
235+
});
236+
237+
it('should return true for div and span with tabindex == 0', () => {
238+
let elements = createElements('div', 'span');
239+
240+
elements.forEach(el => el.setAttribute('tabindex', '0'));
241+
appendElements(elements);
242+
243+
elements.forEach(el => {
244+
expect(checker.isFocusable(el))
245+
.toBe(true, `Expected <${el.nodeName} tabindex="0"> to be focusable`);
246+
});
247+
});
248+
});
249+
250+
describe('isTabbable', () => {
251+
it('should return true for native form controls and anchor without tabindex attribute', () => {
252+
let elements = createElements('input', 'textarea', 'select', 'button', 'a');
253+
appendElements(elements);
254+
255+
elements.forEach(el => {
256+
expect(checker.isTabbable(el)).toBe(true, `Expected <${el.nodeName}> to be tabbable`);
257+
});
258+
});
259+
260+
it('should return false for native form controls and anchor with tabindex == -1', () => {
261+
let elements = createElements('input', 'textarea', 'select', 'button', 'a');
262+
263+
elements.forEach(el => el.setAttribute('tabindex', '-1'));
264+
appendElements(elements);
265+
266+
elements.forEach(el => {
267+
expect(checker.isTabbable(el))
268+
.toBe(false, `Expected <${el.nodeName} tabindex="-1"> not to be tabbable`);
269+
});
270+
});
271+
272+
it('should return true for div and span with tabindex == 0', () => {
273+
let elements = createElements('div', 'span');
274+
275+
elements.forEach(el => el.setAttribute('tabindex', '0'));
276+
appendElements(elements);
277+
278+
elements.forEach(el => {
279+
expect(checker.isTabbable(el))
280+
.toBe(true, `Expected <${el.nodeName} tabindex="0"> to be tabbable`);
281+
});
282+
});
283+
});
284+
285+
/** Creates an array of elements with the given node names. */
286+
function createElements(...nodeNames: string[]) {
287+
return nodeNames.map(name => document.createElement(name));
288+
}
289+
290+
/** Appends elements to the testContainerElement. */
291+
function appendElements(elements: Element[]) {
292+
for (let e of elements) {
293+
testContainerElement.appendChild(e);
294+
}
295+
}
296+
});

0 commit comments

Comments
 (0)