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>
This commit is contained in:
David Langley 2026-03-04 13:18:46 +00:00
parent 2853fefc16
commit a3875fc854
3 changed files with 42 additions and 3 deletions

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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<HTMLDivElement>) => {
// 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 <div className="mx_DocumentView mx_DocumentView_loading" />;
}
@ -215,7 +233,7 @@ export const DocumentView = memo(function DocumentView({ room }: DocumentViewPro
<FormattingButtons composer={wysiwyg} actionStates={actionStates} />
</div>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div className="mx_DocumentView_content" onInput={handleInput}>
<div className="mx_DocumentView_content" onInput={handleInput} onClick={handleContentClick}>
<Editor ref={ref} disabled={!isWysiwygReady} placeholder="Start typing your document…" />
</div>
</div>