2

I have a page that populates a lot of elements based on user interaction. I'm having trouble accomplishing the following:

  1. Apply onclick and onfocus events to all elements without including the inline onclick="function(this)" html code.
  2. Also return if the onclick/on focus happened with the keyboard vs mouse?

var global_focused_id = -1;
var global_clicked_id = -1;

function reply_focus(el) {
  global_focused_id = el.id + ' | ' + el.getAttribute('aria-label') +  ' | ' + Date.now();
  console.log(global_focused_id);
}

function reply_click(el) {
  global_clicked_id = el.id + ' | ' + el.getAttribute('aria-label') + ' | ' + Date.now();
  console.log(global_clicked_id);
}
<button class="btn-class1" id="btn-id1" aria-label="btn-label1" onclick="reply_click(this)" onfocus="reply_focus(this)">Button 1</button>
<input class="input-class1" id="input1" aria-label="input-label1" onclick="reply_click(this)" onfocus="reply_focus(this)">
<div class="div-class1" id="div1" aria-label="div-label1" onclick="reply_click(this)" onfocus="reply_focus(this)">I'm Div 1</div>

<button class="btn-class2" id="btn-id2" aria-label="btn-label2" onclick="reply_click(this)" onfocus="reply_focus(this)">Button 1</button>
<input class="input-class2" id="input2" aria-label="input-label2" onclick="reply_click(this)" onfocus="reply_focus(this)">
<div class="div-class2" id="div2" aria-label="div-label2" onclick="reply_click(this)" onfocus="reply_focus(this)">I'm Div 1</div>
CapsaCool
  • 67
  • 5
  • 2
    Use [event delegation](//developer.mozilla.org/docs/Learn/JavaScript/Building_blocks/Events#Event_delegation) instead of adding several event listeners — it’s more maintainable and applies to dynamically added elements. See [the tag info](/tags/event-delegation/info) and [this Q&A](/q/1687296/4642212). Use the [event argument](//developer.mozilla.org/docs/Web/API/EventTarget/addEventListener#The_event_listener_callback)’s [`target`](//developer.mozilla.org/docs/Web/API/Event/target). It’d be easier if all these elements had a common class. – Sebastian Simon Mar 06 '22 at 00:38

1 Answers1

1

For your first point, you can use event delegation to handle all elements in a single event handler. Add the event listener to a container that is common to all elements and then identify the element that the event originated on with event.target.
One caveat with this is that it relies on event bubbling, but focus events don't bubble, so instead you have to use the focusin event which does.
Here's a simplified example:

function handleFocus(event) {
  const el = event.target;
  console.log('Focus', el);
}

function handleClick(event) {
  const el = event.target;
  console.log('Click', el);
}

 const container = document.getElementById('container');
 container.addEventListener('focusin', handleFocus);
 container.addEventListener('click', handleClick);
<div id="container">
  <button class="btn-class1" id="btn-id1" aria-label="btn-label1">Button 1</button>
  <input class="input-class1" id="input1" aria-label="input-label1">
  <div class="div-class1" id="div1" aria-label="div-label1">I'm Div 1</div>

  <button class="btn-class2" id="btn-id2" aria-label="btn-label2">Button 1</button>
  <input class="input-class2" id="input2" aria-label="input-label2">
  <div class="div-class2" id="div2" aria-label="div-label2">I'm Div 1</div>
</div>

For your second point you're going to have to use a bit of trickery as the FocusEvent and MouseEvent does not distinguish between different types of input devices.

Click event

For the click event, my go-to solution is to use the mouse coordinates to determine if it was triggered by a keyboard or mouse. If it was triggered by a keyboard, event.pageX and event.pageY will always be 0 but if it was triggered by a mouse they will both be 0 only if you clicked in the absolute top left corner of the window, and it's pretty unlikely that any reasonably well designed web page would have something clickable in that position.
Ex:

function handleClick(event) {
  const msg = document.getElementById('msg');
  if (event.pageX === 0 && event.pageY === 0) {
    msg.value = 'Clicked by keyboard';
  } else {
    msg.value = 'Clicked by mouse';
  }
}

document.getElementById('click-btn').addEventListener('click', handleClick);
<button id="click-btn">Click me!</button>
<input id="msg" type="text" readonly/>

Focus event

For the focus event we can exploit the :focus-visible CSS pseudo-class. This doesn't work in Internet Explorer but hopefully that's not a problem now. This pseudo-class is intended to differentiate between scenarios where focus needs to made apparent through styling because it was caused by a generic event (such as keyboard tabbing) and when it doesn't because focus was caused by a deliberate, directed action (such as a mouse click on the element itself). I bet this reasoning is similar to the motivation you have for wanting to differentiate between them.
We can make use of this in our JS too, heres an example:

function handleFocus(event) {
  const msg = document.getElementById('msg');
  if (event.target.matches(':focus-visible')) {
    msg.value = 'Focused by keyboard';
  } else {
    msg.value = 'Focused by mouse';
  }
}

document.getElementById('focus-ipt').addEventListener('focusin', handleFocus);
<label><input id="focus-ipt" type="checkbox"/> Focus me!<br/></label>
<input id="msg" type="text" readonly/>

Disclaimer: Since this is trickery there's no guarantee that it works across all browsers, in all situations or that they will keep working in the future. Browsers might change these behaviors. Especially the :focus-visible trick is problematic as it relies on the browser's interpretation of when it should apply. One current issue is that it applies to text fields (<input type="text"/> and <textarea/>) even when those are focused by a mouse click.
So keep this in mind when using these tricks

Lennholm
  • 7,205
  • 1
  • 21
  • 30