Natural keyboard movement between lists

Applying principles of physicality to pursue a more natural drag and drop experience

Pursing natural movement between lists

Recently at Atlassian we released react-beautiful-dnd: a library that aims to provide a beautiful, natural and accessible drag and drop experience for the web. It allows users to control the entire drag and drop experience using only a keyboard. I have previously written an introductory blog about the library and the thinking behind it.

We have just shipped the the ability to move items between lists. This feature has full keyboard support. In this blog I will go through how we applied principles of physicality to deliver a natural cross list movement experience using only a keyboard.

Index based movement

A simple approach to moving between lists is a system based on the index of the item and its list. To move an item from one list to another we:

  1. Find the list with the next index to the current list
  2. Insert the dragging item into the same index in the destination list
Simple index based movement

This approach works reasonably well if the items are all the same size and the lists have the same start edge on the vertical axis. However, if both of these properties are not true then the index based approach feels strange

Movement feels strange when items are different sizes
We probably would have expected to move to list 2 and not list 1. It is also strange that we are moving above item 6 inside of list 2 rather than below it.

In search of analogies

In the process of building a more natural movement system, a question we kept asking ourselves was:

‘if we were to push these objects together where would they end up?’

When considering this question, we wanted to keep in mind that objects prefer to stay and rest unless acted on by an external force (Newton’s first law — inertia). Objects are lazy and generally want to take the path of least resistance.

Thinking (really) big

Another principle we wanted to observe was gravity. We treat lists as if they were small solar systems that have there own gravitational pull. Within solar systems (a list) we have planets (items). In order to respect this analogy, lists have the primary pull over the destination of moving item, and once the destination list is calculated then the placement of the items is considered.

And so, we wanted the guiding analogies for cross list movement to be inertia, gravity and collisions.

A sample of the white boarding we did coming up with this system

Breaking down natural movement between lists

In this section I will detail how we calculate natural item movement for an item, step by step. None of the examples will refer to the lists by their index — because their index is not relevant 🎉

Phase 1: find the best list

In this phase our goal is to find the most natural list to move to

Our starting list configuration

1: Filter out anything that is not on the side we want to move to

We are filtering out the list on the left hand side of the current list as we are attempting to move right

2: Filter out lists that are outside of the height of the current list

Removing the list that starts after the source list ends

3: Choose the list that is closest on the horizontal plane

In this case the two lists in the middle have the same distance on the horizontal plane from the moving item.

In the event of a tie:

1 If the centre position of the moving item is contained within the height of one list: then that is the closest list.

In this case the centre point of the dragging item intersects with a list on the horizontal plane.

We cannot merely measure the distance to each of the corners at this point as the closest corner might be a shared edge between two lists.

2 If the centre position of the dragging item is not contained within the height of a list — choose the list with the closest corner.

The top list has the closest corner to the moving item

Side point: Why not compare the distance between the centre positions of the lists and choose the closest?

We originally tried this approach — but it turns out the centre position of a list is not a good measurement of list closeness. For example, the centre position of a big list might be a long way away — even though the list itself it quite close.

Top list is punished for having a big height when using this alternative

Phase 2: find the best spot in the list

Now that we have our destination list, we need to find the best spot in that list for the moving item to move into. Here are the steps we go through to find the best spot:

1: Measure the distance to the centre point of each of the items and choose the one that is closest (choosing the path of least resistance)

Measuring the distance to the centre of each of the items

When measuring the distance between two points on a two dimensional plane we use some coordinate geometry based on Pythagoras theorem. Here is a video where you can learn about measuring the distance between two points if you are interested.

2: In order to decide whether to go above or below the target we pretend the objects have a pointed edge and collide them together. If we do this, which object would be on top?

Sharpen the corner of the items and push them together

A simple way of calculating which item should be on top is to determine whether the source centre position is higher or lower than the target centre position

To calculate this collision we check which centre point is higher

3: If there is a tie, such as when the items are the same height, the item that is moving goes on top. This is designed to give precedence and importance to the item that you are interacting with.

* In the event we are moving to an empty list — we move to the top of the list

Moving to the top of an empty list

An alternative approach

An alternative to the above method is to skip most of the ‘find the best list stage’ and simply find the destination by measuring the distance to all items. However, this approach can end up in the situation where underpopulated lists are ‘skipped’ due to their lack of children. This technique gives preference to items rather than lists. This goes against the gravity metaphors we were going for with lists having the primary pull over items.

You can try work around the shortcomings of this approach by inserting fake points at the start and end of lists — but we did not pursue that idea very far.

List is skipped for not having any children

Putting it all together

Here is what using these rules looks like in practice. You are welcome to try them out for yourself on our examples page

The rules in action

Parting words

We are really excited to see these ideas of physicality come to life in react-beautiful-dnd. I would encourage you to check out the repository and to have a play with the examples to see how the movement feels for yourself. If you have suggestions on how we could improve these algorithms please create and issue. If you are interested in what these rules look like in code then you can have a look at it here.

A special thanks to Jared Crowe for his efforts in making this a reality, as well as my other talented colleagues at Atlassian.

Cheers