Unexpected behavior when using keyboard to tab through div/input fields generated using JS

34 views Asked by At

I'm building a page with several input fields each inside a series of DIVs. The inputs themselves are appended programatically to the page using this code:

function addressAutocomplete(containerElement, callback, options) {     
    var inputElement = document.createElement("input");
    inputElement.setAttribute("type", "text");
    inputElement.setAttribute("class", "addressAutocompleteInput");
    inputElement.setAttribute("id", containerElement.id + "-input");
    inputElement.setAttribute("placeholder", options.placeholder);
    inputElement.setAttribute("autocomplete","off");
    inputElement.setAttribute("onfocus", "clickTab(this.parentNode.parentNode.parentNode)");
      
    containerElement.appendChild(inputElement);
}

The DIVs containing the inputs are also created programmatically in this loop based on the number of maps (in this case numMaps = 4):

for (i = 1; i <= numTabs; i++) {
    html = "<div id='MapTab" + i + "' class='addressPickerTab' onclick='clickTab(this)''>";
    html += "<div>";
    html += "<label>Addresss " + i + "</label><span>Incomplete</span>";
    html += "<div class='autocomplete-container' id='autocomplete-container" + i + "'></div>";
    html += "</div>";
    html += "</div>";
    html += "</div>";
    addressesContainer.innerHTML += html;
} 

Finally, I have a function that handles opening/closing the tabs when they're clicked:

function clickTab(tab) {
    clickedTab = document.getElementById(tab.id);
  var tabNum = clickedTab.id.substring(clickedTab.id.indexOf("MapTab") + 6);
  currentMap = tabNum;

    if (clickedTab.classList.contains('active')) { //if the clicked tab is already active
    if (document.activeElement.classList[0] != "addressAutocompleteInput") { //if the active element isn't the autocomplete dropdown then minimize
      clickedTab.classList.remove("active");
      //clickedTab.nextElementSibling.classList.remove("active");
    }
  } else { //if the clicked tab is not currently active
    for (i = 0; i < clickedTab.parentElement.children.length; i++) { //close any tabs that are 'active'
      if (clickedTab.parentElement.children[i].classList.contains('active')) {
        // minimize the tab and hide the details
        clickedTab.parentElement.children[i].classList.remove("active");
      }     
    }
        
    //set the clicked tab to active
    clickedTab.classList.add("active");
    
    document.getElementById("autocomplete-container" + currentMap + "-input").focus();


  }

}

Everything is working fine when using the mouse to click between tabs/inputs but for some reason when using the 'tab' button the tab minimizes but the input stays visible 2from then on and some other sizing and such is off.

I've been searching for a solution (or even an idea as to what's going on) without much success. I did stumble upon the fact that if I use chrome inspector and uncheck/recheck the height attribute of the div containing the input (MapTab#) it fixes the problem.

Any help or direction would be greatly appreciated.

Here the full code and a link to a JSFiddle.

var numTabs = 4;

//populate based on number of maps
addressesContainer = document.getElementById('addressPickerContainer').getElementsByClassName('addressPickerTabColumn')[0];
for (i = 1; i <= numTabs; i++) {

  html = "<div id='MapTab" + i + "' class='addressPickerTab' onclick='clickTab(this)''>";
  html += "<div>";
  html += "<label>Addresss " + i + "</label><span>Incomplete</span>";
  html += "<div class='autocomplete-container' id='autocomplete-container" + i + "'></div>";
  html += "</div>";
  html += "</div>";
  html += "</div>";
  addressesContainer.innerHTML += html;
}

for (i = 1; i <= numTabs; i++) {

  addressAutocomplete(document.getElementById("autocomplete-container" + i), (data) => {
    if (data) {
      //THIS IS WHAT HAPPENS WHEN AN ADDRESS IS SELECTED
            console.log("map will eventually populate.");
            console.log("Lon: " + data.properties.lon, "Lat: " + data.properties.lat);

    } else {
      //THIS HAPPENS WHEN AN ITEM IS BEING CLEARED

    }

  }, {
    placeholder: "Enter an address for map " + i
  });

}
    




function clickTab(tab) {
    clickedTab = document.getElementById(tab.id);
  var tabNum = clickedTab.id.substring(clickedTab.id.indexOf("MapTab") + 6);
  currentMap = tabNum;

    if (clickedTab.classList.contains('active')) { //if the clicked tab is already active
    if (document.activeElement.classList[0] != "addressAutocompleteInput") { //if the active element isn't the autocomplete dropdown then minimize
      clickedTab.classList.remove("active");
      //clickedTab.nextElementSibling.classList.remove("active");
    }
  } else { //if the clicked tab is not currently active
    for (i = 0; i < clickedTab.parentElement.children.length; i++) { //close any tabs that are 'active'
      if (clickedTab.parentElement.children[i].classList.contains('active')) {
        // minimize the tab and hide the details
        clickedTab.parentElement.children[i].classList.remove("active");
      }     
    }
        
    //set the clicked tab to active
    clickedTab.classList.add("active");
    
    document.getElementById("autocomplete-container" + currentMap + "-input").focus();


  }

}

/************** JS FOR ADDRESS AUTO-COMPLETE **************/

/* 
    The addressAutocomplete takes as parameters:
  - a container element (div)
  - callback to notify about address selection
  - geocoder options:
     - placeholder - placeholder text for an input element
     - type - location type
*/
    
function addressAutocomplete(containerElement, callback, options) {
  // create input element
    
  var inputElement = document.createElement("input");
  inputElement.setAttribute("type", "text");
  inputElement.setAttribute("class", "addressAutocompleteInput");
  inputElement.setAttribute("id", containerElement.id + "-input");
  inputElement.setAttribute("placeholder", options.placeholder);
    inputElement.setAttribute("autocomplete","off");
    inputElement.setAttribute("onfocus", "clickTab(this.parentNode.parentNode.parentNode)");

    
  containerElement.appendChild(inputElement);

  // add input field clear button
  var clearButton = document.createElement("div");
  clearButton.classList.add("clear-button");
  addIcon(clearButton);

  //when a clear button is clicked
  clearButton.addEventListener("click", (e) => {
    //clear the currently selected address
    e.stopPropagation();
    inputElement.value = '';
    callback(null);
    clearButton.classList.remove("visible");
    document.getElementById("autocomplete-container" + currentMap + "-input").focus();
    document.getElementById('MapTab' + currentMap).children[0].children[1].style.color = "red";
    closeDropDownList();
  });
  containerElement.appendChild(clearButton);

  /* Current autocomplete items data (GeoJSON.Feature) */
  var currentItems;

  /* Active request promise reject function. To be able to cancel the promise when a new request comes */
  var currentPromiseReject;

  /* Focused item in the autocomplete list. This variable is used to navigate with buttons */
  var focusedItemIndex;

  /* Execute a function when someone writes in the text field: */
  inputElement.addEventListener("input", function(e) {
    var currentValue = this.value;
    var currentInput = this.id;


    /* Close any already open dropdown list */
    closeDropDownList();

    // Cancel previous request promise
    if (currentPromiseReject) {
      currentPromiseReject({
        canceled: true
      });
    }

    if (!currentValue) {
      clearButton.classList.remove("visible");
      return false;
    }

    // Show clearButton when there is a text
    clearButton.classList.add("visible");

    /* Create a new promise and send geocoding request */
    var promise = new Promise((resolve, reject) => {
      currentPromiseReject = reject;

      var apiKey = "47f523a46b944b47862e39509a7833a9";
      var url = `https://api.geoapify.com/v1/geocode/autocomplete?text=${encodeURIComponent(currentValue)}&limit=5&apiKey=${apiKey}`;

      if (options.type) {
        url += `&type=${options.type}`;
      }

      fetch(url)
        .then(response => {
          // check if the call was successful
          if (response.ok) {
            response.json().then(data => resolve(data));
          } else {
            response.json().then(data => reject(data));
          }
        });
    });

    promise.then((data) => {
      currentItems = data.features;

      /*create a DIV element that will contain the items (values):*/
      var autocompleteItemsElement = document.createElement("div");
      autocompleteItemsElement.setAttribute("class", "autocomplete-items");
      containerElement.appendChild(autocompleteItemsElement);

      /* For each item in the results */
      data.features.forEach((feature, index) => {
        /* Create a DIV element for each element: */
        var itemElement = document.createElement("DIV");
        /* Set formatted address as item value */
        itemElement.innerHTML = feature.properties.formatted;

        /* Set the value for the autocomplete text field and notify: */
        itemElement.addEventListener("click", function(e) {
          inputElement.value = currentItems[index].properties.formatted;

          //selecting address by clicking          
          callback(currentItems[index]);
          /* Close the list of autocompleted values: */
          closeDropDownList();
          /* give the focus back to the input field */
          document.getElementById(inputElement.id).focus();

        });

        autocompleteItemsElement.appendChild(itemElement);
      });
    }, (err) => {
      if (!err.canceled) {
        console.log(err);
      }
    });
  });

  /* Add support for keyboard navigation */
  inputElement.addEventListener("keydown", function(e) {
    var autocompleteItemsElement = containerElement.querySelector(".autocomplete-items");
    if (autocompleteItemsElement) {
      var itemElements = autocompleteItemsElement.getElementsByTagName("div");
      if (e.keyCode == 40) { //downarrow
        e.preventDefault();
        /*If the arrow DOWN key is pressed, increase the focusedItemIndex variable:*/
        focusedItemIndex = focusedItemIndex !== itemElements.length - 1 ? focusedItemIndex + 1 : 0;
        /*and and make the current item more visible:*/
        setActive(itemElements, focusedItemIndex);
        
      } else if (e.keyCode == 38) { //uparrow
        e.preventDefault();

        /*If the arrow UP key is pressed, decrease the focusedItemIndex variable:*/
        focusedItemIndex = focusedItemIndex !== 0 ? focusedItemIndex - 1 : focusedItemIndex = (itemElements.length - 1);
        /*and and make the current item more visible:*/
        setActive(itemElements, focusedItemIndex);
        /* get the number of the input being seelected */
        /*
             var mySubString = inputElement.id.substring(
          inputElement.id.indexOf("autocomplete-container") + 22,
          inputElement.id.lastIndexOf("-input")
        );
        inputNum = mySubString.charCodeAt(0) - 64;
                */
        //alert('input ' + inputNum + ' selected.');
        /* end */
      } else if (e.keyCode == 13) { //enter

        /* If the ENTER key is pressed and value as selected, close the list*/
        e.preventDefault();
        const itmNum = focusedItemIndex;

        if (focusedItemIndex > -1) {
          closeDropDownList();
        }

        document.getElementById("layoutImage").style.display = "none";
        document.getElementById("currentMap").innerHTML = "Viewing: Map " + currentMap;

        setActive(itemElements, itmNum);
        tmp_center = ol.proj.fromLonLat([currentItems[itmNum].properties.lon, currentItems[itmNum].properties.lat]);
        setMap(tmp_center, defaultZoom, defaultRotation);

      }
    } else {
      if (e.keyCode == 40) { //downarrow
        /* Open dropdown list again */
        var event = document.createEvent('Event');
        event.initEvent('input', true, true);
        inputElement.dispatchEvent(event);
      }
    }


  });

  // Used when scrolling autocomplete dropdown with keyboard
  function setActive(items, index) {
    if (!items || !items.length) return false;

    for (var i = 0; i < items.length; i++) {
      items[i].classList.remove("autocomplete-active");
    }
    /* Add class "autocomplete-active" to the active element*/
    items[index].classList.add("autocomplete-active");


    // Change input value and notify
    inputElement.value = currentItems[index].properties.formatted;

    callback(currentItems[index]);

  }

  function closeDropDownList() {
    var autocompleteItemsElement = containerElement.querySelector(".autocomplete-items");
    if (autocompleteItemsElement) {
      containerElement.removeChild(autocompleteItemsElement);
    }

    focusedItemIndex = -1;
  }

  function addIcon(buttonElement) {
    var svgElement = document.createElementNS("http://www.w3.org/2000/svg", 'svg');
    svgElement.setAttribute('viewBox', "0 0 24 24");
    svgElement.setAttribute('height', "24");

    var iconElement = document.createElementNS("http://www.w3.org/2000/svg", 'path');
    iconElement.setAttribute("d", "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z");
    iconElement.setAttribute('fill', 'currentColor');
    svgElement.appendChild(iconElement);
    buttonElement.appendChild(svgElement);
  }

  /* Close the autocomplete dropdown when the document is clicked. 
    Skip, when a user clicks on the input field */
  document.addEventListener("click", function(e) {
    if (e.target !== inputElement) {
      closeDropDownList();
    } else if (!containerElement.querySelector(".autocomplete-items")) {
      // open dropdown list again
      var event = document.createEvent('Event');
      event.initEvent('input', true, true);
      inputElement.dispatchEvent(event);
    }
  });

}

/**************  END ADDRESS AUTO-COMPLETE   **************/
#addressPickerContainer {
    width: 100%;
  width: 1000px;
  min-width: 500px;
}
.addressPickerTabColumn {
  width: calc(100% - 344px);
  padding-top: 5px;
  float: left;
}
.addressPickerTab {
  position: relative;
  height: 25px;
  border: 2px solid black;
  background-color: white;
  left: 1px;
  padding: 1px 5px;
  cursor: pointer;
  overflow: hidden;
  border-radius: 3px 0px 0px 3px;
  margin-top: 3px;
  background-color: #dadada;
}
.addressPickerTab.active {
  min-height: 70px;
  border-right: none;
  padding: 5px;
  background-color: #fff;
  overflow: visible;
}
.addressPickerTab input {
  margin-top: 5px;
  width: 94%;
  margin: 2px 3%;
}
.addressPickerTab>div {
  position: relative;
  top: -4px;
}
.addressPickerTab label {
  font-size: x-small;
  font-weight: 700;
  margin: 0;
  cursor: pointer;
}
.addressPickerTab>div>span {
  position: relative;
  top: 6px;
  font-size: 0.6em;
  float: right;
  color: red;
}
.addressPickerTab.active > div > span {
  /* color: orange; */
  top: 2px;
  right: 2px;
}
.addressPickerMap {
  border: 2px solid #000;
    height: 300px;
    width: 300px;
  float: left;
  margin-top: 3px;
  padding: 5px;
}


/********* CSS FOR AUTO COMPLETE ADDRESS ******/
.autocomplete-container {
  /*the container must be positioned relative:*/
  position: relative;
  margin-bottom: 20px;
  z-index: 1000;
}
.autocomplete-items {
  position: absolute;
  border: 1px solid rgba(0, 0, 0, 0.1);
  box-shadow: 0px 2px 10px 2px rgba(0, 0, 0, 0.1);
  border-top: none;
  z-index: 99;
  /*position the autocomplete items to be the same width as the container:*/
  top: calc(100% + 2px);
  left: 0;
  right: 0;

  background-color: #fff;
}
.autocomplete-items div {
  padding: 10px;
  cursor: pointer;
}
.autocomplete-items div:hover {
  /*when hovering an item:*/
  background-color: rgba(0, 0, 0, 0.1);
}
.autocomplete-items .autocomplete-active {
  /*when navigating through the items using the arrow keys:*/
  background-color: rgba(0, 0, 0, 0.1);
}
.clear-button {
  color: rgba(0, 0, 0, 0.4);
  cursor: pointer;

  position: absolute;
  right: 5px;
  right: 3%;
  top: 0;

  height: 100%;
  display: none;
  align-items: center;
}
.clear-button.visible {
  display: flex;
}
.clear-button:hover {
  color: rgba(0, 0, 0, 0.6);
}
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.css" rel="stylesheet"/>
<div style="width: 96%; margin: 20px auto;">
    <div id="addressPickerContainer">
            <!-- this will populate programatically -->
            <div class="addressPickerTabColumn" ></div>
        
            <!-- map controls -->
            <div class="addressPickerMap">
                <div id="mapLock"></div>
                <div id="layoutImage"></div>
                <div class="mapBox">
                    <div id="map" class="map"></div>    
                </div>
                
    </div>
    
    
    
</div>

<!-- svg controls the mask placed over the map and 'd' value changes based on appropriate svgpath -->
<svg height="0" width="0">
  <defs>
    <clipPath id="svgPath">
      <path fill="#FFFFFF" stroke="#000000" stroke-miterlimit="10" 
             d="m 0 0 l 280 0 l 0 280 l -280 0 l 0 -280" />
    </clipPath>
  </defs>
</svg>
<!-- end invisible svg -->

0

There are 0 answers