NUI iFrame Management (tabbing between frames)

Currently, when tabbing inside an nui frame, it’ll tab across to the next iframe even if it isn’t necessarily active.

This can create all sorts of unwanted styling / positioning mis-haps.

A simple fix would be to automatically add tabindex="-1" to the generated iframe, or provide a server config var to attach it.

Below is an example generated iframe.

<iframe name="ghmattimysql" allow="microphone *;" src="nui://ghmattimysql/ui/index.html" style="visibility: visible; z-index: 0;"></iframe>

With the tabindex addition:

<iframe name="ghmattimysql" allow="microphone *;" src="nui://ghmattimysql/ui/index.html" style="visibility: visible; z-index: 0;" tabindex="-1"></iframe>

A current work-around I’m using is running this script in one of my nui frames:

  var baseBody = window.frameElement.parentElement;
  var _iframes = baseBody.getElementsByTagName('iframe')
  for (var i = 0; i < _iframes.length; i += 1) {
    _iframes[i].setAttribute("tabindex", -1)
  }

With further investigation it seems my work-around doesn’t work.

Have you tried if this does fix it if added to root.html’s code?

Sorry for the follow up questions, still relatively new to this stuff.

Is it safe for me to change the root.html file in my local files without fear of repercussions from adhesive or such?

Yes, local file changes will at worst lead to the game closing on or right after startup.

My changes to my local root.html don’t appear to be picked up in-game, when inspecting with nui_devtools.

The file I am editing: %APPDATA%\Local\FiveM\FiveM.app\citizen\ui\root.html.

Did a cache clear as well to see if it was possibly cached.

Also, while testing that, I realized why my work-around wasn’t working. It’s because the nui frame was loading and instantly applying tabindex to all sibling iframes, even though some may have not loaded yet.

  setTimeout(() => {
    var baseBody = window.frameElement.parentElement;
    var _iframes = baseBody.getElementsByTagName('iframe')
    for (var i = 0; i < _iframes.length; i += 1) {
      _iframes[i].setAttribute("tabindex", -1)
    }
  }, 10000);

This work-around does indeed work, although an iframe can still be escaped, it is no longer possible to enter a different one. Simply clicking the resource will regain focus.

I believe adding tabIndex = -1 to the base root.html will give us the desired resolution.

I don’t think that one’s used anymore, over the one in ui.zip.

Hm. That might still be a bit nasty.

Perhaps code should refocus on blur of the currently focused iframe itself.

Alright, so you are right. It’s the root.html file inside the ui.zip which can be edited.

Adding frame.tabIndex = -1; in the createFrame function will stop the cross-resource tabbing.

Unfortunately, as you say, the iframe still being escapable could lead to other problems, and we won’t be able to use an onBlur:focus chain because the native library is using blur to toggle a resource active or not.

Focus traps are a big talking point with all the a11y progress made with browsers of recent, but in this case it would seem ideal to create one. Libraries like this: https://github.com/davidtheclark/focus-trap exist to enable focus traps, which would solve our particular issue here.

Demo / read more: http://davidtheclark.github.io/focus-trap/demo/

The focus trap solution would of course require a focusable element inside each iframe to ensure it works, which isn’t ideal.

I’ve created another work-around that will return focus to the active iframe if you tab out of it.

document.addEventListener("keydown", function() {
  console.log("active", document.activeElement.tagName)
  citFrames[focusStack[focusStack.length - 1]].contentWindow.focus();
})

This is of course very primitive but it’s a small proof of concept to at least keep the active iframe as the focus target.

The above event will only fire when the iframe focus has been escaped.


I also tried attaching an onfocus listener to the body tag but it isn’t fired via tab focus for some reason. May investigate further on that because that would be the most ideal solution.

Okay, finally, I have come up with what I believe to be the most elegant solution:

// init
let handoverBlob = {};
let serverAddress = '';
let frameEscapeFunctions = {};
// createFrame
frame.src = frameUrl;
frame.tabIndex = -1;
...
frameEscapeFunctions[frameName] = () => {
  setTimeout(() => {
    frame.contentWindow.focus();
  });
}
// focusFrame
citFrames[frameName].contentWindow.addEventListener("blur", frameEscapeFunctions[frameName]);
// and focus the frame itself
citFrames[frameName].contentWindow.focus();
// blurFrame
citFrames[frameName].contentWindow.removeEventListener("blur", frameEscapeFunctions[frameName]);
// remove focus
citFrames[frameName].contentWindow.blur();

Essentially, when unintentionally bluring an iframe, steal focus back.

This unfortunately does to seem iFrames that are loaded inside a NUI iframe.

I provided some example code to showcase the behavior here: