react-quill icon indicating copy to clipboard operation
react-quill copied to clipboard

Focus trap

Open joshwcomeau opened this issue 2 years ago • 3 comments

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:

  • It doesn't focus the next item in the tab order, so you need to press tab twice
  • It doesn't stop the "tab" key from being pressed, adding additional whitespace to the editor

Has anyone found a better workaround?

Ticket due diligence

  • [x] I have verified that the issue persists under ReactQuill v2.0.0-beta.2
  • [ ] I can't use the beta version for other reasons

ReactQuill version

  • [ ] master
  • [x] v2.0.0-beta.4
  • [ ] v2.0.0-beta.1
  • [ ] 1.3.5
  • [ ] 1.3.4 or older
  • [ ] Other (fork)

joshwcomeau avatar Dec 05 '21 14:12 joshwcomeau

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.

wfischer42 avatar Jan 11 '22 23:01 wfischer42

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>

dudasaron avatar May 15 '22 22:05 dudasaron

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?

Shelagh-Lewins avatar Aug 26 '22 18:08 Shelagh-Lewins

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

JeremyRippert avatar Nov 20 '22 18:11 JeremyRippert