@@ -7,43 +7,105 @@ export default class Dropdown extends React.Component {
7
7
active : false
8
8
} ;
9
9
10
+ componentDidMount ( ) {
11
+ document . addEventListener ( 'keyup' , this . _closeDropdownOnEsc . bind ( this ) , true ) ;
12
+ document . addEventListener ( 'focus' , this . _closeDropdownIfFocusLost . bind ( this ) , true ) ;
13
+ document . addEventListener ( 'click' , this . _closeDropdownIfFocusLost . bind ( this ) , true ) ;
14
+ }
15
+
16
+ _closeDropdownOnEsc ( e ) {
17
+ if ( e . key === "Escape" && this . state . active ) {
18
+ this . setState ( { active : false } , ( ) => {
19
+ this . dropdownButton . focus ( ) ;
20
+ } ) ;
21
+ }
22
+ }
23
+
24
+ _closeDropdownIfFocusLost ( e ) {
25
+ if ( this . state . active && ! this . dropdown . contains ( e . target ) ) {
26
+ this . setState ( { active : false } ) ;
27
+ }
28
+ }
29
+
10
30
render ( ) {
11
31
let { className = '' , items = [ ] } = this . props ;
12
32
let activeMod = this . state . active ? "dropdown__list--active" : "" ;
13
33
14
34
return (
15
- < div
16
- tabIndex = "0"
35
+ < nav
17
36
className = { `dropdown ${ className } ` }
37
+ ref = { el => this . dropdown = el }
18
38
onMouseOver = { this . _toggle . bind ( this , true ) }
19
- onMouseLeave = { this . _toggle . bind ( this , false ) } >
20
- < img
21
- className = "dropdown__language"
22
- alt = "select language"
23
- src = { LanguageIcon } />
24
- { /* Commented out until media breakpoints are in place
25
- <span>{ items[0].title }</span> */ }
26
- < i aria-hidden = "true" className = "dropdown__arrow" />
27
-
39
+ onMouseLeave = { this . _toggle . bind ( this , false ) }
40
+ >
41
+ < button
42
+ ref = { el => this . dropdownButton = el }
43
+ aria-haspopup = "true"
44
+ aria-expanded = { String ( this . state . active ) }
45
+ aria-label = "Select language"
46
+ onClick = { this . _handleClick . bind ( this ) }
47
+ >
48
+ < img
49
+ className = "dropdown__language"
50
+ alt = "select language"
51
+ src = { LanguageIcon } />
52
+ { /* Commented out until media breakpoints are in place
53
+ <span>{ items[0].title }</span> */ }
54
+ < i aria-hidden = "true" className = "dropdown__arrow" />
55
+ </ button >
28
56
< div className = { `dropdown__list ${ activeMod } ` } >
29
57
< ul >
30
58
{
31
- items . map ( item => {
59
+ items . map ( ( item , i ) => {
32
60
return (
33
61
< li key = { item . title } >
34
- < a href = { item . url } >
35
- < span > { item . title } </ span >
62
+ < a
63
+ onKeyDown = { this . _handleArrowKeys . bind ( this , i , items . length - 1 ) }
64
+ ref = { node => this . links ? this . links . push ( node ) : this . links = [ node ] }
65
+ href = { item . url } >
66
+ < span lang = { item . lang } > { item . title } </ span >
36
67
</ a >
37
68
</ li >
38
69
) ;
39
70
} )
40
71
}
41
72
</ ul >
42
73
</ div >
43
- </ div >
74
+ </ nav >
44
75
) ;
45
76
}
46
77
78
+ _handleArrowKeys ( currentIndex , lastIndex , e ) {
79
+ if ( [ "ArrowDown" , "ArrowUp" ] . includes ( e . key ) ) {
80
+ e . preventDefault ( ) ;
81
+ }
82
+
83
+ let newIndex = currentIndex ;
84
+ if ( e . key === "ArrowDown" ) {
85
+ newIndex ++ ;
86
+ if ( newIndex > lastIndex ) {
87
+ newIndex = 0 ;
88
+ }
89
+ }
90
+
91
+ if ( e . key === "ArrowUp" ) {
92
+ newIndex -- ;
93
+ if ( newIndex < 0 ) {
94
+ newIndex = lastIndex ;
95
+ }
96
+ }
97
+
98
+ this . links [ newIndex ] . focus ( ) ;
99
+ }
100
+
101
+ _handleClick ( e ) {
102
+ this . setState ( { active : ! this . state . active } , ( ) => {
103
+ if ( this . state . active ) {
104
+ this . links [ 0 ] . focus ( ) ;
105
+ }
106
+ } ) ;
107
+ }
108
+
47
109
/**
48
110
* Toggle visibility of dropdown items
49
111
*
0 commit comments