diff --git a/src/app/components/editor/Editor.tsx b/src/app/components/editor/Editor.tsx index bd848dd5..b7e6d2fe 100644 --- a/src/app/components/editor/Editor.tsx +++ b/src/app/components/editor/Editor.tsx @@ -23,6 +23,9 @@ import { RenderElement, RenderLeaf } from './Elements'; import { CustomElement } from './slate'; import * as css from './Editor.css'; import { toggleKeyboardShortcut } from './keyboard'; +import { useSetting } from '../../state/hooks/settings'; +import { settingsAtom } from '../../state/settings'; +import { composerSpellcheckAttributes } from './spellcheck'; const initialValue: CustomElement[] = [ { @@ -90,6 +93,7 @@ export const CustomEditor = forwardRef( }, ref ) => { + const [composerSpellcheck] = useSetting(settingsAtom, 'composerSpellcheck'); const renderElement = useCallback( (props: RenderElementProps) => , [] @@ -146,6 +150,7 @@ export const CustomEditor = forwardRef( onKeyDown={handleKeydown} onKeyUp={onKeyUp} onPaste={onPaste} + {...composerSpellcheckAttributes(composerSpellcheck)} /> {after && ( diff --git a/src/app/components/editor/spellcheck.test.ts b/src/app/components/editor/spellcheck.test.ts new file mode 100644 index 00000000..fa0535cb --- /dev/null +++ b/src/app/components/editor/spellcheck.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; +import { composerSpellcheckAttributes, defaultComposerSpellcheck } from './spellcheck'; + +const media = (coarse: boolean) => (query: string) => ({ + matches: query === '(pointer: coarse)' && coarse, +}); + +describe('composer spellcheck default', () => { + it('is off when the primary pointer is coarse (phone)', () => { + expect(defaultComposerSpellcheck(media(true))).toBe(false); + }); + it('is on for a fine pointer (desktop)', () => { + expect(defaultComposerSpellcheck(media(false))).toBe(true); + }); + it('is on when matchMedia is unavailable', () => { + expect(defaultComposerSpellcheck(undefined)).toBe(true); + }); +}); + +describe('composer spellcheck attributes', () => { + it('off disables both spellcheck and autocorrect', () => { + expect(composerSpellcheckAttributes(false)).toEqual({ spellCheck: false, autoCorrect: 'off' }); + }); + it('on enables spellcheck and leaves autocorrect to the platform', () => { + expect(composerSpellcheckAttributes(true)).toEqual({ spellCheck: true }); + }); +}); diff --git a/src/app/components/editor/spellcheck.ts b/src/app/components/editor/spellcheck.ts new file mode 100644 index 00000000..825676ef --- /dev/null +++ b/src/app/components/editor/spellcheck.ts @@ -0,0 +1,11 @@ +/** Touch devices (phones) get the keyboard's own dictionary; desktop gets the browser's. */ +export const defaultComposerSpellcheck = ( + matchMedia: ((query: string) => { matches: boolean }) | undefined +): boolean => !matchMedia?.('(pointer: coarse)').matches; + +/** + * Attributes for the composer's editable. Off must also switch autocorrect off: + * spellCheck alone leaves Gboard's suggestion strip and correction modal in place. + */ +export const composerSpellcheckAttributes = (enabled: boolean) => + enabled ? ({ spellCheck: true } as const) : ({ spellCheck: false, autoCorrect: 'off' } as const); diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx index b861a060..a51f4175 100644 --- a/src/app/features/settings/general/General.tsx +++ b/src/app/features/settings/general/General.tsx @@ -709,6 +709,10 @@ function DateAndTime() { function Editor() { const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline'); + const [composerSpellcheck, setComposerSpellcheck] = useSetting( + settingsAtom, + 'composerSpellcheck' + ); const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown'); const [hideActivity, setHideActivity] = useSetting(settingsAtom, 'hideActivity'); @@ -724,6 +728,15 @@ function Editor() { after={} /> + + + } + /> +