1

Cross posted on the Esri Community, posting here because traffic there seems to be low.

We are working to implement a keyboard map click to open a map popup. With the map application element focused, the user can pan using the arrow keys and press enter or spacebar to get a popup for the center of the map (where we float a crosshairs). After the enter or spacebar event, it is very useful to change the focus to the popup so the user can access controls for collapsing, docking, and closing the popup without having to tab through all of the other actionable items (e.g. widget buttons) first. It is also useful to first focus on the popup title so a screen reader reads the title after the click.

Please see this codepen: https://codepen.io/jeffreinhart/pen/LYajqNW

This should be easy enough, right? Just wrap the popup title text in a div with a class to select and a tabindex:

`<div tabindex='0' class='popup-title'>Reverse geocode: ${lon}, ${lat}</div>`,

But then, we need to wait for the SDK to add that element to the DOM before we can set focus, so we wait for the waitForElm() function to complete (line 113). That function uses a mutation observer, a link to the SO description is provided in the function definition.

At this point, popupTitleElement.focus() on line 116 should work. But it doesn’t. I cannot figure out what else I need to wait for to be able to focus this element. Best I could come up with is a setInterval() that stops when the element finally gets focus.

Anybody have any idea what else is happening that could be blocking setting focus?

Here is the full code:

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
    <title>Intro to popups | Sample | ArcGIS Maps SDK for JavaScript 4.28</title>

    <style>
      html,
      body,
      #viewDiv {
        padding: 0;
        margin: 0;
        height: 100%;
        width: 100%;
      }

      #instruction {
        z-index: 99;
        position: absolute;
        top: 15px;
        left: 50%;
        padding: 5px;
        margin-left: -175px;
        height: 30px;
        width: 355px;
        background: rgba(25, 25, 25, 0.8);
        color: white;
      }
      .popup-title:focus {
        border: 2px solid red;
      }
    </style>

    <link rel="stylesheet" href="https://js.arcgis.com/4.28/esri/themes/light/main.css" />
    <script src="https://js.arcgis.com/4.28/"></script>

    <script>     
      require(["esri/rest/locator", "esri/Map", "esri/views/MapView"], (locator, Map, MapView) => {
        // https://stackoverflow.com/a/61511955/7507283
        function waitForElm(selector) {
          return new Promise(resolve => {
            if (document.querySelector(selector)) {
              return resolve(document.querySelector(selector));
            }

            const observer = new MutationObserver(mutations => {
              if (document.querySelector(selector)) {
                observer.disconnect();
                resolve(document.querySelector(selector));
              }
            });

            // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
            observer.observe(document.body, {
              childList: true,
              subtree: true
            });
          });
        }
        
        // Set up a locator url using the world geocoding service
        const locatorUrl = "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer";

        // Create the Map
        const map = new Map({
          basemap: "streets-navigation-vector"
        });

        // Create the MapView
        const view = new MapView({
          container: "viewDiv",
          map: map,
          center: [-71.6899, 43.7598],
          zoom: 12
        });

        /*******************************************************************
         * This click event sets generic content on the popup not tied to
         * a layer, graphic, or popupTemplate. The location of the point is
         * used as input to a reverse geocode method and the resulting
         * address is printed to the popup content.
         *******************************************************************/
        view.popupEnabled = false;
        view.on("click", (event) => {
          // Get the coordinates of the click on the view
          const lat = Math.round(event.mapPoint.latitude * 1000) / 1000;
          const lon = Math.round(event.mapPoint.longitude * 1000) / 1000;

          view.openPopup({
            // Set the popup's title to the coordinates of the location
            title: `<div tabindex='0' class='popup-title'>Reverse geocode: ${lon}, ${lat}</div>`,
            location: event.mapPoint // Set the location of the popup to the clicked location
          });

          const params = {
            location: event.mapPoint
          };

          // Display the popup
          // Execute a reverse geocode using the clicked location
          locator
            .locationToAddress(locatorUrl, params)
            .then((response) => {
              // If an address is successfully found, show it in the popup's content
              view.popup.content = response.address;
            })
            .catch(() => {
              // If the promise fails and no result is found, show a generic message
              view.popup.content = "No address was found for this location";
            });
          
          // Set focus
          waitForElm('.popup-title').then((popupTitleElement) => {
            // setting focus here does not work even though the element has been added to the DOM...
            console.log('the element:', popupTitleElement)
            popupTitleElement.focus();
            console.log('current active element:', document.activeElement)
            // Best solution I could find...
            intervalId = setInterval(() => {
              popupTitleElement.focus();
              console.log('is it focused yet?')
              if (document.activeElement === popupTitleElement) {
                console.log('yay! it is focused!')
                clearInterval(intervalId)
              }
            }, 10)
          })
        });
      });
    </script>
  </head>

  <body>
    <div id="viewDiv"></div>
    <div id="instruction" class="esri-widget">Click any location on the map to see its street address</div>
  </body>
</html>
1
  • Thanks for the tip, I added the code. Jan 25 at 21:39

1 Answer 1

1

Opening popup with view.openPopup returns a promise. You can wait till popup is created and then use view.popup.focus() method to set focus to the popup.

Relevant part of the code could then look something like this:

view.openPopup({
  title: `<div tabindex='0' class='popup-title'>Reverse geocode: ${lon}, ${lat}</div>`,
  location: event.mapPoint
}).then(() => {
  view.popup.focus();
});

Here is working JSFiddle: https://jsfiddle.net/TomazicM/unj7d9er/

7
  • Unfortunately, this does not work. I tested it in this codepen: codepen.io/jeffreinhart/pen/bGZYLEL. Using your suggested solution, I believe what we have going on is a race issue. Though the popup has been made visible and has content, the title attribute of the popup is not rendered yet, so we see "Uncaught TypeError: Cannot read properties of undefined (reading 'focus')". This is why I am using a mutation observer in my initial example to watch for the div.popup-title to be added to the DOM. Jan 26 at 21:42
  • Yes, you are right. I tested solution just a few times and it worked then. Now I tested it more thoroughly and sometimes it works and sometimes not. I'll see what can be done here.
    – TomazicM
    Jan 27 at 10:17
  • See modified answer.
    – TomazicM
    Jan 27 at 15:11
  • Update... this has been confirmed by Esri as a bug with a ticket to fix in a later version. community.esri.com/t5/arcgis-javascript-maps-sdk-questions/…. See the comment from RalucaNicola1 on 2024-01-30. Jan 30 at 21:08
  • Bug or not, the above solution works, and as such is answer to your question.
    – TomazicM
    Jan 30 at 21:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.