Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tab order based on distance from center #596

Merged
merged 12 commits into from
Dec 17, 2021
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/mapml-viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export class MapViewer extends HTMLElement {
query: true,
contextMenu: true,
announceMovement: M.options.announceMovement,
featureIndex: true,
mapEl: this,
crs: M[this.projection],
zoom: this.zoom,
Expand Down
15 changes: 13 additions & 2 deletions src/mapml/features/feature.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,13 +284,19 @@ export var Feature = L.Path.extend({
this._coordinateToArrays(span, main, subParts, false, span.getAttribute("class"), parents.concat([span]));
}
let noSpan = coords.textContent.replace(/(<([^>]+)>)/ig, ''),
pairs = noSpan.match(/(\S+\s+\S+)/gim), local = [];
pairs = noSpan.match(/(\S+\s+\S+)/gim), local = [], bounds;
for (let p of pairs) {
let numPair = [];
p.split(/\s+/gim).forEach(M.parseNumber, numPair);
let point = M.pointToPCRSPoint(L.point(numPair), this.options.zoom, this.options.projection, this.options.nativeCS);
local.push(point);
this._bounds = this._bounds ? this._bounds.extend(point) : L.bounds(point, point);
bounds = bounds ? bounds.extend(point) : L.bounds(point, point);
}
if (this._bounds) {
this._bounds.extend(bounds.min);
this._bounds.extend(bounds.max);
} else {
this._bounds = bounds;
}
if (isFirst) {
main.push({ points: local });
Expand All @@ -303,6 +309,7 @@ export var Feature = L.Path.extend({
}
subParts.unshift({
points: local,
center: bounds.getCenter(),
cls: `${cls || ""} ${wrapperAttr.className || ""}`.trim(),
attr: attrMap,
link: wrapperAttr.link,
Expand Down Expand Up @@ -339,6 +346,10 @@ export var Feature = L.Path.extend({
if (!this._bounds) return null;
return this._map.options.crs.unproject(this._bounds.getCenter());
},

getPCRSCenter: function () {
return this._bounds.getCenter();
},
});

/**
Expand Down
93 changes: 66 additions & 27 deletions src/mapml/features/featureGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export var FeatureGroup = L.FeatureGroup.extend({
L.LayerGroup.prototype.initialize.call(this, layers, options);

if((this.options.onEachFeature && this.options.properties) || this.options.link) {
this.options.group.setAttribute('tabindex', '0');
L.DomUtil.addClass(this.options.group, "leaflet-interactive");
L.DomEvent.on(this.options.group, "keyup keydown mousedown", this._handleFocus, this);
let firstLayer = layers[Object.keys(layers)[0]];
Expand All @@ -32,12 +31,59 @@ export var FeatureGroup = L.FeatureGroup.extend({
if(this.options.featureID) this.options.group.setAttribute("data-fid", this.options.featureID);
},

onAdd: function (map) {
L.LayerGroup.prototype.onAdd.call(this, map);
this.updateInteraction();
},

updateInteraction: function () {
let map = this._map || this.options._leafletLayer._map;
if((this.options.onEachFeature && this.options.properties) || this.options.link)
map.featureIndex.addToIndex(this, this.getPCRSCenter(), this.options.group);

for (let layerID in this._layers) {
let layer = this._layers[layerID];
for(let part of layer._parts){
if(layer.featureAttributes && layer.featureAttributes.tabindex)
map.featureIndex.addToIndex(layer, layer.getPCRSCenter(), part.path);
for(let subPart of part.subrings) {
if(subPart.attr && subPart.attr.tabindex) map.featureIndex.addToIndex(layer, subPart.center, subPart.path);
}
}
}
},

/**
* Handler for focus events
* @param {L.DOMEvent} e - Event that occurred
* @private
*/
_handleFocus: function(e) {
if((e.keyCode === 9 || e.keyCode === 16) && e.type === "keydown"){
let index = this._map.featureIndex.currentIndex;
if(e.keyCode === 9 && e.shiftKey) {
if(index === this._map.featureIndex.inBoundFeatures.length - 1)
this._map.featureIndex.inBoundFeatures[index].path.setAttribute("tabindex", -1);
if(index !== 0){
L.DomEvent.stop(e);
this._map.featureIndex.inBoundFeatures[index - 1].path.focus();
this._map.featureIndex.currentIndex--;
}
} else if (e.keyCode === 9) {
if(index !== this._map.featureIndex.inBoundFeatures.length - 1) {
L.DomEvent.stop(e);
this._map.featureIndex.inBoundFeatures[index + 1].path.focus();
this._map.featureIndex.currentIndex++;
} else {
this._map.featureIndex.inBoundFeatures[0].path.setAttribute("tabindex", -1);
this._map.featureIndex.inBoundFeatures[index].path.setAttribute("tabindex", 0);
}
}
} else if (!(e.keyCode === 9 || e.keyCode === 16 || e.keyCode === 13)){
this._map.featureIndex.currentIndex = 0;
this._map.featureIndex.inBoundFeatures[0].path.focus();
}

if(e.target.tagName.toUpperCase() !== "G") return;
if((e.keyCode === 9 || e.keyCode === 16 || e.keyCode === 13) && e.type === "keyup") {
this.openTooltip();
Expand Down Expand Up @@ -69,22 +115,10 @@ export var FeatureGroup = L.FeatureGroup.extend({
* @private
*/
_previousFeature: function(e){
let group = this._source.group.previousSibling;
if(!group){
let currentIndex = this._source.group.closest("div.mapml-layer").style.zIndex;
let overlays = this._map.getPane("overlayPane").children;
for(let i = overlays.length - 1; i >= 0; i--){
let layer = overlays[i];
if(layer.style.zIndex >= currentIndex) continue;
group = layer.querySelector("g.leaflet-interactive");
if(group){
group = group.parentNode.lastChild;
break;
}
}
if (!group) group = this._source.group;
}
group.focus();
L.DomEvent.stop(e);
this._map.featureIndex.currentIndex = Math.max(this._map.featureIndex.currentIndex - 1, 0);
let prevFocus = this._map.featureIndex.inBoundFeatures[this._map.featureIndex.currentIndex];
prevFocus.path.focus();
this._map.closePopup();
},

Expand All @@ -94,19 +128,24 @@ export var FeatureGroup = L.FeatureGroup.extend({
* @private
*/
_nextFeature: function(e){
let group = this._source.group.nextSibling;
if(!group){
let currentIndex = this._source.group.closest("div.mapml-layer").style.zIndex;
L.DomEvent.stop(e);
this._map.featureIndex.currentIndex = Math.min(this._map.featureIndex.currentIndex + 1, this._map.featureIndex.inBoundFeatures.length - 1);
let nextFocus = this._map.featureIndex.inBoundFeatures[this._map.featureIndex.currentIndex];
nextFocus.path.focus();
this._map.closePopup();
},

for(let layer of this._map.getPane("overlayPane").children){
if(layer.style.zIndex <= currentIndex) continue;
group = layer.querySelectorAll("g.leaflet-interactive");
if(group.length > 0)break;
getPCRSCenter: function () {
let bounds;
for(let l in this._layers){
let layer = this._layers[l];
if (!bounds) {
bounds = L.bounds(layer.getPCRSCenter(), layer.getPCRSCenter());
} else {
bounds.extend(layer.getPCRSCenter());
}
group = group && group.length > 0 ? group[0] : this._source.group;
}
group.focus();
this._map.closePopup();
return bounds.getCenter();
},
});

Expand Down
7 changes: 1 addition & 6 deletions src/mapml/features/featureRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
* @returns {*}
*/
export var FeatureRenderer = L.SVG.extend({


/**
* Override method of same name from L.SVG, use the this._container property
* to set up the role="none presentation" on featureGroupu container,
Expand Down Expand Up @@ -48,9 +46,6 @@ export var FeatureRenderer = L.SVG.extend({
if (p.subrings) {
for (let r of p.subrings) {
this._createPath(r, layer.options.className, r.attr['aria-label'], (r.link !== undefined), r.attr);
if(r.attr && r.attr.tabindex){
p.path.setAttribute('tabindex', r.attr.tabindex || '0');
}
}
}
this._updateStyle(layer);
Expand All @@ -77,7 +72,7 @@ export var FeatureRenderer = L.SVG.extend({
if (title) p.setAttribute('aria-label', title);
} else {
for(let [name, value] of Object.entries(attr)){
if(name === "id") continue;
if(name === "id" || name === "tabindex") continue;
p.setAttribute(name, value);
}
}
Expand Down
110 changes: 110 additions & 0 deletions src/mapml/handlers/FeatureIndex.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
export var FeatureIndex = L.Handler.extend({
initialize: function (map) {
L.Handler.prototype.initialize.call(this, map);
this.inBoundFeatures = [];
this.outBoundFeatures = [];
this.currentIndex = 0;
this._mapPCRSBounds = M.pixelToPCRSBounds(
map.getPixelBounds(),
map.getZoom(),
map.options.projection);
},

addHooks: function () {
this._map.on("mapkeyboardfocused", this._updateMapBounds, this);
this._map.on('mapkeyboardfocused', this._sortIndex, this);
},

removeHooks: function () {
this._map.off("mapkeyboardfocused", this._updateMapBounds);
this._map.off('mapkeyboardfocused', this._sortIndex);
},

/**
* Adds a svg element to the index of tabbable features, it also keeps track of the layer it's associated + center
* @param layer - the layer object the feature is associated with
* @param lc - the layer center
* @param path - the svg element that needs to be focused, can be a path or g
*/
addToIndex: function (layer, lc, path) {
let mc = this._mapPCRSBounds.getCenter();
let dist = Math.sqrt(Math.pow(lc.x - mc.x, 2) + Math.pow(lc.y - mc.y, 2));
let index = this._mapPCRSBounds.contains(lc) ? this.inBoundFeatures : this.outBoundFeatures;

let elem = {path: path, layer: layer, center: lc, dist: dist};
path.setAttribute("tabindex", -1);

index.push(elem);

// TODO: this insertion loop has potential to be improved slightly
for (let i = index.length - 1; i > 0 && index[i].dist < index[i-1].dist; i--) {
let tmp = index[i];
index[i] = index[i-1];
index[i-1] = tmp;
}

if (this._mapPCRSBounds.contains(lc))
this.inBoundFeatures = index;
else
this.outBoundFeatures = index;
},

/**
* Removes features that are no longer on the map, also moves features to the respective array depending
* on whether the feature is in the maps viewport or not
*/
cleanIndex: function() {
this.currentIndex = 0;
this.inBoundFeatures = this.inBoundFeatures.filter((elem) => {
let inbound = this._mapPCRSBounds.contains(elem.center);
elem.path.setAttribute("tabindex", -1);
if (elem.layer._map && !inbound) {
this.outBoundFeatures.push(elem);
}
return elem.layer._map && inbound;
});
this.outBoundFeatures = this.outBoundFeatures.filter((elem) => {
let inbound = this._mapPCRSBounds.contains(elem.center);
elem.path.setAttribute("tabindex", -1);
if (elem.layer._map && inbound) {
this.inBoundFeatures.push(elem);
}
return elem.layer._map && !inbound;
});
},

/**
* Sorts the index of features in the map's viewport based on distance from center
* @private
*/
_sortIndex: function() {
this.cleanIndex();
if(this.inBoundFeatures.length === 0) return;

let mc = this._mapPCRSBounds.getCenter();

this.inBoundFeatures.sort(function(a, b) {
let ac = a.center;
let bc = b.center;
a.dist = Math.sqrt(Math.pow(ac.x - mc.x, 2) + Math.pow(ac.y - mc.y, 2));
b.dist = Math.sqrt(Math.pow(bc.x - mc.x, 2) + Math.pow(bc.y - mc.y, 2));
return a.dist - b.dist;
});

this.inBoundFeatures[0].path.setAttribute("tabindex", 0);
},

/**
* Event handler for 'mapfocused' event to update the map's bounds in terms of PCRS
* @param e - the event object
* @private
*/
_updateMapBounds: function (e) {
// TODO: map's PCRS bounds is used in other parts of the viewer, can be moved out to the map object directly
this._mapPCRSBounds = M.pixelToPCRSBounds(
this._map.getPixelBounds(),
this._map.getZoom(),
this._map.options.projection);
},
});

3 changes: 3 additions & 0 deletions src/mapml/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { Feature, feature } from "./features/feature";
import { FeatureRenderer, featureRenderer } from './features/featureRenderer';
import { FeatureGroup, featureGroup} from './features/featureGroup';
import {AnnounceMovement} from "./handlers/AnnounceMovement";
import { FeatureIndex } from "./handlers/FeatureIndex";
import { Options } from "./options";
import "./keyboard";

Expand Down Expand Up @@ -607,11 +608,13 @@ M.gcrsToTileMatrix = Util.gcrsToTileMatrix;
M.QueryHandler = QueryHandler;
M.ContextMenu = ContextMenu;
M.AnnounceMovement = AnnounceMovement;
M.FeatureIndex = FeatureIndex;

// see https://leafletjs.com/examples/extending/extending-3-controls.html#handlers
L.Map.addInitHook('addHandler', 'query', M.QueryHandler);
L.Map.addInitHook('addHandler', 'contextMenu', M.ContextMenu);
L.Map.addInitHook('addHandler', 'announceMovement', M.AnnounceMovement);
L.Map.addInitHook('addHandler', 'featureIndex', M.FeatureIndex);

M.MapMLLayer = MapMLLayer;
M.mapMLLayer = mapMLLayer;
Expand Down
2 changes: 2 additions & 0 deletions src/mapml/layers/Crosshair.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export var Crosshair = L.Layer.extend({
return false;
},

// TODO: should be merged with the 'mapfocused' event emitted by mapml-viewer and map, not trivial
_isMapFocused: function (e) {
//set this._map.isFocused = true if arrow buttons are used
if(!this._map._container.parentNode.activeElement){
Expand All @@ -96,6 +97,7 @@ export var Crosshair = L.Layer.extend({
this._map.isFocused = false;
} else this._map.isFocused = isLeafletContainer && ["keyup", "keydown"].includes(e.type);

if(this._map.isFocused) this._map.fire("mapkeyboardfocused");
this._addOrRemoveMapOutline();
this._addOrRemoveCrosshair();
},
Expand Down
3 changes: 3 additions & 0 deletions src/mapml/layers/FeatureLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export var MapMLFeatures = L.FeatureGroup.extend({
L.DomUtil.remove(this._container);
}
L.FeatureGroup.prototype.onRemove.call(this, map);
this._map.featureIndex.cleanIndex();
},

getEvents: function(){
Expand Down Expand Up @@ -146,6 +147,8 @@ export var MapMLFeatures = L.FeatureGroup.extend({

_resetFeatures : function (zoom){
this.clearLayers();
// since features are removed and re-added by zoom level, need to clean the feature index before re-adding
if(this._map) this._map.featureIndex.cleanIndex();
if(this._features && this._features[zoom]){
for(let k =0;k < this._features[zoom].length;k++){
this.addLayer(this._features[zoom][k]);
Expand Down
Loading