From b8a71b933915c5b9204333fc9fbb9948bd965147 Mon Sep 17 00:00:00 2001 From: Jacky0299 Date: Fri, 28 Apr 2023 11:34:02 -0400 Subject: [PATCH 01/14] Tab compatible with Arrow key --- src/mapml/handlers/ContextMenu.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/mapml/handlers/ContextMenu.js b/src/mapml/handlers/ContextMenu.js index e405d2df6..7a6cc8a25 100644 --- a/src/mapml/handlers/ContextMenu.js +++ b/src/mapml/handlers/ContextMenu.js @@ -884,6 +884,30 @@ export var ContextMenu = L.Handler.extend({ this._hide(); } } + } else if (e.shiftKey && key === 9){ + let arr = ['Back (Alt+Left Arrow)', 'Forward (Alt+Right Arrow)', 'Reload (Ctrl+R)', + 'View Fullscreen (F)', '', 'Copy (C)', 'Paste (P)', '', 'Toggle Controls (T)', + 'Toggle Debug Mode (D)', 'View Map Source (V)']; + let ind = (arr.indexOf(document.activeElement.shadowRoot.activeElement.innerHTML)) - 1; + while (this._items[ind].el.el.disabled || ind === 4 || ind ===7) { + ind--; + if (ind < 0) { + ind = this._items.length - 1; + } + } + this.activeIndex = ind; + } else if (key === 9) { + let arr = ['Back (Alt+Left Arrow)', 'Forward (Alt+Right Arrow)', 'Reload (Ctrl+R)', + 'View Fullscreen (F)', '', 'Copy (C)', 'Paste (P)', '', 'Toggle Controls (T)', + 'Toggle Debug Mode (D)', 'View Map Source (V)']; + let ind = (arr.indexOf(document.activeElement.shadowRoot.activeElement.innerHTML)) + 1; + while (this._items[ind].el.el.disabled || ind === 4 || ind ===7) { + ind++; + if (ind >= this._items.length) { + ind = 0; + } + } + this.activeIndex = ind; } else if(key !== 16 && key!== 9 && !(!(this._layerClicked.className.includes('mapml-layer-item')) && key === 67) && (path[0].innerText !== (M.options.locale.cmCopyCoords + " (C)"))){ From ec296479eaf3beca87eda287d9e7ab367aea42fb Mon Sep 17 00:00:00 2001 From: Jacky0299 Date: Fri, 28 Apr 2023 11:43:55 -0400 Subject: [PATCH 02/14] comment --- src/mapml/handlers/ContextMenu.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mapml/handlers/ContextMenu.js b/src/mapml/handlers/ContextMenu.js index 7a6cc8a25..fdcb28fa1 100644 --- a/src/mapml/handlers/ContextMenu.js +++ b/src/mapml/handlers/ContextMenu.js @@ -885,6 +885,7 @@ export var ContextMenu = L.Handler.extend({ } } } else if (e.shiftKey && key === 9){ + //need to have localized strings below let arr = ['Back (Alt+Left Arrow)', 'Forward (Alt+Right Arrow)', 'Reload (Ctrl+R)', 'View Fullscreen (F)', '', 'Copy (C)', 'Paste (P)', '', 'Toggle Controls (T)', 'Toggle Debug Mode (D)', 'View Map Source (V)']; @@ -897,6 +898,7 @@ export var ContextMenu = L.Handler.extend({ } this.activeIndex = ind; } else if (key === 9) { + //need to have localized strings below let arr = ['Back (Alt+Left Arrow)', 'Forward (Alt+Right Arrow)', 'Reload (Ctrl+R)', 'View Fullscreen (F)', '', 'Copy (C)', 'Paste (P)', '', 'Toggle Controls (T)', 'Toggle Debug Mode (D)', 'View Map Source (V)']; From 54e878c1549479d069f61a6f1437fbba48f02959 Mon Sep 17 00:00:00 2001 From: prushfor Date: Wed, 3 May 2023 10:40:09 -0400 Subject: [PATCH 03/14] Refactor constants to have somewhat meaningful names Rename some methods to be more related to what they do --- src/mapml/handlers/ContextMenu.js | 294 ++++++++++--------- test/e2e/core/ArrowKeyNavContextMenu.test.js | 2 +- 2 files changed, 152 insertions(+), 144 deletions(-) diff --git a/src/mapml/handlers/ContextMenu.js b/src/mapml/handlers/ContextMenu.js index fdcb28fa1..23761213a 100644 --- a/src/mapml/handlers/ContextMenu.js +++ b/src/mapml/handlers/ContextMenu.js @@ -6,7 +6,7 @@ to deal in the Software without restriction, including without limitation the ri and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. */ -/* global M */ +/* global M, KeyboardEvent */ export var ContextMenu = L.Handler.extend({ _touchstart: L.Browser.msPointer ? 'MSPointerDown' : L.Browser.pointer ? 'pointerdown' : 'touchstart', @@ -17,61 +17,79 @@ export var ContextMenu = L.Handler.extend({ this.excludedIndices = [4, 7]; //menu indexes that are -------- this.isRunned = false; //variable for tracking edge case //setting the items in the context menu and their callback functions + this._menuItems = {}; + + const CTXBACK = 0, CTXFWD = 1, CTXRELOAD = 2, CTXFULLSCR = 3, CTXSPACER1 = 4, + CTXCOPY = 5, CTXPASTE = 6, CTXSPACER2 = 7, CTXCNTRLS = 8, CTXDEBUG = 9, + CTXVWSRC = 10; + this._menuItems.CTXBACK = CTXBACK; + this._menuItems.CTXFWD = CTXFWD; + this._menuItems.CTXRELOAD = CTXRELOAD; + this._menuItems.CTXFULLSCR = CTXFULLSCR; + this._menuItems.CTXSPACER1 = CTXSPACER1; + this._menuItems.CTXCOPY = CTXCOPY; + this._menuItems.CTXPASTE = CTXPASTE; + this._menuItems.CTXSPACER2 = CTXSPACER2; + this._menuItems.CTXCNTRLS = CTXCNTRLS; + this._menuItems.CTXDEBUG = CTXDEBUG; + this._menuItems.CTXVWSRC = CTXVWSRC; + this._items = [ - { + { // 0 text: M.options.locale.cmBack + " (Alt+Left Arrow)", callback:this._goBack }, - { + { // 1 text: M.options.locale.cmForward + " (Alt+Right Arrow)", callback:this._goForward }, - { + { // 2 text: M.options.locale.cmReload + " (Ctrl+R)", callback:this._reload }, - { + { // 3 text: M.options.locale.btnFullScreen + " (F)", callback:this._toggleFullScreen }, - { + { // 4 spacer:"-" }, - { + { // 5 text: M.options.locale.cmCopyCoords + " (C)", callback:this._copyCoords, hideOnSelect:false, popup:true, submenu:[ - { + { // 5.0 text: M.options.locale.cmCopyMapML, callback:this._copyMapML }, - { + { // 5.1 text: M.options.locale.cmCopyExtent, callback:this._copyExtent }, - { + { // 5.2 text: M.options.locale.cmCopyLocation, callback:this._copyLocation } ] }, - { + { // 6 text: M.options.locale.cmPasteLayer + " (P)", callback:this._paste }, - { + { // 7 spacer:"-" }, - { + { // 8 text: M.options.locale.cmToggleControls + " (T)", callback:this._toggleControls - }, { + }, + { // 9 text: M.options.locale.cmToggleDebug + " (D)", callback:this._toggleDebug }, - { + { // 10 text: M.options.locale.cmViewSource + " (V)", callback:this._viewSource } @@ -81,12 +99,15 @@ export var ContextMenu = L.Handler.extend({ // should be public as they are used in tests this.defExtCS = M.options.defaultExtCoor; this.defLocCS = M.options.defaultLocCoor; + const LYRZOOMTO = 0, LYRCOPY = 1; + this._menuItems.LYRZOOMTO = LYRZOOMTO; + this._menuItems.LYRCOPY = LYRCOPY; this._layerItems = [ - { + { // 0 text: M.options.locale.lmZoomToLayer + " (Z)", callback:this._zoomToLayer }, - { + { // 1 text: M.options.locale.lmCopyLayer + " (L)", callback:this._copyLayer } @@ -94,34 +115,40 @@ export var ContextMenu = L.Handler.extend({ this._mapMenuVisible = false; this._keyboardEvent = false; - this._container = L.DomUtil.create("div", "mapml-contextmenu", map._container); + this._container = L.DomUtil.create("div", "mapml-contextmenu", map.getContainer()); this._container.setAttribute('hidden', ''); - for (let i = 0; i < 6; i++) { - this._items[i].el = this._createItem(this._container, this._items[i]); - } + this._items[CTXBACK].el = this._createItem(this._container, this._items[CTXBACK]); + this._items[CTXFWD].el = this._createItem(this._container, this._items[CTXFWD]); + this._items[CTXRELOAD].el = this._createItem(this._container, this._items[CTXRELOAD]); + this._items[CTXFULLSCR].el = this._createItem(this._container, this._items[CTXFULLSCR]); + this._items[CTXSPACER1].el = this._createItem(this._container, this._items[CTXSPACER1]); + this._items[CTXCOPY].el = this._createItem(this._container, this._items[CTXCOPY]); - this._coordMenu = L.DomUtil.create("div", "mapml-contextmenu mapml-submenu", this._container); - this._coordMenu.id = "mapml-copy-submenu"; - this._coordMenu.setAttribute('hidden', ''); + this._copySubMenu = L.DomUtil.create("div", "mapml-contextmenu mapml-submenu", this._container); + this._copySubMenu.id = "mapml-copy-submenu"; + this._copySubMenu.setAttribute('hidden', ''); this._clickEvent = null; - for(let i =0;i this._map._container.focus(), 0); @@ -759,38 +786,35 @@ export var ContextMenu = L.Handler.extend({ _onKeyDown: function (e) { if(!this._mapMenuVisible) return; - let key = e.keyCode; - let path = e.path || e.composedPath(); - - if(key === 13) + if(e.key === "Enter" || e.key === "Tab" || (e.shiftKey && e.key === "Tab")) e.preventDefault(); // keep track of where the focus is on the layer menu and when the layer menu is tabbed out of, focus on layer control - if(this._layerMenuTabs && (key === 9 || key === 27)){ + if(this._layerMenuTabs && (e.key === "Tab" || e.key === "Escape")){ // tab or esc if(e.shiftKey){ this._layerMenuTabs -= 1; } else { this._layerMenuTabs += 1; } - if(this._layerMenuTabs === 0 || this._layerMenuTabs === 3 || key === 27){ + if(this._layerMenuTabs === 0 || this._layerMenuTabs === 3 || e.key === "Escape"){ // esc L.DomEvent.stop(e); this._focusOnLayerControl(); } - } else if (key === 38) { //up arrow - if (!this._coordMenu.hasAttribute('hidden') && + } else if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) { //up arrow + if (!this._copySubMenu.hasAttribute('hidden') && (document.activeElement.shadowRoot === null || //null happens when the focus is on submenu and when mouse hovers on main menu, submenu disappears - document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[0].innerHTML)) { //"map" on submenu - this._coordMenu.children[2].focus(); - } else if (!this._coordMenu.hasAttribute('hidden') && - document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[1].innerHTML) { //"extent" on submenu - this._coordMenu.children[0].focus(); - } else if (!this._coordMenu.hasAttribute('hidden') && - document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[2].innerHTML) { //"Location" on submenu - this._coordMenu.children[1].focus(); + document.activeElement.shadowRoot.activeElement.innerHTML === this._copySubMenu.children[this._menuItems.CPYMENUMAP].innerHTML)) { //"map" on submenu + this._copySubMenu.children[this._menuItems.CPYMENULOC].focus(); + } else if (!this._copySubMenu.hasAttribute('hidden') && + document.activeElement.shadowRoot.activeElement.innerHTML === this._copySubMenu.children[this._menuItems.CPYMENUEXTENT].innerHTML) { //"extent" on submenu + this._copySubMenu.children[this._menuItems.CPYMENUMAP].focus(); + } else if (!this._copySubMenu.hasAttribute('hidden') && + document.activeElement.shadowRoot.activeElement.innerHTML === this._copySubMenu.children[this._menuItems.CPYMENULOC].innerHTML) { //"Location" on submenu + this._copySubMenu.children[this._menuItems.CPYMENUEXTENT].focus(); } else if (!this._layerMenu.hasAttribute('hidden') && - document.activeElement.shadowRoot.activeElement.innerHTML === this._layerMenu.children[0].innerHTML) { //"zoom to layer" on layermenu - this._layerMenu.children[1].focus(); + document.activeElement.shadowRoot.activeElement.innerHTML === this._layerMenu.children[this._menuItems.LYRZOOMTO].innerHTML) { //"zoom to layer" on layermenu + this._layerMenu.children[this._menuItems.LYRCOPY].focus(); } else if (!this._layerMenu.hasAttribute('hidden')) { - this._layerMenu.children[0].focus(); + this._layerMenu.children[this._menuItems.LYRZOOMTO].focus(); } else { if (this.activeIndex > 0) { let prevIndex = this.activeIndex - 1; @@ -805,22 +829,22 @@ export var ContextMenu = L.Handler.extend({ this._setActiveItem(this._items.length - 1); } } - } else if (key === 40) { //down arrow - if (!this._coordMenu.hasAttribute('hidden') && + } else if (e.key === "ArrowDown" || e.key === "Tab") { //down arrow + if (!this._copySubMenu.hasAttribute('hidden') && (document.activeElement.shadowRoot === null || - document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[2].innerHTML)) { //"map" on submenu - this._coordMenu.children[0].focus(); - } else if (!this._coordMenu.hasAttribute('hidden') && - document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[1].innerHTML) { //"extent" on submenu - this._coordMenu.children[2].focus(); - } else if (!this._coordMenu.hasAttribute('hidden') && - document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[0].innerHTML) { //"Location" on submenu - this._coordMenu.children[1].focus(); + document.activeElement.shadowRoot.activeElement.innerHTML === this._copySubMenu.children[this._menuItems.CPYMENULOC].innerHTML)) { //"map" on submenu + this._copySubMenu.children[this._menuItems.CPYMENUMAP].focus(); + } else if (!this._copySubMenu.hasAttribute('hidden') && + document.activeElement.shadowRoot.activeElement.innerHTML === this._copySubMenu.children[this._menuItems.CPYMENUEXTENT].innerHTML) { //"extent" on submenu + this._copySubMenu.children[this._menuItems.CPYMENULOC].focus(); + } else if (!this._copySubMenu.hasAttribute('hidden') && + document.activeElement.shadowRoot.activeElement.innerHTML === this._copySubMenu.children[this._menuItems.CPYMENUMAP].innerHTML) { //"Location" on submenu + this._copySubMenu.children[this._menuItems.CPYMENUEXTENT].focus(); } else if (!this._layerMenu.hasAttribute('hidden') && - document.activeElement.shadowRoot.activeElement.innerHTML === this._layerMenu.children[0].innerHTML){ //"zoom to layer" on layermenu - this._layerMenu.children[1].focus(); + document.activeElement.shadowRoot.activeElement.innerHTML === this._layerMenu.children[this._menuItems.LYRZOOMTO].innerHTML){ //"zoom to layer" on layermenu + this._layerMenu.children[this._menuItems.LYRCOPY].focus(); } else if (!this._layerMenu.hasAttribute('hidden')){ - this._layerMenu.children[0].focus(); + this._layerMenu.children[this._menuItems.LYRZOOMTO].focus(); } else { if (this.activeIndex < this._items.length - 1) { //edge case at index 0 @@ -848,128 +872,112 @@ export var ContextMenu = L.Handler.extend({ this._setActiveItem(nextIndex); } } - } else if (key === 39) { //right arrow + } else if (e.key === "ArrowRight") { //right arrow if (document.activeElement.shadowRoot !== null && document.activeElement.shadowRoot.activeElement.innerHTML === - this._items[5].el.el.innerHTML && //'copy' - this._coordMenu.hasAttribute('hidden')){ - this._showCoordMenu(); - this._coordMenu.children[0].focus(); - } else if (document.activeElement.shadowRoot.activeElement.innerHTML === this._items[5].el.el.innerHTML && - !this._coordMenu.hasAttribute('hidden')) { - this._coordMenu.children[0].focus(); + this._items[this._menuItems.CTXCOPY].el.el.innerHTML && //'copy' + this._copySubMenu.hasAttribute('hidden')){ + this._showCopySubMenu(); + this._copySubMenu.children[0].focus(); + } else if (document.activeElement.shadowRoot.activeElement.innerHTML === this._items[this._menuItems.CTXCOPY].el.el.innerHTML && + !this._copySubMenu.hasAttribute('hidden')) { + this._copySubMenu.children[0].focus(); } - } else if (key === 37) { //left arrow - if (!this._coordMenu.hasAttribute('hidden') && + } else if (e.key === "ArrowLeft") { //left arrow + if (!this._copySubMenu.hasAttribute('hidden') && document.activeElement.shadowRoot !== null) { - if (document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[0].innerHTML || - document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[1].innerHTML || - document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[2].innerHTML){ - this._coordMenu.setAttribute('hidden',''); - this._setActiveItem(5); + if (document.activeElement.shadowRoot.activeElement.innerHTML === this._copySubMenu.children[this._menuItems.CPYMENUMAP].innerHTML || + document.activeElement.shadowRoot.activeElement.innerHTML === this._copySubMenu.children[this._menuItems.CPYMENUEXTENT].innerHTML || + document.activeElement.shadowRoot.activeElement.innerHTML === this._copySubMenu.children[this._menuItems.CPYMENULOC].innerHTML){ + this._copySubMenu.setAttribute('hidden',''); + this._setActiveItem(this._menuItems.CTXCOPY); } } - } else if (key === 27) { //esc key + } else if (e.key === "Escape") { //esc key if (document.activeElement.shadowRoot === null) { this._hide(); } else { - if (!this._coordMenu.hasAttribute('hidden')) { - if (document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[0].innerHTML || - document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[1].innerHTML || - document.activeElement.shadowRoot.activeElement.innerHTML === this._coordMenu.children[2].innerHTML){ - this._coordMenu.setAttribute('hidden',''); - this._setActiveItem(5); + if (!this._copySubMenu.hasAttribute('hidden')) { + if (document.activeElement.shadowRoot.activeElement.innerHTML === this._copySubMenu.children[this._menuItems.CPYMENUMAP].innerHTML || + document.activeElement.shadowRoot.activeElement.innerHTML === this._copySubMenu.children[this._menuItems.CPYMENUEXTENT].innerHTML || + document.activeElement.shadowRoot.activeElement.innerHTML === this._copySubMenu.children[this._menuItems.CPYMENULOC].innerHTML){ + this._copySubMenu.setAttribute('hidden',''); + this._setActiveItem(this._menuItems.CTXCOPY); } } else { this._hide(); } } - } else if (e.shiftKey && key === 9){ - //need to have localized strings below - let arr = ['Back (Alt+Left Arrow)', 'Forward (Alt+Right Arrow)', 'Reload (Ctrl+R)', - 'View Fullscreen (F)', '', 'Copy (C)', 'Paste (P)', '', 'Toggle Controls (T)', - 'Toggle Debug Mode (D)', 'View Map Source (V)']; - let ind = (arr.indexOf(document.activeElement.shadowRoot.activeElement.innerHTML)) - 1; - while (this._items[ind].el.el.disabled || ind === 4 || ind ===7) { - ind--; - if (ind < 0) { - ind = this._items.length - 1; - } - } - this.activeIndex = ind; - } else if (key === 9) { - //need to have localized strings below - let arr = ['Back (Alt+Left Arrow)', 'Forward (Alt+Right Arrow)', 'Reload (Ctrl+R)', - 'View Fullscreen (F)', '', 'Copy (C)', 'Paste (P)', '', 'Toggle Controls (T)', - 'Toggle Debug Mode (D)', 'View Map Source (V)']; - let ind = (arr.indexOf(document.activeElement.shadowRoot.activeElement.innerHTML)) + 1; - while (this._items[ind].el.el.disabled || ind === 4 || ind ===7) { - ind++; - if (ind >= this._items.length) { - ind = 0; - } - } - this.activeIndex = ind; - } else if(key !== 16 && key!== 9 && - !(!(this._layerClicked.className.includes('mapml-layer-item')) && key === 67) && - (path[0].innerText !== (M.options.locale.cmCopyCoords + " (C)"))){ - this._hide(); } - switch(key){ - case 13: //ENTER KEY +// else if(key !== 16 && key!== 9 && +// !(!(this._layerClicked.className.includes('mapml-layer-item')) && key === 67) && +// (path[0].innerText !== (M.options.locale.cmCopyCoords + " (C)"))){ +// this._hide(); +// } + switch(e.key){ + case "Enter": //ENTER KEY if(document.activeElement.shadowRoot.activeElement.innerHTML === this._items[5].el.el.innerHTML){ this._copyCoords({ latlng:this._map.getCenter() }); - this._coordMenu.firstChild.focus(); + this._copySubMenu.firstChild.focus(); } else{ if(this._map._container.parentNode.activeElement.parentNode.classList.contains("mapml-contextmenu")) this._map._container.parentNode.activeElement.click(); } break; - case 32: //SPACE KEY + case " ": //SPACE KEY if(this._map._container.parentNode.activeElement.parentNode.classList.contains("mapml-contextmenu")) this._map._container.parentNode.activeElement.click(); break; - case 67: //C KEY + case "c": + case "C": //C KEY this._copyCoords({ latlng:this._map.getCenter() }); - this._coordMenu.firstChild.focus(); + this._copySubMenu.firstChild.focus(); break; - case 68: //D KEY + case "d": + case "D": //D KEY this._toggleDebug(e); break; - case 77: //M KEY + case "m": + case "M": //M KEY this._copyMapML(e); break; - case 76: //L KEY + case "l": + case "L": //L KEY if(this._layerClicked.className.includes('mapml-layer-item')) this._copyLayer(e); break; - case 70: //F KEY + case "f": + case "F": //F KEY this._toggleFullScreen(e); break; - case 80: //P KEY + case "p": + case "P": //P KEY this._paste(e); break; - case 84: //T KEY + case "t": + case "T": //T KEY this._toggleControls(e); break; - case 86: //V KEY + case "v": + case "V": //V KEY this._viewSource(e); break; - case 90: //Z KEY + case "z": + case "Z": //Z KEY if(this._layerClicked.className.includes('mapml-layer-item')) this._zoomToLayer(e); break; } }, - _showCoordMenu: function(e){ + _showCopySubMenu: function(e){ let mapSize = this._map.getSize(), click = this._clickEvent, - menu = this._coordMenu, + menu = this._copySubMenu, copyEl = this._items[5].el.el; copyEl.setAttribute("aria-expanded","true"); @@ -990,11 +998,11 @@ export var ContextMenu = L.Handler.extend({ menu.style.bottom = 'auto'; }, - _hideCoordMenu: function(e){ + _hideCopySubMenu: function(e){ if(!e.relatedTarget || !e.relatedTarget.parentElement || e.relatedTarget.parentElement.classList.contains("mapml-submenu") || e.relatedTarget.classList.contains("mapml-submenu"))return; - let menu = this._coordMenu, copyEl = this._items[4].el.el; + let menu = this._copySubMenu, copyEl = this._items[4].el.el; copyEl.setAttribute("aria-expanded","false"); menu.setAttribute('hidden', ''); this.noActiveEl = true; //variable to keep track of no focus element on contextmenu, bug fix for arrow key navigation @@ -1002,12 +1010,12 @@ export var ContextMenu = L.Handler.extend({ _onItemMouseOver: function (e) { L.DomUtil.addClass(e.target || e.srcElement, 'over'); - if(e.srcElement.innerText === (M.options.locale.cmCopyCoords + " (C)")) this._showCoordMenu(e); + if(e.srcElement.innerText === (M.options.locale.cmCopyCoords + " (C)")) this._showCopySubMenu(e); }, _onItemMouseOut: function (e) { L.DomUtil.removeClass(e.target || e.srcElement, 'over'); - this._hideCoordMenu(e); + this._hideCopySubMenu(e); }, toggleContextMenuItem: function (options,state) { diff --git a/test/e2e/core/ArrowKeyNavContextMenu.test.js b/test/e2e/core/ArrowKeyNavContextMenu.test.js index a51200e6c..c157eb16c 100644 --- a/test/e2e/core/ArrowKeyNavContextMenu.test.js +++ b/test/e2e/core/ArrowKeyNavContextMenu.test.js @@ -141,7 +141,7 @@ test.describe("Using arrow keys to navigate context menu", () => { activeElement = await page.evaluate(() => document.activeElement.shadowRoot.activeElement.innerHTML); expect(activeElement).toEqual('Copy (C)'); - let hide = await page.$eval("body > mapml-viewer", (viewer) => viewer._map.contextMenu._coordMenu.hidden); + let hide = await page.$eval("body > mapml-viewer", (viewer) => viewer._map.contextMenu._copySubMenu.hidden); expect(hide).toEqual(true); }); From 7f26902ac8b12949fc6e42c6ae6a2d89b161ec99 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 3 May 2023 22:41:25 -0400 Subject: [PATCH 04/14] Restore catch-all logic that relates to layer context menu, but is obscure and complicated. Use KeyboardEvent.code for its case-insensitive mnemonics. --- src/mapml/handlers/ContextMenu.js | 60 +++++++++++--------------- test/e2e/core/layerContextMenu.test.js | 1 + 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/mapml/handlers/ContextMenu.js b/src/mapml/handlers/ContextMenu.js index 23761213a..064ecb1e9 100644 --- a/src/mapml/handlers/ContextMenu.js +++ b/src/mapml/handlers/ContextMenu.js @@ -786,20 +786,20 @@ export var ContextMenu = L.Handler.extend({ _onKeyDown: function (e) { if(!this._mapMenuVisible) return; - if(e.key === "Enter" || e.key === "Tab" || (e.shiftKey && e.key === "Tab")) + if(e.code === "Enter" || e.code === "Tab" || (e.shiftKey && e.code === "Tab")) e.preventDefault(); // keep track of where the focus is on the layer menu and when the layer menu is tabbed out of, focus on layer control - if(this._layerMenuTabs && (e.key === "Tab" || e.key === "Escape")){ // tab or esc + if(this._layerMenuTabs && (e.code === "Tab" || e.code === "Escape")){ // tab or esc if(e.shiftKey){ this._layerMenuTabs -= 1; } else { this._layerMenuTabs += 1; } - if(this._layerMenuTabs === 0 || this._layerMenuTabs === 3 || e.key === "Escape"){ // esc + if(this._layerMenuTabs === 0 || this._layerMenuTabs === 3 || e.code === "Escape"){ // esc L.DomEvent.stop(e); this._focusOnLayerControl(); } - } else if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) { //up arrow + } else if (e.code === "ArrowUp" || (e.shiftKey && e.code === "Tab")) { //up arrow if (!this._copySubMenu.hasAttribute('hidden') && (document.activeElement.shadowRoot === null || //null happens when the focus is on submenu and when mouse hovers on main menu, submenu disappears document.activeElement.shadowRoot.activeElement.innerHTML === this._copySubMenu.children[this._menuItems.CPYMENUMAP].innerHTML)) { //"map" on submenu @@ -829,7 +829,7 @@ export var ContextMenu = L.Handler.extend({ this._setActiveItem(this._items.length - 1); } } - } else if (e.key === "ArrowDown" || e.key === "Tab") { //down arrow + } else if (e.code === "ArrowDown" || e.code === "Tab") { //down arrow if (!this._copySubMenu.hasAttribute('hidden') && (document.activeElement.shadowRoot === null || document.activeElement.shadowRoot.activeElement.innerHTML === this._copySubMenu.children[this._menuItems.CPYMENULOC].innerHTML)) { //"map" on submenu @@ -872,7 +872,7 @@ export var ContextMenu = L.Handler.extend({ this._setActiveItem(nextIndex); } } - } else if (e.key === "ArrowRight") { //right arrow + } else if (e.code === "ArrowRight") { //right arrow if (document.activeElement.shadowRoot !== null && document.activeElement.shadowRoot.activeElement.innerHTML === this._items[this._menuItems.CTXCOPY].el.el.innerHTML && //'copy' @@ -883,7 +883,7 @@ export var ContextMenu = L.Handler.extend({ !this._copySubMenu.hasAttribute('hidden')) { this._copySubMenu.children[0].focus(); } - } else if (e.key === "ArrowLeft") { //left arrow + } else if (e.code === "ArrowLeft") { //left arrow if (!this._copySubMenu.hasAttribute('hidden') && document.activeElement.shadowRoot !== null) { if (document.activeElement.shadowRoot.activeElement.innerHTML === this._copySubMenu.children[this._menuItems.CPYMENUMAP].innerHTML || @@ -893,7 +893,7 @@ export var ContextMenu = L.Handler.extend({ this._setActiveItem(this._menuItems.CTXCOPY); } } - } else if (e.key === "Escape") { //esc key + } else if (e.code === "Escape") { //esc key if (document.activeElement.shadowRoot === null) { this._hide(); } else { @@ -908,15 +908,16 @@ export var ContextMenu = L.Handler.extend({ this._hide(); } } + // I don't understand the logic here, but tests break without it... + } else if(e.code !== "Shift" && e.code!== "Tab" && + !(!(this._layerClicked.className.includes('mapml-layer-item')) && e.code === "KeyC") && + (document.activeElement.shadowRoot.activeElement.innerHTML !== this._items[this._menuItems.CTXCOPY].el.el.innerHTML)){ + this._hide(); } -// else if(key !== 16 && key!== 9 && -// !(!(this._layerClicked.className.includes('mapml-layer-item')) && key === 67) && -// (path[0].innerText !== (M.options.locale.cmCopyCoords + " (C)"))){ -// this._hide(); -// } - switch(e.key){ + // using KeyboardEvent.code for its mnemonics + switch(e.code){ case "Enter": //ENTER KEY - if(document.activeElement.shadowRoot.activeElement.innerHTML === this._items[5].el.el.innerHTML){ + if(document.activeElement.shadowRoot.activeElement.innerHTML === this._items[this._menuItems.CTXCOPY].el.el.innerHTML){ this._copyCoords({ latlng:this._map.getCenter() }); @@ -926,48 +927,39 @@ export var ContextMenu = L.Handler.extend({ this._map._container.parentNode.activeElement.click(); } break; - case " ": //SPACE KEY + case "Space": //SPACE KEY if(this._map._container.parentNode.activeElement.parentNode.classList.contains("mapml-contextmenu")) this._map._container.parentNode.activeElement.click(); break; - case "c": - case "C": //C KEY + case "KeyC": //C KEY this._copyCoords({ latlng:this._map.getCenter() }); this._copySubMenu.firstChild.focus(); break; - case "d": - case "D": //D KEY + case "KeyD": //D KEY this._toggleDebug(e); break; - case "m": - case "M": //M KEY + case "KeyM": //M KEY this._copyMapML(e); break; - case "l": - case "L": //L KEY + case "KeyL": //L KEY if(this._layerClicked.className.includes('mapml-layer-item')) this._copyLayer(e); break; - case "f": - case "F": //F KEY + case "KeyF": //F KEY this._toggleFullScreen(e); break; - case "p": - case "P": //P KEY + case "KeyP": //P KEY this._paste(e); break; - case "t": - case "T": //T KEY + case "KeyT": //T KEY this._toggleControls(e); break; - case "v": - case "V": //V KEY + case "KeyV": //V KEY this._viewSource(e); break; - case "z": - case "Z": //Z KEY + case "KeyZ": //Z KEY if(this._layerClicked.className.includes('mapml-layer-item')) this._zoomToLayer(e); break; diff --git a/test/e2e/core/layerContextMenu.test.js b/test/e2e/core/layerContextMenu.test.js index af9259a99..06c3cf61c 100644 --- a/test/e2e/core/layerContextMenu.test.js +++ b/test/e2e/core/layerContextMenu.test.js @@ -5,6 +5,7 @@ test.describe("Playwright Layer Context Menu Tests", () => { let context; test.beforeAll(async () => { context = await chromium.launchPersistentContext(''); + await context.grantPermissions(["clipboard-read", "clipboard-write"]); page = context.pages().find((page) => page.url() === 'about:blank') || await context.newPage(); await page.goto("layerContextMenu.html"); }); From f22b0007ef9ff87d5f0e969a11f5c10eae4d2458 Mon Sep 17 00:00:00 2001 From: prushfor Date: Thu, 4 May 2023 12:52:44 -0400 Subject: [PATCH 05/14] Fix Firefox issue where Shift+F10 would always display the context menu near the origin of the map container. --- src/mapml/handlers/ContextMenu.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/mapml/handlers/ContextMenu.js b/src/mapml/handlers/ContextMenu.js index 78a28e9cf..8a3781f01 100644 --- a/src/mapml/handlers/ContextMenu.js +++ b/src/mapml/handlers/ContextMenu.js @@ -808,8 +808,24 @@ export var ContextMenu = L.Handler.extend({ let layerList = this._map.options.mapEl.layers; this._layerClicked = Array.from(layerList).find((el) => el.checked); // the 'hidden' attribute must be removed before any attempt to get the size of container + let pt = e.containerPoint; + // this is for firefox, which reports the e.containerPoint as x=0 when you + // use a keyboard Shift+F10 to display the context menu; this appears + // to be because blink returns a PointerEvent of type==='contextmenu', + // while gecko returns an object (for e.originalEvent). + if ( + this._clickEvent.originalEvent.clientX === 0 || + this._clickEvent.originalEvent.clientY === 0 + ) { + const getCenter = function (el) { + let w = el.getBoundingClientRect().width; + let h = el.getBoundingClientRect().height; + return { x: Number.parseInt(w / 2), y: Number.parseInt(h / 2) }; + }; + pt = getCenter(this._map.getContainer()); + } this._container.removeAttribute('hidden'); - this._showAtPoint(e.containerPoint, e, this._container); + this._showAtPoint(pt, e, this._container); this._updateCS(); } if (e.originalEvent.button === 0 || e.originalEvent.button === -1) { From a48e7cea0b20f14bdba1e6382d782babfc2e78a0 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Thu, 4 May 2023 17:16:05 -0400 Subject: [PATCH 06/14] Add tests for tab / shift+tab keyboard navigation of context menus --- test/e2e/core/ArrowKeyNavContextMenu.html | 2 +- .../e2e/core/layerContextMenuKeyboard.test.js | 190 ++++++++++++++++++ test/e2e/core/mapContextMenuKeyboard.test.js | 76 +++++++ 3 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 test/e2e/core/layerContextMenuKeyboard.test.js create mode 100644 test/e2e/core/mapContextMenuKeyboard.test.js diff --git a/test/e2e/core/ArrowKeyNavContextMenu.html b/test/e2e/core/ArrowKeyNavContextMenu.html index 373c1c4de..1ee2bcd90 100644 --- a/test/e2e/core/ArrowKeyNavContextMenu.html +++ b/test/e2e/core/ArrowKeyNavContextMenu.html @@ -3,7 +3,7 @@ - locateApi.html + ArrowKeyNavContextMenu.html