Description
Slatejs editor only in Firefox fails with text selection and caret positioning (by mouse). It always selects position before first character (path[0] offset[0]). You can still use the keyboard to select text and position caret.
Recording
Expectation
It should select text and position caret as it does in other browsers.
Environment
Editor implementation for reference
import React, { useCallback, useMemo, useState } from "react";
import {
BaseEditor,
createEditor,
Descendant,
Editor,
Element as SlateElement,
Node,
Text,
Transforms
} from "slate";
import { Slate, Editable, ReactEditor, withReact } from "slate-react";
import { HistoryEditor, withHistory } from "slate-history";
export type CustomEditor = BaseEditor & ReactEditor & HistoryEditor
export type ParagraphElement = {
type: 'paragraph'
children: CustomText[]
}
export type TitleElement = {
type: "title"
children: CustomText[]
}
type CustomElement = ParagraphElement | TitleElement;
type FormattedText = { text: string, bold?: true };
type CustomText = FormattedText;
declare module "slate" {
interface CustomTypes {
Editor: CustomEditor
Element: CustomElement
Text: FormattedText
}
}
////////////////////////////////////
// Custom helpers
////////////////////////////////////
const customEditor = {
isBoldMarkActive(editor: CustomEditor) {
const [match] = Editor.nodes(editor, {
match: (n: any) => n.bold === true,
universal: true,
})
return !!match
},
isTitleActive(editor: CustomEditor) {
const [match] = Editor.nodes(editor, {
match: (n: any) => n.type === "title",
})
return !!match
},
toggleBoldMark(editor: CustomEditor) {
const isActive = customEditor.isBoldMarkActive(editor)
Transforms.setNodes(
editor,
{ bold: isActive ? undefined : true },
{ match: n => Text.isText(n), split: true }
)
},
toggleTitle(editor: CustomEditor) {
const isActive = customEditor.isTitleActive(editor)
Transforms.setNodes(
editor,
{ type: isActive ? undefined : "title" },
{ match: n => Editor.isBlock(editor, n) }
)
},
}
////////////////////////////////////
// Forced layout setup - title + paragraph
////////////////////////////////////
const withLayout = (editor: CustomEditor) => {
const { normalizeNode } = editor
editor.normalizeNode = ([node, path]) => {
if (path.length === 0) {
if (editor.children.length < 1) {
const title: TitleElement = {
type: "title",
children: [{ text: 'Untitled' }],
}
Transforms.insertNodes(editor, title, { at: path.concat(0) })
}
if (editor.children.length < 2) {
const paragraph: ParagraphElement = {
type: 'paragraph',
children: [{ text: '' }],
}
Transforms.insertNodes(editor, paragraph, { at: path.concat(1) })
}
for (const [child, childPath] of Node.children(editor, path)) {
const type = childPath[0] === 0 ? "title" : 'paragraph'
if (SlateElement.isElement(child) && child.type !== type) {
const newProperties: Partial<SlateElement> = { type }
Transforms.setNodes(editor, newProperties, { at: childPath })
}
}
}
return normalizeNode([node, path]);
}
return editor;
}
////////////////////////////////////
const TextEditor = () => {
const initialValue: Descendant[] = [
{
type: 'title',
children: [{ text: 'Enter a title...' }],
},
{
type: 'paragraph',
children: [{ text: 'Enter your question'}]
}
];
const editor = useMemo(() => withLayout(withHistory(withReact(createEditor()))), []);
Transforms.deselect(editor);
const [value, setValue] = useState<Descendant[]>(initialValue);
const renderElement = useCallback((props) => <Element {...props} />, [])
// Define a leaf rendering function that is memoized with `useCallback`.
const renderLeaf = useCallback((props) => {
return <Leaf {...props} />
}, []);
return (
// Add the editable component inside the context.
<Slate
editor={editor}
value={value}
onChange={(value) => setValue(value)}
>
<div>
<button
onMouseDown={event => {
event.preventDefault()
customEditor.toggleBoldMark(editor)
}}
>
B
</button>
<button
onMouseDown={event => {
event.preventDefault()
customEditor.toggleTitle(editor)
}}
>
H2
</button>
</div>
<Editable
renderElement={renderElement}
autoFocus
renderLeaf={renderLeaf}
/>
</Slate>
)
}
export default TextEditor
// Define a React component to render leaves with bold text.
const Leaf = (props: any) => {
return (
<span
{...props.attributes}
style={{ fontWeight: props.leaf.bold ? 'bold' : 'normal' }}
>
{props.children}
</span>
)
}
const Element = ({ attributes, children, element }: any) => {
switch (element.type) {
case 'title':
return <h2 {...attributes}>{children}</h2>
case 'paragraph':
return <p {...attributes}>{children}</p>
default:
return <p {...attributes}>{children}</p>
}
}
Any ideas what might have caused it?
Dumb mistake. My CSS reset file contained: "user-select: none", which was somehow ignored by Chrome.