JedWatson / react-select

The Select Component for React.js
https://react-select.com/
MIT License
27.42k stars 4.1k forks source link

Pressing on Tab key takes me to outside of the form instead of next input. #5882

Open shrihari-prakash opened 3 months ago

shrihari-prakash commented 3 months ago

Thanks for using react-select!

If you are going to ask a question or want to propose a change or a new feature, then please don't file an issue for this. Questions and feature requests have their own place in our discussions section.

Are you reporting a bug or runtime error?

Bug

I am using this inside of react-hook form and pressing tab while the input is focused seems to move the focus on to the outer box (tabindex -1) than the next input element. You can also see in the recording that in a normal input field, this works fine and he focus is moved to the next input:

https://github.com/JedWatson/react-select/assets/35889246/ce23ae41-5e01-430c-bbcb-85bdcde11b19

Is there an option I am missing?

rbracco commented 3 months ago

I have this same issue. Using React-Select inside any modal breaks keyboard navigation. Related issue here: https://github.com/JedWatson/react-select/issues/5377

shrihari-prakash commented 3 months ago

I have this same issue. Using React-Select inside any modal breaks keyboard navigation. Related issue here: https://github.com/JedWatson/react-select/issues/5377

@rbracco your video shows precisely what I am experiencing! With radix UI dialog. Surprisingly no response on it for half a month.

rbracco commented 3 months ago

Yeah unfortunately it seems most issues are not being responded to. That tends to happen with long-term open-source projects, people eventually move on.

I did find a workaround but as I'm not an accessibility specialist I can't guarantee it won't have weird side effects so please test thoroughly if you use it. It intercepts the tab on the select and prevents default and shifts focus to your next input. It gets really gross if you want more than one select lol

const selectRef = React.useRef(null)
    const prevInputRef = React.useRef(null)
    const nextInputRef = React.useRef(null)
    function handleSelectKeyDown(event) {
        if (event.key === 'Tab' && !event.shiftKey) {
            event.preventDefault()
            nextInputRef.current.focus()
        } else if (event.key === 'Tab' && event.shiftKey) {
            event.preventDefault()
            prevInputRef.current.focus()
        }
    }
...
<div onKeyDown={handleSelectKeyDown}/>
    <input type="range" ref={prevInputRef}/> // your previous input must have this ref attached
    <Select/>
    <input type="text" ref={nextInputRef}/> // your next input must have this ref attached
</div>
shrihari-prakash commented 3 months ago

Yeah unfortunately it seems most issues are not being responded to. That tends to happen with long-term open-source projects, people eventually move on.

I did find a workaround but as I'm not an accessibility specialist I can't guarantee it won't have weird side effects so please test thoroughly if you use it. It intercepts the tab on the select and prevents default and shifts focus to your next input. It gets really gross if you want more than one select lol


const selectRef = React.useRef(null)

    const prevInputRef = React.useRef(null)

    const nextInputRef = React.useRef(null)

    function handleSelectKeyDown(event) {

        if (event.key === 'Tab' && !event.shiftKey) {

            event.preventDefault()

            nextInputRef.current.focus()

        } else if (event.key === 'Tab' && event.shiftKey) {

            event.preventDefault()

            prevInputRef.current.focus()

        }

    }

...

<div onKeyDown={handleSelectKeyDown}/>

    <input type="range" ref={prevInputRef}/> // your previous input must have this ref attached

    <Select/>

    <input type="text" ref={nextInputRef}/> // your next input must have this ref attached

</div>

Definitely a good starting point. Of course, ideal is to have a fix in the library, but for now, I would be implementing something similar as well. But as you might have noticed in the recording, I do have two select components unfortunately😂 and also some custom date pickers making the implementation more cumbersome. But can't go without a workaround as well. In any case, I appreciate your help!

rbracco commented 3 months ago

I have two selects too, I'm working on it now, if I get it working I'll share code.

shrihari-prakash commented 3 months ago

Hello @rbracco ,

Do you use react-hook-forms on your side? If yes, I found a hacky but very reliable solution to this:

  const form = useFormz(/* your form options */);
  const { watch } = form;
  const inputFields = watch();
  const inputNames = Object.keys(inputFields);

  const handleKeyDown = (event: any) => {
    const prefix = "react-select-field-";
    if (!event.target.id.startsWith("react-select")) {
      return;
    }

    let parent = event.target.parentNode;
    let field = null;

    while (parent && !parent.className.includes(prefix)) {
      parent = parent.parentNode;
      for (const className of parent.classList) {
        if (className.startsWith(prefix)) {
          field = className.slice(prefix.length, className.length);
        }
      }
    }

    const currentIndex = inputNames.indexOf(field);
    const isTab = event.key === "Tab";
    const isShift = event.shiftKey;
    let nextInputName: string | null = null;

    if (isTab && !isShift) {
      nextInputName = inputNames[currentIndex + 1];
    } else if (isTab && isShift) {
      nextInputName = inputNames[currentIndex - 1];
    }

    if (nextInputName) {
      event.preventDefault();
      setTimeout(() => {
        const nextInput = document.querySelector(`[name="${nextInputName}"]`);
        if (nextInput) {
          (nextInput as any).focus();
        }
      }, 0);
    }
  };

// Later in jsx:

<AsyncSelect
    {...field}
    className={"react-select-field-your-field-name"}
    onKeyDown={handleKeyDown}
/>

This works perfectly no matter how many selects you have in the form. Let me know if you have a different solution that is better. Even if you don't have react-hook-form, this should somewhat work if you are able to get the list of input names.

Essentially, the solution finds the parent element with the prefix react-select-field-your- and get's the field name from this className. Of course, this solution assumes your form schema definition matches the exact order of the form elements in your jsx.

GrimBit1 commented 3 months ago

Hi , @shrihari-prakash I found this work around for tab problem

const form = useRef<HTMLFormElement>(null);
  const handleTabChange = (event: any) => {
    if (event.key !== "Tab") return;

    event.preventDefault();

    // Get all focusable elements within the modal
    const focusableElements: any = form.current?.querySelectorAll(
      'button, [href], input#react-select, select, textarea, [tabindex]:not([tabindex="-1"]):not(:disabled)'
    );
    const firstFocusableElement = focusableElements?.[0];
    const lastFocusableElement =
      focusableElements?.[focusableElements.length - 1];

    // If the shift key is pressed and the first element is focused, move focus to the last element
    if (event.shiftKey && document.activeElement === firstFocusableElement) {
      lastFocusableElement?.focus();
      return;
    }

    // If the shift key is not pressed and the last element is focused, move focus to the first element
    if (!event.shiftKey && document.activeElement === lastFocusableElement) {
      firstFocusableElement?.focus();
      return;
    }

    // Otherwise, move focus to the next element
    const direction = event.shiftKey ? -1 : 1;
    const index = Array.prototype.indexOf.call(
      focusableElements,
      document.activeElement
    );
    const nextElement = focusableElements?.[index + direction];
    if (nextElement) {
      nextElement?.focus();
    }
  };

This makes a focus trap parent and the focus wont leave that , it will just loop on the elements Use form ref to parent or wrapper element

shrihari-prakash commented 3 months ago

Hi , @shrihari-prakash

I found this work around for tab problem


const form = useRef<HTMLFormElement>(null);

  const handleTabChange = (event: any) => {

    if (event.key !== "Tab") return;

    event.preventDefault();

    // Get all focusable elements within the modal

    const focusableElements: any = form.current?.querySelectorAll(

      'button, [href], input#react-select, select, textarea, [tabindex]:not([tabindex="-1"]):not(:disabled)'

    );

    const firstFocusableElement = focusableElements?.[0];

    const lastFocusableElement =

      focusableElements?.[focusableElements.length - 1];

    // If the shift key is pressed and the first element is focused, move focus to the last element

    if (event.shiftKey && document.activeElement === firstFocusableElement) {

      lastFocusableElement?.focus();

      return;

    }

    // If the shift key is not pressed and the last element is focused, move focus to the first element

    if (!event.shiftKey && document.activeElement === lastFocusableElement) {

      firstFocusableElement?.focus();

      return;

    }

    // Otherwise, move focus to the next element

    const direction = event.shiftKey ? -1 : 1;

    const index = Array.prototype.indexOf.call(

      focusableElements,

      document.activeElement

    );

    const nextElement = focusableElements?.[index + direction];

    if (nextElement) {

      nextElement?.focus();

    }

  };

This makes a focus trap parent and the focus wont leave that , it will just loop on the elements

Use form ref to parent or wrapper element

Definitely the cleanest so far. Thanks for sharing this💯

rbracco commented 2 months ago

Hey, sorry I got sidetracked and didn't have time to work on this. I implemented @GrimBit1's solution and it works fully! Thank you both for sharing your work!

SaveliiLukash commented 1 month ago

Facing this issue to this day. Hoping to get an official fix someday. But thanks for what you have come up with, guys!