vaxul
vaxul

Reputation: 503

JavaScript <select> on choose, instead of change, when using keyboard

I have the following situation.

Setup

There is a form with one mandatory select control. Similar to this: https://jsfiddle.net/u47by9rd/

When an <option> of a <select> is choosen, I'like to check the selected value and have two paths: a) nothing more to do, so submit the form and do things (in the example 'human') b) not enough info available, so show a second <select> for specifying

Current solution

The first solution uses the change event to check the selected value.

Current state

This works fine for mouse users.

BUT keyboard user have a major issue, since the change event is already fired, when navigate trough the options. So every time the first options is choosen and one of the paths is continued. In the case of the example, the form will be submitted every time.

Expections

A keyboard use should be able to first choose the wanted <option> before continuing the paths.

Do you have any idea, how to solve this?

Upvotes: 0

Views: 80

Answers (1)

Diego D
Diego D

Reputation: 8196

The closest solution to what you are asking, might be to keep track of any key pressed when the dropdown has focus (storing the information in a data attribute) and when the change event occurs on the dropdown, check if the keydown event happened or not one moment before.

Since in terms of accessibility, the dropdown is actually changing its value when navigating the options with the keyboard, its change event handler will just ignore the occurrence in case any key was pressed one moment before (unless it's a TAB) and will show a button next to it so the user will have a further action to perform to pick an option.

In general the change event handler now has a single point where to perform the action when the event really occurred and in this demo it will invoke a callback passed to the initialization function.

The demo models a form having 2 dropdowns. The first one has 2 options, one that will fire the form submission directly and a second one that will show the second dropdown. When any option will be picked on the second dropdown, the form will be submitted. Both dropdown use the keyboard friendly strategy that doesn't fire the form submit event just as soon as an option was picked while navigating with the keyboard, but only when the choice was made explicitely through the dedicated button.

/**
 * Change event handler for the dropdown #dd1
 */
const changeEventHandlerForDD1 = (dd)=>{
  //just prints on console that the value has changed  
  console.log(`the dropdown ${dd.id} value has changed as: ${dd.value}`);  
  if(dd.value == '1'){
    //submits the parent form (the real submission was commented here)
    console.log('Form was submitted!');
    //dd.closest('form').submit();
  }else if(dd.value == '2'){
    //shows the second dd
    document.getElementById('dd2').closest('.row').classList.remove('hidden');
  }
}

/**
 * Change event handler for the dropdown #dd2
 */
const changeEventHandlerForDD2 = (dd)=>{
  //just prints on console that the value has changed  
  console.log(`the dropdown ${dd.id} value has changed as: ${dd.value}`);  
  //submits the parent form (the real submission was commented here)
  console.log('Form was submitted!');
  //dd.closest('form').submit();
}

/**
 * Initialize the dropdowns with the given change event handlers
 */
initDropDown('dd1', 'Pick this option', changeEventHandlerForDD1);
initDropDown('dd2', 'Pick this option', changeEventHandlerForDD2);

/**
 * Initialize a dropdown so that it won't fire the passed changeEventHandler when its options
 * are navigated with the keyboard but will offer the chance to pick the currently
 * selected one through a dynamically added button that when pressed will finally fire
 * the event like if it was selected with the mouse
 */
function initDropDown(id, pickOptionText, changeEventHandler){
  
  const dd = document.getElementById(id);
  
  dd.classList.add('smartdd','control');
  
  //inits the data attribute keyp as false
  dd.dataset.keyp = 'false';

  //adds an event listener for the keydow event to the dropdown
  dd.addEventListener('keydown', (event)=>{    
    //if the pressed key is not a TAB
    if(event.which !== 9){
      //sets the data attribute keyp as true
      event.target.dataset.keyp = 'true';          
    }    
  });
  
  //adds an event listener for the change event to the dropdown
  dd.addEventListener('change', (event)=>{        
    
    const dd = event.target;    
    
    //removes the "pick option" button if it exists
    const nextSibling = dd.nextElementSibling;    
    if (nextSibling && nextSibling.classList.contains('nextaction'))
      nextSibling.remove();
    
    //if the change comes after the key was pressed
    if(dd.dataset.keyp == 'true'){
      console.log('no action performed since the change occurred from the keyboard');
    
      //creates a "pick option" button
      let next = document.createElement('button');
      next.classList.add('nextaction','control');
      next.innerText = pickOptionText;
      //...with the click event handler that will dispatch the change event to the dropdown
      next.addEventListener('click',(event)=>{        
        event.target.previousElementSibling.dispatchEvent(new Event('change'));
      });
      
      //appends the new button after the dropdown
      dd.after(next);      
    }
    //if the change comes with no key pressed
    else{
      //invoke the callback
      changeEventHandler(dd);
    }
    
    //resets the keyp data attribute
    dd.dataset.keyp = 'false';
  });
}
body{
  font-family: "Open Sans", sans-serif;
}

h1{
  border-bottom: solid 1px darkgray;
}

label{
  font-weight: 600;
  line-height: 1.2;
}

.control{
  line-height: 1.5;
  color: #495057;   
  border: 1px solid #ced4da;
  border-radius: .25rem;  
}

select.smartdd{      
  padding: .25rem .5rem;    
  background-color: #fff;
  cursor: pointer; 
  width: 300px;
}

select.smartdd + button.nextaction{    
  margin-left: 1rem;
  padding: .20rem .5rem;  
}

/*Layout styles*/

.hidden{
  display: none !important;
}

.row{
  display: flex;
  margin-bottom: 1rem;
}

.col1{
  flex: 0 0 25%;
  max-width: 25%;
}

.col2{
  flex: 0 0 50%;
  max-width: 50%;
}
<h1>Form submission</h1>

<form>
  <div class="row">
    <div class="col1">
      <label for="dd2">Choice1:</label>
    </div>
    <div class="col2">
      <select id="dd1" tabindex="1">
        <option value="" disabled selected>Select an option</option>
        <option value="1">Option 1 - Form will be submitted</option>
        <option value="2">Option 2 - Requires another choice</option>
      </select>
    </div>
  </div>
  <div class="row hidden">
    <div class="col1">
      <label for="dd2">Choice2:</label>
    </div>
    <div class="col2">
      <select id="dd2" tabindex="2">
        <option value="" disabled selected>Select an option</option>
        <option value="1">Option 1</option>
        <option value="2">Option 2</option>
        <option value="3">Option 3</option>
      </select>
    </div>
  </div>
</form>

Upvotes: 1

Related Questions