Skip to content

Commit

Permalink
open popup on a marker from keyboard (#6835)
Browse files Browse the repository at this point in the history
* open popup on a marker from keyboard

* arrow functions don't need to bind this

* set tabindex on marker elements with popups and remove keypress listener

* aria-label on default marker

* tabindex value should be string, thanks flow

* flow

* flow

* KeyboardEvent.charCode and keyCode are deprecated, support standard KeyboardEvent.code

* add unit test for opening popup on Enter

* add unit test for opening popup on Space

* retain existing tabindex

* fix lint

* fix flow

* _onKeyPress method

* fix scrolling issue when focusing markers

* prevent marker focus on click
  • Loading branch information
andrewharvey authored and mourner committed Nov 6, 2019
1 parent 7267fa2 commit cd39e14
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 1 deletion.
38 changes: 37 additions & 1 deletion src/ui/marker.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export default class Marker extends Evented {
_rotation: number;
_pitchAlignment: string;
_rotationAlignment: string;
_originalTabIndex: ?string; // original tabindex of _element

constructor(options?: Options, legacyOptions?: Options) {
super();
Expand All @@ -74,7 +75,8 @@ export default class Marker extends Evented {
'_onMove',
'_onUp',
'_addDragHandler',
'_onMapClick'
'_onMapClick',
'_onKeyPress'
], this);

this._anchor = options && options.anchor || 'center';
Expand All @@ -88,6 +90,7 @@ export default class Marker extends Evented {
if (!options || !options.element) {
this._defaultMarker = true;
this._element = DOM.create('div');
this._element.setAttribute('aria-label', 'Map marker');

// create default map marker SVG
const svg = DOM.createNS('http://www.w3.org/2000/svg', 'svg');
Expand Down Expand Up @@ -197,6 +200,16 @@ export default class Marker extends Evented {
this._element.addEventListener('dragstart', (e: DragEvent) => {
e.preventDefault();
});
this._element.addEventListener('mousedown', (e: MouseEvent) => {
// prevent focusing on click
e.preventDefault();
});
this._element.addEventListener('focus', () => {
// revert the default scrolling action of the container
const el = this._map.getContainer();
el.scrollTop = 0;
el.scrollLeft = 0;
});
applyAnchorClass(this._element, this._anchor, 'marker');

this._popup = null;
Expand Down Expand Up @@ -292,6 +305,11 @@ export default class Marker extends Evented {
if (this._popup) {
this._popup.remove();
this._popup = null;
this._element.removeEventListener('keypress', this._onKeyPress);

if (!this._originalTabIndex) {
this._element.removeAttribute('tabindex');
}
}

if (popup) {
Expand All @@ -312,11 +330,29 @@ export default class Marker extends Evented {
}
this._popup = popup;
if (this._lngLat) this._popup.setLngLat(this._lngLat);

this._originalTabIndex = this._element.getAttribute('tabindex');
if (!this._originalTabIndex) {
this._element.setAttribute('tabindex', '0');
}
this._element.addEventListener('keypress', this._onKeyPress);
}

return this;
}

_onKeyPress(e: KeyboardEvent) {
const code = e.code;
const legacyCode = e.charCode || e.keyCode;

if (
(code === 'Space') || (code === 'Enter') ||
(legacyCode === 32) || (legacyCode === 13) // space or enter
) {
this.togglePopup();
}
}

_onMapClick(e: MapMouseEvent) {
const targetElement = e.originalEvent.target;
const element = this._element;
Expand Down
65 changes: 65 additions & 0 deletions test/unit/ui/marker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,71 @@ test('Marker#togglePopup closes a popup that was open', (t) => {
t.end();
});

test('Enter key on Marker opens a popup that was closed', (t) => {
const map = createMap(t);
const marker = new Marker()
.setLngLat([0, 0])
.addTo(map)
.setPopup(new Popup());

// popup not initially open
t.notOk(marker.getPopup().isOpen());

simulate.keypress(marker.getElement(), {code: 'Enter'});

// popup open after Enter keypress
t.ok(marker.getPopup().isOpen());

map.remove();
t.end();
});

test('Space key on Marker opens a popup that was closed', (t) => {
const map = createMap(t);
const marker = new Marker()
.setLngLat([0, 0])
.addTo(map)
.setPopup(new Popup());

// popup not initially open
t.notOk(marker.getPopup().isOpen());

simulate.keypress(marker.getElement(), {code: 'Space'});

// popup open after Enter keypress
t.ok(marker.getPopup().isOpen());

map.remove();
t.end();
});

test('Marker#setPopup sets a tabindex', (t) => {
const popup = new Popup();
const marker = new Marker()
.setPopup(popup);
t.equal(marker.getElement().getAttribute('tabindex'), "0");
t.end();
});

test('Marker#setPopup removes tabindex when unset', (t) => {
const popup = new Popup();
const marker = new Marker()
.setPopup(popup)
.setPopup();
t.notOk(marker.getElement().getAttribute('tabindex'));
t.end();
});

test('Marker#setPopup does not replace existing tabindex', (t) => {
const element = window.document.createElement('div');
element.setAttribute('tabindex', '5');
const popup = new Popup();
const marker = new Marker({element})
.setPopup(popup);
t.equal(marker.getElement().getAttribute('tabindex'), "5");
t.end();
});

test('Marker anchor defaults to center', (t) => {
const map = createMap(t);
const marker = new Marker()
Expand Down
6 changes: 6 additions & 0 deletions test/util/simulate_interaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ events.dblclick = function (target, options) {
target.dispatchEvent(new MouseEvent('dblclick', options));
};

events.keypress = function (target, options) {
options = Object.assign({bubbles: true}, options);
const KeyboardEvent = window(target).KeyboardEvent;
target.dispatchEvent(new KeyboardEvent('keypress', options));
};

[ 'mouseup', 'mousedown', 'mouseover', 'mousemove', 'mouseout' ].forEach((event) => {
events[event] = function (target, options) {
options = Object.assign({bubbles: true}, options);
Expand Down

0 comments on commit cd39e14

Please sign in to comment.