Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to prevent clicking on a checkbox's label from stealing focus

The default behavior on Mac OS X is that clicking checkboxes and buttons doesn't steal focus from other controls that have focus (for example, a textbox). I want to implement this on the web.

I can get it working for buttons and checkboxes themselves but not for checkbox labels.

Take this jsbin as an example:
http://jsbin.com/kopateluze/1/edit?html,console,output

Here is a screenshot of the form:

Screenshot of http://jsbin.com/kopateluze/1/edit?html,console,output

If you first focus the textbox and then click the button, you can use event.preventDefault() on mousedown to prevent the textbox from losing focus. That works nicely. And if you first focus the textbox and then click the checkbox, the same thing works.

But if you first focus the textbox and then click the checkbox's label text, it doesn't work; the textbox loses focus.

Is there a way to prevent clicking on a checkbox label from stealing focus from other controls?

Pure JavaScript, please, but feel free to link to the source code for solutions implemented in libraries.

like image 601
Chris Calo Avatar asked Oct 05 '15 21:10

Chris Calo


1 Answers

Okay, here's what I was able to figure out, a little over two years after the fact.

Preventing buttons from stealing focus

First, let's talk about the example that motivated this question. When a text field has focus, I'd like to prevent clicking buttons from stealing focus. I still want any button click handlers to be called, I just don't want focus to move.

To do that, we just need to know that buttons receive focus on mousedown, so preventing that is as simple as using event.preventDefault():

<input placeholder="First focus me" type="text" />

<br/>

<button
  onmousedown="event.preventDefault()"
  onclick="console.log('button:click')">
    Then click me
</button>

Notice that clicking the button triggers click handlers but never causes the button to receive focus. Also note that buttons can still receive focus via pressing Tab, so this doesn't harm keyboard access.

Preventing checkboxes from stealing focus

It turns out, as long as you use a naked checkbox that has no label, this same technique works perfectly.

<input placeholder="First focus me" type="text" />

<br/>

<input
  type="checkbox"
  onmousedown="event.preventDefault()"
  onclick="console.log('checkbox:click')"
/>
⇐ Then click the checkbox

But this does have accessibility problems because the text describing the checkbox isn't associated with it and the checkbox click target is just the checkbox itself, not the text that goes with it. The solution here is to add a label element, but this is where the problems start.

The problem when a checkbox has a label

<input placeholder="First focus me" type="text">

<br/>

<label>
  <input type="checkbox"
    onmousedown="event.preventDefault()"
    onclick="console.log('checkbox:click')"
  />
  Then click me
</label>

This successfully prevents the checkbox from stealing focus if you click on the checkbox. However, if you click on the text next to the checkbox, focus is stolen from the textbox.

The fix for when a checkbox has a label

The key is in knowing how label elements interact with their associated input elements. The label element's click handler actually gets called twice: once where event.target is the label element, and once where it is the checkbox.

Therefore, the general fix is to:

  1. Call event.preventDefault() on mousedown of both the label and the checkbox.
  2. In the label click handler, which gets executed twice, call .click() on the associated control (the checkbox element), but only when the event.target is equal to the label element.

<input placeholder="First focus me" type="text" />

<br/>

<label for="checkbox"
  onmousedown="event.preventDefault()"
  onclick="
    console.log('label:click');
    if (this === event.target) {
      console.log('label === event.target');
      console.log('label:click preventDefault()');
      // prevent focus and click
      event.preventDefault();
      // call click on the labeled control
      this.control && this.control.click();
    }
  ">
  <input id="checkbox" type="checkbox"
    onmousedown="event.preventDefault()"
    onclick="
      console.log('checkbox:click', `checked = ${this.checked}`);
    "
  />
  Then click me
</label>
like image 59
Chris Calo Avatar answered Oct 09 '22 22:10

Chris Calo