5

I use react and react-modal to create an overlay over a website. This overlay contains various elements and also a form (overview below). I want to be able to guide the user through the form using TAB keys. I assigned tabindex=0 to the required elements to be tabbable in order of appearance.

My problem is: It does not work in Chrome (Version 61.0.3163.100) while it does work in Firefox. I read that this happens if any element up the DOM-tree is invisible or has height/width of 0. I made some styling changes to fix that but with no effect.

<div class="ReactModalPortal">
<div data-reactroot="" class="ReactModal__Overlay" style="position: fixed; top: 0px; left: 0px; right: 0px; bottom: 0px;">
    <div class="ReactModal__Content" tabindex="-1" aria-label="Questionnaire" style="position: absolute; top: 0px; left: 0px; right: 0px; height: 100%; background: transparent none repeat scroll 0% 0%; overflow: auto;">
        <!-- Some other stuff and nested elements -->
        <div id="...">
            <form>
                <input tabindex="0">
                <button tabindex="0">
            </form> 
        </div>   
    </div>   
</div>    

As you can see one of the parent elements has tabindex="-1". When changing it through the inspect function in Chrome or programmatically with JS the problem still persists (or is it a difference if the element was rendered with this index initially?).

Update

I realized that something else was causing the issues. I was using the CSS attribute initial: all on the root node of my modal to fence my inner CSS from everything outside. For some reason this was preventing the tabindex from working. If you can help me understanding I will reward this wis the bounty. My workaround is just not using all: initial (it is not IE-compatible anyways but also there is no real good alternative I am aware of).

Gegenwind
  • 1,388
  • 1
  • 17
  • 27
  • Elements like input, button etc are focusable by default. You don't have to provide `tabindex` – Agney Oct 30 '17 at 10:41
  • Thank you for the hint. I am aware that this is not required but my problem is that its not working. In general tabbing through anything does not work. The tabindex was an attempt to force it but it did not help. – Gegenwind Oct 30 '17 at 11:46
  • I'm using Chrome 62 and it is working for me https://codesandbox.io/s/qzoz5mqxx9 – Agney Oct 30 '17 at 12:16
  • Thank you for the example. I can see that its working in this case. When I se chrome dev tools and I move the inner part of the MODAL-div out of the modal to place it directly into the body-tag I can tab through the fields as expected. Since I now know it has nothing to do with the modal as such I will investigate a bit more and update the details. – Gegenwind Oct 30 '17 at 14:01
  • I updated the answer since there was a CSS attribute involved that I did not think of. – Gegenwind Oct 31 '17 at 10:13
  • Did you mean `all: initial`? – Agney Oct 31 '17 at 11:03
  • Yep sorry, that was what I meant. – Gegenwind Oct 31 '17 at 11:40

2 Answers2

6

all: initial resets all CSS properties of the node with initial properties.

For display property, the initial value would be inline.

So, setting all: initial to the root div would set the display property to inline. An inline element does not have height or width, so these are 0x0.

This is also because the div contains only fixed, absolutely positioned elements.

React Modal checks if elements are focusable by running a loop through all the elements inside the modal. However, for an element to be focusable, it has to visible. For each element, we have to iterate till the body element to ensure it's visibility.

Here is the function that checks whether the element is visible.

function hidden(el) {
  return (
    (el.offsetWidth <= 0 && el.offsetHeight <= 0) || el.style.display === "none"
  );
}

As you can see, our div would have no offsetHeight or offsetWidth and would be deemed as hidden. Therefore, the modal cannot not be focused.

Agney
  • 18,522
  • 7
  • 57
  • 75
  • Excellent explanation of how these things are affecting the tabindex. Thanks for your efforts and help! – Gegenwind Nov 01 '17 at 06:56
  • Darn, there seems to be more to it than just this check: I attached a "min-height" to the root element so that all elements up the tree have both, width and height. Tabbing still does not work. Actually it does not work for as long as this element created by react modal is on the page:
    . When I take the form out of this div through devtools and remove it, tabbing works. I wonder why?
    – Gegenwind Nov 01 '17 at 11:41
  • `min-height` was given with `all: initial`? – Agney Nov 01 '17 at 12:25
  • Not quite, I removed ´all: initial´ entirely from ´ReactModalPortal´ and instead added ´min-height:1px`to this class and the first child ´ReactModal__Overlay´ for testing purposes. Still as long as `ReactModal__Overlay` is in the dom tree all elements below it will not be tabbable. – Gegenwind Nov 01 '17 at 12:39
  • I can't reproduce the issue. I tried giving `.ReactModalPortal, .ReactModal__Overlay` a min-height of 1px and modal remains tabbable. – Agney Nov 02 '17 at 05:27
1

I had the same issue and was not able to get other solutions working quicky, so I came up with brute force approach. Make a ref to the container element that holds the focusable elements that you wish to make tabbable.

  const formRef = useRef();

  <ReactModalTabbing containerRef={formRef}>
        <form ref={formRef} onSubmit={handleSubmit}  >
            <input type="text" />
            <input type="text" />
            <input type="text" />
            <input type="text" />
        </form>
  </ReactModalTabbing>

And this is the component

import React, { useState, useEffect } from 'react';

const ReactModalTabbing = ({ containerRef, children }) => {
    const [configuredTabIndexes, setConfiguredTabIndexes] = useState(false);

    const focusableElements = () => {
        // found this method body here.
        //https://zellwk.com/blog/keyboard-focusable-elements/
        return [...containerRef?.current?.querySelectorAll(
            'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"]):not([type="hidden"]):not([disabled])'
        )];
    }

    const isTabbable = (element) =>{
        if(element.getAttribute('tabindex')){
            return true;
        }
        return false;
    }

    const findElementByTabIndex = (tabIndex) => {
        return containerRef?.current?.querySelector(`[tabindex="${tabIndex}"]`);
    }
    
    const moveFocusToTabIndex = (tabIndex) => {
        findElementByTabIndex(tabIndex)?.focus();
    }

    const handleKeyDownEvent = (event) => {
        if(!isTabbable(event.target)){
            return;
        }

        const tabIndex = parseInt(event.target.getAttribute('tabindex'));

        if(event.shiftKey && event.key === 'Tab'){
            moveFocusToTabIndex(tabIndex - 1);
        }else if(event.key === 'Tab'){ //should probably make sure there is no other modifier key pressed.
            moveFocusToTabIndex(tabIndex + 1);
        }
    }

    useEffect(() => {
        if(!configuredTabIndexes && containerRef.current){
            setConfiguredTabIndexes(true);
            focusableElements().forEach((el, index) => el.setAttribute('tabindex', index + 1));
            containerRef?.current?.addEventListener('keydown', handleKeyDownEvent);
        }
    });

    return children;
}

export default ReactModalTabbing;
Paul W
  • 226
  • 3
  • 5