-
Notifications
You must be signed in to change notification settings - Fork 7
/
index.js
139 lines (114 loc) · 3.27 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
const state = new Map()
const isRtl = window.getComputedStyle(document.documentElement).direction === 'rtl'
const KEYCODE = {
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
}
// when container or children get focus
const onFocusin = e => {
const {currentTarget: rover} = e
if (state.get('last_rover') == rover) return
if (state.has(rover)) {
activate(rover, state.get(rover).active)
state.set('last_rover', rover)
}
}
const onKeydown = e => {
const {currentTarget: rover} = e
switch (e.keyCode) {
case KEYCODE[isRtl ? 'LEFT' : 'RIGHT']:
case KEYCODE.DOWN:
e.preventDefault()
focusNextItem(rover)
break
case KEYCODE[isRtl ? 'RIGHT' : 'LEFT']:
case KEYCODE.UP:
e.preventDefault()
focusPreviousItem(rover)
break
}
}
const mo = new MutationObserver((mutationList, observer) => {
mutationList
.filter(x => x.removedNodes.length > 0)
.forEach(mutation => {
[...mutation.removedNodes]
.filter(x => x.nodeType === 1)
.forEach(removedEl => {
state.forEach((val,key) => {
if (key ==='last_rover') return
if (removedEl.contains(key)) {
key.removeEventListener('focusin', onFocusin)
key.removeEventListener('keydown', onKeydown)
state.delete(key)
val.targets.forEach(a => a.tabIndex = '')
if (state.size === 0 || (state.size === 1 && state.has('last_rover'))) {
state.clear()
mo.disconnect()
}
}
})
})
})
})
export const rovingIndex = ({element:rover, target:selector}) => {
// this api allows empty or a query string
const target_query = selector || ':scope *'
const targets = rover.querySelectorAll(target_query)
const startingPoint = targets[0]
// take container out of the focus flow
rover.tabIndex = -1
// and all the children
targets.forEach(a => a.tabIndex = -1)
// except the first target, that accepts focus
startingPoint.tabIndex = 0
// with the roving container as the key
// save some state and handy references
state.set(rover, {
targets,
active: startingPoint,
index: 0,
})
rover.addEventListener('focusin', onFocusin)
// watch for arrow keys
rover.addEventListener('keydown', onKeydown)
mo.observe(document, {
childList: true,
subtree: true
})
}
const focusNextItem = rover => {
const rx = state.get(rover)
// increment state index
rx.index += 1
// clamp navigation to target bounds
if (rx.index > rx.targets.length - 1)
rx.index = rx.targets.length - 1
// use rover index state to find next
let next = rx.targets[rx.index]
// found something, activate it
next && activate(rover, next)
}
const focusPreviousItem = rover => {
const rx = state.get(rover)
// decrement from the state index
rx.index -= 1
// clamp to 0 and above only
if (rx.index < 1)
rx.index = 0
// use rover index state to find next
let prev = rx.targets[rx.index]
// found something, activate it
prev && activate(rover, prev)
}
const activate = (rover, item) => {
const rx = state.get(rover)
// remove old tab index item
rx.active.tabIndex = -1
// set new active item and focus it
rx.active = item
rx.active.tabIndex = 0
rx.active.focus()
}