@@ -4,6 +4,7 @@ import _ from 'underscore';
4
4
import BaseView from 'oroui/js/app/views/base/view' ;
5
5
import routing from 'routing' ;
6
6
import template from 'tpl-loader!oroproduct/templates/search-autocomplete.html' ;
7
+ import 'jquery-ui/tabbable' ;
7
8
8
9
const SearchAutocompleteView = BaseView . extend ( {
9
10
optionNames : BaseView . prototype . optionNames . concat ( [
@@ -41,29 +42,62 @@ const SearchAutocompleteView = BaseView.extend({
41
42
events : {
42
43
change : '_onInputChange' ,
43
44
keyup : '_onInputChange' ,
44
- focus : '_onInputRefresh'
45
+ focus : '_onInputRefresh' ,
46
+ keydown : '_onKeyDown'
45
47
} ,
46
48
47
49
previousValue : '' ,
48
50
51
+ autocompleteItems : '[role="option"]' ,
52
+
49
53
/**
50
54
* @inheritdoc
51
55
*/
52
56
constructor : function SearchAutocompleteView ( options ) {
57
+ this . renderSuggestions = _ . debounce ( this . renderSuggestions . bind ( this ) , this . delay ) ;
53
58
SearchAutocompleteView . __super__ . constructor . call ( this , options ) ;
54
59
} ,
55
60
61
+ preinitialize ( ) {
62
+ this . comboboxId = `combobox-${ this . cid } ` ;
63
+ } ,
64
+
56
65
/**
57
66
* @inheritdoc
58
67
*/
59
68
initialize ( options ) {
60
- this . $el . attr ( 'autocomplete' , 'off' ) ;
69
+ this . $el . attr ( {
70
+ 'role' : 'combobox' ,
71
+ 'autocomplete' : 'off' ,
72
+ 'aria-haspopup' : true ,
73
+ 'aria-expanded' : false ,
74
+ 'aria-autocomplete' : 'list' ,
75
+ 'aria-controls' : this . comboboxId
76
+ } ) ;
61
77
62
- this . renderSuggestions = _ . debounce ( this . renderSuggestions . bind ( this ) , this . delay ) ;
78
+ SearchAutocompleteView . __super__ . initialize . call ( this , options ) ;
79
+ } ,
80
+
81
+ /**
82
+ * @inheritdoc
83
+ */
84
+ delegateEvents : function ( ) {
85
+ SearchAutocompleteView . __super__ . delegateEvents . call ( this ) ;
63
86
64
87
$ ( 'body' ) . on ( `click${ this . eventNamespace ( ) } ` , this . _onOutsideAction . bind ( this ) ) ;
65
88
66
- SearchAutocompleteView . __super__ . initialize . call ( this , options ) ;
89
+ return this ;
90
+ } ,
91
+
92
+ /**
93
+ * @inheritdoc
94
+ */
95
+ undelegateEvents : function ( ) {
96
+ SearchAutocompleteView . __super__ . undelegateEvents . call ( this ) ;
97
+
98
+ $ ( 'body' ) . off ( this . eventNamespace ( ) ) ;
99
+
100
+ return this ;
67
101
} ,
68
102
69
103
getInputString ( ) {
@@ -75,7 +109,8 @@ const SearchAutocompleteView = BaseView.extend({
75
109
*/
76
110
getTemplateData : function ( data = { } ) {
77
111
return Object . assign ( data , {
78
- inputString : this . getInputString ( )
112
+ inputString : this . getInputString ( ) ,
113
+ comboboxId : this . comboboxId
79
114
} ) ;
80
115
} ,
81
116
@@ -86,20 +121,109 @@ const SearchAutocompleteView = BaseView.extend({
86
121
if ( this . disposed ) {
87
122
return ;
88
123
}
89
- this . close ( ) ;
124
+ this . closeCombobox ( ) ;
90
125
91
- $ ( 'body' ) . off ( this . eventNamespace ( ) ) ;
126
+ this . $el . attr ( 'aria-expanded' , null ) ;
92
127
93
128
SearchAutocompleteView . __super__ . dispose . call ( this ) ;
94
129
} ,
95
130
96
- close ( ) {
131
+ closeCombobox ( ) {
97
132
if ( ! this . $popup ) {
98
133
return ;
99
134
}
100
135
101
136
this . $popup . remove ( ) ;
102
137
this . $popup = null ;
138
+ this . $el . attr ( {
139
+ 'aria-expanded' : false ,
140
+ 'aria-activedescendant' : null
141
+ } ) ;
142
+ this . undoFocusStyle ( ) ;
143
+ } ,
144
+
145
+ hideCombobox ( ) {
146
+ if ( ! this . $popup ) {
147
+ return ;
148
+ }
149
+
150
+ this . $el . attr ( {
151
+ 'aria-expanded' : false ,
152
+ 'aria-activedescendant' : null
153
+ } ) ;
154
+ this . $popup . hide ( ) ;
155
+ this . gerSelectedOption ( ) . removeAttr ( 'aria-selected' ) ;
156
+ this . undoFocusStyle ( ) ;
157
+ } ,
158
+
159
+ showCombobox ( ) {
160
+ if ( ! this . $popup ) {
161
+ return ;
162
+ }
163
+
164
+ this . $popup . show ( ) ;
165
+ } ,
166
+
167
+ hasSelectedOption ( ) {
168
+ return this . gerSelectedOption ( ) . length > 0 ;
169
+ } ,
170
+
171
+ getAutocompleteItems ( ) {
172
+ return this . $el . next ( ) . find ( this . autocompleteItems ) ;
173
+ } ,
174
+
175
+ gerSelectedOption ( ) {
176
+ return this . getAutocompleteItems ( ) . filter ( ( i , el ) => $ ( el ) . attr ( 'aria-selected' ) === 'true' ) ;
177
+ } ,
178
+
179
+ getNextOption ( ) {
180
+ const $options = this . getAutocompleteItems ( ) ;
181
+ const $activeOption = this . gerSelectedOption ( ) ;
182
+
183
+ if (
184
+ $activeOption . length === 0 ||
185
+ ( $options . length - 1 === $options . index ( $activeOption ) )
186
+ ) {
187
+ return $options . first ( ) ;
188
+ }
189
+
190
+ return $options . eq ( $options . index ( $activeOption ) + 1 ) ;
191
+ } ,
192
+
193
+ getPreviousOption ( ) {
194
+ const $options = this . getAutocompleteItems ( ) ;
195
+ const $activeOption = this . gerSelectedOption ( ) ;
196
+
197
+ if (
198
+ $activeOption . length === 0 ||
199
+ $options . index ( $activeOption ) === 0
200
+ ) {
201
+ return $options . last ( ) ;
202
+ }
203
+
204
+ return $options . eq ( $options . index ( $activeOption ) - 1 ) ;
205
+ } ,
206
+
207
+ /**
208
+ * @param {string } direction
209
+ */
210
+ goToOption ( direction = 'down' ) {
211
+ const $options = this . getAutocompleteItems ( ) ;
212
+ const $activeOption = direction === 'down'
213
+ ? this . getNextOption ( )
214
+ : this . getPreviousOption ( )
215
+ ;
216
+
217
+ this . showCombobox ( ) ;
218
+ $options . attr ( 'aria-selected' , false ) ;
219
+ $activeOption . attr ( 'aria-selected' , true ) ;
220
+ this . $el . attr ( 'aria-activedescendant' , $activeOption . attr ( 'id' ) ) ;
221
+ } ,
222
+
223
+ executeSelectedOption ( ) {
224
+ if ( this . hasSelectedOption ( ) ) {
225
+ this . gerSelectedOption ( ) . find ( ':first-child' ) [ 0 ] . click ( ) ;
226
+ }
103
227
} ,
104
228
105
229
_getSearchXHR ( inputString ) {
@@ -117,11 +241,19 @@ const SearchAutocompleteView = BaseView.extend({
117
241
* @inheritdoc
118
242
*/
119
243
render ( suggestions ) {
120
- this . close ( ) ;
244
+ this . closeCombobox ( ) ;
121
245
122
246
if ( this . getInputString ( ) . length ) {
123
247
this . $popup = $ ( this . template ( this . getTemplateData ( suggestions ) ) ) ;
124
248
this . $el . after ( this . $popup ) ;
249
+ this . $el . attr ( 'aria-expanded' , true ) ;
250
+
251
+ this . getAutocompleteItems ( ) . each ( ( i , el ) => {
252
+ $ ( el ) . attr ( {
253
+ 'id' : _ . uniqueId ( 'item-' ) ,
254
+ 'aria-selected' : false
255
+ } ) . find ( ':tabbable' ) . attr ( 'tabindex' , - 1 ) ;
256
+ } ) ;
125
257
}
126
258
127
259
return this ;
@@ -148,6 +280,35 @@ const SearchAutocompleteView = BaseView.extend({
148
280
;
149
281
} ,
150
282
283
+ _onKeyDown ( event ) {
284
+ switch ( event . key ) {
285
+ case 'Tab' :
286
+ case 'Escape' :
287
+ this . hideCombobox ( ) ;
288
+ break ;
289
+ case 'ArrowUp' :
290
+ event . preventDefault ( ) ;
291
+ this . goToOption ( 'up' ) ;
292
+ break ;
293
+ case 'ArrowDown' :
294
+ event . preventDefault ( ) ;
295
+ this . goToOption ( 'down' ) ;
296
+ break ;
297
+ case 'Enter' :
298
+ case ' ' :
299
+ this . executeSelectedOption ( ) ;
300
+ break ;
301
+ default :
302
+ break ;
303
+ }
304
+
305
+ this . undoFocusStyle ( ) ;
306
+ } ,
307
+
308
+ undoFocusStyle ( ) {
309
+ this . $el . toggleClass ( 'undo-focus' , this . hasSelectedOption ( ) ) ;
310
+ } ,
311
+
151
312
_onInputChange ( event ) {
152
313
const inputString = this . getInputString ( ) ;
153
314
if ( inputString === this . previousValue ) {
@@ -156,7 +317,7 @@ const SearchAutocompleteView = BaseView.extend({
156
317
157
318
this . _shouldShowPopup ( inputString )
158
319
? this . renderSuggestions ( inputString )
159
- : this . close ( ) ;
320
+ : this . closeCombobox ( ) ;
160
321
161
322
this . previousValue = inputString ;
162
323
} ,
@@ -166,16 +327,16 @@ const SearchAutocompleteView = BaseView.extend({
166
327
167
328
if ( ! inputString . length && this . searchXHR ) {
168
329
this . searchXHR . abort ( ) ;
169
- } ;
330
+ }
170
331
171
332
this . _shouldShowPopup ( inputString )
172
333
? this . renderSuggestions ( inputString )
173
- : this . close ( ) ;
334
+ : this . closeCombobox ( ) ;
174
335
} ,
175
336
176
337
_onOutsideAction ( event ) {
177
338
if ( ! ( ( event . target === this . el ) || ( this . $popup && $ . contains ( this . $popup [ 0 ] , event . target ) ) ) ) {
178
- this . close ( ) ;
339
+ this . closeCombobox ( ) ;
179
340
}
180
341
}
181
342
} ) ;
0 commit comments