Skip to content

Commit 62c27f2

Browse files
ben-lu-uwMalvoz
andauthored
Feature index (#636)
* 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 <[email protected]> * Optimize the SVG using SVGOMG Co-authored-by: Robert Linder <[email protected]> * Hide output when empty * Adding thin outline to reticle Co-authored-by: Robert Linder <[email protected]> * 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 <[email protected]> * 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 <[email protected]>
1 parent c38f32e commit 62c27f2

File tree

13 files changed

+518
-11
lines changed

13 files changed

+518
-11
lines changed

src/mapml-viewer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ export class MapViewer extends HTMLElement {
219219

220220
this.setControls(false,false,true);
221221
this._crosshair = M.crosshair().addTo(this._map);
222-
222+
if(M.options.featureIndexOverlayOption) this._featureIndexOverlay = M.featureIndexOverlay().addTo(this._map);
223223
// https://github.com/Maps4HTML/Web-Map-Custom-Element/issues/274
224224
this.setAttribute('role', 'application');
225225
// Make the Leaflet container element programmatically identifiable

src/mapml.css

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@
430430
}
431431

432432
/* Disable pointer events where they'd interfere with the intended action. */
433+
.mapml-feature-index-box,
433434
.leaflet-tooltip,
434435
.leaflet-crosshair *,
435436
.mapml-layer-item-settings .mapml-control-layers summary label,
@@ -649,7 +650,7 @@ button.mapml-button:disabled,
649650
box-sizing: border-box;
650651
}
651652

652-
.mapml-layer-item,
653+
.mapml-layer-item,
653654
.mapml-layer-grouped-extents,
654655
.mapml-layer-extent {
655656
background-color: #fff;
@@ -751,7 +752,7 @@ label.mapml-layer-item-toggle {
751752
/*
752753
* Feature styles.
753754
*/
754-
755+
755756
.mapml-vector-container svg :is(
756757
[role="link"]:focus,
757758
[role="link"]:hover,
@@ -803,3 +804,76 @@ label.mapml-layer-item-toggle {
803804
right: 0;
804805
}
805806

807+
808+
809+
/**
810+
* Feature Index
811+
*/
812+
.mapml-feature-index-box {
813+
margin: -8% 0 0 -8%;
814+
width: 16%;
815+
left: 50%;
816+
top: 50%;
817+
position: absolute;
818+
z-index: 10000;
819+
outline: 2px solid #fff;
820+
}
821+
822+
.mapml-feature-index-box:after{
823+
display: block;
824+
content: '';
825+
padding-top: 100%;
826+
}
827+
828+
.mapml-feature-index-box > svg {
829+
position: absolute;
830+
width: 100%;
831+
height: 100%;
832+
}
833+
834+
.mapml-feature-index {
835+
outline: 1px solid #000000;
836+
contain: content;
837+
border-radius: 4px;
838+
background-color: #fff;
839+
cursor: default;
840+
z-index: 1000;
841+
position: absolute;
842+
top: auto;
843+
left: 50%;
844+
-ms-transform: translateX(-50%);
845+
transform: translateX(-50%);
846+
bottom: 30px;
847+
padding-top: 5px;
848+
height: 92px;
849+
width: 450px;
850+
font-size: 16px;
851+
}
852+
853+
.mapml-feature-index-content > span{
854+
width: 140px;
855+
white-space: nowrap;
856+
overflow: hidden;
857+
text-overflow: ellipsis;
858+
display: inline-block;
859+
padding-left: 5px;
860+
padding-right: 5px;
861+
}
862+
863+
.mapml-feature-index-content > span > kbd{
864+
background-color: lightgrey;
865+
padding-right: 4px;
866+
padding-left: 4px;
867+
border-radius: 4px;
868+
}
869+
870+
.mapml-feature-index-content > span > span{
871+
clip: rect(0 0 0 0);
872+
clip-path: inset(50%);
873+
height: 1px;
874+
overflow: hidden;
875+
position: absolute;
876+
white-space: pre;
877+
width: 1px;
878+
}
879+

src/mapml/features/featureGroup.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export var FeatureGroup = L.FeatureGroup.extend({
5959
* @private
6060
*/
6161
_handleFocus: function(e) {
62-
if((e.keyCode === 9 || e.keyCode === 16) && e.type === "keydown"){
62+
if((e.keyCode === 9 || e.keyCode === 16 || e.keyCode === 27) && e.type === "keydown"){
6363
let index = this._map.featureIndex.currentIndex;
6464
if(e.keyCode === 9 && e.shiftKey) {
6565
if(index === this._map.featureIndex.inBoundFeatures.length - 1)
@@ -78,14 +78,17 @@ export var FeatureGroup = L.FeatureGroup.extend({
7878
this._map.featureIndex.inBoundFeatures[0].path.setAttribute("tabindex", -1);
7979
this._map.featureIndex.inBoundFeatures[index].path.setAttribute("tabindex", 0);
8080
}
81+
} else if(e.keyCode === 27 && this._map.options.mapEl.shadowRoot.activeElement.nodeName === "g"){
82+
this._map.featureIndex.currentIndex = 0;
83+
this._map._container.focus();
8184
}
82-
} else if (!([9, 16, 13, 27].includes(e.keyCode))){
85+
} else if (!([9, 16, 13, 27, 49, 50, 51, 52, 53, 54, 55].includes(e.keyCode))){
8386
this._map.featureIndex.currentIndex = 0;
8487
this._map.featureIndex.inBoundFeatures[0].path.focus();
8588
}
8689

8790
if(e.target.tagName.toUpperCase() !== "G") return;
88-
if((e.keyCode === 9 || e.keyCode === 16 || e.keyCode === 13) && e.type === "keyup") {
91+
if((e.keyCode === 9 || e.keyCode === 16 || e.keyCode === 13 || (e.keyCode >= 49 && e.keyCode <= 55)) && e.type === "keyup") {
8992
this.openTooltip();
9093
} else if (e.keyCode === 13 || e.keyCode === 32){
9194
this.closeTooltip();

src/mapml/handlers/FeatureIndex.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,7 @@ export var FeatureIndex = L.Handler.extend({
9090
b.dist = Math.sqrt(Math.pow(bc.x - mc.x, 2) + Math.pow(bc.y - mc.y, 2));
9191
return a.dist - b.dist;
9292
});
93-
94-
this.inBoundFeatures[0].path.setAttribute("tabindex", 0);
93+
if(!M.options.featureIndexOverlayOption) this.inBoundFeatures[0].path.setAttribute("tabindex", 0);
9594
},
9695

9796
/**

src/mapml/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import {AnnounceMovement} from "./handlers/AnnounceMovement";
6161
import { FeatureIndex } from "./handlers/FeatureIndex";
6262
import { Options } from "./options";
6363
import "./keyboard";
64+
import {featureIndexOverlay, FeatureIndexOverlay} from "./layers/FeatureIndexOverlay";
6465

6566
/* global L, Node */
6667
(function (window, document, undefined) {
@@ -655,6 +656,9 @@ M.debugOverlay = debugOverlay;
655656
M.Crosshair = Crosshair;
656657
M.crosshair = crosshair;
657658

659+
M.FeatureIndexOverlay = FeatureIndexOverlay;
660+
M.featureIndexOverlay = featureIndexOverlay;
661+
658662
M.Feature = Feature;
659663
M.feature = feature;
660664

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
export var FeatureIndexOverlay = L.Layer.extend({
2+
onAdd: function (map) {
3+
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>`;
4+
5+
this._container = L.DomUtil.create("div", "mapml-feature-index-box", map._container);
6+
this._container.innerHTML = svgInnerHTML;
7+
8+
this._output = L.DomUtil.create("output", "mapml-feature-index", map._container);
9+
this._output.setAttribute("role", "status");
10+
this._output.setAttribute("aria-live", "polite");
11+
this._output.setAttribute("aria-atomic", "true");
12+
this._body = L.DomUtil.create("span", "mapml-feature-index-content", this._output);
13+
this._body.index = 0;
14+
this._output.initialFocus = false;
15+
map.on("layerchange layeradd layerremove overlayremove", this._toggleEvents, this);
16+
map.on('moveend focus templatedfeatureslayeradd', this._checkOverlap, this);
17+
map.on("keydown", this._onKeyDown, this);
18+
this._addOrRemoveFeatureIndex();
19+
},
20+
21+
_calculateReticleBounds: function () {
22+
let bounds = this._map.getPixelBounds();
23+
let center = bounds.getCenter();
24+
let wRatio = Math.abs(bounds.min.x - bounds.max.x) / (this._map.options.mapEl.width);
25+
let hRatio = Math.abs(bounds.min.y - bounds.max.y) / (this._map.options.mapEl.height);
26+
27+
let reticleDimension = (getComputedStyle(this._container).width).replace(/[^\d.]/g,'');
28+
if((getComputedStyle(this._container).width).slice(-1) === "%") {
29+
reticleDimension = reticleDimension * this._map.options.mapEl.width / 100;
30+
}
31+
let w = wRatio * reticleDimension / 2;
32+
let h = hRatio * reticleDimension / 2;
33+
let minPoint = L.point(center.x - w, center.y + h);
34+
let maxPoint = L.point(center.x + w, center.y - h);
35+
let b = L.bounds(minPoint, maxPoint);
36+
return M.pixelToPCRSBounds(b,this._map.getZoom(),this._map.options.projection);
37+
},
38+
39+
_checkOverlap: function (e) {
40+
if(e.type === "focus") this._output.initialFocus = true;
41+
if(!this._output.initialFocus) return;
42+
if(this._output.popupClosed) {
43+
this._output.popupClosed = false;
44+
return;
45+
}
46+
47+
this._map.fire("mapkeyboardfocused");
48+
49+
let featureIndexBounds = this._calculateReticleBounds();
50+
let features = this._map.featureIndex.inBoundFeatures;
51+
let index = 1;
52+
let keys = Object.keys(features);
53+
let body = this._body;
54+
55+
body.innerHTML = "";
56+
body.index = 0;
57+
58+
body.allFeatures = [];
59+
keys.forEach(i => {
60+
let layer = features[i].layer;
61+
let layers = features[i].layer._layers;
62+
let bounds = L.bounds();
63+
64+
if(layers) {
65+
let keys = Object.keys(layers);
66+
keys.forEach(j => {
67+
if(!bounds) bounds = L.bounds(layer._layers[j]._bounds.min, layer._layers[j]._bounds.max);
68+
bounds.extend(layer._layers[j]._bounds.min);
69+
bounds.extend(layer._layers[j]._bounds.max);
70+
});
71+
} else if(layer._bounds){
72+
bounds = L.bounds(layer._bounds.min, layer._bounds.max);
73+
}
74+
75+
if(featureIndexBounds.overlaps(bounds)){
76+
let label = features[i].path.getAttribute("aria-label");
77+
78+
if (index < 8){
79+
body.appendChild(this._updateOutput(label, index, index));
80+
}
81+
if (index % 7 === 0 || index === 1) {
82+
body.allFeatures.push([]);
83+
}
84+
body.allFeatures[Math.floor((index - 1) / 7)].push({label, index, layer});
85+
if (body.allFeatures[1] && body.allFeatures[1].length === 1){
86+
body.appendChild(this._updateOutput("More results", 0, 9));
87+
}
88+
index += 1;
89+
}
90+
});
91+
this._addToggleKeys();
92+
},
93+
94+
_updateOutput: function (label, index, key) {
95+
let span = document.createElement("span");
96+
span.setAttribute("data-index", index);
97+
//", " adds a brief auditory pause when a screen reader is reading through the feature index
98+
//also prevents names with numbers + key from being combined when read
99+
span.innerHTML = `<kbd>${key}</kbd>` + " " + label + "<span>, </span>";
100+
return span;
101+
},
102+
103+
_addToggleKeys: function () {
104+
let allFeatures = this._body.allFeatures;
105+
for(let i = 0; i < allFeatures.length; i++){
106+
if(allFeatures[i].length === 0) return;
107+
if(allFeatures[i - 1]){
108+
let label = "Previous results";
109+
allFeatures[i].push({label});
110+
}
111+
112+
if(allFeatures[i + 1] && allFeatures[i + 1].length > 0){
113+
let label = "More results";
114+
allFeatures[i].push({label});
115+
}
116+
}
117+
},
118+
119+
_onKeyDown: function (e){
120+
let body = this._body;
121+
let key = e.originalEvent.keyCode;
122+
if (key >= 49 && key <= 55){
123+
if(!body.allFeatures[body.index]) return;
124+
let feature = body.allFeatures[body.index][key - 49];
125+
if (!feature) return;
126+
let layer = feature.layer;
127+
if (layer) {
128+
this._map.featureIndex.currentIndex = feature.index - 1;
129+
if (layer._popup){
130+
this._map.closePopup();
131+
layer.openPopup();
132+
}
133+
else layer.options.group.focus();
134+
}
135+
} else if(key === 56){
136+
this._newContent(body, -1);
137+
} else if(key === 57){
138+
this._newContent(body, 1);
139+
}
140+
},
141+
142+
_newContent: function (body, direction) {
143+
let index = body.firstChild.getAttribute("data-index");
144+
let newContent = body.allFeatures[Math.floor(((index - 1) / 7) + direction)];
145+
if(newContent && newContent.length > 0){
146+
body.innerHTML = "";
147+
body.index += direction;
148+
for(let i = 0; i < newContent.length; i++){
149+
let feature = newContent[i];
150+
let index = feature.index ? feature.index : 0;
151+
let key = i + 1;
152+
if (feature.label === "More results") key = 9;
153+
if (feature.label === "Previous results") key = 8;
154+
body.appendChild(this._updateOutput(feature.label, index, key));
155+
}
156+
}
157+
},
158+
159+
_toggleEvents: function (){
160+
this._map.on("viewreset move moveend focus blur popupclose", this._addOrRemoveFeatureIndex, this);
161+
162+
},
163+
164+
_addOrRemoveFeatureIndex: function (e) {
165+
let features = this._body.allFeatures ? this._body.allFeatures.length : 0;
166+
//Toggle aria-hidden attribute so screen reader rereads the feature index on focus
167+
if (!this._output.initialFocus) {
168+
this._output.setAttribute("aria-hidden", "true");
169+
} else if(this._output.hasAttribute("aria-hidden")){
170+
let obj = this;
171+
setTimeout(function () {
172+
obj._output.removeAttribute("aria-hidden");
173+
}, 100);
174+
}
175+
176+
if(e && e.type === "popupclose") {
177+
this._output.setAttribute("aria-hidden", "true");
178+
this._output.popupClosed = true;
179+
} else if (e && e.type === "focus") {
180+
this._container.removeAttribute("hidden");
181+
if (features !== 0) this._output.classList.remove("mapml-screen-reader-output");
182+
} else if (e && e.originalEvent && e.originalEvent.type === 'pointermove') {
183+
this._container.setAttribute("hidden", "");
184+
this._output.classList.add("mapml-screen-reader-output");
185+
} else if (e && e.target._popup) {
186+
this._container.setAttribute("hidden", "");
187+
} else if (e && e.type === "blur") {
188+
this._container.setAttribute("hidden", "");
189+
this._output.classList.add("mapml-screen-reader-output");
190+
this._output.initialFocus = false;
191+
this._addOrRemoveFeatureIndex();
192+
} else if (this._map.isFocused && e) {
193+
this._container.removeAttribute("hidden");
194+
if (features !== 0) {
195+
this._output.classList.remove("mapml-screen-reader-output");
196+
} else {
197+
this._output.classList.add("mapml-screen-reader-output");
198+
}
199+
} else {
200+
this._container.setAttribute("hidden", "");
201+
this._output.classList.add("mapml-screen-reader-output");
202+
}
203+
204+
},
205+
206+
});
207+
208+
export var featureIndexOverlay = function (options) {
209+
return new FeatureIndexOverlay(options);
210+
};

0 commit comments

Comments
 (0)