Skip to content

Commit ff9ab7a

Browse files
authored
[GLJS-1346] Add option to support browser focus (#538)
Introduce `useBrowserFocus` option, which preserves backward compatibility and gives a user access to the suggestion list with a keyboard with respect to the default browser focus behaviour.
1 parent 66c236f commit ff9ab7a

File tree

11 files changed

+13938
-9618
lines changed

11 files changed

+13938
-9618
lines changed

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
- [ ] briefly describe the changes in this PR
44
- [ ] write tests for all new functionality
55
- [ ] run `npm run docs` and commit changes to API.md
6-
- [ ] update CHANGELOG.md with changes under `master` heading before merging
6+
- [ ] update CHANGELOG.md with changes under `main` heading before merging

.github/workflows/main.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
on:
2+
push:
3+
branches: [ "main" ]
4+
pull_request:
5+
branches: [ "main" ]
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
timeout-minutes: 5
11+
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Setup Node.js
16+
uses: actions/setup-node@v4
17+
with:
18+
node-version: 22.x
19+
cache: 'npm'
20+
21+
- uses: actions/cache@v4
22+
with:
23+
path: |
24+
~/.npm
25+
node_modules
26+
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
27+
restore-keys: |
28+
${{ runner.os }}-npm-
29+
30+
- name: Install dependencies
31+
run: npm ci
32+
33+
- name: Install Firefox
34+
run: |
35+
sudo apt-get update
36+
sudo apt-get install -y firefox
37+
38+
- name: Run tests with Smokestack and Firefox
39+
run: |
40+
export DISPLAY=:99
41+
Xvfb :99 -screen 0 1024x768x24 2> /dev/null &
42+
export MapboxAccessToken=${{ secrets.MAPBOX_ACCESS_TOKEN }}
43+
npm test
44+
env:
45+
MAPBOX_ACCESS_TOKEN: ${{ secrets.MAPBOX_ACCESS_TOKEN }}

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"editor.formatOnSave": false,
33
"editor.codeActionsOnSave": {
4-
"source.fixAll.eslint": false
4+
"source.fixAll.eslint": "never"
55
}
66
}

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

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## HEAD
22

3+
- Introduce `useBrowserFocus` option to use the browser's native focus management instead of the geocoder's custom focus management. This is useful for accessibility.
4+
35
## 5.0.3
46

57
### Features / Improvements 🚀

debug/index.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ mapDiv.style.bottom = 0;
2727

2828
var map = new mapboxgl.Map({
2929
container: mapDiv,
30-
// update to Standard after fix of GLJS-624
31-
style: 'mapbox://styles/mapbox/streets-v12',
3230
center: [-79.4512, 43.6568],
3331
zoom: 13
3432
});
@@ -75,6 +73,7 @@ var coordinatesGeocoder = function(query) {
7573
var geocoder = new MapboxGeocoder({
7674
accessToken: mapboxgl.accessToken,
7775
trackProximity: true,
76+
useBrowserFocus: true,
7877
enableGeolocation: true,
7978
localGeocoder: function(query) {
8079
return coordinatesGeocoder(query);

lib/index.js

Lines changed: 131 additions & 1 deletion
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';
@@ -261,7 +265,6 @@ MapboxGeocoder.prototype = {
261265

262266
el.appendChild(searchIcon);
263267
el.appendChild(this._inputEl);
264-
el.appendChild(actions);
265268

266269
if (this.options.enableGeolocation && this.geolocation.isSupport()) {
267270
this._geolocateEl = document.createElement('button');
@@ -277,20 +280,85 @@ MapboxGeocoder.prototype = {
277280
}
278281

279282
var typeahead = this._typeahead = new Typeahead(this._inputEl, [], {
283+
hideOnBlur: !this.options.useBrowserFocus,
280284
filter: false,
281285
minLength: this.options.minLength,
282286
limit: this.options.limit
283287
});
284288

289+
// To preserve tab navigation order
290+
el.insertBefore(actions, typeahead.list.wrapper);
291+
285292
this.setRenderFunction(this.options.render);
286293
typeahead.getItemValue = this.options.getItemValue;
287294

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

350+
if (self.options.useBrowserFocus) {
351+
typeahead.list.element.querySelectorAll('li').forEach(function(item, index) {
352+
if (index === 0) {
353+
item.focus();
354+
}
355+
item.setAttribute('data-index', index);
356+
item.tabIndex = 0;
357+
item.addEventListener('focus', this._onSuggestionItemFocus);
358+
item.addEventListener('keydown', this._onSuggestionItemKeyDown);
359+
}.bind(self));
360+
}
361+
294362
footerNode.addEventListener('mousedown', function() {
295363
this.selectingListItem = true;
296364
}.bind(this));
@@ -319,6 +387,62 @@ MapboxGeocoder.prototype = {
319387
return el;
320388
},
321389

390+
_onSuggestionItemKeydown(e) {
391+
const keyCode = e.keyCode;
392+
if (keyCode === 9) {
393+
return;
394+
}
395+
396+
if (keyCode === 13) {
397+
e.preventDefault();
398+
const activeItem = this._typeahead.list.element.querySelector('.active');
399+
if (activeItem) {
400+
const itemIndex = activeItem.getAttribute('data-index');
401+
const item = this._typeahead.list.items[itemIndex];
402+
if (item) {
403+
this._typeahead.value(item.original);
404+
this._typeahead.list.hide();
405+
}
406+
}
407+
} else if (keyCode === 38 || keyCode === 40) {
408+
const items = this._typeahead.list.element.querySelectorAll('li');
409+
if (items.length > 0) {
410+
if (keyCode === 38) { // Arrow up
411+
if (this._typeahead.list.active > 0) {
412+
e.preventDefault();
413+
this._typeahead.list.active--;
414+
} else {
415+
this._typeahead.el.focus();
416+
return;
417+
}
418+
} else if (keyCode === 40) { // Arrow down
419+
e.preventDefault();
420+
if (this._typeahead.list.active < items.length - 1) {
421+
this._typeahead.list.active++;
422+
} else {
423+
return;
424+
}
425+
}
426+
items.forEach(function(item) {
427+
item.classList.remove('active');
428+
});
429+
const activeItem = items[this._typeahead.list.active];
430+
if (activeItem) {
431+
activeItem.classList.add('active');
432+
activeItem.focus();
433+
}
434+
}
435+
}
436+
},
437+
438+
_onSuggestionItemFocus(e) {
439+
this._typeahead.list.active = e.target.getAttribute('data-index');
440+
this._typeahead.list.element.querySelectorAll('li').forEach(function(item) {
441+
item.classList.remove('active');
442+
});
443+
e.target.classList.add('active');
444+
},
445+
322446
_geolocateUser: function () {
323447
this._hideGeolocateButton();
324448
this._showLoadingIcon();
@@ -821,6 +945,12 @@ MapboxGeocoder.prototype = {
821945
clear: function(ev) {
822946
this._clear(ev);
823947
this._inputEl.focus();
948+
// We don't hide on blur when using browser focus
949+
// because the list is hidden on blur by default.
950+
// So we hide it here manually.
951+
if (this.options.useBrowserFocus) {
952+
this._typeahead.list.hide();
953+
}
824954
},
825955

826956

0 commit comments

Comments
 (0)