diff --git a/src/ui/marker.js b/src/ui/marker.js index 3204e4eb0c4..b35be447895 100644 --- a/src/ui/marker.js +++ b/src/ui/marker.js @@ -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(); @@ -74,7 +75,8 @@ export default class Marker extends Evented { '_onMove', '_onUp', '_addDragHandler', - '_onMapClick' + '_onMapClick', + '_onKeyPress' ], this); this._anchor = options && options.anchor || 'center'; @@ -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'); @@ -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; @@ -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) { @@ -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; diff --git a/test/unit/ui/marker.test.js b/test/unit/ui/marker.test.js index 620411e7f34..3fed1cf3bfd 100644 --- a/test/unit/ui/marker.test.js +++ b/test/unit/ui/marker.test.js @@ -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() diff --git a/test/util/simulate_interaction.js b/test/util/simulate_interaction.js index b61602403df..7889621c0e1 100644 --- a/test/util/simulate_interaction.js +++ b/test/util/simulate_interaction.js @@ -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);