From 299840cf1cc851e420d8a7dc16705833fdd1b42d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Thu, 18 Jan 2024 16:34:44 +0100 Subject: [PATCH] fix: Fix tab focus when other elements are displayed next to text that are within a focus trap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- src/EditorFactory.js | 3 +- src/components/Editor.vue | 9 ++--- src/components/Editor/ContentContainer.vue | 3 +- src/extensions/FocusTrap.js | 41 ++++++++++++++++++++++ src/extensions/index.js | 2 ++ src/nodes/CodeBlockView.vue | 2 +- 6 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 src/extensions/FocusTrap.js diff --git a/src/EditorFactory.js b/src/EditorFactory.js index 5518ac985dd..af57e08f736 100644 --- a/src/EditorFactory.js +++ b/src/EditorFactory.js @@ -29,7 +29,7 @@ import { lowlight } from 'lowlight/lib/core.js' import hljs from 'highlight.js/lib/core' import { logger } from './helpers/logger.js' -import { Mention, PlainText, RichText } from './extensions/index.js' +import { FocusTrap, Mention, PlainText, RichText } from './extensions/index.js' // eslint-disable-next-line import/no-named-as-default import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight' @@ -64,6 +64,7 @@ const createEditor = ({ language, onCreate, onUpdate = () => {}, extensions, ena }), ], }), + FocusTrap, ] } else { defaultExtensions = [PlainText, CodeBlockLowlight.configure({ lowlight, defaultLanguage: language })] diff --git a/src/components/Editor.vue b/src/components/Editor.vue index 30e891fe873..66d565bf4a1 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -643,16 +643,10 @@ export default { onFocus() { this.emit('focus') - - // Make sure that the focus trap doesn't catch tab keys while being in the contenteditable - window._nc_focus_trap?.[0]?.pause() }, + onBlur() { this.emit('blur') - - // Hand back focus to the previous trap - window._nc_focus_trap?.[0]?.unpause() - this.$el.focus() }, onAddImageNode() { @@ -750,6 +744,7 @@ export default { this.$editor.commands.insertContent('\t') this.$editor.commands.focus() event.preventDefault() + event.stopPropagation() return } diff --git a/src/components/Editor/ContentContainer.vue b/src/components/Editor/ContentContainer.vue index 9daf5e587e5..7cd40891399 100644 --- a/src/components/Editor/ContentContainer.vue +++ b/src/components/Editor/ContentContainer.vue @@ -30,8 +30,7 @@ -
diff --git a/src/extensions/FocusTrap.js b/src/extensions/FocusTrap.js new file mode 100644 index 00000000000..66180244065 --- /dev/null +++ b/src/extensions/FocusTrap.js @@ -0,0 +1,41 @@ +import { Extension } from '@tiptap/core' + +const toggleFocusTrap = ({ editor }) => { + const trapStack = window._nc_focus_trap ?? [] + const activeTrap = trapStack[trapStack.length - 1] + + const possibleEditorTabCommand = editor.can().sinkListItem('listItem') + || editor.can().goToNextCell() + || editor.can().goToPreviousCell() + + if (possibleEditorTabCommand) { + activeTrap?.pause() + } else { + activeTrap?.unpause() + } +} + +const unpauseFocusTrap = ({ editor }) => { + const trapStack = window._nc_focus_trap ?? [] + const activeTrap = trapStack[trapStack.length - 1] + + activeTrap?.unpause() +} + +/** + * The viewer focus trap needs to be paused on the fly in order to properly handle tab commands in the editor, + * as we have no control over if a tab key event is reaching the editor otherwise. This is because the focus trap + * registeres a capture listener (https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#capture), so whenever we reach a tab command in the editor the focus trap will already have captured the event. + * + * We also cannot work around this by pushing our own focus trap to the stack, as the focus trap package does not offer any reliable way to programmatically focus the next element of the parent trap if we allow tabbing out of the editor. + */ +const FocusTrap = Extension.create({ + name: 'focustrap', + onFocus: toggleFocusTrap, + onBlur: unpauseFocusTrap, + onSelectionUpdate: toggleFocusTrap, + onTransaction: toggleFocusTrap, + onUpdate: toggleFocusTrap, +}) + +export default FocusTrap diff --git a/src/extensions/index.js b/src/extensions/index.js index c6929f7ed2c..0127386b12e 100644 --- a/src/extensions/index.js +++ b/src/extensions/index.js @@ -22,6 +22,7 @@ import CollaborationCursor from './CollaborationCursor.js' import Emoji from './Emoji.js' +import FocusTrap from './FocusTrap.js' import Keymap from './Keymap.js' import UserColor from './UserColor.js' import Markdown from './Markdown.js' @@ -33,6 +34,7 @@ import Mention from './Mention.js' export { CollaborationCursor, Emoji, + FocusTrap, Keymap, UserColor, Markdown, diff --git a/src/nodes/CodeBlockView.vue b/src/nodes/CodeBlockView.vue index 28a225795fe..6780cf31aa2 100644 --- a/src/nodes/CodeBlockView.vue +++ b/src/nodes/CodeBlockView.vue @@ -46,7 +46,7 @@
-
+