leaflet
Version:
JavaScript library for mobile-friendly interactive maps
136 lines (111 loc) • 4.01 kB
JavaScript
import {Handler} from '../../core/Handler.js';
import {on, off, stop} from '../../dom/DomEvent.js';
import {Point} from '../../geometry/Point.js';
/** @import {Marker} from '../../layer/marker/Marker.js' */
// Handles keyboard interaction with the map, enabled by default.
export class KeyboardHandler extends Handler {
/**
* The [`KeyboardEvent.key`](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/key) values for each action,
* which can be changed to rebind keys. Zoom keys follow the keyboard layout and cover the numpad, and `=`/`_` keep
* the US `+`/`-` keys working both with and without Shift.
*/
static keys = {
left: ['ArrowLeft'],
right: ['ArrowRight'],
down: ['ArrowDown'],
up: ['ArrowUp'],
zoomIn: ['+', '='],
zoomOut: ['-', '_']
};
addHooks() {
const container = this._map._container;
// make the container focusable by tabbing
if (container.tabIndex <= 0) {
container.tabIndex = 0;
}
const shortcuts = this._map.options.ariaKeyShortcuts;
if (shortcuts) {
// ARIA names physical keys by their unshifted character, so the default is for a US layout
container.ariaKeyShortcuts = shortcuts === true ?
'ArrowLeft ArrowRight ArrowUp ArrowDown = Plus -' : shortcuts;
}
on(container, {
focus: this._onFocus,
blur: this._onBlur,
pointerdown: this._onPointerDown,
keydown: this._onContainerKeyDown
}, this);
}
removeHooks() {
off(document, 'keydown', this._onKeyDown, this);
if (this._map.options.ariaKeyShortcuts) {
this._map._container.removeAttribute('aria-keyshortcuts');
}
off(this._map._container, {
focus: this._onFocus,
blur: this._onBlur,
pointerdown: this._onPointerDown,
keydown: this._onContainerKeyDown
}, this);
}
// acquire/lose focus #594, #1228, #1540
_onPointerDown() {
if (this._focused) { return; }
// script focus matches :focus-visible by default, which would show a focus ring on every click
this._map._container.focus({preventScroll: true, focusVisible: false});
}
_onFocus() {
this._focused = true;
on(document, 'keydown', this._onKeyDown, this);
this._map.fire('focus');
}
_onBlur() {
this._focused = false;
off(document, 'keydown', this._onKeyDown, this);
this._map.fire('blur');
}
// on the container rather than the document, so that Escape also works when focus is on a marker or
// inside the popup, which blurs the container and removes the document listener (#6551); controls are
// left out so that e.g. Escape in a geocoder input doesn't close an unrelated popup
/** @param {KeyboardEvent} e */
_onContainerKeyDown(e) {
const map = this._map,
popup = map._popup,
target = /** @type {Node} */ (e.target);
if (e.key !== 'Escape' || e.defaultPrevented || !popup?.options.closeOnEscapeKey ||
map._controlContainer.contains(target)) { return; }
const inPopup = popup._container.contains(target),
icon = /** @type {Marker | undefined} */ (popup._source)?._icon;
map.closePopup();
e.preventDefault();
// closing removes the focused element, which would send focus back to the start of the page
if (inPopup) { (icon && icon.tabIndex >= 0 ? icon : map._container).focus(); }
}
/** @param {KeyboardEvent} e */
_onKeyDown(e) {
if (e.altKey || e.ctrlKey || e.metaKey) { return; }
const key = e.key,
map = this._map,
{left, right, down, up, zoomIn, zoomOut} = KeyboardHandler.keys,
x = +right.includes(key) - +left.includes(key),
y = +down.includes(key) - +up.includes(key),
dz = +zoomIn.includes(key) - +zoomOut.includes(key),
k = e.shiftKey ? 3 : 1;
if (x || y) {
if (!map._panAnim?._inProgress) {
let offset = new Point(x, y).multiplyBy(k * map.options.keyboardPanDelta);
if (map.options.maxBounds) {
offset = map._limitOffset(offset, map.options.maxBounds);
}
map.panBy(offset);
}
} else if (dz) {
if (!map._zoomInputBlocked()) {
map.setZoom(map.getZoom() + k * dz * map.options.zoomDelta);
}
} else {
return;
}
stop(e);
}
}