Skip to content

Commit 1e2fce8

Browse files
arrow keys to navigate contextmenu (#830)
* arrow keys to navigate contextmenu * bug fix * aliyan suggested change * styling change * bug fix * more bug fix * Tests added * more bug fix * flaky test fix * comments * esc key * Replace == with ===. Add logic to test using Esc to dismiss Copy submenu --------- Co-authored-by: Peter Rushforth <peter.rushforth@gmail.com>
1 parent 0f94342 commit 1e2fce8

File tree

3 files changed

+467
-3
lines changed

3 files changed

+467
-3
lines changed

src/mapml/handlers/ContextMenu.js

Lines changed: 175 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ export var ContextMenu = L.Handler.extend({
1313

1414
initialize: function (map) {
1515
L.Handler.prototype.initialize.call(this, map);
16-
16+
this.activeIndex = 0; //current fous index on menu
17+
this.excludedIndices = [4, 7]; //menu indexes that are --------
18+
this.isRunned = false; //variable for tracking edge case
1719
//setting the items in the context menu and their callback functions
1820
this._items = [
1921
{
@@ -639,6 +641,8 @@ export var ContextMenu = L.Handler.extend({
639641
this._layerMenu.setAttribute('hidden', '');
640642
this._map.fire('contextmenu.hide', {contextmenu: this});
641643
setTimeout(() => this._map._container.focus(), 0);
644+
this.activeIndex = 0;
645+
this.isRunned = false;
642646
}
643647
},
644648

@@ -704,6 +708,54 @@ export var ContextMenu = L.Handler.extend({
704708
delete this._elementInFocus;
705709
},
706710

711+
_setActiveItem: function(index) {
712+
if (document.activeElement.shadowRoot === null && this.noActiveEl === true){
713+
//bug fix when theres no active element
714+
this.noActiveEl = false;
715+
//setting this._items[9] is just for preventing some diabled index, it will be override by later code.
716+
this._items[9].el.el.focus();
717+
}
718+
if (document.activeElement.shadowRoot.activeElement.innerHTML === this._items[index].el.el.innerHTML){
719+
//edge case where pressing shift f10 focuses the first element on contextmenu (if already focused, have to press arrow twice to go down)
720+
let next = index + 1;
721+
while (this._items[next].el.el.disabled) {
722+
next++;
723+
if (next >= this._items.length) {
724+
next = 0;
725+
}
726+
}
727+
this._setActiveItem(next);
728+
} else {
729+
if (this.excludedIndices.includes(index)) {
730+
// find the next or previous non-excluded item
731+
let nextIndex = index + 1;
732+
let prevIndex = index - 1;
733+
while (this.excludedIndices.includes(nextIndex) || this._items[nextIndex].el.el.disabled) {
734+
nextIndex++;
735+
if (nextIndex >= this._items.length) {
736+
nextIndex = 0;
737+
}
738+
}
739+
while (this.excludedIndices.includes(prevIndex) || this._items[prevIndex].el.el.disabled) {
740+
prevIndex--;
741+
if (prevIndex < 0) {
742+
prevIndex = this._items.length - 1;
743+
}
744+
}
745+
// set the active item to the next or previous non-excluded item
746+
if (this.activeIndex < index) {
747+
this._setActiveItem(nextIndex);
748+
} else {
749+
this._setActiveItem(prevIndex);
750+
}
751+
} else {
752+
// set the focus item
753+
this._items[index].el.el.focus();
754+
this.activeIndex = index;
755+
}
756+
}
757+
},
758+
707759
_onKeyDown: function (e) {
708760
if(!this._mapMenuVisible) return;
709761

@@ -723,13 +775,132 @@ export var ContextMenu = L.Handler.extend({
723775
L.DomEvent.stop(e);
724776
this._focusOnLayerControl();
725777
}
778+
} else if (key === 38) { //up arrow
779+
if (!this._coordMenu.hasAttribute('hidden') &&
780+
(document.activeElement.shadowRoot === null || //null happens when the focus is on submenu and when mouse hovers on main menu, submenu disappears
781+
document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[0].innerHTML)) { //"map" on submenu
782+
this._coordMenu.children[2].focus();
783+
} else if (!this._coordMenu.hasAttribute('hidden') &&
784+
document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[1].innerHTML) { //"extent" on submenu
785+
this._coordMenu.children[0].focus();
786+
} else if (!this._coordMenu.hasAttribute('hidden') &&
787+
document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[2].innerHTML) { //"Location" on submenu
788+
this._coordMenu.children[1].focus();
789+
} else if (!this._layerMenu.hasAttribute('hidden') &&
790+
document.activeElement.shadowRoot.activeElement.innerHTML === this._layerMenu.children[0].innerHTML) { //"zoom to layer" on layermenu
791+
this._layerMenu.children[1].focus();
792+
} else if (!this._layerMenu.hasAttribute('hidden')) {
793+
this._layerMenu.children[0].focus();
794+
} else {
795+
if (this.activeIndex > 0) {
796+
let prevIndex = this.activeIndex - 1;
797+
while (this._items[prevIndex].el.el.disabled) {
798+
prevIndex--;
799+
if (prevIndex < 0) {
800+
prevIndex = this._items.length - 1;
801+
}
802+
}
803+
this._setActiveItem(prevIndex);
804+
} else {
805+
this._setActiveItem(this._items.length - 1);
806+
}
807+
}
808+
} else if (key === 40) { //down arrow
809+
if (!this._coordMenu.hasAttribute('hidden') &&
810+
(document.activeElement.shadowRoot === null ||
811+
document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[2].innerHTML)) { //"map" on submenu
812+
this._coordMenu.children[0].focus();
813+
} else if (!this._coordMenu.hasAttribute('hidden') &&
814+
document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[1].innerHTML) { //"extent" on submenu
815+
this._coordMenu.children[2].focus();
816+
} else if (!this._coordMenu.hasAttribute('hidden') &&
817+
document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[0].innerHTML) { //"Location" on submenu
818+
this._coordMenu.children[1].focus();
819+
} else if (!this._layerMenu.hasAttribute('hidden') &&
820+
document.activeElement.shadowRoot.activeElement.innerHTML === this._layerMenu.children[0].innerHTML){ //"zoom to layer" on layermenu
821+
this._layerMenu.children[1].focus();
822+
} else if (!this._layerMenu.hasAttribute('hidden')){
823+
this._layerMenu.children[0].focus();
824+
} else {
825+
if (this.activeIndex < this._items.length - 1) {
826+
//edge case at index 0
827+
if (!this.isRunned && this.activeIndex === 0 && !this._items[this.activeIndex].el.el.disabled){
828+
this._setActiveItem(0);
829+
this.isRunned = true;
830+
} else { //edge case over
831+
let nextIndex = this.activeIndex + 1;
832+
while (this._items[nextIndex].el.el.disabled) {
833+
nextIndex++;
834+
if (nextIndex >= this._items.length) {
835+
nextIndex = 0;
836+
}
837+
}
838+
this._setActiveItem(nextIndex);
839+
}
840+
} else {
841+
let nextIndex = 0;
842+
while (this._items[nextIndex].el.el.disabled) {
843+
nextIndex++;
844+
if (nextIndex >= this._items.length) {
845+
nextIndex = 0;
846+
}
847+
}
848+
this._setActiveItem(nextIndex);
849+
}
850+
}
851+
} else if (key === 39) { //right arrow
852+
if (document.activeElement.shadowRoot !== null &&
853+
document.activeElement.shadowRoot.activeElement.innerHTML ===
854+
this._items[5].el.el.innerHTML && //'copy'
855+
this._coordMenu.hasAttribute('hidden')){
856+
this._showCoordMenu();
857+
this._coordMenu.children[0].focus();
858+
} else if (document.activeElement.shadowRoot.activeElement.innerHTML === this._items[5].el.el.innerHTML &&
859+
!this._coordMenu.hasAttribute('hidden')) {
860+
this._coordMenu.children[0].focus();
861+
}
862+
} else if (key === 37) { //left arrow
863+
if (!this._coordMenu.hasAttribute('hidden') &&
864+
document.activeElement.shadowRoot !== null) {
865+
if (document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[0].innerHTML ||
866+
document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[1].innerHTML ||
867+
document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[2].innerHTML){
868+
this._coordMenu.setAttribute('hidden','');
869+
this._setActiveItem(5);
870+
}
871+
}
872+
} else if (key === 27) { //esc key
873+
if (document.activeElement.shadowRoot === null) {
874+
this._hide();
875+
} else {
876+
if (!this._coordMenu.hasAttribute('hidden')) {
877+
if (document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[0].innerHTML ||
878+
document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[1].innerHTML ||
879+
document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[2].innerHTML){
880+
this._coordMenu.setAttribute('hidden','');
881+
this._setActiveItem(5);
882+
}
883+
} else {
884+
this._hide();
885+
}
886+
}
726887
} else if(key !== 16 && key!== 9 &&
727888
!(!(this._layerClicked.className.includes('mapml-layer-item')) && key === 67) &&
728-
(path[0].innerText !== (M.options.locale.cmCopyCoords + " (C)") || key === 27)){
889+
(path[0].innerText !== (M.options.locale.cmCopyCoords + " (C)"))){
729890
this._hide();
730891
}
731892
switch(key){
732893
case 13: //ENTER KEY
894+
if(document.activeElement.shadowRoot.activeElement.innerHTML === this._items[5].el.el.innerHTML){
895+
this._copyCoords({
896+
latlng:this._map.getCenter()
897+
});
898+
this._coordMenu.firstChild.focus();
899+
} else{
900+
if(this._map._container.parentNode.activeElement.parentNode.classList.contains("mapml-contextmenu"))
901+
this._map._container.parentNode.activeElement.click();
902+
}
903+
break;
733904
case 32: //SPACE KEY
734905
if(this._map._container.parentNode.activeElement.parentNode.classList.contains("mapml-contextmenu"))
735906
this._map._container.parentNode.activeElement.click();
@@ -738,6 +909,7 @@ export var ContextMenu = L.Handler.extend({
738909
this._copyCoords({
739910
latlng:this._map.getCenter()
740911
});
912+
this._coordMenu.firstChild.focus();
741913
break;
742914
case 68: //D KEY
743915
this._toggleDebug(e);
@@ -790,7 +962,6 @@ export var ContextMenu = L.Handler.extend({
790962

791963
menu.style.top = 100 - 22 + 'px';
792964
menu.style.bottom = 'auto';
793-
if(this._keyboardEvent)menu.firstChild.focus();
794965
},
795966

796967
_hideCoordMenu: function(e){
@@ -800,6 +971,7 @@ export var ContextMenu = L.Handler.extend({
800971
let menu = this._coordMenu, copyEl = this._items[4].el.el;
801972
copyEl.setAttribute("aria-expanded","false");
802973
menu.setAttribute('hidden', '');
974+
this.noActiveEl = true; //variable to keep track of no focus element on contextmenu, bug fix for arrow key navigation
803975
},
804976

805977
_onItemMouseOver: function (e) {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width,initial-scale=1">
6+
<title>locateApi.html</title>
7+
<script type="module" src="./mapml-viewer.js"></script>
8+
<style>
9+
html,
10+
body {
11+
height: 100%;
12+
}
13+
* {
14+
margin: 0;
15+
padding: 0;
16+
}
17+
18+
/* Specifying the `:defined` selector is recommended to style the map
19+
element, such that styles don't apply when fallback content is in use
20+
(e.g. when scripting is disabled or when custom/built-in elements isn't
21+
supported in the browser). */
22+
mapml-viewer:defined {
23+
/* Responsive map. */
24+
max-width: 100%;
25+
26+
/* Full viewport. */
27+
width: 100%;
28+
height: 100%;
29+
30+
/* Remove default (native-like) border. */
31+
border: none;
32+
33+
vertical-align: middle;
34+
}
35+
36+
/* Pre-style to avoid FOUC of inline layer- and fallback content. */
37+
mapml-viewer:not(:defined) > * {
38+
display: none;
39+
}
40+
41+
/* Pre-style to avoid Layout Shift. */
42+
mapml-viewer:not(:defined) {
43+
display: inline-block;
44+
contain: size;
45+
contain-intrinsic-size: 304px 154px;
46+
}
47+
48+
/* Ensure inline layer content is hidden if custom/built-in elements isn't
49+
supported, or if javascript is disabled. This needs to be defined separately
50+
from the above, because the `:not(:defined)` selector invalidates the entire
51+
declaration in browsers that do not support it. */
52+
layer- {
53+
display: none;
54+
}
55+
</style>
56+
<noscript>
57+
<style>
58+
/* Ensure fallback content (children of the map element) is displayed if
59+
custom/built-in elements is supported but javascript is disabled. */
60+
mapml-viewer:not(:defined) > :not(layer-) {
61+
display: initial;
62+
}
63+
64+
/* "Reset" the properties used to pre-style (to avoid Layout Shift) if
65+
custom/built-in elements is supported but javascript is disabled. */
66+
mapml-viewer:not(:defined) {
67+
display: initial;
68+
contain: initial;
69+
contain-intrinsic-size: initial;
70+
}
71+
</style>
72+
</noscript>
73+
</head>
74+
<body>
75+
<mapml-viewer projection="CBMTILE" zoom="2" lat="45" lon="-90" controls>
76+
<layer- label="CBMT" src="https://geogratis.gc.ca/mapml/en/cbmtile/cbmt/" checked></layer->
77+
</mapml-viewer>
78+
</body>
79+
</html>

0 commit comments

Comments
 (0)