react-quill
react-quill copied to clipboard
Focus trap
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)
I have the same problem. I modified that workaround to account for your two remaining problems.
- 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. - 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.
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>
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?
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"
/>
}