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 */} -
+