Click twice to get focus in side dialog with react-select used.
Bug report
Current Behavior
Use react-select within dialog of shadcn-ui, after react-select get focused, I need to click other form item (input) twice to focus that.
Expected behavior
Click one time to focus it
Suggested solution
When react-select lost focus, it's sub-dom changed, this makes container get focused, so it steal focus from the one I clicked.Can we just ignore the removal inside container?
// When the focused element gets removed from the DOM, browsers move focus
// back to the document.body. In this case, we move focus to the container
// to keep focus trapped correctly.
function handleMutations(mutations: MutationRecord[]) {
const focusedElement = document.activeElement as HTMLElement | null;
if (focusedElement !== document.body) return;
for (const mutation of mutations) {
if (mutation.removedNodes.length > 0) focus(container);
}
}
I had some time to look at this issue again today and realized my previous analysis was incorrect. Let's re-examine it using the following DialogDemo.
import * as React from "react";
import { Dialog } from "radix-ui";
import { Cross2Icon } from "@radix-ui/react-icons";
import styles from "./styles.module.css";
import Select from 'react-select';
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' },
];
document.addEventListener('focusin', e => {
console.log('focusin', document.activeElement)
})
const DialogDemo = () => {
const [selectedOption, setSelectedOption] = React.useState(null);
return <Dialog.Root>
<Dialog.Trigger asChild>
<button className={`${styles.Button} violet`}>Edit profile</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className={styles.Overlay} />
<Dialog.Content className={styles.Content}>
<Dialog.Title className={styles.Title}>Edit profile</Dialog.Title>
<input
className={styles.Input}
id="name"
onClick={e => {
console.log('clicked', document.activeElement)
}}
defaultValue="Pedro Duarte"
/>
<Select
defaultValue={selectedOption}
onChange={setSelectedOption}
options={options}
/>
<div
style={{ display: "flex", marginTop: 25, justifyContent: "flex-end" }}
>
<Dialog.Close asChild>
<button className={`${styles.Button} green`}>Save changes</button>
</Dialog.Close>
</div>
<Dialog.Close asChild>
<button className={styles.IconButton} aria-label="Close">
<Cross2Icon />
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
};
export default DialogDemo;
Reproduction steps:
-
Click on Select
-
Click on the input field
If we didn't have handleMutations, the focus change process would be:
Select input loses focus -> document.body gains focus -> input gains focus (because it was clicked) -> (input.onClick is triggered, at this point the focused element is input)
But since handleMutations detects that document.body has gained focus, it moves focus to the container. So the focus change process becomes:
Select input loses focus -> document.body gains focus -> container gains focus -> (input.onClick is triggered, at this point the focused element is container)
In other words, the focus call in handleMutations interrupts the original focus change process. The original intention of this function was to ensure focus stays within the container, but it didn't account for cases where focus first moves to body and then to an element inside the container.
Perhaps we could modify handleMutations like this:
function handleMutations(mutations: MutationRecord[]) {
const focusedElement = document.activeElement;
if (focusedElement !== document.body) return;
const shouldFocus = mutations.some(m => m.removedNodes.length > 0);
if (shouldFocus) {
setTimeout(() => {
// double check 👇👇👇
if (document.activeElement && !container.contains(document.activeElement)) {
focus(container)
}
})
}
}
Your environment
| Software | Name(s) | Version |
|---|---|---|
| Radix Package(s) | @radix-ui/react-focus-scope | ^1.1.0 |
| React | n/a | 17 |
| Browser | Chrome | 121.0.6167.139 |
| tech | ||
| Node | n/a | 16 |
| npm/yarn | ||
| Operating System | linux |
My workaround for now was focusing programmatically on click on the input.
<input
onClick={() => {
if (inputRef.current) {
inputRef.current?.focus();
}
}}
ref={inputRef}
/>
I have the same issue. Having a onClick to focus for every input is not ideal UI. Especially since, for a text input, it doesn't focus on where you click but always puts you at the end of the line.
Is there any update on this issue?
What is the real solution to this issue? This makes dialog unusable :(
I have faced the same issue and have found that the problem occurs when using ScrollArea inside the Dialog. Try to remove the ScrollArea or replace it with another lib, it'll work well
Is there any update on this issue?
Hi radix team, any updates on this? This is becoming an issue for us.