zenoamaro / react-quill

A Quill component for React.
https://zenoamaro.github.io/react-quill
MIT License
6.74k stars 917 forks source link

Focus trap #756

Open joshwcomeau opened 2 years ago

joshwcomeau commented 2 years ago

Hi there! Thanks so much for this wonderful project :)

The "Tab" key is used by React Quill for indentation, which makes sense in a text-editing context, but it also overrides the ability for keyboard users to navigate through the page. It essentially "traps" focus and blocks the user from moving past it, unless they use a pointer device like a mouse/trackpad.

Here's an example, to demonstrate the issue. Try to focus the <input> after the ReactQuill instance: https://codesandbox.io/s/react-quill-template-forked-rbmqq

I don't think this is a Quill issue, since this issue suggests that it's been fixed in Quill.

Workaround

Here's what I'm doing right now as a workaround:

<Quill
  onKeyDown={(ev) => {
    const isTabbingInEditor =
      ev.key === 'Tab' &&
      ev.target.getAttribute('class') === 'ql-editor';

    if (isTabbingInEditor) {
      ev.preventDefault();
      ev.target.blur();
      return false;
    }
  }}
/>

This kinda works, but there are two problems:

Has anyone found a better workaround?

Ticket due diligence

ReactQuill version

wfischer42 commented 2 years ago

I have the same problem. I modified that workaround to account for your two remaining problems.

  1. I moved the event capture logic into a wrapper div and put it in the onKeyDownCapture event, so it would prevent the tab from reaching the Quill element.
  2. I'm explicitly focusing on the next tabbable element, instead of blurring the editor. This can be passed in as a prop if you set up a re-usable wrapper component.
<div
  onKeyDownCapture={(ev) => {
    if (ev.key === "Tab") {
      ev.preventDefault();
      ev.stopPropagation();
      document.getElementById(props.nextTabId)?.focus();
    }
  }}>
  <ReactQuill
    ref={editor}
    ...
  />
</div>

I'd like to see a real fix for this as well, but for now at least, my use case is covered.

dudasaron commented 2 years ago

I also have the same issue.

Thanks @wfischer42 for sharing your solution, it helped me a lot.

I just added a few modifications, so it does not trap the tab navigation if the user goes backwards, and for forward navigation it goes through the toolbar buttons, and only goes to nextTabId when the event comes from the editor area.

Also added area-label for the wrapper div for accessibility

<div
      role="textbox"
      aria-label={ariaLabel}
      onKeyDownCapture={(e) => {
          if (e.key === 'Tab' && (e.target as HTMLElement).classList.contains('ql-editor')) {
              e.preventDefault()
              e.stopPropagation()
              document.getElementById(e.shiftKey ? prevTabId : nextTabId)?.focus()
          }
      }}
  >
      <ReactQuill {...props} preserveWhitespace />
</div>
Shelagh-Lewins commented 2 years ago

In your code example, Alt-tab allows a keyboard user to exit the text area. It would be great if this was documented, because I don't think it's obvious. It might aid users if when 'Tab' is pressed, a message popped up saying "to navigate out of the text area, press Alt + Tab" or something similar?

JeremyRippert commented 1 year ago

I found a workaround that doesn't require prevTabdId or nextTabId, by removing the tab key binding (inspired from https://github.com/quilljs/quill/issues/110#issuecomment-43692304). Here is the full snippet (I'm using NextJS, hence the unusual import for 'react-quill'):

// to type 'react-quill' import and 'quillRef'
import QuillComponent, { ReactQuillProps } from 'react-quill';

const ReactQuill = (
  typeof window === 'object' ? require('react-quill') : () => false
) as React.FC<ReactQuillProps  & { ref: React.Ref<QuillComponent> }>;

export const Quill: React.FC = () => {
  const quillRef = useRef<QuillComponent | null>(null);

  useEffect(() => {
    const removeTabBinding = () => {
      if (quillRef.current === null) {
        return;
      }
      const keyboard = quillRef.current.getEditor().getModule('keyboard');
      // 'hotkeys' have been renamed to 'bindings'
      delete keyboard.bindings[9];
    };

    removeTabBinding();
  }, [quillRef);

  return  <ReactQuill
        ref={quillRef}
        value={value}
        onChange={onChange}
        theme="snow"
      />
}
abhishekprajapati1 commented 1 day ago

Hi I have a kind of different issue but it is similar. My requirement was to move the toolbar in bottom of the editor. I tweaked the classes and move it to bottom. But pressing tab is still focusing the toolbar which is fair if it would have on top. But since now it is in the bottom and editor comes first, it is kinda weird. I want to first focus on editor even if it does not focuses on toolbar, will be okay for me for now.

see the form

image

After entering data in first input when I press tab it should focus on editor instead of toolbar

image

I know why this is happening because I have changed the flex direction of container having class .quill to column-reverse is there any tweak that I can apply here to reverse the tabIndex order ?