Skip to content

Commit 95ea920

Browse files
committed
[GLJS-1346] Add option to support browser focus
Introduce `useBrowserFocus` option, which preserves backward compatibility and gives a user access to the suggestion list with a keyboard with respecting the default browser focus behaviour.
1 parent 66c236f commit 95ea920

File tree

3 files changed

+124
-0
lines changed

3 files changed

+124
-0
lines changed

API.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ A geocoder component using the [Mapbox Geocoding API][74]
128128
* `options.routing` **[Boolean][80]** Specify whether to request additional metadata about the recommended navigation destination corresponding to the feature or not. Only applicable for address features. (optional, default `false`)
129129
* `options.worldview` **[String][76]** Filter results to geographic features whose characteristics are defined differently by audiences belonging to various regional, cultural, or political groups. (optional, default `"us"`)
130130
* `options.enableGeolocation` **[Boolean][80]** If `true` enable user geolocation feature. (optional, default `false`)
131+
* `options.useBrowserFocus` **[Boolean][80]** If `true`, the geocoder will use the browser's focus event to show suggestions. If `false`, it will only highlight active suggestions and Tab will not propagate to the suggestions list. (optional, default `false`)
131132
* `options.addressAccuracy` **(`"address"` | `"street"` | `"place"` | `"country"`)** The accuracy for the geolocation feature with which we define the address line to fill. The browser API returns the user's position with accuracy, and sometimes we can get the neighbor's address. To prevent receiving an incorrect address, you can reduce the accuracy of the definition. (optional, default `"street"`)
132133

133134
### Examples

debug/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ var coordinatesGeocoder = function(query) {
7575
var geocoder = new MapboxGeocoder({
7676
accessToken: mapboxgl.accessToken,
7777
trackProximity: true,
78+
useBrowserFocus: true,
7879
enableGeolocation: true,
7980
localGeocoder: function(query) {
8081
return coordinatesGeocoder(query);

lib/index.js

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ function getFooterNode() {
7575
* @param {Boolean} [options.routing=false] Specify whether to request additional metadata about the recommended navigation destination corresponding to the feature or not. Only applicable for address features.
7676
* @param {String} [options.worldview="us"] Filter results to geographic features whose characteristics are defined differently by audiences belonging to various regional, cultural, or political groups.
7777
* @param {Boolean} [options.enableGeolocation=false] If `true` enable user geolocation feature.
78+
* @param {Boolean} [options.useBrowserFocus=false] If `true`, the geocoder will use the browser's focus event to show suggestions. If `false`, it will only highlight active suggestions and Tab will not propagate to the suggestions list.
7879
* @param {('address'|'street'|'place'|'country')} [options.addressAccuracy="street"] The accuracy for the geolocation feature with which we define the address line to fill. The browser API returns the user's position with accuracy, and sometimes we can get the neighbor's address. To prevent receiving an incorrect address, you can reduce the accuracy of the definition.
7980
* @example
8081
* var geocoder = new MapboxGeocoder({ accessToken: mapboxgl.accessToken });
@@ -110,6 +111,7 @@ MapboxGeocoder.prototype = {
110111
clearOnBlur: false,
111112
enableGeolocation: false,
112113
addressAccuracy: 'street',
114+
useBrowserFocus: false,
113115
getItemValue: function(item) {
114116
return item.place_name
115117
},
@@ -211,6 +213,8 @@ MapboxGeocoder.prototype = {
211213
this._clear = this._clear.bind(this);
212214
this._clearOnBlur = this._clearOnBlur.bind(this);
213215
this._geolocateUser = this._geolocateUser.bind(this);
216+
this._onSuggestionItemFocus = this._onSuggestionItemFocus.bind(this);
217+
this._onSuggestionItemKeyDown = this._onSuggestionItemKeydown.bind(this);
214218

215219
var el = (this.container = document.createElement('div'));
216220
el.className = 'mapboxgl-ctrl-geocoder mapboxgl-ctrl';
@@ -277,6 +281,7 @@ MapboxGeocoder.prototype = {
277281
}
278282

279283
var typeahead = this._typeahead = new Typeahead(this._inputEl, [], {
284+
hideOnBlur: !this.options.useBrowserFocus,
280285
filter: false,
281286
minLength: this.options.minLength,
282287
limit: this.options.limit
@@ -285,12 +290,73 @@ MapboxGeocoder.prototype = {
285290
this.setRenderFunction(this.options.render);
286291
typeahead.getItemValue = this.options.getItemValue;
287292

293+
const handleKeyDownTypeahead = this._typeahead.handleKeyDown.bind(this._typeahead);
294+
const handleKeyUpTypeahead = this._typeahead.handleKeyUp.bind(this._typeahead);
295+
296+
this._typeahead.handleKeyUp
297+
298+
if (this.options.useBrowserFocus) {
299+
this._typeahead.handleKeyDown = function(e) {
300+
if (e.keyCode === 9 && !typeahead.list.isEmpty()) {
301+
return;
302+
}
303+
// Arrow down
304+
if (e.keyCode === 40) {
305+
this._typeahead.list.active = 0;
306+
this._typeahead.list.element.querySelectorAll('li').forEach(function(item) {
307+
item.classList.remove('active');
308+
});
309+
this._typeahead.list.element.querySelectorAll('li')[0].classList.add('active');
310+
this._typeahead.list.element.querySelectorAll('li')[0].focus();
311+
return;
312+
// Arrow up
313+
} else if (e.keyCode === 38) {
314+
this._typeahead.list.active = typeahead.list.items.length - 1;
315+
this._typeahead.list.element.querySelectorAll('li').forEach(function(item) {
316+
item.classList.remove('active');
317+
});
318+
this._typeahead.list.element.querySelectorAll('li')[this._typeahead.list.active].classList.add('active');
319+
this._typeahead.list.element.querySelectorAll('li')[this._typeahead.list.active].focus();
320+
return;
321+
}
322+
handleKeyDownTypeahead(e);
323+
}.bind(this);
324+
325+
this._typeahead.handleKeyUp = function(e) {
326+
if (e && e.keyCode === 16) {
327+
e.preventDefault();
328+
return;
329+
}
330+
331+
handleKeyUpTypeahead(e);
332+
}
333+
}
334+
288335
// Add support for footer.
289336
var parentDraw = typeahead.list.draw;
290337
var footerNode = this._footerNode = getFooterNode();
338+
var self = this;
291339
typeahead.list.draw = function() {
340+
if (self.options.useBrowserFocus) {
341+
typeahead.list.element.querySelectorAll('li').forEach(function(item) {
342+
item.removeEventListener('focus', self._onSuggestionItemFocus);
343+
item.removeEventListener('keydown', self._onSuggestionItemKeyDown);
344+
});
345+
}
292346
parentDraw.call(this);
293347

348+
if (self.options.useBrowserFocus) {
349+
typeahead.list.element.querySelectorAll('li').forEach(function(item, index) {
350+
if (index === 0) {
351+
item.focus();
352+
}
353+
item.setAttribute('data-index', index);
354+
item.tabIndex = 0;
355+
item.addEventListener('focus', this._onSuggestionItemFocus);
356+
item.addEventListener('keydown', this._onSuggestionItemKeyDown);
357+
}.bind(self));
358+
}
359+
294360
footerNode.addEventListener('mousedown', function() {
295361
this.selectingListItem = true;
296362
}.bind(this));
@@ -319,6 +385,62 @@ MapboxGeocoder.prototype = {
319385
return el;
320386
},
321387

388+
_onSuggestionItemKeydown(e) {
389+
const keyCode = e.keyCode;
390+
if (keyCode === 9) {
391+
return;
392+
}
393+
394+
if (keyCode === 13) {
395+
e.preventDefault();
396+
const activeItem = this._typeahead.list.element.querySelector('.active');
397+
if (activeItem) {
398+
const itemIndex = activeItem.getAttribute('data-index');
399+
const item = this._typeahead.list.items[itemIndex];
400+
if (item) {
401+
this._typeahead.value(item.original);
402+
this._typeahead.list.hide();
403+
}
404+
}
405+
} else if (keyCode === 38 || keyCode === 40) {
406+
const items = this._typeahead.list.element.querySelectorAll('li');
407+
if (items.length > 0) {
408+
if (keyCode === 38) { // Arrow up
409+
if (this._typeahead.list.active > 0) {
410+
e.preventDefault();
411+
this._typeahead.list.active--;
412+
} else {
413+
this._typeahead.el.focus();
414+
return;
415+
}
416+
} else if (keyCode === 40) { // Arrow down
417+
e.preventDefault();
418+
if (this._typeahead.list.active < items.length - 1) {
419+
this._typeahead.list.active++;
420+
} else {
421+
return;
422+
}
423+
}
424+
items.forEach(function(item) {
425+
item.classList.remove('active');
426+
});
427+
const activeItem = items[this._typeahead.list.active];
428+
if (activeItem) {
429+
activeItem.classList.add('active');
430+
activeItem.focus();
431+
}
432+
}
433+
}
434+
},
435+
436+
_onSuggestionItemFocus(e) {
437+
this._typeahead.list.active = e.target.getAttribute('data-index');
438+
this._typeahead.list.element.querySelectorAll('li').forEach(function(item) {
439+
item.classList.remove('active');
440+
});
441+
e.target.classList.add('active');
442+
},
443+
322444
_geolocateUser: function () {
323445
this._hideGeolocateButton();
324446
this._showLoadingIcon();

0 commit comments

Comments
 (0)