0

Suppose I've got the Code structure below. If I focus an element within my red container (one of the inputs), the focusin event triggers. Likewise, if I click outside the red container, the focusout event triggers.

However, if I click one of the input elements, then directly the other, both a focusout and a focusin event get triggered in quick succession.

Is there an easy way to avoid this or find out whether the second focusout event can be ignored because focus in fact stays within the relevant element, aside from ugly solutions like setting a flag on the first focusout event and waiting for a render tick to see whether another focusin event happens?

document.getElementById("el").addEventListener("focusin", 
  () => document.getElementById("out").innerHTML += "focusin<br/>");
document.getElementById("el").addEventListener("focusout", 
  () => document.getElementById("out").innerHTML += "focusout<br/>");
<div id="el" style="background-color: red; padding: 4px">
<input />
<input />
</div>
<div id="out">

</div>
Lukas Bach
  • 3,559
  • 2
  • 27
  • 31
  • You can track focus from the parent and only fire focusout if the parent no longer has an active child. see: [How to check if element has focused child using javascript?](https://stackoverflow.com/questions/53370088/how-to-check-if-element-has-focused-child-using-javascript) – pilchard Jun 16 '21 at 21:18
  • For some reason, the active element during the temporary focusout seems to be the body element, so apparently the container item truly looses focus for a quick moment, so this won't work, right? (See https://jsfiddle.net/lukasbach/cof08qdr/8/) – Lukas Bach Jun 16 '21 at 21:23
  • 1
    You're right, I just noticed that too. I guess you'll need to check on the next focusin event. Some little discussion here: [How to check if any form element inside a fieldset has focus](https://stackoverflow.com/questions/52079711/how-to-check-if-any-form-element-inside-a-fieldset-has-focus) – pilchard Jun 16 '21 at 21:26

1 Answers1

0

As there doesn't seem to be an easy solution, I've wrote the ugly solution of waiting for the next render tick. I wrote it as reusable React hook, so if it helps someone, here it is.

export const useFocusWithin = (
  element: HTMLElement | undefined,
  onFocusIn?: () => void,
  onFocusOut?: () => void,
) => {
  const [focusWithin, setFocusWithin] = useState(false);
  const isLoosingFocusFlag = useRef(false);

  useHtmlElementEventListener(element, 'focusin', () => {
    setFocusWithin(true);
    onFocusIn?.();
    if (isLoosingFocusFlag.current) {
      isLoosingFocusFlag.current = false;
    }
  });
  
  useHtmlElementEventListener(element, 'focusout', (e) => {
    isLoosingFocusFlag.current = true;
    setTimeout(() => {
      if (isLoosingFocusFlag.current) {
        onFocusOut?.();
        isLoosingFocusFlag.current = false;
        setFocusWithin(false);
      }
    });
  });

  return focusWithin;
};

export const useHtmlElementEventListener = <K extends keyof HTMLElementEventMap>(
  element: HTMLElement | undefined, type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any) => {
  useEffect(() => {
    if (element) {
      element.addEventListener(type, listener as any);
      return () => element.removeEventListener(type, listener as any);
    }
  }, [element, listener, type]);
};
Lukas Bach
  • 3,559
  • 2
  • 27
  • 31