facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
228.22k stars 46.68k forks source link

Bug: autoFocus broken inside <dialog /> #23301

Open jantimon opened 2 years ago

jantimon commented 2 years ago

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 />

mmarkelov commented 2 years ago

@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>
    </>
  );
}
jeongwoopark0514 commented 2 years ago

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!

ye1dos commented 2 years ago

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).

viveleroi commented 1 year ago

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
  })
}
fidaay commented 1 year ago

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.

tounsoo commented 1 year ago

Any update to this?

avdb13 commented 10 months ago

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.

tounsoo commented 10 months ago

Another bump 👊

tewarig commented 9 months ago

bump ++

tounsoo commented 9 months ago

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

dan-ville commented 8 months ago

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>
  )
}
codebycarlos commented 6 months ago

What worked for me:

  1. Create a ref for the component that needs to be autoFocused.

    const autoFocusRef = useRef<HTMLElement>(null);
  2. 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.

alaa-m1 commented 5 months ago

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 } …
....
}
ronaldgj commented 1 month ago

@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>
    </>
  );
}

Looks like this works for me!