Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
shrihari-prakash opened this issue Mar 21, 2024 · 9 comments
Labels
issue/bug-unconfirmed Issues that describe a bug that hasn't been confirmed by a maintainer yet

Comments

@shrihari-prakash
Copy link

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:

react-select-demo.mp4

Is there an option I am missing?

@shrihari-prakash shrihari-prakash added the issue/bug-unconfirmed Issues that describe a bug that hasn't been confirmed by a maintainer yet label Mar 21, 2024
@shrihari-prakash shrihari-prakash changed the title Pressing on Tab key takes me to previous input instead of next input. Pressing on Tab key takes me to outside of the form instead of next input. Mar 21, 2024
@rbracco
Copy link

rbracco commented Mar 25, 2024

I have this same issue. Using React-Select inside any modal breaks keyboard navigation. Related issue here: #5377

@shrihari-prakash
Copy link
Author

I have this same issue. Using React-Select inside any modal breaks keyboard navigation. Related issue here: #5377

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

@rbracco
Copy link

rbracco commented Mar 25, 2024

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
Copy link
Author

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
Copy link

rbracco commented Mar 25, 2024

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

@shrihari-prakash
Copy link
Author

shrihari-prakash commented Mar 31, 2024

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
Copy link

GrimBit1 commented Apr 5, 2024

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
Copy link
Author

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
Copy link

rbracco commented Apr 8, 2024

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
issue/bug-unconfirmed Issues that describe a bug that hasn't been confirmed by a maintainer yet
Projects
None yet
Development

No branches or pull requests

3 participants