Skip to content

Instantly share code, notes, and snippets.

@tannerlinsley
Last active January 30, 2024 09:37
Show Gist options
  • Star 28 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save tannerlinsley/1d3a2122332107fcd8c9cc379be10d88 to your computer and use it in GitHub Desktop.
Save tannerlinsley/1d3a2122332107fcd8c9cc379be10d88 to your computer and use it in GitHub Desktop.
A utility function to detect window focusing without false positives from iframe focus events
type State = {
added: boolean;
interval: false | ReturnType<typeof setInterval>;
inFrame: boolean;
callbacks: Array<SetFocusedCallback>;
};
type EnrichedHTMLIFrameElement = HTMLIFrameElement & { ___onWindowFocusHandled: boolean };
type SetFocusedCallback = (focused?: boolean) => void;
const state: State = {
added: false,
interval: false,
inFrame: false,
callbacks: [],
};
export const onWindowFocus = (newCallback: SetFocusedCallback) => {
state.callbacks.push(newCallback);
start();
return () => {
state.callbacks = state.callbacks.filter(
(registeredCallback) => registeredCallback !== newCallback
);
stop();
};
};
const runIFrameCheck = () => {
const iframes = Array.from(document.getElementsByTagName('iframe'));
console.debug('Polling iframes... found: ', iframes.length);
(iframes as EnrichedHTMLIFrameElement[]).forEach((iframe) => {
if (iframe.___onWindowFocusHandled) {
return;
}
iframe.___onWindowFocusHandled = true;
iframe.addEventListener('touchend', () => {
state.inFrame = true;
});
iframe.addEventListener('mouseup', () => {
state.inFrame = true;
});
iframe.addEventListener('focus', () => {
state.inFrame = true;
});
});
};
const start = () => {
if (state.interval) {
clearInterval(state.interval);
}
if (!state.added) {
state.added = true;
window.addEventListener('focus', () => {
if (state.inFrame) {
state.inFrame = false;
return;
} else {
state.callbacks.forEach((callback) => callback(true));
}
});
}
state.interval = setInterval(runIFrameCheck, 500);
};
const stop = () => {
if (!state.callbacks.length && state.interval) {
clearInterval(state.interval);
}
};
@Chaoste
Copy link

Chaoste commented Aug 2, 2021

Thanks for sharing! Here's a v3 compatible version written in ts:

type State = {
  added: boolean;
  interval: false | ReturnType<typeof setInterval>;
  inFrame: boolean;
  callbacks: Array<SetFocusedCallback>;
};

type EnrichedHTMLIFrameElement = HTMLIFrameElement & { ___onWindowFocusHandled: boolean };

type SetFocusedCallback = (focused?: boolean) => void;

const state: State = {
  added: false,
  interval: false,
  inFrame: false,
  callbacks: [],
};

export const onWindowFocus = (newCallback: SetFocusedCallback) => {
  state.callbacks.push(newCallback);
  start();
  return () => {
    state.callbacks = state.callbacks.filter(
      (registeredCallback) => registeredCallback !== newCallback
    );
    stop();
  };
};

const runIFrameCheck = () => {
  const iframes = Array.from(document.getElementsByTagName('iframe'));
  console.debug('Polling iframes... found: ', iframes.length);

  (iframes as EnrichedHTMLIFrameElement[]).forEach((iframe) => {
    if (iframe.___onWindowFocusHandled) {
      return;
    }
    iframe.___onWindowFocusHandled = true;
    iframe.addEventListener('touchend', () => {
      state.inFrame = true;
    });
    iframe.addEventListener('mouseup', () => {
      state.inFrame = true;
    });
    iframe.addEventListener('focus', () => {
      state.inFrame = true;
    });
  });
};

const start = () => {
  if (state.interval) {
    clearInterval(state.interval);
  }

  if (!state.added) {
    state.added = true;
    window.addEventListener('focus', () => {
      if (state.inFrame) {
        state.inFrame = false;
        return;
      } else {
        state.callbacks.forEach((callback) => callback(true));
      }
    });
  }

  state.interval = setInterval(runIFrameCheck, 500);
};

const stop = () => {
  if (!state.callbacks.length && state.interval) {
    clearInterval(state.interval);
  }
};

@tannerlinsley
Copy link
Author

Nice! I'll update the gist.

@Chaoste
Copy link

Chaoste commented Aug 2, 2021

It might be worth noting that this gist works only if the code runs in the main window.

I have my react app running within an iframe which is not allowed to get notified if the user focussed the main window (probably for security reasons). If I focus an element in the main window, the iframe won't get notified if I switch to another tab or come back to this tab. Only if I focus on the iframe (or an element in the iframe), I get the focus event when switching my browser tab.

From a security perspective, it makes sense that the browser (Chrome in my case) behaves like this. Leaking information between main window and iframe should only be possible via MessageEvents. For people having a similar situation, the solution would be to catch the focus event in the main window, post the message to the iframe where the focusManager can react on the event.

PS: Please note that my snippet might not work for react-query v2 because the payload of the callback changed.

@sudazzle
Copy link

sudazzle commented Nov 15, 2022

const start = () => {
  if (state.interval) {
    clearInterval(state.interval)
  }

  if (!state.added) {
    state.added = true
    window.addEventListener('focus', () => {
      if (state.inFrame) {
        state.inFrame = false
        state.callbacks.forEach((callback) => callback(true))
      } else {
        state.callbacks.forEach((callback) => callback(false))
      }
    })
  }

  state.interval = setInterval(runIFrameCheck, 1000)
}

I tweaked above code a bit and it works on react-query v2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment