Recently, we looked into why keydown is the only keyboard event we need when building web apps. Today, let’s look at an issue that manifests itself to users by looking like a ghost in the machine: cursor jumping.
Maybe you’ve seen the issue: A user is typing along – clickety clack clickety clack – and then BAM! The cursor jumps to the the front, the end, or somewhere betwixt. Not only is it confusing, it’s maddening!
It looks like this:
To interact with this issue in real-time check out this jsfiddle:
In this post, we’re going to look at what cursor jumping is, why it happens, and some strategies for avoiding it altogether.
What’s going on with cursor jumping?
Cursor jumping is simply when the cursor jumps from one location to another, unexpectedly, while the user is typing into a text field. It may be an single line text field (via a <input type=text />
), a multi line text field (via a <textarea></textarea>
), or a WYSIWYG text area (via a <div contenteditable=true ></div>
). It usually happens when the app is programmatically setting/changing the value of the text field in a keydown
, keypress
, or keyup
event.
Let’s take a brief look at what happens when the value of a text field is set.
Programmatically changing the text field value resets its cursor position
When a user types in a text field the cursor position moves right along with their expectations, one character at a time. Contrarily, when code programmatically changes the value of a text field the browser resets the cursor position. In fact, in some browsers, ahem Safari , the cursor position is reset even when the value is unchanged.
Fortunately, browsers are consistent with where cursor positions go when they are reset:
- Single line text fields (via
<input type=text />
resets the cursor to the end of the text field. - Multi line text fields (via
<textarea></textarea>
resets the cursor to the end of the text field. - WYSIWYG text fields (via
<div contenteditable=true></div>
: resets the cursor to the beginning of the text field.
Concrete example
Let’s say that we were working on an application that masks vowels. Any time a vowel is typed we want to replace with an asterisk. We could quickly and easy tackle this behavior in a keyup
event handler. It’s not robust, but it’s enough to get the job done:
As you type notice how vowels are replaced with asterisks. After you’ve typed in a few words move the cursor back a few characters (with your arrow keys or with your mouse), and begin typing some more. You will see your cursor reset its position.
Go ahead, give it a try. The below text field awaits your clickety clacks:
This example is a trivial one and the code is a mere five lines. Hardly enough complexity to shake a stick at, right?
I’m going to channel my inner Dwight Schrute for a moment: False.
The complexity is a different variety than what normally comes to mind when thinking about complexity in an application. It’s not a giant pot of spaghetti code or a monolithic god object responsible for an infinite number of things. The complexity we’re seeing lives at the intersection of keyboard events, browser behavior, and user expectations.
Oh, and I almost forgot: It can be a giant PITA to debug.
The throes of debugging cursors that jump
Put yourself in the mindset of an end user. You’re typing away in a text field. Clickety clacketiny away in the zone typing up something really good. You are heads down for about 30 seconds before you look up and notice that the screen is showing you what you typed, but it’s been chopped up and re-arranged. Hrm. That seems strange. Slowly, you type in another couple of characters. After each stroke you watch keenly to see just where your cursor is. Huh? They’re not in the place you’d expect.
Using the arrow keys you move the cursor to where you expect it to go. You cautiously type in a few more characters. Things appear to be in ship shape. You sigh a sigh of relief and prepare to go heads down again. Let’s get back to this masterpiece! A moment later, you’re back in the zone.
A few minutes pass and you look up again. The rearranged chunks are back again. What the heck?!
You rinse and repeat. It seems random. Maybe you hit your trackpad with your palm while typing. Yes, that must have been it. It happened again?! Maybe you nudged that piece of paper on your desk and that nudged your mouse and it moved your cursor. It happened again?!
Minutes become hours. Hours become days. Days become weeks. It seems to happen with no rhyme or reason. I am clearly going insane. Are you going insane? What is going on?!
Luckily, you’re not going insane. You decide to report the issue.
Wait.
How do you report/describe the issue you’re seeing?
You try your best but fumble thru articulating the issue because you’re not quite sure what exactly is going on. It’s random chaos! A ghost in the machine! But you try your best:
I’m typing away and every now and then I look up and half of what I typed is in the wrong spot.
Or, perhaps:
The cursor randomly goes to the beginning. It doesn’t happen all of the time.
Or, maybe it’s:
It doesn’t seem to do it all the time or in the same text field but intermittently its like there’s a ghost in the machine it moves the cursor. Sometimes to the beginning and other times to the end.
Or, it could be something else entirely. The point is that it can be hard for users to articulate this kind of issue. Some may be do it really well while others may take a shot in the dark.
Okay, you’re no the longer the user. Put yourself back in the mindset of you, The Mighty Developer, or you, The Mighty Support Engineer, or you, The Mighty QA.
You pull up the same browser on the same OS and type in the exact same thing the user typed in, but you don’t see any issues. You do this a few times and all seems well. The app is working great! You chalk it up to a random fluke in the browser. Maybe that last release fixed it. It did go out last night.
The user is satisfied.
Two weeks later they file another support request. They’re frustrated.
This can go on for days, weeks, months even. We saw a product once where this jumpiness plagued a simple WYSIWYG text field for practically a year! It was seemingly so random, but it was also super frustrating. And some users weren’t in the system every day so the feedback loop could be a few days to a few weeks on the user’s end.
Even though the individual bits are simple it becomes really difficult to track down and talk about because the issue only manifests itself when many things happen. It can e difficult to tie together what the user describes, with what you’re seeing when you try to reproduce, with how browsers handle cursor positioning, and how keyboard events come into play, …
Let’s just say this: That’s a lot of information to coordinate to make sense of the issue and nobody is going to hold it against you for stumbling your way through debugging this kind of issue. But, if you do it quickly (and this post is hoping to set you up for doing that) then you’ll surprise the hell of out them, in a good way of course.
Do not massage text while a user is typing
A good rule of thumb is to not massage data while the user is typing (aka in keyboard events). With enough effort you can get this to work, but you would need to keep track of the cursor position and making sure you move the cursor to where the user expects it to go.
For single line (via <input type=text />
and multi line text fields (via <textarea></textarea>
this isn’t all that complicated, but for WYSIWYG text fields (via <div contenteditable=true></div>
) it’s complicated and error prone.
In all likelihood, you’ll run into other problems too. For example, if the text is massaged as the user types within a keyboard event then you’ll have an issue when the user pastes something into the text field. So now, you may need to make sure that paste
events also massage the text. It’s not terribly difficult, but another thing to think about and manage.
There’s another use-case that can make this ghost in the machine seem even more ghosty too: asynchronous saves.
Asynchronous saves and data refreshes to the server
Modern JavaScript frameworks provide support data bindings. This makes our lives as developers awesome, but sometimes it causes problems especially when those bindings are combined with things like save-as-you-type functionality.
Let’s walk down that train of thought: the app has a text field that auto-saves to the server when the user pauses for a second (and it is automatically wired up thru your frameworks data binding support).
When the user pauses the app sends an HTTP PUT request to save what the user currently has typed. That causes the backend to send back an HTTP response which includes the most recent saved text. This text then makes its way thru the automatic data bindings back out the text field. The user sees the latest text. Most of the time, this is not a problem, but…
There are two potential problems here:
- Based on how the app works and how the binding is wired up, the saved data from the server could overwrite any new unsaved characters that the person has typed.
- If the backend changed the text in any way – say it removed trailing new lines – then the text field will reset the cursor position.
The first problem above is outside the scope of this post, but it’s easily solved by having the client-side model be the authority on the text value in the browser after it has been initially loaded. More this below in this post.
The second problem brings a new challenge as this will be more difficult for the user to notice any pattern to when the cursor jumping occurs. This is lower risk for single line or multi line text fields when they use <input type=text />
or <textarea></textarea>
tags, but it’s an easy problem to hit when using the WYSIWYG variety of content-editable DIVs.
Why?
Let’s say that the application supports limited rich text functionality for bolding and italicizing text. This feels like overkill to pull in a more fully-featured WYSIWYG editor like TinyMCE. So, you spend an hour and make your own just-rich-enough-text component using a content-editable DIV and two buttons: one for bold and one for italics.
Content-editable DIVs store the raw HTML and up until now you could ignore the HTML and rely on just the innerText
or textContent
(depending on which one suited your needs). But now, you need some of that HTML markup because there’s where the information about bold and italicized text lives.
Content-editable DIVs are not picky about what HTML markup they store behind the scenes. A user can unknowingly copy and paste rich text and paste in as HTML. It could come from a Word doc, an email, etc. But it’s going to show up in the innerHTML
and be saved to the backend.
To solve the problem of unwanted HTML markup and other kinds of HTML/JS injection attacks it’s common to sanitize the markup. You either need to do it on the frontend and the backend or just the backend. If you just do it on the frontend then you leave your backend open for someone to malicious send in unsafe markup and that leaves you open to security holes.
Being the prudent developer that you are you go ahead and tighten down the backend to sanitize incoming markup for this particular field to only allow the bare minimum of tags needed (e.g. <br>
, <p>
, <strong>
, and <em>
. Now, the frontend saves the innerHTML
, the backend cleans it up and saves only the safe markup you want to support, then responds to the frontend client with sanitized result.
This cursor still jumps if the data binding automatically propagates the sanitized HTML value out to the WYSIWYG text field. The difference with this particular version of the cursor jumping is that it will appear really random to the user based on the sanitization rules and how/when they compose their text, when they pause, etc.
So what’s a developer to do?
Unfortunately, it kind of depends, but here are a few ideas.
Make reformatting or massaging the text a separate step
Separate out massaging the text as a separate step. If the app needs to massage, format, or cleanup text/html content do it when the user is clearly not typing.
This may result in waiting for the text field to lose focus, introducing a button that the user actually clicks to clean things up, or both working together.
It may make sense for reformatting to happen when the user presses the Save
button.
The main win here is that everybody – user, browser/app, developer – is on the same page as when something should happen.
Let the frontend code be the authority on what’s displayed
This approach is all about the front-end being active in its role of authority for what’s being displayed. An old-fashioned letter may explain it best:
Dear frontend code,
Once you initially load all the things you need: you are in charge of the display data. You don’t need to blindly take updates from the server until you’re good and ready to, or more importantly: until the user is good and ready to receive them.
Sincerely,
Yours truly
This can mindset (and convention) can be helpful to avoid the issues where features like autosaving result in users seeing their text change out from underneath them. Or, when WebSockets are in use and the backend tells the frontend that a value has changed, but it happens to be in the same field that the user is typing in. This too results in a rictus of shock and horror.
One last comment on live updating even though it’s a bit outside the scope of this post: Live updating can introduce a potential conflict in data that can be surprising to the user. Based on how users interact with an application you may or may not want to consider different ways for handling conflicts in data management. That’s all I’ll say for now.
If you’re interested in hearing about ways to manage conflicts or would like help in this arena let us know. We’re happy to help.
Let’s move onto the next approach.
Maybe don’t live update
Another option is to not live update at all. WebSockets are great and all, but sometimes teams can be too optimistic with the benefits and not take into consideration the cost, and there is most certainly a cost. They introduce complexity into the communication between users and the application, and sometimes that communication can happen when the user least expects it. Use live-updates where they’re helpful to the user.
Here are a few questions that may help to consider when live updating:
If the user is actively using this part of the page and an update comes in – what happens?
Are we surprising the user in some way?
Are we overwriting what they’re working on?
Is their screen going to jump up or down or flash? Will they know the reason why?
Or, consider putting the user in control of live updates with notifications
Another approach which is a combination of the last two is to utilize notifications. The back-end sends out live updates, but those updates are just notifications that can be used to let the user know there is new data or information available. It’s then up to the user to choose to take action that results in refreshing what they see on the page.
The benefit of this is that it puts the user in control of when to update things and it minimizes the chance that the application is going to surprise them. The downside is that you need to build in notifications and the ability to deep link what needs to be refreshed (unless you refresh the whole page).
Or, maybe just refresh the the data organically during normal app usage
The last approach is really the same as “Maybe don’t live update” section above. I’m figuratively kicking this metaphorical horse to death – I would never be okay with injurying any kind of animal in real life – but because it’s hard to overstate keeping things simple.
Another question to consider:
Can your app avoid live updates and simply refresh the data when a component or page would naturally be loaded in the app anyways?
If it makes sense for your app to do this then the user will see updated information based on when they navigate within the app. It avoids live updates, notifications, and any other complexity.
Then, when a real pain point comes up you can consider adding what you need becaus the compleixty will be warranted.
Summary
Phew! We made it.
Cursor jumping is a tricky issue and I hope feel well-informed for recognizing this kind of issue, rooting out the underlying causes, and strategies for avoiding the issue when developing richer user experiences.
One thing we didn’t dive into was all of the underlying code for managing cursor positions. If you’re interested in seeing how that works for the various solutions let us know on twitter.
Until the next post: go forth and be helpful!
P.S. Happy coding!
Image credit: Chris Murtagh