-
Notifications
You must be signed in to change notification settings - Fork 240
/
tab.ts
168 lines (140 loc) Β· 4.25 KB
/
tab.ts
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import {fireEvent} from '@testing-library/dom'
import {
getActiveElement,
FOCUSABLE_SELECTOR,
isVisible,
isDisabled,
} from './utils'
import {focus} from './focus'
import {blur} from './blur'
function getNextElement(
currentIndex: number,
shift: boolean,
elements: Element[],
focusTrap?: Document | Element,
) {
if (focusTrap === document && currentIndex === 0 && shift) {
return document.body
} else if (
focusTrap === document &&
currentIndex === elements.length - 1 &&
!shift
) {
return document.body
} else {
const nextIndex = shift ? currentIndex - 1 : currentIndex + 1
const defaultIndex = shift ? elements.length - 1 : 0
return elements[nextIndex] || elements[defaultIndex]
}
}
interface tabOptions {
shift?: boolean
focusTrap?: Document | Element
}
function tab({shift = false, focusTrap}: tabOptions = {}) {
const previousElement = getActiveElement(focusTrap?.ownerDocument ?? document)
if (!focusTrap) {
focusTrap = document
}
const focusableElements = focusTrap.querySelectorAll(FOCUSABLE_SELECTOR)
const enabledElements = Array.from(focusableElements).filter(
el =>
el === previousElement ||
(el.getAttribute('tabindex') !== '-1' &&
!isDisabled(el) &&
// Hidden elements are not tabable
isVisible(el)),
)
if (enabledElements.length === 0) return
const orderedElements = enabledElements
.map((el, idx) => ({el, idx}))
.sort((a, b) => {
// tabindex has no effect if the active element has tabindex="-1"
if (
previousElement &&
previousElement.getAttribute('tabindex') === '-1'
) {
return a.idx - b.idx
}
const tabIndexA = Number(a.el.getAttribute('tabindex'))
const tabIndexB = Number(b.el.getAttribute('tabindex'))
const diff = tabIndexA - tabIndexB
return diff === 0 ? a.idx - b.idx : diff
})
.map(({el}) => el)
// TODO: verify/remove type casts
const checkedRadio: Record<string, HTMLInputElement> = {}
let prunedElements: HTMLInputElement[] = []
orderedElements.forEach(currentElement => {
// For radio groups keep only the active radio
// If there is no active radio, keep only the checked radio
// If there is no checked radio, treat like everything else
const el = currentElement as HTMLInputElement
if (el.type === 'radio' && el.name) {
// If the active element is part of the group, add only that
const prev = previousElement as HTMLInputElement | null
if (prev && prev.type === el.type && prev.name === el.name) {
if (el === prev) {
prunedElements.push(el)
}
return
}
// If we stumble upon a checked radio, remove the others
if (el.checked) {
prunedElements = prunedElements.filter(
e => e.type !== el.type || e.name !== el.name,
)
prunedElements.push(el)
checkedRadio[el.name] = el
return
}
// If we already found the checked one, skip
if (typeof checkedRadio[el.name] !== 'undefined') {
return
}
}
prunedElements.push(el)
})
const index = prunedElements.findIndex(el => el === previousElement)
const nextElement = getNextElement(index, shift, prunedElements, focusTrap)
const shiftKeyInit = {
key: 'Shift',
keyCode: 16,
shiftKey: true,
}
const tabKeyInit = {
key: 'Tab',
keyCode: 9,
shiftKey: shift,
}
let continueToTab = true
// not sure how to make it so there's no previous element...
// istanbul ignore else
if (previousElement) {
// preventDefault on the shift key makes no difference
if (shift) fireEvent.keyDown(previousElement, {...shiftKeyInit})
continueToTab = fireEvent.keyDown(previousElement, {...tabKeyInit})
}
const keyUpTarget =
!continueToTab && previousElement ? previousElement : nextElement
if (continueToTab) {
if (nextElement === document.body) {
/* istanbul ignore else */
if (previousElement) {
blur(previousElement)
}
} else {
focus(nextElement)
}
}
fireEvent.keyUp(keyUpTarget, {...tabKeyInit})
if (shift) {
fireEvent.keyUp(keyUpTarget, {...shiftKeyInit, shiftKey: false})
}
}
export {tab}
/*
eslint
complexity: "off",
max-statements: "off",
*/