Can't focus on ios

My problem: Neither focus() on my editor view instance nor editorViewInstance.dom.focus() will trigger the keyboard to show on ios.

My scenario: I mount the editor after a user clicks on a static element on the page, it isn’t in the DOM when the page loads initially.

My attempts to fix it:

  • Add autofocus to the contenteditable DOM element before calling focus on it or the view instance.
  • Call click() on the DOM element.
  • Trigger touchstart on the DOM element before attempting to focus it.

Does anyone know how to fix this?

Not sure if this will work, but trying setting a valid selection (via https://prosemirror.net/docs/ref/#state.EditorState^create^config.selection) and focusing on the editor.

Unfortunately, this didn’t work. You did, however, help me clean up some code where I was setting the selection after the editor was mounted. This is much cleaner, so thank you!

My scenario: I mount the editor after a user clicks on a static element on the page, it isn’t in the DOM when the page loads initially.

Ah I re-read this more carefully.

You need to figure a way to focus the editor after it has been mounted onto the dom. This is because you can’t focus on an element that’s not currently in the dom.

The way I do it with react is via

function EditorContent({editor}: {editor: Editor}) {
  return (
    <div className="editor-content" 
      ref={ref => (ref && editor && editor.view) && { ref.appendChild(editor.view.dom); editor.view.dom.focus() } }
    />
  )
};

Thanks for that info. I create an EditorView in a useEffect hook, then call view.focus() on it. The view is appended in the constructor. Just to be sure, I tried appending it myself like you do, but it didn’t make a difference. This only happens on ios, its weird.

Try doing the focus after a timeout of 0. I usually do focus like this because otherwise I’ve encountered issues like you describe on many platforms.

Thank you both for your responses @bhl and @dminkovsky.

I created this test app as a way to make sure that nothing else in my app was breaking it.

The code for the application is located here.

If you access that in an iPhone (using Safari) or an Android device (using Chrome), you will see that the editor receives focus, but the keyboard doesn’t become shown.

I also tried adding autofocus to the dom element before appending it. That caused the same outcome, although I no longer had to call focus() for the editor to have focus.

Yeah, focus is “true” but the keyboard does not open until I tap. Have you tried just view.focus() directly on view, not dom?. That is what I do and it works fine… https://prosemirror.net/docs/ref/#view.EditorView.focus

useImperativeHandle(ref, () => ({
    focus: () => proseMirror.current.view.focus(),
}));

I have that handle in my component that has the ProseMirror view, and then I call that focus function with a timeout of 0 to make sure the focus works fine on mobile.

Also, not sure about appending the view in the ref handler. Maybe use useEffect for this? Something like: https://github.com/dminkovsky/use-prosemirror/blob/93edf8ae5323e9cbffa03e793b494a28046d490a/src/ProseMirror.tsx?

I have tried all of those things. My actual production code looks like this:

const editorNodeRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
  const view = new EditorView(editorNodeRef.current, {
    state: editorStateRef.current,
  });
  editorViewRef.current = view;

  view.focus();

  return () => {
    view.destroy();
  };
}, []);

return <div ref={editorNodeRef}  />;

All of the things you saw in my test app were direct tests of what bhl recommended. Those didn’t work any better than my production implementation does, unfortunately.

I do have a question though. I don’t see anywhere in your useProsemirror hook that you are focusing on mount like I am trying to do. Am I missing it? Do you have an app which does this that I could test on my iPhone?

Thanks for the help. I feel like I must not have been the only person to face this issue…so there must be a solution.

I just added these lines to my effect, just trying to get anything to work:

view.focus();
$(view.dom).trigger('touchstart');
$(view.dom).trigger('click');
view.dom.focus();
view.focus();

No difference :-/

Oh hey, I’m sorry I totally blanked on this: this is a well-known limitation or user-protection thing (depending on how you look at it) in iOS Safari: it is impossible to focus an element on a stack that did not originate from user interaction!

The reason I blanked on this is because I’ve been developing on iOS using Capacitor, which does not have this limitation because instead of iOS Safari it uses a WKWebView, which lets you enable non-user-interaction-originated focus.

So yeah, this behavior is totally “broken” on iOS Safari.

See, for example, ios - Programmatically focus on a form in a webview (WKWebView) - Stack Overflow.

And of course there are hacks like:

Tip #3: Though programmatically setting focus on an input box won’t cause the keyboard to open, you can set focus on another input while handling the touchDown event; the soft keyboard still opens. Pairing this with #2 means that you can position a fake text input box near the bottom of the page, and when that’s tapped instead set focus to the real input box which is higher on the page (even if it’s offscreen, e.g. placed at top=0 left=99999px) … then, use setTimeout to run a function in a few milliseconds which moves that real input box down lower on the page to where you really wanted it.

But I’ve never tried anything like this. To be honest I’m not even sure I understand it :woozy_face:.

Hmmm, I might be able to use a styled textarea as my click target…then focus the prosemirror editor and remove the textarea…it is worth a shot anyway!

Thank you for your input, I really appreciate it.

Well, I figured it probably wouldn’t let me trick iOS like that, and I was right. Bummer! I guess we’ll have to figure out some UI for dealing with this. Thank you again.

Just to close out this thread, I finally figured it out. I needed to use pointer events rather than onClick. I ended up using onPointerDown and pepjs to polyfill.

I have a few different editors in my app. One of them works great just by rendering it and then calling view.focus(). Two of them require me to do this (I have no idea why non-iOS is different in this situation, but it works):

onPointerDown={
  isIOS
    ? () => setShowEditor(true)
    : () => setTimeout(() => setShowEditor(true), 0)
}

Ironically, if I use the setTimeout on iOS, it breaks. If I don’t use it on desktop browsers, it breaks.

I used the react-device-detect library to achieve this.

Upon render, view.focus() is called in all cases. Everything is working great. Thanks everyone for the help!

2 Likes

Hi @brandoncc, thanks for the update. I’m sorry, I’m confused: are you saying this method got the keyboard to appear without user interaction?

The user clicks on the “container” element where I insert the editor. Other than that, no interaction is required!

@brandoncc Thank you. So you click on a container that looks like the editor,and if for some reason you handle the click with onPointerDown—and the other details you write above—after you’ve inserted the editor, view.focus() will focus the editor and open the keyboard?

Did you try touch events, by the way? Last time I checked (I believe it was iOS 13), iOS didn’t support PointerEvents but did support TouchEvents. Guessing maybe that polyfill translates pointer events to touch events.

Okay, I think I know why this magically worked, even though everything said it shouldn’t. OnPointerDown is causing my overlay element to be removed and the editor to be rendered in its place. By the time the user lifts their finger, the element under their finger is the editor, so they are triggering a touchend on that element.