mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
fix: start typing in the message body when the empty space under it is tapped
This commit is contained in:
@@ -66,6 +66,8 @@ export interface RichTextEditorHandle {
|
||||
*/
|
||||
getHtml(timeoutMs?: number): Promise<string>;
|
||||
focus(): void;
|
||||
/** Focus with the caret where new text goes (before signature / quote). */
|
||||
focusEnd(): void;
|
||||
/** Drop the caret, which closes the keyboard. */
|
||||
blur(): void;
|
||||
}
|
||||
@@ -186,6 +188,9 @@ const RichTextEditor = React.forwardRef<RichTextEditorHandle, Props>(function Ri
|
||||
pendingSnapshots.current.set(id, { resolve, reject, timer });
|
||||
call(`window.__rne && window.__rne.getHtml(${id})`);
|
||||
}),
|
||||
focusEnd: () => {
|
||||
call(`window.__rne && window.__rne.focusEnd()`);
|
||||
},
|
||||
focus: () => {
|
||||
call(`window.__rne && window.__rne.focus()`);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildEditorHtml, FOCUS_END_SOURCE } from '../editor-html';
|
||||
import { LIGHT_COLORS } from '../../theme/tokens';
|
||||
|
||||
// On the iPad most of the composer's body area is empty space under the
|
||||
// editor; a tap there did nothing and the keyboard kept typing into Subject.
|
||||
// That space now calls the page's focusEnd(), which must put the caret where
|
||||
// the user's own text goes: in front of the signature or the quoted message.
|
||||
|
||||
interface FakeNode { name: string; previousElementSibling?: FakeNode | null }
|
||||
|
||||
function run(stop: FakeNode | null) {
|
||||
const calls: string[] = [];
|
||||
const editor = {
|
||||
name: 'editor',
|
||||
focus: () => calls.push('focus'),
|
||||
querySelector: () => stop,
|
||||
};
|
||||
const range = {
|
||||
selectNodeContents: (n: FakeNode) => calls.push(`select ${n.name}`),
|
||||
setStartBefore: (n: FakeNode) => calls.push(`before ${n.name}`),
|
||||
collapse: (toStart: boolean) => calls.push(toStart ? 'collapse start' : 'collapse end'),
|
||||
};
|
||||
const selection = {
|
||||
removeAllRanges: () => calls.push('clear'),
|
||||
addRange: (r: unknown) => calls.push(r === range ? 'add range' : 'add other'),
|
||||
};
|
||||
const fakeDocument = { createRange: () => range };
|
||||
const fakeWindow = { getSelection: () => selection };
|
||||
const focusEnd = new Function('document', 'window', `${FOCUS_END_SOURCE}; return focusEnd;`)(fakeDocument, fakeWindow);
|
||||
focusEnd(editor);
|
||||
return calls;
|
||||
}
|
||||
|
||||
describe('focusEnd', () => {
|
||||
it('puts the caret at the end of the paragraph in front of the signature', () => {
|
||||
const mine = { name: 'mine' };
|
||||
expect(run({ name: 'signature', previousElementSibling: mine })).toEqual([
|
||||
'focus', 'select mine', 'collapse end', 'clear', 'add range',
|
||||
]);
|
||||
});
|
||||
|
||||
it('puts the caret in front of a signature or quote that comes first', () => {
|
||||
expect(run({ name: 'quote', previousElementSibling: null })).toEqual([
|
||||
'focus', 'before quote', 'collapse start', 'clear', 'add range',
|
||||
]);
|
||||
});
|
||||
|
||||
it('puts the caret at the end without signature or quote', () => {
|
||||
expect(run(null)).toEqual(['focus', 'select editor', 'collapse end', 'clear', 'add range']);
|
||||
});
|
||||
|
||||
it('is part of the page script and of its bridge', () => {
|
||||
const html = buildEditorHtml({ initialHtml: '<p><br></p>', placeholder: 'Write…', c: LIGHT_COLORS });
|
||||
expect(html).toContain('function focusEnd(editor)');
|
||||
expect(html).toContain('focusEnd: function () { focusEnd(editor); }');
|
||||
});
|
||||
});
|
||||
@@ -64,6 +64,33 @@ export function shouldBlockEditorRemoteImages(opts: {
|
||||
return !senderTrusted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page-script function for a tap on the empty space under the editor: focus
|
||||
* it with the caret where the user's own text goes, in front of the
|
||||
* signature or the quoted message, else at the end. A string, embedded in
|
||||
* the page script and run by the unit test against stand-in DOM objects.
|
||||
*/
|
||||
export const FOCUS_END_SOURCE = `function focusEnd(editor) {
|
||||
editor.focus();
|
||||
var sel = window.getSelection();
|
||||
if (!sel) return;
|
||||
var stop = editor.querySelector('[data-signature-block="start"], [data-signature-block="separator"], [data-quoted-html]');
|
||||
var range = document.createRange();
|
||||
var prev = stop ? stop.previousElementSibling : null;
|
||||
if (prev) {
|
||||
range.selectNodeContents(prev);
|
||||
range.collapse(false);
|
||||
} else if (stop) {
|
||||
range.setStartBefore(stop);
|
||||
range.collapse(true);
|
||||
} else {
|
||||
range.selectNodeContents(editor);
|
||||
range.collapse(false);
|
||||
}
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}`;
|
||||
|
||||
export function buildEditorHtml(opts: {
|
||||
initialHtml: string;
|
||||
placeholder: string;
|
||||
@@ -261,6 +288,8 @@ export function buildEditorHtml(opts: {
|
||||
});
|
||||
|
||||
// ── Bridge: receive commands from RN ───────────────────────────────
|
||||
${FOCUS_END_SOURCE}
|
||||
|
||||
window.__rne = {
|
||||
exec: function (command, value) {
|
||||
editor.focus();
|
||||
@@ -363,6 +392,7 @@ export function buildEditorHtml(opts: {
|
||||
post('htmlSnapshot', { id: id, html: editor.innerHTML });
|
||||
},
|
||||
focus: function () { editor.focus(); },
|
||||
focusEnd: function () { focusEnd(editor); },
|
||||
};
|
||||
|
||||
reportHeight();
|
||||
|
||||
@@ -2495,6 +2495,16 @@ export default function ComposeScreen({ route, navigation }: Props) {
|
||||
onViewportShift={onEditorViewportShift}
|
||||
/>
|
||||
)}
|
||||
{/* The empty space under the body looks like part of it (on a
|
||||
tablet, most of the panel): a tap there starts typing in it. */}
|
||||
<Pressable
|
||||
style={styles.bodyFiller}
|
||||
accessible={false}
|
||||
onPress={() => {
|
||||
if (plainTextMode) plainBodyRef.current?.focus();
|
||||
else editorRef.current?.focusEnd();
|
||||
}}
|
||||
/>
|
||||
</ScrollView>
|
||||
|
||||
{/* A phone in landscape with the keyboard up has room for a few lines
|
||||
@@ -2991,6 +3001,7 @@ function makeStyles(c: ThemePalette) {
|
||||
backgroundColor: c.background,
|
||||
},
|
||||
scrollContent: { flexGrow: 1 },
|
||||
bodyFiller: { flexGrow: 1, minHeight: 48 },
|
||||
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
|
||||
Reference in New Issue
Block a user