Skip to content

Commit c77e92d

Browse files
committed
Rework APIs
- `install()` has been renamed to `start()` - `uninstall()` has been renamed `stop()` - Newly added `install()` sets require ARIA attributes - Newly added `uninstall()` removes ARIA attributes
1 parent 661ced7 commit c77e92d

File tree

3 files changed

+85
-31
lines changed

3 files changed

+85
-31
lines changed

examples/index.html

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
<form>
1212
<label>
1313
Least favorite robot
14-
<input aria-controls="list-id" role="combobox" type="text" aria-expanded="true">
14+
<input aria-controls="list-id" role="combobox" type="text">
1515
</label>
16-
<ul role="listbox" id="list-id">
16+
<ul role="listbox" id="list-id" hidden>
1717
<li id="baymax" role="option">Baymax</li>
1818
<li id="bb-8" role="option" aria-disabled="true"><del>BB-8</del></li>
1919
<li id="hubot" role="option">Hubot</li>
@@ -23,7 +23,25 @@
2323
</form>
2424
<pre class="events"></pre>
2525
<script type="text/javascript">
26-
comboboxNav.install(document.querySelector('input'), document.querySelector('ul'))
26+
const input = document.querySelector('input')
27+
const list = document.querySelector('ul')
28+
comboboxNav.install(input, list)
29+
30+
function toggleList() {
31+
const hidden = input.value.length === 0
32+
if (hidden) {
33+
comboboxNav.stop(input)
34+
} else {
35+
comboboxNav.start(input)
36+
}
37+
list.hidden = hidden
38+
}
39+
input.addEventListener('input', toggleList)
40+
input.addEventListener('focus', toggleList)
41+
input.addEventListener('blur', () => {
42+
list.hidden = true
43+
comboboxNav.stop(input)
44+
})
2745

2846
const events = document.querySelector('.events')
2947
document.addEventListener('combobox-commit', function(event) {

src/combobox-nav.js

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,70 @@
22

33
import {scrollTo} from './scroll'
44

5+
const comboboxStates = new WeakMap()
6+
57
export function install(input: HTMLTextAreaElement | HTMLInputElement, list: HTMLElement): void {
8+
if (comboboxStates.get(input)) {
9+
uninstall(input)
10+
}
11+
612
if (!list.id) {
713
list.id = `combobox-${Math.random()
814
.toString()
915
.slice(2, 6)}`
1016
}
17+
1118
input.setAttribute('role', 'combobox')
1219
input.setAttribute('aria-controls', list.id)
13-
if (!input.hasAttribute('aria-expanded')) input.setAttribute('aria-expanded', 'false')
20+
input.setAttribute('aria-expanded', 'false')
21+
input.setAttribute('aria-autocomplete', 'list')
22+
comboboxStates.set(input, {list, isComposing: false})
23+
}
24+
25+
export function uninstall(input: HTMLTextAreaElement | HTMLInputElement): void {
26+
const {list} = comboboxStates.get(input) || {}
27+
if (!list) return
28+
clearSelection(input, list)
29+
stop(input)
30+
31+
input.removeAttribute('role')
32+
input.removeAttribute('aria-controls')
33+
input.removeAttribute('aria-expanded')
34+
input.removeAttribute('aria-autocomplete')
35+
comboboxStates.delete(input)
36+
}
37+
38+
export function start(input: HTMLTextAreaElement | HTMLInputElement): void {
39+
const {list} = comboboxStates.get(input) || {}
40+
if (!list) return
41+
42+
input.setAttribute('aria-expanded', 'true')
1443
input.addEventListener('compositionstart', trackComposition)
1544
input.addEventListener('compositionend', trackComposition)
1645
input.addEventListener('keydown', keyboardBindings)
1746
list.addEventListener('click', commitWithElement)
1847
}
1948

20-
export function uninstall(input: HTMLTextAreaElement | HTMLInputElement, list: HTMLElement): void {
49+
export function stop(input: HTMLTextAreaElement | HTMLInputElement): void {
50+
const {list} = comboboxStates.get(input) || {}
51+
if (!list) return
52+
2153
input.removeAttribute('aria-activedescendant')
22-
input.removeAttribute('role')
23-
input.removeAttribute('aria-controls')
24-
input.removeAttribute('aria-expanded')
54+
input.setAttribute('aria-expanded', 'false')
2555
input.removeEventListener('compositionstart', trackComposition)
2656
input.removeEventListener('compositionend', trackComposition)
2757
input.removeEventListener('keydown', keyboardBindings)
2858
list.removeEventListener('click', commitWithElement)
2959
}
3060

31-
let isComposing = false
3261
const ctrlBindings = !!navigator.userAgent.match(/Macintosh/)
3362

3463
function keyboardBindings(event: KeyboardEvent) {
3564
if (event.shiftKey || event.metaKey || event.altKey) return
3665
const input = event.currentTarget
3766
if (!(input instanceof HTMLTextAreaElement || input instanceof HTMLInputElement)) return
38-
if (isComposing) return
39-
const list = document.getElementById(input.getAttribute('aria-controls') || '')
40-
if (!list) return
67+
const {list, isComposing} = comboboxStates.get(input) || {}
68+
if (!list || isComposing) return
4169

4270
switch (event.key) {
4371
case 'Enter':
@@ -137,7 +165,9 @@ export function clearSelection(input: HTMLTextAreaElement | HTMLInputElement, li
137165
function trackComposition(event: Event): void {
138166
const input = event.currentTarget
139167
if (!(input instanceof HTMLTextAreaElement || input instanceof HTMLInputElement)) return
140-
isComposing = event.type === 'compositionstart'
168+
const state = comboboxStates.get(input)
169+
if (!state) return
170+
state.isComposing = event.type === 'compositionstart'
141171

142172
const list = document.getElementById(input.getAttribute('aria-controls') || '')
143173
if (!list) return

test/test.js

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ function click(element) {
88

99
describe('combobox-nav', function() {
1010
describe('with API', function() {
11+
let input, list
1112
beforeEach(function() {
1213
document.body.innerHTML = `
1314
<input type="text">
@@ -18,34 +19,45 @@ describe('combobox-nav', function() {
1819
<li id="r2-d2" role="option">R2-D2</li>
1920
</ul>
2021
`
22+
input = document.querySelector('input')
23+
list = document.querySelector('ul')
2124
})
2225

2326
afterEach(function() {
2427
document.body.innerHTML = ''
2528
})
2629

27-
it('installs, navigates, and uninstalls', function() {
28-
const input = document.querySelector('input')
29-
const list = document.querySelector('ul')
30+
it('installs, starts, navigates, stops, and uninstalls', function() {
3031
comboboxNav.install(input, list)
31-
3232
assert.equal(input.getAttribute('role'), 'combobox')
3333
assert.equal(input.getAttribute('aria-expanded'), 'false')
3434
assert.equal(input.getAttribute('aria-controls'), 'list-id')
35+
assert.equal(input.getAttribute('aria-autocomplete'), 'list')
36+
37+
comboboxNav.start(input)
38+
assert.equal(input.getAttribute('aria-expanded'), 'true')
3539

3640
press(input, 'ArrowDown')
3741
assert.equal(list.children[0].getAttribute('aria-selected'), 'true')
3842
comboboxNav.navigate(input, list, 1)
3943
assert.equal(list.children[2].getAttribute('aria-selected'), 'true')
4044

41-
comboboxNav.uninstall(input, list)
42-
45+
comboboxNav.stop(input)
4346
press(input, 'ArrowDown')
4447
assert.equal(list.children[2].getAttribute('aria-selected'), 'true')
48+
49+
comboboxNav.uninstall(input)
50+
assert.equal(list.children[2].getAttribute('aria-selected'), 'false')
51+
52+
assert(!input.hasAttribute('role'))
53+
assert(!input.hasAttribute('aria-expanded'))
54+
assert(!input.hasAttribute('aria-controls'))
55+
assert(!input.hasAttribute('aria-autocomplete'))
4556
})
4657
})
4758

4859
describe('with default setup', function() {
60+
let input, list, options
4961
beforeEach(function() {
5062
document.body.innerHTML = `
5163
<input type="text">
@@ -59,17 +71,19 @@ describe('combobox-nav', function() {
5971
<li><a href="#wall-e" role="option">Wall-E</a></li>
6072
</ul>
6173
`
62-
comboboxNav.install(document.querySelector('input'), document.querySelector('ul'))
74+
input = document.querySelector('input')
75+
list = document.querySelector('ul')
76+
options = document.querySelectorAll('li')
77+
comboboxNav.install(input, list)
78+
comboboxNav.start(input)
6379
})
6480

6581
afterEach(function() {
66-
comboboxNav.uninstall(document.querySelector('input'), document.querySelector('ul'))
82+
comboboxNav.uninstall(document.querySelector('input'))
6783
document.body.innerHTML = ''
6884
})
6985

7086
it('updates attributes on keyboard events', function() {
71-
const input = document.querySelector('input')
72-
const options = document.querySelectorAll('li')
7387
const expectedTargets = []
7488

7589
document.addEventListener('combobox-commit', function({target}) {
@@ -111,7 +125,6 @@ describe('combobox-nav', function() {
111125
})
112126

113127
it('fires commit events on click', function() {
114-
const options = document.querySelectorAll('li')
115128
const expectedTargets = []
116129

117130
document.addEventListener('combobox-commit', function({target}) {
@@ -139,10 +152,6 @@ describe('combobox-nav', function() {
139152
})
140153

141154
it('clears aria-activedescendant and sets aria-selected=false when cleared', function() {
142-
const input = document.querySelector('input')
143-
const list = document.querySelector('ul')
144-
const options = document.querySelectorAll('li')
145-
146155
press(input, 'ArrowDown')
147156
assert.equal(options[0].getAttribute('aria-selected'), 'true')
148157
assert.equal(input.getAttribute('aria-activedescendant'), 'baymax')
@@ -154,12 +163,9 @@ describe('combobox-nav', function() {
154163
})
155164

156165
it('scrolls when the selected item is not in view', function() {
157-
const input = document.querySelector('input')
158-
const list = document.querySelector('ul')
159166
list.style.overflow = 'auto'
160167
list.style.height = '18px'
161168
list.style.position = 'relative'
162-
const options = document.querySelectorAll('li')
163169
assert.equal(list.scrollTop, 0)
164170

165171
press(input, 'ArrowDown')

0 commit comments

Comments
 (0)