Natural keyboard movement between lists
Applying principles of physicality to pursue a more natural drag and drop experience
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:
- Find the list with the next index to the current list
- Insert the dragging item into the same index in the destination list
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
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.
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
1: Filter out anything that is not on the side we want to move to
2: Filter out lists that are outside of the height of the current list
3: Choose the list that is closest on the horizontal plane
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.
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.
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.
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)
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?
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
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
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.
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
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