react icon indicating copy to clipboard operation
react copied to clipboard

Bug: autoFocus broken inside <dialog />

Open jantimon opened this issue 3 years ago • 14 comments

React version: 17 and 18.0.0-rc.0-next-27b569969-20220211

Steps To Reproduce

  1. render <input /><input autoFocus /> inside <dialog />
  2. execute the showModal() method of the dialog
  3. you will notice that react will not set focus to the correct input element

DialogAutoFocus

Link to code example:

https://codesandbox.io/s/dreamy-meninsky-460wbr?file=/src/App.tsx

The current behavior

In Chrome and Safari TP the element with autofocus="true" will receive focus.
However the element with autoFocus={true} will not receive focus.

The expected behavior

From the html-spec https://html.spec.whatwg.org/multipage/interaction.html#the-autofocus-attribute

The autofocus content attribute allows the author to indicate that an element is to be focused [...] as soon as the dialog within which it finds itself is shown

From https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus

The autofocus global attribute is a Boolean attribute indicating that an element should be focused on page load, or when the <dialog> that it is part of is displayed

Therefore autoFocus={true} should also set the focus similar to autofocus="true" for elements inside <dialog />

jantimon avatar Feb 15 '22 17:02 jantimon

@jantimon until it's not fixed you can try this workaround:

export default function App() {
  const [showDialog1, setShowDialog1] = useState(false);
  const [showDialog2, setShowDialog2] = useState(false);
  return (
    <>
      // ...
      <Dialog open={showDialog2} onClose={() => setShowDialog2(false)}>
        <input value="don't focus on me" />
        <input value="please focus me" autoFocus={showDialog2} />
      </Dialog>
    </>
  );
}

mmarkelov avatar Feb 20 '22 19:02 mmarkelov

Hello, @Jahb, @Mirijam1, @budihan, and I are a team from TU Delft. We are taking a course called Software Architecture. We chose ReactJS as our project, and would like to start our first contribution with this issue!

jeongwoopark0514 avatar Mar 01 '22 19:03 jeongwoopark0514

Hi, I would like to work on this issue. I am taking Open Source course where I must contribute to a project (in my case it is reactJS).

ye1dos avatar Mar 03 '22 11:03 ye1dos

I can reproduce this, and have to find a workaround as it's impacting our application.

It's important to note that the dialog spec confirms that the autofocus attribute must be present for the focus logic to work on dialog open. https://html.spec.whatwg.org/multipage/interactive-elements.html#dialog-focusing-steps

Because react has special/custom handling of autofocus it strips the element, although given the dynamic rendering I'm unsure if it would even work.

So far, the only workaround I can find is using setTimeout to run custom focus/blur logic after the dialog/browser 's logic has run.

if (ref.current) {
  const dialog = ref.current
  dialog.showModal()

  setTimeout(() => {
    const focus = dialog.querySelector(':focus')
    if (focus) {
      focus.blur()
    }

    // or call .focus() on something you do want
  })
}

viveleroi avatar Jul 29 '23 21:07 viveleroi

The dialog html element is dead bugged with React 18.2.0, if you try to use useState within a dialog it will just gave you a lot of autoFocus bugs.

fidaay avatar Sep 17 '23 22:09 fidaay

Any update to this?

tounsoo avatar Oct 05 '23 22:10 tounsoo

The dialog html element is dead bugged with React 18.2.0, if you try to use useState within a dialog it will just gave you a lot of autoFocus bugs.

And that's why I'm probably not gonna bother anymore with the dialog element. I have been stuck for weeks now trying to debug my input fields which refuse to focus when I click on them.

avdb13 avatar Nov 29 '23 17:11 avdb13

Another bump 👊

tounsoo avatar Nov 30 '23 03:11 tounsoo

bump ++

tewarig avatar Dec 21 '23 06:12 tewarig

I'm ok if it takes time to fix, but no one responding to this open ticket is very disappointing.

tounsoo avatar Jan 02 '24 16:01 tounsoo

Also found the same thing. In a dialog element, if I set the autoFocus prop on a button like this:

 <Button type="button" loading={loading} onClick={onClick} autoFocus>
      Save
    </Button>

It does not work, instead the first focusable element in the dialog receives the autofocus. However, if I set the prop like a normal html prop and use a string

 <Button type="button" loading={loading} onClick={onClick} autofocus="true">
      Save
    </Button>

This works properly, although the React compiler/linting breaks here because obviously that's not how React accepts the prop.

As a workaround, we can use this to manually set the autofocus attribute via DOM APIs.

function MyButton() {
  const ref = useRef<HTMLButtonElement>(null)

  useEffect(() => {
    if (!ref.current) return
    ref.current.setAttribute('autofocus', 'true')
  }, [])
  
  return (
    <Button type="button" loading={loading} onClick={onClick} ref={ref}>
      Save
    </Button>
  )
}

dan-ville avatar Jan 29 '24 18:01 dan-ville

What worked for me:

  1. Create a ref for the component that needs to be autoFocused.
const autoFocusRef = useRef<HTMLElement>(null);
  1. Manually focus the element whenever the dialog is opened.
const showModal = async (): Promise<void> => {
  dialog.showModal();

  const hasAnimation =
    window.getComputedStyle(dialog).animationName !== "none";

  if (!hasAnimation) {
    autoFocusRef.current?.focus();
    return;
  }

  dialog.addEventListener(
    "animationend",
    () => autoFocusRef.current?.focus(),
    { once: true, passive: true }
  );
};

This accounts for the dialog possibly being animated, which can otherwise also lead to autoFocus not working.

codebycarlos avatar Apr 12 '24 21:04 codebycarlos

What I am using as a workaround to handle this case with MUI TextField and MUI Dialog is to use a reference "useRef" and then to find the nested "input" element and to apply "focus()" method on it, as the following:

const GenericTextField=({ autoFocus = false }:{ autoFocus?: boolean })=> { 

const inputRef = useRef<HTMLInputElement | null>(null);

  useEffect(() => {
    if (inputRef.current && autoFocus) {
      const inputEl = inputRef?.current?.querySelectorAll?.("input")?.[0];
      if (inputEl) inputEl.focus();
    }
  }, [autoFocus]);


<TextField  ref={ inputRef } …
....
}

alaa-m1 avatar May 09 '24 07:05 alaa-m1