Slate: Selection is null after editor loses focus

Created on 8 Jan 2020  Â·  23Comments  Â·  Source: ianstormtaylor/slate

Do you want to request a _feature_ or report a _bug_?

Bug

What's the current behavior?

Current behavior is that when you click into the toolbar or out of the document the editor's selection becomes null.

2020-01-08 13 25 51

https://codesandbox.io/s/fervent-bouman-ju71u?fontsize=14&hidenavigation=1&theme=dark

Tested on Firefox, Safari and Chrome although Firefox has different behavior. Firefox sometimes sets the cursor position to the end of the text in the editor depending on how you focus out of the editor.

Slate: 0.57.1
Browser: Chrome / Safari / Firefox
OS: Mac

What's the expected behavior?

Focus out shouldn't erase editor selection.

Changes to Example Site To Produce Behavior

In order to test this we forked the rich text example and made sure the toolbar style buttons did not disable on focus out. Then we used the ReactEditor.focus method in the MarkdownButton component's onMouseDown handler in the richtext.js file.

Most helpful comment

ezgif com-video-to-gif

Similar thing happened when I tried to use dialog box in link example instead of alert. While image upload as well the editor focus loses and the image gets appended at the last node instead of at the cursor location. Is there a way to control or change focus?

All 23 comments

ezgif com-video-to-gif

Similar thing happened when I tried to use dialog box in link example instead of alert. While image upload as well the editor focus loses and the image gets appended at the last node instead of at the cursor location. Is there a way to control or change focus?

Found a work around for this issue thanks to a kind developer on slack channel. Writing here in case anybody needs it. Store the selection value just before the editor loses focus. In my case it was when I clicked on input field, so I stored it just when dialog box opens. Similarly it can be applied to image upload, iframes or any action where editor loses focus.

const editorSelection = useRef(editor.selection);
useEffect(() => {
    if (openDialog) {
        editorSelection.current = editor.selection;
    }
}, [openDialog]);

Keeping on this, is there a way to actually keep it selected, as in highligting the text when losing focus ?

Keeping on this, is there a way to actually keep it selected, as in highligting the text when losing focus ?

I don't know if this is a bug or expected behaviour. But here if you have value of selection with you (which is editorSelection.current ) you can pass it down to editor ( editor.selection = editorSelection.current ) before passing editor to Transforms.insertNode or anywhere else. If you want to show the selection, may be try Transforms.setSelection or Transforms.select.

Keeping on this, is there a way to actually keep it selected, as in highligting the text when losing focus ?

I don't know if this is a bug or expected behaviour. But here if you have value of selection with you (which is editorSelection.current ) you can pass it down to editor ( editor.selection = editorSelection.current ) before passing editor to Transforms.insertNode or anywhere else. If you want to show the selection, may be try Transforms.setSelection or Transforms.select.

Yeah, I am currently trying to do Transforms.setSelection with the value I got from the onBlur event , cant seem to make it work. Setting the editor.selection does seem to work correctly.

I just need to find a way to highlight this and generalize the onBlur Event.

I have found that this is checking the onSelectChange event from the dom to unselect , so my next thing is going to try to disable the unselection there , I will report back on my findings.

So a quick work around, is to not allow the editor to be unselected - You can either run your own logic, or you can just "monkey patch" Transforms.deselect to be a empty function in the begginging of your app, this worked like a charm, and I can seem where this is actually being used on the internals of Slate apart from Focus/UnFocus. So so far this is my go to solution.
Better scenario would be to actually change Editable to not call deselect via a prop or something, and you woulc manually call deselect ( in case for multiple editors on the same page)

You can get around this issue by setting readOnly to true before opening the link input or focusing outside the editor, and then setting back to false when done.

However, this PR will need to be merged, since readOnly is broken right now.

https://github.com/ianstormtaylor/slate/pull/3388/files

I also had success with monkey patching Transforms.deselect while my dialog is open, but it feels wrong :-)

I wrote up a simple HOC using what @Lalitj03 posted if anyone's interested:
https://gist.github.com/heyitsaamir/6089165fe6789eee170b46809cb61fc6

It took me hours debugging before I read this issue and found out setting editor.selection works but not calling editor.setSelection... The API name is confusing, yet the doc does not seem to mention this.

I am able to get around this issue by overriding Transforms.deselect with Transforms.deselect = () => {};
Similar to what I assume @Morphexe & @sunesimonsen are doing. However, is there any downside to doing this?

@DianaLease I ran into some issues where the editor could then have it's selection state become out of sync with the content which will end up throwing some hard to debug exceptions

Here is my solution to this problem:

  1. Put command buttons (like "link" on the gif in the first post) out of the <Slate> tag. This is required, so onBlur event will be fired before your button is clicked.
  2. Add "onBlur" handler to the <Editable> tag. In that handler save selection to some property on the editor. E.g. blurSelection.
  3. Slate sets selection to null after blur, so before executing a command on the editor, you need to set selection to the saved one. Do it with Transforms.select(editor, editor.blurSelection); (blurSelection is the name of a variable from step2.
  4. Run your regular command as if a selection was there. Everything will work exactly the same.
  5. Bonus: now, since we have selection we can use ReactEditor.focus(editor) to return focus, so users can just continue typing.

Works well for all the basic commands my editor has: lists, numbers, formatting, headers, etc.

The solution of @bearz works, but it still removes the selection visually, which is problematic for some use cases.

I want to change the font size for the selection via typing the font size into an input field, and for the user it is disturbing that the selection is no longer there visually, only "under the hood".

still removes the selection visually

But that was also the case in the previous version?
It's not easy to keep the selection visually because it's the same document. You could have a custom "Selection" plugin which draws a background behind the selection to have a visual effect maybe?

Sorry, I did not mean that it worked differently in the prev pervious, I just meant that it is still a problem for some use cases.
But I can understand that it's not easy, because that is how the browser works.

I think the safest solution is what you suggested, and draw a background explicitly when the focus is not there.

Step 5 returns focus for me. I can continue to type exactly from the same place. Make sure that your command doesn't modify selection under the hood. If you split nodes or change blocks that might be a case.

You can make it work even when splitting nodes, here is a basic example that works for me:

saveSelection = () => {
  this.editor.savedSelection = this.editor.selection;
};

render () {
  return (
    <Slate editor={this.editor} value={this.state.value} onChange={this.handleChange}>
      <FontColorPicker editor={this.editor} defaultValue='rgba(0, 0, 0, 1)'/>
      <Editable renderLeaf={this.renderLeaf} onBlur={this.saveSelection}/>
    </Slate>
  );
}

In FontColorPicker:

handleChange = (color) => {
  if (this.props.editor.savedSelection) {
    Transforms.select(this.props.editor, this.props.editor.savedSelection);
  }

  Editor.addMark(this.props.editor, 'color', color);

  const sel = this.props.editor.selection;

  Transforms.deselect(this.props.editor);

  setTimeout(() => {
    Transforms.select(this.props.editor, sel);
    ReactEditor.focus(this.props.editor);
  }, 10);
};

For some reason just running ReactEditor.focus(this.props.editor); after Editor.addMark was putting the cursor back to the start even with a timeout, but deselect + select works.

Consider setting CSS for toolbars buttons outside the slate editing area:user-select: none;
<button onClick={handleClick} style={{userSelect:'none'}}>test</button>
Reference MDN: https://developer.mozilla.org/zh-CN/docs/Web/CSS/user-select
After setting, the selection will not be lost

@vsakos do you know why the cursor is put back to the start? I'm seeing this issue as well where inserting a new inline void element causes node splitting. I have to set my timeout pretty high (450ms) to make it work.

I am getting the opposite problem, selection is not null when editor has lost focus.

I needed to sort this out to make a Slate editor look and act like a textarea, which has a border div that can be clicked to focus the editor and needs to remember the selection. It's basically the same approach that @bearz describes. Also important is the // @refresh reset comment, which prevents crashing on React Fast Refresh in newer versions of React.

import clsx from 'clsx';
import React from 'react';
import { createEditor, Editor, Node, Transforms } from 'slate';
import { withHistory } from 'slate-history';
import { Editable, ReactEditor, Slate, withReact } from 'slate-react';
import './EditorTextArea.scss';

export interface EditorTextAreaProps {
  value: Node[];
  onChange: (value: Node[]) => void;
}

// @refresh reset
const EditorTextArea: React.FC<EditorTextAreaProps> = ({ value, onChange }) => {
  const editor = React.useMemo(
    () => withHistory(withReact(createEditor())),
    []
  );

  const [focused, setFocused] = React.useState(false);
  const savedSelection = React.useRef(editor.selection);
  const onFocus = React.useCallback(() => {
    setFocused(true);
    if (!editor.selection) {
      Transforms.select(
        editor,
        savedSelection.current ?? Editor.end(editor, [])
      );
    }
  }, [editor]);
  const onBlur = React.useCallback(() => {
    setFocused(false);
    savedSelection.current = editor.selection;
  }, [editor]);

  const divRef = React.useRef<HTMLDivElement>(null);
  const focusEditor = React.useCallback(
    (e: React.MouseEvent) => {
      if (e.target === divRef.current) {
        ReactEditor.focus(editor);
        e.preventDefault();
      }
    },
    [editor]
  );

  return (
    <div
      ref={divRef}
      className={clsx('editor-textarea', { focused })}
      onMouseDown={focusEditor}>
      <Slate editor={editor} value={value} onChange={onChange}>
        <Editable onFocus={onFocus} onBlur={onBlur} />
      </Slate>
    </div>
  );
};

export default EditorTextArea;

Hey guys, what if you use onMouseDown instead of onClick? This doesn't reset focus for me.

Here is my solution to this problem:

  1. Put command buttons (like "link" on the gif in the first post) out of the <Slate> tag. This is required, so onBlur event will be fired before your button is clicked.
  2. Add "onBlur" handler to the <Editable> tag. In that handler save selection to some property on the editor. E.g. blurSelection.
  3. Slate sets selection to null after blur, so before executing a command on the editor, you need to set selection to the saved one. Do it with Transforms.select(editor, editor.blurSelection); (blurSelection is the name of a variable from step2.
  4. Run your regular command as if a selection was there. Everything will work exactly the same.
  5. Bonus: now, since we have selection we can use ReactEditor.focus(editor) to return focus, so users can just continue typing.

Works well for all the basic commands my editor has: lists, numbers, formatting, headers, etc.

Works like charm. Thanks for sharing!

Hey guys, what if you use onMouseDown instead of onClick? This doesn't reset focus for me.

onMouseDown doesn't lose highlight but lose focus. I am not sure why.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ezakto picture ezakto  Â·  3Comments

Slapbox picture Slapbox  Â·  3Comments

YurkaninRyan picture YurkaninRyan  Â·  3Comments

ianstormtaylor picture ianstormtaylor  Â·  3Comments

chriserickson picture chriserickson  Â·  3Comments