1
+ /*
2
+ * This content is licensed according to the W3C Software License at
3
+ * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
4
+ * Heavily modified to web component by Zach Leatherman
5
+ */
6
+ class SevenMinuteTabs extends HTMLElement {
7
+ constructor ( ) {
8
+ super ( ) ;
9
+
10
+ this . tablist = this . querySelector ( '[role="tablist"]' ) ;
11
+ this . buttons = this . querySelectorAll ( '[role="tab"]' ) ;
12
+ this . panels = this . querySelectorAll ( '[role="tabpanel"]' ) ;
13
+ this . delay = this . determineDelay ( ) ;
14
+
15
+ if ( ! this . tablist || ! this . buttons . length || ! this . panels . length ) {
16
+ return ;
17
+ }
18
+
19
+ this . initButtons ( ) ;
20
+ this . initPanels ( ) ;
21
+ }
22
+
23
+ get keys ( ) {
24
+ return {
25
+ end : 35 ,
26
+ home : 36 ,
27
+ left : 37 ,
28
+ up : 38 ,
29
+ right : 39 ,
30
+ down : 40
31
+ } ;
32
+ }
33
+
34
+ // Add or substract depending on key pressed
35
+ get direction ( ) {
36
+ return {
37
+ 37 : - 1 ,
38
+ 38 : - 1 ,
39
+ 39 : 1 ,
40
+ 40 : 1
41
+ } ;
42
+ }
43
+
44
+ initButtons ( ) {
45
+ let count = 0 ;
46
+ for ( let button of this . buttons ) {
47
+ button . addEventListener ( 'click' , this . clickEventListener . bind ( this ) ) ;
48
+ button . addEventListener ( 'keydown' , this . keydownEventListener . bind ( this ) ) ;
49
+ button . addEventListener ( 'keyup' , this . keyupEventListener . bind ( this ) ) ;
50
+
51
+ button . index = count ++ ;
52
+ }
53
+ }
54
+
55
+ initPanels ( ) {
56
+ let selectedPanelId = this . querySelector ( '[role="tab"][aria-selected="true"]' ) . getAttribute ( "aria-controls" ) ;
57
+ for ( let panel of this . panels ) {
58
+ if ( panel . getAttribute ( "id" ) !== selectedPanelId ) {
59
+ panel . setAttribute ( "hidden" , "" ) ;
60
+ }
61
+ }
62
+ }
63
+
64
+ clickEventListener ( event ) {
65
+ let button = event . target ;
66
+ if ( button . tagName === "A" || button . tagName === "BUTTON" && button . getAttribute ( "type" ) === "submit" ) {
67
+ event . preventDefault ( ) ;
68
+ }
69
+
70
+ this . activateTab ( button , false ) ;
71
+ }
72
+
73
+ // Handle keydown on tabs
74
+ keydownEventListener ( event ) {
75
+ var key = event . keyCode ;
76
+
77
+ switch ( key ) {
78
+ case this . keys . end :
79
+ event . preventDefault ( ) ;
80
+ // Activate last tab
81
+ this . activateTab ( this . buttons [ this . buttons . length - 1 ] ) ;
82
+ break ;
83
+ case this . keys . home :
84
+ event . preventDefault ( ) ;
85
+ // Activate first tab
86
+ this . activateTab ( this . buttons [ 0 ] ) ;
87
+ break ;
88
+
89
+ // Up and down are in keydown
90
+ // because we need to prevent page scroll >:)
91
+ case this . keys . up :
92
+ case this . keys . down :
93
+ this . determineOrientation ( event ) ;
94
+ break ;
95
+ } ;
96
+ }
97
+
98
+ // Handle keyup on tabs
99
+ keyupEventListener ( event ) {
100
+ var key = event . keyCode ;
101
+
102
+ switch ( key ) {
103
+ case this . keys . left :
104
+ case this . keys . right :
105
+ this . determineOrientation ( event ) ;
106
+ break ;
107
+ } ;
108
+ }
109
+
110
+ // When a tablist’s aria-orientation is set to vertical,
111
+ // only up and down arrow should function.
112
+ // In all other cases only left and right arrow function.
113
+ determineOrientation ( event ) {
114
+ var key = event . keyCode ;
115
+ var vertical = this . tablist . getAttribute ( 'aria-orientation' ) == 'vertical' ;
116
+ var proceed = false ;
117
+
118
+ if ( vertical ) {
119
+ if ( key === this . keys . up || key === this . keys . down ) {
120
+ event . preventDefault ( ) ;
121
+ proceed = true ;
122
+ } ;
123
+ }
124
+ else {
125
+ if ( key === this . keys . left || key === this . keys . right ) {
126
+ proceed = true ;
127
+ } ;
128
+ } ;
129
+
130
+ if ( proceed ) {
131
+ this . switchTabOnArrowPress ( event ) ;
132
+ } ;
133
+ }
134
+
135
+ // Either focus the next, previous, first, or last tab
136
+ // depending on key pressed
137
+ switchTabOnArrowPress ( event ) {
138
+ var pressed = event . keyCode ;
139
+
140
+ for ( let button of this . buttons ) {
141
+ button . addEventListener ( 'focus' , this . focusEventHandler . bind ( this ) ) ;
142
+ } ;
143
+
144
+ if ( this . direction [ pressed ] ) {
145
+ var target = event . target ;
146
+ if ( target . index !== undefined ) {
147
+ if ( this . buttons [ target . index + this . direction [ pressed ] ] ) {
148
+ this . buttons [ target . index + this . direction [ pressed ] ] . focus ( ) ;
149
+ }
150
+ else if ( pressed === this . keys . left || pressed === this . keys . up ) {
151
+ this . focusLastTab ( ) ;
152
+ }
153
+ else if ( pressed === this . keys . right || pressed == this . keys . down ) {
154
+ this . focusFirstTab ( ) ;
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ // Activates any given tab panel
161
+ activateTab ( tab , setFocus ) {
162
+ if ( tab . getAttribute ( "role" ) !== "tab" ) {
163
+ tab = tab . closest ( '[role="tab"]' ) ;
164
+ }
165
+
166
+ setFocus = setFocus || true ;
167
+
168
+ // Deactivate all other tabs
169
+ this . deactivateTabs ( ) ;
170
+
171
+ // Remove tabindex attribute
172
+ tab . removeAttribute ( 'tabindex' ) ;
173
+
174
+ // Set the tab as selected
175
+ tab . setAttribute ( 'aria-selected' , 'true' ) ;
176
+
177
+ // Get the value of aria-controls (which is an ID)
178
+ var controls = tab . getAttribute ( 'aria-controls' ) ;
179
+
180
+ // Remove hidden attribute from tab panel to make it visible
181
+ document . getElementById ( controls ) . removeAttribute ( 'hidden' ) ;
182
+
183
+ // Set focus when required
184
+ if ( setFocus ) {
185
+ tab . focus ( ) ;
186
+ }
187
+ }
188
+
189
+ // Deactivate all tabs and tab panels
190
+ deactivateTabs ( ) {
191
+ for ( let button of this . buttons ) {
192
+ button . setAttribute ( 'tabindex' , '-1' ) ;
193
+ button . setAttribute ( 'aria-selected' , 'false' ) ;
194
+ button . removeEventListener ( 'focus' , this . focusEventHandler . bind ( this ) ) ;
195
+ }
196
+
197
+ for ( let panel of this . panels ) {
198
+ panel . setAttribute ( 'hidden' , 'hidden' ) ;
199
+ }
200
+ }
201
+
202
+ focusFirstTab ( ) {
203
+ this . buttons [ 0 ] . focus ( ) ;
204
+ }
205
+
206
+ focusLastTab ( ) {
207
+ this . buttons [ this . buttons . length - 1 ] . focus ( ) ;
208
+ }
209
+
210
+ // Determine whether there should be a delay
211
+ // when user navigates with the arrow keys
212
+ determineDelay ( ) {
213
+ var hasDelay = this . tablist . hasAttribute ( 'data-delay' ) ;
214
+ var delay = 0 ;
215
+
216
+ if ( hasDelay ) {
217
+ var delayValue = this . tablist . getAttribute ( 'data-delay' ) ;
218
+ if ( delayValue ) {
219
+ delay = delayValue ;
220
+ }
221
+ else {
222
+ // If no value is specified, default to 300ms
223
+ delay = 300 ;
224
+ } ;
225
+ } ;
226
+
227
+ return delay ;
228
+ }
229
+
230
+ focusEventHandler ( event ) {
231
+ var target = event . target ;
232
+
233
+ setTimeout ( this . checkTabFocus . bind ( this ) , this . delay , target ) ;
234
+ } ;
235
+
236
+ // Only activate tab on focus if it still has focus after the delay
237
+ checkTabFocus ( target ) {
238
+ let focused = document . activeElement ;
239
+
240
+ if ( target === focused ) {
241
+ this . activateTab ( target , false ) ;
242
+ }
243
+ }
244
+ }
245
+
246
+ window . customElements . define ( "seven-minute-tabs" , SevenMinuteTabs ) ;
0 commit comments