From a3875fc854a6beab551e93b80c24be9de7b7caa8 Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 4 Mar 2026 13:18:46 +0000 Subject: [PATCH] fix(docs): fix document editor focus and cursor issues Three problems prevented the document editor from being usable: 1. The contentEditable div inherited composer sizing (~22px tall) so clicks in the document content area landed outside it and bubbled up to the mx_RoomView div[tabIndex=-1], making the whole room view flash blue instead of focusing the editor. Fixed by making the Editor stack fill the full content area height in _DocumentView.pcss. 2. useSetCursorPosition was not called, so even when the editor received focus there was no selection range and no visible cursor. Added the hook call (same pattern as WysiwygComposer). 3. Any click outside the now-tall contentEditable div (e.g. in padding) still fell through. Added an onClick handler on the content wrapper that calls ref.current.focus() when the click target isn't the editor itself, ensuring any click in the document area focuses the editor. Bonus: suppress the browser default outline on .mx_RoomView:focus so pressing a key no longer shows a blue box around the entire room view. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- apps/web/res/css/structures/_RoomView.pcss | 5 +++++ .../rooms/wysiwyg_composer/_DocumentView.pcss | 20 +++++++++++++++++-- .../rooms/wysiwyg_composer/DocumentView.tsx | 20 ++++++++++++++++++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/apps/web/res/css/structures/_RoomView.pcss b/apps/web/res/css/structures/_RoomView.pcss index 6a5ceb641b..6cc56a1555 100644 --- a/apps/web/res/css/structures/_RoomView.pcss +++ b/apps/web/res/css/structures/_RoomView.pcss @@ -27,6 +27,11 @@ Please see LICENSE files in the repository root for full details. flex: 1; position: relative; + /* The outer div has tabIndex=-1 for keyboard shortcuts. Suppress the + * browser's default focus outline — the editor's own outline is sufficient. */ + &:focus { + outline: none; + } .mx_MainSplit { flex: 1 1 0; } diff --git a/apps/web/res/css/views/rooms/wysiwyg_composer/_DocumentView.pcss b/apps/web/res/css/views/rooms/wysiwyg_composer/_DocumentView.pcss index 3c34ab2430..f32723feec 100644 --- a/apps/web/res/css/views/rooms/wysiwyg_composer/_DocumentView.pcss +++ b/apps/web/res/css/views/rooms/wysiwyg_composer/_DocumentView.pcss @@ -29,16 +29,32 @@ Please see LICENSE files in the repository root for full details. flex: 1; overflow-y: auto; padding: var(--cpd-space-8x) var(--cpd-space-12x); + /* Forward clicks anywhere in this area to the editor */ + cursor: text; - /* Give the editor enough room to feel like a document */ + /* + * The composer's editor stack is designed for a small multi-line input. + * In document mode we want it to fill the full available height so that + * clicking anywhere in the content area lands on the contentEditable div. + */ .mx_WysiwygComposer_Editor { - min-height: 100%; + height: 100%; + min-height: 400px; + + .mx_WysiwygComposer_Editor_container { + height: 100%; + display: flex; + flex-direction: column; + } .mx_WysiwygComposer_Editor_content { + flex: 1; min-height: 400px; font-size: var(--cpd-font-size-body-lg); line-height: 1.6; caret-color: var(--cpd-color-text-primary); + /* Remove the narrow composer outline */ + border: none; } } } diff --git a/apps/web/src/components/views/rooms/wysiwyg_composer/DocumentView.tsx b/apps/web/src/components/views/rooms/wysiwyg_composer/DocumentView.tsx index 3edfcd3c86..464d5877f2 100644 --- a/apps/web/src/components/views/rooms/wysiwyg_composer/DocumentView.tsx +++ b/apps/web/src/components/views/rooms/wysiwyg_composer/DocumentView.tsx @@ -14,6 +14,7 @@ import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext import { FormattingButtons } from "./components/FormattingButtons.tsx"; import { Editor } from "./components/Editor.tsx"; import { ComposerContext, getDefaultContextValue } from "./ComposerContext.ts"; +import { useSetCursorPosition } from "./hooks/useSetCursorPosition.ts"; /** * Matrix event type for incremental Automerge deltas sent as timeline events. @@ -198,12 +199,29 @@ export const DocumentView = memo(function DocumentView({ room }: DocumentViewPro const { ref, isWysiwygReady, wysiwyg, actionStates } = wysiwygResult; const composerModel = wysiwygResult.composerModel; + // Place the cursor at the end and focus the editor once the WASM model is + // ready. Without this the editor is enabled but has no selection, so no + // cursor appears even after the element receives focus. + useSetCursorPosition(!isWysiwygReady, ref); + const { isLoaded, scheduleDeltaSend } = useDocumentSync(room, client, composerModel); const handleInput = useCallback(() => { scheduleDeltaSend(); }, [scheduleDeltaSend]); + // Forward clicks anywhere in the content area to the contentEditable so + // the user can click anywhere in the document space to start typing. + const handleContentClick = useCallback( + (ev: React.MouseEvent) => { + // Only forward if the click didn't already land on the editor itself. + if (ev.target !== ref.current && ref.current) { + ref.current.focus(); + } + }, + [ref], + ); + if (!isLoaded) { return
; } @@ -215,7 +233,7 @@ export const DocumentView = memo(function DocumentView({ room }: DocumentViewPro
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} -
+