javascriptiframefocuskeyboard-focus

How to pass focus from an iframe to next focusable element in host page?


Background

I have an iframe widget which will be embedded in other pages - I have no prior knowledge of the host page's structure and code, and the iframe will be cross-domain and likely sandboxed.

My goal is to give the iframe widget a single tab stop - after focusing on it there's a dedicated key combination to "enter" the widget, but normal tabbing should skip forward to the next focusable element in the host page.

Question: After the iframe receives focus and a new tab keypress is detected, how can I pass the focus to the host page's next focusable element?

Simple example:

<!-- host page -->
<button> first button </button>
<iframe src="myIframeWidget.com"/>
<button> second button </button>

In the above, if the first button is focused, then the expected behavior is that on first tab the iframe will receive focus, and on the next tab the second button will receive focus (skipping all focusable elements inside the iframe).

Once the iframe is focused and tab is pressed, I need to pass the focus from my iframe to the second button.

Notes

  1. Since my iframe DOM is rather large, I don't want to set tabindex=-1 to all internal focusable elements.
  2. At the time of writing, aria-hidden="true" does not remove an element or its descendants from the focus sequence. I'm not aware of any other aria attribute which removes an entire subtree from the focus sequence.
  3. A pure JS solution is preferred to one relaying on external packages.
  4. I'll also need to handle reverse-tabbing, but I assume that the same approach for forward-tabbing will apply - if there are any crucial differences, please mention them in your answer if possible.
  5. There's an edge case where there isn't anything focusable after the widget - typically in those cases the browser will focus on the address bar buttons - if there's a way to handle that it'll be great to know, but this probably should be handled in a separate question.

Thanks!


Solution

  • Let's say, this is the content of the host page

    <button>Main button 1</button>
    <button>Main button 2</button>
    <iframe id="frame1" name="frame1" src="iframe.html"></iframe>
    <button>Main Button 3</button>
    

    Now, when you receive the focus on the iframe, which is iframe.html, on the next tab press, you want to pass the focus to the Main Button 3 button. You might have a lot of focusable content on your iframe.html like,

    <button>I frame button 1</button>
    <button>I frame button 2</button>
    

    that you want to skip. To do that, you can write a simple javascript inside your iframe.html Which is,

    let go_next = false;
    document.body.addEventListener('keyup', (e)=> {
        if( e.which == 9 ) {
            if(go_next){
                window.parent.document.getElementById('frame1').nextElementSibling.focus()
                go_next=false
            }else{
                go_next =true;
            }
        }
    });
    

    Code Explanation

    We don't want the focus to immediately pass to the next element, as the user should be able to focus on the iframe as well. For that reason, the script is assuming that the user has focused on the iframe for the first time by declaring variable go_next = false

    Note: Focusing from Main Button 2 to iframe is counted as a tab press inside the iframe.html. That's why we have to ignore the first tab press inside the iframe.html

    So, just after the user has focused on the iframe, we're making our variable go_next = true. Now on the next tab press, we can proceed to pass the focus to the next element of the host page.

    To do that, we are grabbing the host page using window.parent and selecting the iframe from the host page using document.getElementById('frame1') and the next element of the iframe using nextElementSibling and focusing the next element using the focus function. And after passing the focus, we're making the go_next = false again for the next time.

    I've prepared a small demonstration for you to test here

    Alternative Solution For Cross Domains

    Cross domains often block access to the parent host using Content Script Policy. To overcome this issue, we have to play a bit tricky. We're are going to create an invisible button at the end of our iframe.html page. Eg:

    <button id="last_element" style="opacity:0;filter:alpha(opacity=0);">Hello</button>
    

    Remember we said the tab press that was used to focus the iframe can be accessed inside the iframe.html? Well, we're going to take that advantage and focus on the last button of our iframe.html. To do that, add this script at the end of the iframe.html

    document.body.addEventListener('keyup', (e)=> {
        if( e.which == 9 ) {
            document.querySelector("#last_element").focus()
        }
    });
    

    Now, as the last element of the iframe is focused, the next focus will be always the next element of the host page. Here's a demo on codepen