Skip to content

Commit

Permalink
Feature index (#636)
Browse files Browse the repository at this point in the history
* Add feature indexing

* Toggle feature index on or off

* Toggle feature index on or off

* Check overlap on focus

* Fix function name

* Replace table with output element

* Add CSS for 'more results'

* Limit output to having 9 items at a time

* Optimize the SVG using SVGOMG

Co-authored-by: Robert Linder <robert.vuj.linder@outlook.com>

* Optimize the SVG using SVGOMG

Co-authored-by: Robert Linder <robert.vuj.linder@outlook.com>

* Hide output when empty

* Adding thin outline to reticle

Co-authored-by: Robert Linder <robert.vuj.linder@outlook.com>

* Focus on feature when number key is pressed

* Refactor code

* Fix focusing on feature when number key is pressed

* Fix failing tests

* Add feature index overlay tests

* Make esc key return focus to leaflet container

* Increase reticle size, update tests

* Update feature index overlay output

* change feature index font

Co-authored-by: Robert Linder <robert.vuj.linder@outlook.com>

* Update span attribute

* Add feature index overlay option

* Fix cs of us_pop_density.mapml

* Improve feature index styling for visual users (#1)

* Make reticle responsive

* Open popup instead of focusing

* Static overlay dimensions

* Improve readability of feature index

* Focus feature if no popup is available

* Update featureIndexOverlay.test.js to account for new reticle size

* Fix index popup issue + make sure index keys are valid onKeyDown

* Fix tabbing issue + add popup test

* Change popup behaviour

* Remove features from tabindex when FeatureIndexOverlay is enabled

* Update feature index screen reading behavior  (#2)

* Only start checking overlap when/after the first focus happens

* Announce map details and then feature index on initial focus

* Announce map details and then feature index on refocus

* Focus map directly when popup is closed and feature index option is on

* Remove reticle when the map is not focused

* Add hidden comma for brief pauses in output reading

* Announce feature index on popupclose

* Update featureIndexOverlay.test.js

* Hide reticle when popup is open

Co-authored-by: Robert Linder <robert.vuj.linder@outlook.com>
  • Loading branch information
ben-lu-uw and Malvoz authored May 27, 2022
1 parent c38f32e commit 62c27f2
Show file tree
Hide file tree
Showing 13 changed files with 518 additions and 11 deletions.
2 changes: 1 addition & 1 deletion src/mapml-viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export class MapViewer extends HTMLElement {

this.setControls(false,false,true);
this._crosshair = M.crosshair().addTo(this._map);

if(M.options.featureIndexOverlayOption) this._featureIndexOverlay = M.featureIndexOverlay().addTo(this._map);
// https://github.com/Maps4HTML/Web-Map-Custom-Element/issues/274
this.setAttribute('role', 'application');
// Make the Leaflet container element programmatically identifiable
Expand Down
78 changes: 76 additions & 2 deletions src/mapml.css
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@
}

/* Disable pointer events where they'd interfere with the intended action. */
.mapml-feature-index-box,
.leaflet-tooltip,
.leaflet-crosshair *,
.mapml-layer-item-settings .mapml-control-layers summary label,
Expand Down Expand Up @@ -649,7 +650,7 @@ button.mapml-button:disabled,
box-sizing: border-box;
}

.mapml-layer-item,
.mapml-layer-item,
.mapml-layer-grouped-extents,
.mapml-layer-extent {
background-color: #fff;
Expand Down Expand Up @@ -751,7 +752,7 @@ label.mapml-layer-item-toggle {
/*
* Feature styles.
*/

.mapml-vector-container svg :is(
[role="link"]:focus,
[role="link"]:hover,
Expand Down Expand Up @@ -803,3 +804,76 @@ label.mapml-layer-item-toggle {
right: 0;
}



/**
* Feature Index
*/
.mapml-feature-index-box {
margin: -8% 0 0 -8%;
width: 16%;
left: 50%;
top: 50%;
position: absolute;
z-index: 10000;
outline: 2px solid #fff;
}

.mapml-feature-index-box:after{
display: block;
content: '';
padding-top: 100%;
}

.mapml-feature-index-box > svg {
position: absolute;
width: 100%;
height: 100%;
}

.mapml-feature-index {
outline: 1px solid #000000;
contain: content;
border-radius: 4px;
background-color: #fff;
cursor: default;
z-index: 1000;
position: absolute;
top: auto;
left: 50%;
-ms-transform: translateX(-50%);
transform: translateX(-50%);
bottom: 30px;
padding-top: 5px;
height: 92px;
width: 450px;
font-size: 16px;
}

.mapml-feature-index-content > span{
width: 140px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
padding-left: 5px;
padding-right: 5px;
}

.mapml-feature-index-content > span > kbd{
background-color: lightgrey;
padding-right: 4px;
padding-left: 4px;
border-radius: 4px;
}

.mapml-feature-index-content > span > span{
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: pre;
width: 1px;
}

9 changes: 6 additions & 3 deletions src/mapml/features/featureGroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export var FeatureGroup = L.FeatureGroup.extend({
* @private
*/
_handleFocus: function(e) {
if((e.keyCode === 9 || e.keyCode === 16) && e.type === "keydown"){
if((e.keyCode === 9 || e.keyCode === 16 || e.keyCode === 27) && e.type === "keydown"){
let index = this._map.featureIndex.currentIndex;
if(e.keyCode === 9 && e.shiftKey) {
if(index === this._map.featureIndex.inBoundFeatures.length - 1)
Expand All @@ -78,14 +78,17 @@ export var FeatureGroup = L.FeatureGroup.extend({
this._map.featureIndex.inBoundFeatures[0].path.setAttribute("tabindex", -1);
this._map.featureIndex.inBoundFeatures[index].path.setAttribute("tabindex", 0);
}
} else if(e.keyCode === 27 && this._map.options.mapEl.shadowRoot.activeElement.nodeName === "g"){
this._map.featureIndex.currentIndex = 0;
this._map._container.focus();
}
} else if (!([9, 16, 13, 27].includes(e.keyCode))){
} else if (!([9, 16, 13, 27, 49, 50, 51, 52, 53, 54, 55].includes(e.keyCode))){
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") {
if((e.keyCode === 9 || e.keyCode === 16 || e.keyCode === 13 || (e.keyCode >= 49 && e.keyCode <= 55)) && e.type === "keyup") {
this.openTooltip();
} else if (e.keyCode === 13 || e.keyCode === 32){
this.closeTooltip();
Expand Down
3 changes: 1 addition & 2 deletions src/mapml/handlers/FeatureIndex.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,7 @@ export var FeatureIndex = L.Handler.extend({
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);
if(!M.options.featureIndexOverlayOption) this.inBoundFeatures[0].path.setAttribute("tabindex", 0);
},

/**
Expand Down
4 changes: 4 additions & 0 deletions src/mapml/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {AnnounceMovement} from "./handlers/AnnounceMovement";
import { FeatureIndex } from "./handlers/FeatureIndex";
import { Options } from "./options";
import "./keyboard";
import {featureIndexOverlay, FeatureIndexOverlay} from "./layers/FeatureIndexOverlay";

/* global L, Node */
(function (window, document, undefined) {
Expand Down Expand Up @@ -655,6 +656,9 @@ M.debugOverlay = debugOverlay;
M.Crosshair = Crosshair;
M.crosshair = crosshair;

M.FeatureIndexOverlay = FeatureIndexOverlay;
M.featureIndexOverlay = featureIndexOverlay;

M.Feature = Feature;
M.feature = feature;

Expand Down
210 changes: 210 additions & 0 deletions src/mapml/layers/FeatureIndexOverlay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
export var FeatureIndexOverlay = L.Layer.extend({
onAdd: function (map) {
let svgInnerHTML = `<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 100 100"><path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M0 0h100v100H0z" color="#000" overflow="visible"/></svg>`;

this._container = L.DomUtil.create("div", "mapml-feature-index-box", map._container);
this._container.innerHTML = svgInnerHTML;

this._output = L.DomUtil.create("output", "mapml-feature-index", map._container);
this._output.setAttribute("role", "status");
this._output.setAttribute("aria-live", "polite");
this._output.setAttribute("aria-atomic", "true");
this._body = L.DomUtil.create("span", "mapml-feature-index-content", this._output);
this._body.index = 0;
this._output.initialFocus = false;
map.on("layerchange layeradd layerremove overlayremove", this._toggleEvents, this);
map.on('moveend focus templatedfeatureslayeradd', this._checkOverlap, this);
map.on("keydown", this._onKeyDown, this);
this._addOrRemoveFeatureIndex();
},

_calculateReticleBounds: function () {
let bounds = this._map.getPixelBounds();
let center = bounds.getCenter();
let wRatio = Math.abs(bounds.min.x - bounds.max.x) / (this._map.options.mapEl.width);
let hRatio = Math.abs(bounds.min.y - bounds.max.y) / (this._map.options.mapEl.height);

let reticleDimension = (getComputedStyle(this._container).width).replace(/[^\d.]/g,'');
if((getComputedStyle(this._container).width).slice(-1) === "%") {
reticleDimension = reticleDimension * this._map.options.mapEl.width / 100;
}
let w = wRatio * reticleDimension / 2;
let h = hRatio * reticleDimension / 2;
let minPoint = L.point(center.x - w, center.y + h);
let maxPoint = L.point(center.x + w, center.y - h);
let b = L.bounds(minPoint, maxPoint);
return M.pixelToPCRSBounds(b,this._map.getZoom(),this._map.options.projection);
},

_checkOverlap: function (e) {
if(e.type === "focus") this._output.initialFocus = true;
if(!this._output.initialFocus) return;
if(this._output.popupClosed) {
this._output.popupClosed = false;
return;
}

this._map.fire("mapkeyboardfocused");

let featureIndexBounds = this._calculateReticleBounds();
let features = this._map.featureIndex.inBoundFeatures;
let index = 1;
let keys = Object.keys(features);
let body = this._body;

body.innerHTML = "";
body.index = 0;

body.allFeatures = [];
keys.forEach(i => {
let layer = features[i].layer;
let layers = features[i].layer._layers;
let bounds = L.bounds();

if(layers) {
let keys = Object.keys(layers);
keys.forEach(j => {
if(!bounds) bounds = L.bounds(layer._layers[j]._bounds.min, layer._layers[j]._bounds.max);
bounds.extend(layer._layers[j]._bounds.min);
bounds.extend(layer._layers[j]._bounds.max);
});
} else if(layer._bounds){
bounds = L.bounds(layer._bounds.min, layer._bounds.max);
}

if(featureIndexBounds.overlaps(bounds)){
let label = features[i].path.getAttribute("aria-label");

if (index < 8){
body.appendChild(this._updateOutput(label, index, index));
}
if (index % 7 === 0 || index === 1) {
body.allFeatures.push([]);
}
body.allFeatures[Math.floor((index - 1) / 7)].push({label, index, layer});
if (body.allFeatures[1] && body.allFeatures[1].length === 1){
body.appendChild(this._updateOutput("More results", 0, 9));
}
index += 1;
}
});
this._addToggleKeys();
},

_updateOutput: function (label, index, key) {
let span = document.createElement("span");
span.setAttribute("data-index", index);
//", " adds a brief auditory pause when a screen reader is reading through the feature index
//also prevents names with numbers + key from being combined when read
span.innerHTML = `<kbd>${key}</kbd>` + " " + label + "<span>, </span>";
return span;
},

_addToggleKeys: function () {
let allFeatures = this._body.allFeatures;
for(let i = 0; i < allFeatures.length; i++){
if(allFeatures[i].length === 0) return;
if(allFeatures[i - 1]){
let label = "Previous results";
allFeatures[i].push({label});
}

if(allFeatures[i + 1] && allFeatures[i + 1].length > 0){
let label = "More results";
allFeatures[i].push({label});
}
}
},

_onKeyDown: function (e){
let body = this._body;
let key = e.originalEvent.keyCode;
if (key >= 49 && key <= 55){
if(!body.allFeatures[body.index]) return;
let feature = body.allFeatures[body.index][key - 49];
if (!feature) return;
let layer = feature.layer;
if (layer) {
this._map.featureIndex.currentIndex = feature.index - 1;
if (layer._popup){
this._map.closePopup();
layer.openPopup();
}
else layer.options.group.focus();
}
} else if(key === 56){
this._newContent(body, -1);
} else if(key === 57){
this._newContent(body, 1);
}
},

_newContent: function (body, direction) {
let index = body.firstChild.getAttribute("data-index");
let newContent = body.allFeatures[Math.floor(((index - 1) / 7) + direction)];
if(newContent && newContent.length > 0){
body.innerHTML = "";
body.index += direction;
for(let i = 0; i < newContent.length; i++){
let feature = newContent[i];
let index = feature.index ? feature.index : 0;
let key = i + 1;
if (feature.label === "More results") key = 9;
if (feature.label === "Previous results") key = 8;
body.appendChild(this._updateOutput(feature.label, index, key));
}
}
},

_toggleEvents: function (){
this._map.on("viewreset move moveend focus blur popupclose", this._addOrRemoveFeatureIndex, this);

},

_addOrRemoveFeatureIndex: function (e) {
let features = this._body.allFeatures ? this._body.allFeatures.length : 0;
//Toggle aria-hidden attribute so screen reader rereads the feature index on focus
if (!this._output.initialFocus) {
this._output.setAttribute("aria-hidden", "true");
} else if(this._output.hasAttribute("aria-hidden")){
let obj = this;
setTimeout(function () {
obj._output.removeAttribute("aria-hidden");
}, 100);
}

if(e && e.type === "popupclose") {
this._output.setAttribute("aria-hidden", "true");
this._output.popupClosed = true;
} else if (e && e.type === "focus") {
this._container.removeAttribute("hidden");
if (features !== 0) this._output.classList.remove("mapml-screen-reader-output");
} else if (e && e.originalEvent && e.originalEvent.type === 'pointermove') {
this._container.setAttribute("hidden", "");
this._output.classList.add("mapml-screen-reader-output");
} else if (e && e.target._popup) {
this._container.setAttribute("hidden", "");
} else if (e && e.type === "blur") {
this._container.setAttribute("hidden", "");
this._output.classList.add("mapml-screen-reader-output");
this._output.initialFocus = false;
this._addOrRemoveFeatureIndex();
} else if (this._map.isFocused && e) {
this._container.removeAttribute("hidden");
if (features !== 0) {
this._output.classList.remove("mapml-screen-reader-output");
} else {
this._output.classList.add("mapml-screen-reader-output");
}
} else {
this._container.setAttribute("hidden", "");
this._output.classList.add("mapml-screen-reader-output");
}

},

});

export var featureIndexOverlay = function (options) {
return new FeatureIndexOverlay(options);
};
Loading

0 comments on commit 62c27f2

Please sign in to comment.