Slate: Issue related to native `onBlur` event handling

Created on 29 May 2017  路  6Comments  路  Source: ianstormtaylor/slate

Let me start introducing a notation. By _edit island_ I mean a DOM subtree with a structure similar to

<div contentEditable={false}>
  <div>Not Editable Content</div>
  <EditableElement />
</div>

where EditableElement is an element which contains an editable text. It can be a native element, like inputor textarea, a javascript based editor different from Slate or a collection of Slate nodes.

The code responsible to handle browser onBlur and onFocus events has been rewritten a lot of times.
Every time, one issue is fixed and two or more are introduced. :wink:

In the past, I made two changes to onBlur and onFocus (PR #693 and #692) to fix keyboard navigation issues in presence of edit islands with editable elements consisting of Slate block collections.
After commit 619870808699ed853ca153419e616b764c639762 an issue (#749) concerning embedded input has been opened, the handler code has been modified to fix that bug, but this has raised again the problems I managed to solve with my PRs.

Here, I'll try to show what is the root of these issues.

There are three main different (from the Slate point of view) cases in which browsers fire blur events.

  1. The focus switches from within the Slate Editor to an element not contained into the editor DOM subtree.

  2. The Slate editor DOM subtree contains an edit island and the cursor goes from an element within the editor subtree but outside the edit island
    to an element inside the edit island (or vice versa).

  3. The document visibilityState changes to hidden.

The case gives us headaches is the second.

The task of the handlers is to decide if the focus state of the Slate editor must change or not in response to blur and focus events fired by the browser.

All the versions of the onBlur event handling code take this decision based on the relative inclusion relations among the element losing focus, the one that gains focus and the editor container.
This is a reasonable choice but it can't always give the right answer: there are cases in which the relations are the same but the editor must have different behaviors.

For example, let's consider two Slate editor instances containing custom blocks with edit islands. Let the island editable elements be:

  • an input element in the first case
  • a collection of Slate blocks in the second.

When the caret entering the edit island triggers the onBlur event, the relative inclusion relations among the elements losing and gaining the focus and the editor container are the same but in the first case the editor must be blurred because it is no longer responsible of the editing process while in the second case it has to retain its focus state since it is still Slate that must take care of the editing.

So, what really matters is to establish if the element which gains focus is still managed by Slate or not. But this is impossible to do at level of Content component, simply, because it does not have this info. Only the author of a custom node knows for sure if it uses an external tool or not to handle editing.

I think the best we can do is to adopt a clear policy.

My propose is this. When the editor main component (Content) loses focus in favour of an element belonging to its subtree, Slate should ignore the onBlur event and leave the author of the custom node to put the editor in read-only mode or change its focus state as needed to avoid unwanted interferences.

Of course, authors must be informed and to this aim we have to add a clear statement about this policy in the doc section dedicated to custom nodes.

Moreover, according to this policy, we have to add to the input element of the video component in the _Embeds_ example an onFocus handler like this:

onFocus = (e) => {
  const { state, editor } = this.props
  const next = state
    .transform()
    .blur()
    .apply()
  e.stopPropagation()
  editor.onChange(next)
}

If you agree, I can make a PR implementing these changes. :blush:

bug

All 6 comments

Hey @AlbertHilb, very well framed issue, thanks for taking the time to write that up.

I'm trying to wrap my head around what specifically makes guessing the proper handling of the onBlur event impossible. From your two examples cases, I see:

  • an input element in the first case
  • a collection of Slate blocks in the second.

Can you elaborate on these? Potentially even just drawing out the DOM for both cases?

I'm confused because in the <input> case we can surely detect that one. In the collection of Slate blocks case, is that inside a second, separate Slate editor in a isVoid: true element of the first? Would we not be able to check against document.id or similar to determine how the blur should be handled? I think I'm missing something.

Thanks!

@ianstormtaylor

In the collection of Slate blocks case, is that inside a second, separate Slate editor in a isVoid: true
element of the first?

No, the collection of blocks is inside a container block with isVoid: false. Something like

{
  "kind": "block",
  "type": "environment",
  "data": {
    "envName": "definition"
  },
  nodes: [
    {
      "kind": "block",
      "type": "default",
      "nodes": [
        {
          "kind": "text",
          "text": "First paragraph of a math object definition."
        }
      ]
    },
    {
      "kind": "block",
      "type": "default",
      "nodes": [
        {
          "kind": "text",
          "text": "Second paragraph of a math object definition."
        }
      ]
    }
  ]
}

The container block is rendered essentially as

<section contentEditable={false}>
  <header>Definition.</header>
  <div contentEditable={true}  suppressContentEditableWarning>
    {props.children}
  </div>
</section>

So the content of the container can be edited while the header remains read-only.
Here is a live sample.

in the case we can surely detect that one

Yes, you are right. When the editable element of the island is an input or a textarea, it is easy to detect from the focus target _nature_ that the editor must be blurred.
But in the general case is less simple.
How can we distinguish, for example, the case in which the editable element is a div with contentEditable={true} containing a Slate block collection like the one above, from the case in which the editable element is still a div with contentEditable={true} but it is managed by an external tool (CodeMirror for example).
Yet distinguishing this two cases is essential because when the caret enters the editable div Slate should retain the focus in the former case whereas it should be blurred in the latter.

Of course, we can try to guess or ask authors to give us an hint, for example adding an attribute to editable element inside the island. But I think it is safer to ask authors to put the Slate editor in the right focus state by themselves when needed. Isn't for this blur and focus transforms exist? :blush:

Hmm gotcha! I think using an attribute might actually end up less confusing, because for managing focus/blur it sounds like people will need to understand a lot more about how the DOM events interact. Which isn't ideal I think.

But that is true that neither solution is great. I'd be open to an attribute I think.

@AlbertHilb Thanks for directing me here from #891. Is there any plan for a PR for this? I really love the checkbox example and would love to emulate it, but finding a way to get it working in Chrome is beyond my abilities.

@ianstormtaylor Since block focus isn't working when we use an input, do you recommend using the component state ? We detect input click and input click outside to change the state.

Thank you !

I believe that this may be fixed by https://github.com/ianstormtaylor/slate/pull/3093, which has changed a lot of the logic in Slate and slate-react especially. I'm going to close this out, but as always, feel free to open a new issue if it persists for you. Thanks for understanding.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

chrpeter picture chrpeter  路  3Comments

ianstormtaylor picture ianstormtaylor  路  3Comments

bunterWolf picture bunterWolf  路  3Comments

markolofsen picture markolofsen  路  3Comments

chriserickson picture chriserickson  路  3Comments