From d522be10f5fde94b8f72459897794e0e1eac2be8 Mon Sep 17 00:00:00 2001 From: alexgao1 Date: Mon, 30 Sep 2024 14:40:14 +0000 Subject: [PATCH] Misc. Accessibility (#1246) * form links focus styling * Layer switcher keyboard navigable * Create variant of default keyboard controls that selects features * Adjust bounds creation * tabbable force graph nodes * Search accessible by tab/enter * Focus target icon when map in keyboard navigation --- .../nunaliit2/css/basic/n2.mapAndControls.css | 8 + .../main/js/nunaliit2/css/basic/n2.theme.css | 4 + ...ssibilityMapAndControlsKeyboardControls.js | 159 ++++++++++++++++++ .../main/js/nunaliit2/n2.canvasForceGraph.js | 6 + .../src/main/js/nunaliit2/n2.couchSearch.js | 20 +++ .../main/js/nunaliit2/n2.mapAndControls.js | 17 +- .../main/js/nunaliit2/n2.olLayerSwitcher.js | 13 ++ 7 files changed, 220 insertions(+), 7 deletions(-) create mode 100644 nunaliit2-js/src/main/js/nunaliit2/n2.accessibilityMapAndControlsKeyboardControls.js diff --git a/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.mapAndControls.css b/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.mapAndControls.css index 5cba946c6..d87115f78 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.mapAndControls.css +++ b/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.mapAndControls.css @@ -353,3 +353,11 @@ table.mediaSelection td { .n2_content_map:focus { border: solid #444444 2px; } + +.olMap.n2_content_map.enabledKeyboardControls:focus > .olMapViewport::before { + content: url('data:image/svg+xml;charset=UTF-8, '); + top: 50%; + left: 50%; + position: relative; + z-index: 1000; +} diff --git a/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.theme.css b/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.theme.css index 86577bdc1..7a68e8608 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.theme.css +++ b/nunaliit2-js/src/main/js/nunaliit2/css/basic/n2.theme.css @@ -479,6 +479,10 @@ CreateDocument widget border: 1px solid #9e9e9e; } +.nunaliit_form_link:focus { + border: 3px solid rgb(0, 255,255) +} + .nunaliit_form_link_tree_view { display: none; } diff --git a/nunaliit2-js/src/main/js/nunaliit2/n2.accessibilityMapAndControlsKeyboardControls.js b/nunaliit2-js/src/main/js/nunaliit2/n2.accessibilityMapAndControlsKeyboardControls.js new file mode 100644 index 000000000..6cf1643c3 --- /dev/null +++ b/nunaliit2-js/src/main/js/nunaliit2/n2.accessibilityMapAndControlsKeyboardControls.js @@ -0,0 +1,159 @@ +/* +Copyright (c) 2024, Geomatics and Cartographic Research Centre, Carleton +University +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + - Neither the name of the Geomatics and Cartographic Research Centre, + Carleton University nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +*/ + +; (function ($n2) { + "use strict" + + if (typeof (OpenLayers) !== 'undefined' && OpenLayers.Handler) { + + OpenLayers.Control.MapAndControlsKeyboardControls = OpenLayers.Class(OpenLayers.Control.KeyboardDefaults, { + initialize: function (layers, options) { + OpenLayers.Control.prototype.initialize.apply(this, [options]); + if (OpenLayers.Util.isArray(layers)) { + this.layers = layers + } + else { + this.layers = [layers] + } + if (options.dispatch) this.dispatch = options.dispatch + this.DH = 'MapAndControlsKeyboardControls' + this.defaultSelectionRadius = 100000 + }, + + selectAtMapCenter: function (position) { + // Select everything in the bounds + let docs = [] + const mapZoom = this.map.getZoom() + const radius = this.defaultSelectionRadius / mapZoom + const { lon, lat } = position + var bounds = new OpenLayers.Bounds( + lon - (radius), lat - (radius), lon + (radius), lat + (radius) + ); + var layers = this.layers + var layer; + for (var l = 0; l < layers.length; ++l) { + layer = layers[l]; + for (var i = 0, len = layer.features.length; i < len; ++i) { + var feature = layer.features[i]; + if (!feature.getVisibility()) { + continue; + } + if (bounds.toGeometry().intersects(feature.geometry)) { + // layer.events.triggerEvent("featureselected", {feature: feature}); + const cluster = feature.cluster + if (cluster) { + docs = docs.concat(cluster.map(v => v.data._id)) + } + else { + docs.push(feature.data._id) + } + } + } + } + this.dispatch.send(this.DH, { + type: 'userSelect', + docIds: docs + }) + }, + + defaultKeyPress: function (evt) { + var size, handled = true; + + var target = OpenLayers.Event.element(evt); + if (target && + (target.tagName == 'INPUT' || + target.tagName == 'TEXTAREA' || + target.tagName == 'SELECT')) { + return; + } + + switch (evt.keyCode) { + case OpenLayers.Event.KEY_LEFT: + this.map.pan(-this.slideFactor, 0); + break; + case OpenLayers.Event.KEY_RIGHT: + this.map.pan(this.slideFactor, 0); + break; + case OpenLayers.Event.KEY_UP: + this.map.pan(0, -this.slideFactor); + break; + case OpenLayers.Event.KEY_DOWN: + this.map.pan(0, this.slideFactor); + break; + case OpenLayers.Event.KEY_RETURN: + this.selectAtMapCenter(this.map.getCenter()) + break; + + case 33: // Page Up. Same in all browsers. + size = this.map.getSize(); + this.map.pan(0, -0.75 * size.h); + break; + case 34: // Page Down. Same in all browsers. + size = this.map.getSize(); + this.map.pan(0, 0.75 * size.h); + break; + case 35: // End. Same in all browsers. + size = this.map.getSize(); + this.map.pan(0.75 * size.w, 0); + break; + case 36: // Home. Same in all browsers. + size = this.map.getSize(); + this.map.pan(-0.75 * size.w, 0); + break; + + case 43: // +/= (ASCII), keypad + (ASCII, Opera) + case 61: // +/= (Mozilla, Opera, some ASCII) + case 187: // +/= (IE) + case 107: // keypad + (IE, Mozilla) + this.map.zoomIn(); + break; + case 45: // -/_ (ASCII, Opera), keypad - (ASCII, Opera) + case 109: // -/_ (Mozilla), keypad - (Mozilla, IE) + case 189: // -/_ (IE) + case 95: // -/_ (some ASCII) + this.map.zoomOut(); + break; + default: + handled = false; + } + if (handled) { + // prevent browser default not to move the page + // when moving the page with the keyboard + OpenLayers.Event.stop(evt); + } + } + }) + + + } + +})(nunaliit2) \ No newline at end of file diff --git a/nunaliit2-js/src/main/js/nunaliit2/n2.canvasForceGraph.js b/nunaliit2-js/src/main/js/nunaliit2/n2.canvasForceGraph.js index 5f5574cb0..257964ee6 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/n2.canvasForceGraph.js +++ b/nunaliit2-js/src/main/js/nunaliit2/n2.canvasForceGraph.js @@ -929,9 +929,15 @@ var ForceGraph = $n2.Class({ return this.ownerDocument.createElementNS(this.namespaceURI, "circle"); }) .attr('class','node') + .attr('tabindex', 0) .on('click', function(n,i){ _this._initiateMouseClick(n); }) + .on('keydown', function (n) { + if (d3?.event?.key === 'Enter') { + _this._initiateMouseClick(n) + } + }) .on('mouseover', function(n,i){ _this._initiateMouseOver(n); }) diff --git a/nunaliit2-js/src/main/js/nunaliit2/n2.couchSearch.js b/nunaliit2-js/src/main/js/nunaliit2/n2.couchSearch.js index a9794052b..98b53218a 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/n2.couchSearch.js +++ b/nunaliit2-js/src/main/js/nunaliit2/n2.couchSearch.js @@ -1696,6 +1696,26 @@ var SearchServer = $n2.Class({ // Search icon var searchIcon = $('
') .addClass('searchIcon') + .attr('tabindex', 0) + .on('keydown', (ev) => { + if (ev.key === 'Enter') { + if( $('.nunaliit_search_input').hasClass('search_active') ){ + this.dispatchService.send(DH,{ + type: 'searchDeactivated' + }); + + } else if( $('.nunaliit_search_input').hasClass('search_inactive') ){ + this.dispatchService.send(DH,{ + type: 'searchActivated' + }); + + } else { + this.dispatchService.send(DH,{ + type: 'searchActivated' + }); + }; + } + }) .appendTo($elem); // Text box diff --git a/nunaliit2-js/src/main/js/nunaliit2/n2.mapAndControls.js b/nunaliit2-js/src/main/js/nunaliit2/n2.mapAndControls.js index 86176b74d..6e44d863c 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/n2.mapAndControls.js +++ b/nunaliit2-js/src/main/js/nunaliit2/n2.mapAndControls.js @@ -1570,13 +1570,6 @@ var MapAndControls = $n2.Class('MapAndControls',{ this.map.addControl(scaleLine); }; - if (this.options.enableKeyboardControls) { - const keyboardControls = new OpenLayers.Control.KeyboardDefaults() - this.map.div.tabIndex = 0 - keyboardControls.observeElement = this.map.div - this.map.addControl(keyboardControls) - } - // Disable zoom on mouse wheel if( this.options.enableWheelZoom ) { // Do nothing. Enabled by default @@ -1774,6 +1767,16 @@ var MapAndControls = $n2.Class('MapAndControls',{ // Handle feature events this._installFeatureSelector(); + + if (this.options.enableKeyboardControls) { + const dispatch = this._getDispatchService() + if (!dispatch) throw new Error("Failed to obtain dispatch service for keyboard controls") + const keyboardControls = new OpenLayers.Control.MapAndControlsKeyboardControls(this.vectorLayers, { dispatch }) + this.map.div.tabIndex = 0 + this.map.div.classList.add('enabledKeyboardControls') + keyboardControls.observeElement = this.map.div + this.map.addControl(keyboardControls) + } // Select adding of new features if( this.options.addPointsOnly ) { diff --git a/nunaliit2-js/src/main/js/nunaliit2/n2.olLayerSwitcher.js b/nunaliit2-js/src/main/js/nunaliit2/n2.olLayerSwitcher.js index dc0915c29..76d8b313a 100644 --- a/nunaliit2-js/src/main/js/nunaliit2/n2.olLayerSwitcher.js +++ b/nunaliit2-js/src/main/js/nunaliit2/n2.olLayerSwitcher.js @@ -191,12 +191,25 @@ OpenLayers.Control.NunaliitLayerSwitcher = // populate div with current info this.redraw(); + this.div.tabIndex = 0 + // Do not let click events leave the control and reach the map // This allows the html elements to function properly $(this.div).click((ev) => { this._suppressedClick(ev) }); + $(this.div).keydown((ev) => { + if (ev.key === 'Enter') { + const theCurrentlyVisibleButton = [...this.div.children] + .filter(child => child.classList.contains("olButton")) + .find(button => button.style.display !== 'none') + ev.target = theCurrentlyVisibleButton + this._simulateClick(ev) + this._suppressedClick(ev) + } + }); + // Suppress double click $(this.div).dblclick(function(e){ if (e.stopPropagation) {